tor-browser

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

legacy-serviceworkers-watcher.js (11362B)


      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 const {
      8  WorkersListener,
      9  // eslint-disable-next-line mozilla/reject-some-requires
     10 } = require("resource://devtools/client/shared/workers-listener.js");
     11 
     12 const LegacyWorkersWatcher = require("resource://devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js");
     13 
     14 class LegacyServiceWorkersWatcher extends LegacyWorkersWatcher {
     15  // Holds the current target URL object
     16  #currentTargetURL;
     17 
     18  constructor(targetCommand, onTargetAvailable, onTargetDestroyed, commands) {
     19    super(targetCommand, onTargetAvailable, onTargetDestroyed);
     20    this._registrations = [];
     21    this._processTargets = new Set();
     22    this.commands = commands;
     23 
     24    // We need to listen for registration changes at least in order to properly
     25    // filter service workers by domain when debugging a local tab.
     26    //
     27    // A WorkerTarget instance has a url property, but it points to the url of
     28    // the script, whereas the url property of the ServiceWorkerRegistration
     29    // points to the URL controlled by the service worker.
     30    //
     31    // Historically we have been matching the service worker registration URL
     32    // to match service workers for local tab tools (app panel & debugger).
     33    // Maybe here we could have some more info on the actual worker.
     34    this._workersListener = new WorkersListener(this.rootFront, {
     35      registrationsOnly: true,
     36    });
     37 
     38    // Note that this is called much more often than when a registration
     39    // is created or destroyed. WorkersListener notifies of anything that
     40    // potentially impacted workers.
     41    // I use it as a shortcut in this first patch. Listening to rootFront's
     42    // "serviceWorkerRegistrationListChanged" should be enough to be notified
     43    // about registrations. And if we need to also update the
     44    // "debuggerServiceWorkerStatus" from here, then we would have to
     45    // also listen to "registration-changed" one each registration.
     46    this._onRegistrationListChanged =
     47      this._onRegistrationListChanged.bind(this);
     48    this._onDocumentEvent = this._onDocumentEvent.bind(this);
     49 
     50    // Flag used from the parent class to listen to process targets.
     51    // Decision tree is complicated, keep all logic in the parent methods.
     52    this._isServiceWorkerWatcher = true;
     53  }
     54 
     55  /**
     56   * Override from LegacyWorkersWatcher.
     57   *
     58   * We record all valid service worker targets (ie workers that match a service
     59   * worker registration), but we will only notify about the ones which match
     60   * the current domain.
     61   */
     62  _recordWorkerTarget(workerTarget) {
     63    return !!this._getRegistrationForWorkerTarget(workerTarget);
     64  }
     65 
     66  // Override from LegacyWorkersWatcher.
     67  _supportWorkerTarget(workerTarget) {
     68    if (!workerTarget.isServiceWorker) {
     69      return false;
     70    }
     71 
     72    const registration = this._getRegistrationForWorkerTarget(workerTarget);
     73    return registration && this._isRegistrationValidForTarget(registration);
     74  }
     75 
     76  // Override from LegacyWorkersWatcher.
     77  async listen() {
     78    // Listen to the current target front.
     79    this.target = this.targetCommand.targetFront;
     80 
     81    if (this.targetCommand.descriptorFront.isTabDescriptor) {
     82      this.#currentTargetURL = new URL(this.targetCommand.targetFront.url);
     83    }
     84 
     85    this._workersListener.addListener(this._onRegistrationListChanged);
     86 
     87    // Fetch the registrations before calling listen, since service workers
     88    // might already be available and will need to be compared with the existing
     89    // registrations.
     90    await this._onRegistrationListChanged();
     91 
     92    if (this.targetCommand.descriptorFront.isTabDescriptor) {
     93      await this.commands.resourceCommand.watchResources(
     94        [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
     95        {
     96          onAvailable: this._onDocumentEvent,
     97          ignoreExistingResources: true,
     98        }
     99      );
    100    }
    101 
    102    await super.listen();
    103  }
    104 
    105  // Override from LegacyWorkersWatcher.
    106  unlisten(...args) {
    107    this._workersListener.removeListener(this._onRegistrationListChanged);
    108 
    109    if (this.targetCommand.descriptorFront.isTabDescriptor) {
    110      this.commands.resourceCommand.unwatchResources(
    111        [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
    112        {
    113          onAvailable: this._onDocumentEvent,
    114        }
    115      );
    116    }
    117 
    118    super.unlisten(...args);
    119  }
    120 
    121  // Override from LegacyWorkersWatcher.
    122  async _onProcessAvailable({ targetFront }) {
    123    if (this.targetCommand.descriptorFront.isTabDescriptor) {
    124      // XXX: This has been ported straight from the current debugger
    125      // implementation. Since pauseMatchingServiceWorkers expects an origin
    126      // to filter matching workers, it only makes sense when we are debugging
    127      // a tab. However in theory, parent process debugging could pause all
    128      // service workers without matching anything.
    129      try {
    130        // To support early breakpoint we need to setup the
    131        // `pauseMatchingServiceWorkers` mechanism in each process.
    132        await targetFront.pauseMatchingServiceWorkers({
    133          origin: this.#currentTargetURL.origin,
    134        });
    135      } catch (e) {
    136        if (targetFront.actorID) {
    137          throw e;
    138        } else {
    139          console.warn(
    140            "Process target destroyed while calling pauseMatchingServiceWorkers"
    141          );
    142        }
    143      }
    144    }
    145 
    146    this._processTargets.add(targetFront);
    147    return super._onProcessAvailable({ targetFront });
    148  }
    149 
    150  _onProcessDestroyed({ targetFront }) {
    151    this._processTargets.delete(targetFront);
    152    return super._onProcessDestroyed({ targetFront });
    153  }
    154 
    155  _onDocumentEvent(resources) {
    156    for (const resource of resources) {
    157      if (
    158        resource.resourceType !==
    159        this.commands.resourceCommand.TYPES.DOCUMENT_EVENT
    160      ) {
    161        continue;
    162      }
    163 
    164      if (resource.name === "will-navigate") {
    165        // We rely on will-navigate as the onTargetAvailable for the top-level frame can
    166        // happen after the onTargetAvailable for processes (handled in _onProcessAvailable),
    167        // where we need the origin we navigate to.
    168        this.#currentTargetURL = new URL(resource.newURI);
    169        continue;
    170      }
    171 
    172      // Note that we rely on "dom-loading" rather than "will-navigate" because the
    173      // destroyed/available callbacks should be triggered after the Debugger
    174      // has cleaned up its reducers, which happens on "will-navigate".
    175      // On the other end, "dom-complete", which is a better mapping of "navigate", is
    176      // happening too late (because of resources being throttled), and would cause failures
    177      // in test (like browser_target_command_service_workers_navigation.js), as the new worker
    178      // target would already be registered at this point, and seen as something that would
    179      // need to be destroyed.
    180      if (resource.name === "dom-loading") {
    181        const allServiceWorkerTargets = this._getAllServiceWorkerTargets();
    182 
    183        for (const target of allServiceWorkerTargets) {
    184          // Note: we call isTargetRegistered again because calls to
    185          // onTargetDestroyed might have modified the list of registered targets.
    186          const isRegisteredAfter =
    187            this.targetCommand.isTargetRegistered(target);
    188          const isValidTarget = this._supportWorkerTarget(target);
    189          if (isValidTarget && !isRegisteredAfter) {
    190            // If the target is still valid for the current top target, call
    191            // onTargetAvailable as well.
    192            this.onTargetAvailable(target);
    193          }
    194        }
    195      }
    196    }
    197  }
    198 
    199  async _onRegistrationListChanged() {
    200    if (this.targetCommand.isDestroyed()) {
    201      return;
    202    }
    203 
    204    await this._updateRegistrations();
    205 
    206    // Everything after this point is not strictly necessary for sw support
    207    // in the target list, but it makes the behavior closer to the previous
    208    // listAllWorkers/WorkersListener pair.
    209    const allServiceWorkerTargets = this._getAllServiceWorkerTargets();
    210    for (const target of allServiceWorkerTargets) {
    211      const hasRegistration = this._getRegistrationForWorkerTarget(target);
    212      if (!hasRegistration) {
    213        // XXX: At this point the worker target is not really destroyed, but
    214        // historically, listAllWorkers* APIs stopped returning worker targets
    215        // if worker registrations are no longer available.
    216        if (this.targetCommand.isTargetRegistered(target)) {
    217          // Only emit onTargetDestroyed if it wasn't already done by
    218          // onNavigate (ie the target is still tracked by TargetCommand)
    219          this.onTargetDestroyed(target);
    220        }
    221        // Here we only care about service workers which no longer match *any*
    222        // registration. The worker will be completely destroyed soon, remove
    223        // it from the legacy worker watcher internal targetsByProcess Maps.
    224        this._removeTargetReferences(target);
    225      }
    226    }
    227  }
    228 
    229  // Delete the provided worker target from the internal targetsByProcess Maps.
    230  _removeTargetReferences(target) {
    231    const allProcessTargets = this._getProcessTargets().filter(t =>
    232      this.targetsByProcess.get(t)
    233    );
    234 
    235    for (const processTarget of allProcessTargets) {
    236      this.targetsByProcess.get(processTarget).delete(target);
    237    }
    238  }
    239 
    240  async _updateRegistrations() {
    241    const { registrations } =
    242      await this.rootFront.listServiceWorkerRegistrations();
    243 
    244    this._registrations = registrations;
    245  }
    246 
    247  _getRegistrationForWorkerTarget(workerTarget) {
    248    return this._registrations.find(r => {
    249      return (
    250        r.evaluatingWorker?.id === workerTarget.id ||
    251        r.activeWorker?.id === workerTarget.id ||
    252        r.installingWorker?.id === workerTarget.id ||
    253        r.waitingWorker?.id === workerTarget.id
    254      );
    255    });
    256  }
    257 
    258  _getProcessTargets() {
    259    return [...this._processTargets];
    260  }
    261 
    262  // Flatten all service worker targets in all processes.
    263  _getAllServiceWorkerTargets() {
    264    const allProcessTargets = this._getProcessTargets().filter(target =>
    265      this.targetsByProcess.get(target)
    266    );
    267 
    268    const serviceWorkerTargets = [];
    269    for (const target of allProcessTargets) {
    270      serviceWorkerTargets.push(...this.targetsByProcess.get(target));
    271    }
    272    return serviceWorkerTargets;
    273  }
    274 
    275  // Check if the registration is relevant for the current target, ie
    276  // corresponds to the same domain.
    277  _isRegistrationValidForTarget(registration) {
    278    if (this.targetCommand.descriptorFront.isBrowserProcessDescriptor) {
    279      // All registrations are valid for main process debugging.
    280      return true;
    281    }
    282 
    283    if (!this.targetCommand.descriptorFront.isTabDescriptor) {
    284      // No support for service worker targets outside of main process &
    285      // tab debugging.
    286      return false;
    287    }
    288 
    289    // For local tabs, we match ServiceWorkerRegistrations and the target
    290    // if they share the same hostname for their "url" properties.
    291    const targetDomain = this.#currentTargetURL.hostname;
    292    const registrationDomain = URL.parse(registration.url)?.hostname;
    293    if (registrationDomain) {
    294      return registrationDomain === targetDomain;
    295    }
    296    // XXX: Some registrations have an empty URL.
    297    return false;
    298  }
    299 }
    300 
    301 module.exports = LegacyServiceWorkersWatcher;