tor-browser

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

custom_functions.js (5957B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /* globals browser */
      8 
      9 const replaceStringInRequest = (
     10  requestId,
     11  inString,
     12  outString,
     13  inEncoding = "utf-8"
     14 ) => {
     15  const filter = browser.webRequest.filterResponseData(requestId);
     16  const decoder = new TextDecoder(inEncoding);
     17  const encoder = new TextEncoder();
     18  const RE = new RegExp(inString, "g");
     19  const carryoverLength = inString.length;
     20  let carryover = "";
     21 
     22  filter.ondata = event => {
     23    const replaced = (
     24      carryover + decoder.decode(event.data, { stream: true })
     25    ).replace(RE, outString);
     26    filter.write(encoder.encode(replaced.slice(0, -carryoverLength)));
     27    carryover = replaced.slice(-carryoverLength);
     28  };
     29 
     30  filter.onstop = () => {
     31    if (carryover.length) {
     32      filter.write(encoder.encode(carryover));
     33    }
     34    filter.close();
     35  };
     36 };
     37 
     38 const interventionListeners = new Map();
     39 
     40 function rememberListener(intervention, key, listener) {
     41  if (!interventionListeners.has(intervention)) {
     42    interventionListeners.set(intervention, new Map());
     43  }
     44  const map = interventionListeners.get(intervention);
     45  if (map.has(key)) {
     46    throw new Error(`multiple custom listeners have the same key ${key}`);
     47  }
     48  map.set(key, listener);
     49 }
     50 
     51 function forgetListener(intervention, key) {
     52  const map = interventionListeners.get(intervention);
     53  if (!map) {
     54    return undefined;
     55  }
     56  const listener = map.get(key);
     57  map.delete(key);
     58  return listener;
     59 }
     60 
     61 function makeHeaderAlterer(headerType, webRequestAPI) {
     62  return {
     63    details: ["headers", "replacement"],
     64    optionalDetails: ["fallback", "replace", "types", "urls"],
     65    getKey(config) {
     66      return `alter_${headerType}_headers:${JSON.stringify(config)}`;
     67    },
     68    enable(config, intervention) {
     69      let { fallback, headers, replace, replacement, types, urls } = config;
     70      if (!urls) {
     71        urls = Object.values(intervention.bugs)
     72          .map(bug => bug.matches)
     73          .flat()
     74          .filter(v => v !== undefined);
     75      }
     76      const regex =
     77        replace === null ? null : new RegExp(replace ?? "^.*$", "gi");
     78      const listener = evt => {
     79        let found = false;
     80        const finalHeaders = [];
     81        for (const header of evt[`${headerType}Headers`]) {
     82          if (headers.includes(header.name.toLowerCase())) {
     83            found = true;
     84            if (
     85              regex !== null &&
     86              replacement !== null &&
     87              replacement !== undefined
     88            ) {
     89              const value = header.value.replaceAll(regex, replacement);
     90              finalHeaders.push({ name: header.name, value });
     91            } else if (replacement !== null) {
     92              finalHeaders.push(header);
     93            }
     94          } else {
     95            finalHeaders.push(header);
     96          }
     97        }
     98        if (!found && (replace === undefined || typeof fallback === "string")) {
     99          const value = fallback ?? replacement;
    100          if (value !== null) {
    101            finalHeaders.push({
    102              name: headers[0],
    103              value,
    104            });
    105          }
    106        }
    107        const retval = {};
    108        retval[`${headerType}Headers`] = finalHeaders;
    109        return retval;
    110      };
    111      browser.webRequest[webRequestAPI].addListener(listener, { types, urls }, [
    112        "blocking",
    113        `${headerType}Headers`,
    114      ]);
    115      rememberListener(intervention, this.getKey(config), listener);
    116    },
    117    disable(config, intervention) {
    118      const listener = forgetListener(intervention, this.getKey(config));
    119      if (listener) {
    120        browser.webRequest[webRequestAPI].removeListener(listener);
    121      }
    122    },
    123  };
    124 }
    125 
    126 var CUSTOM_FUNCTIONS = {
    127  alter_request_headers: makeHeaderAlterer("request", "onBeforeSendHeaders"),
    128  alter_response_headers: makeHeaderAlterer("response", "onHeadersReceived"),
    129  replace_string_in_request: {
    130    details: ["find", "replace", "urls"],
    131    optionalDetails: ["types"],
    132    enable(details) {
    133      const { find, replace, urls, types } = details;
    134      const listener = (details.listener = ({ requestId }) => {
    135        replaceStringInRequest(requestId, find, replace);
    136        return {};
    137      });
    138      browser.webRequest.onBeforeRequest.addListener(
    139        listener,
    140        { urls, types },
    141        ["blocking"]
    142      );
    143    },
    144    disable(details) {
    145      const { listener } = details;
    146      browser.webRequest.onBeforeRequest.removeListener(listener);
    147      delete details.listener;
    148    },
    149  },
    150  run_script_before_request: {
    151    details: ["message", "urls", "script"],
    152    optionalDetails: ["types"],
    153    enable(details, intervention) {
    154      const { bug } = intervention;
    155      const { message, script, types, urls } = details;
    156      const warning = `${message} See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`;
    157 
    158      const listener = (details.listener = evt => {
    159        const { tabId, frameId } = evt;
    160        return browser.tabs
    161          .executeScript(tabId, {
    162            file: script,
    163            frameId,
    164            runAt: "document_start",
    165          })
    166          .then(() => {
    167            browser.tabs.executeScript(tabId, {
    168              code: `console.warn(${JSON.stringify(warning)})`,
    169              runAt: "document_start",
    170            });
    171          })
    172          .catch(err => {
    173            console.error(
    174              "Error running script before request for webcompat intervention for bug",
    175              bug,
    176              err
    177            );
    178          });
    179      });
    180 
    181      browser.webRequest.onBeforeRequest.addListener(
    182        listener,
    183        { urls, types: types || ["script"] },
    184        ["blocking"]
    185      );
    186    },
    187    disable(details) {
    188      const { listener } = details;
    189      browser.webRequest.onBeforeRequest.removeListener(listener);
    190      delete details.listener;
    191    },
    192  },
    193 };