• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2010 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31// Ideally, we would rely on platform support for parsing a cookie, since
32// this would save us from any potential inconsistency. However, exposing
33// platform cookie parsing logic would require quite a bit of additional
34// plumbing, and at least some platforms lack support for parsing Cookie,
35// which is in a format slightly different from Set-Cookie and is normally
36// only required on the server side.
37
38/**
39 * @constructor
40 */
41WebInspector.CookieParser = function()
42{
43}
44
45/**
46 * @constructor
47 * @param {string} key
48 * @param {string|undefined} value
49 * @param {number} position
50 */
51WebInspector.CookieParser.KeyValue = function(key, value, position)
52{
53    this.key = key;
54    this.value = value;
55    this.position = position;
56}
57
58WebInspector.CookieParser.prototype = {
59    /**
60     * @return {!Array.<!WebInspector.Cookie>}
61     */
62    cookies: function()
63    {
64        return this._cookies;
65    },
66
67    /**
68     * @param {string|undefined} cookieHeader
69     * @return {?Array.<!WebInspector.Cookie>}
70     */
71    parseCookie: function(cookieHeader)
72    {
73        if (!this._initialize(cookieHeader))
74            return null;
75
76        for (var kv = this._extractKeyValue(); kv; kv = this._extractKeyValue()) {
77            if (kv.key.charAt(0) === "$" && this._lastCookie)
78                this._lastCookie.addAttribute(kv.key.slice(1), kv.value);
79            else if (kv.key.toLowerCase() !== "$version" && typeof kv.value === "string")
80                this._addCookie(kv, WebInspector.Cookie.Type.Request);
81            this._advanceAndCheckCookieDelimiter();
82        }
83        this._flushCookie();
84        return this._cookies;
85    },
86
87    /**
88     * @param {string|undefined} setCookieHeader
89     * @return {?Array.<!WebInspector.Cookie>}
90     */
91    parseSetCookie: function(setCookieHeader)
92    {
93        if (!this._initialize(setCookieHeader))
94            return null;
95        for (var kv = this._extractKeyValue(); kv; kv = this._extractKeyValue()) {
96            if (this._lastCookie)
97                this._lastCookie.addAttribute(kv.key, kv.value);
98            else
99                this._addCookie(kv, WebInspector.Cookie.Type.Response);
100            if (this._advanceAndCheckCookieDelimiter())
101                this._flushCookie();
102        }
103        this._flushCookie();
104        return this._cookies;
105    },
106
107    /**
108     * @param {string|undefined} headerValue
109     * @return {boolean}
110     */
111    _initialize: function(headerValue)
112    {
113        this._input = headerValue;
114        if (typeof headerValue !== "string")
115            return false;
116        this._cookies = [];
117        this._lastCookie = null;
118        this._originalInputLength = this._input.length;
119        return true;
120    },
121
122    _flushCookie: function()
123    {
124        if (this._lastCookie)
125            this._lastCookie.setSize(this._originalInputLength - this._input.length - this._lastCookiePosition);
126        this._lastCookie = null;
127    },
128
129    /**
130     * @return {?WebInspector.CookieParser.KeyValue}
131     */
132    _extractKeyValue: function()
133    {
134        if (!this._input || !this._input.length)
135            return null;
136        // Note: RFCs offer an option for quoted values that may contain commas and semicolons.
137        // Many browsers/platforms do not support this, however (see http://webkit.org/b/16699
138        // and http://crbug.com/12361). The logic below matches latest versions of IE, Firefox,
139        // Chrome and Safari on some old platforms. The latest version of Safari supports quoted
140        // cookie values, though.
141        var keyValueMatch = /^[ \t]*([^\s=;]+)[ \t]*(?:=[ \t]*([^;\n]*))?/.exec(this._input);
142        if (!keyValueMatch) {
143            console.log("Failed parsing cookie header before: " + this._input);
144            return null;
145        }
146
147        var result = new WebInspector.CookieParser.KeyValue(keyValueMatch[1], keyValueMatch[2] && keyValueMatch[2].trim(), this._originalInputLength - this._input.length);
148        this._input = this._input.slice(keyValueMatch[0].length);
149        return result;
150    },
151
152    /**
153     * @return {boolean}
154     */
155    _advanceAndCheckCookieDelimiter: function()
156    {
157        var match = /^\s*[\n;]\s*/.exec(this._input);
158        if (!match)
159            return false;
160        this._input = this._input.slice(match[0].length);
161        return match[0].match("\n") !== null;
162    },
163
164    /**
165     * @param {!WebInspector.CookieParser.KeyValue} keyValue
166     * @param {!WebInspector.Cookie.Type} type
167     */
168    _addCookie: function(keyValue, type)
169    {
170        if (this._lastCookie)
171            this._lastCookie.setSize(keyValue.position - this._lastCookiePosition);
172        // Mozilla bug 169091: Mozilla, IE and Chrome treat single token (w/o "=") as
173        // specifying a value for a cookie with empty name.
174        this._lastCookie = typeof keyValue.value === "string" ? new WebInspector.Cookie(keyValue.key, keyValue.value, type) :
175            new WebInspector.Cookie("", keyValue.key, type);
176        this._lastCookiePosition = keyValue.position;
177        this._cookies.push(this._lastCookie);
178    }
179};
180
181/**
182 * @param {string|undefined} header
183 * @return {?Array.<!WebInspector.Cookie>}
184 */
185WebInspector.CookieParser.parseCookie = function(header)
186{
187    return (new WebInspector.CookieParser()).parseCookie(header);
188}
189
190/**
191 * @param {string|undefined} header
192 * @return {?Array.<!WebInspector.Cookie>}
193 */
194WebInspector.CookieParser.parseSetCookie = function(header)
195{
196    return (new WebInspector.CookieParser()).parseSetCookie(header);
197}
198
199/**
200 * @constructor
201 * @param {string} name
202 * @param {string} value
203 * @param {?WebInspector.Cookie.Type} type
204 */
205WebInspector.Cookie = function(name, value, type)
206{
207    this._name = name;
208    this._value = value;
209    this._type = type;
210    this._attributes = {};
211}
212
213WebInspector.Cookie.prototype = {
214    /**
215     * @return {string}
216     */
217    name: function()
218    {
219        return this._name;
220    },
221
222    /**
223     * @return {string}
224     */
225    value: function()
226    {
227        return this._value;
228    },
229
230    /**
231     * @return {?WebInspector.Cookie.Type}
232     */
233    type: function()
234    {
235        return this._type;
236    },
237
238    /**
239     * @return {boolean}
240     */
241    httpOnly: function()
242    {
243        return "httponly" in this._attributes;
244    },
245
246    /**
247     * @return {boolean}
248     */
249    secure: function()
250    {
251        return "secure" in this._attributes;
252    },
253
254    /**
255     * @return {boolean}
256     */
257    session: function()
258    {
259        // RFC 2965 suggests using Discard attribute to mark session cookies, but this does not seem to be widely used.
260        // Check for absence of explicitly max-age or expiry date instead.
261        return !("expires" in this._attributes || "max-age" in this._attributes);
262    },
263
264    /**
265     * @return {string}
266     */
267    path: function()
268    {
269        return this._attributes["path"];
270    },
271
272    /**
273     * @return {string}
274     */
275    port: function()
276    {
277        return this._attributes["port"];
278    },
279
280    /**
281     * @return {string}
282     */
283    domain: function()
284    {
285        return this._attributes["domain"];
286    },
287
288    /**
289     * @return {string}
290     */
291    expires: function()
292    {
293        return this._attributes["expires"];
294    },
295
296    /**
297     * @return {string}
298     */
299    maxAge: function()
300    {
301        return this._attributes["max-age"];
302    },
303
304    /**
305     * @return {number}
306     */
307    size: function()
308    {
309        return this._size;
310    },
311
312    /**
313     * @param {number} size
314     */
315    setSize: function(size)
316    {
317        this._size = size;
318    },
319
320    /**
321     * @return {?Date}
322     */
323    expiresDate: function(requestDate)
324    {
325        // RFC 6265 indicates that the max-age attribute takes precedence over the expires attribute
326        if (this.maxAge()) {
327            var targetDate = requestDate === null ? new Date() : requestDate;
328            return new Date(targetDate.getTime() + 1000 * this.maxAge());
329        }
330
331        if (this.expires())
332            return new Date(this.expires());
333
334        return null;
335    },
336
337    /**
338     * @return {!Object}
339     */
340    attributes: function()
341    {
342        return this._attributes;
343    },
344
345    /**
346     * @param {string} key
347     * @param {string=} value
348     */
349    addAttribute: function(key, value)
350    {
351        this._attributes[key.toLowerCase()] = value;
352    },
353
354    /**
355     * @param {function(?Protocol.Error)=} callback
356     */
357    remove: function(callback)
358    {
359        PageAgent.deleteCookie(this.name(), (this.secure() ? "https://" : "http://") + this.domain() + this.path(), callback);
360    }
361}
362
363/**
364 * @enum {number}
365 */
366WebInspector.Cookie.Type = {
367    Request: 0,
368    Response: 1
369};
370
371WebInspector.Cookies = {}
372
373/**
374 * @param {function(!Array.<!WebInspector.Cookie>)} callback
375 */
376WebInspector.Cookies.getCookiesAsync = function(callback)
377{
378    /**
379     * @param {?Protocol.Error} error
380     * @param {!Array.<!PageAgent.Cookie>} cookies
381     */
382    function mycallback(error, cookies)
383    {
384        if (error)
385            return;
386        callback(cookies.map(WebInspector.Cookies.buildCookieProtocolObject));
387    }
388
389    PageAgent.getCookies(mycallback);
390}
391
392/**
393 * @param {!PageAgent.Cookie} protocolCookie
394 * @return {!WebInspector.Cookie}
395 */
396WebInspector.Cookies.buildCookieProtocolObject = function(protocolCookie)
397{
398    var cookie = new WebInspector.Cookie(protocolCookie.name, protocolCookie.value, null);
399    cookie.addAttribute("domain", protocolCookie["domain"]);
400    cookie.addAttribute("path", protocolCookie["path"]);
401    cookie.addAttribute("port", protocolCookie["port"]);
402    if (protocolCookie["expires"])
403        cookie.addAttribute("expires", protocolCookie["expires"]);
404    if (protocolCookie["httpOnly"])
405        cookie.addAttribute("httpOnly");
406    if (protocolCookie["secure"])
407        cookie.addAttribute("secure");
408    cookie.setSize(protocolCookie["size"]);
409    return cookie;
410}
411
412/**
413 * @param {!WebInspector.Cookie} cookie
414 * @param {string} resourceURL
415 * @return {boolean}
416 */
417WebInspector.Cookies.cookieMatchesResourceURL = function(cookie, resourceURL)
418{
419    var url = resourceURL.asParsedURL();
420    if (!url || !WebInspector.Cookies.cookieDomainMatchesResourceDomain(cookie.domain(), url.host))
421        return false;
422    return (url.path.startsWith(cookie.path())
423        && (!cookie.port() || url.port == cookie.port())
424        && (!cookie.secure() || url.scheme === "https"));
425}
426
427/**
428 * @param {string} cookieDomain
429 * @param {string} resourceDomain
430 * @return {boolean}
431 */
432WebInspector.Cookies.cookieDomainMatchesResourceDomain = function(cookieDomain, resourceDomain)
433{
434    if (cookieDomain.charAt(0) !== '.')
435        return resourceDomain === cookieDomain;
436    return !!resourceDomain.match(new RegExp("^([^\\.]+\\.)*" + cookieDomain.substring(1).escapeForRegExp() + "$", "i"));
437}
438