pax_global_header00006660000000000000000000000064141425650710014517gustar00rootroot0000000000000052 comment=93b5b7983ffaa91682a5a1008e698c6f7947de6a golang-github-juju-persistent-cookiejar-1.0.0/000077500000000000000000000000001414256507100213615ustar00rootroot00000000000000golang-github-juju-persistent-cookiejar-1.0.0/LICENSE000066400000000000000000000027071414256507100223740ustar00rootroot00000000000000Copyright (c) 2012 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. golang-github-juju-persistent-cookiejar-1.0.0/README.md000066400000000000000000000063751414256507100226530ustar00rootroot00000000000000# cookiejar -- import "github.com/juju/persistent-cookiejar" 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. ## Usage #### func DefaultCookieFile ```go func DefaultCookieFile() string ``` 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 #### type Jar ```go type Jar struct { } ``` Jar implements the http.CookieJar interface from the net/http package. #### func New ```go func New(o *Options) (*Jar, error) ``` 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 (*Jar) Cookies ```go func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) ``` 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) Save ```go func (j *Jar) Save() error ``` Save saves the cookies to the persistent cookie file. Before the file is written, it reads any cookies that have been stored from it and merges them into j. #### func (*Jar) SetCookies ```go func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) ``` SetCookies implements the SetCookies method of the http.CookieJar interface. It does nothing if the URL's scheme is not HTTP or HTTPS. #### type Options ```go 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 } ``` Options are the options for creating a new Jar. #### type PublicSuffixList ```go 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 } ``` 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. golang-github-juju-persistent-cookiejar-1.0.0/dependencies.tsv000066400000000000000000000010141414256507100245410ustar00rootroot00000000000000github.com/frankban/quicktest git 9332c2fb618ecbbb106b76699dd5bc78bd9856a3 2017-10-23T14:39:56Z github.com/google/go-cmp git 7ffe1921f7d789634416694ae7145ebbc1ac82b2 2017-10-05T19:31:44Z github.com/juju/go4 git 40d72ab9641a2a8c36a9c46a51e28367115c8e59 2016-02-22T16:32:58Z golang.org/x/net git b6d7b1396ec874c3b00f6c84cd4301a17c56c8ed 2016-02-17T01:13:48Z gopkg.in/errgo.v1 git 66cb46252b94c1f3d65646f54ee8043ab38d766c 2015-10-07T15:31:57Z gopkg.in/retry.v1 git c09f6b86ba4d5d2cf5bdf0665364aec9fd4815db 2016-10-25T18:14:30Z golang-github-juju-persistent-cookiejar-1.0.0/jar.go000066400000000000000000000460001414256507100224640ustar00rootroot00000000000000// 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 ( "errors" "fmt" "net" "net/http" "net/url" "os" "path/filepath" "runtime" "sort" "strings" "sync" "time" "golang.org/x/net/publicsuffix" "gopkg.in/errgo.v1" ) // 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, errgo.Notef(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") } golang-github-juju-persistent-cookiejar-1.0.0/jar_test.go000066400000000000000000001526671414256507100235440ustar00rootroot00000000000000// Copyright 2013 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 import ( "fmt" "io/ioutil" "net/http" "net/url" "os" "path/filepath" "regexp" "sort" "strings" "sync" "sync/atomic" "testing" "time" qt "github.com/frankban/quicktest" ) // tNow is the synthetic current time used as now during testing. var tNow = time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC) // testPSL implements PublicSuffixList with just two rules: "co.uk" // and the default rule "*". type testPSL struct{} func (testPSL) String() string { return "testPSL" } func (testPSL) PublicSuffix(d string) string { if d == "co.uk" || strings.HasSuffix(d, ".co.uk") { return "co.uk" } return d[strings.LastIndex(d, ".")+1:] } // emptyPSL implements PublicSuffixList with just the default // rule "*". type emptyPSL struct{} func (emptyPSL) String() string { return "emptyPSL" } func (emptyPSL) PublicSuffix(d string) string { return d[strings.LastIndex(d, ".")+1:] } // newTestJar creates an empty Jar with testPSL as the public suffix list. func newTestJar(path string) *Jar { jar, err := New(&Options{ PublicSuffixList: testPSL{}, Filename: path, NoPersist: path == "", }) if err != nil { panic(err) } return jar } var hasDotSuffixTests = [...]struct { s, suffix string }{ {"", ""}, {"", "."}, {"", "x"}, {".", ""}, {".", "."}, {".", ".."}, {".", "x"}, {".", "x."}, {".", ".x"}, {".", ".x."}, {"x", ""}, {"x", "."}, {"x", ".."}, {"x", "x"}, {"x", "x."}, {"x", ".x"}, {"x", ".x."}, {".x", ""}, {".x", "."}, {".x", ".."}, {".x", "x"}, {".x", "x."}, {".x", ".x"}, {".x", ".x."}, {"x.", ""}, {"x.", "."}, {"x.", ".."}, {"x.", "x"}, {"x.", "x."}, {"x.", ".x"}, {"x.", ".x."}, {"com", ""}, {"com", "m"}, {"com", "om"}, {"com", "com"}, {"com", ".com"}, {"com", "x.com"}, {"com", "xcom"}, {"com", "xorg"}, {"com", "org"}, {"com", "rg"}, {"foo.com", ""}, {"foo.com", "m"}, {"foo.com", "om"}, {"foo.com", "com"}, {"foo.com", ".com"}, {"foo.com", "o.com"}, {"foo.com", "oo.com"}, {"foo.com", "foo.com"}, {"foo.com", ".foo.com"}, {"foo.com", "x.foo.com"}, {"foo.com", "xfoo.com"}, {"foo.com", "xfoo.org"}, {"foo.com", "foo.org"}, {"foo.com", "oo.org"}, {"foo.com", "o.org"}, {"foo.com", ".org"}, {"foo.com", "org"}, {"foo.com", "rg"}, } func TestHasDotSuffix(t *testing.T) { for _, tc := range hasDotSuffixTests { got := hasDotSuffix(tc.s, tc.suffix) want := strings.HasSuffix(tc.s, "."+tc.suffix) if got != want { t.Errorf("s=%q, suffix=%q: got %v, want %v", tc.s, tc.suffix, got, want) } } } var canonicalHostTests = map[string]string{ "www.example.com": "www.example.com", "WWW.EXAMPLE.COM": "www.example.com", "wWw.eXAmple.CoM": "www.example.com", "www.example.com:80": "www.example.com", "192.168.0.10": "192.168.0.10", "192.168.0.5:8080": "192.168.0.5", "2001:4860:0:2001::68": "2001:4860:0:2001::68", "[2001:4860:0:::68]:8080": "2001:4860:0:::68", "www.bücher.de": "www.xn--bcher-kva.de", "www.example.com.": "www.example.com", "[bad.unmatched.bracket:": "error", } func TestCanonicalHost(t *testing.T) { for h, want := range canonicalHostTests { got, err := canonicalHost(h) if want == "error" { if err == nil { t.Errorf("%q: got nil error, want non-nil", h) } continue } if err != nil { t.Errorf("%q: %v", h, err) continue } if got != want { t.Errorf("%q: got %q, want %q", h, got, want) continue } } } var hasPortTests = map[string]bool{ "www.example.com": false, "www.example.com:80": true, "127.0.0.1": false, "127.0.0.1:8080": true, "2001:4860:0:2001::68": false, "[2001::0:::68]:80": true, } func TestHasPort(t *testing.T) { for host, want := range hasPortTests { if got := hasPort(host); got != want { t.Errorf("%q: got %t, want %t", host, got, want) } } } var jarKeyTests = map[string]string{ "foo.www.example.com": "example.com", "www.example.com": "example.com", "example.com": "example.com", "com": "com", "foo.www.bbc.co.uk": "bbc.co.uk", "www.bbc.co.uk": "bbc.co.uk", "bbc.co.uk": "bbc.co.uk", "co.uk": "co.uk", "uk": "uk", "192.168.0.5": "192.168.0.5", } func TestJarKey(t *testing.T) { for host, want := range jarKeyTests { if got := jarKey(host, testPSL{}); got != want { t.Errorf("%q: got %q, want %q", host, got, want) } } } var jarKeyNilPSLTests = map[string]string{ "foo.www.example.com": "example.com", "www.example.com": "example.com", "example.com": "example.com", "com": "com", "foo.www.bbc.co.uk": "co.uk", "www.bbc.co.uk": "co.uk", "bbc.co.uk": "co.uk", "co.uk": "co.uk", "uk": "uk", "192.168.0.5": "192.168.0.5", } func TestJarKeyNilPSL(t *testing.T) { for host, want := range jarKeyNilPSLTests { if got := jarKey(host, nil); got != want { t.Errorf("%q: got %q, want %q", host, got, want) } } } var isIPTests = map[string]bool{ "127.0.0.1": true, "1.2.3.4": true, "2001:4860:0:2001::68": true, "example.com": false, "1.1.1.300": false, "www.foo.bar.net": false, "123.foo.bar.net": false, } func TestIsIP(t *testing.T) { for host, want := range isIPTests { if got := isIP(host); got != want { t.Errorf("%q: got %t, want %t", host, got, want) } } } var defaultPathTests = map[string]string{ "/": "/", "/abc": "/", "/abc/": "/abc", "/abc/xyz": "/abc", "/abc/xyz/": "/abc/xyz", "/a/b/c.html": "/a/b", "": "/", "strange": "/", "//": "/", "/a//b": "/a/", "/a/./b": "/a/.", "/a/../b": "/a/..", } func TestDefaultPath(t *testing.T) { for path, want := range defaultPathTests { if got := defaultPath(path); got != want { t.Errorf("%q: got %q, want %q", path, got, want) } } } var domainAndTypeTests = [...]struct { host string // host Set-Cookie header was received from domain string // domain attribute in Set-Cookie header wantDomain string // expected domain of cookie wantHostOnly bool // expected host-cookie flag wantErr error // expected error }{ {"www.example.com", "", "www.example.com", true, nil}, {"127.0.0.1", "", "127.0.0.1", true, nil}, {"2001:4860:0:2001::68", "", "2001:4860:0:2001::68", true, nil}, {"www.example.com", "example.com", "example.com", false, nil}, {"www.example.com", ".example.com", "example.com", false, nil}, {"www.example.com", "www.example.com", "www.example.com", false, nil}, {"www.example.com", ".www.example.com", "www.example.com", false, nil}, {"foo.sso.example.com", "sso.example.com", "sso.example.com", false, nil}, {"bar.co.uk", "bar.co.uk", "bar.co.uk", false, nil}, {"foo.bar.co.uk", ".bar.co.uk", "bar.co.uk", false, nil}, {"127.0.0.1", "127.0.0.1", "", false, errNoHostname}, {"2001:4860:0:2001::68", "2001:4860:0:2001::68", "2001:4860:0:2001::68", false, errNoHostname}, {"www.example.com", ".", "", false, errMalformedDomain}, {"www.example.com", "..", "", false, errMalformedDomain}, {"www.example.com", "other.com", "", false, errIllegalDomain}, {"www.example.com", "com", "", false, errIllegalDomain}, {"www.example.com", ".com", "", false, errIllegalDomain}, {"foo.bar.co.uk", ".co.uk", "", false, errIllegalDomain}, {"127.www.0.0.1", "127.0.0.1", "", false, errIllegalDomain}, {"com", "", "com", true, nil}, {"com", "com", "com", true, nil}, {"com", ".com", "com", true, nil}, {"co.uk", "", "co.uk", true, nil}, {"co.uk", "co.uk", "co.uk", true, nil}, {"co.uk", ".co.uk", "co.uk", true, nil}, } func TestDomainAndType(t *testing.T) { jar := newTestJar("") for _, tc := range domainAndTypeTests { domain, hostOnly, err := jar.domainAndType(tc.host, tc.domain) if err != tc.wantErr { t.Errorf("%q/%q: got %q error, want %q", tc.host, tc.domain, err, tc.wantErr) continue } if err != nil { continue } if domain != tc.wantDomain || hostOnly != tc.wantHostOnly { t.Errorf("%q/%q: got %q/%t want %q/%t", tc.host, tc.domain, domain, hostOnly, tc.wantDomain, tc.wantHostOnly) } } } // basicsTests contains fundamental tests. Each jarTest has to be performed on // a fresh, empty Jar. var basicsTests = [...]jarTest{ { "Retrieval of a plain host cookie.", "http://www.host.test/", []string{"A=a"}, "A=a", []query{ {"http://www.host.test", "A=a"}, {"http://www.host.test/", "A=a"}, {"http://www.host.test/some/path", "A=a"}, {"https://www.host.test", "A=a"}, {"https://www.host.test/", "A=a"}, {"https://www.host.test/some/path", "A=a"}, {"ftp://www.host.test", ""}, {"ftp://www.host.test/", ""}, {"ftp://www.host.test/some/path", ""}, {"http://www.other.org", ""}, {"http://sibling.host.test", ""}, {"http://deep.www.host.test", ""}, }, }, { "Secure cookies are not returned to http.", "http://www.host.test/", []string{"A=a; secure"}, "A=a", []query{ {"http://www.host.test", ""}, {"http://www.host.test/", ""}, {"http://www.host.test/some/path", ""}, {"https://www.host.test", "A=a"}, {"https://www.host.test/", "A=a"}, {"https://www.host.test/some/path", "A=a"}, }, }, { "Explicit path.", "http://www.host.test/", []string{"A=a; path=/some/path"}, "A=a", []query{ {"http://www.host.test", ""}, {"http://www.host.test/", ""}, {"http://www.host.test/some", ""}, {"http://www.host.test/some/", ""}, {"http://www.host.test/some/path", "A=a"}, {"http://www.host.test/some/paths", ""}, {"http://www.host.test/some/path/foo", "A=a"}, {"http://www.host.test/some/path/foo/", "A=a"}, }, }, { "Implicit path #1: path is a directory.", "http://www.host.test/some/path/", []string{"A=a"}, "A=a", []query{ {"http://www.host.test", ""}, {"http://www.host.test/", ""}, {"http://www.host.test/some", ""}, {"http://www.host.test/some/", ""}, {"http://www.host.test/some/path", "A=a"}, {"http://www.host.test/some/paths", ""}, {"http://www.host.test/some/path/foo", "A=a"}, {"http://www.host.test/some/path/foo/", "A=a"}, }, }, { "Implicit path #2: path is not a directory.", "http://www.host.test/some/path/index.html", []string{"A=a"}, "A=a", []query{ {"http://www.host.test", ""}, {"http://www.host.test/", ""}, {"http://www.host.test/some", ""}, {"http://www.host.test/some/", ""}, {"http://www.host.test/some/path", "A=a"}, {"http://www.host.test/some/paths", ""}, {"http://www.host.test/some/path/foo", "A=a"}, {"http://www.host.test/some/path/foo/", "A=a"}, }, }, { "Implicit path #3: no path in URL at all.", "http://www.host.test", []string{"A=a"}, "A=a", []query{ {"http://www.host.test", "A=a"}, {"http://www.host.test/", "A=a"}, {"http://www.host.test/some/path", "A=a"}, }, }, { "Cookies are sorted by path length.", "http://www.host.test/", []string{ "A=a; path=/foo/bar", "B=b; path=/foo/bar/baz/qux", "C=c; path=/foo/bar/baz", "D=d; path=/foo"}, "A=a B=b C=c D=d", []query{ {"http://www.host.test/foo/bar/baz/qux", "B=b C=c A=a D=d"}, {"http://www.host.test/foo/bar/baz/", "C=c A=a D=d"}, {"http://www.host.test/foo/bar", "A=a D=d"}, }, }, // TODO fix this test. It has never actually tested sorting on // creation time because all the cookies are actually created at // the same moment in time. // { // "Creation time determines sorting on same length paths.", // "http://www.host.test/", // []string{ // "A=a; path=/foo/bar", // "X=x; path=/foo/bar", // "Y=y; path=/foo/bar/baz/qux", // "B=b; path=/foo/bar/baz/qux", // "C=c; path=/foo/bar/baz", // "W=w; path=/foo/bar/baz", // "Z=z; path=/foo", // "D=d; path=/foo"}, // "A=a B=b C=c D=d W=w X=x Y=y Z=z", // []query{ // {"http://www.host.test/foo/bar/baz/qux", "Y=y B=b C=c W=w A=a X=x Z=z D=d"}, // {"http://www.host.test/foo/bar/baz/", "C=c W=w A=a X=x Z=z D=d"}, // {"http://www.host.test/foo/bar", "A=a X=x Z=z D=d"}, // }, // }, { "Sorting of same-name cookies.", "http://www.host.test/", []string{ "A=1; path=/", "A=2; path=/path", "A=3; path=/quux", "A=4; path=/path/foo", "A=5; domain=.host.test; path=/path", "A=6; domain=.host.test; path=/quux", "A=7; domain=.host.test; path=/path/foo", }, "A=1 A=2 A=3 A=4 A=5 A=6 A=7", []query{ {"http://www.host.test/path", "A=2 A=5 A=1"}, {"http://www.host.test/path/foo", "A=4 A=7 A=2 A=5 A=1"}, }, }, { "Disallow domain cookie on public suffix.", "http://www.bbc.co.uk", []string{ "a=1", "b=2; domain=co.uk", }, "a=1", []query{{"http://www.bbc.co.uk", "a=1"}}, }, { "Host cookie on IP.", "http://192.168.0.10", []string{"a=1"}, "a=1", []query{{"http://192.168.0.10", "a=1"}}, }, { "Port is ignored #1.", "http://www.host.test/", []string{"a=1"}, "a=1", []query{ {"http://www.host.test", "a=1"}, {"http://www.host.test:8080/", "a=1"}, }, }, { "Port is ignored #2.", "http://www.host.test:8080/", []string{"a=1"}, "a=1", []query{ {"http://www.host.test", "a=1"}, {"http://www.host.test:8080/", "a=1"}, {"http://www.host.test:1234/", "a=1"}, }, }, } func TestBasics(t *testing.T) { for _, test := range basicsTests { jar := newTestJar("") test.run(t, jar) } } // updateAndDeleteTests contains jarTests which must be performed on the same // Jar. var updateAndDeleteTests = [...]jarTest{ { "Set initial cookies.", "http://www.host.test", []string{ "a=1", "b=2; secure", "c=3; httponly", "d=4; secure; httponly"}, "a=1 b=2 c=3 d=4", []query{ {"http://www.host.test", "a=1 c=3"}, {"https://www.host.test", "a=1 b=2 c=3 d=4"}, }, }, { "Update value via http.", "http://www.host.test", []string{ "a=w", "b=x; secure", "c=y; httponly", "d=z; secure; httponly"}, "a=w b=x c=y d=z", []query{ {"http://www.host.test", "a=w c=y"}, {"https://www.host.test", "a=w b=x c=y d=z"}, }, }, { "Clear Secure flag from a http.", "http://www.host.test/", []string{ "b=xx", "d=zz; httponly"}, "a=w b=xx c=y d=zz", []query{{"http://www.host.test", "a=w b=xx c=y d=zz"}}, }, { "Delete all.", "http://www.host.test/", []string{ "a=1; max-Age=-1", // delete via MaxAge "b=2; " + expiresIn(-10), // delete via Expires "c=2; max-age=-1; " + expiresIn(-10), // delete via both "d=4; max-age=-1; " + expiresIn(10)}, // MaxAge takes precedence "", []query{{"http://www.host.test", ""}}, }, { "Refill #1.", "http://www.host.test", []string{ "A=1", "A=2; path=/foo", "A=3; domain=.host.test", "A=4; path=/foo; domain=.host.test"}, "A=1 A=2 A=3 A=4", []query{{"http://www.host.test/foo", "A=2 A=4 A=1 A=3"}}, }, { "Refill #2.", "http://www.google.com", []string{ "A=6", "A=7; path=/foo", "A=8; domain=.google.com", "A=9; path=/foo; domain=.google.com"}, "A=1 A=2 A=3 A=4 A=6 A=7 A=8 A=9", []query{ {"http://www.host.test/foo", "A=2 A=4 A=1 A=3"}, {"http://www.google.com/foo", "A=7 A=9 A=6 A=8"}, }, }, { "Delete A7.", "http://www.google.com", []string{"A=; path=/foo; max-age=-1"}, "A=1 A=2 A=3 A=4 A=6 A=8 A=9", []query{ {"http://www.host.test/foo", "A=2 A=4 A=1 A=3"}, {"http://www.google.com/foo", "A=9 A=6 A=8"}, }, }, { "Delete A4.", "http://www.host.test", []string{"A=; path=/foo; domain=host.test; max-age=-1"}, "A=1 A=2 A=3 A=6 A=8 A=9", []query{ {"http://www.host.test/foo", "A=2 A=1 A=3"}, {"http://www.google.com/foo", "A=9 A=6 A=8"}, }, }, { "Delete A6.", "http://www.google.com", []string{"A=; max-age=-1"}, "A=1 A=2 A=3 A=8 A=9", []query{ {"http://www.host.test/foo", "A=2 A=1 A=3"}, {"http://www.google.com/foo", "A=9 A=8"}, }, }, { "Delete A3.", "http://www.host.test", []string{"A=; domain=host.test; max-age=-1"}, "A=1 A=2 A=8 A=9", []query{ {"http://www.host.test/foo", "A=2 A=1"}, {"http://www.google.com/foo", "A=9 A=8"}, }, }, { "No cross-domain delete.", "http://www.host.test", []string{ "A=; domain=google.com; max-age=-1", "A=; path=/foo; domain=google.com; max-age=-1"}, "A=1 A=2 A=8 A=9", []query{ {"http://www.host.test/foo", "A=2 A=1"}, {"http://www.google.com/foo", "A=9 A=8"}, }, }, { "Delete A8 and A9.", "http://www.google.com", []string{ "A=; domain=google.com; max-age=-1", "A=; path=/foo; domain=google.com; max-age=-1"}, "A=1 A=2", []query{ {"http://www.host.test/foo", "A=2 A=1"}, {"http://www.google.com/foo", ""}, }, }, } func TestUpdateAndDelete(t *testing.T) { jar := newTestJar("") for _, test := range updateAndDeleteTests { test.run(t, jar) } } func TestExpiration(t *testing.T) { jar := newTestJar("") jarTest{ "Expiration.", "http://www.host.test", []string{ "a=1", "b=2; max-age=3", "c=3; " + expiresIn(3), "d=4; max-age=5", "e=5; " + expiresIn(5), "f=6; max-age=100", }, "a=1 b=2 c=3 d=4 e=5 f=6", // executed at t0 + 1001 ms []query{ {"http://www.host.test", "a=1 b=2 c=3 d=4 e=5 f=6"}, // t0 + 2002 ms {"http://www.host.test", "a=1 d=4 e=5 f=6"}, // t0 + 3003 ms {"http://www.host.test", "a=1 d=4 e=5 f=6"}, // t0 + 4004 ms {"http://www.host.test", "a=1 f=6"}, // t0 + 5005 ms {"http://www.host.test", "a=1 f=6"}, // t0 + 6006 ms }, }.run(t, jar) } // // Tests derived from Chromium's cookie_store_unittest.h. // // See http://src.chromium.org/viewvc/chrome/trunk/src/net/cookies/cookie_store_unittest.h?revision=159685&content-type=text/plain // Some of the original tests are in a bad condition (e.g. // DomainWithTrailingDotTest) or are not RFC 6265 conforming (e.g. // TestNonDottedAndTLD #1 and #6) and have not been ported. // chromiumBasicsTests contains fundamental tests. Each jarTest has to be // performed on a fresh, empty Jar. var chromiumBasicsTests = [...]jarTest{ { "DomainWithTrailingDotTest.", "http://www.google.com/", []string{ "a=1; domain=.www.google.com.", "b=2; domain=.www.google.com..", }, "", []query{ {"http://www.google.com", ""}, }, }, { "ValidSubdomainTest #1.", "http://a.b.c.d.com", []string{ "a=1; domain=.a.b.c.d.com", "b=2; domain=.b.c.d.com", "c=3; domain=.c.d.com", "d=4; domain=.d.com", }, "a=1 b=2 c=3 d=4", []query{ {"http://a.b.c.d.com", "a=1 b=2 c=3 d=4"}, {"http://b.c.d.com", "b=2 c=3 d=4"}, {"http://c.d.com", "c=3 d=4"}, {"http://d.com", "d=4"}, }, }, { "ValidSubdomainTest #2.", "http://a.b.c.d.com", []string{ "a=1; domain=.a.b.c.d.com", "b=2; domain=.b.c.d.com", "c=3; domain=.c.d.com", "d=4; domain=.d.com", "X=bcd; domain=.b.c.d.com", "X=cd; domain=.c.d.com"}, "X=bcd X=cd a=1 b=2 c=3 d=4", []query{ {"http://b.c.d.com", "X=bcd X=cd b=2 c=3 d=4"}, {"http://c.d.com", "X=cd c=3 d=4"}, }, }, { "InvalidDomainTest #1.", "http://foo.bar.com", []string{ "a=1; domain=.yo.foo.bar.com", "b=2; domain=.foo.com", "c=3; domain=.bar.foo.com", "d=4; domain=.foo.bar.com.net", "e=5; domain=ar.com", "f=6; domain=.", "g=7; domain=/", "h=8; domain=http://foo.bar.com", "i=9; domain=..foo.bar.com", "j=10; domain=..bar.com", "k=11; domain=.foo.bar.com?blah", "l=12; domain=.foo.bar.com/blah", "m=12; domain=.foo.bar.com:80", "n=14; domain=.foo.bar.com:", "o=15; domain=.foo.bar.com#sup", }, "", // Jar is empty. []query{{"http://foo.bar.com", ""}}, }, { "InvalidDomainTest #2.", "http://foo.com.com", []string{"a=1; domain=.foo.com.com.com"}, "", []query{{"http://foo.bar.com", ""}}, }, { "DomainWithoutLeadingDotTest #1.", "http://manage.hosted.filefront.com", []string{"a=1; domain=filefront.com"}, "a=1", []query{{"http://www.filefront.com", "a=1"}}, }, { "DomainWithoutLeadingDotTest #2.", "http://www.google.com", []string{"a=1; domain=www.google.com"}, "a=1", []query{ {"http://www.google.com", "a=1"}, {"http://sub.www.google.com", "a=1"}, {"http://something-else.com", ""}, }, }, { "CaseInsensitiveDomainTest.", "http://www.google.com", []string{ "a=1; domain=.GOOGLE.COM", "b=2; domain=.www.gOOgLE.coM"}, "a=1 b=2", []query{{"http://www.google.com", "a=1 b=2"}}, }, { "TestIpAddress #1.", "http://1.2.3.4/foo", []string{"a=1; path=/"}, "a=1", []query{{"http://1.2.3.4/foo", "a=1"}}, }, { "TestIpAddress #2.", "http://1.2.3.4/foo", []string{ "a=1; domain=.1.2.3.4", "b=2; domain=.3.4"}, "", []query{{"http://1.2.3.4/foo", ""}}, }, { "TestIpAddress #3.", "http://1.2.3.4/foo", []string{"a=1; domain=1.2.3.4"}, "", []query{{"http://1.2.3.4/foo", ""}}, }, { "TestNonDottedAndTLD #2.", "http://com./index.html", []string{"a=1"}, "a=1", []query{ {"http://com./index.html", "a=1"}, {"http://no-cookies.com./index.html", ""}, }, }, { "TestNonDottedAndTLD #3.", "http://a.b", []string{ "a=1; domain=.b", "b=2; domain=b"}, "", []query{{"http://bar.foo", ""}}, }, { "TestNonDottedAndTLD #4.", "http://google.com", []string{ "a=1; domain=.com", "b=2; domain=com"}, "", []query{{"http://google.com", ""}}, }, { "TestNonDottedAndTLD #5.", "http://google.co.uk", []string{ "a=1; domain=.co.uk", "b=2; domain=.uk"}, "", []query{ {"http://google.co.uk", ""}, {"http://else.co.com", ""}, {"http://else.uk", ""}, }, }, { "TestHostEndsWithDot.", "http://www.google.com", []string{ "a=1", "b=2; domain=.www.google.com."}, "a=1", []query{{"http://www.google.com", "a=1"}}, }, { "PathTest", "http://www.google.izzle", []string{"a=1; path=/wee"}, "a=1", []query{ {"http://www.google.izzle/wee", "a=1"}, {"http://www.google.izzle/wee/", "a=1"}, {"http://www.google.izzle/wee/war", "a=1"}, {"http://www.google.izzle/wee/war/more/more", "a=1"}, {"http://www.google.izzle/weehee", ""}, {"http://www.google.izzle/", ""}, }, }, } func TestChromiumBasics(t *testing.T) { for _, test := range chromiumBasicsTests { jar := newTestJar("") test.run(t, jar) } } // chromiumDomainTests contains jarTests which must be executed all on the // same Jar. var chromiumDomainTests = [...]jarTest{ { "Fill #1.", "http://www.google.izzle", []string{"A=B"}, "A=B", []query{{"http://www.google.izzle", "A=B"}}, }, { "Fill #2.", "http://www.google.izzle", []string{"C=D; domain=.google.izzle"}, "A=B C=D", []query{{"http://www.google.izzle", "A=B C=D"}}, }, { "Verify A is a host cookie and not accessible from subdomain.", "http://unused.nil", []string{}, "A=B C=D", []query{{"http://foo.www.google.izzle", "C=D"}}, }, { "Verify domain cookies are found on proper domain.", "http://www.google.izzle", []string{"E=F; domain=.www.google.izzle"}, "A=B C=D E=F", []query{{"http://www.google.izzle", "A=B C=D E=F"}}, }, { "Leading dots in domain attributes are optional.", "http://www.google.izzle", []string{"G=H; domain=www.google.izzle"}, "A=B C=D E=F G=H", []query{{"http://www.google.izzle", "A=B C=D E=F G=H"}}, }, { "Verify domain enforcement works #1.", "http://www.google.izzle", []string{"K=L; domain=.bar.www.google.izzle"}, "A=B C=D E=F G=H", []query{{"http://bar.www.google.izzle", "C=D E=F G=H"}}, }, { "Verify domain enforcement works #2.", "http://unused.nil", []string{}, "A=B C=D E=F G=H", []query{{"http://www.google.izzle", "A=B C=D E=F G=H"}}, }, } func TestChromiumDomain(t *testing.T) { jar := newTestJar("") for _, test := range chromiumDomainTests { test.run(t, jar) } } // chromiumDeletionTests must be performed all on the same Jar. var chromiumDeletionTests = [...]jarTest{ { "Create session cookie a1.", "http://www.google.com", []string{"a=1"}, "a=1", []query{{"http://www.google.com", "a=1"}}, }, { "Delete sc a1 via MaxAge.", "http://www.google.com", []string{"a=1; max-age=-1"}, "", []query{{"http://www.google.com", ""}}, }, { "Create session cookie b2.", "http://www.google.com", []string{"b=2"}, "b=2", []query{{"http://www.google.com", "b=2"}}, }, { "Delete sc b2 via Expires.", "http://www.google.com", []string{"b=2; " + expiresIn(-10)}, "", []query{{"http://www.google.com", ""}}, }, { "Create persistent cookie c3.", "http://www.google.com", []string{"c=3; max-age=3600"}, "c=3", []query{{"http://www.google.com", "c=3"}}, }, { "Delete pc c3 via MaxAge.", "http://www.google.com", []string{"c=3; max-age=-1"}, "", []query{{"http://www.google.com", ""}}, }, { "Create persistent cookie d4.", "http://www.google.com", []string{"d=4; max-age=3600"}, "d=4", []query{{"http://www.google.com", "d=4"}}, }, { "Delete pc d4 via Expires.", "http://www.google.com", []string{"d=4; " + expiresIn(-10)}, "", []query{{"http://www.google.com", ""}}, }, } func TestChromiumDeletion(t *testing.T) { jar := newTestJar("") for _, test := range chromiumDeletionTests { test.run(t, jar) } } // domainHandlingTests tests and documents the rules for domain handling. // Each test must be performed on an empty new Jar. var domainHandlingTests = [...]jarTest{ { "Host cookie", "http://www.host.test", []string{"a=1"}, "a=1", []query{ {"http://www.host.test", "a=1"}, {"http://host.test", ""}, {"http://bar.host.test", ""}, {"http://foo.www.host.test", ""}, {"http://other.test", ""}, {"http://test", ""}, }, }, { "Domain cookie #1", "http://www.host.test", []string{"a=1; domain=host.test"}, "a=1", []query{ {"http://www.host.test", "a=1"}, {"http://host.test", "a=1"}, {"http://bar.host.test", "a=1"}, {"http://foo.www.host.test", "a=1"}, {"http://other.test", ""}, {"http://test", ""}, }, }, { "Domain cookie #2", "http://www.host.test", []string{"a=1; domain=.host.test"}, "a=1", []query{ {"http://www.host.test", "a=1"}, {"http://host.test", "a=1"}, {"http://bar.host.test", "a=1"}, {"http://foo.www.host.test", "a=1"}, {"http://other.test", ""}, {"http://test", ""}, }, }, { "Host cookie on IDNA domain #1", "http://www.bücher.test", []string{"a=1"}, "a=1", []query{ {"http://www.bücher.test", "a=1"}, {"http://www.xn--bcher-kva.test", "a=1"}, {"http://bücher.test", ""}, {"http://xn--bcher-kva.test", ""}, {"http://bar.bücher.test", ""}, {"http://bar.xn--bcher-kva.test", ""}, {"http://foo.www.bücher.test", ""}, {"http://foo.www.xn--bcher-kva.test", ""}, {"http://other.test", ""}, {"http://test", ""}, }, }, { "Host cookie on IDNA domain #2", "http://www.xn--bcher-kva.test", []string{"a=1"}, "a=1", []query{ {"http://www.bücher.test", "a=1"}, {"http://www.xn--bcher-kva.test", "a=1"}, {"http://bücher.test", ""}, {"http://xn--bcher-kva.test", ""}, {"http://bar.bücher.test", ""}, {"http://bar.xn--bcher-kva.test", ""}, {"http://foo.www.bücher.test", ""}, {"http://foo.www.xn--bcher-kva.test", ""}, {"http://other.test", ""}, {"http://test", ""}, }, }, { "Domain cookie on IDNA domain #1", "http://www.bücher.test", []string{"a=1; domain=xn--bcher-kva.test"}, "a=1", []query{ {"http://www.bücher.test", "a=1"}, {"http://www.xn--bcher-kva.test", "a=1"}, {"http://bücher.test", "a=1"}, {"http://xn--bcher-kva.test", "a=1"}, {"http://bar.bücher.test", "a=1"}, {"http://bar.xn--bcher-kva.test", "a=1"}, {"http://foo.www.bücher.test", "a=1"}, {"http://foo.www.xn--bcher-kva.test", "a=1"}, {"http://other.test", ""}, {"http://test", ""}, }, }, { "Domain cookie on IDNA domain #2", "http://www.xn--bcher-kva.test", []string{"a=1; domain=xn--bcher-kva.test"}, "a=1", []query{ {"http://www.bücher.test", "a=1"}, {"http://www.xn--bcher-kva.test", "a=1"}, {"http://bücher.test", "a=1"}, {"http://xn--bcher-kva.test", "a=1"}, {"http://bar.bücher.test", "a=1"}, {"http://bar.xn--bcher-kva.test", "a=1"}, {"http://foo.www.bücher.test", "a=1"}, {"http://foo.www.xn--bcher-kva.test", "a=1"}, {"http://other.test", ""}, {"http://test", ""}, }, }, { "Host cookie on TLD.", "http://com", []string{"a=1"}, "a=1", []query{ {"http://com", "a=1"}, {"http://any.com", ""}, {"http://any.test", ""}, }, }, { "Domain cookie on TLD becomes a host cookie.", "http://com", []string{"a=1; domain=com"}, "a=1", []query{ {"http://com", "a=1"}, {"http://any.com", ""}, {"http://any.test", ""}, }, }, { "Host cookie on public suffix.", "http://co.uk", []string{"a=1"}, "a=1", []query{ {"http://co.uk", "a=1"}, {"http://uk", ""}, {"http://some.co.uk", ""}, {"http://foo.some.co.uk", ""}, {"http://any.uk", ""}, }, }, { "Domain cookie on public suffix is ignored.", "http://some.co.uk", []string{"a=1; domain=co.uk"}, "", []query{ {"http://co.uk", ""}, {"http://uk", ""}, {"http://some.co.uk", ""}, {"http://foo.some.co.uk", ""}, {"http://any.uk", ""}, }, }, } func TestDomainHandling(t *testing.T) { for _, test := range domainHandlingTests { jar := newTestJar("") test.run(t, jar) } } type mergeCookie struct { when time.Time url string cookie string } func (c mergeCookie) set(jar *Jar) { setCookies(jar, c.url, []string{c.cookie}, c.when) } var mergeTests = []struct { description string setCookies0 []mergeCookie setCookies1 []mergeCookie now time.Time content string queries []query // Queries to test the Jar.Cookies method }{{ description: "empty jar1", setCookies0: []mergeCookie{ {atTime(0), "http://www.host.test", "A=a; max-age=10"}, }, now: atTime(1), content: "A=a", }, { description: "empty jar0", setCookies1: []mergeCookie{ {atTime(0), "http://www.host.test", "A=a; max-age=10"}, }, now: atTime(1), content: "A=a", }, { description: "simple override (1)", setCookies0: []mergeCookie{ {atTime(0), "http://www.host.test", "A=a; max-age=10"}, }, setCookies1: []mergeCookie{ {atTime(1), "http://www.host.test", "A=b; max-age=10"}, }, now: atTime(2), content: "A=b", }, { description: "simple override (2)", setCookies0: []mergeCookie{ {atTime(1), "http://www.host.test", "A=a; max-age=10"}, }, setCookies1: []mergeCookie{ {atTime(0), "http://www.host.test", "A=b; max-age=10"}, }, now: atTime(2), content: "A=a", }, { description: "expired cookie overrides unexpired cookie", setCookies0: []mergeCookie{ {atTime(1), "http://www.host.test", "A=a; max-age=-1"}, }, setCookies1: []mergeCookie{ {atTime(0), "http://www.host.test", "A=b; max-age=10"}, }, now: atTime(2), content: "", }, { description: "set overrides expires", setCookies0: []mergeCookie{ {atTime(1), "http://www.host.test", "A=a; max-age=10"}, }, setCookies1: []mergeCookie{ {atTime(0), "http://www.host.test", "A=b; max-age=-1"}, }, now: atTime(2), content: "A=a", }, { description: "expiry times preserved", setCookies0: []mergeCookie{ {atTime(1), "http://www.host.test", "A=a; " + expiresIn(5)}, }, setCookies1: []mergeCookie{ {atTime(0), "http://www.host.test", "B=b; " + expiresIn(4)}, }, now: atTime(2), content: "A=a B=b", queries: []query{ {"http://www.host.test", "B=b A=a"}, {"http://www.host.test", "A=a"}, {"http://www.host.test", ""}, }, }, { description: "prefer receiver when creation times are identical", setCookies0: []mergeCookie{ {atTime(0), "http://www.host.test", "A=a; max-age=10"}, }, setCookies1: []mergeCookie{ {atTime(0), "http://www.host.test", "A=b; max-age=10"}, }, now: atTime(2), content: "A=a", }, { description: "max-age is persistent even when negative", setCookies0: []mergeCookie{ {atTime(0), "http://www.host.test", "A=a; max-age=10"}, }, setCookies1: []mergeCookie{ {atTime(1), "http://www.host.test", "A=b; max-age=-1"}, }, now: atTime(2), content: "", }, { description: "expires is persistent even when in the past", setCookies0: []mergeCookie{ {atTime(0), "http://www.host.test", "A=a; " + expiresIn(2)}, }, setCookies1: []mergeCookie{ {atTime(1), "http://www.host.test", "A=b; " + expiresIn(-1)}, }, now: atTime(2), content: "", }, { description: "many hosts", setCookies0: []mergeCookie{ {atTime(1), "http://www.host.test", "A=a0; max-age=10"}, {atTime(2), "http://www.host.test/foo/", "A=foo0; max-age=10"}, {atTime(1), "http://www.elsewhere", "X=x; max-age=10"}, }, setCookies1: []mergeCookie{ {atTime(1), "http://www.host.test", "A=a1; max-age=10"}, {atTime(3), "http://www.host.test", "B=b; max-age=10"}, {atTime(1), "http://www.host.test/foo/", "A=foo1; max-age=10"}, {atTime(0), "http://www.host.test/foo/", "C=arble; max-age=10"}, {atTime(1), "http://nowhere.com", "A=n; max-age=10"}, }, now: atTime(2), content: "A=a0 A=foo0 A=n B=b C=arble X=x", queries: []query{ {"http://www.host.test/", "A=a0 B=b"}, {"http://www.host.test/foo/", "C=arble A=foo0 A=a0 B=b"}, {"http://nowhere.com", "A=n"}, {"http://www.elsewhere", "X=x"}, }, }} func TestSaveMerge(t *testing.T) { dir, err := ioutil.TempDir("", "cookiejar-test") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) for i, test := range mergeTests { path := filepath.Join(dir, fmt.Sprintf("jar%d", i)) jar0 := newTestJar(path) for _, sc := range test.setCookies0 { sc.set(jar0) } jar1 := newTestJar(path) for _, sc := range test.setCookies1 { sc.set(jar1) } err := jar1.save(test.now) if err != nil { t.Fatalf("Test %q; cannot save first jar: %v", test.description, err) } err = jar0.save(test.now) if err != nil { t.Fatalf("Test %q; cannot save: %v", test.description, err) } got := allCookies(jar0, test.now) // Make sure jar content matches our expectations. if got != test.content { t.Logf("entries: %#v", jar0.entries) t.Errorf("Test %q Content\ngot %q\nwant %q", test.description, got, test.content) } testQueries(t, test.queries, test.description, jar0, test.now) } } func TestMergeConcurrent(t *testing.T) { // This test is designed to fail when run with the race // detector. The actual final content of the jars is non-deterministic // so we don't test that. const N = 10 f, err := ioutil.TempFile("", "cookiejar-test") if err != nil { t.Fatal(err) } defer os.Remove(f.Name()) defer f.Close() jar0 := newTestJar(f.Name()) jar1 := newTestJar(f.Name()) var wg sync.WaitGroup url := mustParseURL("http://foo.com") merger := func(j *Jar) { defer wg.Done() for i := 0; i < N; i++ { j.Save() } } getter := func(j *Jar) { defer wg.Done() for i := 0; i < N; i++ { j.Cookies(url) } } setter := func(j *Jar, what string) { defer wg.Done() for i := 0; i < N; i++ { setCookies(j, url.String(), []string{fmt.Sprintf("A=a%s%d; max-age=10", what, i)}, time.Now()) } } wg.Add(1) go merger(jar1) wg.Add(1) go merger(jar0) wg.Add(1) go getter(jar0) wg.Add(1) go getter(jar1) wg.Add(1) go setter(jar0, "first") wg.Add(1) go setter(jar1, "second") wg.Wait() } func TestDeleteExpired(t *testing.T) { expirySeconds := int(expiryRemovalDuration / time.Second) jar := newTestJar("") now := tNow setCookies(jar, "http://foo.com", []string{ "a=a; " + expiresIn(1), "b=b; " + expiresIn(expirySeconds+3), }, tNow) setCookies(jar, "http://bar.com", []string{ "c=c; " + expiresIn(1), }, tNow) // Make sure all the cookies are there to start with. got := allCookies(jar, now) want := "a=a b=b c=c" // Make sure jar content matches our expectations. if got != want { t.Errorf("Unexpected content\ngot %q\nwant %q", got, want) } now = now.Add(expiryRemovalDuration - time.Millisecond) // Ensure that they've timed out but their entries // are still around before the cutoff period. jar.deleteExpired(now) got = allCookiesIncludingExpired(jar, now) want = "a= b=b c=" if got != want { t.Errorf("Unexpected content\ngot %q\nwant %q", got, want) } // Try just after the expiry duration. The entries should really have // been removed now. now = now.Add(2 * time.Millisecond) jar.deleteExpired(now) got = allCookiesIncludingExpired(jar, now) want = "b=b" if got != want { t.Errorf("Unexpected content\ngot %q\nwant %q", got, want) } } var serializeTestCookies = []*http.Cookie{{ Name: "foo", Value: "bar", Path: "/p", Domain: "example.com", Expires: time.Now(), RawExpires: time.Now().Format(time.RFC3339Nano), MaxAge: 99, Secure: true, HttpOnly: true, Raw: "raw string", Unparsed: []string{"x", "y", "z"}, }} var serializeTestURL, _ = url.Parse("http://example.com/x") func TestLoadSave(t *testing.T) { c := qt.New(t) d, err := ioutil.TempDir("", "") c.Assert(err, qt.Equals, nil) defer os.RemoveAll(d) file := filepath.Join(d, "cookies") j := newTestJar(file) j.SetCookies(serializeTestURL, serializeTestCookies) err = j.Save() c.Assert(err, qt.Equals, nil) _, err = os.Stat(file) c.Assert(err, qt.Equals, nil) j1 := newTestJar(file) c.Assert(len(j1.entries), qt.Equals, len(serializeTestCookies)) c.Assert(j1.entries, qt.DeepEquals, j.entries) } func TestMarshalJSON(t *testing.T) { c := qt.New(t) j := newTestJar("") j.SetCookies(serializeTestURL, serializeTestCookies) // Marshal the cookies. data, err := j.MarshalJSON() c.Assert(err, qt.Equals, nil) // Save them to disk. d, err := ioutil.TempDir("", "") c.Assert(err, qt.Equals, nil) defer os.RemoveAll(d) file := filepath.Join(d, "cookies") err = ioutil.WriteFile(file, data, 0600) c.Assert(err, qt.Equals, nil) // Load cookies from the file. j1 := newTestJar(file) c.Assert(len(j1.entries), qt.Equals, len(serializeTestCookies)) c.Assert(j1.entries, qt.DeepEquals, j.entries) } func TestLoadSaveWithNoPersist(t *testing.T) { // Create a cookie file so that we can verify // that it's not read when NoPersist is set. d, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("cannot make temp dir: %v", err) } defer os.RemoveAll(d) file := filepath.Join(d, "cookies") j := newTestJar(file) j.SetCookies(serializeTestURL, serializeTestCookies) if err := j.Save(); err != nil { t.Fatalf("cannot save: %v", err) } jar, err := New(&Options{ PublicSuffixList: testPSL{}, Filename: file, NoPersist: true, }) if err != nil { t.Fatal(err) } if got := allCookiesIncludingExpired(jar, tNow); got != "" { t.Errorf("Cookies unexpectedly loaded: %v", got) } if err := os.Remove(file); err != nil { t.Fatal(err) } if err := jar.Save(); err != nil { t.Fatal(err) } if _, err := os.Stat(file); err == nil { t.Fatalf("file was unexpectedly saved") } } func TestLoadNonExistentParent(t *testing.T) { d, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("cannot make temp dir: %v", err) } defer os.RemoveAll(d) file := filepath.Join(d, "foo", "cookies") _, err = New(&Options{ PublicSuffixList: testPSL{}, Filename: file, }) if err != nil { t.Fatalf("cannot make cookie jar: %v", err) } } func TestLoadNonExistentParentOfParent(t *testing.T) { d, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("cannot make temp dir: %v", err) } defer os.RemoveAll(d) file := filepath.Join(d, "foo", "foo", "cookies") _, err = New(&Options{ PublicSuffixList: testPSL{}, Filename: file, }) if err != nil { t.Fatalf("cannot make cookie jar: %v", err) } } func TestLoadOldFormat(t *testing.T) { // Check that loading the old format (a JSON object) // doesn't result in an error. f, err := ioutil.TempFile("", "cookiejar-test") if err != nil { t.Fatal(err) } defer os.Remove(f.Name()) f.Write([]byte("{}")) f.Close() jar, err := New(&Options{ Filename: f.Name(), }) if err != nil { t.Errorf("got error: %v", err) } if jar == nil { t.Errorf("nil jar") } } func TestLoadInvalidJSON(t *testing.T) { f, err := ioutil.TempFile("", "cookiejar-test") if err != nil { t.Fatal(err) } defer os.Remove(f.Name()) f.Write([]byte("[")) f.Close() jar, err := New(&Options{ Filename: f.Name(), }) if err == nil { t.Fatalf("expected error, got none") } want := "cannot load cookies: unexpected EOF" if ok, _ := regexp.MatchString(want, err.Error()); !ok { t.Fatalf("unexpected error message; want %q got %q", want, err.Error()) } if jar != nil { t.Fatalf("got nil jar") } } func TestLoadDifferentPublicSuffixList(t *testing.T) { f, err := ioutil.TempFile("", "cookiejar-test") if err != nil { t.Fatal(err) } f.Close() defer os.Remove(f.Name()) now := tNow // With no public suffix list, some domains that should be // separate can set cookies for each other. jar, err := newAtTime(&Options{ Filename: f.Name(), PublicSuffixList: emptyPSL{}, }, now) if err != nil { t.Fatal(err) } setCookies(jar, "http://foo.co.uk", []string{ "a=a; max-age=10; domain=.co.uk", }, now) setCookies(jar, "http://bar.co.uk", []string{ "b=b; max-age=10; domain=.co.uk", }, now) // With the default public suffix, the cookies are // correctly segmented into their proper domains. queries := []query{ {"http://foo.co.uk/", "a=a b=b"}, {"http://bar.co.uk/", "a=a b=b"}, } testQueries(t, queries, "no public suffix list", jar, now) if err := jar.save(now); err != nil { t.Fatalf("cannot save jar: %v", err) } jar, err = newAtTime(&Options{ Filename: f.Name(), PublicSuffixList: testPSL{}, }, now) if err != nil { t.Fatal(err) } queries = []query{ {"http://foo.co.uk/", "a=a"}, {"http://bar.co.uk/", "b=b"}, } testQueries(t, queries, "with test public suffix list", jar, now) if err := jar.save(now); err != nil { t.Fatalf("cannot save jar: %v", err) } // When we reload with the original (empty) public suffix // we get all the original cookies back. jar, err = newAtTime(&Options{ Filename: f.Name(), PublicSuffixList: emptyPSL{}, }, now) if err != nil { t.Fatal(err) } queries = []query{ {"http://foo.co.uk/", "a=a b=b"}, {"http://bar.co.uk/", "a=a b=b"}, } testQueries(t, queries, "no public suffix list #2", jar, now) if err := jar.save(now); err != nil { t.Fatalf("cannot save jar: %v", err) } } func TestLockFile(t *testing.T) { d, err := ioutil.TempDir("", "cookiejar_test") if err != nil { t.Fatal(err) } defer os.RemoveAll(d) filename := filepath.Join(d, "lockfile") concurrentCount := int64(0) var wg sync.WaitGroup locker := func() { defer wg.Done() closer, err := lockFile(filename) if err != nil { t.Errorf("cannot obtain lock: %v", err) return } x := atomic.AddInt64(&concurrentCount, 1) if x > 1 { t.Errorf("multiple locks held at one time") } defer closer.Close() time.Sleep(10 * time.Millisecond) atomic.AddInt64(&concurrentCount, -1) } wg.Add(4) for i := 0; i < 4; i++ { go locker() } wg.Wait() if concurrentCount != 0 { t.Errorf("expected no running goroutines left") } } // jarTest encapsulates the following actions on a jar: // 1. Perform SetCookies with fromURL and the cookies from setCookies. // (Done at time tNow + 0 ms.) // 2. Check that the entries in the jar matches content. // (Done at time tNow + 1001 ms.) // 3. For each query in tests: Check that Cookies with toURL yields the // cookies in want. // (Query n done at tNow + (n+2)*1001 ms.) type jarTest struct { description string // The description of what this test is supposed to test fromURL string // The full URL of the request from which Set-Cookie headers where received setCookies []string // All the cookies received from fromURL content string // The whole (non-expired) content of the jar queries []query // Queries to test the Jar.Cookies method } // query contains one test of the cookies returned from Jar.Cookies. type query struct { toURL string // the URL in the Cookies call want string // the expected list of cookies (order matters) } // run runs the jarTest. func (test jarTest) run(t *testing.T, jar *Jar) { now := tNow // Populate jar with cookies. setCookies(jar, test.fromURL, test.setCookies, now) now = now.Add(1001 * time.Millisecond) got := allCookies(jar, now) // Make sure jar content matches our expectations. if got != test.content { t.Errorf("Test %q Content\ngot %q\nwant %q", test.description, got, test.content) } testQueries(t, test.queries, test.description, jar, now) } // setCookies sets the given cookies in the given jar associated // with the given URL at the given time. func setCookies(jar *Jar, fromURL string, cookies []string, now time.Time) { setCookies := make([]*http.Cookie, len(cookies)) for i, cs := range cookies { cookies := (&http.Response{Header: http.Header{"Set-Cookie": {cs}}}).Cookies() if len(cookies) != 1 { panic(fmt.Sprintf("Wrong cookie line %q: %#v", cs, cookies)) } setCookies[i] = cookies[0] } jar.setCookies(mustParseURL(fromURL), setCookies, now) } // allCookies returns all unexpired cookies in the jar // in the form "name1=val1 name2=val2" // (entries sorted by string). func allCookies(jar *Jar, now time.Time) string { var cs []string for _, submap := range jar.entries { for _, cookie := range submap { if !cookie.Expires.After(now) { continue } cs = append(cs, cookie.Name+"="+cookie.Value) } } sort.Strings(cs) return strings.Join(cs, " ") } // allCookiesIncludingExpired returns all cookies in the jar // in the form "name1=val1 name2=val2" // (entries sorted by string), including cookies that // have expired (without their values) func allCookiesIncludingExpired(jar *Jar, now time.Time) string { var cs []string for _, submap := range jar.entries { for _, cookie := range submap { if !cookie.Expires.After(now) { cs = append(cs, cookie.Name+"=") } else { cs = append(cs, cookie.Name+"="+cookie.Value) } } } sort.Strings(cs) return strings.Join(cs, " ") } func testQueries(t *testing.T, queries []query, description string, jar *Jar, now time.Time) { // Test different calls to Cookies. for i, query := range queries { now = now.Add(1001 * time.Millisecond) if got := queryJar(jar, query.toURL, now); got != query.want { t.Errorf("Test %q #%d\ngot %q\nwant %q", description, i, got, query.want) } } } // queryJar returns the results of querying jar for // cookies associated with url at the given time, // in "name1=val1 name2=val2" form. func queryJar(jar *Jar, toURL string, now time.Time) string { var s []string for _, c := range jar.cookies(mustParseURL(toURL), now) { s = append(s, c.Name+"="+c.Value) } return strings.Join(s, " ") } // expiresIn creates an expires attribute delta seconds from tNow. func expiresIn(delta int) string { return "expires=" + atTime(delta).Format(time.RFC1123) } // atTime returns a time delta seconds from tNow. func atTime(delta int) time.Time { return tNow.Add(time.Duration(delta) * time.Second) } // mustParseURL parses s to an URL and panics on error. func mustParseURL(s string) *url.URL { u, err := url.Parse(s) if err != nil || u.Scheme == "" || u.Host == "" { panic(fmt.Sprintf("Unable to parse URL %s.", s)) } return u } type setCommand struct { url *url.URL cookies []*http.Cookie } var allCookiesTests = []struct { about string set []setCommand expectCookies []*http.Cookie }{{ about: "no cookies", }, { about: "a cookie", set: []setCommand{{ url: mustParseURL("https://www.google.com/"), cookies: []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Expires: tNow.Add(24 * time.Hour), }, }, }}, expectCookies: []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Domain: "www.google.com", Path: "/", Secure: false, HttpOnly: false, Expires: tNow.Add(24 * time.Hour), }, }, }, { about: "expired cookie", set: []setCommand{{ url: mustParseURL("https://www.google.com/"), cookies: []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Expires: tNow.Add(-24 * time.Hour), }, }, }}, }, { about: "cookie for subpath", set: []setCommand{{ url: mustParseURL("https://www.google.com/subpath/place"), cookies: []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Expires: tNow.Add(24 * time.Hour), }, }, }}, expectCookies: []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Domain: "www.google.com", Path: "/subpath", Secure: false, HttpOnly: false, Expires: tNow.Add(24 * time.Hour), }, }, }, { about: "multiple cookies", set: []setCommand{{ url: mustParseURL("https://www.google.com/"), cookies: []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Expires: tNow.Add(24 * time.Hour), }, }, }, { url: mustParseURL("https://www.google.com/subpath/"), cookies: []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Expires: tNow.Add(24 * time.Hour), }, }, }}, expectCookies: []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Domain: "www.google.com", Path: "/subpath", Secure: false, HttpOnly: false, Expires: tNow.Add(24 * time.Hour), }, &http.Cookie{ Name: "test-cookie", Value: "test-value", Domain: "www.google.com", Path: "/", Secure: false, HttpOnly: false, Expires: tNow.Add(24 * time.Hour), }, }, }} func TestAllCookies(t *testing.T) { dir, err := ioutil.TempDir("", "cookiejar-test") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) for i, test := range allCookiesTests { path := filepath.Join(dir, fmt.Sprintf("jar%d", i)) jar := newTestJar(path) for _, s := range test.set { jar.setCookies(s.url, s.cookies, tNow) } gotCookies := jar.allCookies(tNow) if len(gotCookies) != len(test.expectCookies) { t.Fatalf("Test %q: unexpected number of cookies returned, expected: %d, got: %d", test.about, len(test.expectCookies), len(gotCookies)) } for j, c := range test.expectCookies { if !cookiesEqual(c, gotCookies[j]) { t.Fatalf("Test %q: mismatch in cookies[%d], expected: %#v, got: %#v", test.about, j, *c, *gotCookies[j]) } } } } func TestRemoveCookies(t *testing.T) { jar := newTestJar("") jar.SetCookies( mustParseURL("https://www.google.com"), []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, &http.Cookie{ Name: "test-cookie2", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, }, ) cookies := jar.AllCookies() if len(cookies) != 2 { t.Fatalf("Expected 2 cookies got %d", len(cookies)) } jar.RemoveCookie(cookies[0]) cookies2 := jar.AllCookies() if len(cookies2) != 1 { t.Fatalf("Expected 1 cookie got %d", len(cookies)) } if !cookiesEqual(cookies[1], cookies2[0]) { t.Fatalf("Unexpected cookie removed") } } func TestRemoveAllHost(t *testing.T) { testRemoveAllHost(t, mustParseURL("https://www.apple.com"), "www.apple.com", true) } func TestRemoveAllHostRoot(t *testing.T) { testRemoveAllHost(t, mustParseURL("https://www.apple.com"), "apple.com", false) } func TestRemoveAllHostDifferent(t *testing.T) { testRemoveAllHost(t, mustParseURL("https://www.apple.com"), "foo.apple.com", false) } func TestRemoveAllHostWithPort(t *testing.T) { testRemoveAllHost(t, mustParseURL("https://www.apple.com"), "www.apple.com:80", true) } func TestRemoveAllHostIP(t *testing.T) { testRemoveAllHost(t, mustParseURL("https://10.1.1.1"), "10.1.1.1", true) } func testRemoveAllHost(t *testing.T, setURL *url.URL, removeHost string, shouldRemove bool) { jar := newTestJar("") google := mustParseURL("https://www.google.com") jar.SetCookies( google, []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, &http.Cookie{ Name: "test-cookie2", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, }, ) onlyGoogle := jar.AllCookies() if len(onlyGoogle) != 2 { t.Fatalf("Expected 2 cookies, got %d", len(onlyGoogle)) } jar.SetCookies( setURL, []*http.Cookie{ &http.Cookie{ Name: "test-cookie3", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, &http.Cookie{ Name: "test-cookie4", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, }, ) withSet := jar.AllCookies() if len(withSet) != 4 { t.Fatalf("Expected 4 cookies, got %d", len(withSet)) } jar.RemoveAllHost(removeHost) after := jar.AllCookies() if !shouldRemove { if len(after) != len(withSet) { t.Fatalf("Expected %d cookies, got %d", len(withSet), len(after)) } return } if len(after) != len(onlyGoogle) { t.Fatalf("Expected %d cookies, got %d", len(onlyGoogle), len(after)) } if !cookiesEqual(onlyGoogle[0], after[0]) { t.Fatalf("Expected %v, got %v", onlyGoogle[0], after[0]) } if !cookiesEqual(onlyGoogle[1], after[1]) { t.Fatalf("Expected %v, got %v", onlyGoogle[1], after[1]) } } func TestRemoveAll(t *testing.T) { jar := newTestJar("") jar.SetCookies( mustParseURL("https://www.google.com"), []*http.Cookie{ &http.Cookie{ Name: "test-cookie", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, &http.Cookie{ Name: "test-cookie2", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, }, ) jar.SetCookies( mustParseURL("https://foo.com"), []*http.Cookie{ &http.Cookie{ Name: "test-cookie3", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, &http.Cookie{ Name: "test-cookie4", Value: "test-value", Expires: time.Now().Add(24 * time.Hour), }, }, ) jar.RemoveAll() if after := len(jar.AllCookies()); after != 0 { t.Fatalf("%d cookies remaining after RemoveAll", after) } } func cookiesEqual(a, b *http.Cookie) bool { return a.Name == b.Name && a.Value == b.Value && a.Domain == b.Domain && a.Path == b.Path && a.Expires.Equal(b.Expires) && a.HttpOnly == b.HttpOnly && a.Secure == b.Secure } golang-github-juju-persistent-cookiejar-1.0.0/punycode.go000066400000000000000000000067321414256507100235460ustar00rootroot00000000000000// 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 // This file implements the Punycode algorithm from RFC 3492. import ( "fmt" "strings" "unicode/utf8" ) // These parameter values are specified in section 5. // // All computation is done with int32s, so that overflow behavior is identical // regardless of whether int is 32-bit or 64-bit. const ( base int32 = 36 damp int32 = 700 initialBias int32 = 72 initialN int32 = 128 skew int32 = 38 tmax int32 = 26 tmin int32 = 1 ) // encode encodes a string as specified in section 6.3 and prepends prefix to // the result. // // The "while h < length(input)" line in the specification becomes "for // remaining != 0" in the Go code, because len(s) in Go is in bytes, not runes. func encode(prefix, s string) (string, error) { output := make([]byte, len(prefix), len(prefix)+1+2*len(s)) copy(output, prefix) delta, n, bias := int32(0), initialN, initialBias b, remaining := int32(0), int32(0) for _, r := range s { if r < 0x80 { b++ output = append(output, byte(r)) } else { remaining++ } } h := b if b > 0 { output = append(output, '-') } for remaining != 0 { m := int32(0x7fffffff) for _, r := range s { if m > r && r >= n { m = r } } delta += (m - n) * (h + 1) if delta < 0 { return "", fmt.Errorf("cookiejar: invalid label %q", s) } n = m for _, r := range s { if r < n { delta++ if delta < 0 { return "", fmt.Errorf("cookiejar: invalid label %q", s) } continue } if r > n { continue } q := delta for k := base; ; k += base { t := k - bias if t < tmin { t = tmin } else if t > tmax { t = tmax } if q < t { break } output = append(output, encodeDigit(t+(q-t)%(base-t))) q = (q - t) / (base - t) } output = append(output, encodeDigit(q)) bias = adapt(delta, h+1, h == b) delta = 0 h++ remaining-- } delta++ n++ } return string(output), nil } func encodeDigit(digit int32) byte { switch { case 0 <= digit && digit < 26: return byte(digit + 'a') case 26 <= digit && digit < 36: return byte(digit + ('0' - 26)) } panic("cookiejar: internal error in punycode encoding") } // adapt is the bias adaptation function specified in section 6.1. func adapt(delta, numPoints int32, firstTime bool) int32 { if firstTime { delta /= damp } else { delta /= 2 } delta += delta / numPoints k := int32(0) for delta > ((base-tmin)*tmax)/2 { delta /= base - tmin k += base } return k + (base-tmin+1)*delta/(delta+skew) } // Strictly speaking, the remaining code below deals with IDNA (RFC 5890 and // friends) and not Punycode (RFC 3492) per se. // acePrefix is the ASCII Compatible Encoding prefix. const acePrefix = "xn--" // toASCII converts a domain or domain label to its ASCII form. For example, // toASCII("bücher.example.com") is "xn--bcher-kva.example.com", and // toASCII("golang") is "golang". func toASCII(s string) (string, error) { if ascii(s) { return s, nil } labels := strings.Split(s, ".") for i, label := range labels { if !ascii(label) { a, err := encode(acePrefix, label) if err != nil { return "", err } labels[i] = a } } return strings.Join(labels, "."), nil } func ascii(s string) bool { for i := 0; i < len(s); i++ { if s[i] >= utf8.RuneSelf { return false } } return true } golang-github-juju-persistent-cookiejar-1.0.0/punycode_test.go000066400000000000000000000112061414256507100245750ustar00rootroot00000000000000// 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 import ( "testing" ) var punycodeTestCases = [...]struct { s, encoded string }{ {"", ""}, {"-", "--"}, {"-a", "-a-"}, {"-a-", "-a--"}, {"a", "a-"}, {"a-", "a--"}, {"a-b", "a-b-"}, {"books", "books-"}, {"bücher", "bcher-kva"}, {"Hello世界", "Hello-ck1hg65u"}, {"ü", "tda"}, {"üý", "tdac"}, // The test cases below come from RFC 3492 section 7.1 with Errata 3026. { // (A) Arabic (Egyptian). "\u0644\u064A\u0647\u0645\u0627\u0628\u062A\u0643\u0644" + "\u0645\u0648\u0634\u0639\u0631\u0628\u064A\u061F", "egbpdaj6bu4bxfgehfvwxn", }, { // (B) Chinese (simplified). "\u4ED6\u4EEC\u4E3A\u4EC0\u4E48\u4E0D\u8BF4\u4E2D\u6587", "ihqwcrb4cv8a8dqg056pqjye", }, { // (C) Chinese (traditional). "\u4ED6\u5011\u7232\u4EC0\u9EBD\u4E0D\u8AAA\u4E2D\u6587", "ihqwctvzc91f659drss3x8bo0yb", }, { // (D) Czech. "\u0050\u0072\u006F\u010D\u0070\u0072\u006F\u0073\u0074" + "\u011B\u006E\u0065\u006D\u006C\u0075\u0076\u00ED\u010D" + "\u0065\u0073\u006B\u0079", "Proprostnemluvesky-uyb24dma41a", }, { // (E) Hebrew. "\u05DC\u05DE\u05D4\u05D4\u05DD\u05E4\u05E9\u05D5\u05D8" + "\u05DC\u05D0\u05DE\u05D3\u05D1\u05E8\u05D9\u05DD\u05E2" + "\u05D1\u05E8\u05D9\u05EA", "4dbcagdahymbxekheh6e0a7fei0b", }, { // (F) Hindi (Devanagari). "\u092F\u0939\u0932\u094B\u0917\u0939\u093F\u0928\u094D" + "\u0926\u0940\u0915\u094D\u092F\u094B\u0902\u0928\u0939" + "\u0940\u0902\u092C\u094B\u0932\u0938\u0915\u0924\u0947" + "\u0939\u0948\u0902", "i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd", }, { // (G) Japanese (kanji and hiragana). "\u306A\u305C\u307F\u3093\u306A\u65E5\u672C\u8A9E\u3092" + "\u8A71\u3057\u3066\u304F\u308C\u306A\u3044\u306E\u304B", "n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa", }, { // (H) Korean (Hangul syllables). "\uC138\uACC4\uC758\uBAA8\uB4E0\uC0AC\uB78C\uB4E4\uC774" + "\uD55C\uAD6D\uC5B4\uB97C\uC774\uD574\uD55C\uB2E4\uBA74" + "\uC5BC\uB9C8\uB098\uC88B\uC744\uAE4C", "989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5j" + "psd879ccm6fea98c", }, { // (I) Russian (Cyrillic). "\u043F\u043E\u0447\u0435\u043C\u0443\u0436\u0435\u043E" + "\u043D\u0438\u043D\u0435\u0433\u043E\u0432\u043E\u0440" + "\u044F\u0442\u043F\u043E\u0440\u0443\u0441\u0441\u043A" + "\u0438", "b1abfaaepdrnnbgefbadotcwatmq2g4l", }, { // (J) Spanish. "\u0050\u006F\u0072\u0071\u0075\u00E9\u006E\u006F\u0070" + "\u0075\u0065\u0064\u0065\u006E\u0073\u0069\u006D\u0070" + "\u006C\u0065\u006D\u0065\u006E\u0074\u0065\u0068\u0061" + "\u0062\u006C\u0061\u0072\u0065\u006E\u0045\u0073\u0070" + "\u0061\u00F1\u006F\u006C", "PorqunopuedensimplementehablarenEspaol-fmd56a", }, { // (K) Vietnamese. "\u0054\u1EA1\u0069\u0073\u0061\u006F\u0068\u1ECD\u006B" + "\u0068\u00F4\u006E\u0067\u0074\u0068\u1EC3\u0063\u0068" + "\u1EC9\u006E\u00F3\u0069\u0074\u0069\u1EBF\u006E\u0067" + "\u0056\u0069\u1EC7\u0074", "TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g", }, { // (L) 3B. "\u0033\u5E74\u0042\u7D44\u91D1\u516B\u5148\u751F", "3B-ww4c5e180e575a65lsy2b", }, { // (M) -with-SUPER-MONKEYS. "\u5B89\u5BA4\u5948\u7F8E\u6075\u002D\u0077\u0069\u0074" + "\u0068\u002D\u0053\u0055\u0050\u0045\u0052\u002D\u004D" + "\u004F\u004E\u004B\u0045\u0059\u0053", "-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n", }, { // (N) Hello-Another-Way-. "\u0048\u0065\u006C\u006C\u006F\u002D\u0041\u006E\u006F" + "\u0074\u0068\u0065\u0072\u002D\u0057\u0061\u0079\u002D" + "\u305D\u308C\u305E\u308C\u306E\u5834\u6240", "Hello-Another-Way--fc4qua05auwb3674vfr0b", }, { // (O) 2. "\u3072\u3068\u3064\u5C4B\u6839\u306E\u4E0B\u0032", "2-u9tlzr9756bt3uc0v", }, { // (P) MajiKoi5 "\u004D\u0061\u006A\u0069\u3067\u004B\u006F\u0069\u3059" + "\u308B\u0035\u79D2\u524D", "MajiKoi5-783gue6qz075azm5e", }, { // (Q) de "\u30D1\u30D5\u30A3\u30FC\u0064\u0065\u30EB\u30F3\u30D0", "de-jg4avhby1noc0d", }, { // (R) "\u305D\u306E\u30B9\u30D4\u30FC\u30C9\u3067", "d9juau41awczczp", }, { // (S) -> $1.00 <- "\u002D\u003E\u0020\u0024\u0031\u002E\u0030\u0030\u0020" + "\u003C\u002D", "-> $1.00 <--", }, } func TestPunycode(t *testing.T) { for _, tc := range punycodeTestCases { if got, err := encode("", tc.s); err != nil { t.Errorf(`encode("", %q): %v`, tc.s, err) } else if got != tc.encoded { t.Errorf(`encode("", %q): got %q, want %q`, tc.s, got, tc.encoded) } } } golang-github-juju-persistent-cookiejar-1.0.0/serialize.go000066400000000000000000000103361414256507100237020ustar00rootroot00000000000000// Copyright 2015 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 import ( "encoding/json" "io" "log" "os" "path/filepath" "sort" "time" "gopkg.in/retry.v1" filelock "github.com/juju/go4/lock" "gopkg.in/errgo.v1" ) // Save saves the cookies to the persistent cookie file. // Before the file is written, it reads any cookies that // have been stored from it and merges them into j. func (j *Jar) Save() error { if j.filename == "" { return nil } return j.save(time.Now()) } // MarshalJSON implements json.Marshaler by encoding all persistent cookies // currently in the jar. func (j *Jar) MarshalJSON() ([]byte, error) { j.mu.Lock() defer j.mu.Unlock() // Marshaling entries can never fail. data, _ := json.Marshal(j.allPersistentEntries()) return data, nil } // save is like Save but takes the current time as a parameter. func (j *Jar) save(now time.Time) error { locked, err := lockFile(lockFileName(j.filename)) if err != nil { return errgo.Mask(err) } defer locked.Close() f, err := os.OpenFile(j.filename, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return errgo.Mask(err) } defer f.Close() // TODO optimization: if the file hasn't changed since we // loaded it, don't bother with the merge step. j.mu.Lock() defer j.mu.Unlock() if err := j.mergeFrom(f); err != nil { // The cookie file is probably corrupt. log.Printf("cannot read cookie file to merge it; ignoring it: %v", err) } j.deleteExpired(now) if err := f.Truncate(0); err != nil { return errgo.Notef(err, "cannot truncate file") } if _, err := f.Seek(0, 0); err != nil { return errgo.Mask(err) } return j.writeTo(f) } // load loads the cookies from j.filename. If the file does not exist, // no error will be returned and no cookies will be loaded. func (j *Jar) load() error { if _, err := os.Stat(filepath.Dir(j.filename)); os.IsNotExist(err) { // The directory that we'll store the cookie jar // in doesn't exist, so don't bother trying // to acquire the lock. return nil } locked, err := lockFile(lockFileName(j.filename)) if err != nil { return errgo.Mask(err) } defer locked.Close() f, err := os.Open(j.filename) if err != nil { if os.IsNotExist(err) { return nil } return err } defer f.Close() if err := j.mergeFrom(f); err != nil { return errgo.Mask(err) } return nil } // mergeFrom reads all the cookies from r and stores them in the Jar. func (j *Jar) mergeFrom(r io.Reader) error { decoder := json.NewDecoder(r) // Cope with old cookiejar format by just discarding // cookies, but still return an error if it's invalid JSON. var data json.RawMessage if err := decoder.Decode(&data); err != nil { if err == io.EOF { // Empty file. return nil } return err } var entries []entry if err := json.Unmarshal(data, &entries); err != nil { log.Printf("warning: discarding cookies in invalid format (error: %v)", err) return nil } j.merge(entries) return nil } // writeTo writes all the cookies in the jar to w // as a JSON array. func (j *Jar) writeTo(w io.Writer) error { encoder := json.NewEncoder(w) entries := j.allPersistentEntries() if err := encoder.Encode(entries); err != nil { return err } return nil } // allPersistentEntries returns all the entries in the jar, sorted by primarly by canonical host // name and secondarily by path length. func (j *Jar) allPersistentEntries() []entry { var entries []entry for _, submap := range j.entries { for _, e := range submap { if e.Persistent { entries = append(entries, e) } } } sort.Sort(byCanonicalHost{entries}) return entries } // lockFileName returns the name of the lock file associated with // the given path. func lockFileName(path string) string { return path + ".lock" } var attempt = retry.LimitTime(3*time.Second, retry.Exponential{ Initial: 100 * time.Microsecond, Factor: 1.5, MaxDelay: 100 * time.Millisecond, }) func lockFile(path string) (io.Closer, error) { for a := retry.Start(attempt, nil); a.Next(); { locker, err := filelock.Lock(path) if err == nil { return locker, nil } if !a.More() { return nil, errgo.Notef(err, "file locked for too long; giving up") } } panic("unreachable") }