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.
6package cookiejar
7
8import (
9	"cmp"
10	"errors"
11	"fmt"
12	"net"
13	"net/http"
14	"net/http/internal/ascii"
15	"net/url"
16	"slices"
17	"strings"
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.
36type 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.
51type 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.
62type 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.
79func New(o *Options) (*Jar, error) {
80	jar := &Jar{
81		entries: make(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.
93type entry struct {
94	Name       string
95	Value      string
96	Quoted     bool
97	Domain     string
98	Path       string
99	SameSite   string
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.
115func (e *entry) id() string {
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.
122func (e *entry) shouldSend(https bool, host, path string) 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.
129func (e *entry) domainMatch(host string) 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.
137func (e *entry) pathMatch(requestPath string) bool {
138	if requestPath == e.Path {
139		return true
140	}
141	if strings.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.
152func hasDotSuffix(s, suffix string) 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.
159func (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.
164func (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.
233func (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.
238func (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 = make(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.
301func canonicalHost(host string) (string, 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 = strings.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.
322func hasPort(host string) bool {
323	colons := strings.Count(host, ":")
324	if colons == 0 {
325		return false
326	}
327	if colons == 1 {
328		return true
329	}
330	return host[0] == '[' && strings.Contains(host, "]:")
331}
332
333// jarKey returns the key to use for a jar.
334func jarKey(host string, psl PublicSuffixList) string {
335	if isIP(host) {
336		return host
337	}
338
339	var i int
340	if psl == nil {
341		i = strings.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 := strings.LastIndex(host[:i-1], ".")
361	return host[prevDot+1:]
362}
363
364// isIP reports whether host is an IP address.
365func isIP(host string) bool {
366	if strings.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.
378func defaultPath(path string) string {
379	if len(path) == 0 || path[0] != '/' {
380		return "/" // Path is empty or malformed.
381	}
382
383	i := strings.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.
399func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (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
449var (
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.
457var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
458
459// domainAndType determines the cookie's domain and hostOnly attribute.
460func (j *Jar) domainAndType(host, domain string) (string, 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	if domain[0] == '.' {
504		domain = domain[1:]
505	}
506
507	if len(domain) == 0 || domain[0] == '.' {
508		// Received either "Domain=." or "Domain=..some.thing",
509		// both are illegal.
510		return "", false, errMalformedDomain
511	}
512
513	domain, isASCII := ascii.ToLower(domain)
514	if !isASCII {
515		// Received non-ASCII domain, e.g. "perché.com" instead of "xn--perch-fsa.com"
516		return "", false, errMalformedDomain
517	}
518
519	if domain[len(domain)-1] == '.' {
520		// We received stuff like "Domain=www.example.com.".
521		// Browsers do handle such stuff (actually differently) but
522		// RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in
523		// requiring a reject.  4.1.2.3 is not normative, but
524		// "Domain Matching" (5.1.3) and "Canonicalized Host Names"
525		// (5.1.2) are.
526		return "", false, errMalformedDomain
527	}
528
529	// See RFC 6265 section 5.3 #5.
530	if j.psList != nil {
531		if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
532			if host == domain {
533				// This is the one exception in which a cookie
534				// with a domain attribute is a host cookie.
535				return host, true, nil
536			}
537			return "", false, errIllegalDomain
538		}
539	}
540
541	// The domain must domain-match host: www.mycompany.com cannot
542	// set cookies for .ourcompetitors.com.
543	if host != domain && !hasDotSuffix(host, domain) {
544		return "", false, errIllegalDomain
545	}
546
547	return domain, false, nil
548}
549