tor-browser

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

PushComponents.sys.mjs (12969B)


      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 /**
      6 * This file exports XPCOM components for C++ and chrome JavaScript callers to
      7 * interact with the Push service.
      8 */
      9 
     10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     11 import { ChromePushSubscription } from "./ChromePushSubscription.sys.mjs";
     12 
     13 var isParent =
     14  Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
     15 
     16 const lazy = {};
     17 
     18 // The default Push service implementation.
     19 ChromeUtils.defineLazyGetter(lazy, "PushService", function () {
     20  if (Services.prefs.getBoolPref("dom.push.enabled")) {
     21    const { PushService } = ChromeUtils.importESModule(
     22      "resource://gre/modules/PushService.sys.mjs"
     23    );
     24    PushService.init();
     25    return PushService;
     26  }
     27 
     28  throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
     29 });
     30 
     31 // Observer notification topics for push messages and subscription status
     32 // changes. These are duplicated and used in `nsIPushNotifier`. They're exposed
     33 // on `nsIPushService` so that JS callers only need to import this service.
     34 const OBSERVER_TOPIC_PUSH = "push-message";
     35 const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
     36 const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified";
     37 
     38 /**
     39 * `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively
     40 * implement the `nsIPushService` interface. This interface provides calls
     41 * similar to the Push DOM API, but does not require service workers.
     42 *
     43 * Push service methods may be called from the parent or content process. The
     44 * parent process implementation loads `PushService.sys.mjs` at app startup, and
     45 * calls its methods directly. The content implementation forwards calls to
     46 * the parent Push service via IPC.
     47 *
     48 * The implementations share a class and contract ID.
     49 */
     50 function PushServiceBase() {
     51  this.wrappedJSObject = this;
     52  this._addListeners();
     53 }
     54 
     55 PushServiceBase.prototype = {
     56  classID: Components.ID("{daaa8d73-677e-4233-8acd-2c404bd01658}"),
     57  contractID: "@mozilla.org/push/Service;1",
     58  QueryInterface: ChromeUtils.generateQI([
     59    "nsIObserver",
     60    "nsISupportsWeakReference",
     61    "nsIPushService",
     62    "nsIPushQuotaManager",
     63    "nsIPushErrorReporter",
     64  ]),
     65 
     66  pushTopic: OBSERVER_TOPIC_PUSH,
     67  subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
     68  subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
     69 
     70  ensureReady() {},
     71 
     72  _addListeners() {
     73    for (let message of this._messages) {
     74      this._mm.addMessageListener(message, this);
     75    }
     76  },
     77 
     78  _isValidMessage(message) {
     79    return this._messages.includes(message.name);
     80  },
     81 
     82  observe(subject, topic) {
     83    if (topic === "android-push-service") {
     84      // Load PushService immediately.
     85      this.ensureReady();
     86    }
     87  },
     88 
     89  _deliverSubscription(request, props) {
     90    if (!props) {
     91      request.onPushSubscription(Cr.NS_OK, null);
     92      return;
     93    }
     94    request.onPushSubscription(Cr.NS_OK, new ChromePushSubscription(props));
     95  },
     96 
     97  _deliverSubscriptionError(request, error) {
     98    let result =
     99      typeof error.result == "number" ? error.result : Cr.NS_ERROR_FAILURE;
    100    request.onPushSubscription(result, null);
    101  },
    102 };
    103 
    104 /**
    105 * The parent process implementation of `nsIPushService`. This version loads
    106 * `PushService.sys.mjs` at startup and calls its methods directly. It also
    107 * receives and responds to requests from the content process.
    108 */
    109 let parentInstance;
    110 function PushServiceParent() {
    111  if (parentInstance) {
    112    return parentInstance;
    113  }
    114  parentInstance = this;
    115 
    116  PushServiceBase.call(this);
    117 }
    118 
    119 PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
    120 
    121 XPCOMUtils.defineLazyServiceGetter(
    122  PushServiceParent.prototype,
    123  "_mm",
    124  "@mozilla.org/parentprocessmessagemanager;1",
    125  Ci.nsISupports
    126 );
    127 
    128 Object.assign(PushServiceParent.prototype, {
    129  _messages: [
    130    "Push:Register",
    131    "Push:Registration",
    132    "Push:Unregister",
    133    "Push:Clear",
    134    "Push:ReportError",
    135  ],
    136 
    137  // nsIPushService methods
    138 
    139  subscribe(scope, principal, callback) {
    140    this.subscribeWithKey(scope, principal, [], callback);
    141  },
    142 
    143  subscribeWithKey(scope, principal, key, callback) {
    144    this._handleRequest("Push:Register", principal, {
    145      scope,
    146      appServerKey: key,
    147    })
    148      .then(
    149        result => {
    150          this._deliverSubscription(callback, result);
    151        },
    152        error => {
    153          this._deliverSubscriptionError(callback, error);
    154        }
    155      )
    156      .catch(console.error);
    157  },
    158 
    159  unsubscribe(scope, principal, callback) {
    160    this._handleRequest("Push:Unregister", principal, {
    161      scope,
    162    })
    163      .then(
    164        result => {
    165          callback.onUnsubscribe(Cr.NS_OK, result);
    166        },
    167        () => {
    168          callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
    169        }
    170      )
    171      .catch(console.error);
    172  },
    173 
    174  getSubscription(scope, principal, callback) {
    175    return this._handleRequest("Push:Registration", principal, {
    176      scope,
    177    })
    178      .then(
    179        result => {
    180          this._deliverSubscription(callback, result);
    181        },
    182        error => {
    183          this._deliverSubscriptionError(callback, error);
    184        }
    185      )
    186      .catch(console.error);
    187  },
    188 
    189  clearForDomain(domain, originAttributesPattern, callback) {
    190    return this._handleRequest("Push:Clear", null, {
    191      domain,
    192      originAttributesPattern,
    193    })
    194      .then(
    195        () => {
    196          callback.onClear(Cr.NS_OK);
    197        },
    198        () => {
    199          callback.onClear(Cr.NS_ERROR_FAILURE);
    200        }
    201      )
    202      .catch(console.error);
    203  },
    204 
    205  clearForPrincipal(principal, callback) {
    206    return this._handleRequest("Push:Clear", null, {
    207      principal,
    208    })
    209      .then(
    210        () => {
    211          callback.onClear(Cr.NS_OK);
    212        },
    213        () => {
    214          callback.onClear(Cr.NS_ERROR_FAILURE);
    215        }
    216      )
    217      .catch(console.error);
    218  },
    219 
    220  // nsIPushQuotaManager methods
    221 
    222  notificationForOriginShown(origin) {
    223    this.service.notificationForOriginShown(origin);
    224  },
    225 
    226  notificationForOriginClosed(origin) {
    227    this.service.notificationForOriginClosed(origin);
    228  },
    229 
    230  // nsIPushErrorReporter methods
    231 
    232  reportDeliveryError(messageId, reason) {
    233    this.service.reportDeliveryError(messageId, reason);
    234  },
    235 
    236  receiveMessage(message) {
    237    if (!this._isValidMessage(message)) {
    238      return;
    239    }
    240    let { name, target, data } = message;
    241    if (name === "Push:ReportError") {
    242      this.reportDeliveryError(data.messageId, data.reason);
    243      return;
    244    }
    245    this._handleRequest(name, data.principal, data)
    246      .then(
    247        result => {
    248          target.sendAsyncMessage(this._getResponseName(name, "OK"), {
    249            requestID: data.requestID,
    250            result,
    251          });
    252        },
    253        error => {
    254          target.sendAsyncMessage(this._getResponseName(name, "KO"), {
    255            requestID: data.requestID,
    256            result: error.result,
    257          });
    258        }
    259      )
    260      .catch(console.error);
    261  },
    262 
    263  ensureReady() {
    264    this.service.init();
    265  },
    266 
    267  _toPageRecord(principal, data) {
    268    if (!data.scope) {
    269      throw new Error("Invalid page record: missing scope");
    270    }
    271    if (!principal) {
    272      throw new Error("Invalid page record: missing principal");
    273    }
    274    if (principal.isNullPrincipal || principal.isExpandedPrincipal) {
    275      throw new Error("Invalid page record: unsupported principal");
    276    }
    277 
    278    // System subscriptions can only be created by chrome callers, and are
    279    // exempt from the background message quota and permission checks. They
    280    // also do not fire service worker events.
    281    data.systemRecord = principal.isSystemPrincipal;
    282 
    283    data.originAttributes = ChromeUtils.originAttributesToSuffix(
    284      principal.originAttributes
    285    );
    286 
    287    return data;
    288  },
    289 
    290  async _handleRequest(name, principal, data) {
    291    if (name == "Push:Clear") {
    292      return this.service.clear(data);
    293    }
    294 
    295    let pageRecord;
    296    try {
    297      pageRecord = this._toPageRecord(principal, data);
    298    } catch (e) {
    299      return Promise.reject(e);
    300    }
    301 
    302    if (name === "Push:Register") {
    303      return this.service.register(pageRecord);
    304    }
    305    if (name === "Push:Registration") {
    306      return this.service.registration(pageRecord);
    307    }
    308    if (name === "Push:Unregister") {
    309      return this.service.unregister(pageRecord);
    310    }
    311 
    312    return Promise.reject(new Error("Invalid request: unknown name"));
    313  },
    314 
    315  _getResponseName(requestName, suffix) {
    316    let name = requestName.slice("Push:".length);
    317    return "PushService:" + name + ":" + suffix;
    318  },
    319 
    320  // Methods used for mocking in tests.
    321 
    322  replaceServiceBackend(options) {
    323    return this.service.changeTestServer(options.serverURI, options);
    324  },
    325 
    326  restoreServiceBackend() {
    327    var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
    328    return this.service.changeTestServer(defaultServerURL);
    329  },
    330 });
    331 
    332 // Used to replace the implementation with a mock.
    333 Object.defineProperty(PushServiceParent.prototype, "service", {
    334  get() {
    335    return this._service || lazy.PushService;
    336  },
    337  set(impl) {
    338    this._service = impl;
    339  },
    340 });
    341 
    342 let contentInstance;
    343 /**
    344 * The content process implementation of `nsIPushService`. This version
    345 * uses the child message manager to forward calls to the parent process.
    346 * The parent Push service instance handles the request, and responds with a
    347 * message containing the result.
    348 */
    349 function PushServiceContent() {
    350  if (contentInstance) {
    351    return contentInstance;
    352  }
    353  contentInstance = this;
    354 
    355  PushServiceBase.apply(this, arguments);
    356  this._requests = new Map();
    357  this._requestId = 0;
    358 }
    359 
    360 PushServiceContent.prototype = Object.create(PushServiceBase.prototype);
    361 
    362 XPCOMUtils.defineLazyServiceGetter(
    363  PushServiceContent.prototype,
    364  "_mm",
    365  "@mozilla.org/childprocessmessagemanager;1",
    366  Ci.nsISupports
    367 );
    368 
    369 Object.assign(PushServiceContent.prototype, {
    370  _messages: [
    371    "PushService:Register:OK",
    372    "PushService:Register:KO",
    373    "PushService:Registration:OK",
    374    "PushService:Registration:KO",
    375    "PushService:Unregister:OK",
    376    "PushService:Unregister:KO",
    377    "PushService:Clear:OK",
    378    "PushService:Clear:KO",
    379  ],
    380 
    381  // nsIPushService methods
    382 
    383  subscribe(scope, principal, callback) {
    384    this.subscribeWithKey(scope, principal, [], callback);
    385  },
    386 
    387  subscribeWithKey(scope, principal, key, callback) {
    388    let requestID = this._addRequest(callback);
    389    this._mm.sendAsyncMessage("Push:Register", {
    390      scope,
    391      appServerKey: key,
    392      requestID,
    393      principal,
    394    });
    395  },
    396 
    397  unsubscribe(scope, principal, callback) {
    398    let requestID = this._addRequest(callback);
    399    this._mm.sendAsyncMessage("Push:Unregister", {
    400      scope,
    401      requestID,
    402      principal,
    403    });
    404  },
    405 
    406  getSubscription(scope, principal, callback) {
    407    let requestID = this._addRequest(callback);
    408    this._mm.sendAsyncMessage("Push:Registration", {
    409      scope,
    410      requestID,
    411      principal,
    412    });
    413  },
    414 
    415  clearForDomain(domain, callback) {
    416    let requestID = this._addRequest(callback);
    417    this._mm.sendAsyncMessage("Push:Clear", {
    418      domain,
    419      requestID,
    420    });
    421  },
    422 
    423  // nsIPushErrorReporter methods
    424 
    425  reportDeliveryError(messageId, reason) {
    426    this._mm.sendAsyncMessage("Push:ReportError", {
    427      messageId,
    428      reason,
    429    });
    430  },
    431 
    432  _addRequest(data) {
    433    let id = ++this._requestId;
    434    this._requests.set(id, data);
    435    return id;
    436  },
    437 
    438  _takeRequest(requestId) {
    439    let d = this._requests.get(requestId);
    440    this._requests.delete(requestId);
    441    return d;
    442  },
    443 
    444  receiveMessage(message) {
    445    if (!this._isValidMessage(message)) {
    446      return;
    447    }
    448    let { name, data } = message;
    449    let request = this._takeRequest(data.requestID);
    450 
    451    if (!request) {
    452      return;
    453    }
    454 
    455    switch (name) {
    456      case "PushService:Register:OK":
    457      case "PushService:Registration:OK":
    458        this._deliverSubscription(request, data.result);
    459        break;
    460 
    461      case "PushService:Register:KO":
    462      case "PushService:Registration:KO":
    463        this._deliverSubscriptionError(request, data);
    464        break;
    465 
    466      case "PushService:Unregister:OK":
    467        if (typeof data.result === "boolean") {
    468          request.onUnsubscribe(Cr.NS_OK, data.result);
    469        } else {
    470          request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
    471        }
    472        break;
    473 
    474      case "PushService:Unregister:KO":
    475        request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
    476        break;
    477 
    478      case "PushService:Clear:OK":
    479        request.onClear(Cr.NS_OK);
    480        break;
    481 
    482      case "PushService:Clear:KO":
    483        request.onClear(Cr.NS_ERROR_FAILURE);
    484        break;
    485 
    486      default:
    487        break;
    488    }
    489  },
    490 });
    491 
    492 // Export the correct implementation depending on whether we're running in
    493 // the parent or content process.
    494 export let Service = isParent ? PushServiceParent : PushServiceContent;