FxAccountsPush.sys.mjs (10009B)
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 import { Async } from "resource://services-common/async.sys.mjs"; 6 7 import { 8 FXA_PUSH_SCOPE_ACCOUNT_UPDATE, 9 ONLOGOUT_NOTIFICATION, 10 ON_ACCOUNT_DESTROYED_NOTIFICATION, 11 ON_COLLECTION_CHANGED_NOTIFICATION, 12 ON_COMMAND_RECEIVED_NOTIFICATION, 13 ON_DEVICE_CONNECTED_NOTIFICATION, 14 ON_DEVICE_DISCONNECTED_NOTIFICATION, 15 ON_PASSWORD_CHANGED_NOTIFICATION, 16 ON_PASSWORD_RESET_NOTIFICATION, 17 ON_PROFILE_CHANGE_NOTIFICATION, 18 ON_PROFILE_UPDATED_NOTIFICATION, 19 ON_VERIFY_LOGIN_NOTIFICATION, 20 log, 21 } from "resource://gre/modules/FxAccountsCommon.sys.mjs"; 22 23 /** 24 * FxAccountsPushService manages Push notifications for Firefox Accounts in the browser 25 * 26 * @param [options] 27 * Object, custom options that used for testing 28 * @class 29 */ 30 export function FxAccountsPushService(options = {}) { 31 this.log = log; 32 33 if (options.log) { 34 // allow custom log for testing purposes 35 this.log = options.log; 36 } 37 38 this.log.debug("FxAccountsPush loading service"); 39 this.wrappedJSObject = this; 40 this.initialize(options); 41 } 42 43 FxAccountsPushService.prototype = { 44 /** 45 * Helps only initialize observers once. 46 */ 47 _initialized: false, 48 /** 49 * Instance of the nsIPushService or a mocked object. 50 */ 51 pushService: null, 52 /** 53 * Instance of FxAccountsInternal or a mocked object. 54 */ 55 fxai: null, 56 /** 57 * Component ID of this service, helps register this component. 58 */ 59 classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"), 60 /** 61 * Register used interfaces in this service 62 */ 63 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 64 /** 65 * Initialize the service and register all the required observers. 66 * 67 * @param [options] 68 */ 69 initialize(options) { 70 if (this._initialized) { 71 return false; 72 } 73 74 this._initialized = true; 75 76 if (options.pushService) { 77 this.pushService = options.pushService; 78 } else { 79 this.pushService = Cc["@mozilla.org/push/Service;1"].getService( 80 Ci.nsIPushService 81 ); 82 } 83 84 if (options.fxai) { 85 this.fxai = options.fxai; 86 } else { 87 const { getFxAccountsSingleton } = ChromeUtils.importESModule( 88 "resource://gre/modules/FxAccounts.sys.mjs" 89 ); 90 const fxAccounts = getFxAccountsSingleton(); 91 this.fxai = fxAccounts._internal; 92 } 93 94 this.asyncObserver = Async.asyncObserver(this, this.log); 95 // We use an async observer because a device waking up can 96 // observe multiple "Send Tab received" push notifications at the same time. 97 // The way these notifications are handled is as follows: 98 // Read index from storage, make network request, update the index. 99 // You can imagine what happens when multiple calls race: we load 100 // the same index multiple times and receive the same exact tabs, multiple times. 101 // The async observer will ensure we make these network requests serially. 102 Services.obs.addObserver(this.asyncObserver, this.pushService.pushTopic); 103 Services.obs.addObserver( 104 this.asyncObserver, 105 this.pushService.subscriptionChangeTopic 106 ); 107 Services.obs.addObserver(this.asyncObserver, ONLOGOUT_NOTIFICATION); 108 109 this.log.debug("FxAccountsPush initialized"); 110 return true; 111 }, 112 /** 113 * Registers a new endpoint with the Push Server 114 * 115 * @returns {Promise} 116 * Promise always resolves with a subscription or a null if failed to subscribe. 117 */ 118 registerPushEndpoint() { 119 this.log.trace("FxAccountsPush registerPushEndpoint"); 120 121 return new Promise(resolve => { 122 this.pushService.subscribe( 123 FXA_PUSH_SCOPE_ACCOUNT_UPDATE, 124 Services.scriptSecurityManager.getSystemPrincipal(), 125 (result, subscription) => { 126 if (Components.isSuccessCode(result)) { 127 this.log.debug("FxAccountsPush got subscription"); 128 resolve(subscription); 129 } else { 130 this.log.warn("FxAccountsPush failed to subscribe", result); 131 resolve(null); 132 } 133 } 134 ); 135 }); 136 }, 137 /** 138 * Async observer interface to listen to push messages, changes and logout. 139 * 140 * @param subject 141 * @param topic 142 * @param data 143 * @returns {Promise} 144 */ 145 async observe(subject, topic, data) { 146 try { 147 this.log.trace( 148 `observed topic=${topic}, data=${data}, subject=${subject}` 149 ); 150 switch (topic) { 151 case this.pushService.pushTopic: 152 if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) { 153 let message = subject.QueryInterface(Ci.nsIPushMessage); 154 await this._onPushMessage(message); 155 } 156 break; 157 case this.pushService.subscriptionChangeTopic: 158 if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) { 159 await this._onPushSubscriptionChange(); 160 } 161 break; 162 case ONLOGOUT_NOTIFICATION: 163 // user signed out, we need to stop polling the Push Server 164 await this.unsubscribe(); 165 break; 166 } 167 } catch (err) { 168 this.log.error(err); 169 } 170 }, 171 172 /** 173 * Fired when the Push server sends a notification. 174 * 175 * @private 176 * @returns {Promise} 177 */ 178 async _onPushMessage(message) { 179 this.log.trace("FxAccountsPushService _onPushMessage"); 180 if (!message.data) { 181 // Use the empty signal to check the verification state of the account right away 182 this.log.debug( 183 "empty push message, but oauth doesn't require checking account status - ignoring" 184 ); 185 return; 186 } 187 let payload = message.data.json(); 188 this.log.debug(`push command: ${payload.command}`); 189 switch (payload.command) { 190 case ON_COMMAND_RECEIVED_NOTIFICATION: 191 await this.fxai.commands.pollDeviceCommands(payload.data.index); 192 break; 193 case ON_DEVICE_CONNECTED_NOTIFICATION: 194 Services.obs.notifyObservers( 195 null, 196 ON_DEVICE_CONNECTED_NOTIFICATION, 197 payload.data.deviceName 198 ); 199 break; 200 case ON_DEVICE_DISCONNECTED_NOTIFICATION: 201 this.fxai._handleDeviceDisconnection(payload.data.id); 202 return; 203 case ON_PROFILE_UPDATED_NOTIFICATION: 204 // We already have a "profile updated" notification sent via WebChannel, 205 // let's just re-use that. 206 Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION); 207 return; 208 case ON_PASSWORD_CHANGED_NOTIFICATION: 209 case ON_PASSWORD_RESET_NOTIFICATION: 210 this._onPasswordChanged(); 211 return; 212 case ON_ACCOUNT_DESTROYED_NOTIFICATION: 213 this.fxai._handleAccountDestroyed(payload.data.uid); 214 return; 215 case ON_COLLECTION_CHANGED_NOTIFICATION: 216 Services.obs.notifyObservers( 217 null, 218 ON_COLLECTION_CHANGED_NOTIFICATION, 219 payload.data.collections 220 ); 221 return; 222 case ON_VERIFY_LOGIN_NOTIFICATION: 223 Services.obs.notifyObservers( 224 null, 225 ON_VERIFY_LOGIN_NOTIFICATION, 226 JSON.stringify(payload.data) 227 ); 228 break; 229 default: 230 this.log.warn("FxA Push command unrecognized: " + payload.command); 231 } 232 }, 233 /** 234 * Check the FxA session status after a password change/reset event. 235 * If the session is invalid, reset credentials and notify listeners of 236 * ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed 237 * 238 * @returns {Promise} 239 * @private 240 */ 241 _onPasswordChanged() { 242 return this.fxai.withCurrentAccountState(async state => { 243 return this.fxai.checkAccountStatus(state); 244 }); 245 }, 246 /** 247 * Fired when the Push server drops a subscription, or the subscription identifier changes. 248 * 249 * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages 250 * 251 * @returns {Promise} 252 * @private 253 */ 254 _onPushSubscriptionChange() { 255 this.log.trace("FxAccountsPushService _onPushSubscriptionChange"); 256 return this.fxai.updateDeviceRegistration(); 257 }, 258 /** 259 * Unsubscribe from the Push server 260 * 261 * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe() 262 * 263 * @returns {Promise} - The promise resolves with a bool to indicate if we successfully unsubscribed. 264 * The promise never rejects. 265 * @private 266 */ 267 unsubscribe() { 268 this.log.trace("FxAccountsPushService unsubscribe"); 269 return new Promise(resolve => { 270 this.pushService.unsubscribe( 271 FXA_PUSH_SCOPE_ACCOUNT_UPDATE, 272 Services.scriptSecurityManager.getSystemPrincipal(), 273 (result, ok) => { 274 if (Components.isSuccessCode(result)) { 275 if (ok === true) { 276 this.log.debug("FxAccountsPushService unsubscribed"); 277 } else { 278 this.log.debug( 279 "FxAccountsPushService had no subscription to unsubscribe" 280 ); 281 } 282 } else { 283 this.log.warn( 284 "FxAccountsPushService failed to unsubscribe", 285 result 286 ); 287 } 288 return resolve(ok); 289 } 290 ); 291 }); 292 }, 293 294 /** 295 * Get our Push server subscription. 296 * 297 * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#getSubscription() 298 * 299 * @returns {Promise} - resolves with the subscription or null. Never rejects. 300 */ 301 getSubscription() { 302 return new Promise(resolve => { 303 this.pushService.getSubscription( 304 FXA_PUSH_SCOPE_ACCOUNT_UPDATE, 305 Services.scriptSecurityManager.getSystemPrincipal(), 306 (result, subscription) => { 307 if (!subscription) { 308 this.log.info("FxAccountsPushService no subscription found"); 309 return resolve(null); 310 } 311 return resolve(subscription); 312 } 313 ); 314 }); 315 }, 316 };