LEFT | RIGHT |
1 // Copyright 2009 The Go Authors. All rights reserved. | 1 // Copyright 2011 The Go Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style | 2 // Use of this source code is governed by a BSD-style |
3 // license that can be found in the LICENSE file. | 3 // license that can be found in the LICENSE file. |
4 | 4 |
5 package http | 5 package http |
6 | 6 |
7 import ( | 7 import ( |
8 "net/url" | 8 "net/url" |
9 "strconv" | |
10 "strings" | |
11 "sync" | |
12 "time" | |
13 ) | 9 ) |
14 | 10 |
15 // CookieJar is the interface for a cookie jar. | 11 // A CookieJar manages storage and use of cookies in HTTP requests.· |
| 12 // |
| 13 // Implementations of CookieJar must be safe for concurrent use by multiple |
| 14 // goroutines. |
16 type CookieJar interface { | 15 type CookieJar interface { |
17 » // Update will add the given cookies to the jar, update those allready | 16 » // SetCookies handles the receipt of the cookies in a reply for the· |
18 » // in the jar and delete those which are requested to be deleted. | 17 » // given URL. It may or may not choose to save the cookies, depending· |
19 » // The host is the host from which the Set-Cookie response headers | 18 » // on the jar's policy and implementation.· |
20 » // have been recieved. Invalid cookies (e.g. with a wildcard | 19 » SetCookies(u *url.URL, cookies []*Cookie) |
21 » // top level domain or cookies whose domain mismatches the host) | |
22 » // are silently ignored. | |
23 » Update(cookies []*Cookie, host string) | |
24 | 20 |
25 » // Select will return a list of all cookies from the jar which | 21 » // Cookies returns the cookies to send in a request for the given URL. |
26 » // should be sent to the given URL. I.e. unexpired, proper domain | 22 » // It is up to the implementation to honor the standard cookie use· |
27 » // proper path, honouring secure and http-only settings | 23 » // restrictions such as in RFC 6265.· |
28 » Select(u *url.URL) []*Cookie | 24 » Cookies(u *url.URL) []*Cookie |
29 } | 25 } |
30 | 26 |
31 // BlackHoleJar implements a black hole cookie jar: Whatever you stuck in | 27 type blackHoleJar struct{} |
32 // via Update will never come back out on a Select. | |
33 type BlackHoleJar int | |
34 | 28 |
35 func (bh BlackHoleJar) Update(cookies []*Cookie, host string) {} | 29 func (blackHoleJar) SetCookies(u *url.URL, cookies []*Cookie) {} |
36 func (bh BlackHoleJar) Select(u *url.URL) []*Cookie { return nil } | 30 func (blackHoleJar) Cookies(u *url.URL) []*Cookie { return nil } |
37 | |
38 // normalizeCookie returns a copy of cookie where the domain, path and expires | |
39 // fields are properly set.· | |
40 // - The Domain of the returned cookie is the effective domain: www.domain.org
will· | |
41 // become .www.domain.org | |
42 // - MaxAge is set to 0 | |
43 // - Path is set to its default "/" if unset. | |
44 // - The Expires Time struct is cleared and the expiration time in UTC seconds | |
45 // is encoded in the Year field: This allows for much quicker checks if | |
46 // the cookie is expired and can be replaced by a non-hack solution oce | |
47 // package time is updated. A value of 0 in the Year field indicates a sessio
n | |
48 // cookie. | |
49 // This hack will no longer be a hack once the new Time has come. | |
50 // It will return nil for a expired cookie or if an invalid domain is | |
51 // set (a top level domain like .net or anything like ??.??, eg. co.uk.· | |
52 func normalizeCookie(cookie *Cookie, domain string) *Cookie { | |
53 c := *cookie | |
54 | |
55 // Path defaults to / | |
56 if c.Path == "" { | |
57 c.Path = "/" | |
58 } | |
59 | |
60 // Handle MaxAge. | |
61 switch true { | |
62 case c.MaxAge > 0: | |
63 // MaxAge was set in cookie: Value is seconds until expiration | |
64 c.Expires = time.Now().Add(time.Duration(c.MaxAge) * time.Second
) | |
65 case c.MaxAge < 0: | |
66 // MaxAge was set and requires deletion of cookie: | |
67 // set expiration to "long ago" | |
68 c.Expires = time.Now().Add(-10 * time.Hour) | |
69 default: | |
70 // MaxAge was not set in response | |
71 } | |
72 c.MaxAge = 0 | |
73 | |
74 // use host from reguest if domain not set | |
75 if c.Domain == "" { | |
76 c.Domain = domain | |
77 return &c // assuming domain is valid | |
78 } | |
79 if c.Domain == "localhost" { | |
80 return &c | |
81 } | |
82 | |
83 // make effective domain and prevent wildcards for tlds | |
84 if c.Domain[0] != '.' { | |
85 c.Domain = "." + c.Domain | |
86 } | |
87 n := strings.Count(c.Domain, ".") | |
88 if n < 2 { | |
89 return nil // ".org" is forbidden | |
90 } | |
91 if n == 2 && len(c.Domain) == 6 && strings.LastIndex(c.Domain, ".") == 3
{ | |
92 // ".co.uk" is forbidden (bad luck for domains like o2.uk but br
owser· | |
93 // do it the same way) | |
94 return nil | |
95 } | |
96 | |
97 // TODO: how to handle ip addresses insted of host/domain names? | |
98 // reject? disallow wildcards? --> no wildcards | |
99 if isIP(domain) { | |
100 if isIP(c.Domain[1:]) && domain == c.Domain[1:] { | |
101 return &c // valid: both IP and exact match | |
102 } | |
103 return nil | |
104 } | |
105 if isIP(c.Domain[1:]) { | |
106 return nil | |
107 } | |
108 | |
109 // c.Domain now of the form .ibm.com or .www.company.org or even .test.i
nt.company.net | |
110 if !strings.HasSuffix(c.Domain, domain) { | |
111 // okay: domain==www.company.net and c.Domain==.sso.company.net | |
112 // not okay : domain==www.company.net and c.Domain==.www.other.o
rg | |
113 i := strings.LastIndex(domain, ".") | |
114 i = strings.LastIndex(domain[:i], ".") | |
115 domain = domain[i:] // works as n>=3 | |
116 if !strings.HasSuffix(c.Domain, domain) { | |
117 return nil | |
118 } | |
119 } | |
120 | |
121 return &c | |
122 } | |
123 | |
124 // isIP check if the given host is not a host name but an IP address. | |
125 func isIP(host string) bool { | |
126 // TODO: maybe handle IPv6 | |
127 r := strings.Split(host, ".") | |
128 if len(r) != 4 { | |
129 return false | |
130 } | |
131 is0to255 := func(s string) bool { | |
132 n, err := strconv.Atoi(s) | |
133 return err != nil || n < 0 || n > 255 | |
134 } | |
135 return is0to255(r[0]) && is0to255(r[1]) && is0to255(r[2]) && is0to255(r[
3]) // almost... | |
136 } | |
137 | |
138 type CookieState int // used for qualifyCookie | |
139 const ( | |
140 CookieValid CookieState = iota // include into request | |
141 CookieInvalid // do not include as some filed prohibi
t sending | |
142 CookieExpired // do not include, cookie is expired | |
143 ) | |
144 | |
145 // qualifyCookie checks if a cookie qualifies to be sent to the url. | |
146 // The relevant parts of the url have to be provided as host, path and scheme. | |
147 // The current time (as UTC().Seconds()) has to be provided as now. | |
148 // If strict is true, than no wildcard domains in cookies are allowed: | |
149 // I.e. the domain of the cookie must match the host exactly. | |
150 func qualifyCookie(cookie *Cookie, host, path, scheme string, now time.Time, str
ict bool) CookieState { | |
151 if expired(cookie, now) { | |
152 return CookieExpired | |
153 } | |
154 if !cookieDomainMatch(host, cookie.Domain, strict) { | |
155 return CookieInvalid | |
156 } | |
157 if !cookiePathMatch(path, cookie.Path) { | |
158 return CookieInvalid | |
159 } | |
160 if cookie.HttpOnly && !(scheme == "http" || scheme == "https") { | |
161 return CookieInvalid | |
162 } | |
163 if cookie.Secure && scheme != "https" { | |
164 return CookieInvalid | |
165 } | |
166 return CookieValid | |
167 } | |
168 | |
169 // hostname returns the hostname without port from the given URL u. | |
170 func hostname(u *url.URL) string { | |
171 host := u.Host | |
172 if i := strings.Index(host, ":"); i != -1 { | |
173 host = host[:i] | |
174 } | |
175 return host | |
176 } | |
177 | |
178 // expired checks if a normalized cookie is expired | |
179 func expired(cookie *Cookie, now time.Time) bool { | |
180 return !cookie.Expires.IsZero() && cookie.Expires.Before(now) | |
181 } | |
182 | |
183 // cookieDomainMatch checks if the requestHost matches the effective domain· | |
184 // of the cookie cookieDomain. All domains of the cookies in the jar are | |
185 // assumed to be effective domains, i.e. the start with a dot ".". | |
186 // Setting strictSameDomain to true will disable wildcard domains in | |
187 // the cookies. | |
188 func cookieDomainMatch(requestHost, cookieDomain string, strictSameDomain bool)
bool { | |
189 if requestHost == cookieDomain[1:] { | |
190 return true // www.host.com = .www.host.com | |
191 } | |
192 if !strictSameDomain && strings.HasSuffix(requestHost, cookieDomain) { | |
193 return true // sso.host.com = .host.com | |
194 } | |
195 return false // all other cases | |
196 } | |
197 | |
198 // pathMatch check if the the cookie with path cookiePath sould be sent to | |
199 // a request with path requestPath: | |
200 // Include cookie with Path="/some" in request to "/some", | |
201 // "/some/path" as well as "/some/pathways" (TODO: check this case)· | |
202 // but not in requests to "/other/path". | |
203 func cookiePathMatch(requestPath, cookiePath string) bool { | |
204 return strings.HasPrefix(requestPath, cookiePath) | |
205 } | |
206 | |
207 // TrivialJar implements a very simple CookieJar. TrivialJar is not thread- | |
208 // or goroutine-safe and shows bad performance for large amounts of cookies. | |
209 type TrivialJar struct { | |
210 cookies []*Cookie | |
211 } | |
212 | |
213 // NewTrivialJar sets up a TrivialJar with an initial capacity for n cookies. | |
214 func NewTrivialJar(n int) TrivialJar { | |
215 jar := TrivialJar{} | |
216 jar.cookies = make([]*Cookie, 0, n) | |
217 return jar | |
218 } | |
219 | |
220 // Update updates all cookies in the jar. | |
221 func (jar TrivialJar) Update(cookies []*Cookie, host string) { | |
222 for _, cookie := range cookies { | |
223 cookie = normalizeCookie(cookie, host) | |
224 for i, c := range jar.cookies { | |
225 if c.Domain == cookie.Domain && c.Path == cookie.Path &&
c.Name == cookie.Name { | |
226 jar.cookies[i] = cookie | |
227 continue | |
228 } | |
229 } | |
230 jar.cookies = append(jar.cookies, cookie) | |
231 } | |
232 } | |
233 | |
234 // Select selects all cookies from jar to be sent to the URL u. | |
235 func (jar TrivialJar) Select(u *url.URL) []*Cookie { | |
236 selection := make([]*Cookie, 0, 10) | |
237 now := time.Now() | |
238 host := hostname(u) | |
239 | |
240 for _, cookie := range jar.cookies { | |
241 if qualifyCookie(cookie, host, u.Path, u.Scheme, now, false) !=
CookieValid { | |
242 continue | |
243 } | |
244 // check if this cookie is new or more specific than an existing | |
245 replaced := false | |
246 for i, c := range selection { | |
247 if c.Name != cookie.Name { | |
248 continue | |
249 } | |
250 if len(cookie.Path) > len(c.Path) { | |
251 // this one is more specific than the one we fou
nd allready | |
252 selection[i] = cookie | |
253 replaced = true | |
254 } | |
255 } | |
256 if !replaced { | |
257 selection = append(selection, cookie) | |
258 } | |
259 } | |
260 return selection | |
261 } | |
262 | |
263 // SimpleCookieJar implements a simple cookie jar. It can be shared by different | |
264 // goroutines. It's unsutable for very large amounts of cookies. | |
265 type SimpleCookieJar struct { | |
266 cookies []*Cookie // list of all cookies | |
267 mutex sync.Mutex // mutex to prevent concurrent modification | |
268 expired map[int]bool // index of expired/deleted cookies | |
269 } | |
270 | |
271 // NewSimpleCookieJar sets up a new empty cookie jar with initial capacity for n
cookies. | |
272 func NewSimpleCookieJar(n int) *SimpleCookieJar { | |
273 jar := &SimpleCookieJar{} | |
274 jar.cookies = make([]*Cookie, 0, n) | |
275 jar.expired = make(map[int]bool, 30) | |
276 return jar | |
277 } | |
278 | |
279 // Find looks up the index of cookie in our cookie jar. | |
280 // The lookup is based on an exact match of the (Name, Domain, Path) tripple. | |
281 func (jar *SimpleCookieJar) find(domain, path, name string) (int, *Cookie) { | |
282 for i, c := range jar.cookies { | |
283 if c.Domain == domain && path == c.Path && name == c.Name { | |
284 return i, c | |
285 } | |
286 } | |
287 return -1, nil | |
288 } | |
289 | |
290 // Update add/update cookies in the jar. | |
291 func (jar *SimpleCookieJar) Update(cookies []*Cookie, domain string) { | |
292 now := time.Now() | |
293 jar.mutex.Lock() | |
294 defer jar.mutex.Unlock() | |
295 for _, c := range cookies { | |
296 cookie := normalizeCookie(c, domain) | |
297 if cookie == nil { | |
298 continue // bad domain | |
299 } | |
300 idx, _ := jar.find(cookie.Domain, cookie.Path, cookie.Name) | |
301 if expired(cookie, now) && idx != -1 { | |
302 jar.expired[idx] = true | |
303 continue | |
304 } | |
305 | |
306 if idx == -1 { // new cookie | |
307 jar.cookies = append(jar.cookies, cookie) | |
308 } else { // update | |
309 jar.cookies[idx] = cookie | |
310 } | |
311 } | |
312 jar.cleanup() | |
313 return | |
314 } | |
315 | |
316 // Select selects all cookies from jar which should be sent to the given URL u. | |
317 func (jar *SimpleCookieJar) Select(u *url.URL) []*Cookie { | |
318 host := hostname(u) | |
319 now := time.Now() | |
320 | |
321 // list of possible cookies | |
322 selection := make([]*Cookie, 0, 10) | |
323 jar.mutex.Lock() | |
324 defer jar.mutex.Unlock() | |
325 for idx, cookie := range jar.cookies { | |
326 if _, expired := jar.expired[idx]; expired { | |
327 continue | |
328 } | |
329 if state := qualifyCookie(cookie, host, u.Path, u.Scheme, now, f
alse); state == CookieInvalid { | |
330 continue | |
331 } else if state == CookieExpired { | |
332 jar.expired[idx] = true | |
333 continue | |
334 } | |
335 replaced := false | |
336 for i, c := range selection { | |
337 if c.Name != cookie.Name { | |
338 continue | |
339 } | |
340 if len(cookie.Path) > len(c.Path) { | |
341 // this one is more specific than the one we fou
nd allready | |
342 selection[i] = cookie | |
343 replaced = true | |
344 } | |
345 } | |
346 if !replaced { | |
347 selection = append(selection, cookie) | |
348 } | |
349 } | |
350 jar.cleanup() | |
351 | |
352 return selection | |
353 } | |
354 | |
355 // cleanup will remove all expired cookies from the jar (and clear the set of· | |
356 // expired cookies) if more than 20 expired cookies are present. The jar must | |
357 // be locked before calling this method. | |
358 func (jar *SimpleCookieJar) cleanup() { | |
359 exp := len(jar.expired) | |
360 if exp < 20 { | |
361 return | |
362 } | |
363 | |
364 n := len(jar.cookies) - 1 | |
365 for i := 0; i < len(jar.cookies)-exp; i++ { | |
366 if _, expired := jar.expired[i]; expired { | |
367 jar.cookies[i] = jar.cookies[n] | |
368 n-- | |
369 } | |
370 } | |
371 | |
372 // clear set of expired cookies | |
373 jar.expired = make(map[int]bool, 30) | |
374 jar.cookies = jar.cookies[:n+1] | |
375 } | |
LEFT | RIGHT |