diff options
Diffstat (limited to 'remote/marionette/cookie.sys.mjs')
-rw-r--r-- | remote/marionette/cookie.sys.mjs | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/remote/marionette/cookie.sys.mjs b/remote/marionette/cookie.sys.mjs new file mode 100644 index 0000000000..117ccc33ed --- /dev/null +++ b/remote/marionette/cookie.sys.mjs @@ -0,0 +1,296 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + pprint: "chrome://remote/content/shared/Format.sys.mjs", +}); + +const IPV4_PORT_EXPR = /:\d+$/; + +const SAMESITE_MAP = new Map([ + ["None", Ci.nsICookie.SAMESITE_NONE], + ["Lax", Ci.nsICookie.SAMESITE_LAX], + ["Strict", Ci.nsICookie.SAMESITE_STRICT], +]); + +/** @namespace */ +export const cookie = { + manager: Services.cookies, +}; + +/** + * @name Cookie + * + * @returns {Object<string, (number|boolean|string)>} + */ + +/** + * Unmarshal a JSON Object to a cookie representation. + * + * Effectively this will run validation checks on ``json``, which + * will produce the errors expected by WebDriver if the input is + * not valid. + * + * @param {Object<string, (number | boolean | string)>} json + * Cookie to be deserialised. ``name`` and ``value`` are required + * fields which must be strings. The ``path`` and ``domain`` fields + * are optional, but must be a string if provided. The ``secure``, + * and ``httpOnly`` are similarly optional, but must be booleans. + * Likewise, the ``expiry`` field is optional but must be + * unsigned integer. + * + * @returns {Cookie} + * Valid cookie object. + * + * @throws {InvalidArgumentError} + * If any of the properties are invalid. + */ +cookie.fromJSON = function (json) { + let newCookie = {}; + + lazy.assert.object(json, lazy.pprint`Expected cookie object, got ${json}`); + + newCookie.name = lazy.assert.string(json.name, "Cookie name must be string"); + newCookie.value = lazy.assert.string( + json.value, + "Cookie value must be string" + ); + + if (typeof json.path != "undefined") { + newCookie.path = lazy.assert.string( + json.path, + "Cookie path must be string" + ); + } + if (typeof json.domain != "undefined") { + newCookie.domain = lazy.assert.string( + json.domain, + "Cookie domain must be string" + ); + } + if (typeof json.secure != "undefined") { + newCookie.secure = lazy.assert.boolean( + json.secure, + "Cookie secure flag must be boolean" + ); + } + if (typeof json.httpOnly != "undefined") { + newCookie.httpOnly = lazy.assert.boolean( + json.httpOnly, + "Cookie httpOnly flag must be boolean" + ); + } + if (typeof json.expiry != "undefined") { + newCookie.expiry = lazy.assert.positiveInteger( + json.expiry, + "Cookie expiry must be a positive integer" + ); + } + if (typeof json.sameSite != "undefined") { + newCookie.sameSite = lazy.assert.in( + json.sameSite, + Array.from(SAMESITE_MAP.keys()), + "Cookie SameSite flag must be one of None, Lax, or Strict" + ); + } + + return newCookie; +}; + +/** + * Insert cookie to the cookie store. + * + * @param {Cookie} newCookie + * Cookie to add. + * @param {object} options + * @param {string=} options.restrictToHost + * Perform test that ``newCookie``'s domain matches this. + * @param {string=} options.protocol + * The protocol of the caller. It can be `http:` or `https:`. + * + * @throws {TypeError} + * If ``name``, ``value``, or ``domain`` are not present and + * of the correct type. + * @throws {InvalidCookieDomainError} + * If ``restrictToHost`` is set and ``newCookie``'s domain does + * not match. + * @throws {UnableToSetCookieError} + * If an error occurred while trying to save the cookie. + */ +cookie.add = function ( + newCookie, + { restrictToHost = null, protocol = null } = {} +) { + lazy.assert.string(newCookie.name, "Cookie name must be string"); + lazy.assert.string(newCookie.value, "Cookie value must be string"); + + if (typeof newCookie.path == "undefined") { + newCookie.path = "/"; + } + + let hostOnly = false; + if (typeof newCookie.domain == "undefined") { + hostOnly = true; + newCookie.domain = restrictToHost; + } + lazy.assert.string(newCookie.domain, "Cookie domain must be string"); + if (newCookie.domain.substring(0, 1) === ".") { + newCookie.domain = newCookie.domain.substring(1); + } + + if (typeof newCookie.secure == "undefined") { + newCookie.secure = false; + } + if (typeof newCookie.httpOnly == "undefined") { + newCookie.httpOnly = false; + } + if (typeof newCookie.expiry == "undefined") { + // The XPCOM interface requires the expiry field even for session cookies. + newCookie.expiry = Number.MAX_SAFE_INTEGER; + newCookie.session = true; + } else { + newCookie.session = false; + } + newCookie.sameSite = SAMESITE_MAP.get(newCookie.sameSite || "None"); + + let isIpAddress = false; + try { + Services.eTLD.getPublicSuffixFromHost(newCookie.domain); + } catch (e) { + switch (e.result) { + case Cr.NS_ERROR_HOST_IS_IP_ADDRESS: + isIpAddress = true; + break; + default: + throw new lazy.error.InvalidCookieDomainError(newCookie.domain); + } + } + + if (!hostOnly && !isIpAddress) { + // only store this as a domain cookie if the domain was specified in the + // request and it wasn't an IP address. + newCookie.domain = "." + newCookie.domain; + } + + if (restrictToHost) { + if ( + !restrictToHost.endsWith(newCookie.domain) && + "." + restrictToHost !== newCookie.domain && + restrictToHost !== newCookie.domain + ) { + throw new lazy.error.InvalidCookieDomainError( + `Cookies may only be set ` + + `for the current domain (${restrictToHost})` + ); + } + } + + let schemeType = Ci.nsICookie.SCHEME_UNSET; + switch (protocol) { + case "http:": + schemeType = Ci.nsICookie.SCHEME_HTTP; + break; + case "https:": + schemeType = Ci.nsICookie.SCHEME_HTTPS; + break; + default: + // Any other protocol that is supported by the cookie service. + break; + } + + // remove port from domain, if present. + // unfortunately this catches IPv6 addresses by mistake + // TODO: Bug 814416 + newCookie.domain = newCookie.domain.replace(IPV4_PORT_EXPR, ""); + + try { + cookie.manager.add( + newCookie.domain, + newCookie.path, + newCookie.name, + newCookie.value, + newCookie.secure, + newCookie.httpOnly, + newCookie.session, + newCookie.expiry, + {} /* origin attributes */, + newCookie.sameSite, + schemeType + ); + } catch (e) { + throw new lazy.error.UnableToSetCookieError(e); + } +}; + +/** + * Remove cookie from the cookie store. + * + * @param {Cookie} toDelete + * Cookie to remove. + */ +cookie.remove = function (toDelete) { + cookie.manager.remove( + toDelete.domain, + toDelete.name, + toDelete.path, + {} /* originAttributes */ + ); +}; + +/** + * Iterates over the cookies for the current ``host``. You may + * optionally filter for specific paths on that ``host`` by specifying + * a path in ``currentPath``. + * + * @param {string} host + * Hostname to retrieve cookies for. + * @param {string=} [currentPath="/"] currentPath + * Optionally filter the cookies for ``host`` for the specific path. + * Defaults to ``/``, meaning all cookies for ``host`` are included. + * + * @returns {Iterable.<Cookie>} + * Iterator. + */ +cookie.iter = function* (host, currentPath = "/") { + lazy.assert.string(host, "host must be string"); + lazy.assert.string(currentPath, "currentPath must be string"); + + const isForCurrentPath = path => currentPath.includes(path); + + let cookies = cookie.manager.getCookiesFromHost(host, {}); + for (let cookie of cookies) { + // take the hostname and progressively shorten + let hostname = host; + do { + if ( + (cookie.host == "." + hostname || cookie.host == hostname) && + isForCurrentPath(cookie.path) + ) { + let data = { + name: cookie.name, + value: cookie.value, + path: cookie.path, + domain: cookie.host, + secure: cookie.isSecure, + httpOnly: cookie.isHttpOnly, + }; + + if (!cookie.isSession) { + data.expiry = cookie.expiry; + } + + data.sameSite = [...SAMESITE_MAP].find( + ([, value]) => cookie.sameSite === value + )[0]; + + yield data; + } + hostname = hostname.replace(/^.*?\./, ""); + } while (hostname.includes(".")); + } +}; |