| /* |
| * 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 |
| */ |
| export default class CookieParser { |
| constructor() { |
| } |
| |
| /** |
| * @param {string|undefined} header |
| * @return {?Array<!SDK.Cookie>} |
| */ |
| static parseCookie(header) { |
| return (new CookieParser()).parseCookie(header); |
| } |
| |
| /** |
| * @param {string|undefined} header |
| * @return {?Array<!SDK.Cookie>} |
| */ |
| static parseSetCookie(header) { |
| return (new CookieParser()).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 (let 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, 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 (let kv = this._extractKeyValue(); kv; kv = this._extractKeyValue()) { |
| if (this._lastCookie) { |
| this._lastCookie.addAttribute(kv.key, kv.value); |
| } else { |
| this._addCookie(kv, 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._lastCookieLine = ''; |
| this._originalInputLength = this._input.length; |
| return true; |
| } |
| |
| _flushCookie() { |
| if (this._lastCookie) { |
| this._lastCookie.setSize(this._originalInputLength - this._input.length - this._lastCookiePosition); |
| this._lastCookie._setCookieLine(this._lastCookieLine.replace('\n', '')); |
| } |
| this._lastCookie = null; |
| this._lastCookieLine = ''; |
| } |
| |
| /** |
| * @return {?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. |
| const keyValueMatch = /^[ \t]*([^\s=;]+)[ \t]*(?:=[ \t]*([^;\n]*))?/.exec(this._input); |
| if (!keyValueMatch) { |
| console.error('Failed parsing cookie header before: ' + this._input); |
| return null; |
| } |
| |
| const result = new KeyValue( |
| keyValueMatch[1], keyValueMatch[2] && keyValueMatch[2].trim(), this._originalInputLength - this._input.length); |
| this._lastCookieLine += keyValueMatch[0]; |
| this._input = this._input.slice(keyValueMatch[0].length); |
| return result; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| _advanceAndCheckCookieDelimiter() { |
| const match = /^\s*[\n;]\s*/.exec(this._input); |
| if (!match) { |
| return false; |
| } |
| this._lastCookieLine += match[0]; |
| this._input = this._input.slice(match[0].length); |
| return match[0].match('\n') !== null; |
| } |
| |
| /** |
| * @param {!KeyValue} keyValue |
| * @param {!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(keyValue.key, keyValue.value, type) : |
| new SDK.Cookie('', keyValue.key, type); |
| this._lastCookiePosition = keyValue.position; |
| this._cookies.push(this._lastCookie); |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| class KeyValue { |
| /** |
| * @param {string} key |
| * @param {string|undefined} value |
| * @param {number} position |
| */ |
| constructor(key, value, position) { |
| this.key = key; |
| this.value = value; |
| this.position = position; |
| } |
| } |
| |
| |
| /** |
| * @unrestricted |
| */ |
| export class Cookie { |
| /** |
| * @param {string} name |
| * @param {string} value |
| * @param {?Type} type |
| */ |
| constructor(name, value, type) { |
| this._name = name; |
| this._value = value; |
| this._type = type; |
| this._attributes = {}; |
| this._size = 0; |
| /** @type {string|null} */ |
| this._cookieLine = null; |
| } |
| |
| /** |
| * @param {!Protocol.Network.Cookie} protocolCookie |
| * @return {!SDK.Cookie} |
| */ |
| static fromProtocolCookie(protocolCookie) { |
| const cookie = new SDK.Cookie(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'] * 1000); |
| } |
| 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; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| name() { |
| return this._name; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| value() { |
| return this._value; |
| } |
| |
| /** |
| * @return {?Type} |
| */ |
| type() { |
| return this._type; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| httpOnly() { |
| return 'httponly' in this._attributes; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| secure() { |
| return 'secure' in this._attributes; |
| } |
| |
| /** |
| * @return {!Protocol.Network.CookieSameSite} |
| */ |
| sameSite() { |
| // TODO(allada) This should not rely on _attributes and instead store them individually. |
| return /** @type {!Protocol.Network.CookieSameSite} */ (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 {number} |
| */ |
| expires() { |
| return this._attributes['expires']; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| maxAge() { |
| return this._attributes['max-age']; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| size() { |
| return this._size; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| url() { |
| return (this.secure() ? 'https://' : 'http://') + this.domain() + this.path(); |
| } |
| |
| /** |
| * @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()) { |
| const 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|number=} value |
| */ |
| addAttribute(key, value) { |
| this._attributes[key.toLowerCase()] = value; |
| } |
| |
| /** |
| * @param {string} cookieLine |
| */ |
| _setCookieLine(cookieLine) { |
| this._cookieLine = cookieLine; |
| } |
| |
| /** |
| * @return {string|null} |
| */ |
| getCookieLine() { |
| return this._cookieLine; |
| } |
| } |
| |
| /** |
| * @enum {number} |
| */ |
| export const Type = { |
| Request: 0, |
| Response: 1 |
| }; |
| |
| /** |
| * @enum {string} |
| */ |
| export const Attributes = { |
| Name: 'name', |
| Value: 'value', |
| Size: 'size', |
| Domain: 'domain', |
| Path: 'path', |
| Expires: 'expires', |
| HttpOnly: 'httpOnly', |
| Secure: 'secure', |
| SameSite: 'sameSite', |
| }; |
| |
| /* Legacy exported object */ |
| self.SDK = self.SDK || {}; |
| |
| /* Legacy exported object */ |
| SDK = SDK || {}; |
| |
| /** @constructor */ |
| SDK.CookieParser = CookieParser; |
| |
| /** @constructor */ |
| SDK.Cookie = Cookie; |
| |
| /** |
| * @enum {number} |
| */ |
| SDK.Cookie.Type = Type; |
| |
| /** |
| * @enum {string} |
| */ |
| SDK.Cookie.Attributes = Attributes; |