tor-browser

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

resource.sys.mjs (8259B)


      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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 import { Log } from "resource://gre/modules/Log.sys.mjs";
      8 
      9 import { Observers } from "resource://services-common/observers.sys.mjs";
     10 import { CommonUtils } from "resource://services-common/utils.sys.mjs";
     11 import { Utils } from "resource://services-sync/util.sys.mjs";
     12 import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
     13 
     14 /*
     15 * Resource represents a remote network resource, identified by a URI.
     16 * Create an instance like so:
     17 *
     18 *   let resource = new Resource("http://foobar.com/path/to/resource");
     19 *
     20 * The 'resource' object has the following methods to issue HTTP requests
     21 * of the corresponding HTTP methods:
     22 *
     23 *   get(callback)
     24 *   put(data, callback)
     25 *   post(data, callback)
     26 *   delete(callback)
     27 */
     28 export function Resource(uri) {
     29  this._log = Log.repository.getLogger(this._logName);
     30  this._log.manageLevelFromPref("services.sync.log.logger.network.resources");
     31  this.uri = uri;
     32  this._headers = {};
     33 }
     34 
     35 // (static) Caches the latest server timestamp (X-Weave-Timestamp header).
     36 Resource.serverTime = null;
     37 
     38 XPCOMUtils.defineLazyPreferenceGetter(
     39  Resource,
     40  "SEND_VERSION_INFO",
     41  "services.sync.sendVersionInfo",
     42  true
     43 );
     44 Resource.prototype = {
     45  _logName: "Sync.Resource",
     46 
     47  /**
     48   * Callback to be invoked at request time to add authentication details.
     49   * If the callback returns a promise, it will be awaited upon.
     50   *
     51   * By default, a global authenticator is provided. If this is set, it will
     52   * be used instead of the global one.
     53   */
     54  authenticator: null,
     55 
     56  // Wait 5 minutes before killing a request.
     57  ABORT_TIMEOUT: 300000,
     58 
     59  // Headers to be included when making a request for the resource.
     60  // Note: Header names should be all lower case, there's no explicit
     61  // check for duplicates due to case!
     62  get headers() {
     63    return this._headers;
     64  },
     65  set headers(_) {
     66    throw new Error("headers can't be mutated directly. Please use setHeader.");
     67  },
     68  setHeader(header, value) {
     69    this._headers[header.toLowerCase()] = value;
     70  },
     71 
     72  // URI representing this resource.
     73  get uri() {
     74    return this._uri;
     75  },
     76  set uri(value) {
     77    if (typeof value == "string") {
     78      this._uri = CommonUtils.makeURI(value);
     79    } else {
     80      this._uri = value;
     81    }
     82  },
     83 
     84  // Get the string representation of the URI.
     85  get spec() {
     86    if (this._uri) {
     87      return this._uri.spec;
     88    }
     89    return null;
     90  },
     91 
     92  /**
     93   * @param {string} method HTTP method
     94   * @returns {Headers}
     95   */
     96  async _buildHeaders(method) {
     97    const headers = new Headers(this._headers);
     98 
     99    if (Resource.SEND_VERSION_INFO) {
    100      headers.append("user-agent", Utils.userAgent);
    101    }
    102 
    103    if (this.authenticator) {
    104      const result = await this.authenticator(this, method);
    105      if (result && result.headers) {
    106        for (const [k, v] of Object.entries(result.headers)) {
    107          headers.append(k.toLowerCase(), v);
    108        }
    109      }
    110    } else {
    111      this._log.debug("No authenticator found.");
    112    }
    113 
    114    // PUT and POST are treated differently because they have payload data.
    115    if (("PUT" == method || "POST" == method) && !headers.has("content-type")) {
    116      headers.append("content-type", "text/plain");
    117    }
    118 
    119    if (this._log.level <= Log.Level.Trace) {
    120      for (const [k, v] of headers) {
    121        if (k == "authorization" || k == "x-client-state") {
    122          this._log.trace(`HTTP Header ${k}: ***** (suppressed)`);
    123        } else {
    124          this._log.trace(`HTTP Header ${k}: ${v}`);
    125        }
    126      }
    127    }
    128 
    129    if (!headers.has("accept")) {
    130      headers.append("accept", "application/json;q=0.9,*/*;q=0.2");
    131    }
    132 
    133    return headers;
    134  },
    135 
    136  /**
    137   * @param {string} method HTTP method
    138   * @param {string} data HTTP body
    139   * @param {object} signal AbortSignal instance
    140   * @returns {Request}
    141   */
    142  async _createRequest(method, data, signal) {
    143    const headers = await this._buildHeaders(method);
    144    const init = {
    145      cache: "no-store", // No cache.
    146      headers,
    147      method,
    148      signal,
    149      mozErrors: true, // Return nsresult error codes instead of a generic
    150      // NetworkError when fetch rejects.
    151    };
    152 
    153    if (data) {
    154      if (!(typeof data == "string" || data instanceof String)) {
    155        data = JSON.stringify(data);
    156      }
    157      this._log.debug(`${method} Length: ${data.length}`);
    158      this._log.trace(`${method} Body: ${data}`);
    159      init.body = data;
    160    }
    161    return new Request(this.uri.spec, init);
    162  },
    163 
    164  /**
    165   * @param {string} method HTTP method
    166   * @param {string} [data] HTTP body
    167   * @returns {Response}
    168   */
    169  async _doRequest(method, data = null) {
    170    const controller = new AbortController();
    171    const request = await this._createRequest(method, data, controller.signal);
    172    const responsePromise = fetch(request); // Rejects on network failure.
    173    let didTimeout = false;
    174    const timeoutId = setTimeout(() => {
    175      didTimeout = true;
    176      this._log.error(
    177        `Request timed out after ${this.ABORT_TIMEOUT}ms. Aborting.`
    178      );
    179      controller.abort();
    180    }, this.ABORT_TIMEOUT);
    181    let response;
    182    try {
    183      response = await responsePromise;
    184    } catch (e) {
    185      this._log.warn(`${method} request to ${this.uri.spec} failed`, e);
    186      if (!didTimeout) {
    187        throw e;
    188      }
    189      throw Components.Exception(
    190        "Request aborted (timeout)",
    191        Cr.NS_ERROR_NET_TIMEOUT
    192      );
    193    } finally {
    194      clearTimeout(timeoutId);
    195    }
    196    return this._processResponse(response, method);
    197  },
    198 
    199  async _processResponse(response, method) {
    200    const data = await response.text();
    201    this._logResponse(response, method, data);
    202    this._processResponseHeaders(response);
    203 
    204    const ret = {
    205      data,
    206      url: response.url,
    207      status: response.status,
    208      success: response.ok,
    209      headers: {},
    210    };
    211    for (const [k, v] of response.headers) {
    212      ret.headers[k] = v;
    213    }
    214 
    215    // Make a lazy getter to convert the json response into an object.
    216    // Note that this can cause a parse error to be thrown far away from the
    217    // actual fetch, so be warned!
    218    ChromeUtils.defineLazyGetter(ret, "obj", () => {
    219      try {
    220        return JSON.parse(ret.data);
    221      } catch (ex) {
    222        this._log.warn("Got exception parsing response body", ex);
    223        // Stringify to avoid possibly printing non-printable characters.
    224        this._log.debug(
    225          "Parse fail: Response body starts",
    226          (ret.data + "").slice(0, 100)
    227        );
    228        throw ex;
    229      }
    230    });
    231 
    232    return ret;
    233  },
    234 
    235  _logResponse(response, method, data) {
    236    const { status, ok: success, url } = response;
    237 
    238    // Log the status of the request.
    239    this._log.debug(
    240      `${method} ${success ? "success" : "fail"} ${status} ${url}`
    241    );
    242 
    243    // Additionally give the full response body when Trace logging.
    244    if (this._log.level <= Log.Level.Trace) {
    245      this._log.trace(`${method} body`, data);
    246    }
    247 
    248    if (!success) {
    249      this._log.warn(
    250        `${method} request to ${url} failed with status ${status}`
    251      );
    252    }
    253  },
    254 
    255  _processResponseHeaders({ headers, ok: success }) {
    256    if (headers.has("x-weave-timestamp")) {
    257      Resource.serverTime = parseFloat(headers.get("x-weave-timestamp"));
    258    }
    259    // This is a server-side safety valve to allow slowing down
    260    // clients without hurting performance.
    261    if (headers.has("x-weave-backoff")) {
    262      let backoff = headers.get("x-weave-backoff");
    263      this._log.debug(`Got X-Weave-Backoff: ${backoff}`);
    264      Observers.notify("weave:service:backoff:interval", parseInt(backoff, 10));
    265    }
    266 
    267    if (success && headers.has("x-weave-quota-remaining")) {
    268      Observers.notify(
    269        "weave:service:quota:remaining",
    270        parseInt(headers.get("x-weave-quota-remaining"), 10)
    271      );
    272    }
    273  },
    274 
    275  get() {
    276    return this._doRequest("GET");
    277  },
    278 
    279  put(data) {
    280    return this._doRequest("PUT", data);
    281  },
    282 
    283  post(data) {
    284    return this._doRequest("POST", data);
    285  },
    286 
    287  delete() {
    288    return this._doRequest("DELETE");
    289  },
    290 };