Files
atom/providers/httpclient/cookiejar/jar.go
2023-05-12 20:02:29 +08:00

705 lines
19 KiB
Go

// 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.
//
// This implementation is a fork of net/http/cookiejar which also
// implements methods for dumping the cookies to persistent
// storage and retrieving them.
package cookiejar
import (
"fmt"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"golang.org/x/net/publicsuffix"
)
// 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.
//
// If this is nil, the public suffix list implementation in golang.org/x/net/publicsuffix
// is used.
PublicSuffixList PublicSuffixList
// Filename holds the file to use for storage of the cookies.
// If it is empty, the value of DefaultCookieFile will be used.
Filename string
// NoPersist specifies whether no persistence should be used
// (useful for tests). If this is true, the value of Filename will be
// ignored.
NoPersist bool
}
// Jar implements the http.CookieJar interface from the net/http package.
type Jar struct {
// filename holds the file that the cookies were loaded from.
filename string
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
}
var noOptions Options
// New returns a new cookie jar. A nil *Options is equivalent to a zero
// Options.
//
// New will return an error if the cookies could not be loaded
// from the file for any reason than if the file does not exist.
func New(o *Options) (*Jar, error) {
return newAtTime(o, time.Now())
}
// newAtTime is like New but takes the current time as a parameter.
func newAtTime(o *Options, now time.Time) (*Jar, error) {
jar := &Jar{
entries: make(map[string]map[string]entry),
}
if o == nil {
o = &noOptions
}
if jar.psList = o.PublicSuffixList; jar.psList == nil {
jar.psList = publicsuffix.List
}
if !o.NoPersist {
if jar.filename = o.Filename; jar.filename == "" {
jar.filename = DefaultCookieFile()
}
if err := jar.load(); err != nil {
return nil, errors.Wrap(err, "cannot load cookies")
}
}
jar.deleteExpired(now)
return jar, nil
}
// homeDir returns the OS-specific home path as specified in the environment.
func homeDir() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH"))
}
return os.Getenv("HOME")
}
// 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.
// Note that this structure is marshaled to JSON, so backward-compatibility
// should be preserved.
type entry struct {
Name string
Value string
Domain string
Path string
Secure bool
HttpOnly bool
Persistent bool
HostOnly bool
Expires time.Time
Creation time.Time
LastAccess time.Time
// Updated records when the cookie was updated.
// This is different from creation time because a cookie
// can be changed without updating the creation time.
Updated time.Time
// CanonicalHost stores the original canonical host name
// that the cookie was associated with. We store this
// so that even if the public suffix list changes (for example
// when storing/loading cookies) we can still get the correct
// jar keys.
CanonicalHost string
}
// id returns the domain;path;name triple of e as an id.
func (e *entry) id() string {
return id(e.Domain, e.Path, e.Name)
}
// id returns the domain;path;name triple as an id.
func id(domain, path, name string) 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 (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
}
type byCanonicalHost struct {
byPathLength
}
func (s byCanonicalHost) Less(i, j int) bool {
e0, e1 := &s.byPathLength[i], &s.byPathLength[j]
if e0.CanonicalHost != e1.CanonicalHost {
return e0.CanonicalHost < e1.CanonicalHost
}
return s.byPathLength.Less(i, j)
}
// byPathLength is a []entry sort.Interface that sorts according to RFC 6265
// section 5.4 point 2: by longest path and then by earliest creation time.
type byPathLength []entry
func (s byPathLength) Len() int { return len(s) }
func (s byPathLength) Less(i, j int) bool {
e0, e1 := &s[i], &s[j]
if len(e0.Path) != len(e1.Path) {
return len(e0.Path) > len(e1.Path)
}
if !e0.Creation.Equal(e1.Creation) {
return e0.Creation.Before(e1.Creation)
}
// The following are not strictly necessary
// but are useful for providing deterministic
// behaviour in tests.
if e0.Name != e1.Name {
return e0.Name < e1.Name
}
return e0.Value < e1.Value
}
func (s byPathLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// 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 = "/"
}
var selected []entry
for id, e := range submap {
if !e.Expires.After(now) {
// Save some space by deleting the value when the cookie
// expires. We can't delete the cookie itself because then
// we wouldn't know that the cookie had expired when
// we merge with another cookie jar.
if e.Value != "" {
e.Value = ""
submap[id] = e
}
continue
}
if !e.shouldSend(https, host, path) {
continue
}
e.LastAccess = now
submap[id] = e
selected = append(selected, e)
}
sort.Sort(byPathLength(selected))
for _, e := range selected {
cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
}
return cookies
}
// AllCookies returns all cookies in the jar. The returned cookies will
// have Domain, Expires, HttpOnly, Name, Secure, Path, and Value filled
// out. Expired cookies will not be returned. This function does not
// modify the cookie jar.
func (j *Jar) AllCookies() (cookies []*http.Cookie) {
return j.allCookies(time.Now())
}
// allCookies is like AllCookies but takes the current time as a parameter.
func (j *Jar) allCookies(now time.Time) []*http.Cookie {
var selected []entry
j.mu.Lock()
defer j.mu.Unlock()
for _, submap := range j.entries {
for _, e := range submap {
if !e.Expires.After(now) {
// Do not return expired cookies.
continue
}
selected = append(selected, e)
}
}
sort.Sort(byCanonicalHost{byPathLength(selected)})
cookies := make([]*http.Cookie, len(selected))
for i, e := range selected {
// Note: The returned cookies do not contain sufficient
// information to recreate the database.
cookies[i] = &http.Cookie{
Name: e.Name,
Value: e.Value,
Path: e.Path,
Domain: e.Domain,
Expires: e.Expires,
Secure: e.Secure,
HttpOnly: e.HttpOnly,
}
}
return cookies
}
// RemoveCookie removes the cookie matching the name, domain and path
// specified by c.
func (j *Jar) RemoveCookie(c *http.Cookie) {
j.mu.Lock()
defer j.mu.Unlock()
id := id(c.Domain, c.Path, c.Name)
key := jarKey(c.Domain, j.psList)
if e, ok := j.entries[key][id]; ok {
e.Value = ""
e.Expires = time.Now().Add(-1 * time.Second)
j.entries[key][id] = e
}
}
// merge merges all the given entries into j. More recently changed
// cookies take precedence over older ones.
func (j *Jar) merge(entries []entry) {
for _, e := range entries {
if e.CanonicalHost == "" {
continue
}
key := jarKey(e.CanonicalHost, j.psList)
id := e.id()
submap := j.entries[key]
if submap == nil {
j.entries[key] = map[string]entry{
id: e,
}
continue
}
oldEntry, ok := submap[id]
if !ok || e.Updated.After(oldEntry.Updated) {
submap[id] = e
}
}
}
var expiryRemovalDuration = 24 * time.Hour
// deleteExpired deletes all entries that have expired for long enough
// that we can actually expect there to be no external copies of it that
// might resurrect the dead cookie.
func (j *Jar) deleteExpired(now time.Time) {
for tld, submap := range j.entries {
for id, e := range submap {
if !e.Expires.After(now) && !e.Updated.Add(expiryRemovalDuration).After(now) {
delete(submap, id)
}
}
if len(submap) == 0 {
delete(j.entries, tld)
}
}
}
// RemoveAllHost removes any cookies from the jar that were set for the given host.
func (j *Jar) RemoveAllHost(host string) {
host, err := canonicalHost(host)
if err != nil {
return
}
key := jarKey(host, j.psList)
j.mu.Lock()
defer j.mu.Unlock()
expired := time.Now().Add(-1 * time.Second)
submap := j.entries[key]
for id, e := range submap {
if e.CanonicalHost == host {
// Save some space by deleting the value when the cookie
// expires. We can't delete the cookie itself because then
// we wouldn't know that the cookie had expired when
// we merge with another cookie jar.
e.Value = ""
e.Expires = expired
submap[id] = e
}
}
}
// RemoveAll removes all the cookies from the jar.
func (j *Jar) RemoveAll() {
expired := time.Now().Add(-1 * time.Second)
j.mu.Lock()
defer j.mu.Unlock()
for _, submap := range j.entries {
for id, e := range submap {
// Save some space by deleting the value when the cookie
// expires. We can't delete the cookie itself because then
// we wouldn't know that the cookie had expired when
// we merge with another cookie jar.
e.Value = ""
e.Expires = expired
submap[id] = e
}
}
}
// 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" {
// TODO is this really correct? It might be nice to send
// cookies to websocket connections, for example.
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]
for _, cookie := range cookies {
e, err := j.newEntry(cookie, now, defPath, host)
if err != nil {
continue
}
e.CanonicalHost = host
id := e.id()
if submap == nil {
submap = make(map[string]entry)
j.entries[key] = submap
}
if old, ok := submap[id]; ok {
e.Creation = old.Creation
} else {
e.Creation = now
}
e.Updated = now
e.LastAccess = now
submap[id] = e
}
}
// canonicalHost strips port from host if present and returns the canonicalized
// host name.
func canonicalHost(host string) (string, error) {
var err error
host = strings.ToLower(host)
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]
}
return toASCII(host)
}
// 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 == -1 {
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
}
}
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.
//
// The returned entry should be removed if its expiry time is in the
// past. 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, 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, err
}
// MaxAge takes precedence over Expires.
if c.MaxAge != 0 {
e.Persistent = true
e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
if c.MaxAge < 0 {
return e, nil
}
} else if c.Expires.IsZero() {
e.Expires = endOfTime
} else {
e.Persistent = true
e.Expires = c.Expires
if !c.Expires.After(now) {
return e, nil
}
}
e.Value = c.Value
e.Secure = c.Secure
e.HttpOnly = c.HttpOnly
return e, 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 = strings.ToLower(domain)
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
}
// DefaultCookieFile returns the default cookie file to use
// for persisting cookie data.
// The following names will be used in decending order of preference:
// - the value of the $GOCOOKIES environment variable.
// - $HOME/.go-cookies
func DefaultCookieFile() string {
if f := os.Getenv("GOCOOKIES"); f != "" {
return f
}
return filepath.Join(homeDir(), ".go-cookies")
}