diff options
Diffstat (limited to 'services/sync/modules/SyncDisconnect.sys.mjs')
-rw-r--r-- | services/sync/modules/SyncDisconnect.sys.mjs | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/services/sync/modules/SyncDisconnect.sys.mjs b/services/sync/modules/SyncDisconnect.sys.mjs new file mode 100644 index 0000000000..5829085084 --- /dev/null +++ b/services/sync/modules/SyncDisconnect.sys.mjs @@ -0,0 +1,240 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// This module provides a facility for disconnecting Sync and FxA, optionally +// sanitizing profile data as part of the process. + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", + Log: "resource://gre/modules/Log.sys.mjs", + Sanitizer: "resource:///modules/Sanitizer.sys.mjs", + Utils: "resource://services-sync/util.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => { + return ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ).getFxAccountsSingleton(); +}); + +XPCOMUtils.defineLazyGetter(lazy, "FxAccountsCommon", function () { + return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js"); +}); + +export const SyncDisconnectInternal = { + lockRetryInterval: 1000, // wait 1 seconds before trying for the lock again. + lockRetryCount: 120, // Try 120 times (==2 mins) before giving up in disgust. + promiseDisconnectFinished: null, // If we are sanitizing, a promise for completion. + + // mocked by tests. + getWeave() { + return ChromeUtils.importESModule("resource://services-sync/main.sys.mjs") + .Weave; + }, + + // Returns a promise that resolves when we are not syncing, waiting until + // a current Sync completes if necessary. Resolves with true if we + // successfully waited, in which case the sync lock will have been taken to + // ensure future syncs don't state, or resolves with false if we gave up + // waiting for the sync to complete (in which case we didn't take a lock - + // but note that Sync probably remains locked in this case regardless.) + async promiseNotSyncing(abortController) { + let weave = this.getWeave(); + let log = lazy.Log.repository.getLogger("Sync.Service"); + // We might be syncing - poll for up to 2 minutes waiting for the lock. + // (2 minutes seems extreme, but should be very rare.) + return new Promise(resolve => { + abortController.signal.onabort = () => { + resolve(false); + }; + + let attempts = 0; + let checkLock = () => { + if (abortController.signal.aborted) { + // We've already resolved, so don't want a new timer to ever start. + return; + } + if (weave.Service.lock()) { + resolve(true); + return; + } + attempts += 1; + if (attempts >= this.lockRetryCount) { + log.error( + "Gave up waiting for the sync lock - going ahead with sanitize anyway" + ); + resolve(false); + return; + } + log.debug("Waiting a couple of seconds to get the sync lock"); + lazy.setTimeout(checkLock, this.lockRetryInterval); + }; + checkLock(); + }); + }, + + // Sanitize Sync-related data. + async doSanitizeSyncData() { + let weave = this.getWeave(); + // Get the sync logger - if stuff goes wrong it can be useful to have that + // recorded in the sync logs. + let log = lazy.Log.repository.getLogger("Sync.Service"); + log.info("Starting santitize of Sync data"); + try { + // We clobber data for all Sync engines that are enabled. + await weave.Service.promiseInitialized; + weave.Service.enabled = false; + + log.info("starting actual sanitization"); + for (let engine of weave.Service.engineManager.getAll()) { + if (engine.enabled) { + try { + log.info("Wiping engine", engine.name); + await engine.wipeClient(); + } catch (ex) { + log.error("Failed to wipe engine", ex); + } + } + } + // Reset the pref which is used to show a warning when a different user + // signs in - this is no longer a concern now that we've removed the + // data from the profile. + Services.prefs.clearUserPref(lazy.FxAccountsCommon.PREF_LAST_FXA_USER); + + log.info("Finished wiping sync data"); + } catch (ex) { + log.error("Failed to sanitize Sync data", ex); + console.error("Failed to sanitize Sync data", ex); + } + try { + // ensure any logs we wrote are flushed to disk. + await weave.Service.errorHandler.resetFileLog(); + } catch (ex) { + console.log("Failed to flush the Sync log", ex); + } + }, + + // Sanitize all Browser data. + async doSanitizeBrowserData() { + try { + // sanitize everything other than "open windows" (and we don't do that + // because it may confuse the user - they probably want to see + // about:prefs with the disconnection reflected. + let itemsToClear = Object.keys(lazy.Sanitizer.items).filter( + k => k != "openWindows" + ); + await lazy.Sanitizer.sanitize(itemsToClear); + } catch (ex) { + console.error("Failed to sanitize other data", ex); + } + }, + + async doSyncAndAccountDisconnect(shouldUnlock) { + // We do a startOver of Sync first - if we do the account first we end + // up with Sync configured but FxA not configured, which causes the browser + // UI to briefly enter a "needs reauth" state. + let Weave = this.getWeave(); + await Weave.Service.promiseInitialized; + await Weave.Service.startOver(); + await lazy.fxAccounts.signOut(); + // Sync may have been disabled if we santized, so re-enable it now or + // else the user will be unable to resync should they sign in before a + // restart. + Weave.Service.enabled = true; + + // and finally, if we managed to get the lock before, we should unlock it + // now. + if (shouldUnlock) { + Weave.Service.unlock(); + } + }, + + // Start the sanitization process. Returns a promise that resolves when + // the sanitize is complete, and an AbortController which can be used to + // abort the process of waiting for a sync to complete. + async _startDisconnect(abortController, sanitizeData = false) { + // This is a bit convoluted - we want to wait for a sync to finish before + // sanitizing, but want to abort that wait if the browser shuts down while + // we are waiting (in which case we'll charge ahead anyway). + // So we do this by using an AbortController and passing that to the + // function that waits for the sync lock - it will immediately resolve + // if the abort controller is aborted. + let log = lazy.Log.repository.getLogger("Sync.Service"); + + // If the master-password is locked then we will fail to fully sanitize, + // so prompt for that now. If canceled, we just abort now. + log.info("checking master-password state"); + if (!lazy.Utils.ensureMPUnlocked()) { + log.warn( + "The master-password needs to be unlocked to fully disconnect from sync" + ); + return; + } + + log.info("waiting for any existing syncs to complete"); + let locked = await this.promiseNotSyncing(abortController); + + if (sanitizeData) { + await this.doSanitizeSyncData(); + + // We disconnect before sanitizing the browser data - in a worst-case + // scenario where the sanitize takes so long that even the shutdown + // blocker doesn't allow it to finish, we should still at least be in + // a disconnected state on the next startup. + log.info("disconnecting account"); + await this.doSyncAndAccountDisconnect(locked); + + await this.doSanitizeBrowserData(); + } else { + log.info("disconnecting account"); + await this.doSyncAndAccountDisconnect(locked); + } + }, + + async disconnect(sanitizeData) { + if (this.promiseDisconnectFinished) { + throw new Error("A disconnect is already in progress"); + } + let abortController = new AbortController(); + let promiseDisconnectFinished = this._startDisconnect( + abortController, + sanitizeData + ); + this.promiseDisconnectFinished = promiseDisconnectFinished; + let shutdownBlocker = () => { + // oh dear - we are sanitizing (probably stuck waiting for a sync to + // complete) and the browser is shutting down. Let's avoid the wait + // for sync to complete and continue the process anyway. + abortController.abort(); + return promiseDisconnectFinished; + }; + lazy.AsyncShutdown.quitApplicationGranted.addBlocker( + "SyncDisconnect: removing requested data", + shutdownBlocker + ); + + // wait for it to finish - hopefully without the blocker being called. + await promiseDisconnectFinished; + this.promiseDisconnectFinished = null; + + // sanitize worked so remove our blocker - it's a noop if the blocker + // did call us. + lazy.AsyncShutdown.quitApplicationGranted.removeBlocker(shutdownBlocker); + }, +}; + +export const SyncDisconnect = { + get promiseDisconnectFinished() { + return SyncDisconnectInternal.promiseDisconnectFinished; + }, + + disconnect(sanitizeData) { + return SyncDisconnectInternal.disconnect(sanitizeData); + }, +}; |