Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(2)

Delta Between Two Patch Sets: src/pkg/net/http/jar.go

Issue 5399043: code review 5399043: net/http: Added interface for a cookie jar. (Closed)
Left Patch Set: diff -r c93109f5d3ca https://go.googlecode.com/hg/ Created 13 years, 3 months ago
Right Patch Set: diff -r 6cfb9f2415fa https://go.googlecode.com/hg/ Created 13 years, 3 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « src/pkg/net/http/client.go ('k') | no next file » | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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 }
LEFTRIGHT

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b