Verified Commit 384dff97 authored by Pier Angelo Vendrame's avatar Pier Angelo Vendrame 🎃
Browse files

fixup! Bug 40933: Add tor-launcher functionality

Remove the final references to TorMonitorService and TorProtocolService.
parent cd92f30e
Loading
Loading
Loading
Loading
+40 −341
Original line number Diff line number Diff line
@@ -274,6 +274,44 @@ class AsyncSocket {
 * the command
 */

/**
 * @typedef {object} Bridge
 * @property {string} transport The transport of the bridge, or vanilla if not
 * specified.
 * @property {string} addr The IP address and port of the bridge
 * @property {string} id The fingerprint of the bridge
 * @property {string} args Optional arguments passed to the bridge
 */
/**
 * @typedef {object} PTInfo The information about a pluggable transport
 * @property {string[]} transports An array with all the transports supported by
 * this configuration.
 * @property {string} type Either socks4, socks5 or exec
 * @property {string} [ip] The IP address of the proxy (only for socks4 and
 * socks5)
 * @property {integer} [port] The port of the proxy (only for socks4 and socks5)
 * @property {string} [pathToBinary] Path to the binary that is run (only for
 * exec)
 * @property {string} [options] Optional options passed to the binary (only for
 * exec)
 */
/**
 * @typedef {object} OnionAuthKeyInfo
 * @property {string} address The address of the onion service
 * @property {string} typeAndKey Onion service key and type of key, as
 * `type:base64-private-key`
 * @property {string} Flags Additional flags, such as Permanent
 */
/**
 * @callback EventFilterCallback
 * @param {any} data Either a raw string, or already parsed data
 * @returns {boolean}
 */
/**
 * @callback EventCallback
 * @param {any} data Either a raw string, or already parsed data
 */

class TorError extends Error {
  constructor(command, reply) {
    super(`${command} -> ${reply}`);
@@ -584,319 +622,6 @@ class ControlSocket {
  }
}

// ## utils
// A namespace for utility functions
let utils = {};

// __utils.identity(x)__.
// Returns its argument unchanged.
utils.identity = function (x) {
  return x;
};

// __utils.capture(string, regex)__.
// Takes a string and returns an array of capture items, where regex must have a single
// capturing group and use the suffix /.../g to specify a global search.
utils.capture = function (string, regex) {
  let matches = [];
  // Special trick to use string.replace for capturing multiple matches.
  string.replace(regex, function (a, captured) {
    matches.push(captured);
  });
  return matches;
};

// __utils.extractor(regex)__.
// Returns a function that takes a string and returns an array of regex matches. The
// regex must use the suffix /.../g to specify a global search.
utils.extractor = function (regex) {
  return function (text) {
    return utils.capture(text, regex);
  };
};

// __utils.splitLines(string)__.
// Splits a string into an array of strings, each corresponding to a line.
utils.splitLines = function (string) {
  return string.split(/\r?\n/);
};

// __utils.splitAtSpaces(string)__.
// Splits a string into chunks between spaces. Does not split at spaces
// inside pairs of quotation marks.
utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);

// __utils.splitAtFirst(string, regex)__.
// Splits a string at the first instance of regex match. If no match is
// found, returns the whole string.
utils.splitAtFirst = function (string, regex) {
  let match = string.match(regex);
  return match
    ? [
        string.substring(0, match.index),
        string.substring(match.index + match[0].length),
      ]
    : string;
};

// __utils.splitAtEquals(string)__.
// Splits a string into chunks between equals. Does not split at equals
// inside pairs of quotation marks.
utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g);

// __utils.mergeObjects(arrayOfObjects)__.
// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object.
// Pure function.
utils.mergeObjects = function (arrayOfObjects) {
  let result = {};
  for (let obj of arrayOfObjects) {
    for (let key in obj) {
      result[key] = obj[key];
    }
  }
  return result;
};

