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