tor-browser

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

socket.js (19607B)


      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 // Ensure PSM is initialized to support TLS sockets
      8 Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
      9 
     10 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
     11 var { dumpn } = DevToolsUtils;
     12 loader.lazyRequireGetter(
     13  this,
     14  "WebSocketServer",
     15  "resource://devtools/server/socket/websocket-server.js"
     16 );
     17 loader.lazyRequireGetter(
     18  this,
     19  "DebuggerTransport",
     20  "resource://devtools/shared/transport/transport.js",
     21  true
     22 );
     23 loader.lazyRequireGetter(
     24  this,
     25  "WebSocketDebuggerTransport",
     26  "resource://devtools/shared/transport/websocket-transport.js"
     27 );
     28 loader.lazyRequireGetter(
     29  this,
     30  "discovery",
     31  "resource://devtools/shared/discovery/discovery.js"
     32 );
     33 loader.lazyRequireGetter(
     34  this,
     35  "Authenticators",
     36  "resource://devtools/shared/security/auth.js",
     37  true
     38 );
     39 loader.lazyRequireGetter(
     40  this,
     41  "AuthenticationResult",
     42  "resource://devtools/shared/security/auth.js",
     43  true
     44 );
     45 const lazy = {};
     46 
     47 DevToolsUtils.defineLazyGetter(
     48  lazy,
     49  "DevToolsSocketStatus",
     50  () =>
     51    ChromeUtils.importESModule(
     52      "resource://devtools/shared/security/DevToolsSocketStatus.sys.mjs",
     53      // DevToolsSocketStatus is also accessed by non-devtools modules and
     54      // should be loaded in the regular / shared global.
     55      { global: "shared" }
     56    ).DevToolsSocketStatus
     57 );
     58 
     59 loader.lazyRequireGetter(
     60  this,
     61  "EventEmitter",
     62  "resource://devtools/shared/event-emitter.js"
     63 );
     64 
     65 DevToolsUtils.defineLazyGetter(this, "nsFile", () => {
     66  return Components.Constructor(
     67    "@mozilla.org/file/local;1",
     68    "nsIFile",
     69    "initWithPath"
     70  );
     71 });
     72 
     73 DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => {
     74  return Cc["@mozilla.org/network/socket-transport-service;1"].getService(
     75    Ci.nsISocketTransportService
     76  );
     77 });
     78 
     79 class DebuggerSocket {
     80  /**
     81   * Connects to a devtools server socket.
     82   *
     83   * @param {object} settings
     84   * @param {string} settings.host
     85   *        The host name or IP address of the devtools server.
     86   * @param {number} settings.port
     87   *        The port number of the devtools server.
     88   * @param {boolean} [settings.webSocket]
     89   *        Whether to use WebSocket protocol to connect. Defaults to false.
     90   * @param {Authenticator} [settings.authenticator]
     91   *        |Authenticator| instance matching the mode in use by the server.
     92   *        Defaults to a PROMPT instance if not supplied.
     93   * @return {Promise}
     94   *         Resolved to a DebuggerTransport instance.
     95   */
     96  static async connect(settings) {
     97    // Default to PROMPT |Authenticator| instance if not supplied
     98    if (!settings.authenticator) {
     99      settings.authenticator = new (Authenticators.get().Client)();
    100    }
    101    _validateSettings(settings);
    102    // eslint-disable-next-line no-shadow
    103    const { host, port, authenticator } = settings;
    104    const transport = await _getTransport(settings);
    105    await authenticator.authenticate({
    106      host,
    107      port,
    108      transport,
    109    });
    110    transport.connectionSettings = settings;
    111    return transport;
    112  }
    113 }
    114 /**
    115 * Validate that the connection settings have been set to a supported configuration.
    116 */
    117 function _validateSettings(settings) {
    118  const { authenticator } = settings;
    119 
    120  authenticator.validateSettings(settings);
    121 }
    122 
    123 /**
    124 * Try very hard to create a DevTools transport, potentially making several
    125 * connect attempts in the process.
    126 *
    127 * @param host string
    128 *        The host name or IP address of the devtools server.
    129 * @param port number
    130 *        The port number of the devtools server.
    131 * @param webSocket boolean (optional)
    132 *        Whether to use WebSocket protocol to connect to the server. Defaults to false.
    133 * @param authenticator Authenticator
    134 *        |Authenticator| instance matching the mode in use by the server.
    135 *        Defaults to a PROMPT instance if not supplied.
    136 * @return transport DebuggerTransport
    137 *         A possible DevTools transport (if connection succeeded and streams
    138 *         are actually alive and working)
    139 */
    140 var _getTransport = async function (settings) {
    141  const { host, port, webSocket } = settings;
    142 
    143  if (webSocket) {
    144    // Establish a connection and wait until the WebSocket is ready to send and receive
    145    const socket = await new Promise((resolve, reject) => {
    146      const s = new WebSocket(`ws://${host}:${port}`);
    147      s.onopen = () => resolve(s);
    148      s.onerror = err => reject(err);
    149    });
    150 
    151    return new WebSocketDebuggerTransport(socket);
    152  }
    153 
    154  const attempt = await _attemptTransport(settings);
    155  if (attempt.transport) {
    156    // Success
    157    return attempt.transport;
    158  }
    159 
    160  throw new Error("Connection failed");
    161 };
    162 
    163 /**
    164 * Make a single attempt to connect and create a DevTools transport.
    165 *
    166 * @param host string
    167 *        The host name or IP address of the devtools server.
    168 * @param port number
    169 *        The port number of the devtools server.
    170 * @param authenticator Authenticator
    171 *        |Authenticator| instance matching the mode in use by the server.
    172 *        Defaults to a PROMPT instance if not supplied.
    173 * @return transport DebuggerTransport
    174 *         A possible DevTools transport (if connection succeeded and streams
    175 *         are actually alive and working)
    176 * @return s nsISocketTransport
    177 *         Underlying socket transport, in case more details are needed.
    178 */
    179 var _attemptTransport = async function (settings) {
    180  const { authenticator } = settings;
    181  // _attemptConnect only opens the streams.  Any failures at that stage
    182  // aborts the connection process immedidately.
    183  const { s, input, output } = await _attemptConnect(settings);
    184 
    185  // Check if the input stream is alive.
    186  let alive;
    187  try {
    188    const results = await _isInputAlive(input);
    189    alive = results.alive;
    190  } catch (e) {
    191    // For other unexpected errors, like NS_ERROR_CONNECTION_REFUSED, we reach
    192    // this block.
    193    input.close();
    194    output.close();
    195    throw e;
    196  }
    197 
    198  // The |Authenticator| examines the connection as well and may determine it
    199  // should be dropped.
    200  alive =
    201    alive &&
    202    authenticator.validateConnection({
    203      host: settings.host,
    204      port: settings.port,
    205      socket: s,
    206    });
    207 
    208  let transport;
    209  if (alive) {
    210    transport = new DebuggerTransport(input, output);
    211  } else {
    212    // Something went wrong, close the streams.
    213    input.close();
    214    output.close();
    215  }
    216 
    217  return { transport, s };
    218 };
    219 
    220 /**
    221 * Try to connect to a remote server socket.
    222 *
    223 * If successsful, the socket transport and its opened streams are returned.
    224 * Typically, this will only fail if the host / port is unreachable.  Other
    225 * problems, such as security errors, will allow this stage to succeed, but then
    226 * fail later when the streams are actually used.
    227 *
    228 * @return s nsISocketTransport
    229 *         Underlying socket transport, in case more details are needed.
    230 * @return input nsIAsyncInputStream
    231 *         The socket's input stream.
    232 * @return output nsIAsyncOutputStream
    233 *         The socket's output stream.
    234 */
    235 var _attemptConnect = async function ({ host, port }) {
    236  const s = socketTransportService.createTransport([], host, port, null, null);
    237 
    238  // Force disabling IPV6 if we aren't explicitely connecting to an IPv6 address
    239  // It fails intermitently on MacOS when opening the Browser Toolbox (bug 1615412)
    240  if (!host.includes(":")) {
    241    s.connectionFlags |= Ci.nsISocketTransport.DISABLE_IPV6;
    242  }
    243 
    244  // By default the CONNECT socket timeout is very long, 65535 seconds,
    245  // so that if we race to be in CONNECT state while the server socket is still
    246  // initializing, the connection is stuck in connecting state for 18.20 hours!
    247  s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
    248 
    249  let input;
    250  let output;
    251  return new Promise((resolve, reject) => {
    252    s.setEventSink(
    253      {
    254        onTransportStatus(transport, status) {
    255          if (status != Ci.nsISocketTransport.STATUS_CONNECTING_TO) {
    256            return;
    257          }
    258          try {
    259            input = s.openInputStream(0, 0, 0);
    260          } catch (e) {
    261            reject(e);
    262          }
    263          resolve({ s, input, output });
    264        },
    265      },
    266      Services.tm.currentThread
    267    );
    268 
    269    // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race
    270    // where the nsISocketTransport gets shutdown in between its instantiation and
    271    // the call to this method.
    272    try {
    273      output = s.openOutputStream(0, 0, 0);
    274    } catch (e) {
    275      reject(e);
    276    }
    277  }).catch(e => {
    278    if (input) {
    279      input.close();
    280    }
    281    if (output) {
    282      output.close();
    283    }
    284    DevToolsUtils.reportException("_attemptConnect", e);
    285  });
    286 };
    287 
    288 /**
    289 * Check if the input stream is alive.
    290 */
    291 function _isInputAlive(input) {
    292  return new Promise((resolve, reject) => {
    293    input.asyncWait(
    294      {
    295        onInputStreamReady(stream) {
    296          try {
    297            stream.available();
    298            resolve({ alive: true });
    299          } catch (e) {
    300            reject(e);
    301          }
    302        },
    303      },
    304      0,
    305      0,
    306      Services.tm.currentThread
    307    );
    308  });
    309 }
    310 
    311 /**
    312 * Creates a new socket listener for remote connections to the DevToolsServer.
    313 * This helps contain and organize the parts of the server that may differ or
    314 * are particular to one given listener mechanism vs. another.
    315 * This can be closed at any later time by calling |close|.
    316 * If remote connections are disabled, an error is thrown.
    317 */
    318 class SocketListener extends EventEmitter {
    319  /**
    320   * @param {DevToolsServer} devToolsServer
    321   * @param {object} socketOptions
    322   *        options of socket as follows
    323   *        {
    324   *          authenticator:
    325   *            Controls the |Authenticator| used, which hooks various socket steps to
    326   *            implement an authentication policy.  It is expected that different use
    327   *            cases may override pieces of the |Authenticator|.  See auth.js.
    328   *            We set the default |Authenticator|, which is |Prompt|.
    329   *          discoverable:
    330   *            Controls whether this listener is announced via the service discovery
    331   *            mechanism. Defaults is false.
    332   *          fromBrowserToolbox:
    333   *            Should only be passed when opening a socket for a Browser Toolbox
    334   *            session. DevToolsSocketStatus will track the socket separately to
    335   *            avoid triggering the visual cue in the URL bar.
    336   *          portOrPath:
    337   *            The port or path to listen on.
    338   *            If given an integer, the port to listen on.  Use -1 to choose any available
    339   *            port. Otherwise, the path to the unix socket domain file to listen on.
    340   *            Defaults is null.
    341   *          webSocket:
    342   *            Whether to use WebSocket protocol. Defaults is false.
    343   *        }
    344   */
    345  constructor(devToolsServer, socketOptions) {
    346    super();
    347 
    348    this._devToolsServer = devToolsServer;
    349 
    350    // Set socket options with default value
    351    this._socketOptions = {
    352      authenticator:
    353        socketOptions.authenticator || new (Authenticators.get().Server)(),
    354      discoverable: !!socketOptions.discoverable,
    355      fromBrowserToolbox: !!socketOptions.fromBrowserToolbox,
    356      portOrPath: socketOptions.portOrPath || null,
    357      webSocket: !!socketOptions.webSocket,
    358    };
    359  }
    360 
    361  get authenticator() {
    362    return this._socketOptions.authenticator;
    363  }
    364 
    365  get discoverable() {
    366    return this._socketOptions.discoverable;
    367  }
    368 
    369  get fromBrowserToolbox() {
    370    return this._socketOptions.fromBrowserToolbox;
    371  }
    372 
    373  get portOrPath() {
    374    return this._socketOptions.portOrPath;
    375  }
    376 
    377  get webSocket() {
    378    return this._socketOptions.webSocket;
    379  }
    380 
    381  /**
    382   * Validate that all options have been set to a supported configuration.
    383   */
    384  _validateOptions() {
    385    if (this.portOrPath === null) {
    386      throw new Error("Must set a port / path to listen on.");
    387    }
    388    if (this.discoverable && !Number(this.portOrPath)) {
    389      throw new Error("Discovery only supported for TCP sockets.");
    390    }
    391  }
    392 
    393  /**
    394   * Listens on the given port or socket file for remote debugger connections.
    395   */
    396  open() {
    397    this._validateOptions();
    398    this._devToolsServer.addSocketListener(this);
    399 
    400    let flags = Ci.nsIServerSocket.KeepWhenOffline;
    401    // A preference setting can force binding on the loopback interface.
    402    if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
    403      flags |= Ci.nsIServerSocket.LoopbackOnly;
    404    }
    405 
    406    const self = this;
    407    return (async function () {
    408      const backlog = 4;
    409      self._socket = self._createSocketInstance();
    410      if (self.isPortBased) {
    411        const port = Number(self.portOrPath);
    412        self._socket.initSpecialConnection(port, flags, backlog);
    413      } else if (self.portOrPath.startsWith("/")) {
    414        const file = nsFile(self.portOrPath);
    415        if (file.exists()) {
    416          file.remove(false);
    417        }
    418        self._socket.initWithFilename(file, parseInt("666", 8), backlog);
    419      } else {
    420        // Path isn't absolute path, so we use abstract socket address
    421        self._socket.initWithAbstractAddress(self.portOrPath, backlog);
    422      }
    423      self._socket.asyncListen(self);
    424      dumpn("Socket listening on: " + (self.port || self.portOrPath));
    425    })()
    426      .then(() => {
    427        lazy.DevToolsSocketStatus.notifySocketOpened({
    428          fromBrowserToolbox: self.fromBrowserToolbox,
    429        });
    430        this._advertise();
    431      })
    432      .catch(e => {
    433        dumpn(
    434          "Could not start debugging listener on '" +
    435            this.portOrPath +
    436            "': " +
    437            e
    438        );
    439        this.close();
    440      });
    441  }
    442 
    443  _advertise() {
    444    if (!this.discoverable || !this.port) {
    445      return;
    446    }
    447 
    448    const advertisement = {
    449      port: this.port,
    450    };
    451 
    452    this.authenticator.augmentAdvertisement(this, advertisement);
    453 
    454    discovery.addService("devtools", advertisement);
    455  }
    456 
    457  _createSocketInstance() {
    458    return Cc["@mozilla.org/network/server-socket;1"].createInstance(
    459      Ci.nsIServerSocket
    460    );
    461  }
    462 
    463  /**
    464   * Closes the SocketListener.  Notifies the server to remove the listener from
    465   * the set of active SocketListeners.
    466   */
    467  close() {
    468    if (this.discoverable && this.port) {
    469      discovery.removeService("devtools");
    470    }
    471    if (this._socket) {
    472      this._socket.close();
    473      this._socket = null;
    474 
    475      lazy.DevToolsSocketStatus.notifySocketClosed({
    476        fromBrowserToolbox: this.fromBrowserToolbox,
    477      });
    478    }
    479    this._devToolsServer.removeSocketListener(this);
    480  }
    481 
    482  get host() {
    483    if (!this._socket) {
    484      return null;
    485    }
    486    if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
    487      return "127.0.0.1";
    488    }
    489    return "0.0.0.0";
    490  }
    491 
    492  /**
    493   * Gets whether this listener uses a port number vs. a path.
    494   */
    495  get isPortBased() {
    496    return !!Number(this.portOrPath);
    497  }
    498 
    499  /**
    500   * Gets the port that a TCP socket listener is listening on, or null if this
    501   * is not a TCP socket (so there is no port).
    502   */
    503  get port() {
    504    if (!this.isPortBased || !this._socket) {
    505      return null;
    506    }
    507    return this._socket.port;
    508  }
    509 
    510  onAllowedConnection(transport) {
    511    dumpn("onAllowedConnection, transport: " + transport);
    512    this.emit("accepted", transport, this);
    513  }
    514 
    515  // nsIServerSocketListener implementation
    516 
    517  onSocketAccepted = DevToolsUtils.makeInfallible(function (
    518    socket,
    519    socketTransport
    520  ) {
    521    const connection = new ServerSocketConnection(this, socketTransport);
    522    connection.once("allowed", this.onAllowedConnection.bind(this));
    523  }, "SocketListener.onSocketAccepted");
    524 
    525  onStopListening(socket, status) {
    526    dumpn("onStopListening, status: " + status);
    527  }
    528 }
    529 
    530 /**
    531 * A |ServerSocketConnection| is created by a |SocketListener| for each accepted
    532 * incoming socket.
    533 */
    534 class ServerSocketConnection extends EventEmitter {
    535  constructor(listener, socketTransport) {
    536    super();
    537 
    538    this._listener = listener;
    539    this._socketTransport = socketTransport;
    540    this._handle();
    541  }
    542  get authentication() {
    543    return this._listener.authenticator.mode;
    544  }
    545 
    546  get host() {
    547    return this._socketTransport.host;
    548  }
    549 
    550  get port() {
    551    return this._socketTransport.port;
    552  }
    553 
    554  get address() {
    555    return this.host + ":" + this.port;
    556  }
    557 
    558  get client() {
    559    const client = {
    560      host: this.host,
    561      port: this.port,
    562    };
    563    return client;
    564  }
    565 
    566  get server() {
    567    const server = {
    568      host: this._listener.host,
    569      port: this._listener.port,
    570    };
    571    return server;
    572  }
    573 
    574  /**
    575   * This is the main authentication workflow.  If any pieces reject a promise,
    576   * the connection is denied.  If the entire process resolves successfully,
    577   * the connection is finally handed off to the |DevToolsServer|.
    578   */
    579  async _handle() {
    580    dumpn("Debugging connection starting authentication on " + this.address);
    581    try {
    582      await this._createTransport();
    583      await this._authenticate();
    584      this.allow();
    585    } catch (e) {
    586      this.deny(e);
    587    }
    588  }
    589 
    590  /**
    591   * We need to open the streams early on, as that is required in the case of
    592   * TLS sockets to keep the handshake moving.
    593   */
    594  async _createTransport() {
    595    const input = this._socketTransport.openInputStream(0, 0, 0);
    596    const output = this._socketTransport.openOutputStream(0, 0, 0);
    597 
    598    if (this._listener.webSocket) {
    599      const socket = await WebSocketServer.accept(
    600        this._socketTransport,
    601        input,
    602        output
    603      );
    604      this._transport = new WebSocketDebuggerTransport(socket);
    605    } else {
    606      this._transport = new DebuggerTransport(input, output);
    607    }
    608 
    609    // Start up the transport to observe the streams in case they are closed
    610    // early.  This allows us to clean up our state as well.
    611    this._transport.hooks = {
    612      onTransportClosed: reason => {
    613        this.deny(reason);
    614      },
    615    };
    616    this._transport.ready();
    617  }
    618 
    619  async _authenticate() {
    620    const result = await this._listener.authenticator.authenticate({
    621      client: this.client,
    622      server: this.server,
    623      transport: this._transport,
    624    });
    625 
    626    // If result is fine, we can stop here
    627    if (
    628      result === AuthenticationResult.ALLOW ||
    629      result === AuthenticationResult.ALLOW_PERSIST
    630    ) {
    631      return;
    632    }
    633 
    634    if (result === AuthenticationResult.DISABLE_ALL) {
    635      this._listener._devToolsServer.closeAllSocketListeners();
    636      Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
    637    }
    638 
    639    // If we got an error (DISABLE_ALL, DENY, …), let's throw a NS_ERROR_CONNECTION_REFUSED
    640    // exception
    641    throw Components.Exception("", Cr.NS_ERROR_CONNECTION_REFUSED);
    642  }
    643 
    644  deny(result) {
    645    if (this._destroyed) {
    646      return;
    647    }
    648    let errorName = result;
    649    for (const name in Cr) {
    650      if (Cr[name] === result) {
    651        errorName = name;
    652        break;
    653      }
    654    }
    655    dumpn(
    656      "Debugging connection denied on " + this.address + " (" + errorName + ")"
    657    );
    658    if (this._transport) {
    659      this._transport.hooks = null;
    660      this._transport.close(result);
    661    }
    662    this._socketTransport.close(result);
    663    this.destroy();
    664  }
    665 
    666  allow() {
    667    if (this._destroyed) {
    668      return;
    669    }
    670    dumpn("Debugging connection allowed on " + this.address);
    671    this.emit("allowed", this._transport);
    672    this.destroy();
    673  }
    674 
    675  destroy() {
    676    this._destroyed = true;
    677    this._listener = null;
    678    this._socketTransport = null;
    679    this._transport = null;
    680  }
    681 }
    682 
    683 exports.DebuggerSocket = DebuggerSocket;
    684 exports.SocketListener = SocketListener;