// __utils.listMapData(parameterString, listNames)__.
// Takes a list of parameters separated by spaces, of which the first several are
// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames
// to the unnamed parameters, and combine them in a map with the named parameters.
// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH`
//
//     utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH",
//                       ["streamID", "event", "circuitID", "IP"])
//     // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0",
//     //      "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}"
utils.listMapData = function (parameterString, listNames) {
  // Split out the space-delimited parameters.
  let parameters = utils.splitAtSpaces(parameterString),
    dataMap = {};
  // Assign listNames to the first n = listNames.length parameters.
  for (let i = 0; i < listNames.length; ++i) {
    dataMap[listNames[i]] = parameters[i];
  }
  // Read key-value pairs and copy these to the dataMap.
  for (let i = listNames.length; i < parameters.length; ++i) {
    let [key, value] = utils.splitAtEquals(parameters[i]);
    if (key && value) {
      dataMap[key] = value;
    }
  }
  return dataMap;
};

// ## info
// A namespace for functions related to tor's GETINFO and GETCONF command.
let info = {};

// __info.keyValueStringsFromMessage(messageText)__.
// Takes a message (text) response to GETINFO or GETCONF and provides
// a series of key-value strings, which are either multiline (with a `250+` prefix):
//
//     250+config/defaults=
//     AccountingMax "0 bytes"
//     AllowDotExit "0"
//     .
//
// or single-line (with a `250-` or `250 ` prefix):
//
//     250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
info.keyValueStringsFromMessage = utils.extractor(
  /^(250\+[\s\S]+?^\.|250[- ].+?)$/gim
);

// __info.applyPerLine(transformFunction)__.
// Returns a function that splits text into lines,
// and applies transformFunction to each line.
info.applyPerLine = function (transformFunction) {
  return function (text) {
    return utils.splitLines(text.trim()).map(transformFunction);
  };
};

// __info.routerStatusParser(valueString)__.
// Parses a router status entry as, described in
// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
// (search for "router status entry")
info.routerStatusParser = function (valueString) {
  let lines = utils.splitLines(valueString),
    objects = [];
  for (let line of lines) {
    // Drop first character and grab data following it.
    let myData = line.substring(2),
      // Accumulate more maps with data, depending on the first character in the line.
      dataFun = {
        r: data =>
          utils.listMapData(data, [
            "nickname",
            "identity",
            "digest",
            "publicationDate",
            "publicationTime",
            "IP",
            "ORPort",
            "DirPort",
          ]),
        a: data => ({ IPv6: data }),
        s: data => ({ statusFlags: utils.splitAtSpaces(data) }),
        v: data => ({ version: data }),
        w: data => utils.listMapData(data, []),
        p: data => ({ portList: data.split(",") }),
      }[line.charAt(0)];
    if (dataFun !== undefined) {
      objects.push(dataFun(myData));
    }
  }
  return utils.mergeObjects(objects);
};

// __info.circuitStatusParser(line)__.
// Parse the output of a circuit status line.
info.circuitStatusParser = function (line) {
  let data = utils.listMapData(line, ["id", "status", "circuit"]),
    circuit = data.circuit;
  // Parse out the individual circuit IDs and names.
  if (circuit) {
    data.circuit = circuit.split(",").map(function (x) {
      return x.split(/~|=/);
    });
  }
  return data;
};

// __info.streamStatusParser(line)__.
// Parse the output of a stream status line.
info.streamStatusParser = function (text) {
  return utils.listMapData(text, [
    "StreamID",
    "StreamStatus",
    "CircuitID",
    "Target",
  ]);
};

// TODO: fix this parsing logic to handle bridgeLine correctly
// fingerprint/id is an optional parameter
// __info.bridgeParser(bridgeLine)__.
// Takes a single line from a `getconf bridge` result and returns
// a map containing the bridge's type, address, and ID.
info.bridgeParser = function (bridgeLine) {
  let result = {},
    tokens = bridgeLine.split(/\s+/);
  // First check if we have a "vanilla" bridge:
  if (tokens[0].match(/^\d+\.\d+\.\d+\.\d+/)) {
    result.type = "vanilla";
    [result.address, result.ID] = tokens;
    // Several bridge types have a similar format:
  } else {
    result.type = tokens[0];
    if (
      [
        "flashproxy",
        "fte",
        "meek",
        "meek_lite",
        "obfs3",
        "obfs4",
        "scramblesuit",
        "snowflake",
      ].includes(result.type)
    ) {
      [result.address, result.ID] = tokens.slice(1);
    }
  }
  return result.type ? result : null;
};

