blob: 118b790caf17b8b40b0319b7bd598ef44427c787 [file] [log] [blame]
/*
* Copyright (C) 2010 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Ideally, we would rely on platform support for parsing a cookie, since
// this would save us from any potential inconsistency. However, exposing
// platform cookie parsing logic would require quite a bit of additional
// plumbing, and at least some platforms lack support for parsing Cookie,
// which is in a format slightly different from Set-Cookie and is normally
// only required on the server side.
/**
* @unrestricted
*/
SDK.CookieParser = class {
/**
* @param {!SDK.Target} target
*/
constructor(target) {
this._target = target;
}
/**
* @param {!SDK.Target} target
* @param {string|undefined} header
* @return {?Array.<!SDK.Cookie>}
*/
static parseCookie(target, header) {
return (new SDK.CookieParser(target)).parseCookie(header);
}
/**
* @param {!SDK.Target} target
* @param {string|undefined} header
* @return {?Array.<!SDK.Cookie>}
*/
static parseSetCookie(target, header) {
return (new SDK.CookieParser(target)).parseSetCookie(header);
}
/**
* @return {!Array.<!SDK.Cookie>}
*/
cookies() {
return this._cookies;
}
/**
* @param {string|undefined} cookieHeader
* @return {?Array.<!SDK.Cookie>}
*/
parseCookie(cookieHeader) {
if (!this._initialize(cookieHeader))
return null;
for (var kv = this._extractKeyValue(); kv; kv = this._extractKeyValue()) {
if (kv.key.charAt(0) === '$' && this._lastCookie)
this._lastCookie.addAttribute(kv.key.slice(1), kv.value);
else if (kv.key.toLowerCase() !== '$version' && typeof kv.value === 'string')
this._addCookie(kv, SDK.Cookie.Type.Request);
this._advanceAndCheckCookieDelimiter();
}
this._flushCookie();
return this._cookies;
}
/**
* @param {string|undefined} setCookieHeader
* @return {?Array.<!SDK.Cookie>}
*/
parseSetCookie(setCookieHeader) {
if (!this._initialize(setCookieHeader))
return null;
for (var kv = this._extractKeyValue(); kv; kv = this._extractKeyValue()) {
if (this._lastCookie)
this._lastCookie.addAttribute(kv.key, kv.value);
else
this._addCookie(kv, SDK.Cookie.Type.Response);
if (this._advanceAndCheckCookieDelimiter())
this._flushCookie();
}
this._flushCookie();
return this._cookies;
}
/**
* @param {string|undefined} headerValue
* @return {boolean}
*/
_initialize(headerValue) {
this._input = headerValue;
if (typeof headerValue !== 'string')
return false;
this._cookies = [];
this._lastCookie = null;
this._originalInputLength = this._input.length;
return true;
}
_flushCookie() {
if (this._lastCookie)
this._lastCookie.setSize(this._originalInputLength - this._input.length - this._lastCookiePosition);
this._lastCookie = null;
}
/**
* @return {?SDK.CookieParser.KeyValue}
*/
_extractKeyValue() {
if (!this._input || !this._input.length)
return null;
// Note: RFCs offer an option for quoted values that may contain commas and semicolons.
// Many browsers/platforms do not support this, however (see http://webkit.org/b/16699
// and http://crbug.com/12361). The logic below matches latest versions of IE, Firefox,
// Chrome and Safari on some old platforms. The latest version of Safari supports quoted
// cookie values, though.
var keyValueMatch = /^[ \t]*([^\s=;]+)[ \t]*(?:=[ \t]*([^;\n]*))?/.exec(this._input);
if (!keyValueMatch) {
console.error('Failed parsing cookie header before: ' + this._input);
return null;
}
var result = new SDK.CookieParser.KeyValue(
keyValueMatch[1], keyValueMatch[2] && keyValueMatch[2].trim(), this._originalInputLength - this._input.length);
this._input = this._input.slice(keyValueMatch[0].length);
return result;
}
/**
* @return {boolean}
*/
_advanceAndCheckCookieDelimiter() {
var match = /^\s*[\n;]\s*/.exec(this._input);
if (!match)
return false;
this._input = this._input.slice(match[0].length);
return match[0].match('\n') !== null;
}
/**
* @param {!SDK.CookieParser.KeyValue} keyValue
* @param {!SDK.Cookie.Type} type
*/
_addCookie(keyValue, type) {
if (this._lastCookie)
this._lastCookie.setSize(keyValue.position - this._lastCookiePosition);
// Mozilla bug 169091: Mozilla, IE and Chrome treat single token (w/o "=") as
// specifying a value for a cookie with empty name.
this._lastCookie = typeof keyValue.value === 'string' ?
new SDK.Cookie(this._target, keyValue.key, keyValue.value, type) :
new SDK.Cookie(this._target, '', keyValue.key, type);
this._lastCookiePosition = keyValue.position;
this._cookies.push(this._lastCookie);
}
};
/**
* @unrestricted
*/
SDK.CookieParser.KeyValue = class {
/**
* @param {string} key
* @param {string|undefined} value
* @param {number} position
*/
constructor(key, value, position) {
this.key = key;
this.value = value;
this.position = position;
}
};
/**
* @unrestricted
*/
SDK.Cookie = class {
/**
* @param {!SDK.Target} target
* @param {string} name
* @param {string} value
* @param {?SDK.Cookie.Type} type
*/
constructor(target, name, value, type) {
this._target = target;
this._name = name;
this._value = value;
this._type = type;
this._attributes = {};
}
/**
* @return {string}
*/
name() {
return this._name;
}
/**
* @return {string}
*/
value() {
return this._value;
}
/**
* @return {?SDK.Cookie.Type}
*/
type() {
return this._type;
}
/**
* @return {boolean}
*/
httpOnly() {
return 'httponly' in this._attributes;
}
/**
* @return {boolean}
*/
secure() {
return 'secure' in this._attributes;
}
/**
* @return {string}
*/
sameSite() {
return this._attributes['samesite'];
}
/**
* @return {boolean}
*/
session() {
// RFC 2965 suggests using Discard attribute to mark session cookies, but this does not seem to be widely used.
// Check for absence of explicitly max-age or expiry date instead.
return !('expires' in this._attributes || 'max-age' in this._attributes);
}
/**
* @return {string}
*/
path() {
return this._attributes['path'];
}
/**
* @return {string}
*/
port() {
return this._attributes['port'];
}
/**
* @return {string}
*/
domain() {
return this._attributes['domain'];
}
/**
* @return {string}
*/
expires() {
return this._attributes['expires'];
}
/**
* @return {string}
*/
maxAge() {
return this._attributes['max-age'];
}
/**
* @return {number}
*/
size() {
return this._size;
}
/**
* @param {number} size
*/
setSize(size) {
this._size = size;
}
/**
* @return {?Date}
*/
expiresDate(requestDate) {
// RFC 6265 indicates that the max-age attribute takes precedence over the expires attribute
if (this.maxAge()) {
var targetDate = requestDate === null ? new Date() : requestDate;
return new Date(targetDate.getTime() + 1000 * this.maxAge());
}
if (this.expires())
return new Date(this.expires());
return null;
}
/**
* @return {!Object}
*/
attributes() {
return this._attributes;
}
/**
* @param {string} key
* @param {string=} value
*/
addAttribute(key, value) {
this._attributes[key.toLowerCase()] = value;
}
/**
* @param {function(?Protocol.Error)=} callback
*/
remove(callback) {
this._target.networkAgent().deleteCookie(
this.name(), (this.secure() ? 'https://' : 'http://') + this.domain() + this.path(), callback);
}
};
/**
* @enum {number}
*/
SDK.Cookie.Type = {
Request: 0,
Response: 1
};
SDK.Cookies = {};
/**
* @param {function(!Array.<!SDK.Cookie>)} callback
*/
SDK.Cookies.getCookiesAsync = function(callback) {
var allCookies = [];
/**
* @param {!SDK.Target} target
* @param {?Protocol.Error} error
* @param {!Array.<!Protocol.Network.Cookie>} cookies
*/
function mycallback(target, error, cookies) {
if (error) {
console.error(error);
return;
}
for (var i = 0; i < cookies.length; ++i)
allCookies.push(SDK.Cookies._parseProtocolCookie(target, cookies[i]));
}
var barrier = new CallbackBarrier();
for (var target of SDK.targetManager.targets(SDK.Target.Capability.Network))
target.networkAgent().getCookies(barrier.createCallback(mycallback.bind(null, target)));
barrier.callWhenDone(callback.bind(null, allCookies));
};
/**
* @param {!SDK.Target} target
* @param {!Protocol.Network.Cookie} protocolCookie
* @return {!SDK.Cookie}
*/
SDK.Cookies._parseProtocolCookie = function(target, protocolCookie) {
var cookie = new SDK.Cookie(target, protocolCookie.name, protocolCookie.value, null);
cookie.addAttribute('domain', protocolCookie['domain']);
cookie.addAttribute('path', protocolCookie['path']);
cookie.addAttribute('port', protocolCookie['port']);
if (protocolCookie['expires'])
cookie.addAttribute('expires', protocolCookie['expires']);
if (protocolCookie['httpOnly'])
cookie.addAttribute('httpOnly');
if (protocolCookie['secure'])
cookie.addAttribute('secure');
if (protocolCookie['sameSite'])
cookie.addAttribute('sameSite', protocolCookie['sameSite']);
cookie.setSize(protocolCookie['size']);
return cookie;
};
/**
* @param {!SDK.Cookie} cookie
* @param {string} resourceURL
* @return {boolean}
*/
SDK.Cookies.cookieMatchesResourceURL = function(cookie, resourceURL) {
var url = resourceURL.asParsedURL();
if (!url || !SDK.Cookies.cookieDomainMatchesResourceDomain(cookie.domain(), url.host))
return false;
return (
url.path.startsWith(cookie.path()) && (!cookie.port() || url.port === cookie.port()) &&
(!cookie.secure() || url.scheme === 'https'));
};
/**
* @param {string} cookieDomain
* @param {string} resourceDomain
* @return {boolean}
*/
SDK.Cookies.cookieDomainMatchesResourceDomain = function(cookieDomain, resourceDomain) {
if (cookieDomain.charAt(0) !== '.')
return resourceDomain === cookieDomain;
return !!resourceDomain.match(new RegExp('^([^\\.]+\\.)*' + cookieDomain.substring(1).escapeForRegExp() + '$', 'i'));
};