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