/* * Copyright (C) 2017 Apple 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: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. */ WI.Cookie = class Cookie { constructor(type, name, value, {header, expires, session, maxAge, path, domain, secure, httpOnly, sameSite} = {}) { console.assert(Object.values(WI.Cookie.Type).includes(type)); console.assert(typeof name === "string"); console.assert(typeof value === "string"); console.assert(!header || typeof header === "string"); console.assert(!expires || expires instanceof Date); console.assert(!session || typeof session === "boolean"); console.assert(!maxAge || typeof maxAge === "number"); console.assert(!path || typeof path === "string"); console.assert(!domain || typeof domain === "string"); console.assert(!secure || typeof secure === "boolean"); console.assert(!httpOnly || typeof httpOnly === "boolean"); console.assert(!sameSite || Object.values(WI.Cookie.SameSiteType).includes(sameSite)); this._type = type; this._name = name; this._value = value; this._size = this._name.length + this._value.length; if (this._type === WI.Cookie.Type.Response) { this._header = header || ""; this._expires = (!session && expires) || null; this._session = session || false; this._maxAge = maxAge || null; this._path = path || null; this._domain = domain || null; this._secure = secure || false; this._httpOnly = httpOnly || false; this._sameSite = sameSite || WI.Cookie.SameSiteType.None; } } // Static static fromPayload(payload) { let {name, value, ...options} = payload; options.expires = options.expires ? new Date(options.expires.maxDecimals(-3)) : null; return new WI.Cookie(WI.Cookie.Type.Response, name, value, options); } // RFC 6265 defines the HTTP Cookie and Set-Cookie header fields: // https://www.ietf.org/rfc/rfc6265.txt static parseCookieRequestHeader(header) { if (!header) return []; header = header.trim(); if (!header) return []; let cookies = []; // Cookie: = ( ";" SP = )*? // NOTE: Just name/value pairs. let pairs = header.split(/; /); for (let pair of pairs) { let match = pair.match(/^(?[^\s=]+)[ \t]*=[ \t]*(?.*)$/); if (!match) { WI.reportInternalError("Failed to parse Cookie pair", {header, pair}); continue; } let {name, value} = match.groups; cookies.push(new WI.Cookie(WI.Cookie.Type.Request, name, value)); } return cookies; } static displayNameForSameSiteType(sameSiteType) { switch (sameSiteType) { case WI.Cookie.SameSiteType.None: return WI.unlocalizedString("None"); case WI.Cookie.SameSiteType.Lax: return WI.unlocalizedString("Lax"); case WI.Cookie.SameSiteType.Strict: return WI.unlocalizedString("Strict"); default: console.error("Invalid SameSite type", sameSiteType); return sameSiteType; } } // static parseSameSiteAttributeValue(attributeValue) { if (!attributeValue) return WI.Cookie.SameSiteType.None; switch (attributeValue.toLowerCase()) { case "lax": return WI.Cookie.SameSiteType.Lax; case "strict": return WI.Cookie.SameSiteType.Strict; } return WI.Cookie.SameSiteType.None; } static parseSetCookieResponseHeader(header) { if (!header) return null; // Set-Cookie: = ( ";" SP )*? // NOTE: Some attributes can have pairs (e.g. "Path=/"), some are only a // single word (e.g. "Secure"). // Parse name/value. let nameValueMatch = header.match(/^(?[^\s=]+)[ \t]*=[ \t]*(?[^;]*)/); if (!nameValueMatch) { WI.reportInternalError("Failed to parse Set-Cookie header", {header}); return null; } let {name, value} = nameValueMatch.groups; let expires = null; let session = false; let maxAge = null; let path = null; let domain = null; let secure = false; let httpOnly = false; let sameSite = WI.Cookie.SameSiteType.None; // Parse Attributes let remaining = header.substr(nameValueMatch[0].length); let attributes = remaining.split(/; ?/); for (let attribute of attributes) { if (!attribute) continue; let match = attribute.match(/^(?[^\s=]+)(?:=(?.*))?$/); if (!match) { console.error("Failed to parse Set-Cookie attribute:", attribute); continue; } let attributeName = match.groups.name; let attributeValue = match.groups.value; switch (attributeName.toLowerCase()) { case "expires": console.assert(attributeValue); expires = new Date(attributeValue); if (isNaN(expires.getTime())) { console.warn("Invalid Expires date:", attributeValue); expires = null; } break; case "max-age": console.assert(attributeValue); maxAge = parseInt(attributeValue, 10); if (isNaN(maxAge) || !/^\d+$/.test(attributeValue)) { console.warn("Invalid MaxAge value:", attributeValue); maxAge = null; } break; case "path": console.assert(attributeValue); path = attributeValue; break; case "domain": console.assert(attributeValue); domain = attributeValue; break; case "secure": console.assert(!attributeValue); secure = true; break; case "httponly": console.assert(!attributeValue); httpOnly = true; break; case "samesite": sameSite = WI.Cookie.parseSameSiteAttributeValue(attributeValue); break; default: console.warn("Unknown Cookie attribute:", attribute); break; } } if (!expires) session = true; return new WI.Cookie(WI.Cookie.Type.Response, name, value, {header, expires, session, maxAge, path, domain, secure, httpOnly, sameSite}); } // Public get type() { return this._type; } get name() { return this._name; } get value() { return this._value; } get header() { return this._header; } get expires() { return this._expires; } get session() { return this._session; } get maxAge() { return this._maxAge; } get path() { return this._path; } get domain() { return this._domain; } get secure() { return this._secure; } get httpOnly() { return this._httpOnly; } get sameSite() { return this._sameSite; } get size() { return this._size; } get url() { let url = this._secure ? "https://" : "http://"; url += this._domain || ""; url += this._path || ""; return url; } expirationDate(requestSentDate) { if (this._session) return null; if (this._maxAge) { let startDate = requestSentDate || new Date; return new Date(startDate.getTime() + (this._maxAge * 1000)); } return this._expires; } equals(other) { return this._type === other.type && this._name === other.name && this._value === other.value && this._header === other.header && this._expires?.getTime() === other.expires?.getTime() && this._session === other.session && this._maxAge === other.maxAge && this._path === other.path && this._domain === other.domain && this._secure === other.secure && this._httpOnly === other.httpOnly && this._sameSite === other.sameSite; } toProtocol() { if (typeof this._name !== "string") return null; if (typeof this._value !== "string") return null; if (typeof this._domain !== "string") return null; if (typeof this._path !== "string") return null; if (!this._session && !this._expires) return null; if (!Object.values(WI.Cookie.SameSiteType).includes(this._sameSite)) return null; let json = { name: this._name, value: this._value, domain: this._domain, path: this._path, expires: this._expires?.getTime(), session: this._session, httpOnly: !!this._httpOnly, secure: !!this._secure, sameSite: this._sameSite, }; return json; } }; WI.Cookie.Type = { Request: "request", Response: "response", }; // Keep these in sync with the "CookieSameSitePolicy" enum defined by the "Page" domain. WI.Cookie.SameSiteType = { None: "None", Lax: "Lax", Strict: "Strict", };