Source file src/net/http/cookiejar/jar.go

     1  // Copyright 2012 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar.
     6  package cookiejar
     7  
     8  import (
     9  	"cmp"
    10  	"errors"
    11  	"fmt"
    12  	"net"
    13  	"net/http"
    14  	"net/http/internal/ascii"
    15  	"net/netip"
    16  	"net/url"
    17  	"slices"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  )
    22  
    23  // PublicSuffixList provides the public suffix of a domain. For example:
    24  //   - the public suffix of "example.com" is "com",
    25  //   - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and
    26  //   - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us".
    27  //
    28  // Implementations of PublicSuffixList must be safe for concurrent use by
    29  // multiple goroutines.
    30  //
    31  // An implementation that always returns "" is valid and may be useful for
    32  // testing but it is not secure: it means that the HTTP server for foo.com can
    33  // set a cookie for bar.com.
    34  //
    35  // A public suffix list implementation is in the package
    36  // golang.org/x/net/publicsuffix.
    37  type PublicSuffixList interface {
    38  	// PublicSuffix returns the public suffix of domain.
    39  	//
    40  	// TODO: specify which of the caller and callee is responsible for IP
    41  	// addresses, for leading and trailing dots, for case sensitivity, and
    42  	// for IDN/Punycode.
    43  	PublicSuffix(domain string) string
    44  
    45  	// String returns a description of the source of this public suffix
    46  	// list. The description will typically contain something like a time
    47  	// stamp or version number.
    48  	String() string
    49  }
    50  
    51  // Options are the options for creating a new Jar.
    52  type Options struct {
    53  	// PublicSuffixList is the public suffix list that determines whether
    54  	// an HTTP server can set a cookie for a domain.
    55  	//
    56  	// A nil value is valid and may be useful for testing but it is not
    57  	// secure: it means that the HTTP server for foo.co.uk can set a cookie
    58  	// for bar.co.uk.
    59  	PublicSuffixList PublicSuffixList
    60  }
    61  
    62  // Jar implements the http.CookieJar interface from the net/http package.
    63  type Jar struct {
    64  	psList PublicSuffixList
    65  
    66  	// mu locks the remaining fields.
    67  	mu sync.Mutex
    68  
    69  	// entries is a set of entries, keyed by their eTLD+1 and subkeyed by
    70  	// their name/domain/path.
    71  	entries map[string]map[string]entry
    72  
    73  	// nextSeqNum is the next sequence number assigned to a new cookie
    74  	// created SetCookies.
    75  	nextSeqNum uint64
    76  }
    77  
    78  // New returns a new cookie jar. A nil [*Options] is equivalent to a zero
    79  // Options.
    80  func New(o *Options) (*Jar, error) {
    81  	jar := &Jar{
    82  		entries: make(map[string]map[string]entry),
    83  	}
    84  	if o != nil {
    85  		jar.psList = o.PublicSuffixList
    86  	}
    87  	return jar, nil
    88  }
    89  
    90  // entry is the internal representation of a cookie.
    91  //
    92  // This struct type is not used outside of this package per se, but the exported
    93  // fields are those of RFC 6265.
    94  type entry struct {
    95  	Name       string
    96  	Value      string
    97  	Quoted     bool
    98  	Domain     string
    99  	Path       string
   100  	SameSite   string
   101  	Secure     bool
   102  	HttpOnly   bool
   103  	Persistent bool
   104  	HostOnly   bool
   105  	Expires    time.Time
   106  	Creation   time.Time
   107  	LastAccess time.Time
   108  
   109  	// seqNum is a sequence number so that Cookies returns cookies in a
   110  	// deterministic order, even for cookies that have equal Path length and
   111  	// equal Creation time. This simplifies testing.
   112  	seqNum uint64
   113  }
   114  
   115  // id returns the domain;path;name triple of e as an id.
   116  func (e *entry) id() string {
   117  	return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name)
   118  }
   119  
   120  // shouldSend determines whether e's cookie qualifies to be included in a
   121  // request to host/path. It is the caller's responsibility to check if the
   122  // cookie is expired.
   123  func (e *entry) shouldSend(https bool, host, path string) bool {
   124  	return e.domainMatch(host) && e.pathMatch(path) && e.secureMatch(https)
   125  }
   126  
   127  // domainMatch checks whether e's Domain allows sending e back to host.
   128  // It differs from "domain-match" of RFC 6265 section 5.1.3 because we treat
   129  // a cookie with an IP address in the Domain always as a host cookie.
   130  func (e *entry) domainMatch(host string) bool {
   131  	if e.Domain == host {
   132  		return true
   133  	}
   134  	return !e.HostOnly && hasDotSuffix(host, e.Domain)
   135  }
   136  
   137  // pathMatch implements "path-match" according to RFC 6265 section 5.1.4.
   138  func (e *entry) pathMatch(requestPath string) bool {
   139  	if requestPath == e.Path {
   140  		return true
   141  	}
   142  	if strings.HasPrefix(requestPath, e.Path) {
   143  		if e.Path[len(e.Path)-1] == '/' {
   144  			return true // The "/any/" matches "/any/path" case.
   145  		} else if requestPath[len(e.Path)] == '/' {
   146  			return true // The "/any" matches "/any/path" case.
   147  		}
   148  	}
   149  	return false
   150  }
   151  
   152  // secureMatch checks whether a cookie should be sent based on the protocol
   153  // and the Secure flag. Localhost is considered a secure origin regardless
   154  // of protocol, matching browser behavior.
   155  func (e *entry) secureMatch(https bool) bool {
   156  	if !e.Secure {
   157  		// Cookies not marked secure are always sent.
   158  		return true
   159  	}
   160  	// Everything below is about cookies marked secure.
   161  	if https {
   162  		// HTTPS request matches secure cookies.
   163  		return true
   164  	}
   165  	// Consider localhost to be secure like browsers.
   166  	if isLocalhost(e.Domain) {
   167  		return true
   168  	}
   169  	ip, err := netip.ParseAddr(e.Domain)
   170  	if err == nil && ip.IsLoopback() {
   171  		return true
   172  	}
   173  	return false
   174  }
   175  
   176  func isLocalhost(host string) bool {
   177  	host = strings.TrimSuffix(host, ".")
   178  	if idx := strings.LastIndex(host, "."); idx >= 0 {
   179  		host = host[idx+1:]
   180  	}
   181  	return ascii.EqualFold(host, "localhost")
   182  }
   183  
   184  // hasDotSuffix reports whether s ends in "."+suffix.
   185  func hasDotSuffix(s, suffix string) bool {
   186  	return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
   187  }
   188  
   189  // Cookies implements the Cookies method of the [http.CookieJar] interface.
   190  //
   191  // It returns an empty slice if the URL's scheme is not HTTP or HTTPS.
   192  func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
   193  	return j.cookies(u, time.Now())
   194  }
   195  
   196  // cookies is like Cookies but takes the current time as a parameter.
   197  func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
   198  	if u.Scheme != "http" && u.Scheme != "https" {
   199  		return cookies
   200  	}
   201  	host, err := canonicalHost(u.Host)
   202  	if err != nil {
   203  		return cookies
   204  	}
   205  	key := jarKey(host, j.psList)
   206  
   207  	j.mu.Lock()
   208  	defer j.mu.Unlock()
   209  
   210  	submap := j.entries[key]
   211  	if submap == nil {
   212  		return cookies
   213  	}
   214  
   215  	https := u.Scheme == "https"
   216  	path := u.Path
   217  	if path == "" {
   218  		path = "/"
   219  	}
   220  
   221  	modified := false
   222  	var selected []entry
   223  	for id, e := range submap {
   224  		if e.Persistent && !e.Expires.After(now) {
   225  			delete(submap, id)
   226  			modified = true
   227  			continue
   228  		}
   229  		if !e.shouldSend(https, host, path) {
   230  			continue
   231  		}
   232  		e.LastAccess = now
   233  		submap[id] = e
   234  		selected = append(selected, e)
   235  		modified = true
   236  	}
   237  	if modified {
   238  		if len(submap) == 0 {
   239  			delete(j.entries, key)
   240  		} else {
   241  			j.entries[key] = submap
   242  		}
   243  	}
   244  
   245  	// sort according to RFC 6265 section 5.4 point 2: by longest
   246  	// path and then by earliest creation time.
   247  	slices.SortFunc(selected, func(a, b entry) int {
   248  		if r := cmp.Compare(b.Path, a.Path); r != 0 {
   249  			return r
   250  		}
   251  		if r := a.Creation.Compare(b.Creation); r != 0 {
   252  			return r
   253  		}
   254  		return cmp.Compare(a.seqNum, b.seqNum)
   255  	})
   256  	for _, e := range selected {
   257  		cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value, Quoted: e.Quoted})
   258  	}
   259  
   260  	return cookies
   261  }
   262  
   263  // SetCookies implements the SetCookies method of the [http.CookieJar] interface.
   264  //
   265  // It does nothing if the URL's scheme is not HTTP or HTTPS.
   266  func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
   267  	j.setCookies(u, cookies, time.Now())
   268  }
   269  
   270  // setCookies is like SetCookies but takes the current time as parameter.
   271  func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {
   272  	if len(cookies) == 0 {
   273  		return
   274  	}
   275  	if u.Scheme != "http" && u.Scheme != "https" {
   276  		return
   277  	}
   278  	host, err := canonicalHost(u.Host)
   279  	if err != nil {
   280  		return
   281  	}
   282  	key := jarKey(host, j.psList)
   283  	defPath := defaultPath(u.Path)
   284  
   285  	j.mu.Lock()
   286  	defer j.mu.Unlock()
   287  
   288  	submap := j.entries[key]
   289  
   290  	modified := false
   291  	for _, cookie := range cookies {
   292  		e, remove, err := j.newEntry(cookie, now, defPath, host)
   293  		if err != nil {
   294  			continue
   295  		}
   296  		id := e.id()
   297  		if remove {
   298  			if submap != nil {
   299  				if _, ok := submap[id]; ok {
   300  					delete(submap, id)
   301  					modified = true
   302  				}
   303  			}
   304  			continue
   305  		}
   306  		if submap == nil {
   307  			submap = make(map[string]entry)
   308  		}
   309  
   310  		if old, ok := submap[id]; ok {
   311  			e.Creation = old.Creation
   312  			e.seqNum = old.seqNum
   313  		} else {
   314  			e.Creation = now
   315  			e.seqNum = j.nextSeqNum
   316  			j.nextSeqNum++
   317  		}
   318  		e.LastAccess = now
   319  		submap[id] = e
   320  		modified = true
   321  	}
   322  
   323  	if modified {
   324  		if len(submap) == 0 {
   325  			delete(j.entries, key)
   326  		} else {
   327  			j.entries[key] = submap
   328  		}
   329  	}
   330  }
   331  
   332  // canonicalHost strips port from host if present and returns the canonicalized
   333  // host name.
   334  func canonicalHost(host string) (string, error) {
   335  	var err error
   336  	if hasPort(host) {
   337  		host, _, err = net.SplitHostPort(host)
   338  		if err != nil {
   339  			return "", err
   340  		}
   341  	}
   342  	// Strip trailing dot from fully qualified domain names.
   343  	host = strings.TrimSuffix(host, ".")
   344  	encoded, err := toASCII(host)
   345  	if err != nil {
   346  		return "", err
   347  	}
   348  	// We know this is ascii, no need to check.
   349  	lower, _ := ascii.ToLower(encoded)
   350  	return lower, nil
   351  }
   352  
   353  // hasPort reports whether host contains a port number. host may be a host
   354  // name, an IPv4 or an IPv6 address.
   355  func hasPort(host string) bool {
   356  	colons := strings.Count(host, ":")
   357  	if colons == 0 {
   358  		return false
   359  	}
   360  	if colons == 1 {
   361  		return true
   362  	}
   363  	return host[0] == '[' && strings.Contains(host, "]:")
   364  }
   365  
   366  // jarKey returns the key to use for a jar.
   367  func jarKey(host string, psl PublicSuffixList) string {
   368  	if isIP(host) {
   369  		return host
   370  	}
   371  
   372  	var i int
   373  	if psl == nil {
   374  		i = strings.LastIndex(host, ".")
   375  		if i <= 0 {
   376  			return host
   377  		}
   378  	} else {
   379  		suffix := psl.PublicSuffix(host)
   380  		if suffix == host {
   381  			return host
   382  		}
   383  		i = len(host) - len(suffix)
   384  		if i <= 0 || host[i-1] != '.' {
   385  			// The provided public suffix list psl is broken.
   386  			// Storing cookies under host is a safe stopgap.
   387  			return host
   388  		}
   389  		// Only len(suffix) is used to determine the jar key from
   390  		// here on, so it is okay if psl.PublicSuffix("www.buggy.psl")
   391  		// returns "com" as the jar key is generated from host.
   392  	}
   393  	prevDot := strings.LastIndex(host[:i-1], ".")
   394  	return host[prevDot+1:]
   395  }
   396  
   397  // isIP reports whether host is an IP address.
   398  func isIP(host string) bool {
   399  	if strings.ContainsAny(host, ":%") {
   400  		// Probable IPv6 address.
   401  		// Hostnames can't contain : or %, so this is definitely not a valid host.
   402  		// Treating it as an IP is the more conservative option, and avoids the risk
   403  		// of interpreting ::1%.www.example.com as a subdomain of www.example.com.
   404  		return true
   405  	}
   406  	return net.ParseIP(host) != nil
   407  }
   408  
   409  // defaultPath returns the directory part of a URL's path according to
   410  // RFC 6265 section 5.1.4.
   411  func defaultPath(path string) string {
   412  	if len(path) == 0 || path[0] != '/' {
   413  		return "/" // Path is empty or malformed.
   414  	}
   415  
   416  	i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1.
   417  	if i == 0 {
   418  		return "/" // Path has the form "/abc".
   419  	}
   420  	return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/".
   421  }
   422  
   423  // newEntry creates an entry from an http.Cookie c. now is the current time and
   424  // is compared to c.Expires to determine deletion of c. defPath and host are the
   425  // default-path and the canonical host name of the URL c was received from.
   426  //
   427  // remove records whether the jar should delete this cookie, as it has already
   428  // expired with respect to now. In this case, e may be incomplete, but it will
   429  // be valid to call e.id (which depends on e's Name, Domain and Path).
   430  //
   431  // A malformed c.Domain will result in an error.
   432  func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) {
   433  	e.Name = c.Name
   434  
   435  	if c.Path == "" || c.Path[0] != '/' {
   436  		e.Path = defPath
   437  	} else {
   438  		e.Path = c.Path
   439  	}
   440  
   441  	e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain)
   442  	if err != nil {
   443  		return e, false, err
   444  	}
   445  
   446  	// MaxAge takes precedence over Expires.
   447  	if c.MaxAge < 0 {
   448  		return e, true, nil
   449  	} else if c.MaxAge > 0 {
   450  		e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
   451  		e.Persistent = true
   452  	} else {
   453  		if c.Expires.IsZero() {
   454  			e.Expires = endOfTime
   455  			e.Persistent = false
   456  		} else {
   457  			if !c.Expires.After(now) {
   458  				return e, true, nil
   459  			}
   460  			e.Expires = c.Expires
   461  			e.Persistent = true
   462  		}
   463  	}
   464  
   465  	e.Value = c.Value
   466  	e.Quoted = c.Quoted
   467  	e.Secure = c.Secure
   468  	e.HttpOnly = c.HttpOnly
   469  
   470  	switch c.SameSite {
   471  	case http.SameSiteDefaultMode:
   472  		e.SameSite = "SameSite"
   473  	case http.SameSiteStrictMode:
   474  		e.SameSite = "SameSite=Strict"
   475  	case http.SameSiteLaxMode:
   476  		e.SameSite = "SameSite=Lax"
   477  	}
   478  
   479  	return e, false, nil
   480  }
   481  
   482  var (
   483  	errIllegalDomain   = errors.New("cookiejar: illegal cookie domain attribute")
   484  	errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute")
   485  )
   486  
   487  // endOfTime is the time when session (non-persistent) cookies expire.
   488  // This instant is representable in most date/time formats (not just
   489  // Go's time.Time) and should be far enough in the future.
   490  var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
   491  
   492  // domainAndType determines the cookie's domain and hostOnly attribute.
   493  func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
   494  	if domain == "" {
   495  		// No domain attribute in the SetCookie header indicates a
   496  		// host cookie.
   497  		return host, true, nil
   498  	}
   499  
   500  	if isIP(host) {
   501  		// RFC 6265 is not super clear here, a sensible interpretation
   502  		// is that cookies with an IP address in the domain-attribute
   503  		// are allowed.
   504  
   505  		// RFC 6265 section 5.2.3 mandates to strip an optional leading
   506  		// dot in the domain-attribute before processing the cookie.
   507  		//
   508  		// Most browsers don't do that for IP addresses, only curl
   509  		// (version 7.54) and IE (version 11) do not reject a
   510  		//     Set-Cookie: a=1; domain=.127.0.0.1
   511  		// This leading dot is optional and serves only as hint for
   512  		// humans to indicate that a cookie with "domain=.bbc.co.uk"
   513  		// would be sent to every subdomain of bbc.co.uk.
   514  		// It just doesn't make sense on IP addresses.
   515  		// The other processing and validation steps in RFC 6265 just
   516  		// collapse to:
   517  		if host != domain {
   518  			return "", false, errIllegalDomain
   519  		}
   520  
   521  		// According to RFC 6265 such cookies should be treated as
   522  		// domain cookies.
   523  		// As there are no subdomains of an IP address the treatment
   524  		// according to RFC 6265 would be exactly the same as that of
   525  		// a host-only cookie. Contemporary browsers (and curl) do
   526  		// allows such cookies but treat them as host-only cookies.
   527  		// So do we as it just doesn't make sense to label them as
   528  		// domain cookies when there is no domain; the whole notion of
   529  		// domain cookies requires a domain name to be well defined.
   530  		return host, true, nil
   531  	}
   532  
   533  	// From here on: If the cookie is valid, it is a domain cookie (with
   534  	// the one exception of a public suffix below).
   535  	// See RFC 6265 section 5.2.3.
   536  	domain = strings.TrimPrefix(domain, ".")
   537  
   538  	if len(domain) == 0 || domain[0] == '.' {
   539  		// Received either "Domain=." or "Domain=..some.thing",
   540  		// both are illegal.
   541  		return "", false, errMalformedDomain
   542  	}
   543  
   544  	domain, isASCII := ascii.ToLower(domain)
   545  	if !isASCII {
   546  		// Received non-ASCII domain, e.g. "perché.com" instead of "xn--perch-fsa.com"
   547  		return "", false, errMalformedDomain
   548  	}
   549  
   550  	if domain[len(domain)-1] == '.' {
   551  		// We received stuff like "Domain=www.example.com.".
   552  		// Browsers do handle such stuff (actually differently) but
   553  		// RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in
   554  		// requiring a reject.  4.1.2.3 is not normative, but
   555  		// "Domain Matching" (5.1.3) and "Canonicalized Host Names"
   556  		// (5.1.2) are.
   557  		return "", false, errMalformedDomain
   558  	}
   559  
   560  	// See RFC 6265 section 5.3 #5.
   561  	if j.psList != nil {
   562  		if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
   563  			if host == domain {
   564  				// This is the one exception in which a cookie
   565  				// with a domain attribute is a host cookie.
   566  				return host, true, nil
   567  			}
   568  			return "", false, errIllegalDomain
   569  		}
   570  	}
   571  
   572  	// The domain must domain-match host: www.mycompany.com cannot
   573  	// set cookies for .ourcompetitors.com.
   574  	if host != domain && !hasDotSuffix(host, domain) {
   575  		return "", false, errIllegalDomain
   576  	}
   577  
   578  	return domain, false, nil
   579  }
   580  

View as plain text