jar.go 13 KB
Newer Older
Nigel Tao's avatar
Nigel Tao committed
1 2 3 4
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

5
// Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar.
Nigel Tao's avatar
Nigel Tao committed
6 7 8
package cookiejar

import (
9 10
	"errors"
	"fmt"
11
	"net"
Nigel Tao's avatar
Nigel Tao committed
12 13
	"net/http"
	"net/url"
14
	"sort"
15 16 17
	"strings"
	"sync"
	"time"
Nigel Tao's avatar
Nigel Tao committed
18 19 20 21 22
)

// PublicSuffixList provides the public suffix of a domain. For example:
//      - the public suffix of "example.com" is "com",
//      - 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".
Nigel Tao's avatar
Nigel Tao committed
24 25 26 27 28 29 30
//
// Implementations of PublicSuffixList must be safe for concurrent use by
// multiple goroutines.
//
// An implementation that always returns "" is valid and may be useful for
// testing but it is not secure: it means that the HTTP server for foo.com can
// set a cookie for bar.com.
31 32 33
//
// A public suffix list implementation is in the package
// code.google.com/p/go.net/publicsuffix.
Nigel Tao's avatar
Nigel Tao committed
34 35 36
type PublicSuffixList interface {
	// PublicSuffix returns the public suffix of domain.
	//
37 38 39
	// TODO: specify which of the caller and callee is responsible for IP
	// addresses, for leading and trailing dots, for case sensitivity, and
	// for IDN/Punycode.
Nigel Tao's avatar
Nigel Tao committed
40 41
	PublicSuffix(domain string) string

42 43 44
	// String returns a description of the source of this public suffix
	// list. The description will typically contain something like a time
	// stamp or version number.
Nigel Tao's avatar
Nigel Tao committed
45 46 47 48 49
	String() string
}

// Options are the options for creating a new Jar.
type Options struct {
50 51 52 53
	// PublicSuffixList is the public suffix list that determines whether
	// an HTTP server can set a cookie for a domain.
	//
	// A nil value is valid and may be useful for testing but it is not
54 55
	// secure: it means that the HTTP server for foo.co.uk can set a cookie
	// for bar.co.uk.
Nigel Tao's avatar
Nigel Tao committed
56 57 58 59 60
	PublicSuffixList PublicSuffixList
}

// Jar implements the http.CookieJar interface from the net/http package.
type Jar struct {
61
	psList PublicSuffixList
62 63 64 65 66 67 68

	// mu locks the remaining fields.
	mu sync.Mutex

	// entries is a set of entries, keyed by their eTLD+1 and subkeyed by
	// their name/domain/path.
	entries map[string]map[string]entry
69 70 71 72

	// nextSeqNum is the next sequence number assigned to a new cookie
	// created SetCookies.
	nextSeqNum uint64
Nigel Tao's avatar
Nigel Tao committed
73 74
}

75 76 77
// New returns a new cookie jar. A nil *Options is equivalent to a zero
// Options.
func New(o *Options) (*Jar, error) {
78 79 80 81 82 83 84 85 86 87
	jar := &Jar{
		entries: make(map[string]map[string]entry),
	}
	if o != nil {
		jar.psList = o.PublicSuffixList
	}
	return jar, nil
}

// entry is the internal representation of a cookie.
88 89 90
//
// This struct type is not used outside of this package per se, but the exported
// fields are those of RFC 6265.
91 92 93 94 95 96 97 98 99 100 101 102
type entry struct {
	Name       string
	Value      string
	Domain     string
	Path       string
	Secure     bool
	HttpOnly   bool
	Persistent bool
	HostOnly   bool
	Expires    time.Time
	Creation   time.Time
	LastAccess time.Time
103 104 105 106 107

	// seqNum is a sequence number so that Cookies returns cookies in a
	// deterministic order, even for cookies that have equal Path length and
	// equal Creation time. This simplifies testing.
	seqNum uint64
Nigel Tao's avatar
Nigel Tao committed
108 109
}

110 111 112 113 114
// Id returns the domain;path;name triple of e as an id.
func (e *entry) id() string {
	return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name)
}

115 116 117 118 119 120 121 122 123 124 125 126
// shouldSend determines whether e's cookie qualifies to be included in a
// request to host/path. It is the caller's responsibility to check if the
// cookie is expired.
func (e *entry) shouldSend(https bool, host, path string) bool {
	return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure)
}

// domainMatch implements "domain-match" of RFC 6265 section 5.1.3.
func (e *entry) domainMatch(host string) bool {
	if e.Domain == host {
		return true
	}
127
	return !e.HostOnly && hasDotSuffix(host, e.Domain)
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
}

// pathMatch implements "path-match" according to RFC 6265 section 5.1.4.
func (e *entry) pathMatch(requestPath string) bool {
	if requestPath == e.Path {
		return true
	}
	if strings.HasPrefix(requestPath, e.Path) {
		if e.Path[len(e.Path)-1] == '/' {
			return true // The "/any/" matches "/any/path" case.
		} else if requestPath[len(e.Path)] == '/' {
			return true // The "/any" matches "/any/path" case.
		}
	}
	return false
}

