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