tor-browser

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

Front.js (15010B)


      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 { settleAll } = require("resource://devtools/shared/DevToolsUtils.js");
      8 var EventEmitter = require("resource://devtools/shared/event-emitter.js");
      9 
     10 var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
     11 var {
     12  getStack,
     13  callFunctionWithAsyncStack,
     14 } = require("resource://devtools/shared/platform/stack.js");
     15 
     16 /**
     17 * Base class for client-side actor fronts.
     18 *
     19 * @param [DevToolsClient|null] conn
     20 *   The conn must either be DevToolsClient or null. Must have
     21 *   addActorPool, removeActorPool, and poolFor.
     22 *   conn can be null if the subclass provides a conn property.
     23 * @param [Target|null] target
     24 *   If we are instantiating a target-scoped front, this is a reference to the front's
     25 *   Target instance, otherwise this is null.
     26 * @param [Front|null] parentFront
     27 *   The parent front. This is only available if the Front being initialized is a child
     28 *   of a parent front.
     29 * @class
     30 */
     31 class Front extends Pool {
     32  constructor(conn = null, targetFront = null, parentFront = null) {
     33    super(conn);
     34    if (!conn) {
     35      throw new Error("Front without conn");
     36    }
     37    this.actorID = null;
     38    // The targetFront attribute represents the debuggable context. Only target-scoped
     39    // fronts and their children fronts will have the targetFront attribute set.
     40    this.targetFront = targetFront;
     41    // The parentFront attribute points to its parent front. Only children of
     42    // target-scoped fronts will have the parentFront attribute set.
     43    this.parentFront = parentFront;
     44    this._requests = [];
     45 
     46    // Front listener functions registered via `watchFronts`
     47    this._frontCreationListeners = null;
     48    this._frontDestructionListeners = null;
     49 
     50    // List of optional listener for each event, that is processed immediatly on packet
     51    // receival, before emitting event via EventEmitter on the Front.
     52    // These listeners are register via Front.before function.
     53    // Map(Event Name[string] => Event Listener[function])
     54    this._beforeListeners = new Map();
     55 
     56    // This flag allows to check if the `initialize` method has resolved.
     57    // Used to avoid notifying about initialized fronts in `watchFronts`.
     58    this._initializeResolved = false;
     59  }
     60 
     61  /**
     62   * Return the parent front.
     63   */
     64  getParent() {
     65    return this.parentFront && this.parentFront.actorID
     66      ? this.parentFront
     67      : null;
     68  }
     69 
     70  destroy() {
     71    // Prevent destroying twice if a `forwardCancelling` event has already been received
     72    // and already called `baseFrontClassDestroy`
     73    this.baseFrontClassDestroy();
     74 
     75    // Keep `clearEvents` out of baseFrontClassDestroy as we still want TargetMixin to be
     76    // able to emit `target-destroyed` after we called baseFrontClassDestroy from DevToolsClient.purgeRequests.
     77    this.clearEvents();
     78  }
     79 
     80  // This method is also called from `DevToolsClient`, when a connector is destroyed
     81  // and we should:
     82  // - reject all pending request made to the remote process/target/thread.
     83  // - avoid trying to do new request against this remote context.
     84  // - unmanage this front, so that DevToolsClient.getFront no longer returns it.
     85  //
     86  // When a connector is destroyed a `forwardCancelling` RDP event is sent by the server.
     87  // This is done in a distinct method from `destroy` in order to do all that immediately,
     88  // even if `Front.destroy` is overloaded by an async method.
     89  baseFrontClassDestroy() {
     90    // Reject all outstanding requests, they won't make sense after
     91    // the front is destroyed.
     92    while (this._requests.length) {
     93      const { deferred, to, type, stack } = this._requests.shift();
     94      // Note: many tests are ignoring `Connection closed` promise rejections,
     95      // via PromiseTestUtils.allowMatchingRejectionsGlobally.
     96      // Do not update the message without updating the tests.
     97      const msg =
     98        "Connection closed, pending request to " +
     99        to +
    100        ", type " +
    101        type +
    102        " failed" +
    103        "\n\nRequest stack:\n" +
    104        stack.formattedStack;
    105      deferred.reject(new Error(msg));
    106    }
    107 
    108    if (this.actorID) {
    109      super.destroy();
    110      this.actorID = null;
    111    }
    112    this._isDestroyed = true;
    113 
    114    this.targetFront = null;
    115    this.parentFront = null;
    116    this._frontCreationListeners = null;
    117    this._frontDestructionListeners = null;
    118    this._beforeListeners = null;
    119  }
    120 
    121  async manage(front, form, ctx) {
    122    if (!front.actorID) {
    123      throw new Error(
    124        "Can't manage front without an actor ID.\n" +
    125          "Ensure server supports " +
    126          front.typeName +
    127          "."
    128      );
    129    }
    130 
    131    if (front.parentFront && front.parentFront !== this) {
    132      throw new Error(
    133        `${this.actorID} (${this.typeName}) can't manage ${front.actorID}
    134        (${front.typeName}) since it has a different parentFront ${
    135          front.parentFront
    136            ? front.parentFront.actorID + "(" + front.parentFront.typeName + ")"
    137            : "<no parentFront>"
    138        }`
    139      );
    140    }
    141 
    142    super.manage(front);
    143 
    144    if (typeof front.initialize == "function") {
    145      await front.initialize();
    146    }
    147    front._initializeResolved = true;
    148 
    149    // Ensure calling form() *before* notifying about this front being just created.
    150    // We exprect the front to be fully initialized, especially via its form attributes.
    151    // But do that *after* calling manage() so that the front is already registered
    152    // in Pools and can be fetched by its ID, in case a child actor, created in form()
    153    // tries to get a reference to its parent via the actor ID.
    154    if (form) {
    155      front.form(form, ctx);
    156    }
    157 
    158    // Call listeners registered via `watchFronts` method
    159    // (ignore if this front has been destroyed)
    160    if (this._frontCreationListeners) {
    161      this._frontCreationListeners.emit(front.typeName, front);
    162    }
    163  }
    164 
    165  async unmanage(front) {
    166    super.unmanage(front);
    167 
    168    // Call listeners registered via `watchFronts` method
    169    if (this._frontDestructionListeners) {
    170      this._frontDestructionListeners.emit(front.typeName, front);
    171    }
    172  }
    173 
    174  /**
    175   * Listen for the creation and/or destruction of fronts matching one of the provided types.
    176   *
    177   * @param {string} typeName
    178   *        Actor type to watch.
    179   * @param {Function} onAvailable (optional)
    180   *        Callback fired when a front has been just created or was already available.
    181   *        The function is called with one arguments, the front.
    182   * @param {Function} onDestroy (optional)
    183   *        Callback fired in case of front destruction.
    184   *        The function is called with the same argument than onAvailable.
    185   */
    186  watchFronts(typeName, onAvailable, onDestroy) {
    187    if (this.isDestroyed()) {
    188      // The front was already destroyed, bail out.
    189      console.error(
    190        `Tried to call watchFronts for the '${typeName}' type on an ` +
    191          `already destroyed front '${this.typeName}'.`
    192      );
    193      return;
    194    }
    195 
    196    if (onAvailable) {
    197      // First fire the callback on fronts with the correct type and which have
    198      // been initialized. If initialize() is still in progress, the front will
    199      // be emitted via _frontCreationListeners shortly after.
    200      for (const front of this.poolChildren()) {
    201        if (front.typeName == typeName && front._initializeResolved) {
    202          onAvailable(front);
    203        }
    204      }
    205 
    206      if (!this._frontCreationListeners) {
    207        this._frontCreationListeners = new EventEmitter();
    208      }
    209      // Then register the callback for fronts instantiated in the future
    210      this._frontCreationListeners.on(typeName, onAvailable);
    211    }
    212 
    213    if (onDestroy) {
    214      if (!this._frontDestructionListeners) {
    215        this._frontDestructionListeners = new EventEmitter();
    216      }
    217      this._frontDestructionListeners.on(typeName, onDestroy);
    218    }
    219  }
    220 
    221  /**
    222   * Stop listening for the creation and/or destruction of a given type of fronts.
    223   * See `watchFronts()` for documentation of the arguments.
    224   */
    225  unwatchFronts(typeName, onAvailable, onDestroy) {
    226    if (this.isDestroyed()) {
    227      // The front was already destroyed, bail out.
    228      console.error(
    229        `Tried to call unwatchFronts for the '${typeName}' type on an ` +
    230          `already destroyed front '${this.typeName}'.`
    231      );
    232      return;
    233    }
    234 
    235    if (onAvailable && this._frontCreationListeners) {
    236      this._frontCreationListeners.off(typeName, onAvailable);
    237    }
    238    if (onDestroy && this._frontDestructionListeners) {
    239      this._frontDestructionListeners.off(typeName, onDestroy);
    240    }
    241  }
    242 
    243  /**
    244   * Register an event listener that will be called immediately on packer receival.
    245   * The given callback is going to be called before emitting the event via EventEmitter
    246   * API on the Front. Event emitting will be delayed if the callback is async.
    247   * Only one such listener can be registered per type of event.
    248   *
    249   * @param String type
    250   *   Event emitted by the actor to intercept.
    251   * @param Function callback
    252   *   Function that will process the event.
    253   */
    254  before(type, callback) {
    255    if (this._beforeListeners.has(type)) {
    256      throw new Error(
    257        `Can't register multiple before listeners for "${type}".`
    258      );
    259    }
    260    this._beforeListeners.set(type, callback);
    261  }
    262 
    263  toString() {
    264    return "[Front for " + this.typeName + "/" + this.actorID + "]";
    265  }
    266 
    267  /**
    268   * Update the actor from its representation.
    269   * Subclasses should override this.
    270   */
    271  form() {}
    272 
    273  /**
    274   * Send a packet on the connection.
    275   *
    276   * @param {object} packet
    277   * @param {object} options
    278   * @param {boolean} options.bulk
    279   *        To be set to true, if the packet relates to bulk request.
    280   *        Bulk request allows to send raw bytes over the wire instead of
    281   *        having to create a JSON string packet.
    282   * @param {Function} options.clientBulkCallback
    283   *        For bulk request, function called with a StreamWriter as only argument.
    284   *        This is called when the StreamWriter is available in order to send
    285   *        bytes to the server.
    286   */
    287  send(packet, { bulk = false, clientBulkCallback = null } = {}) {
    288    // The connection might be closed during the promise resolution
    289    if (!this.conn?._transport) {
    290      return;
    291    }
    292 
    293    if (!bulk) {
    294      if (!packet.to) {
    295        packet.to = this.actorID;
    296      }
    297      this.conn._transport.send(packet);
    298    } else {
    299      if (!packet.actor) {
    300        packet.actor = this.actorID;
    301      }
    302      this.conn._transport.startBulkSend(packet).then(clientBulkCallback);
    303    }
    304  }
    305 
    306  /**
    307   * Send a two-way request on the connection.
    308   *
    309   * See `send()` jsdoc for parameters definition.
    310   */
    311  request(packet, { bulk = false, clientBulkCallback = null } = {}) {
    312    const deferred = Promise.withResolvers();
    313    // Save packet basics for debugging
    314    const { to, type } = packet;
    315    this._requests.push({
    316      deferred,
    317      to: to || this.actorID,
    318      type,
    319      packet,
    320      stack: getStack(),
    321      clientBulkCallback,
    322    });
    323    this.send(packet, { bulk, clientBulkCallback });
    324    return deferred.promise;
    325  }
    326 
    327  /**
    328   * Handler for incoming packets from the client's actor.
    329   */
    330  onPacket(packet) {
    331    if (this.isDestroyed()) {
    332      // If the Front was already destroyed, all the requests have been purged
    333      // and rejected with detailed error messages in baseFrontClassDestroy.
    334      return;
    335    }
    336 
    337    // Pick off event packets
    338    const type = packet.type || undefined;
    339    if (this._clientSpec.events && this._clientSpec.events.has(type)) {
    340      const event = this._clientSpec.events.get(packet.type);
    341      let args;
    342      try {
    343        args = event.request.read(packet, this);
    344      } catch (ex) {
    345        console.error("Error reading event: " + packet.type);
    346        console.exception(ex);
    347        throw ex;
    348      }
    349      // Check for "pre event" callback to be processed before emitting events on fronts
    350      // Use event.name instead of packet.type to use specific event name instead of RDP
    351      // packet's type.
    352      const beforeEvent = this._beforeListeners.get(event.name);
    353      if (beforeEvent) {
    354        const result = beforeEvent.apply(this, args);
    355        // Check to see if the beforeEvent returned a promise -- if so,
    356        // wait for their resolution before emitting. Otherwise, emit synchronously.
    357        if (result && typeof result.then == "function") {
    358          result.then(() => {
    359            super.emit(event.name, ...args);
    360            ChromeUtils.addProfilerMarker(
    361              "DevTools:RDP Front",
    362              null,
    363              `${this.typeName}.${event.name}`
    364            );
    365          });
    366          return;
    367        }
    368      }
    369 
    370      super.emit(event.name, ...args);
    371      ChromeUtils.addProfilerMarker(
    372        "DevTools:RDP Front",
    373        null,
    374        `${this.typeName}.${event.name}`
    375      );
    376      return;
    377    }
    378 
    379    // Remaining packets must be responses.
    380    if (this._requests.length === 0) {
    381      const msg =
    382        "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
    383      const err = Error(msg);
    384      console.error(err);
    385      throw err;
    386    }
    387 
    388    const { deferred, packet: clientPacket, stack } = this._requests.shift();
    389    callFunctionWithAsyncStack(
    390      () => {
    391        if (packet.error) {
    392          let message;
    393          if (packet.error && packet.message) {
    394            message =
    395              "Protocol error (" + packet.error + "): " + packet.message;
    396          } else {
    397            message = packet.error;
    398          }
    399          message += " from: " + this.actorID;
    400          if (packet.fileName) {
    401            const { fileName, columnNumber, lineNumber } = packet;
    402            message += ` (${fileName}:${lineNumber}:${columnNumber})`;
    403          }
    404          const packetError = new Error(message);
    405 
    406          // Pass the packets on the exception object to convey them to AppErrorBoundary
    407          packetError.serverPacket = packet;
    408          packetError.clientPacket = clientPacket;
    409 
    410          deferred.reject(packetError);
    411        } else {
    412          deferred.resolve(packet);
    413        }
    414      },
    415      stack,
    416      "DevTools RDP"
    417    );
    418  }
    419 
    420  /**
    421   * DevToolsClient will notify Fronts about bulk packet via this method.
    422   */
    423  onBulkPacket(packet) {
    424    // We can actually consider the bulk packet as a regular packet.
    425    this.onPacket(packet);
    426  }
    427 
    428  hasRequests() {
    429    return !!this._requests.length;
    430  }
    431 
    432  /**
    433   * Wait for all current requests from this front to settle.  This is especially useful
    434   * for tests and other utility environments that may not have events or mechanisms to
    435   * await the completion of requests without this utility.
    436   *
    437   * @return Promise
    438   *         Resolved when all requests have settled.
    439   */
    440  waitForRequestsToSettle() {
    441    return settleAll(this._requests.map(({ deferred }) => deferred.promise));
    442  }
    443 }
    444 
    445 exports.Front = Front;