145
// hasDotSuffix reports whether s ends in "."+suffix.
146 147 148 149
func hasDotSuffix(s, suffix string) bool {
	return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
}

150 151 152 153 154 155 156
// byPathLength is a []entry sort.Interface that sorts according to RFC 6265
// section 5.4 point 2: by longest path and then by earliest creation time.
type byPathLength []entry

func (s byPathLength) Len() int { return len(s) }

func (s byPathLength) Less(i, j int) bool {
157 158 159 160
	if len(s[i].Path) != len(s[j].Path) {
		return len(s[i].Path) > len(s[j].Path)
	}
	if !s[i].Creation.Equal(s[j].Creation) {
161 162
		return s[i].Creation.Before(s[j].Creation)
	}
163
	return s[i].seqNum < s[j].seqNum
164 165 166 167
}

func (s byPathLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

Nigel Tao's avatar
Nigel Tao committed
168 169 170
// Cookies implements the Cookies method of the http.CookieJar interface.
//
// 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 173 174 175 176
	return j.cookies(u, time.Now())
}

// cookies is like Cookies but takes the current time as a parameter.
func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
	if u.Scheme != "http" && u.Scheme != "https" {
		return cookies
	}
	host, err := canonicalHost(u.Host)
	if err != nil {
		return cookies
	}
	key := jarKey(host, j.psList)

	j.mu.Lock()
	defer j.mu.Unlock()

	submap := j.entries[key]
	if submap == nil {
		return cookies
	}

194 195 196 197 198 199
	https := u.Scheme == "https"
	path := u.Path
	if path == "" {
		path = "/"
	}

200
	modified := false
201 202 203 204 205 206 207 208 209 210 211 212 213 214
	var selected []entry
	for id, e := range submap {
		if e.Persistent && !e.Expires.After(now) {
			delete(submap, id)
			modified = true
			continue
		}
		if !e.shouldSend(https, host, path) {
			continue
		}
		e.LastAccess = now
		submap[id] = e
		selected = append(selected, e)
		modified = true
215 216 217 218 219 220 221 222 223
	}
	if modified {
		if len(submap) == 0 {
			delete(j.entries, key)
		} else {
			j.entries[key] = submap
		}
	}

224 225 226 227
	sort.Sort(byPathLength(selected))
	for _, e := range selected {
		cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
	}
228 229

	return cookies
Nigel Tao's avatar
Nigel Tao committed
230 231 232 233 234 235
}

// SetCookies implements the SetCookies method of the http.CookieJar interface.
//
// It does nothing if the URL's scheme is not HTTP or HTTPS.
func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
236 237 238 239 240
	j.setCookies(u, cookies, time.Now())
}

// setCookies is like SetCookies but takes the current time as parameter.
func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {
241 242 243 244 245 246 247 248 249 250 251
	if len(cookies) == 0 {
		return
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return
	}
	host, err := canonicalHost(u.Host)
	if err != nil {
		return
	}
	key := jarKey(host, j.psList)
252
	defPath := defaultPath(u.Path)
253 254 255 256 257 258 259

	j.mu.Lock()
	defer j.mu.Unlock()

	submap := j.entries[key]

	modified := false
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
	for _, cookie := range cookies {
		e, remove, err := j.newEntry(cookie, now, defPath, host)
		if err != nil {
			continue
		}
		id := e.id()
		if remove {
			if submap != nil {
				if _, ok := submap[id]; ok {
					delete(submap, id)
					modified = true
				}
			}
			continue
		}
		if submap == nil {
			submap = make(map[string]entry)
		}

		if old, ok := submap[id]; ok {
			e.Creation = old.Creation
281
			e.seqNum = old.seqNum
282 283
		} else {
			e.Creation = now
284 285
			e.seqNum = j.nextSeqNum
			j.nextSeqNum++
286 287 288 289
		}
		e.LastAccess = now
		submap[id] = e
		modified = true
290 291 292 293 294 295 296 297 298 299 300 301
	}

	if modified {
		if len(submap) == 0 {
			delete(j.entries, key)
		} else {
			j.entries[key] = submap
		}
	}
}

// canonicalHost strips port from host if present and returns the canonicalized
302
// host name.
303 304 305 306 307 308 309 310 311 312 313 314 315
func canonicalHost(host string) (string, error) {
	var err error
	host = strings.ToLower(host)
	if hasPort(host) {
		host, _, err = net.SplitHostPort(host)
		if err != nil {
			return "", err
		}
	}
	if strings.HasSuffix(host, ".") {
		// Strip trailing dot from fully qualified domain names.
		host = host[:len(host)-1]
	}
316
	return toASCII(host)
317 318
}

