GeckoViewPush.sys.mjs (7043B)
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 { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs"; 6 7 const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPush"); 8 9 const lazy = {}; 10 11 ChromeUtils.defineESModuleGetters(lazy, { 12 EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", 13 PushCrypto: "resource://gre/modules/PushCrypto.sys.mjs", 14 }); 15 16 // Observer notification topics for push messages and subscription status 17 // changes. These are duplicated and used in `nsIPushNotifier`. They're exposed 18 // on `nsIPushService` so that JS callers only need to import this service. 19 const OBSERVER_TOPIC_PUSH = "push-message"; 20 const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change"; 21 const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified"; 22 23 function createSubscription({ 24 scope, 25 browserPublicKey, 26 authSecret, 27 endpoint, 28 appServerKey, 29 }) { 30 const decodedBrowserKey = ChromeUtils.base64URLDecode(browserPublicKey, { 31 padding: "ignore", 32 }); 33 const decodedAuthSecret = ChromeUtils.base64URLDecode(authSecret, { 34 padding: "ignore", 35 }); 36 37 return new PushSubscription({ 38 endpoint, 39 scope, 40 p256dhKey: decodedBrowserKey, 41 authenticationSecret: decodedAuthSecret, 42 appServerKey, 43 }); 44 } 45 46 function scopeWithAttrs(scope, attrs) { 47 return scope + ChromeUtils.originAttributesToSuffix(attrs); 48 } 49 50 export class PushService { 51 constructor() { 52 this.wrappedJSObject = this; 53 } 54 55 pushTopic = OBSERVER_TOPIC_PUSH; 56 subscriptionChangeTopic = OBSERVER_TOPIC_SUBSCRIPTION_CHANGE; 57 subscriptionModifiedTopic = OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED; 58 59 // nsIObserver methods 60 61 observe() {} 62 63 // nsIPushService methods 64 65 subscribe(scope, principal, callback) { 66 this.subscribeWithKey(scope, principal, null, callback); 67 } 68 69 async subscribeWithKey(scope, principal, appServerKey, callback) { 70 const keyView = new Uint8Array(appServerKey); 71 72 if (appServerKey != null) { 73 try { 74 await lazy.PushCrypto.validateAppServerKey(keyView); 75 } catch (error) { 76 callback.onPushSubscription(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR, null); 77 return; 78 } 79 } 80 81 try { 82 const response = await lazy.EventDispatcher.instance.sendRequestForResult( 83 { 84 type: "GeckoView:PushSubscribe", 85 scope: scopeWithAttrs(scope, principal.originAttributes), 86 appServerKey: appServerKey 87 ? ChromeUtils.base64URLEncode(keyView, { 88 pad: true, 89 }) 90 : null, 91 } 92 ); 93 94 let subscription = null; 95 if (response) { 96 subscription = createSubscription({ 97 ...response, 98 scope, 99 principal, 100 appServerKey, 101 }); 102 } 103 104 callback.onPushSubscription(Cr.NS_OK, subscription); 105 } catch (e) { 106 callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null); 107 } 108 } 109 110 async unsubscribe(scope, principal, callback) { 111 try { 112 await lazy.EventDispatcher.instance.sendRequestForResult({ 113 type: "GeckoView:PushUnsubscribe", 114 scope: scopeWithAttrs(scope, principal.originAttributes), 115 }); 116 117 callback.onUnsubscribe(Cr.NS_OK, true); 118 } catch (e) { 119 callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false); 120 } 121 } 122 123 async getSubscription(scope, principal, callback) { 124 try { 125 const response = await lazy.EventDispatcher.instance.sendRequestForResult( 126 { 127 type: "GeckoView:PushGetSubscription", 128 scope: scopeWithAttrs(scope, principal.originAttributes), 129 } 130 ); 131 132 let subscription = null; 133 if (response) { 134 subscription = createSubscription({ 135 ...response, 136 scope, 137 principal, 138 }); 139 } 140 141 callback.onPushSubscription(Cr.NS_OK, subscription); 142 } catch (e) { 143 callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null); 144 } 145 } 146 147 clearForDomain(domain, originAttributesPattern, callback) { 148 callback.onClear(Cr.NS_OK); 149 } 150 151 clearForPrincipal(principal, callback) { 152 callback.onClear(Cr.NS_OK); 153 } 154 155 // nsIPushQuotaManager methods 156 157 notificationForOriginShown() {} 158 159 notificationForOriginClosed() {} 160 161 // nsIPushErrorReporter methods 162 163 reportDeliveryError() {} 164 } 165 166 PushService.prototype.classID = Components.ID( 167 "{a54d84d7-98a4-4fec-b664-e42e512ae9cc}" 168 ); 169 PushService.prototype.contractID = "@mozilla.org/push/Service;1"; 170 PushService.prototype.QueryInterface = ChromeUtils.generateQI([ 171 "nsIObserver", 172 "nsISupportsWeakReference", 173 "nsIPushService", 174 "nsIPushQuotaManager", 175 "nsIPushErrorReporter", 176 ]); 177 178 /** `PushSubscription` instances are passed to all subscription callbacks. */ 179 class PushSubscription { 180 constructor(props) { 181 this._props = props; 182 } 183 184 /** The URL for sending messages to this subscription. */ 185 get endpoint() { 186 return this._props.endpoint; 187 } 188 189 /** The last time a message was sent to this subscription. */ 190 get lastPush() { 191 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 192 } 193 194 /** The total number of messages sent to this subscription. */ 195 get pushCount() { 196 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 197 } 198 199 /** 200 * The app will take care of throttling, so we don't 201 * care about the quota stuff here. 202 */ 203 get quota() { 204 return -1; 205 } 206 207 /** 208 * Indicates whether this subscription was created with the system principal. 209 * System subscriptions are exempt from the background message quota and 210 * permission checks. 211 */ 212 get isSystemSubscription() { 213 return false; 214 } 215 216 /** The private key used to decrypt incoming push messages, in JWK format */ 217 get p256dhPrivateKey() { 218 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 219 } 220 221 /** 222 * Indicates whether this subscription is subject to the background message 223 * quota. 224 */ 225 quotaApplies() { 226 return false; 227 } 228 229 /** 230 * Indicates whether this subscription exceeded the background message quota, 231 * or the user revoked the notification permission. The caller must request a 232 * new subscription to continue receiving push messages. 233 */ 234 isExpired() { 235 return false; 236 } 237 238 /** 239 * Returns a key for encrypting messages sent to this subscription. JS 240 * callers receive the key buffer as a return value, while C++ callers 241 * receive the key size and buffer as out parameters. 242 */ 243 getKey(name) { 244 switch (name) { 245 case "p256dh": 246 return this._getRawKey(this._props.p256dhKey); 247 248 case "auth": 249 return this._getRawKey(this._props.authenticationSecret); 250 251 case "appServer": 252 return this._getRawKey(this._props.appServerKey); 253 } 254 return []; 255 } 256 257 _getRawKey(key) { 258 if (!key) { 259 return []; 260 } 261 return new Uint8Array(key); 262 } 263 } 264 265 PushSubscription.prototype.QueryInterface = ChromeUtils.generateQI([ 266 "nsIPushSubscription", 267 ]);