0cd67066d163c87a067556e581450f97149e677a
[WebKit-https.git] / Source / WebCore / inspector / front-end / CookieParser.js
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  */
41 WebInspector.CookieParser = function()
42 {
43 }
44
45 /**
46  * @constructor
47  * @param {string} key
48  * @param {string|undefined} value
49  * @param {number} position
50  */
51 WebInspector.CookieParser.KeyValue = function(key, value, position)
52 {
53     this.key = key;
54     this.value = value;
55     this.position = position;
56 }
57
58 WebInspector.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  */
185 WebInspector.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  */
194 WebInspector.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  */
205 WebInspector.Cookie = function(name, value, type)
206 {
207     this._name = name;
208     this._value = value;
209     this._type = type;
210     this._attributes = {};
211 }
212
213 WebInspector.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 /**
356  * @enum {number}
357  */
358 WebInspector.Cookie.Type = {
359     Request: 0,
360     Response: 1
361 };
362
363 WebInspector.Cookies = {}
364
365 WebInspector.Cookies.getCookiesAsync = function(callback)
366 {
367     /**
368      * @param {?Protocol.Error} error 
369      * @param {Array.<WebInspector.Cookie>} cookies 
370      * @param {string} cookiesString 
371      */ 
372     function mycallback(error, cookies, cookiesString)
373     {
374         if (error)
375             return;
376         if (cookiesString)
377             callback(WebInspector.Cookies.buildCookiesFromString(cookiesString), false);
378         else
379             callback(cookies.map(WebInspector.Cookies.buildCookieProtocolObject), true);
380     }
381
382     PageAgent.getCookies(mycallback);
383 }
384
385 /**
386  * @param {string} rawCookieString 
387  * @return {Array.<WebInspector.Cookie>}
388  */
389 WebInspector.Cookies.buildCookiesFromString = function(rawCookieString)
390 {
391     var rawCookies = rawCookieString.split(/;\s*/);
392     var cookies = [];
393
394     if (!(/^\s*$/.test(rawCookieString))) {
395         for (var i = 0; i < rawCookies.length; ++i) {
396             var rawCookie = rawCookies[i];
397             var delimIndex = rawCookie.indexOf("=");
398             var name = rawCookie.substring(0, delimIndex);
399             var value = rawCookie.substring(delimIndex + 1);
400             var size = name.length + value.length;
401             var cookie = new WebInspector.Cookie(name, value, null);
402             cookie.setSize(size);
403             cookies.push(cookie);
404         }
405     }
406
407     return cookies;
408 }
409
410 /**
411  * @param {Object} protocolCookie
412  * @return {!WebInspector.Cookie}
413  */
414 WebInspector.Cookies.buildCookieProtocolObject = function(protocolCookie)
415 {
416     var cookie = new WebInspector.Cookie(protocolCookie.name, protocolCookie.value, null);
417     cookie.addAttribute("domain", protocolCookie["domain"]);
418     cookie.addAttribute("path", protocolCookie["path"]);
419     cookie.addAttribute("port", protocolCookie["port"]);
420     if (protocolCookie["expires"])
421         cookie.addAttribute("expires", protocolCookie["expires"]);
422     if (protocolCookie["httpOnly"])
423         cookie.addAttribute("httpOnly");
424     if (protocolCookie["secure"])
425         cookie.addAttribute("secure");
426     cookie.setSize(protocolCookie["size"]);
427     return cookie;
428 }
429
430 /**
431  * @param {WebInspector.Cookie} cookie 
432  * @param {string} resourceURL
433  */
434 WebInspector.Cookies.cookieMatchesResourceURL = function(cookie, resourceURL)
435 {
436     var url = resourceURL.asParsedURL();
437     if (!url || !WebInspector.Cookies.cookieDomainMatchesResourceDomain(cookie.domain(), url.host))
438         return false;
439     return (url.path.startsWith(cookie.path())
440         && (!cookie.port() || url.port == cookie.port())
441         && (!cookie.secure() || url.scheme === "https"));
442 }
443
444 /**
445  * @param {string} cookieDomain 
446  * @param {string} resourceDomain
447  */
448 WebInspector.Cookies.cookieDomainMatchesResourceDomain = function(cookieDomain, resourceDomain)
449 {
450     if (cookieDomain.charAt(0) !== '.')
451         return resourceDomain === cookieDomain;
452     return !!resourceDomain.match(new RegExp("^([^\\.]+\\.)*" + cookieDomain.substring(1).escapeForRegExp() + "$", "i"));
453 }