tor-browser

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

index.js (17025B)


      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  ACTIVITY_TYPE,
      9  EVENTS,
     10  TEST_EVENTS,
     11 } = require("resource://devtools/client/netmonitor/src/constants.js");
     12 const FirefoxDataProvider = require("resource://devtools/client/netmonitor/src/connector/firefox-data-provider.js");
     13 const {
     14  getDisplayedTimingMarker,
     15 } = require("resource://devtools/client/netmonitor/src/selectors/index.js");
     16 
     17 const {
     18  TYPES,
     19 } = require("resource://devtools/shared/commands/resource/resource-command.js");
     20 
     21 // Network throttling
     22 loader.lazyRequireGetter(
     23  this,
     24  "throttlingProfiles",
     25  "resource://devtools/client/shared/components/throttling/profiles.js"
     26 );
     27 
     28 loader.lazyRequireGetter(
     29  this,
     30  "HarMetadataCollector",
     31  "resource://devtools/client/netmonitor/src/connector/har-metadata-collector.js",
     32  true
     33 );
     34 
     35 const DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF = "devtools.netmonitor.persistlog";
     36 
     37 /**
     38 * Connector to Firefox backend.
     39 */
     40 class Connector {
     41  constructor() {
     42    // Public methods
     43    this.connect = this.connect.bind(this);
     44    this.disconnect = this.disconnect.bind(this);
     45    this.willNavigate = this.willNavigate.bind(this);
     46    this.navigate = this.navigate.bind(this);
     47    this.triggerActivity = this.triggerActivity.bind(this);
     48    this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
     49    this.requestData = this.requestData.bind(this);
     50    this.getTimingMarker = this.getTimingMarker.bind(this);
     51    this.updateNetworkThrottling = this.updateNetworkThrottling.bind(this);
     52 
     53    // Internals
     54    this.getLongString = this.getLongString.bind(this);
     55    this.onResourceAvailable = this.onResourceAvailable.bind(this);
     56    this.onResourceUpdated = this.onResourceUpdated.bind(this);
     57    this.updatePersist = this.updatePersist.bind(this);
     58 
     59    this.networkFront = null;
     60  }
     61 
     62  static NETWORK_RESOURCES = [
     63    TYPES.NETWORK_EVENT,
     64    TYPES.NETWORK_EVENT_STACKTRACE,
     65    TYPES.WEBSOCKET,
     66    TYPES.SERVER_SENT_EVENT,
     67  ];
     68 
     69  get networkResources() {
     70    const networkResources = Array.from(Connector.NETWORK_RESOURCES);
     71    if (
     72      Services.prefs.getBoolPref("devtools.netmonitor.features.webtransport")
     73    ) {
     74      networkResources.push(TYPES.WEBTRANSPORT);
     75    }
     76    return networkResources;
     77  }
     78 
     79  get currentTarget() {
     80    return this.commands.targetCommand.targetFront;
     81  }
     82 
     83  /**
     84   * Connect to the backend.
     85   *
     86   * @param {object} connection object with e.g. reference to the Toolbox.
     87   * @param {object} actions (optional) is used to fire Redux actions to update store.
     88   * @param {object} getState (optional) is used to get access to the state.
     89   */
     90  async connect(connection, actions, getState) {
     91    this.actions = actions;
     92    this.getState = getState;
     93    this.toolbox = connection.toolbox;
     94    this.commands = this.toolbox.commands;
     95    this.networkCommand = this.commands.networkCommand;
     96 
     97    // The owner object (NetMonitorAPI) received all events.
     98    this.owner = connection.owner;
     99 
    100    this.networkFront =
    101      await this.commands.watcherFront.getNetworkParentActor();
    102 
    103    this.dataProvider = new FirefoxDataProvider({
    104      commands: this.commands,
    105      actions: this.actions,
    106      owner: this.owner,
    107    });
    108 
    109    this._harMetadataCollector = new HarMetadataCollector(this.commands);
    110    await this._harMetadataCollector.connect();
    111 
    112    await this.commands.resourceCommand.watchResources([TYPES.DOCUMENT_EVENT], {
    113      onAvailable: this.onResourceAvailable,
    114    });
    115 
    116    await this.resume(false);
    117 
    118    // Server side persistance of the data across reload is disabled by default.
    119    // Ensure enabling it, if the related frontend pref is true.
    120    if (Services.prefs.getBoolPref(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF)) {
    121      await this.updatePersist();
    122    }
    123    Services.prefs.addObserver(
    124      DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF,
    125      this.updatePersist
    126    );
    127  }
    128 
    129  disconnect() {
    130    // As this function might be called twice, we need to guard if already called.
    131    if (this._destroyed) {
    132      return;
    133    }
    134 
    135    this._destroyed = true;
    136 
    137    this.commands.resourceCommand.unwatchResources([TYPES.DOCUMENT_EVENT], {
    138      onAvailable: this.onResourceAvailable,
    139    });
    140 
    141    this.pause();
    142 
    143    Services.prefs.removeObserver(
    144      DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF,
    145      this.updatePersist
    146    );
    147 
    148    if (this.actions) {
    149      this.actions.batchReset();
    150    }
    151 
    152    this.dataProvider.destroy();
    153    this.dataProvider = null;
    154    this._harMetadataCollector.destroy();
    155  }
    156 
    157  /**
    158   * Clear network data from the connector.
    159   *
    160   * @param {object} options
    161   * @param {boolean} options.isExplicitClear
    162   *     Set to true if the call to clear requests is explicitly requested by
    163   *     the user, to false if this is an automated clear, eg on navigation.
    164   */
    165  clear({ isExplicitClear }) {
    166    // Clear all the caches in the data provider
    167    this.dataProvider.clear();
    168 
    169    this._harMetadataCollector.clear();
    170 
    171    if (isExplicitClear) {
    172      // Only clear the resources if the clear was initiated explicitly by the
    173      // UI, in other cases (eg navigation) the server handles the cleanup.
    174      this.commands.resourceCommand.clearResources(this.networkResources);
    175      this.emitForTests("clear-network-resources");
    176    }
    177 
    178    // Disable the related network logs in the webconsole
    179    this.toolbox.disableAllConsoleNetworkLogs();
    180  }
    181 
    182  pause() {
    183    return this.commands.resourceCommand.unwatchResources(
    184      this.networkResources,
    185      {
    186        onAvailable: this.onResourceAvailable,
    187        onUpdated: this.onResourceUpdated,
    188      }
    189    );
    190  }
    191 
    192  resume(ignoreExistingResources = true) {
    193    return this.commands.resourceCommand.watchResources(this.networkResources, {
    194      onAvailable: this.onResourceAvailable,
    195      onUpdated: this.onResourceUpdated,
    196      ignoreExistingResources,
    197    });
    198  }
    199 
    200  async onResourceAvailable(resources, { areExistingResources }) {
    201    for (const resource of resources) {
    202      if (resource.resourceType === TYPES.DOCUMENT_EVENT) {
    203        this.onDocEvent(resource, { areExistingResources });
    204        continue;
    205      }
    206 
    207      if (resource.resourceType === TYPES.NETWORK_EVENT) {
    208        this.dataProvider.onNetworkResourceAvailable(resource);
    209        continue;
    210      }
    211 
    212      if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) {
    213        this.dataProvider.onStackTraceAvailable(resource);
    214        continue;
    215      }
    216 
    217      if (resource.resourceType === TYPES.WEBSOCKET) {
    218        const { wsMessageType } = resource;
    219 
    220        switch (wsMessageType) {
    221          case "webSocketOpened": {
    222            this.dataProvider.onWebSocketOpened(
    223              resource.httpChannelId,
    224              resource.effectiveURI,
    225              resource.protocols,
    226              resource.extensions
    227            );
    228            break;
    229          }
    230          case "webSocketClosed": {
    231            this.dataProvider.onWebSocketClosed(
    232              resource.httpChannelId,
    233              resource.wasClean,
    234              resource.code,
    235              resource.reason
    236            );
    237            break;
    238          }
    239          case "frameReceived": {
    240            this.dataProvider.onFrameReceived(
    241              resource.httpChannelId,
    242              resource.data
    243            );
    244            break;
    245          }
    246          case "frameSent": {
    247            this.dataProvider.onFrameSent(
    248              resource.httpChannelId,
    249              resource.data
    250            );
    251            break;
    252          }
    253        }
    254        continue;
    255      }
    256 
    257      if (resource.resourceType === TYPES.SERVER_SENT_EVENT) {
    258        const { messageType, httpChannelId, data } = resource;
    259        switch (messageType) {
    260          case "eventSourceConnectionClosed": {
    261            this.dataProvider.onEventSourceConnectionClosed(httpChannelId);
    262            break;
    263          }
    264          case "eventReceived": {
    265            this.dataProvider.onEventReceived(httpChannelId, data);
    266            break;
    267          }
    268        }
    269      }
    270    }
    271  }
    272 
    273  async onResourceUpdated(updates) {
    274    for (const { resource, update } of updates) {
    275      this.dataProvider.onNetworkResourceUpdated(resource, update);
    276    }
    277  }
    278 
    279  enableActions(enable) {
    280    this.dataProvider.enableActions(enable);
    281  }
    282 
    283  willNavigate() {
    284    if (this.actions) {
    285      if (!Services.prefs.getBoolPref(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF)) {
    286        this.actions.batchReset();
    287        this.actions.clearRequests({ isExplicitClear: false });
    288      } else {
    289        // If the log is persistent, just clear all accumulated timing markers.
    290        this.actions.clearTimingMarkers();
    291      }
    292    }
    293 
    294    if (this.actions && this.getState) {
    295      const state = this.getState();
    296      // Resume is done automatically on page reload/navigation.
    297      if (!state.requests.recording) {
    298        this.actions.toggleRecording();
    299      }
    300 
    301      // Stop any ongoing search.
    302      if (state.search.ongoingSearch) {
    303        this.actions.stopOngoingSearch();
    304      }
    305    }
    306  }
    307 
    308  navigate() {
    309    if (!this.dataProvider.hasPendingRequests()) {
    310      this.onReloaded();
    311      return;
    312    }
    313    const listener = () => {
    314      if (this.dataProvider && this.dataProvider.hasPendingRequests()) {
    315        return;
    316      }
    317      if (this.owner) {
    318        this.owner.off(EVENTS.PAYLOAD_READY, listener);
    319      }
    320      // Netmonitor may already be destroyed,
    321      // so do not try to notify the listeners
    322      if (this.dataProvider) {
    323        this.onReloaded();
    324      }
    325    };
    326    if (this.owner) {
    327      this.owner.on(EVENTS.PAYLOAD_READY, listener);
    328    }
    329  }
    330 
    331  onReloaded() {
    332    const panel = this.toolbox.getPanel("netmonitor");
    333    if (panel) {
    334      panel.emit("reloaded");
    335    }
    336  }
    337 
    338  /**
    339   * The "DOMContentLoaded" and "Load" events sent by the console actor.
    340   *
    341   * @param {object} resource The DOCUMENT_EVENT resource
    342   */
    343  onDocEvent(resource, { areExistingResources }) {
    344    if (!resource.targetFront.isTopLevel) {
    345      // Only consider top level document, and ignore remote iframes top document
    346      return;
    347    }
    348 
    349    // Netmonitor does not support dom-loading
    350    if (
    351      resource.name != "dom-interactive" &&
    352      resource.name != "dom-complete" &&
    353      resource.name != "will-navigate"
    354    ) {
    355      return;
    356    }
    357 
    358    if (resource.name == "will-navigate") {
    359      // When we open the netmonitor while the page already started loading,
    360      // we don't want to clear it. So here, we ignore will-navigate events
    361      // which were stored in the ResourceCommand cache and only consider
    362      // the live one coming straight from the server.
    363      if (!areExistingResources) {
    364        this.willNavigate();
    365      }
    366      return;
    367    }
    368 
    369    if (this.actions) {
    370      this.actions.addTimingMarker(resource);
    371    }
    372 
    373    if (resource.name === "dom-complete") {
    374      this.navigate();
    375    }
    376 
    377    this.emitForTests(TEST_EVENTS.TIMELINE_EVENT, resource);
    378  }
    379 
    380  async updatePersist() {
    381    const enabled = Services.prefs.getBoolPref(
    382      DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF
    383    );
    384 
    385    await this.networkFront.setPersist(enabled);
    386 
    387    this.emitForTests(TEST_EVENTS.PERSIST_CHANGED, enabled);
    388  }
    389 
    390  /**
    391   * Triggers a specific "activity" to be performed by the frontend.
    392   * This can be, for example, triggering reloads or enabling/disabling cache.
    393   *
    394   * @param {number} type The activity type. See the ACTIVITY_TYPE const.
    395   * @return {object} A promise resolved once the activity finishes and the frontend
    396   *                  is back into "standby" mode.
    397   */
    398  triggerActivity(type) {
    399    // Puts the frontend into "standby" (when there's no particular activity).
    400    const standBy = () => {
    401      this.currentActivity = ACTIVITY_TYPE.NONE;
    402    };
    403 
    404    // Reconfigures the tab, optionally triggering a reload.
    405    const reconfigureTab = async options => {
    406      await this.commands.targetConfigurationCommand.updateConfiguration(
    407        options
    408      );
    409    };
    410 
    411    // Reconfigures the tab and waits for the target to finish navigating.
    412    const reconfigureTabAndReload = async options => {
    413      await reconfigureTab(options);
    414      await this.commands.targetCommand.reloadTopLevelTarget();
    415    };
    416 
    417    switch (type) {
    418      case ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT:
    419        return reconfigureTabAndReload({}).then(standBy);
    420      case ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED:
    421        this.currentActivity = ACTIVITY_TYPE.ENABLE_CACHE;
    422        this.commands.resourceCommand
    423          .waitForNextResource(
    424            this.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
    425            {
    426              ignoreExistingResources: true,
    427              predicate(resource) {
    428                return resource.name == "will-navigate";
    429              },
    430            }
    431          )
    432          .then(() => {
    433            this.currentActivity = type;
    434          });
    435        return reconfigureTabAndReload({
    436          cacheDisabled: false,
    437        }).then(standBy);
    438      case ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED:
    439        this.currentActivity = ACTIVITY_TYPE.DISABLE_CACHE;
    440        this.commands.resourceCommand
    441          .waitForNextResource(
    442            this.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
    443            {
    444              ignoreExistingResources: true,
    445              predicate(resource) {
    446                return resource.name == "will-navigate";
    447              },
    448            }
    449          )
    450          .then(() => {
    451            this.currentActivity = type;
    452          });
    453        return reconfigureTabAndReload({
    454          cacheDisabled: true,
    455        }).then(standBy);
    456      case ACTIVITY_TYPE.ENABLE_CACHE:
    457        this.currentActivity = type;
    458        return reconfigureTab({
    459          cacheDisabled: false,
    460        }).then(standBy);
    461      case ACTIVITY_TYPE.DISABLE_CACHE:
    462        this.currentActivity = type;
    463        return reconfigureTab({
    464          cacheDisabled: true,
    465        }).then(standBy);
    466    }
    467    this.currentActivity = ACTIVITY_TYPE.NONE;
    468    return Promise.reject(new Error("Invalid activity type"));
    469  }
    470 
    471  /**
    472   * Fetches the full text of a LongString.
    473   *
    474   * @param {object|string} stringGrip
    475   *        The long string grip containing the corresponding actor.
    476   *        If you pass in a plain string (by accident or because you're lazy),
    477   *        then a promise of the same string is simply returned.
    478   * @return {object}
    479   *         A promise that is resolved when the full string contents
    480   *         are available, or rejected if something goes wrong.
    481   */
    482  getLongString(stringGrip) {
    483    return this.dataProvider.getLongString(stringGrip);
    484  }
    485 
    486  /**
    487   * Used for HAR generation.
    488   */
    489  getHarData() {
    490    return this._harMetadataCollector.getHarData();
    491  }
    492 
    493  /**
    494   * Getter that returns the current toolbox instance.
    495   *
    496   * @return {Toolbox} toolbox instance
    497   */
    498  getToolbox() {
    499    return this.toolbox;
    500  }
    501 
    502  /**
    503   * Open a given source in Debugger
    504   *
    505   * @param {string} sourceURL source url
    506   * @param {number} sourceLine source line number
    507   */
    508  viewSourceInDebugger(sourceURL, sourceLine, sourceColumn) {
    509    if (this.toolbox) {
    510      this.toolbox.viewSourceInDebugger(sourceURL, sourceLine, sourceColumn);
    511    }
    512  }
    513 
    514  /**
    515   * Fetch networkEventUpdate websocket message from back-end when
    516   * data provider is connected.
    517   *
    518   * @param {object} request network request instance
    519   * @param {string} type NetworkEventUpdate type
    520   */
    521  requestData(request, type) {
    522    return this.dataProvider.requestData(request, type);
    523  }
    524 
    525  getTimingMarker(name) {
    526    if (!this.getState) {
    527      return -1;
    528    }
    529 
    530    const state = this.getState();
    531    return getDisplayedTimingMarker(state, name);
    532  }
    533 
    534  async updateNetworkThrottling(enabled, profile) {
    535    if (!enabled) {
    536      this.networkFront.clearNetworkThrottling();
    537      await this.commands.targetConfigurationCommand.updateConfiguration({
    538        setTabOffline: false,
    539      });
    540    } else {
    541      // The profile can be either a profile id which is used to
    542      // search the predefined throttle profiles or a profile object
    543      // as defined in the trottle tests.
    544      if (typeof profile === "string") {
    545        profile = throttlingProfiles.profiles.find(({ id }) => id == profile);
    546      }
    547      const { download, upload, latency, id } = profile;
    548 
    549      // The offline profile has download and upload set to false
    550      await this.commands.targetConfigurationCommand.updateConfiguration({
    551        setTabOffline: id === throttlingProfiles.PROFILE_CONSTANTS.OFFLINE,
    552      });
    553 
    554      await this.networkFront.setNetworkThrottling({
    555        downloadThroughput: download,
    556        uploadThroughput: upload,
    557        latency,
    558      });
    559    }
    560 
    561    this.emitForTests(TEST_EVENTS.THROTTLING_CHANGED, { profile });
    562  }
    563 
    564  /**
    565   * Fire events for the owner object. These events are only
    566   * used in tests so, don't fire them in production release.
    567   */
    568  emitForTests(type, data) {
    569    if (this.owner) {
    570      this.owner.emitForTests(type, data);
    571    }
    572  }
    573 }
    574 module.exports.Connector = Connector;