tor-browser

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

mockpushserviceparent.js (5248B)


      1 /* eslint-env mozilla/chrome-script */
      2 
      3 "use strict";
      4 
      5 /**
      6 * Defers one or more callbacks until the next turn of the event loop. Multiple
      7 * callbacks are executed in order.
      8 *
      9 * @param {Function[]} callbacks The callbacks to execute. One callback will be
     10 *  executed per tick.
     11 */
     12 function waterfall(...callbacks) {
     13  callbacks
     14    .reduce(
     15      (promise, callback) =>
     16        promise.then(() => {
     17          callback();
     18        }),
     19      Promise.resolve()
     20    )
     21    .catch(console.error);
     22 }
     23 
     24 /**
     25 * Minimal implementation of a mock WebSocket connect to be used with
     26 * PushService. Forwards and receive messages from the implementation
     27 * that lives in the content process.
     28 */
     29 function MockWebSocketParent(originalURI) {
     30  this._originalURI = originalURI;
     31 }
     32 
     33 MockWebSocketParent.prototype = {
     34  _originalURI: null,
     35 
     36  _listener: null,
     37  _context: null,
     38 
     39  QueryInterface: ChromeUtils.generateQI(["nsIWebSocketChannel"]),
     40 
     41  get originalURI() {
     42    return this._originalURI;
     43  },
     44 
     45  asyncOpen(uri, origin, originAttributes, windowId, listener, context) {
     46    this._listener = listener;
     47    this._context = context;
     48    waterfall(() => this._listener.onStart(this._context));
     49  },
     50 
     51  sendMsg(msg) {
     52    sendAsyncMessage("socket-client-msg", msg);
     53  },
     54 
     55  close() {
     56    waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
     57  },
     58 
     59  serverSendMsg(msg) {
     60    waterfall(
     61      () => this._listener.onMessageAvailable(this._context, msg),
     62      () => this._listener.onAcknowledge(this._context, 0)
     63    );
     64  },
     65 };
     66 
     67 var pushService = Cc["@mozilla.org/push/Service;1"].getService(
     68  Ci.nsIPushService
     69 ).wrappedJSObject;
     70 
     71 var mockSocket;
     72 var serverMsgs = [];
     73 
     74 addMessageListener("socket-setup", function () {
     75  pushService.replaceServiceBackend({
     76    serverURI: "wss://push.example.org/",
     77    makeWebSocket(uri) {
     78      mockSocket = new MockWebSocketParent(uri);
     79      while (serverMsgs.length) {
     80        let msg = serverMsgs.shift();
     81        mockSocket.serverSendMsg(msg);
     82      }
     83      return mockSocket;
     84    },
     85  });
     86 });
     87 
     88 addMessageListener("socket-teardown", function () {
     89  pushService
     90    .restoreServiceBackend()
     91    .then(_ => {
     92      serverMsgs.length = 0;
     93      if (mockSocket) {
     94        mockSocket.close();
     95        mockSocket = null;
     96      }
     97      sendAsyncMessage("socket-server-teardown");
     98    })
     99    .catch(error => {
    100      console.error(`Error restoring service backend: ${error}`);
    101    });
    102 });
    103 
    104 addMessageListener("socket-server-msg", function (msg) {
    105  if (mockSocket) {
    106    mockSocket.serverSendMsg(msg);
    107  } else {
    108    serverMsgs.push(msg);
    109  }
    110 });
    111 
    112 var MockService = {
    113  requestID: 1,
    114  resolvers: new Map(),
    115 
    116  sendRequest(name, params) {
    117    return new Promise((resolve, reject) => {
    118      let id = this.requestID++;
    119      this.resolvers.set(id, { resolve, reject });
    120      sendAsyncMessage("service-request", {
    121        name,
    122        id,
    123        // The request params from the real push service may contain a
    124        // principal, which cannot be passed to the unprivileged
    125        // mochitest scope, and will cause the message to be dropped if
    126        // present. The mochitest scope fortunately does not need the
    127        // principal, though, so set it to null before sending.
    128        params: Object.assign({}, params, { principal: null }),
    129      });
    130    });
    131  },
    132 
    133  handleResponse(response) {
    134    if (!this.resolvers.has(response.id)) {
    135      console.error(`Unexpected response for request ${response.id}`);
    136      return;
    137    }
    138    let resolver = this.resolvers.get(response.id);
    139    this.resolvers.delete(response.id);
    140    if (response.error) {
    141      resolver.reject(response.error);
    142    } else {
    143      resolver.resolve(response.result);
    144    }
    145  },
    146 
    147  init() {},
    148 
    149  register(pageRecord) {
    150    return this.sendRequest("register", pageRecord);
    151  },
    152 
    153  registration(pageRecord) {
    154    return this.sendRequest("registration", pageRecord);
    155  },
    156 
    157  unregister(pageRecord) {
    158    return this.sendRequest("unregister", pageRecord);
    159  },
    160 
    161  reportDeliveryError(messageId, reason) {
    162    sendAsyncMessage("service-delivery-error", {
    163      messageId,
    164      reason,
    165    });
    166  },
    167 
    168  uninit() {
    169    return Promise.resolve();
    170  },
    171 };
    172 
    173 async function replaceService(service) {
    174  // `?.` because `service` can be null
    175  // (either by calling this function with null, or the push module doesn't have the
    176  // field at all e.g. in GeckoView)
    177  // Passing null here resets it to the default implementation on desktop
    178  // (so `.service` never becomes null there) but not for GeckoView.
    179  // XXX(krosylight): we need to remove this deviation.
    180  await pushService.service?.uninit();
    181  pushService.service = service;
    182  await pushService.service?.init();
    183 }
    184 
    185 addMessageListener("service-replace", function () {
    186  replaceService(MockService)
    187    .then(_ => {
    188      sendAsyncMessage("service-replaced");
    189    })
    190    .catch(error => {
    191      console.error(`Error replacing service: ${error}`);
    192    });
    193 });
    194 
    195 addMessageListener("service-restore", function () {
    196  replaceService(null)
    197    .then(_ => {
    198      sendAsyncMessage("service-restored");
    199    })
    200    .catch(error => {
    201      console.error(`Error restoring service: ${error}`);
    202    });
    203 });
    204 
    205 addMessageListener("service-response", function (response) {
    206  MockService.handleResponse(response);
    207 });