tor-browser

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

root.js (21015B)


      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 // protocol.js uses objects as exceptions in order to define
      8 // error packets.
      9 /* eslint-disable no-throw-literal */
     10 
     11 const { Actor, Pool } = require("resource://devtools/shared/protocol.js");
     12 const { rootSpec } = require("resource://devtools/shared/specs/root.js");
     13 
     14 const {
     15  LazyPool,
     16  createExtraActors,
     17 } = require("resource://devtools/shared/protocol/lazy-pool.js");
     18 const {
     19  DevToolsServer,
     20 } = require("resource://devtools/server/devtools-server.js");
     21 const Resources = require("resource://devtools/server/actors/resources/index.js");
     22 
     23 loader.lazyRequireGetter(
     24  this,
     25  "ProcessDescriptorActor",
     26  "resource://devtools/server/actors/descriptors/process.js",
     27  true
     28 );
     29 
     30 /* Root actor for the remote debugging protocol. */
     31 
     32 /**
     33 * Create a remote debugging protocol root actor.
     34 *
     35 * @param conn
     36 *     The DevToolsServerConnection whose root actor we are constructing.
     37 *
     38 * @param parameters
     39 *     The properties of |parameters| provide backing objects for the root
     40 *     actor's requests; if a given property is omitted from |parameters|, the
     41 *     root actor won't implement the corresponding requests or notifications.
     42 *     Supported properties:
     43 *
     44 *     - tabList: a live list (see below) of target actors for tabs. If present,
     45 *       the new root actor supports the 'listTabs' request, providing the live
     46 *       list's elements as its target actors, and sending 'tabListChanged'
     47 *       notifications when the live list's contents change. One actor in
     48 *       this list must have a true '.selected' property.
     49 *
     50 *     - addonList: a live list (see below) of addon actors. If present, the
     51 *       new root actor supports the 'listAddons' request, providing the live
     52 *       list's elements as its addon actors, and sending 'addonListchanged'
     53 *       notifications when the live list's contents change.
     54 *
     55 *     - globalActorFactories: an object |A| describing further actors to
     56 *       attach to the 'listTabs' reply. This is the type accumulated by
     57 *       ActorRegistry.addGlobalActor. For each own property |P| of |A|,
     58 *       the root actor adds a property named |P| to the 'listTabs'
     59 *       reply whose value is the name of an actor constructed by
     60 *       |A[P]|.
     61 *
     62 *     - onShutdown: a function to call when the root actor is destroyed.
     63 *
     64 * Instance properties:
     65 *
     66 * - applicationType: the string the root actor will include as the
     67 *      "applicationType" property in the greeting packet. By default, this
     68 *      is "browser".
     69 *
     70 * Live lists:
     71 *
     72 * A "live list", as used for the |tabList|, is an object that presents a
     73 * list of actors, and also notifies its clients of changes to the list. A
     74 * live list's interface is two properties:
     75 *
     76 * - getList: a method that returns a promise to the contents of the list.
     77 *
     78 * - onListChanged: a handler called, with no arguments, when the set of
     79 *             values the iterator would produce has changed since the last
     80 *             time 'iterator' was called. This may only be set to null or a
     81 *             callable value (one for which the typeof operator returns
     82 *             'function'). (Note that the live list will not call the
     83 *             onListChanged handler until the list has been iterated over
     84 *             once; if nobody's seen the list in the first place, nobody
     85 *             should care if its contents have changed!)
     86 *
     87 * When the list changes, the list implementation should ensure that any
     88 * actors yielded in previous iterations whose referents (tabs) still exist
     89 * get yielded again in subsequent iterations. If the underlying referent
     90 * is the same, the same actor should be presented for it.
     91 *
     92 * The root actor registers an 'onListChanged' handler on the appropriate
     93 * list when it may need to send the client 'tabListChanged' notifications,
     94 * and is careful to remove the handler whenever it does not need to send
     95 * such notifications (including when it is destroyed). This means that
     96 * live list implementations can use the state of the handler property (set
     97 * or null) to install and remove observers and event listeners.
     98 *
     99 * Note that, as the only way for the root actor to see the members of the
    100 * live list is to begin an iteration over the list, the live list need not
    101 * actually produce any actors until they are reached in the course of
    102 * iteration: alliterative lazy live lists.
    103 */
    104 class RootActor extends Actor {
    105  constructor(conn, parameters) {
    106    super(conn, rootSpec);
    107 
    108    this._parameters = parameters;
    109    this._onTabListChanged = this.onTabListChanged.bind(this);
    110    this._onAddonListChanged = this.onAddonListChanged.bind(this);
    111    this._onWorkerListChanged = this.onWorkerListChanged.bind(this);
    112    this._onServiceWorkerRegistrationListChanged =
    113      this.onServiceWorkerRegistrationListChanged.bind(this);
    114    this._onProcessListChanged = this.onProcessListChanged.bind(this);
    115 
    116    this._extraActors = {};
    117 
    118    this._globalActorPool = new LazyPool(this.conn);
    119 
    120    this.applicationType = "browser";
    121 
    122    // Compute the list of all supported Root Resources
    123    const supportedResources = {};
    124    for (const resourceType in Resources.RootResources) {
    125      supportedResources[resourceType] = true;
    126    }
    127 
    128    this.traits = {
    129      networkMonitor: true,
    130      resources: supportedResources,
    131      // @backward-compat { version 84 } Expose the pref value to the client.
    132      // Services.prefs is undefined in xpcshell tests.
    133      workerConsoleApiMessagesDispatchedToMainThread: Services.prefs
    134        ? Services.prefs.getBoolPref(
    135            "dom.worker.console.dispatch_events_to_main_thread"
    136          )
    137        : true,
    138      // @backward-compat { version 151 } Process Descriptor's `getWatcher()`
    139      // supports a new 'enableWindowGlobalThreadActors' flag to enable
    140      // the WindowGlobal's thread actors when debugging the whole browser.
    141      // This was actually changed in 137, but we support it for VSCode until
    142      // ESR 140 is the only ESR available.
    143      //
    144      // ESR 115 EOL is currently planned for March 24 2026. Do not remove
    145      // this trait before that date AND make sure the extension has been
    146      // updated (https://github.com/firefox-devtools/vscode-firefox-debug/issues/391).
    147      // Contact Holger Benl (hbenl) for topics related to the extension.
    148      supportsEnableWindowGlobalThreadActors: true,
    149    };
    150  }
    151 
    152  /**
    153   * Return a 'hello' packet as specified by the Remote Debugging Protocol.
    154   */
    155  sayHello() {
    156    return {
    157      from: this.actorID,
    158      applicationType: this.applicationType,
    159      /* This is not in the spec, but it's used by tests. */
    160      testConnectionPrefix: this.conn.prefix,
    161      traits: this.traits,
    162    };
    163  }
    164 
    165  forwardingCancelled(prefix) {
    166    return {
    167      from: this.actorID,
    168      type: "forwardingCancelled",
    169      prefix,
    170    };
    171  }
    172 
    173  /**
    174   * Destroys the actor from the browser window.
    175   */
    176  destroy() {
    177    Resources.unwatchAllResources(this);
    178 
    179    super.destroy();
    180 
    181    /* Tell the live lists we aren't watching any more. */
    182    if (this._parameters.tabList) {
    183      this._parameters.tabList.destroy();
    184    }
    185    if (this._parameters.addonList) {
    186      this._parameters.addonList.onListChanged = null;
    187    }
    188    if (this._parameters.workerList) {
    189      this._parameters.workerList.destroy();
    190    }
    191    if (this._parameters.serviceWorkerRegistrationList) {
    192      this._parameters.serviceWorkerRegistrationList.onListChanged = null;
    193    }
    194    if (this._parameters.processList) {
    195      this._parameters.processList.onListChanged = null;
    196    }
    197    if (typeof this._parameters.onShutdown === "function") {
    198      this._parameters.onShutdown();
    199    }
    200    // Cleanup Actors on destroy
    201    if (this._tabDescriptorActorPool) {
    202      this._tabDescriptorActorPool.destroy();
    203    }
    204    if (this._processDescriptorActorPool) {
    205      this._processDescriptorActorPool.destroy();
    206    }
    207    if (this._globalActorPool) {
    208      this._globalActorPool.destroy();
    209    }
    210    if (this._addonTargetActorPool) {
    211      this._addonTargetActorPool.destroy();
    212    }
    213    if (this._workerDescriptorActorPool) {
    214      this._workerDescriptorActorPool.destroy();
    215    }
    216    if (this._frameDescriptorActorPool) {
    217      this._frameDescriptorActorPool.destroy();
    218    }
    219 
    220    if (this._serviceWorkerRegistrationActorPool) {
    221      this._serviceWorkerRegistrationActorPool.destroy();
    222    }
    223    this._extraActors = null;
    224    this._tabDescriptorActorPool = null;
    225    this._globalActorPool = null;
    226    this._parameters = null;
    227  }
    228 
    229  /**
    230   * Method called by the client right after the root actor is communicated to it,
    231   * with information about the frontend.
    232   *
    233   * For now this is used by Servo which implements different backend APIs,
    234   * based on the frontend version. (backward compat to support many frontend versions
    235   * on the same backend revision)
    236   */
    237  // eslint-disable-next-line no-unused-vars
    238  connect({ frontendVersion }) {}
    239 
    240  /**
    241   * Gets the "root" form, which lists all the global actors that affect the entire
    242   * browser.
    243   */
    244  getRoot() {
    245    // Create global actors
    246    if (!this._globalActorPool) {
    247      this._globalActorPool = new LazyPool(this.conn);
    248    }
    249    const actors = createExtraActors(
    250      this._parameters.globalActorFactories,
    251      this._globalActorPool,
    252      this
    253    );
    254 
    255    return actors;
    256  }
    257 
    258  /* The 'listTabs' request and the 'tabListChanged' notification. */
    259 
    260  /**
    261   * Handles the listTabs request. The actors will survive until at least
    262   * the next listTabs request.
    263   */
    264  async listTabs() {
    265    const tabList = this._parameters.tabList;
    266    if (!tabList) {
    267      throw {
    268        error: "noTabs",
    269        message: "This root actor has no browser tabs.",
    270      };
    271    }
    272 
    273    // Now that a client has requested the list of tabs, we reattach the onListChanged
    274    // listener in order to be notified if the list of tabs changes again in the future.
    275    tabList.onListChanged = this._onTabListChanged;
    276 
    277    // Walk the tab list, accumulating the array of target actors for the reply, and
    278    // moving all the actors to a new Pool. We'll replace the old tab target actor
    279    // pool with the one we build here, thus retiring any actors that didn't get listed
    280    // again, and preparing any new actors to receive packets.
    281    const newActorPool = new Pool(this.conn, "listTabs-tab-descriptors");
    282 
    283    const tabDescriptorActors = await tabList.getList();
    284    for (const tabDescriptorActor of tabDescriptorActors) {
    285      newActorPool.manage(tabDescriptorActor);
    286    }
    287 
    288    // Drop the old actorID -> actor map. Actors that still mattered were added to the
    289    // new map; others will go away.
    290    if (this._tabDescriptorActorPool) {
    291      this._tabDescriptorActorPool.destroy();
    292    }
    293    this._tabDescriptorActorPool = newActorPool;
    294 
    295    return tabDescriptorActors;
    296  }
    297 
    298  /**
    299   * Return the tab descriptor actor for the tab identified by one of the IDs
    300   * passed as argument.
    301   *
    302   * See BrowserTabList.prototype.getTab for the definition of these IDs.
    303   */
    304  async getTab({ browserId }) {
    305    const tabList = this._parameters.tabList;
    306    if (!tabList) {
    307      throw {
    308        error: "noTabs",
    309        message: "This root actor has no browser tabs.",
    310      };
    311    }
    312    if (!this._tabDescriptorActorPool) {
    313      this._tabDescriptorActorPool = new Pool(
    314        this.conn,
    315        "getTab-tab-descriptors"
    316      );
    317    }
    318 
    319    let descriptorActor;
    320    try {
    321      descriptorActor = await tabList.getTab({
    322        browserId,
    323      });
    324    } catch (error) {
    325      if (error.error) {
    326        // Pipe expected errors as-is to the client
    327        throw error;
    328      }
    329      throw {
    330        error: "noTab",
    331        message: "Unexpected error while calling getTab(): " + error,
    332      };
    333    }
    334 
    335    descriptorActor.parentID = this.actorID;
    336    this._tabDescriptorActorPool.manage(descriptorActor);
    337 
    338    return descriptorActor;
    339  }
    340 
    341  onTabListChanged() {
    342    this.conn.send({ from: this.actorID, type: "tabListChanged" });
    343    /* It's a one-shot notification; no need to watch any more. */
    344    this._parameters.tabList.onListChanged = null;
    345  }
    346 
    347  /**
    348   * This function can receive the following option from devtools client.
    349   *
    350   * @param {object} option
    351   *        - iconDataURL: {boolean}
    352   *            When true, make data url from the icon of addon, then make possible to
    353   *            access by iconDataURL in the actor. The iconDataURL is useful when
    354   *            retrieving addons from a remote device, because the raw iconURL might not
    355   *            be accessible on the client.
    356   */
    357  async listAddons(option) {
    358    const addonList = this._parameters.addonList;
    359    if (!addonList) {
    360      throw {
    361        error: "noAddons",
    362        message: "This root actor has no browser addons.",
    363      };
    364    }
    365 
    366    // Reattach the onListChanged listener now that a client requested the list.
    367    addonList.onListChanged = this._onAddonListChanged;
    368 
    369    const addonTargetActors = await addonList.getList();
    370    const addonTargetActorPool = new Pool(this.conn, "addon-descriptors");
    371    for (const addonTargetActor of addonTargetActors) {
    372      if (option.iconDataURL) {
    373        await addonTargetActor.loadIconDataURL();
    374      }
    375 
    376      addonTargetActorPool.manage(addonTargetActor);
    377    }
    378 
    379    if (this._addonTargetActorPool) {
    380      this._addonTargetActorPool.destroy();
    381    }
    382    this._addonTargetActorPool = addonTargetActorPool;
    383 
    384    return addonTargetActors;
    385  }
    386 
    387  onAddonListChanged() {
    388    this.conn.send({ from: this.actorID, type: "addonListChanged" });
    389    this._parameters.addonList.onListChanged = null;
    390  }
    391 
    392  listWorkers() {
    393    const workerList = this._parameters.workerList;
    394    if (!workerList) {
    395      throw {
    396        error: "noWorkers",
    397        message: "This root actor has no workers.",
    398      };
    399    }
    400 
    401    // Reattach the onListChanged listener now that a client requested the list.
    402    workerList.onListChanged = this._onWorkerListChanged;
    403 
    404    return workerList.getList().then(actors => {
    405      const pool = new Pool(this.conn, "worker-targets");
    406      for (const actor of actors) {
    407        pool.manage(actor);
    408      }
    409 
    410      // Do not destroy the pool before transfering ownership to the newly created
    411      // pool, so that we do not accidently destroy actors that are still in use.
    412      if (this._workerDescriptorActorPool) {
    413        this._workerDescriptorActorPool.destroy();
    414      }
    415 
    416      this._workerDescriptorActorPool = pool;
    417 
    418      return {
    419        workers: actors,
    420      };
    421    });
    422  }
    423 
    424  onWorkerListChanged() {
    425    this.conn.send({ from: this.actorID, type: "workerListChanged" });
    426    this._parameters.workerList.onListChanged = null;
    427  }
    428 
    429  listServiceWorkerRegistrations() {
    430    const registrationList = this._parameters.serviceWorkerRegistrationList;
    431    if (!registrationList) {
    432      throw {
    433        error: "noServiceWorkerRegistrations",
    434        message: "This root actor has no service worker registrations.",
    435      };
    436    }
    437 
    438    // Reattach the onListChanged listener now that a client requested the list.
    439    registrationList.onListChanged =
    440      this._onServiceWorkerRegistrationListChanged;
    441 
    442    return registrationList.getList().then(actors => {
    443      const pool = new Pool(this.conn, "service-workers-registrations");
    444      for (const actor of actors) {
    445        pool.manage(actor);
    446      }
    447 
    448      if (this._serviceWorkerRegistrationActorPool) {
    449        this._serviceWorkerRegistrationActorPool.destroy();
    450      }
    451      this._serviceWorkerRegistrationActorPool = pool;
    452 
    453      return {
    454        registrations: actors,
    455      };
    456    });
    457  }
    458 
    459  onServiceWorkerRegistrationListChanged() {
    460    this.conn.send({
    461      from: this.actorID,
    462      type: "serviceWorkerRegistrationListChanged",
    463    });
    464    this._parameters.serviceWorkerRegistrationList.onListChanged = null;
    465  }
    466 
    467  listProcesses() {
    468    const { processList } = this._parameters;
    469    if (!processList) {
    470      throw {
    471        error: "noProcesses",
    472        message: "This root actor has no processes.",
    473      };
    474    }
    475    processList.onListChanged = this._onProcessListChanged;
    476    const processes = processList.getList();
    477    const pool = new Pool(this.conn, "process-descriptors");
    478    for (const metadata of processes) {
    479      let processDescriptor = this._getKnownDescriptor(
    480        metadata.id,
    481        this._processDescriptorActorPool
    482      );
    483      if (!processDescriptor) {
    484        processDescriptor = new ProcessDescriptorActor(this.conn, metadata);
    485      }
    486      pool.manage(processDescriptor);
    487    }
    488    // Do not destroy the pool before transfering ownership to the newly created
    489    // pool, so that we do not accidently destroy actors that are still in use.
    490    if (this._processDescriptorActorPool) {
    491      this._processDescriptorActorPool.destroy();
    492    }
    493    this._processDescriptorActorPool = pool;
    494    return [...this._processDescriptorActorPool.poolChildren()];
    495  }
    496 
    497  onProcessListChanged() {
    498    this.conn.send({ from: this.actorID, type: "processListChanged" });
    499    this._parameters.processList.onListChanged = null;
    500  }
    501 
    502  async getProcess(id) {
    503    if (!DevToolsServer.allowChromeProcess) {
    504      throw {
    505        error: "forbidden",
    506        message: "You are not allowed to debug chrome.",
    507      };
    508    }
    509    if (typeof id != "number") {
    510      throw {
    511        error: "wrongParameter",
    512        message: "getProcess requires a valid `id` attribute.",
    513      };
    514    }
    515    this._processDescriptorActorPool =
    516      this._processDescriptorActorPool ||
    517      new Pool(this.conn, "process-descriptors");
    518 
    519    let processDescriptor = this._getKnownDescriptor(
    520      id,
    521      this._processDescriptorActorPool
    522    );
    523    if (!processDescriptor) {
    524      // The parent process has id == 0, based on ProcessActorList::getList implementation
    525      const options = { id, parent: id === 0 };
    526      processDescriptor = new ProcessDescriptorActor(this.conn, options);
    527      this._processDescriptorActorPool.manage(processDescriptor);
    528    }
    529    return processDescriptor;
    530  }
    531 
    532  _getKnownDescriptor(id, pool) {
    533    // if there is no pool, then we do not have any descriptors
    534    if (!pool) {
    535      return null;
    536    }
    537    for (const descriptor of pool.poolChildren()) {
    538      if (descriptor.id === id) {
    539        return descriptor;
    540      }
    541    }
    542    return null;
    543  }
    544 
    545  /**
    546   * Remove the extra actor (added by ActorRegistry.addGlobalActor or
    547   * ActorRegistry.addTargetScopedActor) name |name|.
    548   */
    549  removeActorByName(name) {
    550    if (name in this._extraActors) {
    551      const actor = this._extraActors[name];
    552      if (this._globalActorPool.has(actor.actorID)) {
    553        actor.destroy();
    554      }
    555      if (this._tabDescriptorActorPool) {
    556        // Iterate over WindowGlobalTargetActor instances to also remove target-scoped
    557        // actors created during listTabs for each document.
    558        for (const tab in this._tabDescriptorActorPool.poolChildren()) {
    559          tab.removeActorByName(name);
    560        }
    561      }
    562      delete this._extraActors[name];
    563    }
    564  }
    565 
    566  /**
    567   * Start watching for a list of resource types.
    568   *
    569   * See WatcherActor.watchResources.
    570   */
    571  async watchResources(resourceTypes) {
    572    await Resources.watchResources(this, resourceTypes);
    573  }
    574 
    575  /**
    576   * Stop watching for a list of resource types.
    577   *
    578   * See WatcherActor.unwatchResources.
    579   */
    580  unwatchResources(resourceTypes) {
    581    Resources.unwatchResources(this, resourceTypes);
    582  }
    583 
    584  /**
    585   * Clear resources of a list of resource types.
    586   *
    587   * See WatcherActor.clearResources.
    588   */
    589  clearResources(resourceTypes) {
    590    Resources.clearResources(this, resourceTypes);
    591  }
    592 
    593  /**
    594   * Called by Resource Watchers, when new resources are available, updated or destroyed.
    595   *
    596   * @param String updateType
    597   *        Can be "available", "updated" or "destroyed"
    598   * @param String resourceType
    599   *        The type of resources to be notified about.
    600   * @param Array<json> resources
    601   *        List of all resources. A resource is a JSON object piped over to the client.
    602   *        It can contain actor IDs.
    603   *        It can also be or contain an actor form, to be manually marshalled by the client.
    604   *        (i.e. the frontend would have to manually instantiate a Front for the given actor form)
    605   */
    606  notifyResources(updateType, resourceType, resources) {
    607    if (resources.length === 0) {
    608      // Don't try to emit if the resources array is empty.
    609      return;
    610    }
    611 
    612    switch (updateType) {
    613      case "available":
    614        this.emit(`resources-available-array`, [[resourceType, resources]]);
    615        break;
    616      case "updated":
    617        this.emit(`resources-updated-array`, [[resourceType, resources]]);
    618        break;
    619      case "destroyed":
    620        this.emit(`resources-destroyed-array`, [[resourceType, resources]]);
    621        break;
    622      default:
    623        throw new Error("Unsupported update type: " + updateType);
    624    }
    625  }
    626 }
    627 
    628 exports.RootActor = RootActor;