├── .gitignore ├── cookie.go ├── publicsuffixes_test.go ├── storage.go ├── internals_test.go ├── maketable.go ├── publicsuffixes.go ├── jar.go └── jar_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /cookie.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Volker Dobler. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cookiejar 6 | 7 | import ( 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // Cookie is the representation of a cookie in the cookie jar. 13 | type Cookie struct { 14 | Name string // the name of the cookie 15 | Value string // the value of cookie 16 | Domain string // the domain (no leading dot) 17 | Path string // the path 18 | Expires time.Time // zero value indicates Session cookie 19 | Secure bool // send to https only 20 | HostOnly bool // a Host cookie if true, else a Domain cookie 21 | HttpOnly bool // corresponding field in http.Cookie 22 | Created time.Time // time of creation 23 | LastAccess time.Time // last update or send action 24 | } 25 | 26 | // shouldSend determines whether the cookie c qualifies to be included in a 27 | // request to host/path. It is the callers responsibility to check if the 28 | // cookie is expired. 29 | func (c *Cookie) shouldSend(https bool, host, path string) bool { 30 | return c.domainMatch(host) && 31 | c.pathMatch(path) && 32 | secureEnough(c.Secure, https) 33 | } 34 | 35 | // Every cookie is sent via https. If the protocol is just http, then the 36 | // cookie must not be marked as secure. 37 | func secureEnough(cookieIsSecure, requestIsSecure bool) bool { 38 | return requestIsSecure || !cookieIsSecure 39 | } 40 | 41 | // domainMatch implements "domain-match" of RFC 6265 section 5.1.3: 42 | // A string domain-matches a given domain string if at least one of the 43 | // following conditions hold: 44 | // o The domain string and the string are identical. (Note that both 45 | // the domain string and the string will have been canonicalized to 46 | // lower case at this point.) 47 | // o All of the following conditions hold: 48 | // * The domain string is a suffix of the string. 49 | // * The last character of the string that is not included in the 50 | // domain string is a %x2E (".") character. 51 | // * The string is a host name (i.e., not an IP address). 52 | func (c *Cookie) domainMatch(host string) bool { 53 | if c.Domain == host { 54 | return true 55 | } 56 | return !c.HostOnly && strings.HasSuffix(host, "."+c.Domain) 57 | } 58 | 59 | // pathMatch implements "path-match" according to RFC 6265 section 5.1.4: 60 | // A request-path path-matches a given cookie-path if at least one of 61 | // the following conditions holds: 62 | // o The cookie-path and the request-path are identical. 63 | // o The cookie-path is a prefix of the request-path, and the last 64 | // character of the cookie-path is %x2F ("/"). 65 | // o The cookie-path is a prefix of the request-path, and the first 66 | // character of the request-path that is not included in the cookie- 67 | // path is a %x2F ("/") character. 68 | func (c *Cookie) pathMatch(requestPath string) bool { 69 | if requestPath == c.Path { // the simple case 70 | return true 71 | } 72 | 73 | if strings.HasPrefix(requestPath, c.Path) { 74 | if c.Path[len(c.Path)-1] == '/' { 75 | return true // "/any/path" matches "/" and "/any/" 76 | } else if requestPath[len(c.Path)] == '/' { 77 | return true // "/any" matches "/any/some" 78 | } 79 | } 80 | 81 | return false 82 | } 83 | 84 | // Expired checks if the cookie c is expired. 85 | func (c *Cookie) Expired() bool { 86 | return !c.Session() && c.Expires.Before(time.Now()) 87 | } 88 | 89 | // Session checks if a cookie c is a session cookie (i.e. has a 90 | // zero value for its Expires field). 91 | func (c *Cookie) Session() bool { 92 | return c.Expires.IsZero() 93 | } 94 | 95 | // ------------------------------------------------------------------------ 96 | // Sorting cookies 97 | 98 | // sendList is the list of cookies to be sent in a HTTP request. 99 | // sendLists can be sorted according to RFC 6265 section 5.4 point 2. 100 | type sendList []*Cookie 101 | 102 | func (l sendList) Len() int { return len(l) } 103 | 104 | func (l sendList) Less(i, j int) bool { 105 | // RFC 6265 says (section 5.4 point 2) we should sort our cookies 106 | // like: 107 | // o longer paths go firts 108 | // o for same length paths: earlier creation time goes first 109 | in, jn := len(l[i].Path), len(l[j].Path) 110 | if in == jn { 111 | return l[i].Created.Before(l[j].Created) 112 | } 113 | return in > jn 114 | } 115 | 116 | func (l sendList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 117 | -------------------------------------------------------------------------------- /publicsuffixes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Volker Dobler. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cookiejar 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | // Test case table derived from 12 | // http://mxr.mozilla.org/mozilla-central/source/netwerk/test/unit/data/test_psl.txt?raw=1 13 | // See http://publicsuffix.org/list/ for details. 14 | var effectiveTLDPlusOneTests = []struct { 15 | domain string 16 | etldp1 string 17 | }{ 18 | /***** We never use empty domains, mixed cases or leading dots ***** 19 | // null input. 20 | {"", ""}, 21 | // Mixed case. 22 | {"COM", ""}, 23 | {"example.COM", "example.com"}, 24 | {"WwW.example.COM", "example.com"}, 25 | // Leading dot. 26 | {".com", ""}, 27 | {".example", ""}, 28 | {".example.com", ""}, 29 | {".example.example", ""}, 30 | **************************************************************/ 31 | 32 | // Unlisted TLD. 33 | 34 | {"example", ""}, 35 | {"example.example", "example.example"}, 36 | {"b.example.example", "example.example"}, 37 | {"a.b.example.example", "example.example"}, 38 | 39 | // Listed, but non-Internet, TLD. (Yes, these are commented out in the original too.) 40 | // {"local", ""}, 41 | // {"example.local", ""}, 42 | // {"b.example.local", ""}, 43 | // {"a.b.example.local", ""}, 44 | 45 | // TLD with only 1 rule. 46 | {"biz", ""}, 47 | {"domain.biz", "domain.biz"}, 48 | {"b.domain.biz", "domain.biz"}, 49 | {"a.b.domain.biz", "domain.biz"}, 50 | // TLD with some 2-level rules. 51 | {"com", ""}, 52 | {"example.com", "example.com"}, 53 | {"b.example.com", "example.com"}, 54 | {"a.b.example.com", "example.com"}, 55 | {"uk.com", ""}, 56 | {"example.uk.com", "example.uk.com"}, 57 | {"b.example.uk.com", "example.uk.com"}, 58 | {"a.b.example.uk.com", "example.uk.com"}, 59 | {"test.ac", "test.ac"}, 60 | // TLD with only 1 (wildcard) rule. 61 | {"cy", ""}, 62 | {"c.cy", ""}, 63 | {"b.c.cy", "b.c.cy"}, 64 | {"a.b.c.cy", "b.c.cy"}, 65 | // More complex TLD. 66 | {"jp", ""}, 67 | {"test.jp", "test.jp"}, 68 | {"www.test.jp", "test.jp"}, 69 | {"ac.jp", ""}, 70 | {"test.ac.jp", "test.ac.jp"}, 71 | {"www.test.ac.jp", "test.ac.jp"}, 72 | {"kyoto.jp", ""}, 73 | {"test.kyoto.jp", "test.kyoto.jp"}, 74 | {"ide.kyoto.jp", ""}, 75 | {"b.ide.kyoto.jp", "b.ide.kyoto.jp"}, 76 | {"a.b.ide.kyoto.jp", "b.ide.kyoto.jp"}, 77 | {"c.kobe.jp", ""}, 78 | {"b.c.kobe.jp", "b.c.kobe.jp"}, 79 | {"a.b.c.kobe.jp", "b.c.kobe.jp"}, 80 | {"city.kobe.jp", "city.kobe.jp"}, 81 | 82 | // TLD with a wildcard rule and exceptions. 83 | {"om", ""}, 84 | {"test.om", ""}, 85 | {"b.test.om", "b.test.om"}, 86 | {"a.b.test.om", "b.test.om"}, 87 | {"songfest.om", "songfest.om"}, 88 | {"www.songfest.om", "songfest.om"}, 89 | // US K12. 90 | {"us", ""}, 91 | {"test.us", "test.us"}, 92 | {"www.test.us", "test.us"}, 93 | {"ak.us", ""}, 94 | {"test.ak.us", "test.ak.us"}, 95 | {"www.test.ak.us", "test.ak.us"}, 96 | {"k12.ak.us", ""}, 97 | {"test.k12.ak.us", "test.k12.ak.us"}, 98 | {"www.test.k12.ak.us", "test.k12.ak.us"}, 99 | } 100 | 101 | func TestEffectiveTLDPlusOneTests(t *testing.T) { 102 | for i, tt := range effectiveTLDPlusOneTests { 103 | etldp1 := EffectiveTLDPlusOne(tt.domain) 104 | 105 | if etldp1 != tt.etldp1 { 106 | t.Errorf("%d. domain=%q: got %q, want %q.", 107 | i, tt.domain, etldp1, tt.etldp1) 108 | } 109 | } 110 | } 111 | 112 | var allowCookiesOnTests = []struct { 113 | domain string 114 | allow bool 115 | }{ 116 | {"something.strange", true}, 117 | {"ourintranet", false}, 118 | {"com", false}, 119 | {"google.com", true}, 120 | {"www.google.com", true}, 121 | {"uk", false}, 122 | {"co.uk", false}, 123 | {"bbc.co.uk", true}, 124 | {"foo.www.bbc.co.uk", true}, 125 | {"kawasaki.jp", false}, 126 | {"bar.kawasaki.jp", false}, 127 | {"foo.bar.kawasaki.jp", true}, 128 | {"city.kawasaki.jp", true}, 129 | {"aichi.jp", false}, 130 | {"aisai.aichi.jp", false}, 131 | {"foo.aisai.aichi.jp", true}, 132 | } 133 | 134 | func TestAllowCookiesOn(t *testing.T) { 135 | for i, tt := range allowCookiesOnTests { 136 | allow := allowDomainCookies(tt.domain) 137 | if allow != tt.allow { 138 | t.Errorf("%d: domain=%q expected %t got %t", i, tt.domain, tt.allow, allow) 139 | } 140 | } 141 | } 142 | 143 | func BenchmarkAllowDomainCookies(b *testing.B) { 144 | for i := 0; i < b.N; i++ { 145 | for _, tt := range allowCookiesOnTests { 146 | allowDomainCookies(tt.domain) 147 | } 148 | } 149 | } 150 | 151 | var unlistedDomains = []string{ 152 | "www.google.ch", 153 | "www.123abc.com", 154 | "www.aaaaaaa.com", 155 | "www.ddddddd.com", 156 | "www.iiiiiii.com", 157 | "www.mmmmmmm.net", 158 | "www.ppppppp.net", 159 | "www.rrrrrrr.org", 160 | "www.uuuuuuu.org", 161 | "www.xxxxxxx.org", 162 | "www.zzzzzzz.de", 163 | "www.yyyyyyy.it", 164 | "www.wwwwwww.jp", 165 | } 166 | 167 | func BenchmarkAllowULDomainCookies(b *testing.B) { 168 | for i := 0; i < b.N; i++ { 169 | for _, domain := range unlistedDomains { 170 | allowDomainCookies(domain) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Volker Dobler. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cookiejar 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | var _ = fmt.Printf 12 | 13 | // ------------------------------------------------------------------------- 14 | // Storage 15 | 16 | // storage is the interface of a cookie monster. 17 | type storage interface { 18 | retrieve(https bool, host, path string) []*Cookie 19 | find(domain, path, name string) *Cookie 20 | delete(domain, path, name string) bool 21 | } 22 | 23 | // ------------------------------------------------------------------------- 24 | // Flat 25 | 26 | // flat implements a simple storage for cookies. The actual storage 27 | // is an unsorted arry of pointers to the stored cookies which is searched 28 | // linearely any time we look for a cookie 29 | type flat []*Cookie 30 | 31 | // retrieve fetches the unsorted list of cookies to be sent 32 | func (f *flat) retrieve(https bool, host, path string) []*Cookie { 33 | selection := make([]*Cookie, 0) 34 | expired := 0 35 | for _, cookie := range *f { 36 | if cookie.Expired() { 37 | expired++ 38 | } else { 39 | if cookie.shouldSend(https, host, path) { 40 | selection = append(selection, cookie) 41 | } 42 | } 43 | } 44 | 45 | if expired > 10 && expired > len(*f)/5 { 46 | f.cleanup(expired) 47 | } 48 | 49 | return selection 50 | } 51 | 52 | // find looks up the cookie or returns a "new" cookie 53 | // (which might be the reuse of an existing but expired one). 54 | func (f *flat) find(domain, path, name string) *Cookie { 55 | expiredIdx := -1 56 | for i, cookie := range *f { 57 | // see if the cookie is there 58 | if domain == cookie.Domain && 59 | path == cookie.Path && 60 | name == cookie.Name { 61 | return cookie 62 | } 63 | 64 | // track expired 65 | if expiredIdx == -1 { 66 | if cookie.Expired() { 67 | expiredIdx = i 68 | } 69 | } 70 | } 71 | 72 | // reuse expired cookie 73 | if expiredIdx != -1 { 74 | (*f)[expiredIdx].Name = "" // clear name to indicate "new" cookie 75 | return (*f)[expiredIdx] 76 | } 77 | 78 | // a genuine new cookie 79 | cookie := &Cookie{} 80 | *f = append(*f, cookie) 81 | return cookie 82 | } 83 | 84 | // delete the cookie from the storage. Returns true if the 85 | // cookie was present in the jar. 86 | func (f *flat) delete(domain, path, name string) bool { 87 | n := len(*f) 88 | if n == 0 { 89 | return false 90 | } 91 | for i := range *f { 92 | if domain == (*f)[i].Domain && 93 | path == (*f)[i].Path && 94 | name == (*f)[i].Name { 95 | if i < n-1 { 96 | (*f)[i] = (*f)[n-1] 97 | } 98 | (*f) = (*f)[:n-1] 99 | return true 100 | } 101 | } 102 | return false 103 | } 104 | 105 | // cleanup removes expired cookies from f 106 | func (f *flat) cleanup(num int) { 107 | // corner cases 108 | if num == 0 { 109 | return 110 | } 111 | if num == len(*f) { 112 | *f = (*f)[:0] 113 | return 114 | } 115 | 116 | i, j, n := 0, len(*f), 0 117 | 118 | for n < num { 119 | for i < j && !(*f)[i].Expired() { // find next expired 120 | i++ 121 | } 122 | if i == j-1 { 123 | j-- 124 | break 125 | } 126 | j-- 127 | for j > i && (*f)[j].Expired() { // find non expired from back 128 | j-- 129 | n++ 130 | } 131 | 132 | if i == j || n == num { 133 | break 134 | } 135 | (*f)[i] = (*f)[j] // overwrite expired with non-expired 136 | i++ 137 | n++ 138 | } 139 | 140 | *f = (*f)[0:j] // reslice 141 | } 142 | 143 | // ------------------------------------------------------------------------- 144 | // Boxed 145 | 146 | // boxed is a storage grouped by domain. 147 | type boxed map[string]*flat 148 | 149 | // return the proper flat for host or nil if non present 150 | func (b *boxed) flat(host string) *flat { 151 | box := EffectiveTLDPlusOne(host) 152 | if box == "" { 153 | box = host 154 | } 155 | return (*b)[box] 156 | } 157 | 158 | // retrieve fetches the unsorted list of cookies to be sent 159 | func (b *boxed) retrieve(https bool, host, path string) []*Cookie { 160 | if flat := b.flat(host); flat != nil { 161 | return flat.retrieve(https, host, path) 162 | } 163 | return nil 164 | } 165 | 166 | // find looks up the cookie or returns a "new" cookie 167 | // (which might be the reuse of an existing but expired one). 168 | func (b *boxed) find(domain, path, name string) *Cookie { 169 | if flat := b.flat(domain); flat != nil { 170 | return flat.find(domain, path, name) 171 | } 172 | 173 | f := make(flat, 1) 174 | box := EffectiveTLDPlusOne(domain) 175 | if box == "" { 176 | box = domain 177 | } 178 | f[0] = &Cookie{} 179 | (*b)[box] = &f 180 | return f[0] 181 | } 182 | 183 | // delete the cookie from the storage. Returns true if the 184 | // cookie was present in the jar. 185 | func (b *boxed) delete(domain, path, name string) bool { 186 | if flat := b.flat(domain); flat != nil { 187 | return flat.delete(domain, path, name) 188 | } 189 | return false 190 | } 191 | -------------------------------------------------------------------------------- /internals_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Volker Dobler. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cookiejar 6 | 7 | // Tests for the unexported helper functions. 8 | 9 | import ( 10 | "fmt" 11 | "net/url" 12 | "strings" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | var defaultPathTests = []struct{ path, dir string }{ 18 | {"", "/"}, 19 | {"xy", "/"}, 20 | {"xy/z", "/"}, 21 | {"/", "/"}, 22 | {"/abc", "/"}, 23 | {"/ab/xy", "/ab"}, 24 | {"/ab/xy/z", "/ab/xy"}, 25 | {"/ab/", "/ab"}, 26 | {"/ab/xy/z/", "/ab/xy/z"}, 27 | } 28 | 29 | func TestDefaultPath(t *testing.T) { 30 | for i, tt := range defaultPathTests { 31 | u := url.URL{Path: tt.path} 32 | got := defaultPath(&u) 33 | if got != tt.dir { 34 | t.Errorf("#%d %q: want %q, got %q", i, tt.path, got, tt.dir) 35 | } 36 | } 37 | } 38 | 39 | var pathMatchTests = []struct { 40 | cookiePath string 41 | urlPath string 42 | match bool 43 | }{ 44 | {"/", "/", true}, 45 | {"/x", "/x", true}, 46 | {"/", "/abc", true}, 47 | {"/abc", "/foo", false}, 48 | {"/abc", "/foo/", false}, 49 | {"/abc", "/abcd", false}, 50 | {"/abc", "/abc/d", true}, 51 | {"/path", "/", false}, 52 | {"/path", "/path", true}, 53 | {"/path", "/path/x", true}, 54 | } 55 | 56 | func TestPathMatch(t *testing.T) { 57 | for i, tt := range pathMatchTests { 58 | c := &Cookie{Path: tt.cookiePath} 59 | if c.pathMatch(tt.urlPath) != tt.match { 60 | t.Errorf("#%d want %t for %q ~ %q", i, tt.match, tt.cookiePath, tt.urlPath) 61 | } 62 | } 63 | } 64 | 65 | var hostTests = []struct { 66 | in, expected string 67 | }{ 68 | {"www.example.com", "www.example.com"}, 69 | {"www.EXAMPLE.com", "www.example.com"}, 70 | {"wWw.eXAmple.CoM", "www.example.com"}, 71 | {"www.example.com:80", "www.example.com"}, 72 | {"12.34.56.78:8080", "12.34.56.78"}, 73 | // TODO: add IDN testcase 74 | } 75 | 76 | func TestHost(t *testing.T) { 77 | for i, tt := range hostTests { 78 | out, _ := host(&url.URL{Host: tt.in}) 79 | if out != tt.expected { 80 | t.Errorf("#%d %q: got %q, want %Q", i, tt.in, out, tt.expected) 81 | } 82 | } 83 | } 84 | 85 | var isIPTests = []struct { 86 | host string 87 | isIP bool 88 | }{ 89 | {"example.com", false}, 90 | {"127.0.0.1", true}, 91 | {"1.1.1.300", false}, 92 | {"www.foo.bar.net", false}, 93 | {"123.foo.bar.net", false}, 94 | // TODO: IPv6 test 95 | } 96 | 97 | func TestIsIP(t *testing.T) { 98 | for i, tt := range isIPTests { 99 | if isIP(tt.host) != tt.isIP { 100 | t.Errorf("#%d %q: want %t", i, tt.host, tt.isIP) 101 | } 102 | } 103 | } 104 | 105 | var domainAndTypeTests = []struct { 106 | inHost string 107 | inCookieDomain string 108 | outDomain string 109 | outHostOnly bool 110 | }{ 111 | {"www.example.com", "", "www.example.com", true}, 112 | {"127.www.0.0.1", "127.0.0.1", "", false}, 113 | {"www.example.com", ".", "", false}, 114 | {"www.example.com", "..", "", false}, 115 | {"www.example.com", "com", "", false}, 116 | {"www.example.com", ".com", "", false}, 117 | {"www.example.com", "example.com", "example.com", false}, 118 | {"www.example.com", ".example.com", "example.com", false}, 119 | {"www.example.com", "www.example.com", "www.example.com", false}, // Unsure about this and 120 | {"www.example.com", ".www.example.com", "www.example.com", false}, // this one. 121 | {"foo.sso.example.com", "sso.example.com", "sso.example.com", false}, 122 | } 123 | 124 | func TestDomainAndType(t *testing.T) { 125 | jar := Jar{} 126 | for i, tt := range domainAndTypeTests { 127 | d, h, _ := jar.domainAndType(tt.inHost, tt.inCookieDomain) 128 | if d != tt.outDomain || h != tt.outHostOnly { 129 | t.Errorf("#%d %q/%q: want %q/%t got %q/%t", 130 | i, tt.inHost, tt.inCookieDomain, 131 | tt.outDomain, tt.outHostOnly, d, h) 132 | } 133 | } 134 | } 135 | 136 | var flatCleanupTests = []struct { 137 | spec string // E: expired cookie at this position in flat slice 138 | exp string // expected order of cookies after cleanup 139 | }{ 140 | {"vvvvv", "01234"}, 141 | {"vvvvE", "0123"}, 142 | {"vvvEE", "012"}, 143 | {"Evvvv", "4123"}, 144 | {"EEvvv", "432"}, 145 | {"EvEvv", "413"}, 146 | {"EvEvE", "31"}, 147 | {"EvEEE", "1"}, 148 | {"EEEvv", "43"}, 149 | {"EEEvE", "3"}, 150 | {"EEEEE", ""}, 151 | {"EEEvvEEE", "43"}, 152 | {"EEvEvEEE", "42"}, 153 | {"EEvEvvEE", "542"}, 154 | {"EEvEEvEE", "52"}, 155 | {"vvEEEEEE", "01"}, 156 | } 157 | 158 | func TestFlatCleanup(t *testing.T) { 159 | past := time.Now().Add(-1 * time.Hour) 160 | generate := func(spec string) *flat { 161 | // turn a spec into a flat slice 162 | f := make(flat, len(spec)) 163 | for i := range spec { 164 | name := fmt.Sprintf("%d", i) // name is index in original slice 165 | cookie := Cookie{Name: name} 166 | if spec[i] == 'E' { 167 | cookie.Expires = past 168 | } 169 | f[i] = &cookie 170 | } 171 | return &f 172 | } 173 | 174 | for i, tt := range flatCleanupTests { 175 | fp := generate(tt.spec) 176 | fp.cleanup(strings.Count(tt.spec, "E")) 177 | s := "" 178 | for i := range *fp { 179 | s += (*fp)[i].Name 180 | } 181 | if s != tt.exp { 182 | t.Errorf("%d %s: Want %q, got %q", i, tt.spec, tt.exp, s) 183 | } 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /maketable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Volker Dobler. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build ignore 6 | 7 | package main 8 | 9 | // 10 | // recreate the tables with 11 | // go run maketable.go > table.go && go fmt 12 | // 13 | 14 | import ( 15 | "fmt" 16 | "io" 17 | "io/ioutil" 18 | "log" 19 | "net/http" 20 | "sort" 21 | "os" 22 | "strings" 23 | "time" 24 | ) 25 | 26 | const tableUrl = "http://mxr.mozilla.org/mozilla-central/source/netwerk/dns/effective_tld_names.dat?raw=1" 27 | 28 | type ruleKind uint8 29 | 30 | const ( 31 | none ruleKind = iota // not a rule, just internal node 32 | normalRule 33 | exceptionRule 34 | wildcardRule 35 | ) 36 | 37 | type node struct { 38 | label string 39 | kind ruleKind 40 | sub []node 41 | } 42 | 43 | func findLabel(label string, nodes []node) *node { 44 | // do *not* replace with binary search 45 | for i := range nodes { 46 | if nodes[i].label == label { 47 | return &nodes[i] 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | func insert(nl []node, parts []string) []node { 54 | if len(parts) == 1 { 55 | label := parts[0] 56 | kind := normalRule 57 | switch label[0] { 58 | case '!': 59 | kind = exceptionRule 60 | label = label[1:] 61 | case '*': 62 | kind = wildcardRule 63 | label = label[1:] 64 | } 65 | if w := findLabel(label, nl); w != nil { 66 | if w.kind != none { 67 | log.Fatalf("Duplicate rule for " + label) 68 | } 69 | w.kind = kind // just update kind 70 | return nl 71 | } 72 | return append(nl, node{label, kind, nil}) 73 | } 74 | last := len(parts)-1 75 | label := parts[last] 76 | w := findLabel(label, nl) 77 | if w == nil { 78 | nl = append(nl, node{label, none, nil}) 79 | w = & nl[len(nl)-1] 80 | } 81 | w.sub = insert(w.sub, parts[:last]) 82 | return nl 83 | } 84 | 85 | type nodeList []node 86 | 87 | func (t nodeList) Len() int { return len(t) } 88 | func (t nodeList) Less(i, j int) bool { 89 | return t[i].label < t[j].label 90 | } 91 | func (t nodeList) Swap(i, j int) { t[i], t[j] = t[j], t[i] } 92 | 93 | func main() { 94 | var input io.Reader 95 | 96 | if file, err := os.Open("effective_tld_names.dat"); err == nil { 97 | defer file.Close() 98 | input = file 99 | } else { 100 | resp, err := http.Get(tableUrl) 101 | if err != nil { 102 | log.Fatal(err) 103 | } 104 | defer resp.Body.Close() 105 | input = resp.Body 106 | } 107 | all, err := ioutil.ReadAll(input) 108 | if err != nil { 109 | log.Fatal(err) 110 | } 111 | 112 | fmt.Println("// Copyright 2012 Volker Dobler. All rights reserved.") 113 | fmt.Println("// Use of this source code is governed by a BSD-style") 114 | fmt.Println("// license that can be found in the LICENSE file.") 115 | fmt.Println("") 116 | fmt.Println("package cookiejar") 117 | fmt.Println("") 118 | fmt.Println("// This file was generated by performing") 119 | fmt.Println("// go run maketable.go") 120 | fmt.Println("// on ", time.Now().Format(time.RFC1123Z)) 121 | fmt.Println("// Do not modify.") 122 | fmt.Println("") 123 | fmt.Println("// A 'public suffix' is one under which Internet users can directly register") 124 | fmt.Println("// names. This list is maintained on http://publicsuffix.org/") 125 | fmt.Println("// See there for a description of the format and further details.") 126 | fmt.Println("") 127 | 128 | var root []node = make([]node, 0, 200) 129 | 130 | // read in list: remove comments and empty lines, and fill tlds and rules 131 | lines := strings.Split(string(all), "\n") 132 | for _, line := range lines { 133 | // remove noise 134 | line = strings.TrimSpace(line) 135 | if len(line) == 0 || strings.HasPrefix(line, "//") { 136 | continue 137 | } 138 | 139 | // saveguard for too fancy wildcards 140 | if strings.Contains(line,"*") { 141 | if strings.Contains(line[1:], "*") || len(line)<2 || line[1]!='.' { 142 | log.Fatalf("Cannot handle complex wildcard rule %q", line) 143 | } 144 | // transform "*.kobe.jp" to "*kobe.jp" 145 | line = "*" + line[2:] 146 | } 147 | 148 | parts := strings.Split(line, ".") 149 | root = insert(root, parts) 150 | } 151 | 152 | // write out data structure 153 | fmt.Printf("var PublicSuffixes = Node{\"\", 0, []Node{\n") 154 | printNodelist(root, 1) 155 | fmt.Printf("}}\n") 156 | fmt.Println() 157 | fmt.Println("// the needed fibonacci numbers") 158 | fmt.Printf("var fibonacci = []int{0, 1") 159 | a, b := 0, 1 160 | for b longest { 177 | longest = len(list) 178 | } 179 | prefix := strings.Repeat("\t", indent) 180 | for _, n := range list { 181 | fmt.Printf("%s{%q, %d, ", prefix, n.label, n.kind) 182 | if len(n.sub) == 0 { 183 | fmt.Printf("nil},\n") 184 | } else { 185 | fmt.Printf("[]Node{\n") 186 | printNodelist(n.sub, indent+1) 187 | fmt.Printf("%s},\n%s},\n", prefix, prefix) 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /publicsuffixes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Volker Dobler. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cookiejar 6 | 7 | // The public suffix stuff tries to answer the question: 8 | // "Should we allow to set a domain cookie for domain d?" 9 | // It also contains code to calculate the "effective top 10 | // level domain plus one" (etldp1) which are the registered 11 | // or registrable domains. 12 | // See http://publicsuffix.org/ for details. 13 | // 14 | // From http://publicsuffix.org/list/: 15 | // A domain is said to match a rule if, when the domain and rule are both 16 | // split,and one compares the labels from the rule to the labels from the 17 | // domain, beginning at the right hand end, one finds that for every pair 18 | // either they are identical, or that the label from the rule is "*" (star). 19 | // The domain may legitimately have labels remaining at the end of this 20 | // matching process. 21 | // 22 | // Algorithm from http://publicsuffix.org/list/ 23 | // 1. Match domain against all rules and take note of the matching ones. 24 | // 2. If no rules match, the prevailing rule is "*". 25 | // 3. If more than one rule matches, the prevailing rule is the one which 26 | // is an exception rule. 27 | // 4. If there is no matching exception rule, the prevailing rule is the one 28 | // with the most labels. 29 | // 5. If the prevailing rule is a exception rule, modify it by removing the 30 | // leftmost label. 31 | // 6. The public suffix is the set of labels from the domain which directly 32 | // match the labels of the prevailing rule (joined by dots). 33 | // 7. The registered or registrable domain is the public suffix plus one 34 | // additional label. 35 | // As this algorithm is prohibitive slow we store the list of rules as 36 | // a tree and search this tree for a longest match. Beeing an exception rule 37 | // is stored naturaly on the node. Wildcard rules are handled the same 38 | // A rule like "*.a.b" contains a node "a" and this node's kind is wildcard. 39 | // This data structure works as there are no two rules of the type. 40 | // "!a.b" and "*.a.b". 41 | // 42 | 43 | import ( 44 | "strings" 45 | ) 46 | 47 | // Rule is the type or kind of a rule in the public suffix list 48 | type Rule uint8 49 | 50 | const ( 51 | None Rule = iota // not a rule, just internal node 52 | Normal // a normal rule like "com.ac" 53 | Exception // an exception rule like "!city.kobe.jp" 54 | Wildcard // a wildcard rule like "*.ar" 55 | ) 56 | 57 | // Node describes a single label in public suffix rule. 58 | // The list of rules is stored as a tree of Node nodes. 59 | type Node struct { 60 | Label string 61 | Kind Rule 62 | Sub []Node 63 | } 64 | 65 | // findLabel looks up the node with label in nodes. 66 | func findLabel(label string, nodes []Node) *Node { 67 | N := len(nodes) 68 | if N == 0 { 69 | return nil 70 | } 71 | 72 | // Fibonacci search 73 | // k, M := T[N].k, T[N].M 74 | k := 0 75 | for ; fibonacci[k] <= N; k++ { 76 | } 77 | k-- 78 | M := fibonacci[k+1] - N - 1 79 | i, p, q := fibonacci[k]-1, fibonacci[k-1], fibonacci[k-2] 80 | 81 | if label > nodes[i].Label { 82 | i -= M 83 | if p == 1 { 84 | return nil 85 | } 86 | i += q 87 | p -= q 88 | q -= p 89 | } 90 | 91 | for { 92 | if label == nodes[i].Label { 93 | return &nodes[i] 94 | } 95 | if label < nodes[i].Label { 96 | if q == 0 { 97 | return nil 98 | } 99 | i -= q 100 | p, q = q, p-q 101 | } else { 102 | if p == 1 { 103 | return nil 104 | } 105 | i += q 106 | p -= q 107 | q -= p 108 | } 109 | } 110 | panic("not reached") 111 | } 112 | 113 | // effectiveTldPlusOne retrieves TLD + 1 respective the publicsuffix + 1. 114 | // For domains which are too short (tld ony, or publixsuffix only) 115 | // the empty string is returned. 116 | // 117 | // Algorithm 118 | // 6. The public suffix is the set of labels from the domain which directly 119 | // match the labels of the prevailing rule (joined by dots). 120 | // 7. The registered or registrable domain is the public suffix plus one 121 | // additional label. 122 | func EffectiveTLDPlusOne(domain string) (ret string) { 123 | parts := strings.Split(domain, ".") 124 | m := len(parts) 125 | nodes := PublicSuffixes.Sub 126 | var np *Node 127 | for m > 0 { 128 | m-- 129 | sub := findLabel(parts[m], nodes) 130 | if sub == nil { 131 | m++ 132 | break 133 | } 134 | nodes = sub.Sub 135 | np = sub 136 | } 137 | // np now points to last matching node 138 | 139 | if np == nil || np.Kind == None { 140 | // no rule found, default is "*" 141 | if len(parts) == 2 { 142 | return domain 143 | } else if len(parts) > 2 { 144 | i := len(parts) - 1 145 | return parts[i-1] + "." + parts[i] 146 | } else { 147 | return "" 148 | } 149 | } 150 | 151 | switch np.Kind { 152 | case Normal: 153 | m-- 154 | case Exception: 155 | case Wildcard: 156 | m -= 2 157 | } 158 | if m < 0 { 159 | return "" 160 | } 161 | return strings.Join(parts[m:], ".") 162 | } 163 | 164 | // check whether domain is "specific" enough to allow domain cookies 165 | // to be set for this domain. 166 | func allowDomainCookies(domain string) bool { 167 | // TODO: own algorithm to save unused string gymnastics 168 | etldp1 := EffectiveTLDPlusOne(domain) 169 | // fmt.Printf(" etldp1 = %s\n", etldp1) 170 | return etldp1 != "" 171 | } 172 | -------------------------------------------------------------------------------- /jar.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Volker Dobler. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package cookiejar provides a in-memory storage for http cookies. 6 | // 7 | // Jar implements the http.CookieJar interface and conforms 8 | // to RFC 6265 with the one exception: Cookies from internationalized 9 | // domain names are not handled properly. 10 | // 11 | package cookiejar 12 | 13 | // BUG 14 | // Jar does not handle internationalized domain names (IDN). 15 | // The Jar should (but does not) transform the domain name of the URL 16 | // to punycode before matching the domain attribute of a recieved cookie. 17 | 18 | import ( 19 | "errors" 20 | "net" 21 | "net/http" 22 | "net/url" 23 | "sort" 24 | "strings" 25 | "sync" 26 | "time" 27 | ) 28 | 29 | // ------------------------------------------------------------------------- 30 | // Jar 31 | 32 | // A Jar implements the http.CookieJar interface. 33 | // 34 | // Jar keeps all cookies in memory and does not limit the amount of stored 35 | // cookies. 36 | // Jar will neither store cookies in a call to SetCookies nor return cookies 37 | // from a call to Cookies if the URL is a non-HTTP URL. 38 | // As HTTP would require full qualified domain names in the URL anyway, this 39 | // cookie jar implementation treats all domain names as beeing fully qualified 40 | // (absolute) even if not ending in a ".". 41 | type Jar struct { 42 | // MaxBytesPerCookie is the maximum number of bytes allowed for name plus 43 | // value of the cookie. Cookies whith len(Name)+len(Value) exceeding 44 | // MaxBytesPerCookie are not stored. 45 | // A value <= 0 indicates unlimited storage capacity. 46 | MaxBytesPerCookie int 47 | 48 | // HostCookiesOnIP may be set to true to allow a host cookie 49 | // on an IP address. Host cookies on an IP address are forbidden 50 | // by RCF 6265 but most browsers do allow them. 51 | HostCookieOnIP bool 52 | 53 | // DomainCookiesOnPublicSuffixes may be set to true to allow domain cookies 54 | // on all domains, especially on top level domains and domains 55 | // browsers normaly deny domain cookies like co.uk. 56 | // See http://publicsuffix.org/ for detailed information. 57 | DomainCookiesOnPublicSuffixes bool 58 | 59 | content storage // our cookies 60 | 61 | sync.Mutex 62 | } 63 | 64 | // NewJar sets up an empty cookie jar. 65 | // A Jar with boxedStorage can handle cookies from lots of different 66 | // domains more efficient than a Jar with flat storage. 67 | // 68 | // The created Jar will allow 4096 bytes for Name plus Value, won't accpet 69 | // host cookies for IP-addresses and won't accept a domain cookie for a 70 | // known public suffix domain. 71 | func NewJar(boxedStorage bool) *Jar { 72 | jar := Jar{ 73 | MaxBytesPerCookie: 4096, 74 | HostCookieOnIP: false, 75 | DomainCookiesOnPublicSuffixes: false, 76 | } 77 | if boxedStorage { 78 | tmp := make(boxed) 79 | jar.content = &tmp 80 | } else { 81 | tmp := make(flat, 0, 16) 82 | jar.content = &tmp 83 | } 84 | 85 | return &jar 86 | } 87 | 88 | // ------------------------------------------------------------------------- 89 | // The methods of the http.CookieJar interface. 90 | 91 | // SetCookies updates the content of jar with the cookies recieved 92 | // from a request to u. 93 | // 94 | // Cookies with len(Name) + len(Value) > MaxBytesPerCookie will be ignored 95 | // silently as well as any cookie with a malformed domain field. 96 | func (jar *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { 97 | if u == nil || !isHTTP(u) { 98 | return // this is a strict HTTP only jar 99 | } 100 | 101 | host, err := host(u) 102 | if err != nil { 103 | return 104 | } 105 | defaultpath := defaultPath(u) 106 | 107 | jar.Lock() 108 | defer jar.Unlock() 109 | 110 | for _, cookie := range cookies { 111 | if jar.MaxBytesPerCookie > 0 && len(cookie.Name)+len(cookie.Value) > jar.MaxBytesPerCookie { 112 | continue 113 | } 114 | jar.update(host, defaultpath, cookie) 115 | } 116 | } 117 | 118 | // SetCookies handles the receipt of the cookies in a reply for the given URL. 119 | func (jar *Jar) Cookies(u *url.URL) []*http.Cookie { 120 | if !isHTTP(u) { 121 | return nil // this is a strict HTTP only jar 122 | } 123 | 124 | jar.Lock() 125 | defer jar.Unlock() 126 | 127 | // set up host, path and secure 128 | host, err := host(u) 129 | if err != nil { 130 | return nil 131 | } 132 | 133 | https := isSecure(u) 134 | path := u.Path 135 | if path == "" { 136 | path = "/" 137 | } 138 | 139 | cookies := jar.content.retrieve(https, host, path) 140 | sort.Sort(sendList(cookies)) 141 | 142 | // fill into slice of http.Cookies and update LastAccess time 143 | now := time.Now() 144 | httpCookies := make([]*http.Cookie, len(cookies)) 145 | for i, cookie := range cookies { 146 | httpCookies[i] = &http.Cookie{Name: cookie.Name, Value: cookie.Value} 147 | 148 | // update last access with a strictly increasing timestamp 149 | cookie.LastAccess = now 150 | now = now.Add(time.Nanosecond) 151 | } 152 | 153 | return httpCookies 154 | } 155 | 156 | // ------------------------------------------------------------------------- 157 | // Other exported methods 158 | 159 | // All returns a copy of all non-expired cookies in the jar. 160 | func (jar *Jar) All() []Cookie { 161 | if b, ok := jar.content.(*boxed); ok { 162 | cookies := make([]Cookie, 0, 32) 163 | for _, f := range *b { 164 | for _, cookie := range *f { 165 | if cookie.Expired() { 166 | continue 167 | } 168 | cookies = append(cookies, *cookie) 169 | } 170 | } 171 | return cookies 172 | } else { 173 | f := jar.content.(*flat) 174 | cookies := make([]Cookie, 0, len(*f)) 175 | for _, cookie := range *f { 176 | if cookie.Expired() { 177 | continue 178 | } 179 | cookies = append(cookies, *cookie) 180 | } 181 | return cookies 182 | } 183 | panic("Not reached") 184 | } 185 | 186 | // Add adds all non-expired elements of cookies to the jar. Expired cookies 187 | // are silently ignored. If a cookie is already present in the jar it will 188 | // be overwritten. The LastAccess field of the given cookies are not modified. 189 | func (jar *Jar) Add(cookies []Cookie) { 190 | for _, cookie := range cookies { 191 | if cookie.Expired() { 192 | continue 193 | } 194 | c := jar.content.find(cookie.Domain, cookie.Path, cookie.Name) 195 | *c = cookie 196 | } 197 | } 198 | 199 | // Remove deletes the cookie identified by domain, path and name from jar. 200 | // The function returns true if the cookie was present in the jar. 201 | func (jar *Jar) Remove(domain, path, name string) bool { 202 | // sanitize domain 203 | domain = strings.Trim(strings.ToLower(domain), ".") 204 | existed := jar.content.delete(domain, path, name) 205 | return existed 206 | } 207 | 208 | // ------------------------------------------------------------------------- 209 | // Internals to SetCookies 210 | 211 | // the following action codes are for internal bookkeeping 212 | type updateAction int 213 | 214 | const ( 215 | invalidCookie updateAction = iota 216 | createCookie 217 | updateCookie 218 | deleteCookie 219 | noSuchCookie 220 | ) 221 | 222 | // host returns the (canonical) host from an URL u. 223 | // See RFC 6265 section 5.1.2 224 | // TODO: idns are not handeled at all. 225 | func host(u *url.URL) (host string, err error) { 226 | host = strings.ToLower(u.Host) 227 | if strings.HasSuffix(host, ".") { 228 | // treat all domain names the same: 229 | // strip trailing dot from fully qualified domain names 230 | host = host[:len(host)-1] 231 | } 232 | if strings.Index(host, ":") != -1 { 233 | host, _, err = net.SplitHostPort(host) 234 | if err != nil { 235 | return "", err 236 | } 237 | } 238 | 239 | host, err = punycodeToASCII(host) 240 | if err != nil { 241 | return "", err 242 | } 243 | 244 | return host, nil 245 | } 246 | 247 | // isSecure checks for https scheme in u. 248 | func isSecure(u *url.URL) bool { 249 | return strings.ToLower(u.Scheme) == "https" 250 | } 251 | 252 | // isHTTP checks for http or https scheme in u. 253 | func isHTTP(u *url.URL) bool { 254 | scheme := strings.ToLower(u.Scheme) 255 | return scheme == "http" || scheme == "https" 256 | } 257 | 258 | // isIP check if host is formaly an IPv4 address. 259 | func isIP(host string) bool { 260 | ip := net.ParseIP(host) 261 | if ip == nil { 262 | return false 263 | } 264 | return ip.String() == host 265 | } 266 | 267 | // This is a dummy helper function which once can do the IDN stuff. 268 | func punycodeToASCII(s string) (string, error) { 269 | return s, nil 270 | } 271 | 272 | // defaultPath returns "directory" part of path from u. Empty and 273 | // malformed paths yield "/". 274 | // See RFC 6265 section 5.1.4: 275 | // path in url | directory 276 | // --------------+------------ 277 | // "" | "/" 278 | // "xy/z" | "/" 279 | // "/abc" | "/" 280 | // "/ab/xy/km" | "/ab/xy" 281 | // "/abc/" | "/abc" 282 | // A trailing "/" is removed during storage to faciliate the test in 283 | // pathMatch(). 284 | func defaultPath(u *url.URL) string { 285 | path := u.Path 286 | 287 | // the "" and "xy/z" case 288 | if len(path) == 0 || path[0] != '/' { 289 | return "/" 290 | } 291 | 292 | // path starts with "/" --> i!=-1 293 | i := strings.LastIndex(path, "/") 294 | if i == 0 { 295 | // the "/abc" case 296 | return "/" 297 | } 298 | 299 | // the "/ab/xy/km" and "/abc/" case 300 | return path[:i] 301 | } 302 | 303 | // update is the workhorse which stores, updates or deletes the recieved cookie 304 | // in the jar. host is the (canonical) hostname from which the cookie was 305 | // recieved and defaultpath the apropriate default path ("directory" of the 306 | // request path. 307 | func (jar *Jar) update(host, defaultpath string, recieved *http.Cookie) updateAction { 308 | 309 | // Domain, hostOnly and our storage key 310 | domain, hostOnly, err := jar.domainAndType(host, recieved.Domain) 311 | if err != nil { 312 | return invalidCookie 313 | } 314 | 315 | now := time.Now() 316 | 317 | // Path 318 | path := recieved.Path 319 | if path == "" || path[0] != '/' { 320 | path = defaultpath 321 | } 322 | 323 | // Check for deletion of cookie and determine expiration time: 324 | // MaxAge takes precedence over Expires. 325 | var deleteRequest bool 326 | var expires time.Time 327 | if recieved.MaxAge < 0 { 328 | deleteRequest = true 329 | } else if recieved.MaxAge > 0 { 330 | expires = time.Now().Add(time.Duration(recieved.MaxAge) * time.Second) 331 | } else if !recieved.Expires.IsZero() { 332 | if recieved.Expires.Before(now) { 333 | deleteRequest = true 334 | } else { 335 | expires = recieved.Expires 336 | } 337 | } 338 | if deleteRequest { 339 | if existed := jar.content.delete(domain, path, recieved.Name); existed { 340 | return deleteCookie 341 | } else { 342 | return noSuchCookie 343 | } 344 | } 345 | 346 | cookie := jar.content.find(domain, path, recieved.Name) 347 | if len(cookie.Name) == 0 { 348 | // a new cookie 349 | cookie.Domain = domain 350 | cookie.HostOnly = hostOnly 351 | cookie.Path = path 352 | cookie.Name = recieved.Name 353 | cookie.Value = recieved.Value 354 | cookie.HttpOnly = recieved.HttpOnly 355 | cookie.Secure = recieved.Secure 356 | cookie.Expires = expires 357 | cookie.Created = now 358 | cookie.LastAccess = now 359 | return createCookie 360 | } 361 | 362 | // an update for a cookie 363 | cookie.HostOnly = hostOnly 364 | cookie.Value = recieved.Value 365 | cookie.HttpOnly = recieved.HttpOnly 366 | cookie.Expires = expires 367 | cookie.Secure = recieved.Secure 368 | cookie.LastAccess = now 369 | return updateCookie 370 | } 371 | 372 | var ( 373 | errNoHostname = errors.New("No hostname (IP only) available") 374 | errMalformedDomain = errors.New("Domain attribute of cookie is malformed") 375 | errTLDDomainCookie = errors.New("No domain cookies for TLDs allowed") 376 | errIllegalPSDomain = errors.New("Illegal cookie domain attribute for public suffix") 377 | errBadDomain = errors.New("Bad cookie domaine attribute") 378 | ) 379 | 380 | // domainAndType determines the Cookies Domain and HostOnly attribute. 381 | // It uses the host name the cookie was recieved from and the domain attribute 382 | // of the cookie. 383 | func (jar *Jar) domainAndType(host, domainAttr string) (domain string, hostOnly bool, err error) { 384 | if domainAttr == "" { 385 | // A RFC6265 conforming Host Cookie: no domain given 386 | return host, true, nil 387 | } 388 | 389 | // no hostname, but just an IP address 390 | if isIP(host) { 391 | if jar.HostCookieOnIP && domainAttr == host { 392 | // in non-strict mode: allow host cookie if both domain 393 | // and host are IP addresses and equal. (IE/FF/Chrome) 394 | return host, true, nil 395 | } 396 | // According to RFC 6265 domain-matching includes not beeing 397 | // an IP address. 398 | return "", false, errNoHostname 399 | } 400 | 401 | // If valid: A Domain Cookie (with one strange exeption). 402 | // We note the fact "domain cookie" as hostOnly==false and strip 403 | // possible leading "." from the domain. 404 | domain = domainAttr 405 | if domain[0] == '.' { 406 | domain = domain[1:] 407 | } 408 | 409 | if len(domain) == 0 || domain[0] == '.' { 410 | // we recieved either "Domain=." or "Domain=..some.thing" 411 | // both are illegal 412 | return "", false, errMalformedDomain 413 | } 414 | domain = strings.ToLower(domain) // see RFC 6265 section 5.2.3 415 | 416 | if domain[len(domain)-1] == '.' { 417 | // we recieved stuff like "Domain=www.example.com." 418 | // Browsers do handle such stuff (actually differently) but 419 | // RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in 420 | // requiering a reject. 4.1.2.3 is not normative, but 421 | // "Domain Matching" (5.1.3) and "Canonicalized Host Names" 422 | // (5.1.2) are. 423 | return "", false, errMalformedDomain 424 | } 425 | 426 | // Never allow Domain Cookies for TLDs. TODO: decide on "localhost". 427 | if i := strings.Index(domain, "."); i == -1 { 428 | return "", false, errTLDDomainCookie 429 | } 430 | 431 | if !jar.DomainCookiesOnPublicSuffixes { 432 | // RFC 6265 section 5.3: 433 | // 5. If the user agent is configured to reject "public 434 | // suffixes" and the domain-attribute is a public suffix: 435 | // If the domain-attribute is identical to the 436 | // canonicalized request-host: 437 | // Let the domain-attribute be the empty string. 438 | // [that is a host cookie] 439 | // Otherwise: 440 | // Ignore the cookie entirely and abort these 441 | // steps. [error] 442 | // fmt.Printf(" allowDomainCookies(%s) = %t\n", domain, allowDomainCookies(domain)) 443 | 444 | if !allowDomainCookies(domain) { 445 | // the "domain is a public suffix" case 446 | if host == domainAttr { 447 | return host, true, nil 448 | } 449 | return "", false, errIllegalPSDomain 450 | } 451 | } 452 | 453 | // domain must domain-match host: www.mycompany.com cannot 454 | // set cookies for .ourcompetitors.com. 455 | if host != domain && !strings.HasSuffix(host, "."+domain) { 456 | return "", false, errBadDomain 457 | } 458 | 459 | return domain, false, nil 460 | } 461 | -------------------------------------------------------------------------------- /jar_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Volker Dobler. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cookiejar 6 | 7 | // Tests for the exported methods of Jar. 8 | 9 | import ( 10 | "fmt" 11 | "net/http" 12 | "net/url" 13 | "sort" 14 | "strings" 15 | "testing" 16 | "time" 17 | ) 18 | 19 | // ------------------------------------------------------------------------- 20 | // Helper functions and methods to simplify testing 21 | 22 | // list yields the (non-expired) cookies of jar in a simple 23 | // and deterministic format like "name1=value1 name2=value2": 24 | // sorted alphabetical. 25 | func (jar *Jar) list() string { 26 | all := jar.All() 27 | elements := make([]string, len(all)) 28 | for i, cookie := range all { 29 | elements[i] = cookie.Name + "=" + cookie.Value 30 | } 31 | sort.Strings(elements) 32 | return strings.Join(elements, " ") 33 | } 34 | 35 | // difference compares recieved to expected (both in the above 36 | // simple format) and returns any found differences in human 37 | // readable format. 38 | func difference(recieved, expected string) string { 39 | got := list2map(recieved) 40 | want := list2map(expected) 41 | 42 | excess := "" 43 | for k, _ := range got { 44 | if _, ok := want[k]; !ok { 45 | excess += " " + k 46 | } 47 | } 48 | if excess != "" { 49 | excess = "Excess:" + excess + "; " 50 | } 51 | 52 | missing := "" 53 | for k, _ := range want { 54 | if _, ok := got[k]; !ok { 55 | missing += " " + k 56 | } 57 | } 58 | if missing != "" { 59 | missing = "Missing:" + missing 60 | } 61 | 62 | return excess + missing 63 | } 64 | 65 | func list2map(list string) map[string]struct{} { 66 | m := make(map[string]struct{}) 67 | for _, c := range strings.Fields(list) { 68 | m[c] = struct{}{} 69 | } 70 | return m 71 | } 72 | 73 | // stringRep transforms a http.Cookie slice to our 74 | // "a=1 c=3" format for cookie checking. 75 | func stringRep(cookies []*http.Cookie) string { 76 | s := "" 77 | for i, c := range cookies { 78 | if i > 0 { 79 | s += " " 80 | } 81 | s += c.Name + "=" + c.Value 82 | } 83 | return s 84 | } 85 | 86 | // parseCookie turns s (format of Set-Cookie header) into a http.Cookie. 87 | func parseCookie(s string) *http.Cookie { 88 | cookies := (&http.Response{Header: http.Header{"Set-Cookie": {s}}}).Cookies() 89 | if len(cookies) != 1 { 90 | panic(fmt.Sprintf("Wrong cookie line %q: %#v", s, cookies)) 91 | } 92 | return cookies[0] 93 | } 94 | 95 | // expiresIn creates an expires attribute delta seconds from now. 96 | func expiresIn(delta int) string { 97 | t := time.Now().Add(time.Duration(delta) * time.Second) 98 | return "expires=" + t.Format(time.RFC1123) 99 | } 100 | 101 | // parse s to an URL and panic on error 102 | func URL(s string) *url.URL { 103 | u, err := url.Parse(s) 104 | if err != nil || u.Scheme == "" || u.Host == "" { 105 | panic(fmt.Sprintf("Unable to parse URL %s.", s)) 106 | } 107 | return u 108 | } 109 | 110 | func TestTestHelpers(t *testing.T) { 111 | if difference("a=1 b=2 c=3", "c=3 a=1 b=2") != "" { 112 | t.Errorf("difference not order invariant") 113 | } 114 | 115 | jar := NewJar(false) 116 | jar.Add([]Cookie{ 117 | Cookie{Name: "a", Value: "1"}, 118 | Cookie{Name: "b", Value: "2"}, 119 | Cookie{Name: "c", Value: "3"}}) 120 | 121 | diff := difference(jar.list(), "b=2 c=3 d=4") 122 | if diff != "Excess: a=1; Missing: d=4" { 123 | t.Errorf("Got diff=%q", diff) 124 | } 125 | } 126 | 127 | // ------------------------------------------------------------------------- 128 | // jarTest: test SetCookies and Cookies methods 129 | 130 | // jarTest encapsulatest the following actions on a jar: 131 | // 1. Perform SetCookies() with fromURL and the cookies from setCookies. 132 | // 2. Check that the content of the jar matches content. 133 | // 3. For each query in test: Check that Cookies() with toURL yields the 134 | // cookies in expected. 135 | type jarTest struct { 136 | description string // the description of what this test is supposed to test 137 | fromURL string // the full URL of the request to which Set-Cookie headers where recieved 138 | setCookies []string // all the cookies recieved from fromURL in simplyfied form (see above) 139 | content string // the whole content of the jar 140 | tests []query // several testhat to expect, again as a cookie header line 141 | } 142 | 143 | // query contains one test of the cookies returned to Cookies(). 144 | type query struct { 145 | toURL string // the URL in the Cookies() call 146 | expected string // the expected list of cookies (order matters) 147 | } 148 | 149 | // run performs the actions and test of test on jar. 150 | func (test jarTest) run(t *testing.T, jar *Jar) { 151 | u := URL(test.fromURL) 152 | 153 | // populate jar with cookies 154 | setcookies := make([]*http.Cookie, len(test.setCookies)) 155 | for i, cs := range test.setCookies { 156 | setcookies[i] = parseCookie(cs) 157 | } 158 | jar.SetCookies(u, setcookies) 159 | 160 | // make sure jar content matches our expectations 161 | if jar.list() != test.content { 162 | t.Errorf("Test %q: Wrong content.\nWant %q, got %q.", 163 | test.description, test.content, jar.list()) 164 | } 165 | 166 | // test different calls to Cookies() 167 | for i, query := range test.tests { 168 | u := URL(query.toURL) 169 | cookies := jar.Cookies(u) 170 | recieved := stringRep(cookies) 171 | if recieved != query.expected { 172 | diff := difference(recieved, query.expected) 173 | if diff == "" { 174 | t.Errorf("Test %q, #%d: Wrong sorting.\nWant %q, got %q.", 175 | test.description, i, query.expected, recieved) 176 | } else { 177 | t.Errorf("Test %q, #%d: Wrong cookies.\nWant %q, got %q."+ 178 | "\n Difference: %s", 179 | test.description, i, query.expected, recieved, 180 | diff) 181 | } 182 | } 183 | } 184 | } 185 | 186 | // ------------------------------------------------------------------------- 187 | // Basic test on Jar. 188 | 189 | // basicJarTest contains test for the basic features of a cookie jar. 190 | var basicJarTests = []jarTest{ 191 | {"Retrieval of a plain cookie.", 192 | "http://www.host.test/", 193 | []string{"A=a"}, 194 | "A=a", 195 | []query{ 196 | {"http://www.host.test", "A=a"}, 197 | {"http://www.host.test/", "A=a"}, 198 | {"http://www.host.test/some/path", "A=a"}, 199 | {"https://www.host.test", "A=a"}, 200 | {"https://www.host.test/", "A=a"}, 201 | {"https://www.host.test/some/path", "A=a"}, 202 | {"ftp://www.host.test", ""}, 203 | {"ftp://www.host.test/", ""}, 204 | {"ftp://www.host.test/some/path", ""}, 205 | {"http://www.other.org", ""}, 206 | {"http://sibling.host.test", ""}, 207 | {"http://deep.www.host.test", ""}, 208 | }, 209 | }, 210 | {"HttpOnly is a noop as our jar is http only.", 211 | "http://www.host.test/", 212 | []string{"A=a; httponly"}, 213 | "A=a", 214 | []query{ 215 | {"http://www.host.test", "A=a"}, 216 | {"http://www.host.test/", "A=a"}, 217 | {"http://www.host.test/some/path", "A=a"}, 218 | {"https://www.host.test", "A=a"}, 219 | {"https://www.host.test/", "A=a"}, 220 | {"https://www.host.test/some/path", "A=a"}, 221 | {"ftp://www.host.test", ""}, 222 | {"ftp://www.host.test/", ""}, 223 | {"ftp://www.host.test/some/path", ""}, 224 | {"http://www.other.org", ""}, 225 | {"http://sibling.host.test", ""}, 226 | {"http://deep.www.host.test", ""}, 227 | }, 228 | }, 229 | {"Secure cookies are not returned to http.", 230 | "http://www.host.test/", 231 | []string{"A=a; secure"}, 232 | "A=a", 233 | []query{ 234 | {"http://www.host.test", ""}, 235 | {"http://www.host.test/", ""}, 236 | {"http://www.host.test/some/path", ""}, 237 | {"https://www.host.test", "A=a"}, 238 | {"https://www.host.test/", "A=a"}, 239 | {"https://www.host.test/some/path", "A=a"}, 240 | {"ftp://www.host.test", ""}, 241 | {"ftp://www.host.test/", ""}, 242 | {"ftp://www.host.test/some/path", ""}, 243 | {"http://www.other.org", ""}, 244 | {"http://sibling.host.test", ""}, 245 | {"http://deep.www.host.test", ""}, 246 | }, 247 | }, 248 | {"HttpOnly is a noop for secure cookies too.", 249 | "http://www.host.test/", 250 | []string{"A=a; secure; httponly"}, 251 | "A=a", 252 | []query{ 253 | {"http://www.host.test", ""}, 254 | {"http://www.host.test/", ""}, 255 | {"http://www.host.test/some/path", ""}, 256 | {"https://www.host.test", "A=a"}, 257 | {"https://www.host.test/", "A=a"}, 258 | {"https://www.host.test/some/path", "A=a"}, 259 | {"ftp://www.host.test", ""}, 260 | {"ftps://www.host.test", ""}, 261 | {"ftp://www.host.test/", ""}, 262 | {"ftp://www.host.test/some/path", ""}, 263 | {"http://www.other.org", ""}, 264 | {"http://sibling.host.test", ""}, 265 | {"http://deep.www.host.test", ""}, 266 | }, 267 | }, 268 | {"Cookie with explicit path.", 269 | "http://www.host.test/", 270 | []string{"A=a; path=/some/path"}, 271 | "A=a", 272 | []query{ 273 | {"http://www.host.test", ""}, 274 | {"http://www.host.test/", ""}, 275 | {"http://www.host.test/some", ""}, 276 | {"http://www.host.test/some/", ""}, 277 | {"http://www.host.test/some/path", "A=a"}, 278 | {"http://www.host.test/some/paths", ""}, 279 | {"http://www.host.test/some/path/foo", "A=a"}, 280 | {"http://www.host.test/some/path/foo/", "A=a"}, 281 | }, 282 | }, 283 | {"Cookie with implicit path, variant a: path is directoy.", 284 | "http://www.host.test/some/path/", 285 | []string{"A=a"}, 286 | "A=a", 287 | []query{ 288 | {"http://www.host.test", ""}, 289 | {"http://www.host.test/", ""}, 290 | {"http://www.host.test/some", ""}, 291 | {"http://www.host.test/some/", ""}, 292 | {"http://www.host.test/some/path", "A=a"}, 293 | {"http://www.host.test/some/paths", ""}, 294 | {"http://www.host.test/some/path/foo", "A=a"}, 295 | {"http://www.host.test/some/path/foo/", "A=a"}, 296 | }, 297 | }, 298 | {"Cookie with implicit path, variant b:: path is not directory", 299 | "http://www.host.test/some/path/index.html", 300 | []string{"A=a"}, 301 | "A=a", 302 | []query{ 303 | {"http://www.host.test", ""}, 304 | {"http://www.host.test/", ""}, 305 | {"http://www.host.test/some", ""}, 306 | {"http://www.host.test/some/", ""}, 307 | {"http://www.host.test/some/path", "A=a"}, 308 | {"http://www.host.test/some/paths", ""}, 309 | {"http://www.host.test/some/path/foo", "A=a"}, 310 | {"http://www.host.test/some/path/foo/", "A=a"}, 311 | }, 312 | }, 313 | {"Cookie with implicit path, version c: no path in url at all.", 314 | "http://www.host.test", 315 | []string{"A=a"}, 316 | "A=a", 317 | []query{ 318 | {"http://www.host.test", "A=a"}, 319 | {"http://www.host.test/", "A=a"}, 320 | {"http://www.host.test/some/path", "A=a"}, 321 | }, 322 | }, 323 | {"Returned cookies are sorted by path length.", 324 | "http://www.host.test/", 325 | []string{ 326 | "A=a; path=/foo/bar", 327 | "B=b; path=/foo/bar/baz/qux", 328 | "C=c; path=/foo/bar/baz", 329 | "D=d; path=/foo"}, 330 | "A=a B=b C=c D=d", 331 | []query{ 332 | {"http://www.host.test/foo/bar/baz/qux", "B=b C=c A=a D=d"}, 333 | {"http://www.host.test/foo/bar/baz/", "C=c A=a D=d"}, 334 | {"http://www.host.test/foo/bar", "A=a D=d"}, 335 | }, 336 | }, 337 | {"Returned cookies are sorted by creation time if path lengths are the same.", 338 | "http://www.host.test/", 339 | []string{ 340 | "A=a; path=/foo/bar", 341 | "X=x; path=/foo/bar", 342 | "Y=y; path=/foo/bar/baz/qux", 343 | "B=b; path=/foo/bar/baz/qux", 344 | "C=c; path=/foo/bar/baz", 345 | "W=w; path=/foo/bar/baz", 346 | "Z=z; path=/foo", 347 | "D=d; path=/foo"}, 348 | "A=a B=b C=c D=d W=w X=x Y=y Z=z", 349 | []query{ 350 | {"http://www.host.test/foo/bar/baz/qux", "Y=y B=b C=c W=w A=a X=x Z=z D=d"}, 351 | {"http://www.host.test/foo/bar/baz/", "C=c W=w A=a X=x Z=z D=d"}, 352 | {"http://www.host.test/foo/bar", "A=a X=x Z=z D=d"}, 353 | }, 354 | }, 355 | {"Several cookies with the same name but different paths and/or domain are sorted on path length and creation time", 356 | "http://www.test.org/", 357 | []string{"A=1; path=/", 358 | "A=2; path=/path", 359 | "A=3; path=/quux", 360 | "A=4; path=/path/foo", 361 | "A=5; domain=.test.org; path=/path", 362 | "A=6; domain=.test.org; path=/quux", 363 | "A=7; domain=.test.org; path=/path/foo", 364 | }, 365 | "A=1 A=2 A=3 A=4 A=5 A=6 A=7", 366 | []query{ 367 | {"http://www.test.org/path", "A=2 A=5 A=1"}, 368 | {"http://www.test.org/path/foo", "A=4 A=7 A=2 A=5 A=1"}, 369 | }, 370 | }, 371 | } 372 | 373 | func TestBasicFeatures(t *testing.T) { 374 | for _, test := range basicJarTests { 375 | jar := NewJar(false) 376 | test.run(t, jar) 377 | } 378 | for _, test := range basicJarTests { 379 | jar := NewJar(true) 380 | test.run(t, jar) 381 | } 382 | } 383 | 384 | var updateAndDeleteTests = []jarTest{ 385 | {"Set some initial cookies", 386 | "http://www.example.com", 387 | []string{"a=1", "b=2; secure", "c=3; httponly", "d=4; secure; httponly"}, 388 | "a=1 b=2 c=3 d=4", 389 | []query{ 390 | {"http://www.example.com", "a=1 c=3"}, 391 | {"https://www.example.com", "a=1 b=2 c=3 d=4"}, 392 | }, 393 | }, 394 | {"We can update all of them to new value via http", 395 | "http://www.example.com", 396 | []string{"a=w", "b=x; secure", "c=y; httponly", "d=z; secure; httponly"}, 397 | "a=w b=x c=y d=z", 398 | []query{ 399 | {"http://www.example.com", "a=w c=y"}, 400 | {"https://www.example.com", "a=w b=x c=y d=z"}, 401 | }, 402 | }, 403 | {"We can clear a Secure flag from a http request", 404 | "http://www.example.com/", 405 | []string{"b=xx", "d=zz; httponly"}, 406 | "a=w b=xx c=y d=zz", 407 | []query{{"http://www.example.com", "a=w b=xx c=y d=zz"}}, 408 | }, 409 | {"We can delete all of them", 410 | "http://www.example.com/", 411 | []string{"a=1; max-Age=-1", // delete via MaxAge 412 | "b=2; " + expiresIn(-10), // delete via Expires 413 | "c=2; max-age=-1; " + expiresIn(-10), // delete via both 414 | "d=4; max-age=-1; " + expiresIn(10)}, // maxAge takes precedence 415 | "", 416 | []query{{"http://www.example.com", ""}}, 417 | }, 418 | } 419 | 420 | func TestUpdateAndDelete(t *testing.T) { 421 | jar := NewJar(false) 422 | for _, test := range updateAndDeleteTests { 423 | test.run(t, jar) 424 | } 425 | jar = NewJar(true) 426 | for _, test := range updateAndDeleteTests { 427 | test.run(t, jar) 428 | } 429 | } 430 | 431 | var cookieDeletionTests = []jarTest{ 432 | {"TestCookieDeletion: Fill jar part 1.", 433 | "http://www.host.test", 434 | []string{ 435 | "A=1", 436 | "A=2; path=/foo", 437 | "A=3; domain=.host.test", 438 | "A=4; path=/foo; domain=.host.test"}, 439 | "A=1 A=2 A=3 A=4", 440 | []query{{"http://www.host.test/foo", "A=2 A=4 A=1 A=3"}}, 441 | }, 442 | {"TestCookieDeletion: Fill jar part 2.", 443 | "http://www.google.com", 444 | []string{ 445 | "A=6", 446 | "A=7; path=/foo", 447 | "A=8; domain=.google.com", 448 | "A=9; path=/foo; domain=.google.com"}, 449 | "A=1 A=2 A=3 A=4 A=6 A=7 A=8 A=9", 450 | []query{ 451 | {"http://www.host.test/foo", "A=2 A=4 A=1 A=3"}, 452 | {"http://www.google.com/foo", "A=7 A=9 A=6 A=8"}, 453 | }, 454 | }, 455 | {"TestCookieDeletion: Delete A7", 456 | "http://www.google.com", 457 | []string{"A=; path=/foo; max-age=-1"}, 458 | "A=1 A=2 A=3 A=4 A=6 A=8 A=9", 459 | []query{ 460 | {"http://www.host.test/foo", "A=2 A=4 A=1 A=3"}, 461 | {"http://www.google.com/foo", "A=9 A=6 A=8"}, 462 | }, 463 | }, 464 | {"TestCookieDeletion: Delete A4", 465 | "http://www.host.test", 466 | []string{"A=; path=/foo; domain=host.test; max-age=-1"}, 467 | "A=1 A=2 A=3 A=6 A=8 A=9", 468 | []query{ 469 | {"http://www.host.test/foo", "A=2 A=1 A=3"}, 470 | {"http://www.google.com/foo", "A=9 A=6 A=8"}, 471 | }, 472 | }, 473 | {"TestCookieDeletion: Delete A6", 474 | "http://www.google.com", 475 | []string{"A=; max-age=-1"}, 476 | "A=1 A=2 A=3 A=8 A=9", 477 | []query{ 478 | {"http://www.host.test/foo", "A=2 A=1 A=3"}, 479 | {"http://www.google.com/foo", "A=9 A=8"}, 480 | }, 481 | }, 482 | {"TestCookieDeletion: Delete A3", 483 | "http://www.host.test", 484 | []string{"A=; domain=host.test; max-age=-1"}, 485 | "A=1 A=2 A=8 A=9", 486 | []query{ 487 | {"http://www.host.test/foo", "A=2 A=1"}, 488 | {"http://www.google.com/foo", "A=9 A=8"}, 489 | }, 490 | }, 491 | {"TestCookieDeletion: no cross-domain delete", 492 | "http://www.host.test", 493 | []string{"A=; domain=google.com; max-age=-1", "A=; path=/foo; domain=google.com; max-age=-1"}, 494 | "A=1 A=2 A=8 A=9", 495 | []query{ 496 | {"http://www.host.test/foo", "A=2 A=1"}, 497 | {"http://www.google.com/foo", "A=9 A=8"}, 498 | }, 499 | }, 500 | {"TestCookieDeletion: Delete A8 and A9", 501 | "http://www.google.com", 502 | []string{"A=; domain=google.com; max-age=-1", "A=; path=/foo; domain=google.com; max-age=-1"}, 503 | "A=1 A=2", 504 | []query{ 505 | {"http://www.host.test/foo", "A=2 A=1"}, 506 | {"http://www.google.com/foo", ""}, 507 | }, 508 | }, 509 | } 510 | 511 | func TestCookieDeletion(t *testing.T) { 512 | jar := NewJar(false) 513 | for _, test := range cookieDeletionTests { 514 | test.run(t, jar) 515 | } 516 | jar = NewJar(true) 517 | for _, test := range cookieDeletionTests { 518 | test.run(t, jar) 519 | } 520 | } 521 | 522 | func TestMaxBytesPerCookie(t *testing.T) { 523 | jar := NewJar(false) 524 | jarTest{"Fill jar", "http://www.host.test", 525 | []string{"a=1", "longcookiename=2"}, 526 | "a=1 longcookiename=2", 527 | []query{{"http://www.host.test", "a=1 longcookiename=2"}}, 528 | }.run(t, jar) 529 | jar.MaxBytesPerCookie = 8 530 | jarTest{"Too big cookies", "http://www.host.test", 531 | []string{"b=3", "verylongcookiename=4", "c=verylongvalue"}, 532 | "a=1 b=3 longcookiename=2", 533 | []query{{"http://www.host.test", "a=1 longcookiename=2 b=3"}}, 534 | }.run(t, jar) 535 | } 536 | 537 | func TestHostCookieOnIP(t *testing.T) { 538 | jar := NewJar(false) 539 | jarTest{"Dissallow host cookie on IP", "http://127.0.0.1", 540 | []string{"a=1; domain=127.0.0.1"}, 541 | "", 542 | []query{{"http://127.0.0.1", ""}}, 543 | }.run(t, jar) 544 | jar.HostCookieOnIP = true 545 | jarTest{"Allow host cookie on IP", "http://127.0.0.1", 546 | []string{"b=2; domain=127.0.0.1"}, 547 | "b=2", 548 | []query{ 549 | {"http://127.0.0.1", "b=2"}, 550 | // The following cannot happen but does test the 551 | // expected behaviour of beeing a host cookie. 552 | {"http://www.127.0.0.1", ""}, 553 | }, 554 | }.run(t, jar) 555 | f := jar.content.(*flat) 556 | if (*f)[0].HostOnly != true { 557 | t.Errorf("Not a host cookie.") 558 | } 559 | } 560 | 561 | func TestDomainCookiesOnPublicSuffixes(t *testing.T) { 562 | jar := NewJar(false) 563 | jarTest{"Dissallow PS", "http://www.bbc.co.uk", 564 | []string{"a=1", "b=2; domain=co.uk"}, 565 | "a=1", 566 | []query{{"http://www.bbc.co.uk", "a=1"}}, 567 | }.run(t, jar) 568 | jar.DomainCookiesOnPublicSuffixes = true 569 | jarTest{"Allow PS", "http://www.bbc.co.uk", 570 | []string{"c=3; domain=co.uk"}, 571 | "a=1 c=3", 572 | []query{{"http://www.bbc.co.uk", "a=1 c=3"}}, 573 | }.run(t, jar) 574 | } 575 | 576 | func TestExpiration(t *testing.T) { 577 | for _, b := range []bool{true, false} { 578 | jar := NewJar(b) 579 | jarTest{ 580 | "Fill jar", 581 | "http://www.host.test", 582 | []string{ 583 | "a=1", 584 | "b=2; max-age=1", 585 | "c=3; " + expiresIn(1), 586 | "d=4; max-age=100", 587 | }, 588 | "a=1 b=2 c=3 d=4", 589 | []query{{"http://www.host.test", "a=1 b=2 c=3 d=4"}}, 590 | }.run(t, jar) 591 | time.Sleep(1005 * time.Millisecond) 592 | 593 | jarTest{ 594 | "Check jar", 595 | "http://www.host.test", 596 | []string{}, 597 | "a=1 d=4", 598 | []query{{"http://www.host.test", "a=1 d=4"}}, 599 | }.run(t, jar) 600 | 601 | // make sure the expired cookies get reused 602 | jarTest{ 603 | "Adding two more", 604 | "http://www.host.test", 605 | []string{"e=5", "f=6"}, 606 | "a=1 d=4 e=5 f=6", 607 | []query{{"http://www.host.test", "a=1 d=4 e=5 f=6"}}, 608 | }.run(t, jar) 609 | if f, ok := jar.content.(*flat); ok { 610 | if len(*f) != 4 { 611 | t.Errorf("Strange jar size %d", len(*f)) 612 | } 613 | } else { 614 | // TODO: test it here too? 615 | } 616 | } 617 | } 618 | 619 | // ------------------------------------------------------------------------- 620 | // Test derived from chromiums cookie_store_unittest.h. 621 | // See http://src.chromium.org/viewvc/chrome/trunk/src/net/cookies/cookie_store_unittest.h?revision=159685&content-type=text/plain 622 | // Some of these tests (e.g. DomainWithTrailingDotTest) are in a bad condition 623 | // (aka buggy), so not all have been ported. 624 | 625 | func TestChromiumDomainTest(t *testing.T) { 626 | for _, b := range []bool{true, false} { 627 | jar := NewJar(b) 628 | wwwGoogleIzzle := URL("http://www.google.izzle") 629 | fooWwwGoogleIzzle := URL("http://foo.www.google.izzle") 630 | aIzzle := URL("http://a.izzle") 631 | barWwwGoogleIzzle := URL("http://bar.www.google.izzle") 632 | 633 | jar.SetCookies(wwwGoogleIzzle, []*http.Cookie{parseCookie("A=B")}) 634 | if got := stringRep(jar.Cookies(wwwGoogleIzzle)); got != "A=B" { 635 | t.Errorf("Got " + got) 636 | } 637 | 638 | jar.SetCookies(wwwGoogleIzzle, []*http.Cookie{parseCookie("C=D; domain=.google.izzle")}) 639 | if got := stringRep(jar.Cookies(wwwGoogleIzzle)); got != "A=B C=D" { 640 | t.Errorf("Got " + got) 641 | } 642 | 643 | // verify A is a host cokkie and not accessible from subdomain 644 | if got := stringRep(jar.Cookies(fooWwwGoogleIzzle)); got != "C=D" { 645 | t.Errorf("Got " + got) 646 | } 647 | 648 | // verify domain cookies are found on proper domain 649 | jar.SetCookies(wwwGoogleIzzle, []*http.Cookie{parseCookie("E=F; domain=.www.google.izzle")}) 650 | if got := stringRep(jar.Cookies(wwwGoogleIzzle)); got != "A=B C=D E=F" { 651 | t.Errorf("Got " + got) 652 | } 653 | 654 | // leading dots in domain attributes are optional 655 | jar.SetCookies(wwwGoogleIzzle, []*http.Cookie{parseCookie("G=H; domain=www.google.izzle")}) 656 | if got := stringRep(jar.Cookies(wwwGoogleIzzle)); got != "A=B C=D E=F G=H" { 657 | t.Errorf("Got " + got) 658 | } 659 | 660 | // verify domain enforcement works (this one is bogus if public 661 | // suffixes are used: .izzle is considered a public suffix and 662 | // the domain cookie is silently rejected.) 663 | jar.SetCookies(wwwGoogleIzzle, []*http.Cookie{parseCookie("I=J; domain=.izzle")}) 664 | if got := stringRep(jar.Cookies(aIzzle)); got != "" { 665 | t.Errorf("Got " + got) 666 | } 667 | jar.SetCookies(wwwGoogleIzzle, []*http.Cookie{parseCookie("K=L; domain=.bar.www.google.izzle")}) 668 | if got := stringRep(jar.Cookies(barWwwGoogleIzzle)); got != "C=D E=F G=H" { 669 | t.Errorf("Got " + got) 670 | } 671 | if got := stringRep(jar.Cookies(wwwGoogleIzzle)); got != "A=B C=D E=F G=H" { 672 | t.Errorf("Got " + got) 673 | } 674 | } 675 | } 676 | 677 | // tests which can be done with the help of jarTest 678 | var chromiumTests = []jarTest{ 679 | {"DomainWithTrailingDotTest: Trailing dots in domain attributes are illegal", 680 | "http://www.google.com/", 681 | []string{"a=1; domain=.www.google.com.", "b=2; domain=.www.google.com.."}, 682 | "", 683 | []query{ 684 | {"http://www.google.com", ""}, 685 | }, 686 | }, 687 | {"ValidSubdomainTest part 1: domain cookies on higer level domains are not" + 688 | "visible on lower level domains", 689 | "http://a.b.c.d.com", 690 | []string{ 691 | "a=1; domain=.a.b.c.d.com", 692 | "b=2; domain=.b.c.d.com", 693 | "c=3; domain=.c.d.com", 694 | "d=4; domain=.d.com"}, 695 | "a=1 b=2 c=3 d=4", 696 | []query{ 697 | {"http://a.b.c.d.com", "a=1 b=2 c=3 d=4"}, 698 | {"http://b.c.d.com", "b=2 c=3 d=4"}, 699 | {"http://c.d.com", "c=3 d=4"}, 700 | {"http://d.com", "d=4"}, 701 | }, 702 | }, 703 | {"ValidSubdomainTest part 2: 'same' cookie on several sub-domains", 704 | "http://a.b.c.d.com", 705 | []string{ 706 | "a=1; domain=.a.b.c.d.com", 707 | "b=2; domain=.b.c.d.com", 708 | "c=3; domain=.c.d.com", 709 | "d=4; domain=.d.com", 710 | "X=bcd; domain=.b.c.d.com", 711 | "X=cd; domain=.c.d.com"}, 712 | "X=bcd X=cd a=1 b=2 c=3 d=4", 713 | []query{ 714 | {"http://b.c.d.com", "b=2 c=3 d=4 X=bcd X=cd"}, 715 | {"http://c.d.com", "c=3 d=4 X=cd"}, 716 | }, 717 | }, 718 | {"InvalidDomainTest 1: ignore cookies whose domain attribute does not match originatin domain", 719 | "http://foo.bar.com", 720 | []string{"a=1; domain=.yo.foo.bar.com", 721 | "b=2; domain=.foo.com", 722 | "c=3; domain=.bar.foo.com", 723 | "d=4; domain=.foo.bar.com.net", 724 | "e=5; domain=ar.com", 725 | "f=6; domain=.", 726 | "g=7; domain=/", 727 | "h=8; domain=http://foo.bar.com", 728 | "i=9; domain=..foo.bar.com", 729 | "j=10; domain=..bar.com", 730 | "k=11; domain=.foo.bar.com?blah", 731 | "l=12; domain=.foo.bar.com/blah", 732 | "m=12; domain=.foo.bar.com:80", 733 | "n=14; domain=.foo.bar.com:", 734 | "o=15; domain=.foo.bar.com#sup", 735 | }, 736 | "", // jar is empty 737 | []query{{"http://foo.bar.com", ""}}, 738 | }, 739 | {"InvalidDomainTest 2: special case with same domain and registry", 740 | "http://foo.com.com", 741 | []string{"a=1; domain=.foo.com.com.com"}, 742 | "", 743 | []query{{"http://foo.bar.com", ""}}, 744 | }, 745 | {"DomainWithoutLeadingDotTest 1: Leading dot is optional for domain cookies", 746 | "http://manage.hosted.filefront.com", 747 | []string{"a=1; domain=filefront.com"}, 748 | "a=1", 749 | []query{{"http://www.filefront.com", "a=1"}}, 750 | }, 751 | {"DomainWithoutLeadingDotTest 2: still domain cookie, even if domain and " + 752 | "domain attribute match exactly", 753 | "http://www.google.com", 754 | []string{"a=1; domain=www.google.com"}, 755 | "a=1", 756 | []query{ 757 | {"http://www.google.com", "a=1"}, 758 | {"http://sub.www.google.com", "a=1"}, 759 | {"http://something-else.com", ""}, 760 | }, 761 | }, 762 | {"CaseInsensitiveDomainTest", 763 | "http://www.google.com", 764 | []string{"a=1; domain=.GOOGLE.COM", "b=2; domain=.www.gOOgLE.coM"}, 765 | "a=1 b=2", 766 | []query{{"http://www.google.com", "a=1 b=2"}}, 767 | }, 768 | {"TestIpAddress 1: allow host cookies on IP address", 769 | "http://1.2.3.4/foo", 770 | []string{"a=1; path=/"}, 771 | "a=1", 772 | []query{{"http://1.2.3.4/foo", "a=1"}}, 773 | }, 774 | {"TestIpAddress 2: disallow domain cookies on IP address", 775 | "http://1.2.3.4/foo", 776 | []string{"a=1; domain=.1.2.3.4", "b=2; domain=.3.4"}, 777 | "", 778 | []query{{"http://1.2.3.4/foo", ""}}, 779 | }, 780 | {"TestIpAddress 3: really disallow domain cookies on IP address (even if IE&FF allow this case)", 781 | "http://1.2.3.4/foo", 782 | []string{"a=1; domain=1.2.3.4"}, 783 | "", 784 | []query{{"http://1.2.3.4/foo", ""}}, 785 | }, 786 | {"TestNonDottedAndTLD 1: allow on com but only as host cookie", 787 | "http://com/", 788 | []string{"a=1", "b=2; domain=.com", "c=3; domain=com"}, 789 | "a=1", 790 | []query{ 791 | {"http://com/", "a=1"}, 792 | {"http://no-cookies.com/", ""}, 793 | {"http://.com/", ""}, 794 | }, 795 | }, 796 | {"TestNonDottedAndTLD 2: treat com. same as com", 797 | "http://com./index.html", 798 | []string{"a=1"}, 799 | "a=1", 800 | []query{ 801 | {"http://com./index.html", "a=1"}, 802 | {"http://no-cookies.com./index.html", ""}, 803 | }, 804 | }, 805 | {"TestNonDottedAndTLD 3: cannot set host cookie from subdomain", 806 | "http://a.b", 807 | []string{"a=1; domain=.b", "b=2; domain=b"}, 808 | "", 809 | []query{{"http://bar.foo", ""}}, 810 | }, 811 | {"TestNonDottedAndTLD 4: same as above but for known TLD (com)", 812 | "http://google.com", 813 | []string{"a=1; domain=.com", "b=2; domain=com"}, 814 | "", 815 | []query{{"http://google.com", ""}}, 816 | }, 817 | {"TestNonDottedAndTLD 5: cannot set on TLD which is dotted", 818 | "http://google.co.uk", 819 | []string{"a=1; domain=.co.uk", "b=2; domain=.uk"}, 820 | "", 821 | []query{ 822 | {"http://google.co.uk", ""}, 823 | {"http://else.co.com", ""}, 824 | {"http://else.uk", ""}, 825 | }, 826 | }, 827 | {"TestNonDottedAndTLD 6: intranet URLs may set host cookies only", 828 | "http://b", 829 | []string{"a=1", "b=2; domain=.b", "c=3; domain=b"}, 830 | "a=1", 831 | []query{{"http://b", "a=1"}}, 832 | }, 833 | {"TestHostEndsWithDot: this seemes to be disallowed by RFC6265 even if browsers do other", 834 | "http://www.google.com", 835 | []string{"a=1", "b=2; domain=.www.google.com."}, 836 | "a=1", 837 | []query{{"http://www.google.com", "a=1"}}, 838 | }, 839 | {"PathTest", 840 | "http://www.google.izzle", 841 | []string{"a=1; path=/wee"}, 842 | "a=1", 843 | []query{ 844 | {"http://www.google.izzle/wee", "a=1"}, 845 | {"http://www.google.izzle/wee/", "a=1"}, 846 | {"http://www.google.izzle/wee/war", "a=1"}, 847 | {"http://www.google.izzle/wee/war/more/more", "a=1"}, 848 | {"http://www.google.izzle/weehee", ""}, 849 | {"http://www.google.izzle/", ""}, 850 | }, 851 | }, 852 | } 853 | 854 | func TestChromiumTestcases(t *testing.T) { 855 | for _, test := range chromiumTests { 856 | jar := NewJar(false) 857 | test.run(t, jar) 858 | jar = NewJar(true) 859 | test.run(t, jar) 860 | } 861 | } 862 | 863 | var chromiumDeletionTests = []jarTest{ 864 | {"TestCookieDeletion: Create session cookie a1", 865 | "http://www.google.com", 866 | []string{"a=1"}, 867 | "a=1", 868 | []query{{"http://www.google.com", "a=1"}}, 869 | }, 870 | {"TestCookieDeletion: Delete sc a1 via MaxAge", 871 | "http://www.google.com", 872 | []string{"a=1; max-age=-1"}, 873 | "", 874 | []query{{"http://www.google.com", ""}}, 875 | }, 876 | {"TestCookieDeletion: Create session cookie b2", 877 | "http://www.google.com", 878 | []string{"b=2"}, 879 | "b=2", 880 | []query{{"http://www.google.com", "b=2"}}, 881 | }, 882 | {"TestCookieDeletion: Delete sc b2 via Expires", 883 | "http://www.google.com", 884 | []string{"b=2; " + expiresIn(-10)}, 885 | "", 886 | []query{{"http://www.google.com", ""}}, 887 | }, 888 | {"TestCookieDeletion: Create persistent cookie c3", 889 | "http://www.google.com", 890 | []string{"c=3; max-age=3600"}, 891 | "c=3", 892 | []query{{"http://www.google.com", "c=3"}}, 893 | }, 894 | {"TestCookieDeletion: Delete pc c3 via MaxAge", 895 | "http://www.google.com", 896 | []string{"c=3; max-age=-1"}, 897 | "", 898 | []query{{"http://www.google.com", ""}}, 899 | }, 900 | {"TestCookieDeletion: Create persistant cookie d4", 901 | "http://www.google.com", 902 | []string{"d=4; max-age=3600"}, 903 | "d=4", 904 | []query{{"http://www.google.com", "d=4"}}, 905 | }, 906 | {"TestCookieDeletion: Delete pc d4 via Expires", 907 | "http://www.google.com", 908 | []string{"d=4; " + expiresIn(-10)}, 909 | "", 910 | []query{{"http://www.google.com", ""}}, 911 | }, 912 | } 913 | 914 | func TestChromiumCookieDeletion(t *testing.T) { 915 | jar := NewJar(true) 916 | for _, test := range chromiumDeletionTests { 917 | test.run(t, jar) 918 | } 919 | jar = NewJar(false) 920 | for _, test := range chromiumDeletionTests { 921 | test.run(t, jar) 922 | } 923 | } 924 | 925 | // ------------------------------------------------------------------------- 926 | // Test for the other exported methods 927 | 928 | func TestAdd(t *testing.T) { 929 | for _, b := range []bool{true, false} { 930 | jar := NewJar(b) 931 | 932 | // a=1 gets added, b=2 is ignored as already expired, c=3 is a session cookie 933 | jar.Add([]Cookie{ 934 | Cookie{ 935 | Name: "a", Value: "1", 936 | Domain: "www.host.test", 937 | Path: "/foo", 938 | Expires: time.Now().Add(time.Hour), 939 | Secure: true, 940 | HostOnly: false, 941 | }, 942 | Cookie{ 943 | Name: "b", Value: "2", 944 | Domain: "www.host.test", 945 | Path: "/", 946 | Expires: time.Now().Add(-time.Minute), // expired 947 | }, 948 | Cookie{ 949 | Name: "c", Value: "3", 950 | Domain: "www.google.com", 951 | Path: "/", 952 | Expires: time.Time{}, // zero value = session cookie 953 | }, 954 | }) 955 | if jar.list() != "a=1 c=3" { 956 | t.Fatalf("Wrong content. Got %q", jar.list()) 957 | } 958 | 959 | // adding d=4 960 | jar.Add([]Cookie{ 961 | Cookie{ 962 | Name: "d", Value: "4", 963 | Domain: "www.somewhere.else", 964 | Path: "/", 965 | Expires: time.Now().Add(time.Hour), 966 | Secure: true, 967 | HostOnly: false, 968 | }, 969 | }) 970 | if jar.list() != "a=1 c=3 d=4" { 971 | t.Fatalf("Wrong content. Got %q", jar.list()) 972 | } 973 | 974 | // updating a 975 | jar.Add([]Cookie{ 976 | Cookie{ 977 | Name: "a", Value: "X", 978 | Domain: "www.host.test", 979 | Path: "/foo", 980 | Expires: time.Now().Add(time.Hour), 981 | Secure: false, 982 | HostOnly: true, 983 | Created: time.Now().Add(-time.Hour), 984 | LastAccess: time.Now().Add(-time.Minute), 985 | }, 986 | }) 987 | if jar.list() != "a=X c=3 d=4" { 988 | t.Fatalf("Wrong content. Got %q", jar.list()) 989 | } 990 | u := URL("http://www.host.test/foo/bar") // not https! 991 | recieved := stringRep(jar.Cookies(u)) 992 | if recieved != "a=X" { 993 | t.Errorf("Wrong cookies. Got %q", recieved) 994 | } 995 | } 996 | } 997 | 998 | func TestRemove(t *testing.T) { 999 | for _, b := range []bool{true, false} { 1000 | jar := NewJar(b) 1001 | 1002 | jar.Add([]Cookie{ 1003 | Cookie{ 1004 | Name: "a", Value: "1", 1005 | Domain: "www.host.test", 1006 | Path: "/foo", 1007 | }, 1008 | Cookie{ 1009 | Name: "a", Value: "2", 1010 | Domain: "www.host.test", 1011 | Path: "/bar", 1012 | }, 1013 | Cookie{ 1014 | Name: "a", Value: "3", 1015 | Domain: "www.google.com", 1016 | Path: "/bar", 1017 | }, 1018 | Cookie{ 1019 | Name: "b", Value: "4", 1020 | Domain: "www.google.com", 1021 | Path: "/bar", 1022 | }, 1023 | }) 1024 | if jar.list() != "a=1 a=2 a=3 b=4" { 1025 | t.Fatalf("Wrong content. Got %q", jar.list()) 1026 | } 1027 | 1028 | // cannot remove nonexisting cookie 1029 | if jar.Remove("www.host.test", "/bar", "x") { 1030 | t.Errorf("Could remove non-existing cookie x.") 1031 | } 1032 | 1033 | // remove a=2 1034 | if !jar.Remove("www.host.test", "/bar", "a") { 1035 | t.Errorf("Could not remove cookie a=2.") 1036 | } 1037 | if jar.list() != "a=1 a=3 b=4" { 1038 | t.Fatalf("Wrong content. Got %q", jar.list()) 1039 | } 1040 | 1041 | // remove a=3 1042 | if !jar.Remove("www.google.com", "/bar", "a") { 1043 | t.Errorf("Could not remove cookie a=3.") 1044 | } 1045 | if jar.list() != "a=1 b=4" { 1046 | t.Fatalf("Wrong content. Got %q", jar.list()) 1047 | } 1048 | // cannot remove an already removed cookie 1049 | if jar.Remove("www.google.com", "/bar", "a") { 1050 | t.Errorf("Could re-remove removed cookie a=3.") 1051 | } 1052 | } 1053 | } 1054 | 1055 | // ------------------------------------------------------------------------- 1056 | // Test update of LastAccess 1057 | 1058 | func TestLastAccess(t *testing.T) { 1059 | for _, b := range []bool{true, false} { 1060 | f := "Mon, 02 Jan 2006 15:04:05.9999999 MST" // RFC1123 with sub-musec precision 1061 | // helper to get the two cookies named "a" and "b" from a two-cookie jar. 1062 | aAndB := func(jar *Jar) (cookieA, cookieB Cookie) { 1063 | all := jar.All() 1064 | if len(all) != 2 { 1065 | panic(fmt.Sprintf("Expected two cookies. Got %", jar.list())) 1066 | } 1067 | // order in all is arbitary 1068 | if all[0].Name == "a" { 1069 | cookieA = all[0] 1070 | cookieB = all[1] 1071 | } else { 1072 | cookieA = all[1] 1073 | cookieB = all[0] 1074 | } 1075 | if cookieA.Name != "a" || cookieB.Name != "b" { 1076 | panic(fmt.Sprintf("Expected cookies a and b. Got %", jar.list())) 1077 | } 1078 | return 1079 | } 1080 | 1081 | jar := NewJar(b) 1082 | t0 := time.Now().Add(-time.Second) 1083 | 1084 | jar.Add([]Cookie{ 1085 | Cookie{ 1086 | Name: "a", Value: "1", 1087 | Domain: "www.host.test", 1088 | Path: "/foo", 1089 | LastAccess: t0, 1090 | }, 1091 | Cookie{ 1092 | Name: "b", Value: "2", 1093 | Domain: "www.host.test", 1094 | Path: "/bar", 1095 | LastAccess: t0, 1096 | }, 1097 | }) 1098 | 1099 | // access a=1 1100 | u := URL("http://www.host.test/foo/bar") 1101 | recieved := stringRep(jar.Cookies(u)) 1102 | if recieved != "a=1" { 1103 | t.Errorf("Wrong cookies. Got %q", recieved) 1104 | } 1105 | 1106 | // b=2 keeps last access time while a=1 gets its updated 1107 | cookieA, cookieB := aAndB(jar) 1108 | t1 := time.Now() 1109 | if !cookieA.LastAccess.After(t0) && cookieA.LastAccess.Before(t1) { 1110 | t.Errorf("Bad LastAccess %s. Should be between %s and %s", 1111 | cookieA.LastAccess.Format(f), t0.Format(f), t1.Format(f)) 1112 | } 1113 | if cookieB.LastAccess != t0 { 1114 | t.Errorf("Bad LastAccess %s. Should equal %s", 1115 | cookieB.LastAccess.Format(f), t0.Format(f)) 1116 | } 1117 | 1118 | // access b=2 1119 | u = URL("http://www.host.test/bar") 1120 | recieved = stringRep(jar.Cookies(u)) 1121 | if recieved != "b=2" { 1122 | t.Errorf("Wrong cookies. Got %q", recieved) 1123 | } 1124 | 1125 | // b=2 now fresher than a=1 1126 | cookieA, cookieB = aAndB(jar) 1127 | if !cookieB.LastAccess.After(cookieA.LastAccess) { 1128 | t.Errorf("a: LastAccess=%s, b: LastAccess=%s", 1129 | cookieA.LastAccess.Format(f), cookieB.LastAccess.Format(f)) 1130 | } 1131 | } 1132 | } 1133 | --------------------------------------------------------------------------------