// __info.parsers__.
// A map of GETINFO and GETCONF keys to parsing function, which convert
// result strings to JavaScript data.
info.parsers = {
  "ns/id/": info.routerStatusParser,
  "ip-to-country/": utils.identity,
  "circuit-status": info.applyPerLine(info.circuitStatusParser),
  bridge: info.bridgeParser,
  // Currently unused parsers:
  //  "ns/name/" : info.routerStatusParser,
  //  "stream-status" : info.applyPerLine(info.streamStatusParser),
  //  "version" : utils.identity,
  //  "config-file" : utils.identity,
};

// __info.getParser(key)__.
// Takes a key and determines the parser function that should be used to
// convert its corresponding valueString to JavaScript data.
info.getParser = function (key) {
  return (
    info.parsers[key] ||
    info.parsers[key.substring(0, key.lastIndexOf("/") + 1)]
  );
};

// __info.stringToValue(string)__.
// Converts a key-value string as from GETINFO or GETCONF to a value.
info.stringToValue = function (string) {
  // key should look something like `250+circuit-status=` or `250-circuit-status=...`
  // or `250 circuit-status=...`
  let matchForKey = string.match(/^250[ +-](.+?)=/),
    key = matchForKey ? matchForKey[1] : null;
  if (key === null) {
    return null;
  }
  // matchResult finds a single-line result for `250-` or `250 `,
  // or a multi-line one for `250+`.
  let matchResult =
      string.match(/^250[ -].+?=(.*)$/) ||
      string.match(/^250\+.+?=([\s\S]*?)^\.$/m),
    // Retrieve the captured group (the text of the value in the key-value pair)
    valueString = matchResult ? matchResult[1] : null,
    // Get the parser function for the key found.
    parse = info.getParser(key.toLowerCase());
  if (parse === undefined) {
    throw new Error("No parser found for '" + key + "'");
  }
  // Return value produced by the parser.
  return parse(valueString);
};

/**
 * @typedef {object} Bridge
 * @property {string} transport The transport of the bridge, or vanilla if not
 * specified.
 * @property {string} addr The IP address and port of the bridge
 * @property {string} id The fingerprint of the bridge
 * @property {string} args Optional arguments passed to the bridge
 */
/**
 * @typedef {object} PTInfo The information about a pluggable transport
 * @property {string[]} transports An array with all the transports supported by
 * this configuration.
 * @property {string} type Either socks4, socks5 or exec
 * @property {string} [ip] The IP address of the proxy (only for socks4 and
 * socks5)
 * @property {integer} [port] The port of the proxy (only for socks4 and socks5)
 * @property {string} [pathToBinary] Path to the binary that is run (only for
 * exec)
 * @property {string} [options] Optional options passed to the binary (only for
 * exec)
 */
/**
 * @typedef {object} OnionAuthKeyInfo
 * @property {string} address The address of the onion service
 * @property {string} typeAndKey Onion service key and type of key, as
 * `type:base64-private-key`
 * @property {string} Flags Additional flags, such as Permanent
 */
/**
 * @callback EventFilterCallback
 * @param {any} data Either a raw string, or already parsed data
 * @returns {boolean}
 */
/**
 * @callback EventCallback
 * @param {any} data Either a raw string, or already parsed data
 */

class TorController {
  /**
   * The control socket
@@ -905,16 +630,6 @@ class TorController {
   */
  #socket;

  /**
   * A map of EVENT keys to parsing functions, which convert result strings to
   * JavaScript data.
   */
  #eventParsers = {
    stream: info.streamStatusParser,
    // Currently unused:
    // "circ" : info.circuitStatusParser,
  };

  /**
   * Builds a new TorController.
   *
@@ -1340,28 +1055,12 @@ class TorController {
   * first.
   *
   * @param {string} type The event type to catch
   * @param {EventFilterCallback?} filter An optional callback to filter
   * events for which the callback will be called. If null, all events will be
   * passed.
   * @param {EventCallback} callback The callback that will handle the event
   * @param {boolean} raw Tell whether to ignore the data parser, even if
   * supported
   */
  watchEvent(type, filter, callback, raw = false) {
  watchEvent(type, callback) {
    this.#expectString(type, "type");
    const start = `650 ${type}`;
    this.#socket.addNotificationCallback(new RegExp(`^${start}`), message => {
      // Remove also the initial text
      const dataText = message.substring(start.length + 1);
      const parser = this.#eventParsers[type.toLowerCase()];
      const data = dataText && parser ? parser(dataText) : null;
      // FIXME: This is the original code, but we risk of not filtering on the
      // data, if we ask for raw data (which we always do at the moment, but we
      // do not use a filter either...)
      if (filter === null || filter(data)) {
        callback(data && !raw ? data : message);
      }
    });
    this.#socket.addNotificationCallback(new RegExp(`^${start}`), callback);
  }

  // Other helpers
