jar.mx raw

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