tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

cookie.sys.mjs (10167B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
      9  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     10  pprint: "chrome://remote/content/shared/Format.sys.mjs",
     11 });
     12 
     13 const IPV4_PORT_EXPR = /:\d+$/;
     14 
     15 const SAMESITE_MAP = new Map([
     16  [Ci.nsICookie.SAMESITE_NONE, "None"],
     17  [Ci.nsICookie.SAMESITE_LAX, "Lax"],
     18  [Ci.nsICookie.SAMESITE_STRICT, "Strict"],
     19  [Ci.nsICookie.SAMESITE_UNSET, "None"],
     20 ]);
     21 
     22 /** @namespace */
     23 export const cookie = {
     24  manager: Services.cookies,
     25 };
     26 
     27 /**
     28 * @name Cookie
     29 *
     30 * @returns {Record<string, (number|boolean|string)>}
     31 */
     32 
     33 /**
     34 * Unmarshal a JSON Object to a cookie representation.
     35 *
     36 * Effectively this will run validation checks on ``json``, which
     37 * will produce the errors expected by WebDriver if the input is
     38 * not valid.
     39 *
     40 * @param {Record<string, (number | boolean | string)>} json
     41 *     Cookie to be deserialised. ``name`` and ``value`` are required
     42 *     fields which must be strings.  The ``path`` and ``domain`` fields
     43 *     are optional, but must be a string if provided.  The ``secure``,
     44 *     and ``httpOnly`` are similarly optional, but must be booleans.
     45 *     Likewise, the ``expiry`` field is optional but must be
     46 *     unsigned integer.
     47 *
     48 * @returns {Cookie}
     49 *     Valid cookie object.
     50 *
     51 * @throws {InvalidArgumentError}
     52 *     If any of the properties are invalid.
     53 */
     54 cookie.fromJSON = function (json) {
     55  let newCookie = {};
     56 
     57  lazy.assert.object(
     58    json,
     59    lazy.pprint`Expected "cookie" to be an object, got ${json}`
     60  );
     61 
     62  newCookie.name = lazy.assert.string(
     63    json.name,
     64    lazy.pprint`Expected cookie "name" to be a string, got ${json.name}`
     65  );
     66  newCookie.value = lazy.assert.string(
     67    json.value,
     68    lazy.pprint`Expected cookie "value" to be a string, got ${json.value}`
     69  );
     70 
     71  if (typeof json.path != "undefined") {
     72    newCookie.path = lazy.assert.string(
     73      json.path,
     74      lazy.pprint`Expected cookie "path" to be a string, got ${json.path}`
     75    );
     76  }
     77  if (typeof json.domain != "undefined") {
     78    newCookie.domain = lazy.assert.string(
     79      json.domain,
     80      lazy.pprint`Expected cookie "domain" to be a string, got ${json.domain}`
     81    );
     82  }
     83  if (typeof json.secure != "undefined") {
     84    newCookie.secure = lazy.assert.boolean(
     85      json.secure,
     86      lazy.pprint`Expected cookie "secure" to be a boolean, got ${json.secure}`
     87    );
     88  }
     89  if (typeof json.httpOnly != "undefined") {
     90    newCookie.httpOnly = lazy.assert.boolean(
     91      json.httpOnly,
     92      lazy.pprint`Expected cookie "httpOnly" to be a boolean, got ${json.httpOnly}`
     93    );
     94  }
     95  if (typeof json.expiry != "undefined") {
     96    newCookie.expiry = lazy.assert.positiveInteger(
     97      json.expiry,
     98      lazy.pprint`Expected cookie "expiry" to be a positive integer, got ${json.expiry}`
     99    );
    100  }
    101  if (typeof json.sameSite != "undefined") {
    102    const validOptions = Array.from(SAMESITE_MAP.values());
    103    newCookie.sameSite = lazy.assert.in(
    104      json.sameSite,
    105      validOptions,
    106      `Expected cookie "sameSite" to be one of ${validOptions.toString()}, ` +
    107        lazy.pprint`got ${json.sameSite}`
    108    );
    109  }
    110 
    111  return newCookie;
    112 };
    113 
    114 /**
    115 * Insert cookie to the cookie store.
    116 *
    117 * @param {Cookie} newCookie
    118 *     Cookie to add.
    119 * @param {object} options
    120 * @param {string=} options.restrictToHost
    121 *     Perform test that ``newCookie``'s domain matches this.
    122 * @param {string=} options.protocol
    123 *     The protocol of the caller. It can be `http:` or `https:`.
    124 *
    125 * @throws {TypeError}
    126 *     If ``name``, ``value``, or ``domain`` are not present and
    127 *     of the correct type.
    128 * @throws {InvalidCookieDomainError}
    129 *     If ``restrictToHost`` is set and ``newCookie``'s domain does
    130 *     not match.
    131 * @throws {UnableToSetCookieError}
    132 *     If an error occurred while trying to save the cookie.
    133 */
    134 cookie.add = function (
    135  newCookie,
    136  { restrictToHost = null, protocol = null } = {}
    137 ) {
    138  lazy.assert.string(
    139    newCookie.name,
    140    lazy.pprint`Expected cookie "name" to be a string, got ${newCookie.name}`
    141  );
    142  lazy.assert.string(
    143    newCookie.value,
    144    lazy.pprint`Expected cookie "value" to be a string, got ${newCookie.value}`
    145  );
    146 
    147  if (typeof newCookie.path == "undefined") {
    148    newCookie.path = "/";
    149  }
    150 
    151  let hostOnly = false;
    152  if (typeof newCookie.domain == "undefined") {
    153    hostOnly = true;
    154    newCookie.domain = restrictToHost;
    155  }
    156  lazy.assert.string(
    157    newCookie.domain,
    158    lazy.pprint`Expected cookie "domain" to be a string, got ${newCookie.domain}`
    159  );
    160  if (newCookie.domain.substring(0, 1) === ".") {
    161    newCookie.domain = newCookie.domain.substring(1);
    162  }
    163 
    164  if (typeof newCookie.secure == "undefined") {
    165    newCookie.secure = false;
    166  }
    167  if (typeof newCookie.httpOnly == "undefined") {
    168    newCookie.httpOnly = false;
    169  }
    170 
    171  if (typeof newCookie.expiry == "undefined") {
    172    // The XPCOM interface requires the expiry field even for session cookies.
    173    newCookie.expiry = Number.MAX_SAFE_INTEGER;
    174    newCookie.session = true;
    175  } else {
    176    newCookie.session = false;
    177    // Gecko expects the expiry value to be in milliseconds, WebDriver uses seconds.
    178    // The maximum allowed value is capped at 400 days.
    179    newCookie.expiry = Services.cookies.maybeCapExpiry(newCookie.expiry * 1000);
    180  }
    181 
    182  let sameSite = [...SAMESITE_MAP].find(
    183    ([, value]) => newCookie.sameSite === value
    184  );
    185  newCookie.sameSite = sameSite ? sameSite[0] : Ci.nsICookie.SAMESITE_UNSET;
    186 
    187  let isIpAddress = false;
    188  try {
    189    Services.eTLD.getPublicSuffixFromHost(newCookie.domain);
    190  } catch (e) {
    191    switch (e.result) {
    192      case Cr.NS_ERROR_HOST_IS_IP_ADDRESS:
    193        isIpAddress = true;
    194        break;
    195      default:
    196        throw new lazy.error.InvalidCookieDomainError(newCookie.domain);
    197    }
    198  }
    199 
    200  if (!hostOnly && !isIpAddress) {
    201    // only store this as a domain cookie if the domain was specified in the
    202    // request and it wasn't an IP address.
    203    newCookie.domain = "." + newCookie.domain;
    204  }
    205 
    206  if (restrictToHost) {
    207    if (
    208      !restrictToHost.endsWith(newCookie.domain) &&
    209      "." + restrictToHost !== newCookie.domain &&
    210      restrictToHost !== newCookie.domain
    211    ) {
    212      throw new lazy.error.InvalidCookieDomainError(
    213        `Cookies may only be set ` +
    214          `for the current domain (${restrictToHost})`
    215      );
    216    }
    217  }
    218 
    219  let schemeType = Ci.nsICookie.SCHEME_UNSET;
    220  switch (protocol) {
    221    case "http:":
    222      schemeType = Ci.nsICookie.SCHEME_HTTP;
    223      break;
    224    case "https:":
    225      schemeType = Ci.nsICookie.SCHEME_HTTPS;
    226      break;
    227    default:
    228      // Any other protocol that is supported by the cookie service.
    229      break;
    230  }
    231 
    232  // remove port from domain, if present.
    233  // unfortunately this catches IPv6 addresses by mistake
    234  // TODO: Bug 814416
    235  newCookie.domain = newCookie.domain.replace(IPV4_PORT_EXPR, "");
    236 
    237  let cv;
    238  try {
    239    cv = cookie.manager.add(
    240      newCookie.domain,
    241      newCookie.path,
    242      newCookie.name,
    243      newCookie.value,
    244      newCookie.secure,
    245      newCookie.httpOnly,
    246      newCookie.session,
    247      newCookie.expiry,
    248      {} /* origin attributes */,
    249      newCookie.sameSite,
    250      schemeType
    251    );
    252  } catch (e) {
    253    throw new lazy.error.UnableToSetCookieError(e);
    254  }
    255 
    256  if (cv.result !== Ci.nsICookieValidation.eOK) {
    257    throw new lazy.error.UnableToSetCookieError(
    258      `Invalid cookie: ${cv.errorString}`
    259    );
    260  }
    261 };
    262 
    263 /**
    264 * Remove cookie from the cookie store.
    265 *
    266 * @param {Cookie} toDelete
    267 *     Cookie to remove.
    268 */
    269 cookie.remove = function (toDelete) {
    270  cookie.manager.remove(
    271    toDelete.domain,
    272    toDelete.name,
    273    toDelete.path,
    274    {} /* originAttributes */
    275  );
    276 };
    277 
    278 /**
    279 * Iterates over the cookies for the current ``host``.  You may
    280 * optionally filter for specific paths on that ``host`` by specifying
    281 * a path in ``currentPath``.
    282 *
    283 * @param {string} host
    284 *     Hostname to retrieve cookies for.
    285 * @param {BrowsingContext=} [browsingContext=undefined] browsingContext
    286 *     The BrowsingContext that is reading these cookies.
    287 *     Used to get the correct partitioned cookies.
    288 * @param {string=} [currentPath="/"] currentPath
    289 *     Optionally filter the cookies for ``host`` for the specific path.
    290 *     Defaults to ``/``, meaning all cookies for ``host`` are included.
    291 *
    292 * @returns {Iterable.<Cookie>}
    293 *     Iterator.
    294 */
    295 cookie.iter = function* (host, browsingContext = undefined, currentPath = "/") {
    296  lazy.assert.string(
    297    host,
    298    lazy.pprint`Expected "host" to be a string, got ${host}`
    299  );
    300  lazy.assert.string(
    301    currentPath,
    302    lazy.pprint`Expected "currentPath" to be a string, got ${currentPath}`
    303  );
    304 
    305  const isForCurrentPath = path => currentPath.includes(path);
    306 
    307  let cookies = cookie.manager.getCookiesFromHost(host, {});
    308  if (browsingContext) {
    309    let partitionedOriginAttributes = {
    310      partitionKey:
    311        browsingContext.currentWindowGlobal?.cookieJarSettings?.partitionKey,
    312    };
    313    let cookiesPartitioned = cookie.manager.getCookiesFromHost(
    314      host,
    315      partitionedOriginAttributes
    316    );
    317    cookies.push(...cookiesPartitioned);
    318  }
    319  for (let cookie of cookies) {
    320    // take the hostname and progressively shorten
    321    let hostname = host;
    322    do {
    323      if (
    324        (cookie.host == "." + hostname || cookie.host == hostname) &&
    325        isForCurrentPath(cookie.path)
    326      ) {
    327        let data = {
    328          name: cookie.name,
    329          value: cookie.value,
    330          path: cookie.path,
    331          domain: cookie.host,
    332          secure: cookie.isSecure,
    333          httpOnly: cookie.isHttpOnly,
    334        };
    335 
    336        if (!cookie.isSession) {
    337          // Internally expiry is in ms, WebDriver expects seconds.
    338          data.expiry = Math.round(cookie.expiry / 1000);
    339        }
    340 
    341        data.sameSite = SAMESITE_MAP.get(cookie.sameSite) || "None";
    342 
    343        yield data;
    344      }
    345      hostname = hostname.replace(/^.*?\./, "");
    346    } while (hostname.includes("."));
    347  }
    348 };