319
// hasPort reports whether host contains a port number. host may be a host
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
// name, an IPv4 or an IPv6 address.
func hasPort(host string) bool {
	colons := strings.Count(host, ":")
	if colons == 0 {
		return false
	}
	if colons == 1 {
		return true
	}
	return host[0] == '[' && strings.Contains(host, "]:")
}

// jarKey returns the key to use for a jar.
func jarKey(host string, psl PublicSuffixList) string {
	if isIP(host) {
		return host
	}
337 338

	var i int
339
	if psl == nil {
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
		i = strings.LastIndex(host, ".")
		if i == -1 {
			return host
		}
	} else {
		suffix := psl.PublicSuffix(host)
		if suffix == host {
			return host
		}
		i = len(host) - len(suffix)
		if i <= 0 || host[i-1] != '.' {
			// The provided public suffix list psl is broken.
			// Storing cookies under host is a safe stopgap.
			return host
		}
355 356 357 358 359
	}
	prevDot := strings.LastIndex(host[:i-1], ".")
	return host[prevDot+1:]
}

360
// isIP reports whether host is an IP address.
361 362
func isIP(host string) bool {
	return net.ParseIP(host) != nil
Nigel Tao's avatar
Nigel Tao committed
363
}
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382

// defaultPath returns the directory part of an URL's path according to
// RFC 6265 section 5.1.4.
func defaultPath(path string) string {
	if len(path) == 0 || path[0] != '/' {
		return "/" // Path is empty or malformed.
	}

	i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1.
	if i == 0 {
		return "/" // Path has the form "/abc".
	}
	return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/".
}

// newEntry creates an entry from a http.Cookie c. now is the current time and
// is compared to c.Expires to determine deletion of c. defPath and host are the
// default-path and the canonical host name of the URL c was received from.
//
383
// remove records whether the jar should delete this cookie, as it has already
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
// expired with respect to now. In this case, e may be incomplete, but it will
// be valid to call e.id (which depends on e's Name, Domain and Path).
//
// A malformed c.Domain will result in an error.
func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) {
	e.Name = c.Name

	if c.Path == "" || c.Path[0] != '/' {
		e.Path = defPath
	} else {
		e.Path = c.Path
	}

	e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain)
	if err != nil {
		return e, false, err
	}

	// MaxAge takes precedence over Expires.
	if c.MaxAge < 0 {
		return e, true, nil
	} else if c.MaxAge > 0 {
		e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
		e.Persistent = true
	} else {
		if c.Expires.IsZero() {
			e.Expires = endOfTime
			e.Persistent = false
		} else {
413
			if !c.Expires.After(now) {
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
				return e, true, nil
			}
			e.Expires = c.Expires
			e.Persistent = true
		}
	}

	e.Value = c.Value
	e.Secure = c.Secure
	e.HttpOnly = c.HttpOnly

	return e, false, nil
}

var (
	errIllegalDomain   = errors.New("cookiejar: illegal cookie domain attribute")
	errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute")
	errNoHostname      = errors.New("cookiejar: no host name available (IP only)")
)

// endOfTime is the time when session (non-persistent) cookies expire.
// This instant is representable in most date/time formats (not just
// Go's time.Time) and should be far enough in the future.
var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)

// domainAndType determines the cookie's domain and hostOnly attribute.
func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
	if domain == "" {
		// No domain attribute in the SetCookie header indicates a
		// host cookie.
		return host, true, nil
	}

	if isIP(host) {
		// According to RFC 6265 domain-matching includes not being
		// an IP address.
		// TODO: This might be relaxed as in common browsers.
		return "", false, errNoHostname
	}

	// From here on: If the cookie is valid, it is a domain cookie (with
	// the one exception of a public suffix below).
	// See RFC 6265 section 5.2.3.
	if domain[0] == '.' {
		domain = domain[1:]
	}

	if len(domain) == 0 || domain[0] == '.' {
		// Received either "Domain=." or "Domain=..some.thing",
		// both are illegal.
		return "", false, errMalformedDomain
	}
	domain = strings.ToLower(domain)

	if domain[len(domain)-1] == '.' {
		// We received stuff like "Domain=www.example.com.".
		// Browsers do handle such stuff (actually differently) but
		// RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in
		// requiring a reject.  4.1.2.3 is not normative, but
		// "Domain Matching" (5.1.3) and "Canonicalized Host Names"
		// (5.1.2) are.
		return "", false, errMalformedDomain
	}

	// See RFC 6265 section 5.3 #5.
	if j.psList != nil {
480
		if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
481 482 483 484 485 486 487 488 489 490 491
			if host == domain {
				// This is the one exception in which a cookie
				// with a domain attribute is a host cookie.
				return host, true, nil
			}
			return "", false, errIllegalDomain
		}
	}

	// The domain must domain-match host: www.mycompany.com cannot
	// set cookies for .ourcompetitors.com.
492
	if host != domain && !hasDotSuffix(host, domain) {
493 494 495 496 497
		return "", false, errIllegalDomain
	}

	return domain, false, nil
}