tor-browser

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

har-collector.js (13873B)


      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  getLongStringFullText,
      9 } = require("resource://devtools/client/shared/string-utils.js");
     10 
     11 // Helper tracer. Should be generic sharable by other modules (bug 1171927)
     12 const trace = {
     13  log() {},
     14 };
     15 
     16 /**
     17 * This object is responsible for collecting data related to all
     18 * HTTP requests executed by the page (including inner iframes).
     19 */
     20 class HarCollector {
     21  constructor(options) {
     22    this.commands = options.commands;
     23 
     24    this.onResourceAvailable = this.onResourceAvailable.bind(this);
     25    this.onResourceUpdated = this.onResourceUpdated.bind(this);
     26    this.onRequestHeaders = this.onRequestHeaders.bind(this);
     27    this.onRequestCookies = this.onRequestCookies.bind(this);
     28    this.onRequestPostData = this.onRequestPostData.bind(this);
     29    this.onResponseHeaders = this.onResponseHeaders.bind(this);
     30    this.onResponseCookies = this.onResponseCookies.bind(this);
     31    this.onResponseContent = this.onResponseContent.bind(this);
     32    this.onEventTimings = this.onEventTimings.bind(this);
     33 
     34    this.clear();
     35  }
     36 
     37  // Connection
     38 
     39  async start() {
     40    await this.commands.resourceCommand.watchResources(
     41      [this.commands.resourceCommand.TYPES.NETWORK_EVENT],
     42      {
     43        onAvailable: this.onResourceAvailable,
     44        onUpdated: this.onResourceUpdated,
     45      }
     46    );
     47  }
     48 
     49  async stop() {
     50    await this.commands.resourceCommand.unwatchResources(
     51      [this.commands.resourceCommand.TYPES.NETWORK_EVENT],
     52      {
     53        onAvailable: this.onResourceAvailable,
     54        onUpdated: this.onResourceUpdated,
     55      }
     56    );
     57  }
     58 
     59  clear() {
     60    // Any pending requests events will be ignored (they turn
     61    // into zombies, since not present in the files array).
     62    this.files = new Map();
     63    this.items = [];
     64    this.firstRequestStart = -1;
     65    this.lastRequestStart = -1;
     66    this.requests = [];
     67  }
     68 
     69  waitForHarLoad() {
     70    // There should be yet another timeout e.g.:
     71    // 'devtools.netmonitor.har.pageLoadTimeout'
     72    // that should force export even if page isn't fully loaded.
     73    return new Promise(resolve => {
     74      this.waitForResponses().then(() => {
     75        trace.log("HarCollector.waitForHarLoad; DONE HAR loaded!");
     76        resolve(this);
     77      });
     78    });
     79  }
     80 
     81  waitForResponses() {
     82    trace.log("HarCollector.waitForResponses; " + this.requests.length);
     83 
     84    // All requests for additional data must be received to have complete
     85    // HTTP info to generate the result HAR file. So, wait for all current
     86    // promises. Note that new promises (requests) can be generated during the
     87    // process of HTTP data collection.
     88    return waitForAll(this.requests).then(() => {
     89      // All responses are received from the backend now. We yet need to
     90      // wait for a little while to see if a new request appears. If yes,
     91      // lets's start gathering HTTP data again. If no, we can declare
     92      // the page loaded.
     93      // If some new requests appears in the meantime the promise will
     94      // be rejected and we need to wait for responses all over again.
     95 
     96      this.pageLoadDeferred = this.waitForTimeout().then(
     97        () => {
     98          // Page loaded!
     99        },
    100        () => {
    101          trace.log(
    102            "HarCollector.waitForResponses; NEW requests " +
    103              "appeared during page timeout!"
    104          );
    105          // New requests executed, let's wait again.
    106          return this.waitForResponses();
    107        }
    108      );
    109      return this.pageLoadDeferred;
    110    });
    111  }
    112 
    113  // Page Loaded Timeout
    114 
    115  /**
    116   * The page is loaded when there are no new requests within given period
    117   * of time. The time is set in preferences:
    118   * 'devtools.netmonitor.har.pageLoadedTimeout'
    119   */
    120  waitForTimeout() {
    121    // The auto-export is not done if the timeout is set to zero (or less).
    122    // This is useful in cases where the export is done manually through
    123    // API exposed to the content.
    124    const timeout = Services.prefs.getIntPref(
    125      "devtools.netmonitor.har.pageLoadedTimeout"
    126    );
    127 
    128    trace.log("HarCollector.waitForTimeout; " + timeout);
    129 
    130    return new Promise((resolve, reject) => {
    131      if (timeout <= 0) {
    132        resolve();
    133      }
    134      this.pageLoadReject = reject;
    135      this.pageLoadTimeout = setTimeout(() => {
    136        trace.log("HarCollector.onPageLoadTimeout;");
    137        resolve();
    138      }, timeout);
    139    });
    140  }
    141 
    142  resetPageLoadTimeout() {
    143    // Remove the current timeout.
    144    if (this.pageLoadTimeout) {
    145      trace.log("HarCollector.resetPageLoadTimeout;");
    146 
    147      clearTimeout(this.pageLoadTimeout);
    148      this.pageLoadTimeout = null;
    149    }
    150 
    151    // Reject the current page load promise
    152    if (this.pageLoadReject) {
    153      this.pageLoadReject();
    154      this.pageLoadReject = null;
    155    }
    156  }
    157 
    158  // Collected Data
    159 
    160  getFile(actorId) {
    161    return this.files.get(actorId);
    162  }
    163 
    164  getItems() {
    165    return this.items;
    166  }
    167 
    168  // Event Handlers
    169 
    170  onResourceAvailable(resources) {
    171    for (const resource of resources) {
    172      trace.log("HarCollector.onNetworkEvent; ", resource);
    173 
    174      const { actor, startedDateTime, method, url, isXHR } = resource;
    175      const startTime = Date.parse(startedDateTime);
    176 
    177      if (this.firstRequestStart == -1) {
    178        this.firstRequestStart = startTime;
    179      }
    180 
    181      if (this.lastRequestEnd < startTime) {
    182        this.lastRequestEnd = startTime;
    183      }
    184 
    185      let file = this.getFile(actor);
    186      if (file) {
    187        console.error(
    188          "HarCollector.onNetworkEvent; ERROR " + "existing file conflict!"
    189        );
    190        continue;
    191      }
    192 
    193      file = {
    194        id: actor,
    195        startedDeltaMs: startTime - this.firstRequestStart,
    196        startedMs: startTime,
    197        method,
    198        url,
    199        isXHR,
    200      };
    201 
    202      this.files.set(actor, file);
    203 
    204      // Mimic the Net panel data structure
    205      this.items.push(file);
    206    }
    207  }
    208 
    209  onResourceUpdated(updates) {
    210    for (const { resource } of updates) {
    211      // Skip events from unknown actors (not in the list).
    212      // It can happen when there are zombie requests received after
    213      // the target is closed or multiple tabs are attached through
    214      // one connection (one DevToolsClient object).
    215      const file = this.getFile(resource.actor);
    216      if (!file) {
    217        return;
    218      }
    219 
    220      const includeResponseBodies = Services.prefs.getBoolPref(
    221        "devtools.netmonitor.har.includeResponseBodies"
    222      );
    223 
    224      [
    225        {
    226          type: "eventTimings",
    227          method: "getEventTimings",
    228          callbackName: "onEventTimings",
    229        },
    230        {
    231          type: "requestHeaders",
    232          method: "getRequestHeaders",
    233          callbackName: "onRequestHeaders",
    234        },
    235        {
    236          type: "requestPostData",
    237          method: "getRequestPostData",
    238          callbackName: "onRequestPostData",
    239        },
    240        {
    241          type: "responseHeaders",
    242          method: "getResponseHeaders",
    243          callbackName: "onResponseHeaders",
    244        },
    245        { type: "responseStart" },
    246        {
    247          type: "responseContent",
    248          method: "getResponseContent",
    249          callbackName: "onResponseContent",
    250        },
    251        {
    252          type: "requestCookies",
    253          method: "getRequestCookies",
    254          callbackName: "onRequestCookies",
    255        },
    256        {
    257          type: "responseCookies",
    258          method: "getResponseCookies",
    259          callbackName: "onResponseCookies",
    260        },
    261      ].forEach(updateType => {
    262        trace.log(
    263          "HarCollector.onNetworkEventUpdate; " + updateType.type,
    264          resource
    265        );
    266 
    267        let request;
    268        if (resource[`${updateType.type}Available`]) {
    269          if (updateType.type == "responseStart") {
    270            file.httpVersion = resource.httpVersion;
    271            file.status = resource.status;
    272            file.statusText = resource.statusText;
    273          } else if (updateType.type == "responseContent") {
    274            file.contentSize = resource.contentSize;
    275            file.mimeType = resource.mimeType;
    276            file.transferredSize = resource.transferredSize;
    277            if (includeResponseBodies) {
    278              request = this.getData(
    279                resource.actor,
    280                updateType.method,
    281                this[updateType.callbackName]
    282              );
    283            }
    284          } else {
    285            request = this.getData(
    286              resource.actor,
    287              updateType.method,
    288              this[updateType.callbackName]
    289            );
    290          }
    291        }
    292 
    293        if (request) {
    294          this.requests.push(request);
    295        }
    296        this.resetPageLoadTimeout();
    297      });
    298    }
    299  }
    300 
    301  async getData(actor, method, callback) {
    302    const file = this.getFile(actor);
    303 
    304    trace.log(
    305      "HarCollector.getData; REQUEST " + method + ", " + file.url,
    306      file
    307    );
    308 
    309    // Bug 1519082: We don't create fronts for NetworkEvent actors,
    310    // so that we have to do the request manually via DevToolsClient.request()
    311    const packet = {
    312      to: actor,
    313      type: method,
    314    };
    315    const response = await this.commands.client.request(packet);
    316 
    317    trace.log(
    318      "HarCollector.getData; RESPONSE " + method + ", " + file.url,
    319      response
    320    );
    321    callback(response);
    322    return response;
    323  }
    324 
    325  /**
    326   * Handles additional information received for a "requestHeaders" packet.
    327   *
    328   * @param {object} response
    329   *        The message received from the server.
    330   */
    331  onRequestHeaders(response) {
    332    const file = this.getFile(response.from);
    333    file.requestHeaders = response;
    334 
    335    this.getLongHeaders(response.headers);
    336  }
    337 
    338  /**
    339   * Handles additional information received for a "requestCookies" packet.
    340   *
    341   * @param {object} response
    342   *        The message received from the server.
    343   */
    344  onRequestCookies(response) {
    345    const file = this.getFile(response.from);
    346    file.requestCookies = response;
    347 
    348    this.getLongHeaders(response.cookies);
    349  }
    350 
    351  /**
    352   * Handles additional information received for a "requestPostData" packet.
    353   *
    354   * @param {object} response
    355   *        The message received from the server.
    356   */
    357  onRequestPostData(response) {
    358    trace.log("HarCollector.onRequestPostData;", response);
    359 
    360    const file = this.getFile(response.from);
    361    file.requestPostData = response;
    362 
    363    // Resolve long string
    364    const { text } = response.postData;
    365    if (typeof text == "object") {
    366      this.getString(text).then(value => {
    367        response.postData.text = value;
    368      });
    369    }
    370  }
    371 
    372  /**
    373   * Handles additional information received for a "responseHeaders" packet.
    374   *
    375   * @param {object} response
    376   *        The message received from the server.
    377   */
    378  onResponseHeaders(response) {
    379    const file = this.getFile(response.from);
    380    file.responseHeaders = response;
    381 
    382    this.getLongHeaders(response.headers);
    383  }
    384 
    385  /**
    386   * Handles additional information received for a "responseCookies" packet.
    387   *
    388   * @param {object} response
    389   *        The message received from the server.
    390   */
    391  onResponseCookies(response) {
    392    const file = this.getFile(response.from);
    393    file.responseCookies = response;
    394 
    395    this.getLongHeaders(response.cookies);
    396  }
    397 
    398  /**
    399   * Handles additional information received for a "responseContent" packet.
    400   *
    401   * @param {object} response
    402   *        The message received from the server.
    403   */
    404  onResponseContent(response) {
    405    const file = this.getFile(response.from);
    406    file.responseContent = response;
    407 
    408    // Resolve long string
    409    const { text } = response.content;
    410    if (typeof text == "object") {
    411      this.getString(text).then(value => {
    412        response.content.text = value;
    413      });
    414    }
    415  }
    416 
    417  /**
    418   * Handles additional information received for a "eventTimings" packet.
    419   *
    420   * @param {object} response
    421   *        The message received from the server.
    422   */
    423  onEventTimings(response) {
    424    const file = this.getFile(response.from);
    425    file.eventTimings = response;
    426    file.totalTime = response.totalTime;
    427  }
    428 
    429  // Helpers
    430  getLongHeaders(headers) {
    431    for (const header of headers) {
    432      if (typeof header.value == "object") {
    433        try {
    434          this.getString(header.value).then(value => {
    435            header.value = value;
    436          });
    437        } catch (error) {
    438          trace.log("HarCollector.getLongHeaders; ERROR when getString", error);
    439        }
    440      }
    441    }
    442  }
    443 
    444  /**
    445   * Fetches the full text of a string.
    446   *
    447   * @param {object | string} stringGrip
    448   *        The long string grip containing the corresponding actor.
    449   *        If you pass in a plain string (by accident or because you're lazy),
    450   *        then a promise of the same string is simply returned.
    451   * @return {object} Promise
    452   *         A promise that is resolved when the full string contents
    453   *         are available, or rejected if something goes wrong.
    454   */
    455  async getString(stringGrip) {
    456    const promise = getLongStringFullText(this.commands.client, stringGrip);
    457    this.requests.push(promise);
    458    return promise;
    459  }
    460 }
    461 
    462 // Helpers
    463 
    464 /**
    465 * Helper function that allows to wait for array of promises. It is
    466 * possible to dynamically add new promises in the provided array.
    467 * The function will wait even for the newly added promises.
    468 * (this isn't possible with the default Promise.all);
    469 */
    470 function waitForAll(promises) {
    471  // Remove all from the original array and get clone of it.
    472  const clone = promises.splice(0, promises.length);
    473 
    474  // Wait for all promises in the given array.
    475  return Promise.all(clone).then(() => {
    476    // If there are new promises (in the original array)
    477    // to wait for - chain them!
    478    if (promises.length) {
    479      return waitForAll(promises);
    480    }
    481 
    482    return undefined;
    483  });
    484 }
    485 
    486 // Exports from this module
    487 exports.HarCollector = HarCollector;