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