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

View as plain text