newidentity.js (19673B)
1 "use strict"; 2 3 // Use a lazy getter because NewIdentityButton is declared more than once 4 // otherwise. 5 ChromeUtils.defineLazyGetter(this, "NewIdentityButton", () => { 6 // Logger adapted from CustomizableUI.jsm 7 const logger = (() => { 8 const consoleOptions = { 9 maxLogLevelPref: "browser.new_identity.log_level", 10 prefix: "NewIdentity", 11 }; 12 return console.createInstance(consoleOptions); 13 })(); 14 15 const topics = Object.freeze({ 16 newIdentityRequested: "new-identity-requested", 17 }); 18 19 /** 20 * This class contains the actual implementation of the various step involved 21 * when running new identity. 22 */ 23 class NewIdentityImpl { 24 async run() { 25 this.disableAllJS(); 26 await this.clearState(); 27 await this.openNewWindow(); 28 this.closeOldWindow(); 29 this.broadcast(); 30 } 31 32 // Disable JS (as a defense-in-depth measure) 33 34 disableAllJS() { 35 logger.info("Disabling JavaScript"); 36 const enumerator = Services.wm.getEnumerator("navigator:browser"); 37 while (enumerator.hasMoreElements()) { 38 const win = enumerator.getNext(); 39 this.disableWindowJS(win); 40 } 41 } 42 43 disableWindowJS(win) { 44 const browsers = win.gBrowser?.browsers || []; 45 for (const browser of browsers) { 46 if (!browser) { 47 continue; 48 } 49 this.disableBrowserJS(browser); 50 try { 51 browser.webNavigation?.stop(browser.webNavigation.STOP_ALL); 52 } catch (e) { 53 logger.warn("Could not stop navigation", e, browser.currentURI); 54 } 55 } 56 } 57 58 disableBrowserJS(browser) { 59 if (!browser) { 60 return; 61 } 62 // Does the following still apply? 63 // Solution from: https://bugzilla.mozilla.org/show_bug.cgi?id=409737 64 // XXX: This kills the entire window. We need to redirect 65 // focus and inform the user via a lightbox. 66 const eventSuppressor = browser.contentWindow?.windowUtils; 67 if (browser.browsingContext) { 68 browser.browsingContext.allowJavascript = false; 69 } 70 try { 71 // My estimation is that this does not get the inner iframe windows, 72 // but that does not matter, because iframes should be destroyed 73 // on the next load. 74 // Should we log when browser.contentWindow is null? 75 if (browser.contentWindow) { 76 browser.contentWindow.name = null; 77 browser.contentWindow.window.name = null; 78 } 79 } catch (e) { 80 logger.warn("Failed to reset window.name", e); 81 } 82 eventSuppressor?.suppressEventHandling(true); 83 } 84 85 // Clear state 86 87 async clearState() { 88 logger.info("Clearing the state"); 89 this.closeTabs(); 90 this.clearSearchBar(); 91 this.clearPrivateSessionHistory(); 92 this.clearHTTPAuths(); 93 this.clearCryptoTokens(); 94 this.clearOCSPCache(); 95 this.clearSecuritySettings(); 96 this.clearImageCaches(); 97 this.clearStorage(); 98 this.clearPreferencesAndPermissions(); 99 await this.clearData(); 100 await this.reloadAddons(); 101 this.clearConnections(); 102 this.clearPrivateSession(); 103 } 104 105 clearSiteSpecificZoom() { 106 Services.prefs.setBoolPref( 107 "browser.zoom.siteSpecific", 108 !Services.prefs.getBoolPref("browser.zoom.siteSpecific") 109 ); 110 Services.prefs.setBoolPref( 111 "browser.zoom.siteSpecific", 112 !Services.prefs.getBoolPref("browser.zoom.siteSpecific") 113 ); 114 } 115 116 closeTabs() { 117 if ( 118 !Services.prefs.getBoolPref("browser.new_identity.close_newnym", true) 119 ) { 120 logger.info("Not closing tabs"); 121 return; 122 } 123 // TODO: muck around with browser.tabs.warnOnClose.. maybe.. 124 logger.info("Closing tabs..."); 125 const enumerator = Services.wm.getEnumerator("navigator:browser"); 126 const windowsToClose = []; 127 while (enumerator.hasMoreElements()) { 128 const win = enumerator.getNext(); 129 const browser = win.gBrowser; 130 if (!browser) { 131 logger.warn("No browser for possible window to close"); 132 continue; 133 } 134 const tabsToRemove = []; 135 for (const b of browser.browsers) { 136 const tab = browser.getTabForBrowser(b); 137 if (tab) { 138 tabsToRemove.push(tab); 139 } else { 140 logger.warn("Browser has a null tab", b); 141 } 142 } 143 if (win == window) { 144 browser.addWebTab("about:blank"); 145 } else { 146 // It is a bad idea to alter the window list while iterating 147 // over it, so add this window to an array and close it later. 148 windowsToClose.push(win); 149 } 150 // Close each tab except the new blank one that we created. 151 tabsToRemove.forEach(aTab => browser.removeTab(aTab)); 152 } 153 // Close all XUL windows except this one. 154 logger.info("Closing windows..."); 155 windowsToClose.forEach(aWin => aWin.close()); 156 logger.info("Closed all tabs"); 157 158 // This clears the undo tab history. 159 const tabs = Services.prefs.getIntPref( 160 "browser.sessionstore.max_tabs_undo" 161 ); 162 Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", 0); 163 Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", tabs); 164 } 165 166 clearSearchBar() { 167 logger.info("Clearing searchbox"); 168 // Bug #10800: Trying to clear search/find can cause exceptions 169 // in unknown cases. Just log for now. 170 try { 171 const searchBar = window.document.getElementById("searchbar"); 172 if (searchBar) { 173 searchBar.textbox.reset(); 174 } 175 } catch (e) { 176 logger.error("Exception on clearing search box", e); 177 } 178 try { 179 if (gFindBarInitialized) { 180 const findbox = gFindBar.getElement("findbar-textbox"); 181 findbox.reset(); 182 gFindBar.close(); 183 } 184 } catch (e) { 185 logger.error("Exception on clearing find bar", e); 186 } 187 } 188 189 clearPrivateSessionHistory() { 190 logger.info("Emitting Private Browsing Session clear event"); 191 Services.obs.notifyObservers(null, "browser:purge-session-history"); 192 } 193 194 clearHTTPAuths() { 195 if ( 196 !Services.prefs.getBoolPref( 197 "browser.new_identity.clear_http_auth", 198 true 199 ) 200 ) { 201 logger.info("Skipping HTTP Auths, because disabled"); 202 return; 203 } 204 logger.info("Clearing HTTP Auths"); 205 const auth = Cc["@mozilla.org/network/http-auth-manager;1"].getService( 206 Ci.nsIHttpAuthManager 207 ); 208 auth.clearAll(); 209 } 210 211 clearCryptoTokens() { 212 logger.info("Clearing Crypto Tokens"); 213 // Clear all crypto auth tokens. This includes calls to PK11_LogoutAll(), 214 // nsNSSComponent::LogoutAuthenticatedPK11() and clearing the SSL session 215 // cache. 216 const sdr = Cc["@mozilla.org/security/sdr;1"].getService( 217 Ci.nsISecretDecoderRing 218 ); 219 sdr.logoutAndTeardown(); 220 } 221 222 clearOCSPCache() { 223 // nsNSSComponent::Observe() watches security.OCSP.enabled, which calls 224 // setValidationOptions(), which in turn calls setNonPkixOcspEnabled() which, 225 // if security.OCSP.enabled is set to 0, calls CERT_DisableOCSPChecking(), 226 // which calls CERT_ClearOCSPCache(). 227 // See: https://mxr.mozilla.org/comm-esr24/source/mozilla/security/manager/ssl/src/nsNSSComponent.cpp 228 const ocsp = Services.prefs.getIntPref("security.OCSP.enabled"); 229 Services.prefs.setIntPref("security.OCSP.enabled", 0); 230 Services.prefs.setIntPref("security.OCSP.enabled", ocsp); 231 } 232 233 clearSecuritySettings() { 234 // Clear site security settings 235 const sss = Cc["@mozilla.org/ssservice;1"].getService( 236 Ci.nsISiteSecurityService 237 ); 238 sss.clearAll(); 239 } 240 241 clearImageCaches() { 242 logger.info("Clearing Image Cache"); 243 // In Firefox 18 and newer, there are two image caches: one that is used 244 // for regular browsing, and one that is used for private browsing. 245 this.clearImageCacheRB(); 246 this.clearImageCachePB(); 247 } 248 249 clearImageCacheRB() { 250 try { 251 const imgTools = Cc["@mozilla.org/image/tools;1"].getService( 252 Ci.imgITools 253 ); 254 const imgCache = imgTools.getImgCacheForDocument(null); 255 // Evict all but chrome cache 256 imgCache.clearCache(false); 257 } catch (e) { 258 // FIXME: This can happen in some rare cases involving XULish image data 259 // in combination with our image cache isolation patch. Sure isn't 260 // a good thing, but it's not really a super-cookie vector either. 261 // We should fix it eventually. 262 logger.error("Exception on image cache clearing", e); 263 } 264 } 265 266 clearImageCachePB() { 267 const imgTools = Cc["@mozilla.org/image/tools;1"].getService( 268 Ci.imgITools 269 ); 270 try { 271 // Try to clear the private browsing cache. To do so, we must locate a 272 // content document that is contained within a private browsing window. 273 let didClearPBCache = false; 274 const enumerator = Services.wm.getEnumerator("navigator:browser"); 275 while (!didClearPBCache && enumerator.hasMoreElements()) { 276 const win = enumerator.getNext(); 277 let browserDoc = win.document.documentElement; 278 if (!browserDoc.hasAttribute("privatebrowsingmode")) { 279 continue; 280 } 281 const tabbrowser = win.gBrowser; 282 if (!tabbrowser) { 283 continue; 284 } 285 for (const browser of tabbrowser.browsers) { 286 const doc = browser.contentDocument; 287 if (doc) { 288 const imgCache = imgTools.getImgCacheForDocument(doc); 289 // Evict all but chrome cache 290 imgCache.clearCache(false); 291 didClearPBCache = true; 292 break; 293 } 294 } 295 } 296 } catch (e) { 297 logger.error("Exception on private browsing image cache clearing", e); 298 } 299 } 300 301 clearStorage() { 302 logger.info("Clearing Disk and Memory Caches"); 303 try { 304 Services.cache2.clear(); 305 } catch (e) { 306 logger.error("Exception on cache clearing", e); 307 } 308 309 logger.info("Clearing Cookies and DOM Storage"); 310 Services.cookies.removeAll(); 311 } 312 313 clearPreferencesAndPermissions() { 314 logger.info("Clearing Content Preferences"); 315 ChromeUtils.defineESModuleGetters(this, { 316 PrivateBrowsingUtils: 317 "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 318 }); 319 const pbCtxt = PrivateBrowsingUtils.privacyContextFromWindow(window); 320 const cps = Cc["@mozilla.org/content-pref/service;1"].getService( 321 Ci.nsIContentPrefService2 322 ); 323 cps.removeAllDomains(pbCtxt); 324 this.clearSiteSpecificZoom(); 325 326 logger.info("Clearing permissions"); 327 try { 328 Services.perms.removeAll(); 329 } catch (e) { 330 // Actually, this catch does not appear to be needed. Leaving it in for 331 // safety though. 332 logger.error("Cannot clear permissions", e); 333 } 334 335 logger.info("Syncing prefs"); 336 // Force prefs to be synced to disk 337 Services.prefs.savePrefFile(null); 338 } 339 340 async clearData() { 341 logger.info("Calling the clearDataService"); 342 const flags = 343 Services.clearData.CLEAR_ALL ^ Services.clearData.CLEAR_PASSWORDS; 344 return new Promise(resolve => { 345 Services.clearData.deleteData(flags, { 346 onDataDeleted(code) { 347 if (code !== Cr.NS_OK) { 348 logger.error(`Error while calling the clearDataService: ${code}`); 349 } 350 // We always resolve, because we do not want to interrupt the new 351 // identity procedure. 352 resolve(); 353 }, 354 }); 355 }); 356 } 357 358 clearConnections() { 359 logger.info("Closing open connections"); 360 // Clear keep-alive 361 Services.obs.notifyObservers(this, "net:prune-all-connections"); 362 } 363 364 clearPrivateSession() { 365 logger.info("Ending any remaining private browsing sessions."); 366 Services.obs.notifyObservers(null, "last-pb-context-exited"); 367 } 368 369 async reloadAddons() { 370 logger.info("Reloading add-ons to clear their temporary state."); 371 // Reload all active extensions except search engines, which would throw. 372 const addons = await AddonManager.getAddonsByTypes(["extension"]); 373 const isSearchEngine = async addon => 374 (await (await fetch(addon.getResourceURI("manifest.json").spec)).json()) 375 ?.chrome_settings_overrides?.search_provider; 376 const reloadIfNeeded = async addon => 377 addon.isActive && !(await isSearchEngine(addon)) && addon.reload(); 378 await Promise.all(addons.map(addon => reloadIfNeeded(addon))); 379 } 380 381 // Broadcast as a hook to clear other data 382 383 broadcast() { 384 logger.info("Broadcasting the new identity"); 385 Services.obs.notifyObservers({}, topics.newIdentityRequested); 386 } 387 388 // Window management 389 390 openNewWindow() { 391 logger.info("Opening a new window"); 392 return new Promise(resolve => { 393 // Open a new window forcing the about:privatebrowsing page (tor-browser#41765) 394 // unless user explicitly overrides this policy (tor-browser #42236) 395 const trustedHomePref = "browser.startup.homepage.new_identity"; 396 const homeURL = HomePage.get(); 397 const defaultHomeURL = HomePage.getDefault(); 398 const isTrustedHome = 399 homeURL === defaultHomeURL || 400 homeURL === "chrome://browser/content/blanktab.html" || // about:blank 401 homeURL === Services.prefs.getStringPref(trustedHomePref, ""); 402 const isCustomHome = 403 Services.prefs.getIntPref("browser.startup.page") === 1; 404 const win = OpenBrowserWindow({ 405 private: isCustomHome && isTrustedHome ? "private" : "no-home", 406 }); 407 // This mechanism to know when the new window is ready is used by 408 // OpenBrowserWindow itself (see its definition in browser.js). 409 win.addEventListener( 410 "MozAfterPaint", 411 () => { 412 resolve(); 413 if (isTrustedHome || !isCustomHome) { 414 return; 415 } 416 const tbl = win.TabsProgressListener; 417 const { onLocationChange } = tbl; 418 tbl.onLocationChange = (...args) => { 419 tbl.onLocationChange = onLocationChange; 420 tbl.onLocationChange(...args); 421 const url = URL.parse(homeURL); 422 if (!url) { 423 // malformed URL, bail out 424 return; 425 } 426 427 let displayAddress = url.hostname; 428 if (!displayAddress) { 429 // no host, use full address and truncate if too long 430 const MAX_LEN = 32; 431 displayAddress = url.href; 432 if (displayAddress.length > MAX_LEN) { 433 displayAddress = `${displayAddress.substring(0, MAX_LEN)}…`; 434 } 435 } 436 const callback = () => { 437 Services.prefs.setStringPref(trustedHomePref, homeURL); 438 win.BrowserHome(); 439 }; 440 const notificationBox = win.gBrowser.getNotificationBox(); 441 notificationBox.appendNotification( 442 "new-identity-safe-home", 443 { 444 label: { 445 "l10n-id": "new-identity-blocked-home-notification", 446 "l10n-args": { url: displayAddress }, 447 }, 448 priority: notificationBox.PRIORITY_INFO_MEDIUM, 449 }, 450 [ 451 { 452 "l10n-id": "new-identity-blocked-home-ignore-button", 453 callback, 454 }, 455 ] 456 ); 457 }; 458 }, 459 { once: true } 460 ); 461 }); 462 } 463 464 closeOldWindow() { 465 logger.info("Closing the old window"); 466 467 // Run garbage collection and cycle collection after window is gone. 468 // This ensures that blob URIs are forgotten. 469 window.addEventListener("unload", function () { 470 logger.debug("Initiating New Identity GC pass"); 471 // Clear out potential pending sInterSliceGCTimer: 472 window.windowUtils.runNextCollectorTimer(); 473 // Clear out potential pending sICCTimer: 474 window.windowUtils.runNextCollectorTimer(); 475 // Schedule a garbage collection in 4000-1000ms... 476 window.windowUtils.garbageCollect(); 477 // To ensure the GC runs immediately instead of 4-10s from now, we need 478 // to poke it at least 11 times. 479 // We need 5 pokes for GC, 1 poke for the interSliceGC, and 5 pokes for 480 // CC. 481 // See nsJSContext::RunNextCollectorTimer() in 482 // https://mxr.mozilla.org/mozilla-central/source/dom/base/nsJSEnvironment.cpp#1970. 483 // XXX: We might want to make our own method for immediate full GC... 484 for (let poke = 0; poke < 11; poke++) { 485 window.windowUtils.runNextCollectorTimer(); 486 } 487 // And now, since the GC probably actually ran *after* the CC last time, 488 // run the whole thing again. 489 window.windowUtils.garbageCollect(); 490 for (let poke = 0; poke < 11; poke++) { 491 window.windowUtils.runNextCollectorTimer(); 492 } 493 logger.debug("Completed New Identity GC pass"); 494 }); 495 496 // Close the current window for added safety 497 window.close(); 498 } 499 } 500 501 let newIdentityInProgress = false; 502 return { 503 async onCommand() { 504 try { 505 // Ignore if there's a New Identity in progress to avoid race 506 // conditions leading to failures (see bug 11783 for an example). 507 if (newIdentityInProgress) { 508 return; 509 } 510 newIdentityInProgress = true; 511 512 const prefConfirm = "browser.new_identity.confirm_newnym"; 513 const shouldConfirm = Services.prefs.getBoolPref(prefConfirm, true); 514 if (shouldConfirm) { 515 const [titleString, bodyString, checkboxString, restartString] = 516 await document.l10n.formatValues([ 517 { id: "new-identity-dialog-title" }, 518 { id: "new-identity-dialog-description" }, 519 { id: "restart-warning-dialog-do-not-warn-checkbox" }, 520 { id: "restart-warning-dialog-restart-button" }, 521 ]); 522 const flags = 523 Services.prompt.BUTTON_POS_0 * 524 Services.prompt.BUTTON_TITLE_IS_STRING + 525 Services.prompt.BUTTON_POS_0_DEFAULT + 526 Services.prompt.BUTTON_DEFAULT_IS_DESTRUCTIVE + 527 Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL; 528 const propBag = await Services.prompt.asyncConfirmEx( 529 window.browsingContext, 530 Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, 531 titleString, 532 bodyString, 533 flags, 534 restartString, 535 null, 536 null, 537 checkboxString, 538 false 539 ); 540 if (propBag.get("buttonNumClicked") !== 0) { 541 return; 542 } 543 if (propBag.get("checked")) { 544 Services.prefs.setBoolPref(prefConfirm, false); 545 } 546 } 547 548 const impl = new NewIdentityImpl(); 549 await impl.run(); 550 } catch (e) { 551 // If something went wrong make sure we have the New Identity button 552 // enabled (again). 553 logger.error("Unexpected error", e); 554 window.alert("New Identity unexpected error: " + e); 555 } finally { 556 newIdentityInProgress = false; 557 } 558 }, 559 }; 560 });