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