+0 −42
Original line number Diff line number Diff line
// Copyright (c) 2022, The Tor Project, Inc.

import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs";

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
});

export const TorMonitorTopics = Object.freeze({
  BridgeChanged: TorProviderTopics.BridgeChanged,
  StreamSucceeded: TorProviderTopics.StreamSucceeded,
});

/**
 * This service monitors an existing Tor instance, or starts one, if needed, and
 * then starts monitoring it.
 *
 * This is the service which should be queried to know information about the
 * status of the bootstrap, the logs, etc...
 */
export const TorMonitorService = {
  get currentBridge() {
    return lazy.TorProtocolService.currentBridge;
  },

  get ownsTorDaemon() {
    return lazy.TorProtocolService.ownsTorDaemon;
  },

  get isRunning() {
    return lazy.TorProtocolService.isRunning;
  },

  get isBootstrapDone() {
    return lazy.TorProtocolService.isBootstrapDone;
  },

  getLog() {
    return lazy.TorProtocolService.getLog();
  },
};
+6 −13
Original line number Diff line number Diff line
// Copyright (c) 2021, The Tor Project, Inc.
/* 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/. */

import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
@@ -70,7 +72,7 @@ const ControlConnTimings = Object.freeze({
 * It can start a new tor instance, or connect to an existing one.
 * In the former case, it also takes its ownership by default.
 */
class TorProvider {
export class TorProvider {
  #inited = false;

  // Maintain a map of tor settings set by Tor Browser so that we don't
@@ -775,10 +777,7 @@ class TorProvider {
  _monitorEvent(type, callback) {
    logger.info(`Watching events of type ${type}.`);
    let replyObj = {};
    this._connection.watchEvent(
      type,
      null,
      line => {
    this._connection.watchEvent(type, line => {
      if (!line) {
        return;
      }
@@ -803,9 +802,7 @@ class TorProvider {
      } catch (e) {
        logger.error("Exception while handling an event", reply, e);
      }
      },
      true
    );
    });
  }

  _processLog(type, lines) {
@@ -1018,7 +1015,3 @@ class TorProvider {
    this.clearBootstrapError();
  }
}

// TODO: Stop defining TorProtocolService, make the builder instance the
// TorProvider.
export const TorProtocolService = new TorProvider();
+13 −4
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
  TorProvider: "resource://gre/modules/TorProvider.sys.mjs",
});

export const TorProviderTopics = Object.freeze({
@@ -19,16 +19,25 @@ export const TorProviderTopics = Object.freeze({
});

export class TorProviderBuilder {
  static #provider = null;

  static async init() {
    await lazy.TorProtocolService.init();
    const provider = new lazy.TorProvider();
    await provider.init();
    // Assign it only when initialization succeeds.
    TorProviderBuilder.#provider = provider;
  }

  static uninit() {
    lazy.TorProtocolService.uninit();
    TorProviderBuilder.#provider.uninit();
    TorProviderBuilder.#provider = null;
  }

  // TODO: Switch to an async build?
  static build() {
    return lazy.TorProtocolService;
    if (!TorProviderBuilder.#provider) {
      throw new Error("TorProviderBuilder has not been initialized yet.");
    }
    return TorProviderBuilder.#provider;
  }
}
+1 −2
Original line number Diff line number Diff line
@@ -3,10 +3,9 @@ EXTRA_JS_MODULES += [
    "TorControlPort.sys.mjs",
    "TorDomainIsolator.sys.mjs",
    "TorLauncherUtil.sys.mjs",
    "TorMonitorService.sys.mjs",
    "TorParsers.sys.mjs",
    "TorProcess.sys.mjs",
    "TorProtocolService.sys.mjs",
    "TorProvider.sys.mjs",
    "TorProviderBuilder.sys.mjs",
    "TorStartupService.sys.mjs",
]