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

View as plain text