tor-browser

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

devtools-server-connection.js (17634B)


      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 var { Pool } = require("resource://devtools/shared/protocol.js");
      8 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
      9 var { dumpn } = DevToolsUtils;
     10 
     11 loader.lazyRequireGetter(
     12  this,
     13  "EventEmitter",
     14  "resource://devtools/shared/event-emitter.js"
     15 );
     16 loader.lazyRequireGetter(
     17  this,
     18  "DevToolsServer",
     19  "resource://devtools/server/devtools-server.js",
     20  true
     21 );
     22 
     23 /**
     24 * Creates a DevToolsServerConnection.
     25 *
     26 * Represents a connection to this debugging global from a client.
     27 * Manages a set of actors and actor pools, allocates actor ids, and
     28 * handles incoming requests.
     29 */
     30 class DevToolsServerConnection {
     31  /**
     32   * @param {string} prefix
     33   *        All actor IDs created by this connection should be prefixed
     34   *        with prefix.
     35   * @param {Transport} transport
     36   *        Packet transport for the debugging protocol.
     37   * @param {SocketListener} socketListener
     38   *        SocketListener which accepted the transport.
     39   *        If this is null, the transport is not that was accepted by SocketListener.
     40   */
     41  constructor(prefix, transport, socketListener) {
     42    this._prefix = prefix;
     43    this._transport = transport;
     44    this._transport.hooks = this;
     45    this._nextID = 1;
     46    this._socketListener = socketListener;
     47 
     48    this._actorPool = new Pool(this, "server-connection");
     49    this._extraPools = [this._actorPool];
     50 
     51    // Responses to a given actor must be returned the the client
     52    // in the same order as the requests that they're replying to, but
     53    // Implementations might finish serving requests in a different
     54    // order.  To keep things in order we generate a promise for each
     55    // request, chained to the promise for the request before it.
     56    // This map stores the latest request promise in the chain, keyed
     57    // by an actor ID string.
     58    this._actorResponses = new Map();
     59 
     60    /*
     61     * We can forward packets to other servers, if the actors on that server
     62     * all use a distinct prefix on their names. This is a map from prefixes
     63     * to transports: it maps a prefix P to a transport T if T conveys
     64     * packets to the server whose actors' names all begin with P + "/".
     65     */
     66    this._forwardingPrefixes = new Map();
     67 
     68    EventEmitter.decorate(this);
     69  }
     70 
     71  _prefix = null;
     72  get prefix() {
     73    return this._prefix;
     74  }
     75 
     76  _transport = null;
     77  get transport() {
     78    return this._transport;
     79  }
     80 
     81  close(options) {
     82    if (this._transport) {
     83      this._transport.close(options);
     84    }
     85  }
     86 
     87  send(packet) {
     88    this.transport.send(packet);
     89  }
     90 
     91  /**
     92   * Used when sending a bulk reply from an actor.
     93   *
     94   * @see DebuggerTransport.prototype.startBulkSend
     95   */
     96  startBulkSend(header) {
     97    return this.transport.startBulkSend(header);
     98  }
     99 
    100  allocID(prefix) {
    101    return this.prefix + (prefix || "") + this._nextID++;
    102  }
    103 
    104  /**
    105   * Add a map of actor IDs to the connection.
    106   */
    107  addActorPool(actorPool) {
    108    this._extraPools.push(actorPool);
    109  }
    110 
    111  /**
    112   * Remove a previously-added pool of actors to the connection.
    113   *
    114   * @param {Pool} actorPool
    115   *        The Pool instance you want to remove.
    116   */
    117  removeActorPool(actorPool) {
    118    // When a connection is closed, it removes each of its actor pools. When an
    119    // actor pool is removed, it calls the destroy method on each of its
    120    // actors. Some actors, such as ThreadActor, manage their own actor pools.
    121    // When the destroy method is called on these actors, they manually
    122    // remove their actor pools. Consequently, this method is reentrant.
    123    //
    124    // In addition, some actors, such as ThreadActor, perform asynchronous work
    125    // (in the case of ThreadActor, because they need to resume), before they
    126    // remove each of their actor pools. Since we don't wait for this work to
    127    // be completed, we can end up in this function recursively after the
    128    // connection already set this._extraPools to null.
    129    //
    130    // This is a bug: if the destroy method can perform asynchronous work,
    131    // then we should wait for that work to be completed before setting this.
    132    // _extraPools to null. As a temporary solution, it should be acceptable
    133    // to just return early (if this._extraPools has been set to null, all
    134    // actors pools for this connection should already have been removed).
    135    if (this._extraPools === null) {
    136      return;
    137    }
    138    const index = this._extraPools.lastIndexOf(actorPool);
    139    if (index > -1) {
    140      this._extraPools.splice(index, 1);
    141    }
    142  }
    143 
    144  /**
    145   * Add an actor to the default actor pool for this connection.
    146   */
    147  addActor(actor) {
    148    this._actorPool.manage(actor);
    149  }
    150 
    151  /**
    152   * Remove an actor to the default actor pool for this connection.
    153   */
    154  removeActor(actor) {
    155    this._actorPool.unmanage(actor);
    156  }
    157 
    158  /**
    159   * Match the api expected by the protocol library.
    160   */
    161  unmanage(actor) {
    162    return this.removeActor(actor);
    163  }
    164 
    165  /**
    166   * Look up an actor implementation for an actorID.  Will search
    167   * all the actor pools registered with the connection.
    168   *
    169   * @param actorID string
    170   *        Actor ID to look up.
    171   */
    172  getActor(actorID) {
    173    const pool = this.poolFor(actorID);
    174    if (pool) {
    175      return pool.getActorByID(actorID);
    176    }
    177 
    178    if (actorID === "root") {
    179      return this.rootActor;
    180    }
    181 
    182    return null;
    183  }
    184 
    185  _getOrCreateActor(actorID) {
    186    try {
    187      const actor = this.getActor(actorID);
    188      if (!actor) {
    189        this.transport.send({
    190          from: actorID ? actorID : "root",
    191          error: "noSuchActor",
    192          message: "No such actor for ID: " + actorID,
    193        });
    194        return null;
    195      }
    196 
    197      if (typeof actor !== "object") {
    198        // Pools should now contain only actor instances (i.e. objects)
    199        throw new Error(
    200          `Unexpected actor constructor/function in Pool for actorID "${actorID}".`
    201        );
    202      }
    203 
    204      return actor;
    205    } catch (error) {
    206      const prefix = `Error occurred while creating actor' ${actorID}`;
    207      this.transport.send(this._unknownError(actorID, prefix, error));
    208    }
    209    return null;
    210  }
    211 
    212  poolFor(actorID) {
    213    for (const pool of this._extraPools) {
    214      if (pool.has(actorID)) {
    215        return pool;
    216      }
    217    }
    218    return null;
    219  }
    220 
    221  _unknownError(from, prefix, error) {
    222    const errorString = prefix + ": " + DevToolsUtils.safeErrorString(error);
    223    // On worker threads we don't have access to Cu.
    224    if (!isWorker) {
    225      console.error(errorString);
    226    }
    227    dumpn(errorString);
    228    return {
    229      from,
    230      error: "unknownError",
    231      message: errorString,
    232    };
    233  }
    234 
    235  _queueResponse(from, type, responseOrPromise) {
    236    const pendingResponse =
    237      this._actorResponses.get(from) || Promise.resolve(null);
    238    const responsePromise = pendingResponse
    239      .then(() => {
    240        return responseOrPromise;
    241      })
    242      .then(response => {
    243        if (!this.transport) {
    244          throw new Error(
    245            `Connection closed, pending response from '${from}', ` +
    246              `type '${type}' failed`
    247          );
    248        }
    249 
    250        if (!response.from) {
    251          response.from = from;
    252        }
    253 
    254        this.transport.send(response);
    255      })
    256      .catch(error => {
    257        if (!this.transport) {
    258          throw new Error(
    259            `Connection closed, pending error from '${from}', ` +
    260              `type '${type}' failed`
    261          );
    262        }
    263 
    264        const prefix = `error occurred while queuing response for '${type}'`;
    265        this.transport.send(this._unknownError(from, prefix, error));
    266      });
    267 
    268    this._actorResponses.set(from, responsePromise);
    269  }
    270 
    271  /**
    272   * This function returns whether the connection was accepted by passed SocketListener.
    273   *
    274   * @param {SocketListener} socketListener
    275   * @return {boolean} return true if this connection was accepted by socketListener,
    276   *         else returns false.
    277   */
    278  isAcceptedBy(socketListener) {
    279    return this._socketListener === socketListener;
    280  }
    281 
    282  /* Forwarding packets to other transports based on actor name prefixes. */
    283 
    284  /**
    285   * Arrange to forward packets to another server. This is how we
    286   * forward debugging connections to child processes.
    287   *
    288   * If we receive a packet for an actor whose name begins with |prefix|
    289   * followed by '/', then we will forward that packet to |transport|.
    290   *
    291   * This overrides any prior forwarding for |prefix|.
    292   *
    293   * @param {string} prefix
    294   *    The actor name prefix, not including the '/'.
    295   * @param {object} transport
    296   *    A packet transport to which we should forward packets to actors
    297   *    whose names begin with |(prefix + '/').|
    298   */
    299  setForwarding(prefix, transport) {
    300    this._forwardingPrefixes.set(prefix, transport);
    301  }
    302 
    303  /*
    304   * Stop forwarding messages to actors whose names begin with
    305   * |prefix+'/'|. Such messages will now elicit 'noSuchActor' errors.
    306   */
    307  cancelForwarding(prefix) {
    308    this._forwardingPrefixes.delete(prefix);
    309 
    310    // Notify the client that forwarding in now cancelled for this prefix.
    311    // There could be requests in progress that the client should abort rather leaving
    312    // handing indefinitely.
    313    if (this.rootActor) {
    314      this.send(this.rootActor.forwardingCancelled(prefix));
    315    }
    316  }
    317 
    318  sendActorEvent(actorID, eventName, event = {}) {
    319    event.from = actorID;
    320    event.type = eventName;
    321    this.send(event);
    322  }
    323 
    324  // Transport hooks.
    325 
    326  /**
    327   * Called by DebuggerTransport to dispatch incoming packets as appropriate.
    328   *
    329   * @param {object} packet
    330   *        The incoming packet.
    331   */
    332  onPacket(packet) {
    333    // If the actor's name begins with a prefix we've been asked to
    334    // forward, do so.
    335    //
    336    // Note that the presence of a prefix alone doesn't indicate that
    337    // forwarding is needed: in DevToolsServerConnection instances in child
    338    // processes, every actor has a prefixed name.
    339    if (this._forwardingPrefixes.size > 0) {
    340      let to = packet.to;
    341      let separator = to.lastIndexOf("/");
    342      while (separator >= 0) {
    343        to = to.substring(0, separator);
    344        const forwardTo = this._forwardingPrefixes.get(
    345          packet.to.substring(0, separator)
    346        );
    347        if (forwardTo) {
    348          forwardTo.send(packet);
    349          return;
    350        }
    351        separator = to.lastIndexOf("/");
    352      }
    353    }
    354 
    355    const actor = this._getOrCreateActor(packet.to);
    356    if (!actor) {
    357      return;
    358    }
    359 
    360    let ret = null;
    361 
    362    // handle "requestTypes" RDP request.
    363    if (packet.type == "requestTypes") {
    364      ret = {
    365        from: actor.actorID,
    366        requestTypes: Object.keys(actor.requestTypes),
    367      };
    368    } else if (actor.requestTypes?.[packet.type]) {
    369      // Dispatch the request to the actor.
    370      try {
    371        this.currentPacket = packet;
    372        ret = actor.requestTypes[packet.type].bind(actor)(packet, this);
    373      } catch (error) {
    374        // Support legacy errors from old actors such as thread actor which
    375        // throw { error, message } objects.
    376        let errorMessage = error;
    377        if (error?.error && error?.message) {
    378          errorMessage = `"(${error.error}) ${error.message}"`;
    379        }
    380 
    381        const prefix = `error occurred while processing '${packet.type}'`;
    382        this.transport.send(
    383          this._unknownError(actor.actorID, prefix, errorMessage)
    384        );
    385      } finally {
    386        this.currentPacket = undefined;
    387      }
    388    } else {
    389      ret = {
    390        error: "unrecognizedPacketType",
    391        message: `Actor ${actor.actorID} does not recognize the packet type '${packet.type}'`,
    392      };
    393    }
    394 
    395    // There will not be a return value if a bulk reply is sent.
    396    if (ret) {
    397      this._queueResponse(packet.to, packet.type, ret);
    398    }
    399  }
    400 
    401  /**
    402   * Called by the DebuggerTransport to dispatch incoming bulk packets as
    403   * appropriate.
    404   *
    405   * @param packet object
    406   *        The incoming packet, which contains:
    407   *        * actor:        Name of actor that will receive the packet
    408   *        * type:         Name of actor's method that should be called on receipt
    409   *        * length:       Size of the data to be read
    410   *        * stream:       This input stream should only be used directly if you can
    411   *                        ensure that you will read exactly |length| bytes and will
    412   *                        not close the stream when reading is complete
    413   *        * done:         If you use the stream directly (instead of |copyTo|
    414   *                        or |copyToBuffer| below), you must signal completion
    415   *                        by resolving / rejecting this deferred.  If it's
    416   *                        rejected, the transport will be closed.  If an Error
    417   *                        is supplied as a rejection value, it will be logged
    418   *                        via |dumpn|.  If you do use |copyTo| or
    419   *                        |copyToBuffer|, resolving is taken care of for you
    420   *                        when copying completes.
    421   *        * copyTo:       A helper function for getting your data out of the stream
    422   *                        that meets the stream handling requirements above, and has
    423   *                        the following signature:
    424   *          @param  output nsIAsyncOutputStream
    425   *                        The stream to copy to.
    426   *          @return Promise
    427   *                        The promise is resolved when copying completes or rejected
    428   *                        if any (unexpected) errors occur.
    429   *                        This object also emits "progress" events for each chunk
    430   *                        that is copied.  See stream-utils.js.
    431   *        * copyToBuffer: a helper function for getting your data out of the stream
    432   *                        that meets the stream handling requirements above, and has
    433   *                        the following signature:
    434   *          @param output ArrayBuffer
    435   *                        The buffer to copy to. It needs to be the same length as the data
    436   *                        to be transfered.
    437   *          @return Promise
    438   *                        The promise is resolved when copying completes or rejected if any
    439   *                        (unexpected) error occurs.
    440   */
    441  onBulkPacket(packet) {
    442    const { actor: actorKey, type } = packet;
    443 
    444    const actor = this._getOrCreateActor(actorKey);
    445    if (!actor) {
    446      return;
    447    }
    448 
    449    // Dispatch the request to the actor.
    450    let ret;
    451    if (actor.requestTypes?.[type]) {
    452      try {
    453        // Protocol.js Actor.js expects the connection to be passed as second argument.
    454        ret = actor.requestTypes[type].call(actor, packet, this);
    455      } catch (error) {
    456        const prefix = `error occurred while processing bulk packet '${type}'`;
    457        this.transport.send(this._unknownError(actorKey, prefix, error));
    458        packet.done.reject(error);
    459      }
    460    } else {
    461      const message = `Actor ${actorKey} does not recognize the bulk packet type '${type}'`;
    462      ret = { error: "unrecognizedPacketType", message };
    463      packet.done.reject(new Error(message));
    464    }
    465 
    466    // If there is a JSON response, queue it for sending back to the client.
    467    if (ret) {
    468      this._queueResponse(actorKey, type, ret);
    469    }
    470  }
    471 
    472  /**
    473   * Called by DebuggerTransport when the underlying stream is closed.
    474   *
    475   * @param status nsresult
    476   *        The status code that corresponds to the reason for closing
    477   *        the stream.
    478   * @param {object} options
    479   * @param {boolean} options.isModeSwitching
    480   *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
    481   */
    482  onTransportClosed(status, options) {
    483    dumpn("Cleaning up connection.");
    484    if (!this._actorPool) {
    485      // Ignore this call if the connection is already closed.
    486      return;
    487    }
    488    this._actorPool = null;
    489 
    490    this.emit("closed", status, this.prefix);
    491 
    492    // Use filter in order to create a copy of the extraPools array,
    493    // which might be modified by removeActorPool calls.
    494    // The isTopLevel check ensures that the pools retrieved here will not be
    495    // destroyed by another Pool::destroy. Non top-level pools will be destroyed
    496    // by the recursive Pool::destroy mechanism.
    497    // See test_connection_closes_all_pools.js for practical examples of Pool
    498    // hierarchies.
    499    const topLevelPools = this._extraPools.filter(p => p.isTopPool());
    500    topLevelPools.forEach(p => p.destroy(options));
    501 
    502    this._extraPools = null;
    503 
    504    this.rootActor = null;
    505    this._transport = null;
    506    DevToolsServer._connectionClosed(this);
    507  }
    508 
    509  dumpPool(pool, output = [], dumpedPools) {
    510    const actorIds = [];
    511    const children = [];
    512 
    513    if (dumpedPools.has(pool)) {
    514      return;
    515    }
    516    dumpedPools.add(pool);
    517 
    518    // TRUE if the pool is a Pool
    519    if (!pool.__poolMap) {
    520      return;
    521    }
    522 
    523    for (const actor of pool.poolChildren()) {
    524      children.push(actor);
    525      actorIds.push(actor.actorID);
    526    }
    527    const label = pool.label || pool.actorID;
    528 
    529    output.push([label, actorIds]);
    530    dump(`- ${label}: ${JSON.stringify(actorIds)}\n`);
    531    children.forEach(childPool =>
    532      this.dumpPool(childPool, output, dumpedPools)
    533    );
    534  }
    535 
    536  /*
    537   * Debugging helper for inspecting the state of the actor pools.
    538   */
    539  dumpPools() {
    540    const output = [];
    541    const dumpedPools = new Set();
    542 
    543    this._extraPools.forEach(pool => this.dumpPool(pool, output, dumpedPools));
    544 
    545    return output;
    546  }
    547 }
    548 
    549 exports.DevToolsServerConnection = DevToolsServerConnection;