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