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