github.com/useflyent/fhttp@v0.0.0-20211004035111-333f430cfbbf/cookiejar/jar.go (about)

     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/url"
    13  	"sort"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	http "github.com/useflyent/fhttp"
    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 implements "domain-match" of RFC 6265 section 5.1.3.
   125  func (e *entry) domainMatch(host string) bool {
   126  	if e.Domain == host {
   127  		return true
   128  	}
   129  	return !e.HostOnly && hasDotSuffix(host, e.Domain)
   130  }
   131  
   132  // pathMatch implements "path-match" according to RFC 6265 section 5.1.4.
   133  func (e *entry) pathMatch(requestPath string) bool {
   134  	if requestPath == e.Path {
   135  		return true
   136  	}
   137  	if strings.HasPrefix(requestPath, e.Path) {
   138  		if e.Path[len(e.Path)-1] == '/' {
   139  			return true // The "/any/" matches "/any/path" case.
   140  		} else if requestPath[len(e.Path)] == '/' {
   141  			return true // The "/any" matches "/any/path" case.
   142  		}
   143  	}
   144  	return false
   145  }
   146  
   147  // hasDotSuffix reports whether s ends in "."+suffix.
   148  func hasDotSuffix(s, suffix string) bool {
   149  	return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
   150  }
   151  
   152  // Cookies implements the Cookies method of the http.CookieJar interface.
   153  //
   154  // It returns an empty slice if the URL's scheme is not HTTP or HTTPS.
   155  func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
   156  	return j.cookies(u, time.Now())
   157  }
   158  
   159  // cookies is like Cookies but takes the current time as a parameter.
   160  func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
   161  	if u.Scheme != "http" && u.Scheme != "https" {
   162  		return cookies
   163  	}
   164  	host, err := canonicalHost(u.Host)
   165  	if err != nil {
   166  		return cookies
   167  	}
   168  	key := jarKey(host, j.psList)
   169  
   170  	j.mu.Lock()
   171  	defer j.mu.Unlock()
   172  
   173  	submap := j.entries[key]
   174  	if submap == nil {
   175  		return cookies
   176  	}
   177  
   178  	https := u.Scheme == "https"
   179  	path := u.Path
   180  	if path == "" {
   181  		path = "/"
   182  	}
   183  
   184  	modified := false
   185  	var selected []entry
   186  	for id, e := range submap {
   187  		if e.Persistent && !e.Expires.After(now) {
   188  			delete(submap, id)
   189  			modified = true
   190  			continue
   191  		}
   192  		if !e.shouldSend(https, host, path) {
   193  			continue
   194  		}
   195  		e.LastAccess = now
   196  		submap[id] = e
   197  		selected = append(selected, e)
   198  		modified = true
   199  	}
   200  	if modified {
   201  		if len(submap) == 0 {
   202  			delete(j.entries, key)
   203  		} else {
   204  			j.entries[key] = submap
   205  		}
   206  	}
   207  
   208  	// sort according to RFC 6265 section 5.4 point 2: by longest
   209  	// path and then by earliest creation time.
   210  	sort.Slice(selected, func(i, j int) bool {
   211  		s := selected
   212  		if len(s[i].Path) != len(s[j].Path) {
   213  			return len(s[i].Path) > len(s[j].Path)
   214  		}
   215  		if !s[i].Creation.Equal(s[j].Creation) {
   216  			return s[i].Creation.Before(s[j].Creation)
   217  		}
   218  		return s[i].seqNum < s[j].seqNum
   219  	})
   220  	for _, e := range selected {
   221  		cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
   222  	}
   223  
   224  	return cookies
   225  }
   226  
   227  // SetCookies implements the SetCookies method of the http.CookieJar interface.
   228  //
   229  // It does nothing if the URL's scheme is not HTTP or HTTPS.
   230  func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
   231  	j.setCookies(u, cookies, time.Now())
   232  }
   233  
   234  // setCookies is like SetCookies but takes the current time as parameter.
   235  func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {
   236  	if len(cookies) == 0 {
   237  		return
   238  	}
   239  	if u.Scheme != "http" && u.Scheme != "https" {
   240  		return
   241  	}
   242  	host, err := canonicalHost(u.Host)
   243  	if err != nil {
   244  		return
   245  	}
   246  	key := jarKey(host, j.psList)
   247  	defPath := defaultPath(u.Path)
   248  
   249  	j.mu.Lock()
   250  	defer j.mu.Unlock()
   251  
   252  	submap := j.entries[key]
   253  
   254  	modified := false
   255  	for _, cookie := range cookies {
   256  		e, remove, err := j.newEntry(cookie, now, defPath, host)
   257  		if err != nil {
   258  			continue
   259  		}
   260  		id := e.id()
   261  		if remove {
   262  			if submap != nil {
   263  				if _, ok := submap[id]; ok {
   264  					delete(submap, id)
   265  					modified = true
   266  				}
   267  			}
   268  			continue
   269  		}
   270  		if submap == nil {
   271  			submap = make(map[string]entry)
   272  		}
   273  
   274  		if old, ok := submap[id]; ok {
   275  			e.Creation = old.Creation
   276  			e.seqNum = old.seqNum
   277  		} else {
   278  			e.Creation = now
   279  			e.seqNum = j.nextSeqNum
   280  			j.nextSeqNum++
   281  		}
   282  		e.LastAccess = now
   283  		submap[id] = e
   284  		modified = true
   285  	}
   286  
   287  	if modified {
   288  		if len(submap) == 0 {
   289  			delete(j.entries, key)
   290  		} else {
   291  			j.entries[key] = submap
   292  		}
   293  	}
   294  }
   295  
   296  // canonicalHost strips port from host if present and returns the canonicalized
   297  // host name.
   298  func canonicalHost(host string) (string, error) {
   299  	var err error
   300  	host = strings.ToLower(host)
   301  	if hasPort(host) {
   302  		host, _, err = net.SplitHostPort(host)
   303  		if err != nil {
   304  			return "", err
   305  		}
   306  	}
   307  	if strings.HasSuffix(host, ".") {
   308  		// Strip trailing dot from fully qualified domain names.
   309  		host = host[:len(host)-1]
   310  	}
   311  	return toASCII(host)
   312  }
   313  
   314  // hasPort reports whether host contains a port number. host may be a host
   315  // name, an IPv4 or an IPv6 address.
   316  func hasPort(host string) bool {
   317  	colons := strings.Count(host, ":")
   318  	if colons == 0 {
   319  		return false
   320  	}
   321  	if colons == 1 {
   322  		return true
   323  	}
   324  	return host[0] == '[' && strings.Contains(host, "]:")
   325  }
   326  
   327  // jarKey returns the key to use for a jar.
   328  func jarKey(host string, psl PublicSuffixList) string {
   329  	if isIP(host) {
   330  		return host
   331  	}
   332  
   333  	var i int
   334  	if psl == nil {
   335  		i = strings.LastIndex(host, ".")
   336  		if i <= 0 {
   337  			return host
   338  		}
   339  	} else {
   340  		suffix := psl.PublicSuffix(host)
   341  		if suffix == host {
   342  			return host
   343  		}
   344  		i = len(host) - len(suffix)
   345  		if i <= 0 || host[i-1] != '.' {
   346  			// The provided public suffix list psl is broken.
   347  			// Storing cookies under host is a safe stopgap.
   348  			return host
   349  		}
   350  		// Only len(suffix) is used to determine the jar key from
   351  		// here on, so it is okay if psl.PublicSuffix("www.buggy.psl")
   352  		// returns "com" as the jar key is generated from host.
   353  	}
   354  	prevDot := strings.LastIndex(host[:i-1], ".")
   355  	return host[prevDot+1:]
   356  }
   357  
   358  // isIP reports whether host is an IP address.
   359  func isIP(host string) bool {
   360  	return net.ParseIP(host) != nil
   361  }
   362  
   363  // defaultPath returns the directory part of an URL's path according to
   364  // RFC 6265 section 5.1.4.
   365  func defaultPath(path string) string {
   366  	if len(path) == 0 || path[0] != '/' {
   367  		return "/" // Path is empty or malformed.
   368  	}
   369  
   370  	i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1.
   371  	if i == 0 {
   372  		return "/" // Path has the form "/abc".
   373  	}
   374  	return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/".
   375  }
   376  
   377  // newEntry creates an entry from a http.Cookie c. now is the current time and
   378  // is compared to c.Expires to determine deletion of c. defPath and host are the
   379  // default-path and the canonical host name of the URL c was received from.
   380  //
   381  // remove records whether the jar should delete this cookie, as it has already
   382  // expired with respect to now. In this case, e may be incomplete, but it will
   383  // be valid to call e.id (which depends on e's Name, Domain and Path).
   384  //
   385  // A malformed c.Domain will result in an error.
   386  func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) {
   387  	e.Name = c.Name
   388  
   389  	if c.Path == "" || c.Path[0] != '/' {
   390  		e.Path = defPath
   391  	} else {
   392  		e.Path = c.Path
   393  	}
   394  
   395  	e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain)
   396  	if err != nil {
   397  		return e, false, err
   398  	}
   399  
   400  	// MaxAge takes precedence over Expires.
   401  	if c.MaxAge < 0 {
   402  		return e, true, nil
   403  	} else if c.MaxAge > 0 {
   404  		e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
   405  		e.Persistent = true
   406  	} else {
   407  		if c.Expires.IsZero() {
   408  			e.Expires = endOfTime
   409  			e.Persistent = false
   410  		} else {
   411  			if !c.Expires.After(now) {
   412  				return e, true, nil
   413  			}
   414  			e.Expires = c.Expires
   415  			e.Persistent = true
   416  		}
   417  	}
   418  
   419  	e.Value = c.Value
   420  	e.Secure = c.Secure
   421  	e.HttpOnly = c.HttpOnly
   422  
   423  	switch c.SameSite {
   424  	case http.SameSiteDefaultMode:
   425  		e.SameSite = "SameSite"
   426  	case http.SameSiteStrictMode:
   427  		e.SameSite = "SameSite=Strict"
   428  	case http.SameSiteLaxMode:
   429  		e.SameSite = "SameSite=Lax"
   430  	}
   431  
   432  	return e, false, nil
   433  }
   434  
   435  var (
   436  	errIllegalDomain   = errors.New("cookiejar: illegal cookie domain attribute")
   437  	errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute")
   438  	errNoHostname      = errors.New("cookiejar: no host name available (IP only)")
   439  )
   440  
   441  // endOfTime is the time when session (non-persistent) cookies expire.
   442  // This instant is representable in most date/time formats (not just
   443  // Go's time.Time) and should be far enough in the future.
   444  var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
   445  
   446  // domainAndType determines the cookie's domain and hostOnly attribute.
   447  func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
   448  	if domain == "" {
   449  		// No domain attribute in the SetCookie header indicates a
   450  		// host cookie.
   451  		return host, true, nil
   452  	}
   453  
   454  	if isIP(host) {
   455  		// According to RFC 6265 domain-matching includes not being
   456  		// an IP address.
   457  		// TODO: This might be relaxed as in common browsers.
   458  		return "", false, errNoHostname
   459  	}
   460  
   461  	// From here on: If the cookie is valid, it is a domain cookie (with
   462  	// the one exception of a public suffix below).
   463  	// See RFC 6265 section 5.2.3.
   464  	if domain[0] == '.' {
   465  		domain = domain[1:]
   466  	}
   467  
   468  	if len(domain) == 0 || domain[0] == '.' {
   469  		// Received either "Domain=." or "Domain=..some.thing",
   470  		// both are illegal.
   471  		return "", false, errMalformedDomain
   472  	}
   473  	domain = strings.ToLower(domain)
   474  
   475  	if domain[len(domain)-1] == '.' {
   476  		// We received stuff like "Domain=www.example.com.".
   477  		// Browsers do handle such stuff (actually differently) but
   478  		// RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in
   479  		// requiring a reject.  4.1.2.3 is not normative, but
   480  		// "Domain Matching" (5.1.3) and "Canonicalized Host Names"
   481  		// (5.1.2) are.
   482  		return "", false, errMalformedDomain
   483  	}
   484  
   485  	// See RFC 6265 section 5.3 #5.
   486  	if j.psList != nil {
   487  		if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
   488  			if host == domain {
   489  				// This is the one exception in which a cookie
   490  				// with a domain attribute is a host cookie.
   491  				return host, true, nil
   492  			}
   493  			return "", false, errIllegalDomain
   494  		}
   495  	}
   496  
   497  	// The domain must domain-match host: www.mycompany.com cannot
   498  	// set cookies for .ourcompetitors.com.
   499  	if host != domain && !hasDotSuffix(host, domain) {
   500  		return "", false, errIllegalDomain
   501  	}
   502  
   503  	return domain, false, nil
   504  }