Home | History | Annotate | Download | only in sdk
      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      * @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  */
    366 WebInspector.Cookie.Type = {
    367     Request: 0,
    368     Response: 1
    369 };
    370 
    371 WebInspector.Cookies = {}
    372 
    373 /**
    374  * @param {function(!Array.<!WebInspector.Cookie>)} callback
    375  */
    376 WebInspector.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  */
    396 WebInspector.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  */
    417 WebInspector.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  */
    432 WebInspector.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