// 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.

// Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar.
package cookiejar import ( ) // 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 // - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us". // // 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. // // A public suffix list implementation is in the package // golang.org/x/net/publicsuffix. type PublicSuffixList interface { // PublicSuffix returns the public suffix of domain. // // 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. PublicSuffix(domain string) string // 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. String() string } // Options are the options for creating a new Jar. type Options struct { // 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 // secure: it means that the HTTP server for foo.co.uk can set a cookie // for bar.co.uk. PublicSuffixList PublicSuffixList } // Jar implements the http.CookieJar interface from the net/http package. type Jar struct { psList PublicSuffixList // 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 // nextSeqNum is the next sequence number assigned to a new cookie // created SetCookies. nextSeqNum uint64 } // New returns a new cookie jar. A nil [*Options] is equivalent to a zero // Options. func ( *Options) (*Jar, error) { := &Jar{ entries: make(map[string]map[string]entry), } if != nil { .psList = .PublicSuffixList } return , nil } // entry is the internal representation of a cookie. // // This struct type is not used outside of this package per se, but the exported // fields are those of RFC 6265. type entry struct { Name string Value string Quoted bool Domain string Path string SameSite string Secure bool HttpOnly bool Persistent bool HostOnly bool Expires time.Time Creation time.Time LastAccess time.Time // 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 } // id returns the domain;path;name triple of e as an id. func ( *entry) () string { return fmt.Sprintf("%s;%s;%s", .Domain, .Path, .Name) } // 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 ( *entry) ( bool, , string) bool { return .domainMatch() && .pathMatch() && ( || !.Secure) } // domainMatch checks whether e's Domain allows sending e back to host. // It differs from "domain-match" of RFC 6265 section 5.1.3 because we treat // a cookie with an IP address in the Domain always as a host cookie. func ( *entry) ( string) bool { if .Domain == { return true } return !.HostOnly && hasDotSuffix(, .Domain) } // pathMatch implements "path-match" according to RFC 6265 section 5.1.4. func ( *entry) ( string) bool { if == .Path { return true } if strings.HasPrefix(, .Path) { if .Path[len(.Path)-1] == '/' { return true // The "/any/" matches "/any/path" case. } else if [len(.Path)] == '/' { return true // The "/any" matches "/any/path" case. } } return false } // hasDotSuffix reports whether s ends in "."+suffix. func hasDotSuffix(, string) bool { return len() > len() && [len()-len()-1] == '.' && [len()-len():] == } // 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. func ( *Jar) ( *url.URL) ( []*http.Cookie) { return .cookies(, time.Now()) } // cookies is like Cookies but takes the current time as a parameter. func ( *Jar) ( *url.URL, time.Time) ( []*http.Cookie) { if .Scheme != "http" && .Scheme != "https" { return } , := canonicalHost(.Host) if != nil { return } := jarKey(, .psList) .mu.Lock() defer .mu.Unlock() := .entries[] if == nil { return } := .Scheme == "https" := .Path if == "" { = "/" } := false var []entry for , := range { if .Persistent && !.Expires.After() { delete(, ) = true continue } if !.shouldSend(, , ) { continue } .LastAccess = [] = = append(, ) = true } if { if len() == 0 { delete(.entries, ) } else { .entries[] = } } // sort according to RFC 6265 section 5.4 point 2: by longest // path and then by earliest creation time. slices.SortFunc(, func(, entry) int { if := cmp.Compare(.Path, .Path); != 0 { return } if := .Creation.Compare(.Creation); != 0 { return } return cmp.Compare(.seqNum, .seqNum) }) for , := range { = append(, &http.Cookie{Name: .Name, Value: .Value, Quoted: .Quoted}) } return } // SetCookies implements the SetCookies method of the [http.CookieJar] interface. // // It does nothing if the URL's scheme is not HTTP or HTTPS. func ( *Jar) ( *url.URL, []*http.Cookie) { .setCookies(, , time.Now()) } // setCookies is like SetCookies but takes the current time as parameter. func ( *Jar) ( *url.URL, []*http.Cookie, time.Time) { if len() == 0 { return } if .Scheme != "http" && .Scheme != "https" { return } , := canonicalHost(.Host) if != nil { return } := jarKey(, .psList) := defaultPath(.Path) .mu.Lock() defer .mu.Unlock() := .entries[] := false for , := range { , , := .newEntry(, , , ) if != nil { continue } := .id() if { if != nil { if , := []; { delete(, ) = true } } continue } if == nil { = make(map[string]entry) } if , := []; { .Creation = .Creation .seqNum = .seqNum } else { .Creation = .seqNum = .nextSeqNum .nextSeqNum++ } .LastAccess = [] = = true } if { if len() == 0 { delete(.entries, ) } else { .entries[] = } } } // canonicalHost strips port from host if present and returns the canonicalized // host name. func canonicalHost( string) (string, error) { var error if hasPort() { , _, = net.SplitHostPort() if != nil { return "", } } // Strip trailing dot from fully qualified domain names. = strings.TrimSuffix(, ".") , := toASCII() if != nil { return "", } // We know this is ascii, no need to check. , := ascii.ToLower() return , nil } // hasPort reports whether host contains a port number. host may be a host // name, an IPv4 or an IPv6 address. func hasPort( string) bool { := strings.Count(, ":") if == 0 { return false } if == 1 { return true } return [0] == '[' && strings.Contains(, "]:") } // jarKey returns the key to use for a jar. func jarKey( string, PublicSuffixList) string { if isIP() { return } var int if == nil { = strings.LastIndex(, ".") if <= 0 { return } } else { := .PublicSuffix() if == { return } = len() - len() if <= 0 || [-1] != '.' { // The provided public suffix list psl is broken. // Storing cookies under host is a safe stopgap. return } // Only len(suffix) is used to determine the jar key from // here on, so it is okay if psl.PublicSuffix("www.buggy.psl") // returns "com" as the jar key is generated from host. } := strings.LastIndex([:-1], ".") return [+1:] } // isIP reports whether host is an IP address. func isIP( string) bool { if strings.ContainsAny(, ":%") { // Probable IPv6 address. // Hostnames can't contain : or %, so this is definitely not a valid host. // Treating it as an IP is the more conservative option, and avoids the risk // of interpreting ::1%.www.example.com as a subdomain of www.example.com. return true } return net.ParseIP() != nil } // defaultPath returns the directory part of a URL's path according to // RFC 6265 section 5.1.4. func defaultPath( string) string { if len() == 0 || [0] != '/' { return "/" // Path is empty or malformed. } := strings.LastIndex(, "/") // Path starts with "/", so i != -1. if == 0 { return "/" // Path has the form "/abc". } return [:] // Path is either of form "/abc/xyz" or "/abc/xyz/". } // newEntry creates an entry from an 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. // // remove records whether the jar should delete this cookie, as it has already // 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 ( *Jar) ( *http.Cookie, time.Time, , string) ( entry, bool, error) { .Name = .Name if .Path == "" || .Path[0] != '/' { .Path = } else { .Path = .Path } .Domain, .HostOnly, = .domainAndType(, .Domain) if != nil { return , false, } // MaxAge takes precedence over Expires. if .MaxAge < 0 { return , true, nil } else if .MaxAge > 0 { .Expires = .Add(time.Duration(.MaxAge) * time.Second) .Persistent = true } else { if .Expires.IsZero() { .Expires = endOfTime .Persistent = false } else { if !.Expires.After() { return , true, nil } .Expires = .Expires .Persistent = true } } .Value = .Value .Quoted = .Quoted .Secure = .Secure .HttpOnly = .HttpOnly switch .SameSite { case http.SameSiteDefaultMode: .SameSite = "SameSite" case http.SameSiteStrictMode: .SameSite = "SameSite=Strict" case http.SameSiteLaxMode: .SameSite = "SameSite=Lax" } return , false, nil } var ( errIllegalDomain = errors.New("cookiejar: illegal cookie domain attribute") errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute") ) // 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 ( *Jar) (, string) (string, bool, error) { if == "" { // No domain attribute in the SetCookie header indicates a // host cookie. return , true, nil } if isIP() { // RFC 6265 is not super clear here, a sensible interpretation // is that cookies with an IP address in the domain-attribute // are allowed. // RFC 6265 section 5.2.3 mandates to strip an optional leading // dot in the domain-attribute before processing the cookie. // // Most browsers don't do that for IP addresses, only curl // (version 7.54) and IE (version 11) do not reject a // Set-Cookie: a=1; domain=.127.0.0.1 // This leading dot is optional and serves only as hint for // humans to indicate that a cookie with "domain=.bbc.co.uk" // would be sent to every subdomain of bbc.co.uk. // It just doesn't make sense on IP addresses. // The other processing and validation steps in RFC 6265 just // collapse to: if != { return "", false, errIllegalDomain } // According to RFC 6265 such cookies should be treated as // domain cookies. // As there are no subdomains of an IP address the treatment // according to RFC 6265 would be exactly the same as that of // a host-only cookie. Contemporary browsers (and curl) do // allows such cookies but treat them as host-only cookies. // So do we as it just doesn't make sense to label them as // domain cookies when there is no domain; the whole notion of // domain cookies requires a domain name to be well defined. return , true, nil } // 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 [0] == '.' { = [1:] } if len() == 0 || [0] == '.' { // Received either "Domain=." or "Domain=..some.thing", // both are illegal. return "", false, errMalformedDomain } , := ascii.ToLower() if ! { // Received non-ASCII domain, e.g. "perché.com" instead of "xn--perch-fsa.com" return "", false, errMalformedDomain } if [len()-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 .psList != nil { if := .psList.PublicSuffix(); != "" && !hasDotSuffix(, ) { if == { // This is the one exception in which a cookie // with a domain attribute is a host cookie. return , true, nil } return "", false, errIllegalDomain } } // The domain must domain-match host: www.mycompany.com cannot // set cookies for .ourcompetitors.com. if != && !hasDotSuffix(, ) { return "", false, errIllegalDomain } return , false, nil }