tor-browser

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

head.js (14257B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 var { XPCOMUtils } = ChromeUtils.importESModule(
      7  "resource://gre/modules/XPCOMUtils.sys.mjs"
      8 );
      9 
     10 ChromeUtils.defineESModuleGetters(this, {
     11  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
     12  PermissionTestUtils: "resource://testing-common/PermissionTestUtils.sys.mjs",
     13  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
     14  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     15  Preferences: "resource://gre/modules/Preferences.sys.mjs",
     16  PushCrypto: "resource://gre/modules/PushCrypto.sys.mjs",
     17  PushService: "resource://gre/modules/PushService.sys.mjs",
     18  PushServiceWebSocket: "resource://gre/modules/PushServiceWebSocket.sys.mjs",
     19  pushBroadcastService: "resource://gre/modules/PushBroadcastService.sys.mjs",
     20 });
     21 
     22 var {
     23  clearInterval,
     24  clearTimeout,
     25  setInterval,
     26  setIntervalWithTarget,
     27  setTimeout,
     28  setTimeoutWithTarget,
     29 } = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs");
     30 
     31 XPCOMUtils.defineLazyServiceGetter(
     32  this,
     33  "PushServiceComponent",
     34  "@mozilla.org/push/Service;1",
     35  Ci.nsIPushService
     36 );
     37 
     38 const servicePrefs = new Preferences("dom.push.");
     39 
     40 const WEBSOCKET_CLOSE_GOING_AWAY = 1001;
     41 
     42 const MS_IN_ONE_DAY = 24 * 60 * 60 * 1000;
     43 
     44 var isParent =
     45  Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
     46 
     47 // Stop and clean up after the PushService.
     48 Services.obs.addObserver(function observe(subject, topic) {
     49  Services.obs.removeObserver(observe, topic);
     50  PushService.uninit();
     51  // Occasionally, `profile-change-teardown` and `xpcom-shutdown` will fire
     52  // before the PushService and AlarmService finish writing to IndexedDB. This
     53  // causes spurious errors and crashes, so we spin the event loop to let the
     54  // writes finish.
     55  let done = false;
     56  setTimeout(() => (done = true), 1000);
     57  let thread = Services.tm.mainThread;
     58  while (!done) {
     59    try {
     60      thread.processNextEvent(true);
     61    } catch (e) {
     62      console.error(e);
     63    }
     64  }
     65 }, "profile-change-net-teardown");
     66 
     67 /**
     68 * Gates a function so that it is called only after the wrapper is called a
     69 * given number of times.
     70 *
     71 * @param {number} times The number of wrapper calls before |func| is called.
     72 * @param {Function} func The function to gate.
     73 * @returns {Function} The gated function wrapper.
     74 */
     75 function after(times, func) {
     76  return function afterFunc() {
     77    if (--times <= 0) {
     78      func.apply(this, arguments);
     79    }
     80  };
     81 }
     82 
     83 /**
     84 * Defers one or more callbacks until the next turn of the event loop. Multiple
     85 * callbacks are executed in order.
     86 *
     87 * @param {Function[]} callbacks The callbacks to execute. One callback will be
     88 *  executed per tick.
     89 */
     90 function waterfall(...callbacks) {
     91  callbacks
     92    .reduce(
     93      (promise, callback) =>
     94        promise.then(() => {
     95          callback();
     96        }),
     97      Promise.resolve()
     98    )
     99    .catch(console.error);
    100 }
    101 
    102 /**
    103 * Waits for an observer notification to fire.
    104 *
    105 * @param {string} topic The notification topic.
    106 * @returns {Promise} A promise that fulfills when the notification is fired.
    107 */
    108 function promiseObserverNotification(topic, matchFunc) {
    109  return new Promise(resolve => {
    110    Services.obs.addObserver(function observe(subject, aTopic, data) {
    111      let matches = typeof matchFunc != "function" || matchFunc(subject, data);
    112      if (!matches) {
    113        return;
    114      }
    115      Services.obs.removeObserver(observe, aTopic);
    116      resolve({ subject, data });
    117    }, topic);
    118  });
    119 }
    120 
    121 /**
    122 * Wraps an object in a proxy that traps property gets and returns stubs. If
    123 * the stub is a function, the original value will be passed as the first
    124 * argument. If the original value is a function, the proxy returns a wrapper
    125 * that calls the stub; otherwise, the stub is called as a getter.
    126 *
    127 * @param {object} target The object to wrap.
    128 * @param {object} stubs An object containing stubbed values and functions.
    129 * @returns {Proxy} A proxy that returns stubs for property gets.
    130 */
    131 function makeStub(target, stubs) {
    132  return new Proxy(target, {
    133    get(aTarget, property) {
    134      if (!stubs || typeof stubs != "object" || !(property in stubs)) {
    135        return aTarget[property];
    136      }
    137      let stub = stubs[property];
    138      if (typeof stub != "function") {
    139        return stub;
    140      }
    141      let original = aTarget[property];
    142      if (typeof original != "function") {
    143        return stub.call(this, original);
    144      }
    145      return function callStub(...params) {
    146        return stub.call(this, original, ...params);
    147      };
    148    },
    149  });
    150 }
    151 
    152 /**
    153 * Sets default PushService preferences. All pref names are prefixed with
    154 * `dom.push.`; any additional preferences will override the defaults.
    155 *
    156 * @param {object} [prefs] Additional preferences to set.
    157 */
    158 function setPrefs(prefs = {}) {
    159  let defaultPrefs = Object.assign(
    160    {
    161      loglevel: "all",
    162      serverURL: "wss://push.example.org",
    163      "connection.enabled": true,
    164      userAgentID: "",
    165      enabled: true,
    166      // Defaults taken from /modules/libpref/init/all.js.
    167      requestTimeout: 10000,
    168      retryBaseInterval: 5000,
    169      pingInterval: 30 * 60 * 1000,
    170      // Misc. defaults.
    171      maxQuotaPerSubscription: 16,
    172      quotaUpdateDelay: 3000,
    173      "testing.notifyWorkers": false,
    174    },
    175    prefs
    176  );
    177  for (let pref in defaultPrefs) {
    178    servicePrefs.set(pref, defaultPrefs[pref]);
    179  }
    180 }
    181 
    182 function compareAscending(a, b) {
    183  if (a > b) {
    184    return 1;
    185  }
    186  return a < b ? -1 : 0;
    187 }
    188 
    189 /**
    190 * Creates a mock WebSocket object that implements a subset of the
    191 * nsIWebSocketChannel interface used by the PushService.
    192 *
    193 * The given protocol handlers are invoked for each Simple Push command sent
    194 * by the PushService. The ping handler is optional; all others will throw if
    195 * the PushService sends a command for which no handler is registered.
    196 *
    197 * All nsIWebSocketListener methods will be called asynchronously.
    198 * serverSendMsg() and serverClose() can be used to respond to client messages
    199 * and close the "server" end of the connection, respectively.
    200 *
    201 * @param {nsIURI} originalURI The original WebSocket URL.
    202 * @param {Function} options.onHello The "hello" handshake command handler.
    203 * @param {Function} options.onRegister The "register" command handler.
    204 * @param {Function} options.onUnregister The "unregister" command handler.
    205 * @param {Function} options.onACK The "ack" command handler.
    206 * @param {Function} [options.onPing] An optional ping handler.
    207 */
    208 function MockWebSocket(originalURI, handlers = {}) {
    209  this._originalURI = originalURI;
    210  this._onHello = handlers.onHello;
    211  this._onRegister = handlers.onRegister;
    212  this._onUnregister = handlers.onUnregister;
    213  this._onACK = handlers.onACK;
    214  this._onPing = handlers.onPing;
    215  this._onBroadcastSubscribe = handlers.onBroadcastSubscribe;
    216 }
    217 
    218 MockWebSocket.prototype = {
    219  _originalURI: null,
    220  _onHello: null,
    221  _onRegister: null,
    222  _onUnregister: null,
    223  _onACK: null,
    224  _onPing: null,
    225 
    226  _listener: null,
    227  _context: null,
    228 
    229  QueryInterface: ChromeUtils.generateQI(["nsIWebSocketChannel"]),
    230 
    231  get originalURI() {
    232    return this._originalURI;
    233  },
    234 
    235  asyncOpen(uri, origin, originAttributes, windowId, listener, context) {
    236    this._listener = listener;
    237    this._context = context;
    238    waterfall(() => this._listener.onStart(this._context));
    239  },
    240 
    241  _handleMessage(msg) {
    242    let messageType, request;
    243    if (msg == "{}") {
    244      request = {};
    245      messageType = "ping";
    246    } else {
    247      request = JSON.parse(msg);
    248      messageType = request.messageType;
    249    }
    250    switch (messageType) {
    251      case "hello":
    252        if (typeof this._onHello != "function") {
    253          throw new Error("Unexpected handshake request");
    254        }
    255        this._onHello(request);
    256        break;
    257 
    258      case "register":
    259        if (typeof this._onRegister != "function") {
    260          throw new Error("Unexpected register request");
    261        }
    262        this._onRegister(request);
    263        break;
    264 
    265      case "unregister":
    266        if (typeof this._onUnregister != "function") {
    267          throw new Error("Unexpected unregister request");
    268        }
    269        this._onUnregister(request);
    270        break;
    271 
    272      case "ack":
    273        if (typeof this._onACK != "function") {
    274          throw new Error("Unexpected acknowledgement");
    275        }
    276        this._onACK(request);
    277        break;
    278 
    279      case "ping":
    280        if (typeof this._onPing == "function") {
    281          this._onPing(request);
    282        } else {
    283          // Echo ping packets.
    284          this.serverSendMsg("{}");
    285        }
    286        break;
    287 
    288      case "broadcast_subscribe":
    289        if (typeof this._onBroadcastSubscribe != "function") {
    290          throw new Error("Unexpected broadcast_subscribe");
    291        }
    292        this._onBroadcastSubscribe(request);
    293        break;
    294 
    295      default:
    296        throw new Error("Unexpected message: " + messageType);
    297    }
    298  },
    299 
    300  sendMsg(msg) {
    301    this._handleMessage(msg);
    302  },
    303 
    304  close() {
    305    waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
    306  },
    307 
    308  /**
    309   * Responds with the given message, calling onMessageAvailable() and
    310   * onAcknowledge() synchronously. Throws if the message is not a string.
    311   * Used by the tests to respond to client commands.
    312   *
    313   * @param {string} msg The message to send to the client.
    314   */
    315  serverSendMsg(msg) {
    316    if (typeof msg != "string") {
    317      throw new Error("Invalid response message");
    318    }
    319    waterfall(
    320      () => this._listener.onMessageAvailable(this._context, msg),
    321      () => this._listener.onAcknowledge(this._context, 0)
    322    );
    323  },
    324 
    325  /**
    326   * Closes the server end of the connection, calling onServerClose()
    327   * followed by onStop(). Used to test abrupt connection termination.
    328   *
    329   * @param {number} [statusCode] The WebSocket connection close code.
    330   * @param {string} [reason] The connection close reason.
    331   */
    332  serverClose(statusCode, reason = "") {
    333    if (!isFinite(statusCode)) {
    334      statusCode = WEBSOCKET_CLOSE_GOING_AWAY;
    335    }
    336    waterfall(
    337      () => this._listener.onServerClose(this._context, statusCode, reason),
    338      () => this._listener.onStop(this._context, Cr.NS_BASE_STREAM_CLOSED)
    339    );
    340  },
    341 
    342  serverInterrupt(result = Cr.NS_ERROR_NET_RESET) {
    343    waterfall(() => this._listener.onStop(this._context, result));
    344  },
    345 };
    346 
    347 var setUpServiceInParent = async function (service, db) {
    348  if (!isParent) {
    349    return;
    350  }
    351 
    352  let userAgentID = "ce704e41-cb77-4206-b07b-5bf47114791b";
    353  setPrefs({
    354    userAgentID,
    355  });
    356 
    357  await db.put({
    358    channelID: "6e2814e1-5f84-489e-b542-855cc1311f09",
    359    pushEndpoint: "https://example.org/push/get",
    360    scope: "https://example.com/get/ok",
    361    originAttributes: "",
    362    version: 1,
    363    pushCount: 10,
    364    lastPush: 1438360548322,
    365    quota: 16,
    366  });
    367  await db.put({
    368    channelID: "3a414737-2fd0-44c0-af05-7efc172475fc",
    369    pushEndpoint: "https://example.org/push/unsub",
    370    scope: "https://example.com/unsub/ok",
    371    originAttributes: "",
    372    version: 2,
    373    pushCount: 10,
    374    lastPush: 1438360848322,
    375    quota: 4,
    376  });
    377  await db.put({
    378    channelID: "ca3054e8-b59b-4ea0-9c23-4a3c518f3161",
    379    pushEndpoint: "https://example.org/push/stale",
    380    scope: "https://example.com/unsub/fail",
    381    originAttributes: "",
    382    version: 3,
    383    pushCount: 10,
    384    lastPush: 1438362348322,
    385    quota: 1,
    386  });
    387 
    388  service.init({
    389    serverURI: "wss://push.example.org/",
    390    db: makeStub(db, {
    391      put(prev, record) {
    392        if (record.scope == "https://example.com/sub/fail") {
    393          return Promise.reject("synergies not aligned");
    394        }
    395        return prev.call(this, record);
    396      },
    397      delete(prev, channelID) {
    398        if (channelID == "ca3054e8-b59b-4ea0-9c23-4a3c518f3161") {
    399          return Promise.reject("splines not reticulated");
    400        }
    401        return prev.call(this, channelID);
    402      },
    403      getByIdentifiers(prev, identifiers) {
    404        if (identifiers.scope == "https://example.com/get/fail") {
    405          return Promise.reject("qualia unsynchronized");
    406        }
    407        return prev.call(this, identifiers);
    408      },
    409    }),
    410    makeWebSocket(uri) {
    411      return new MockWebSocket(uri, {
    412        onHello() {
    413          this.serverSendMsg(
    414            JSON.stringify({
    415              messageType: "hello",
    416              uaid: userAgentID,
    417              status: 200,
    418            })
    419          );
    420        },
    421        onRegister(request) {
    422          if (request.key) {
    423            let appServerKey = new Uint8Array(
    424              ChromeUtils.base64URLDecode(request.key, {
    425                padding: "require",
    426              })
    427            );
    428            equal(appServerKey.length, 65, "Wrong app server key length");
    429            equal(appServerKey[0], 4, "Wrong app server key format");
    430          }
    431          this.serverSendMsg(
    432            JSON.stringify({
    433              messageType: "register",
    434              uaid: userAgentID,
    435              channelID: request.channelID,
    436              status: 200,
    437              pushEndpoint: "https://example.org/push/" + request.channelID,
    438            })
    439          );
    440        },
    441        onUnregister(request) {
    442          this.serverSendMsg(
    443            JSON.stringify({
    444              messageType: "unregister",
    445              channelID: request.channelID,
    446              status: 200,
    447            })
    448          );
    449        },
    450      });
    451    },
    452  });
    453 };
    454 
    455 var tearDownServiceInParent = async function (db) {
    456  if (!isParent) {
    457    return;
    458  }
    459 
    460  let record = await db.getByIdentifiers({
    461    scope: "https://example.com/sub/ok",
    462    originAttributes: "",
    463  });
    464  ok(
    465    record.pushEndpoint.startsWith("https://example.org/push"),
    466    "Wrong push endpoint in subscription record"
    467  );
    468 
    469  record = await db.getByKeyID("3a414737-2fd0-44c0-af05-7efc172475fc");
    470  ok(!record, "Unsubscribed record should not exist");
    471 };
    472 
    473 function putTestRecord(db, keyID, scope, quota) {
    474  return db.put({
    475    channelID: keyID,
    476    pushEndpoint: "https://example.org/push/" + keyID,
    477    scope,
    478    pushCount: 0,
    479    lastPush: 0,
    480    version: null,
    481    originAttributes: "",
    482    quota,
    483    systemRecord: quota == Infinity,
    484  });
    485 }
    486 
    487 function getAllKeyIDs(db) {
    488  return db
    489    .getAllKeyIDs()
    490    .then(records =>
    491      records.map(record => record.keyID).sort(compareAscending)
    492    );
    493 }