tor-browser

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

Push.sys.mjs (9544B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineLazyGetter(lazy, "console", () => {
     10  return console.createInstance({
     11    maxLogLevelPref: "dom.push.loglevel",
     12    prefix: "Push",
     13  });
     14 });
     15 
     16 XPCOMUtils.defineLazyServiceGetter(
     17  lazy,
     18  "PushService",
     19  "@mozilla.org/push/Service;1",
     20  Ci.nsIPushService
     21 );
     22 
     23 /**
     24 * The Push component runs in the child process and exposes the Push API
     25 * to the web application. The PushService running in the parent process is the
     26 * one actually performing all operations.
     27 */
     28 export class Push {
     29  constructor() {
     30    lazy.console.debug("Push()");
     31  }
     32 
     33  get contractID() {
     34    return "@mozilla.org/push/PushManager;1";
     35  }
     36 
     37  get classID() {
     38    return Components.ID("{cde1d019-fad8-4044-b141-65fb4fb7a245}");
     39  }
     40 
     41  get QueryInterface() {
     42    return ChromeUtils.generateQI([
     43      "nsIDOMGlobalPropertyInitializer",
     44      "nsISupportsWeakReference",
     45      "nsIObserver",
     46    ]);
     47  }
     48 
     49  init(win) {
     50    lazy.console.debug("init()");
     51 
     52    this._window = win;
     53 
     54    // Get the client principal from the window. This won't be null because the
     55    // service worker should be available when accessing the push manager.
     56    this._principal = win.clientPrincipal;
     57 
     58    if (!this._principal) {
     59      throw new Error(" The client principal of the window is not available");
     60    }
     61 
     62    try {
     63      this._topLevelPrincipal = win.top.document.nodePrincipal;
     64    } catch (error) {
     65      // Accessing the top-level document might fails if cross-origin
     66      this._topLevelPrincipal = undefined;
     67    }
     68  }
     69 
     70  __init(scope) {
     71    this._scope = scope;
     72  }
     73 
     74  askPermission() {
     75    lazy.console.debug("askPermission()");
     76 
     77    let hasValidTransientUserGestureActivation =
     78      this._window.document.hasValidTransientUserGestureActivation;
     79 
     80    return new this._window.Promise((resolve, reject) => {
     81      // Test permission before requesting to support GeckoView:
     82      // * GeckoViewPermissionChild wants to return early when requested without user activation
     83      //   before doing actual permission check:
     84      //   https://searchfox.org/mozilla-central/rev/0ba4632ee85679a1ccaf652df79c971fa7e9b9f7/mobile/android/actors/GeckoViewPermissionChild.sys.mjs#46-56
     85      //   which is partly because:
     86      // * GeckoView test runner has no real permission check but just returns VALUE_ALLOW.
     87      //   https://searchfox.org/mozilla-central/rev/6e5b9a5a1edab13a1b2e2e90944b6e06b4d8149c/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java#108-123
     88      if (this.#testPermission() === Ci.nsIPermissionManager.ALLOW_ACTION) {
     89        resolve();
     90        return;
     91      }
     92 
     93      let permissionDenied = () => {
     94        reject(
     95          new this._window.DOMException(
     96            "User denied permission to use the Push API.",
     97            "NotAllowedError"
     98          )
     99        );
    100      };
    101 
    102      if (
    103        Services.prefs.getBoolPref("dom.push.testing.ignorePermission", false)
    104      ) {
    105        resolve();
    106        return;
    107      }
    108 
    109      this.#requestPermission(
    110        hasValidTransientUserGestureActivation,
    111        resolve,
    112        permissionDenied
    113      );
    114    });
    115  }
    116 
    117  subscribe(options) {
    118    lazy.console.debug("subscribe()", this._scope);
    119 
    120    return this.askPermission().then(
    121      () =>
    122        new this._window.Promise((resolve, reject) => {
    123          let callback = new PushSubscriptionCallback(this, resolve, reject);
    124 
    125          if (!options || options.applicationServerKey === null) {
    126            lazy.PushService.subscribe(this._scope, this._principal, callback);
    127            return;
    128          }
    129 
    130          let keyView = this.#normalizeAppServerKey(
    131            options.applicationServerKey
    132          );
    133          if (keyView.byteLength === 0) {
    134            callback.rejectWithError(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
    135            return;
    136          }
    137          lazy.PushService.subscribeWithKey(
    138            this._scope,
    139            this._principal,
    140            keyView,
    141            callback
    142          );
    143        })
    144    );
    145  }
    146 
    147  #normalizeAppServerKey(appServerKey) {
    148    let key;
    149    if (typeof appServerKey == "string") {
    150      try {
    151        key = Cu.cloneInto(
    152          ChromeUtils.base64URLDecode(appServerKey, {
    153            padding: "reject",
    154          }),
    155          this._window
    156        );
    157      } catch (e) {
    158        throw new this._window.DOMException(
    159          "String contains an invalid character",
    160          "InvalidCharacterError"
    161        );
    162      }
    163    } else if (this._window.ArrayBuffer.isView(appServerKey)) {
    164      key = appServerKey.buffer;
    165    } else {
    166      // `appServerKey` is an array buffer.
    167      key = appServerKey;
    168    }
    169    return new this._window.Uint8Array(key);
    170  }
    171 
    172  getSubscription() {
    173    lazy.console.debug("getSubscription()", this._scope);
    174 
    175    return new this._window.Promise((resolve, reject) => {
    176      let callback = new PushSubscriptionCallback(this, resolve, reject);
    177      lazy.PushService.getSubscription(this._scope, this._principal, callback);
    178    });
    179  }
    180 
    181  permissionState() {
    182    lazy.console.debug("permissionState()", this._scope);
    183 
    184    return new this._window.Promise((resolve, reject) => {
    185      let permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
    186 
    187      try {
    188        permission = this.#testPermission();
    189      } catch (e) {
    190        reject();
    191        return;
    192      }
    193 
    194      let pushPermissionStatus = "prompt";
    195      if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
    196        pushPermissionStatus = "granted";
    197      } else if (permission == Ci.nsIPermissionManager.DENY_ACTION) {
    198        pushPermissionStatus = "denied";
    199      }
    200      resolve(pushPermissionStatus);
    201    });
    202  }
    203 
    204  #testPermission() {
    205    let permission = Services.perms.testExactPermissionFromPrincipal(
    206      this._principal,
    207      "desktop-notification"
    208    );
    209    if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
    210      return permission;
    211    }
    212    try {
    213      if (Services.prefs.getBoolPref("dom.push.testing.ignorePermission")) {
    214        permission = Ci.nsIPermissionManager.ALLOW_ACTION;
    215      }
    216    } catch (e) {}
    217    return permission;
    218  }
    219 
    220  #requestPermission(
    221    hasValidTransientUserGestureActivation,
    222    allowCallback,
    223    cancelCallback
    224  ) {
    225    // Create an array with a single nsIContentPermissionType element.
    226    let type = {
    227      type: "desktop-notification",
    228      options: [],
    229      QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionType"]),
    230    };
    231    let typeArray = Cc["@mozilla.org/array;1"].createInstance(
    232      Ci.nsIMutableArray
    233    );
    234    typeArray.appendElement(type);
    235 
    236    // create a nsIContentPermissionRequest
    237    let request = {
    238      QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionRequest"]),
    239      types: typeArray,
    240      principal: this._principal,
    241      hasValidTransientUserGestureActivation,
    242      topLevelPrincipal: this._topLevelPrincipal,
    243      allow: allowCallback,
    244      cancel: cancelCallback,
    245      window: this._window,
    246    };
    247 
    248    // Using askPermission from nsIDOMWindowUtils that takes care of the
    249    // remoting if needed.
    250    let windowUtils = this._window.windowUtils;
    251    windowUtils.askPermission(request);
    252  }
    253 }
    254 
    255 class PushSubscriptionCallback {
    256  constructor(pushManager, resolve, reject) {
    257    this.pushManager = pushManager;
    258    this.resolve = resolve;
    259    this.reject = reject;
    260  }
    261 
    262  get QueryInterface() {
    263    return ChromeUtils.generateQI(["nsIPushSubscriptionCallback"]);
    264  }
    265 
    266  onPushSubscription(ok, subscription) {
    267    let { pushManager } = this;
    268    if (!Components.isSuccessCode(ok)) {
    269      this.rejectWithError(ok);
    270      return;
    271    }
    272 
    273    if (!subscription) {
    274      this.resolve(null);
    275      return;
    276    }
    277 
    278    let p256dhKey = this.#getKey(subscription, "p256dh");
    279    let authSecret = this.#getKey(subscription, "auth");
    280    let options = {
    281      endpoint: subscription.endpoint,
    282      scope: pushManager._scope,
    283      p256dhKey,
    284      authSecret,
    285    };
    286    let appServerKey = this.#getKey(subscription, "appServer");
    287    if (appServerKey) {
    288      // Avoid passing null keys to work around bug 1256449.
    289      options.appServerKey = appServerKey;
    290    }
    291    let sub = new pushManager._window.PushSubscription(options);
    292    this.resolve(sub);
    293  }
    294 
    295  #getKey(subscription, name) {
    296    let rawKey = Cu.cloneInto(
    297      subscription.getKey(name),
    298      this.pushManager._window
    299    );
    300    if (!rawKey.length) {
    301      return null;
    302    }
    303 
    304    let key = new this.pushManager._window.ArrayBuffer(rawKey.length);
    305    let keyView = new this.pushManager._window.Uint8Array(key);
    306    keyView.set(rawKey);
    307    return key;
    308  }
    309 
    310  rejectWithError(result) {
    311    let error;
    312    switch (result) {
    313      case Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR:
    314        error = new this.pushManager._window.DOMException(
    315          "Invalid raw ECDSA P-256 public key.",
    316          "InvalidAccessError"
    317        );
    318        break;
    319 
    320      case Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR:
    321        error = new this.pushManager._window.DOMException(
    322          "A subscription with a different application server key already exists.",
    323          "InvalidStateError"
    324        );
    325        break;
    326 
    327      default:
    328        error = new this.pushManager._window.DOMException(
    329          "Error retrieving push subscription.",
    330          "AbortError"
    331        );
    332    }
    333    this.reject(error);
    334  }
    335 }