123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- // 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 (
- "errors"
- "fmt"
- "net"
- "net/http"
- "net/http/internal/ascii"
- "net/url"
- "sort"
- "strings"
- "sync"
- "time"
- )
- // 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 New(o *Options) (*Jar, error) {
- 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.
- //
- // 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
- 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 (e *entry) id() string {
- return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.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 (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
- }
- return !e.HostOnly && hasDotSuffix(host, e.Domain)
- }
- // 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
- }
- // hasDotSuffix reports whether s ends in "."+suffix.
- func hasDotSuffix(s, suffix string) bool {
- return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
- }
- // 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 (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
- 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) {
- 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
- }
- https := u.Scheme == "https"
- path := u.Path
- if path == "" {
- path = "/"
- }
- modified := false
- 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
- }
- if modified {
- if len(submap) == 0 {
- delete(j.entries, key)
- } else {
- j.entries[key] = submap
- }
- }
- // sort according to RFC 6265 section 5.4 point 2: by longest
- // path and then by earliest creation time.
- sort.Slice(selected, func(i, j int) bool {
- s := selected
- 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) {
- return s[i].Creation.Before(s[j].Creation)
- }
- return s[i].seqNum < s[j].seqNum
- })
- for _, e := range selected {
- cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
- }
- return cookies
- }
- // 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) {
- 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) {
- 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)
- defPath := defaultPath(u.Path)
- j.mu.Lock()
- defer j.mu.Unlock()
- submap := j.entries[key]
- modified := false
- 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
- e.seqNum = old.seqNum
- } else {
- e.Creation = now
- e.seqNum = j.nextSeqNum
- j.nextSeqNum++
- }
- e.LastAccess = now
- submap[id] = e
- modified = true
- }
- 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
- // host name.
- func canonicalHost(host string) (string, error) {
- var err error
- 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]
- }
- encoded, err := toASCII(host)
- if err != nil {
- return "", err
- }
- // We know this is ascii, no need to check.
- lower, _ := ascii.ToLower(encoded)
- return lower, nil
- }
- // hasPort reports whether host contains a port number. host may be a host
- // 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
- }
- var i int
- if psl == nil {
- i = strings.LastIndex(host, ".")
- if i <= 0 {
- 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
- }
- // 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.
- }
- prevDot := strings.LastIndex(host[:i-1], ".")
- return host[prevDot+1:]
- }
- // isIP reports whether host is an IP address.
- func isIP(host string) bool {
- return net.ParseIP(host) != nil
- }
- // 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.
- //
- // 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 (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 {
- if !c.Expires.After(now) {
- return e, true, nil
- }
- e.Expires = c.Expires
- e.Persistent = true
- }
- }
- e.Value = c.Value
- e.Secure = c.Secure
- e.HttpOnly = c.HttpOnly
- switch c.SameSite {
- case http.SameSiteDefaultMode:
- e.SameSite = "SameSite"
- case http.SameSiteStrictMode:
- e.SameSite = "SameSite=Strict"
- case http.SameSiteLaxMode:
- e.SameSite = "SameSite=Lax"
- }
- 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, isASCII := ascii.ToLower(domain)
- if !isASCII {
- // Received non-ASCII domain, e.g. "perché.com" instead of "xn--perch-fsa.com"
- return "", false, errMalformedDomain
- }
- 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 {
- if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
- 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.
- if host != domain && !hasDotSuffix(host, domain) {
- return "", false, errIllegalDomain
- }
- return domain, false, nil
- }
|