Home | History | Annotate | Download | only in cookiejar
      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/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.
     34 type 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.
     49 type 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.
     60 type 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.
     77 func 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.
     91 type 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.
    111 func (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.
    118 func (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.
    123 func (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.
    131 func (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.
    146 func 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.
    153 func (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.
    158 func (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.
    228 func (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.
    233 func (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.
    296 func 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.
    314 func 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.
    326 func 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.
    357 func 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.
    363 func 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.
    384 func (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 
    424 var (
    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.
    433 var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
    434 
    435 // domainAndType determines the cookie's domain and hostOnly attribute.
    436 func (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