lists.torproject.org
Sign In Sign Up
Manage this list Sign In Sign Up

Keyboard Shortcuts

Thread View

  • j: Next unread message
  • k: Previous unread message
  • j a: Jump to all threads
  • j l: Jump to MailingList overview

tbb-commits

Thread Start a new thread
Download
Threads by month
  • ----- 2025 -----
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2024 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2023 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2022 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2021 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2020 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2019 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2018 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2017 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2016 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2015 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2014 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
tbb-commits@lists.torproject.org

  • 1 participants
  • 18730 discussions
[Git][tpo/applications/tor-browser][base-browser-102.14.0esr-12.5-1] 2 commits: Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs
by ma1 (@ma1) 31 Jul '23

31 Jul '23
ma1 pushed to branch base-browser-102.14.0esr-12.5-1 at The Tor Project / Applications / Tor Browser Commits: f24d6cd4 by Edgar Chen at 2023-07-31T21:15:57+02:00 Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs Fullscreen/PointerLock warnings are initialized with hidden=&quot;true&quot;, but change to hidden=&quot;&quot; after being shown and hidden again. I think this started happening when we began using HTML elements instead of XUL as they handle hidden attribute differently. Differential Revision: https://phabricator.services.mozilla.com/D177790 - - - - - 53142727 by Edgar Chen at 2023-07-31T21:16:28+02:00 Bug 1821884 - Reshow initial fullscreen notification; r=Gijs Depends on D177790 Differential Revision: https://phabricator.services.mozilla.com/D178339 - - - - - 4 changed files: - browser/base/content/browser-fullScreenAndPointerLock.js - browser/base/content/fullscreen-and-pointerlock.inc.xhtml - browser/base/content/test/fullscreen/browser_fullscreen_warning.js - dom/tests/browser/browser_pointerlock_warning.js Changes: ===================================== browser/base/content/browser-fullScreenAndPointerLock.js ===================================== @@ -62,9 +62,14 @@ var PointerlockFsWarning = { this._element = document.getElementById(elementId); // Setup event listeners this._element.addEventListener("transitionend", this); + this._element.addEventListener("transitioncancel", this); window.addEventListener("mousemove", this, true); + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); // The timeout to hide the warning box after a while. this._timeoutHide = new this.Timeout(() => { + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); this._state = "hidden"; }, timeout); // The timeout to show the warning box when the pointer is at the top @@ -116,11 +121,10 @@ var PointerlockFsWarning = { return; } - // Explicitly set the last state to hidden to avoid the warning - // box being hidden immediately because of mousemove. - this._state = "onscreen"; - this._lastState = "hidden"; - this._timeoutHide.start(); + if (Services.focus.activeWindow == window) { + this._state = "onscreen"; + this._timeoutHide.start(); + } }, /** @@ -148,7 +152,10 @@ var PointerlockFsWarning = { this._element.hidden = true; // Remove all event listeners this._element.removeEventListener("transitionend", this); + this._element.removeEventListener("transitioncancel", this); window.removeEventListener("mousemove", this, true); + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); // Clear fields this._element = null; this._timeoutHide = null; @@ -186,7 +193,7 @@ var PointerlockFsWarning = { } if (newState != "hidden") { if (currentState != "hidden") { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } else { // When the previous state is hidden, the display was none, // thus no box was constructed. We need to wait for the new @@ -197,7 +204,7 @@ var PointerlockFsWarning = { requestAnimationFrame(() => { requestAnimationFrame(() => { if (this._element) { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } }); }); @@ -217,7 +224,7 @@ var PointerlockFsWarning = { } else if (this._timeoutShow.delay >= 0) { this._timeoutShow.start(); } - } else { + } else if (state != "onscreen") { let elemRect = this._element.getBoundingClientRect(); if (state == "hiding" && this._lastState != "hidden") { // If we are on the hiding transition, and the pointer @@ -239,12 +246,23 @@ var PointerlockFsWarning = { } break; } - case "transitionend": { + case "transitionend": + case "transitioncancel": { if (this._state == "hiding") { this._element.hidden = true; } break; } + case "activate": { + this._state = "onscreen"; + this._timeoutHide.start(); + break; + } + case "deactivate": { + this._state = "hidden"; + this._timeoutHide.cancel(); + break; + } } }, }; ===================================== browser/base/content/fullscreen-and-pointerlock.inc.xhtml ===================================== @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. <html:div id="fullscreen-and-pointerlock-wrapper"> - <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> @@ -20,7 +20,7 @@ </html:button> </html:div> - <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> ===================================== browser/base/content/test/fullscreen/browser_fullscreen_warning.js ===================================== @@ -3,14 +3,35 @@ "use strict"; -add_task(async function test_fullscreen_display_none() { +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + +add_setup(async function init() { await SpecialPowers.pushPrefEnv({ set: [ ["full-screen-api.enabled", true], ["full-screen-api.allow-trusted-requests-only", false], ], }); +}); +add_task(async function test_fullscreen_display_none() { await BrowserTestUtils.withNewTab( { gBrowser, @@ -30,11 +51,13 @@ add_task(async function test_fullscreen_display_none() { }, async function(browser) { let warning = document.getElementById("fullscreen-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( warning, - "true" + "hidden", + "Should not show full screen warning initially" ); + + let warningShownPromise = waitForWarningState(warning, "onscreen"); // Enter fullscreen await SpecialPowers.spawn(browser, [], async () => { let frame = content.document.querySelector("iframe"); @@ -54,39 +77,33 @@ add_task(async function test_fullscreen_display_none() { ); document.getElementById("fullscreen-exit-button").click(); await exitFullscreenPromise; + + checkWarningState( + warning, + "hidden", + "Should hide fullscreen warning after exiting fullscreen" + ); } ); }); add_task(async function test_fullscreen_pointerlock_conflict() { - await SpecialPowers.pushPrefEnv({ - set: [ - ["full-screen-api.enabled", true], - ["full-screen-api.allow-trusted-requests-only", false], - ], - }); - await BrowserTestUtils.withNewTab("https://example.com", async browser => { let fsWarning = document.getElementById("fullscreen-warning"); let plWarning = document.getElementById("pointerlock-warning"); - is( - fsWarning.getAttribute("onscreen"), - null, - "Should not show full screen warning initially." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning initially." - ); - - let fsWarningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( fsWarning, - "true" + "hidden", + "Should not show full screen warning initially" + ); + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning initially" ); + let fsWarningShownPromise = waitForWarningState(fsWarning, "onscreen"); info("Entering full screen and pointer lock."); await SpecialPowers.spawn(browser, [], async () => { await content.document.body.requestFullscreen(); @@ -94,15 +111,10 @@ add_task(async function test_fullscreen_pointerlock_conflict() { }); await fsWarningShownPromise; - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should show full screen warning." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); info("Exiting pointerlock"); @@ -110,18 +122,19 @@ add_task(async function test_fullscreen_pointerlock_conflict() { await content.document.exitPointerLock(); }); - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should still show full screen warning." + checkWarningState( + fsWarning, + "onscreen", + "Should still show full screen warning" ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); // Cleanup + info("Exiting fullscreen"); await document.exitFullscreen(); }); }); ===================================== dom/tests/browser/browser_pointerlock_warning.js ===================================== @@ -15,6 +15,25 @@ const FRAME_TEST_URL = encodeURI(BODY_URL) + '"></iframe></body>'; +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + // Make sure the pointerlock warning is shown and exited with the escape key add_task(async function show_pointerlock_warning_escape() { let urls = [TEST_URL, FRAME_TEST_URL]; @@ -24,11 +43,7 @@ add_task(async function show_pointerlock_warning_escape() { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); let warning = document.getElementById("pointerlock-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", - warning, - "true" - ); + let warningShownPromise = waitForWarningState(warning, "onscreen"); let expectedWarningText; @@ -49,11 +64,7 @@ add_task(async function show_pointerlock_warning_escape() { ok(true, "Pointerlock warning shown"); - let warningHiddenPromise = BrowserTestUtils.waitForAttribute( - "hidden", - warning, - "" - ); + let warningHiddenPromise = waitForWarningState(warning, "hidden"); await BrowserTestUtils.waitForCondition( () => warning.innerText == expectedWarningText, View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/fadc59… -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/fadc59… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
[Git][tpo/applications/tor-browser] Pushed new tag tor-browser-102.14.0esr-12.5-1-build2
by ma1 (@ma1) 31 Jul '23

31 Jul '23
ma1 pushed new tag tor-browser-102.14.0esr-12.5-1-build2 at The Tor Project / Applications / Tor Browser -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/tree/tor-brows… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
[Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] 9 commits: fixup! Add TorStrings module for localization
by Pier Angelo Vendrame (@pierov) 31 Jul '23

31 Jul '23
Pier Angelo Vendrame pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser Commits: c3d2496b by Pier Angelo Vendrame at 2023-07-27T21:07:52+02:00 fixup! Add TorStrings module for localization Move the `getLocale` function here from TorButton util.js and stop importing it. - - - - - 642df684 by Pier Angelo Vendrame at 2023-07-27T21:07:53+02:00 fixup! Bug 40933: Add tor-launcher functionality Fix a couple of problems in TorLauncherUtil and TorParsers. Also, moved the function to parse bridges in TorParsers. - - - - - 9e825b61 by Pier Angelo Vendrame at 2023-07-27T21:07:53+02:00 fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection Use the bridge line parser from TorParsers. - - - - - 8061f810 by Pier Angelo Vendrame at 2023-07-27T21:07:53+02:00 fixup! Bug 40933: Add tor-launcher functionality Use actual private members for TorProcess. - - - - - f5a3f4af by Pier Angelo Vendrame at 2023-07-27T21:07:54+02:00 fixup! Bug 40933: Add tor-launcher functionality Group arg pushes in one line (keys and values together). - - - - - 71b08c4e by Pier Angelo Vendrame at 2023-07-27T21:07:54+02:00 fixup! Bug 40933: Add tor-launcher functionality TorProcess: use real private properties instead of _, and removed the dependency on TorProtocolService (temporarily moved it to TorMonitorService, but eventually we should unify TorProtocolService and TorMonitorService, to then split them again in a smarter way). - - - - - e1a69b4e by Pier Angelo Vendrame at 2023-07-27T21:07:55+02:00 fixup! Bug 10760: Integrate TorButton to TorBrowser core Move the SOCKS preference updater to TorProtocolService. We will need to refactor all this kind of stuff, but at least let&#39;s get it in a single place. Also, since this was the last bit of the startup service, remove the file, the component and what else was needed to add them. - - - - - d457b6f8 by Pier Angelo Vendrame at 2023-07-31T20:42:54+02:00 fixup! Bug 40933: Add tor-launcher functionality Hashing the control port password is needed only on the process, so move this function to TorProcess. - - - - - c4e8cd0f by Pier Angelo Vendrame at 2023-07-31T20:42:57+02:00 fixup! Bug 10760: Integrate TorButton to TorBrowser core The hashpassword parameter has been removed. - - - - - 13 changed files: - browser/components/torpreferences/content/connectionPane.js - browser/installer/package-manifest.in - browser/modules/TorStrings.jsm - toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs - toolkit/components/tor-launcher/TorMonitorService.sys.mjs - toolkit/components/tor-launcher/TorParsers.sys.mjs - toolkit/components/tor-launcher/TorProcess.sys.mjs - toolkit/components/tor-launcher/TorProtocolService.sys.mjs - toolkit/torbutton/chrome/content/torbutton.js - toolkit/torbutton/components.conf - − toolkit/torbutton/modules/TorbuttonStartupObserver.jsm - toolkit/torbutton/moz.build - − toolkit/torbutton/torbutton.manifest Changes: ===================================== browser/components/torpreferences/content/connectionPane.js ===================================== @@ -14,8 +14,11 @@ const { setTimeout, clearTimeout } = ChromeUtils.import( const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource } = ChromeUtils.import("resource:///modules/TorSettings.jsm"); -const { TorProtocolService } = ChromeUtils.import( - "resource://gre/modules/TorProtocolService.jsm" +const { TorParsers } = ChromeUtils.importESModule( + "resource://gre/modules/TorParsers.sys.mjs" +); +const { TorProtocolService } = ChromeUtils.importESModule( + "resource://gre/modules/TorProtocolService.sys.mjs" ); const { TorMonitorService, TorMonitorTopics } = ChromeUtils.import( "resource://gre/modules/TorMonitorService.jsm" @@ -495,7 +498,7 @@ const gConnectionPane = (function () { }); const idString = TorStrings.settings.bridgeId; const id = card.querySelector(selectors.bridges.cardId); - const details = parseBridgeLine(bridgeString); + const details = TorParsers.parseBridgeLine(bridgeString); if (details && details.id !== undefined) { card.setAttribute("data-bridge-id", details.id); } @@ -1111,23 +1114,3 @@ function makeBridgeId(bridgeString) { hash & 0x000000ff, ]; } - -function parseBridgeLine(line) { - const re = - /^\s*(\S+\s+)?([0-9a-fA-F\.\[\]\:]+:\d{1,5})(\s+[0-9a-fA-F]{40})?(\s+.+)?/; - const matches = line.match(re); - if (!matches) { - return null; - } - let bridge = { addr: matches[2] }; - if (matches[1] !== undefined) { - bridge.transport = matches[1].trim(); - } - if (matches[3] !== undefined) { - bridge.id = matches[3].trim().toUpperCase(); - } - if (matches[4] !== undefined) { - bridge.args = matches[4].trim(); - } - return bridge; -} ===================================== browser/installer/package-manifest.in ===================================== @@ -228,7 +228,6 @@ @RESPATH@/components/tor-launcher.manifest @RESPATH@/chrome/torbutton.manifest @RESPATH@/chrome/torbutton/* -@RESPATH@/components/torbutton.manifest @RESPATH@/chrome/toolkit@JAREXT@ @RESPATH@/chrome/toolkit.manifest #ifdef MOZ_GTK ===================================== browser/modules/TorStrings.jsm ===================================== @@ -11,9 +11,11 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { AppConstants } = ChromeUtils.import( "resource://gre/modules/AppConstants.jsm" ); -const { getLocale } = ChromeUtils.import( - "resource://torbutton/modules/utils.js" -); + +function getLocale() { + const locale = Services.locale.appLocaleAsBCP47; + return locale === "ja-JP-macos" ? "ja" : locale; +} /* Tor Property String Bundle ===================================== toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs ===================================== @@ -5,6 +5,12 @@ * Tor Launcher Util JS Module *************************************************************************/ +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.jsm", +}); + const kPropBundleURI = "chrome://torbutton/locale/torlauncher.properties"; const kPropNamePrefix = "torlauncher."; const kIPCDirPrefName = "extensions.torlauncher.tmp_ipc_dir"; @@ -209,14 +215,15 @@ class TorFile { // and return a file object. The control and SOCKS IPC objects will be // created by tor. normalize() { - if (!this.file.exists() && !this.isIPC) { + if (this.file.exists()) { + try { + this.file.normalize(); + } catch (e) { + console.warn("Normalization of the path failed", e); + } + } else if (!this.isIPC) { throw new Error(`${this.fileType} file not found: ${this.file.path}`); } - try { - this.file.normalize(); - } catch (e) { - console.warn("Normalization of the path failed", e); - } // Ensure that the IPC path length is short enough for use by the // operating system. If not, create and use a unique directory under @@ -452,6 +459,154 @@ export const TorLauncherUtil = Object.freeze({ return result ? result : ""; }, + /** + * Determine what kind of SOCKS port has been requested for this session or + * the browser has been configured for. + * On Windows (where Unix domain sockets are not supported), TCP is always + * used. + * + * The following environment variables are supported and take precedence over + * preferences: + * TOR_TRANSPROXY (do not use a proxy) + * TOR_SOCKS_IPC_PATH (file system path; ignored on Windows) + * TOR_SOCKS_HOST + * TOR_SOCKS_PORT + * + * The following preferences are consulted: + * network.proxy.socks + * network.proxy.socks_port + * extensions.torlauncher.socks_port_use_ipc (Boolean) + * extensions.torlauncher.socks_ipc_path (file system path) + * If extensions.torlauncher.socks_ipc_path is empty, a default path is used. + * + * When using TCP, if a value is not defined via an env variable it is + * taken from the corresponding browser preference if possible. The + * exceptions are: + * If network.proxy.socks contains a file: URL, a default value of + * "127.0.0.1" is used instead. + * If the network.proxy.socks_port value is not valid (outside the + * (0; 65535] range), a default value of 9150 is used instead. + * + * The SOCKS configuration will not influence the launch of a tor daemon and + * the configuration of the control port in any way. + * When a SOCKS configuration is required without TOR_SKIP_LAUNCH, the browser + * will try to configure the tor instance to use the required configuration. + * This also applies to TOR_TRANSPROXY (at least for now): tor will be + * launched with its defaults. + * + * TODO: add a preference to ignore the current configuration, and let tor + * listen on any free port. Then, the browser will prompt the daemon the port + * to use through the control port (even though this is quite dangerous at the + * moment, because with network disabled tor will disable also the SOCKS + * listeners, so it means that we will have to check it every time we change + * the network status). + */ + getPreferredSocksConfiguration() { + if (Services.env.exists("TOR_TRANSPROXY")) { + Services.prefs.setBoolPref("network.proxy.socks_remote_dns", false); + Services.prefs.setIntPref("network.proxy.type", 0); + Services.prefs.setIntPref("network.proxy.socks_port", 0); + Services.prefs.setCharPref("network.proxy.socks", ""); + return { transproxy: true }; + } + + let useIPC; + const socksPortInfo = { + transproxy: false, + }; + + if (!this.isWindows && Services.env.exists("TOR_SOCKS_IPC_PATH")) { + useIPC = true; + const ipcPath = Services.env.get("TOR_SOCKS_IPC_PATH"); + if (ipcPath) { + socksPortInfo.ipcFile = new lazy.FileUtils.File(ipcPath); + } + } else { + // Check for TCP host and port environment variables. + if (Services.env.exists("TOR_SOCKS_HOST")) { + socksPortInfo.host = Services.env.get("TOR_SOCKS_HOST"); + useIPC = false; + } + if (Services.env.exists("TOR_SOCKS_PORT")) { + const port = parseInt(Services.env.get("TOR_SOCKS_PORT"), 10); + if (Number.isInteger(port) && port > 0 && port <= 65535) { + socksPortInfo.port = port; + useIPC = false; + } + } + } + + if (useIPC === undefined) { + socksPortInfo.useIPC = + !this.isWindows && + Services.prefs.getBoolPref( + "extensions.torlauncher.socks_port_use_ipc", + false + ); + } + + // Fill in missing SOCKS info from prefs. + if (socksPortInfo.useIPC) { + if (!socksPortInfo.ipcFile) { + socksPortInfo.ipcFile = TorLauncherUtil.getTorFile("socks_ipc", false); + } + } else { + if (!socksPortInfo.host) { + let socksAddr = Services.prefs.getCharPref( + "network.proxy.socks", + "127.0.0.1" + ); + let socksAddrHasHost = socksAddr && !socksAddr.startsWith("file:"); + socksPortInfo.host = socksAddrHasHost ? socksAddr : "127.0.0.1"; + } + + if (!socksPortInfo.port) { + let socksPort = Services.prefs.getIntPref( + "network.proxy.socks_port", + 0 + ); + // This pref is set as 0 by default in Firefox, use 9150 if we get 0. + socksPortInfo.port = + socksPort > 0 && socksPort <= 65535 ? socksPort : 9150; + } + } + + return socksPortInfo; + }, + + setProxyConfiguration(socksPortInfo) { + if (socksPortInfo.transproxy) { + return; + } + + if (socksPortInfo.useIPC) { + const fph = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + const fileURI = fph.newFileURI(socksPortInfo.ipcFile); + Services.prefs.setCharPref("network.proxy.socks", fileURI.spec); + Services.prefs.setIntPref("network.proxy.socks_port", 0); + } else { + if (socksPortInfo.host) { + Services.prefs.setCharPref("network.proxy.socks", socksPortInfo.host); + } + if (socksPortInfo.port) { + Services.prefs.setIntPref( + "network.proxy.socks_port", + socksPortInfo.port + ); + } + } + + if (socksPortInfo.ipcFile || socksPortInfo.host || socksPortInfo.port) { + Services.prefs.setBoolPref("network.proxy.socks_remote_dns", true); + Services.prefs.setIntPref("network.proxy.type", 1); + } + + // Force prefs to be synced to disk + Services.prefs.savePrefFile(null); + }, + get shouldStartAndOwnTor() { const kPrefStartTor = "extensions.torlauncher.start_tor"; try { ===================================== toolkit/components/tor-launcher/TorMonitorService.sys.mjs ===================================== @@ -13,6 +13,10 @@ import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs" const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs", +}); + ChromeUtils.defineModuleGetter( lazy, "controller", @@ -233,7 +237,10 @@ export const TorMonitorService = { // TorProcess should be instanced once, then always reused and restarted // only through the prompt it exposes when the controlled process dies. if (!this._torProcess) { - this._torProcess = new TorProcess(); + this._torProcess = new TorProcess( + lazy.TorProtocolService.torControlPortInfo, + lazy.TorProtocolService.torSOCKSPortInfo + ); this._torProcess.onExit = () => { this._shutDownEventMonitor(); Services.obs.notifyObservers(null, TorTopics.ProcessExited); @@ -254,6 +261,7 @@ export const TorMonitorService = { await this._torProcess.start(); if (this._torProcess.isRunning) { logger.info("tor started"); + this._torProcessStartTime = Date.now(); } } catch (e) { // TorProcess already logs the error. ===================================== toolkit/components/tor-launcher/TorParsers.sys.mjs ===================================== @@ -267,4 +267,18 @@ export const TorParsers = Object.freeze({ rv += aStr.substring(lastAdded, aStr.length - 1); return rv; }, + + parseBridgeLine(line) { + const re = + /\s*(?:(?<transport>\S+)\s+)?(?<addr>[0-9a-fA-F\.\[\]\:]+:\d{1,5})(?:\s+(?<id>[0-9a-fA-F]{40}))?(?:\s+(?<args>.+))?/; + const match = re.exec(line); + if (!match) { + throw new Error("Invalid bridge line."); + } + const bridge = match.groups; + if (!bridge.transport) { + bridge.transport = "vanilla"; + } + return bridge; + }, }); ===================================== toolkit/components/tor-launcher/TorProcess.sys.mjs ===================================== @@ -1,21 +1,17 @@ +/* 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/. */ + import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs"; import { Subprocess } from "resource://gre/modules/Subprocess.sys.mjs"; const lazy = {}; -ChromeUtils.defineModuleGetter( - lazy, - "TorProtocolService", - "resource://gre/modules/TorProtocolService.jsm" -); -const { TorLauncherUtil } = ChromeUtils.import( - "resource://gre/modules/TorLauncherUtil.jsm" -); - -const { TorParsers } = ChromeUtils.import( - "resource://gre/modules/TorParsers.jsm" -); +ChromeUtils.defineESModuleGetters(lazy, { + TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs", + TorParsers: "resource://gre/modules/TorParsers.sys.mjs", +}); const TorProcessStatus = Object.freeze({ Unknown: 0, @@ -30,53 +26,86 @@ const logger = new ConsoleAPI({ }); export class TorProcess { - _exeFile = null; - _dataDir = null; - _args = []; - _subprocess = null; - _status = TorProcessStatus.Unknown; - _torProcessStartTime = null; // JS Date.now() - _didConnectToTorControlPort = false; // Have we ever made a connection? + #controlSettings; + #socksSettings; + #exeFile = null; + #dataDir = null; + #args = []; + #subprocess = null; + #status = TorProcessStatus.Unknown; + // Have we ever made a connection on the control port? + #didConnectToTorControlPort = false; + + onExit = exitCode => {}; + onRestart = () => {}; + + constructor(controlSettings, socksSettings) { + if ( + controlSettings && + !controlSettings.password && + !controlSettings.cookieFilePath + ) { + throw new Error("Unauthenticated control port is not supported"); + } - onExit = null; - onRestart = null; + const checkPort = port => + port === undefined || + (Number.isInteger(controlSettings.port) && + controlSettings.port > 0 && + controlSettings.port < 65535); + if (!checkPort(controlSettings?.port)) { + throw new Error("Invalid control port"); + } + if (!checkPort(socksSettings.port)) { + throw new Error("Invalid port specified for the SOCKS port"); + } + + this.#controlSettings = { ...controlSettings }; + const ipcFileToString = file => + "unix:" + lazy.TorParsers.escapeString(file.path); + if (controlSettings.ipcFile) { + this.#controlSettings.ipcFile = ipcFileToString(controlSettings.ipcFile); + } + this.#socksSettings = { ...socksSettings }; + if (socksSettings.ipcFile) { + this.#socksSettings.ipcFile = ipcFileToString(socksSettings.ipcFile); + } + } get status() { - return this._status; + return this.#status; } get isRunning() { return ( - this._status === TorProcessStatus.Starting || - this._status === TorProcessStatus.Running + this.#status === TorProcessStatus.Starting || + this.#status === TorProcessStatus.Running ); } async start() { - if (this._subprocess) { + if (this.#subprocess) { return; } - this._status = TorProcessStatus.Unknown; + this.#status = TorProcessStatus.Unknown; try { - this._makeArgs(); - this._addControlPortArg(); - this._addSocksPortArg(); + this.#makeArgs(); + this.#addControlPortArgs(); + this.#addSocksPortArg(); const pid = Services.appinfo.processID; if (pid !== 0) { - this._args.push("__OwningControllerProcess"); - this._args.push("" + pid); + this.#args.push("__OwningControllerProcess", pid.toString()); } - if (TorLauncherUtil.shouldShowNetworkSettings) { - this._args.push("DisableNetwork"); - this._args.push("1"); + if (lazy.TorLauncherUtil.shouldShowNetworkSettings) { + this.#args.push("DisableNetwork", "1"); } - this._status = TorProcessStatus.Starting; - this._didConnectToTorControlPort = false; + this.#status = TorProcessStatus.Starting; + this.#didConnectToTorControlPort = false; // useful for simulating slow tor daemon launch const kPrefTorDaemonLaunchDelay = "extensions.torlauncher.launch_delay"; @@ -88,29 +117,31 @@ export class TorProcess { await new Promise(resolve => setTimeout(() => resolve(), launchDelay)); } - logger.debug(`Starting ${this._exeFile.path}`, this._args); + logger.debug(`Starting ${this.#exeFile.path}`, this.#args); const options = { - command: this._exeFile.path, - arguments: this._args, + command: this.#exeFile.path, + arguments: this.#args, stderr: "stdout", - workdir: TorLauncherUtil.getTorFile("pt-startup-dir", false).path, + workdir: lazy.TorLauncherUtil.getTorFile("pt-startup-dir", false).path, }; - this._subprocess = await Subprocess.call(options); - this._dumpStdout(); - this._watchProcess(); - this._status = TorProcessStatus.Running; - this._torProcessStartTime = Date.now(); + this.#subprocess = await Subprocess.call(options); + this.#status = TorProcessStatus.Running; } catch (e) { - this._status = TorProcessStatus.Exited; - this._subprocess = null; + this.#status = TorProcessStatus.Exited; + this.#subprocess = null; logger.error("startTor error:", e); throw e; } + + // Do not await the following functions, as they will return only when the + // process exits. + this.#dumpStdout(); + this.#watchProcess(); } // Forget about a process. // - // Instead of killing the tor process, we rely on the TAKEOWNERSHIP feature + // Instead of killing the tor process, we rely on the TAKEOWNERSHIP feature // to shut down tor when we close the control port connection. // // Previously, we sent a SIGNAL HALT command to the tor control port, @@ -123,36 +154,38 @@ export class TorProcess { // Still, before closing the owning connection, this class should forget about // the process, so that future notifications will be ignored. forget() { - this._subprocess = null; - this._status = TorProcessStatus.Exited; + this.#subprocess = null; + this.#status = TorProcessStatus.Exited; } // The owner of the process can use this function to tell us that they // successfully connected to the control port. This information will be used // only to decide which text to show in the confirmation dialog if tor exits. connectionWorked() { - this._didConnectToTorControlPort = true; + this.#didConnectToTorControlPort = true; } - async _dumpStdout() { + async #dumpStdout() { let string; while ( - this._subprocess && - (string = await this._subprocess.stdout.readString()) + this.#subprocess && + (string = await this.#subprocess.stdout.readString()) ) { dump(string); } } - async _watchProcess() { - const watched = this._subprocess; + async #watchProcess() { + const watched = this.#subprocess; if (!watched) { return; } + let processExitCode; try { const { exitCode } = await watched.wait(); + processExitCode = exitCode; - if (watched !== this._subprocess) { + if (watched !== this.#subprocess) { logger.debug(`A Tor process exited with code ${exitCode}.`); } else if (exitCode) { logger.warn(`The watched Tor process exited with code ${exitCode}.`); @@ -163,30 +196,31 @@ export class TorProcess { logger.error("Failed to watch the tor process", e); } - if (watched === this._subprocess) { - this._processExitedUnexpectedly(); + if (watched === this.#subprocess) { + this.#processExitedUnexpectedly(processExitCode); } } - _processExitedUnexpectedly() { - this._subprocess = null; - this._status = TorProcessStatus.Exited; + #processExitedUnexpectedly(exitCode) { + this.#subprocess = null; + this.#status = TorProcessStatus.Exited; // TODO: Move this logic somewhere else? let s; - if (!this._didConnectToTorControlPort) { + if (!this.#didConnectToTorControlPort) { // tor might be misconfigured, becauser we could never connect to it const key = "tor_exited_during_startup"; - s = TorLauncherUtil.getLocalizedString(key); + s = lazy.TorLauncherUtil.getLocalizedString(key); } else { // tor exited suddenly, so configuration should be okay s = - TorLauncherUtil.getLocalizedString("tor_exited") + + lazy.TorLauncherUtil.getLocalizedString("tor_exited") + "\n\n" + - TorLauncherUtil.getLocalizedString("tor_exited2"); + lazy.TorLauncherUtil.getLocalizedString("tor_exited2"); } logger.info(s); - const defaultBtnLabel = TorLauncherUtil.getLocalizedString("restart_tor"); + const defaultBtnLabel = + lazy.TorLauncherUtil.getLocalizedString("restart_tor"); let cancelBtnLabel = "OK"; try { const kSysBundleURI = "chrome://global/locale/commonDialogs.properties"; @@ -196,51 +230,43 @@ export class TorProcess { logger.warn("Could not localize the cancel button", e); } - const restart = TorLauncherUtil.showConfirm( + const restart = lazy.TorLauncherUtil.showConfirm( null, s, defaultBtnLabel, cancelBtnLabel ); if (restart) { - this.start().then(() => { - if (this.onRestart) { - this.onRestart(); - } - }); - } else if (this.onExit) { - this.onExit(); + this.start().then(this.onRestart); + } else { + this.onExit(exitCode); } } - _makeArgs() { - // Ideally, we would cd to the Firefox application directory before - // starting tor (but we don't know how to do that). Instead, we - // rely on the TBB launcher to start Firefox from the right place. - + #makeArgs() { + this.#exeFile = lazy.TorLauncherUtil.getTorFile("tor", false); + const torrcFile = lazy.TorLauncherUtil.getTorFile("torrc", true); // Get the Tor data directory first so it is created before we try to // construct paths to files that will be inside it. - this._exeFile = TorLauncherUtil.getTorFile("tor", false); - const torrcFile = TorLauncherUtil.getTorFile("torrc", true); - this._dataDir = TorLauncherUtil.getTorFile("tordatadir", true); - const onionAuthDir = TorLauncherUtil.getTorFile("toronionauthdir", true); - const hashedPassword = lazy.TorProtocolService.torGetPassword(true); + this.#dataDir = lazy.TorLauncherUtil.getTorFile("tordatadir", true); + const onionAuthDir = lazy.TorLauncherUtil.getTorFile( + "toronionauthdir", + true + ); let detailsKey; - if (!this._exeFile) { + if (!this.#exeFile) { detailsKey = "tor_missing"; } else if (!torrcFile) { detailsKey = "torrc_missing"; - } else if (!this._dataDir) { + } else if (!this.#dataDir) { detailsKey = "datadir_missing"; } else if (!onionAuthDir) { detailsKey = "onionauthdir_missing"; - } else if (!hashedPassword) { - detailsKey = "password_hash_missing"; } if (detailsKey) { - const details = TorLauncherUtil.getLocalizedString(detailsKey); + const details = lazy.TorLauncherUtil.getLocalizedString(detailsKey); const key = "unable_to_start_tor"; - const err = TorLauncherUtil.getFormattedLocalizedString( + const err = lazy.TorLauncherUtil.getFormattedLocalizedString( key, [details], 1 @@ -248,7 +274,7 @@ export class TorProcess { throw new Error(err); } - const torrcDefaultsFile = TorLauncherUtil.getTorFile( + const torrcDefaultsFile = lazy.TorLauncherUtil.getTorFile( "torrc-defaults", false ); @@ -258,77 +284,131 @@ export class TorProcess { const geoip6File = torrcDefaultsFile.clone(); geoip6File.leafName = "geoip6"; - this._args = []; + this.#args = []; if (torrcDefaultsFile) { - this._args.push("--defaults-torrc"); - this._args.push(torrcDefaultsFile.path); + this.#args.push("--defaults-torrc", torrcDefaultsFile.path); } - this._args.push("-f"); - this._args.push(torrcFile.path); - this._args.push("DataDirectory"); - this._args.push(this._dataDir.path); - this._args.push("ClientOnionAuthDir"); - this._args.push(onionAuthDir.path); - this._args.push("GeoIPFile"); - this._args.push(geoipFile.path); - this._args.push("GeoIPv6File"); - this._args.push(geoip6File.path); - this._args.push("HashedControlPassword"); - this._args.push(hashedPassword); + this.#args.push("-f", torrcFile.path); + this.#args.push("DataDirectory", this.#dataDir.path); + this.#args.push("ClientOnionAuthDir", onionAuthDir.path); + this.#args.push("GeoIPFile", geoipFile.path); + this.#args.push("GeoIPv6File", geoip6File.path); } - _addControlPortArg() { - // Include a ControlPort argument to support switching between - // a TCP port and an IPC port (e.g., a Unix domain socket). We - // include a "+__" prefix so that (1) this control port is added - // to any control ports that the user has defined in their torrc - // file and (2) it is never written to torrc. + /** + * Add all the arguments related to the control port. + * We use the + prefix so that the the port is added to any other port already + * defined in the torrc, and the __ prefix so that it is never written to + * torrc. + */ + #addControlPortArgs() { + if (!this.#controlSettings) { + return; + } + let controlPortArg; - const controlIPCFile = lazy.TorProtocolService.torGetControlIPCFile(); - const controlPort = lazy.TorProtocolService.torGetControlPort(); - if (controlIPCFile) { - controlPortArg = this._ipcPortArg(controlIPCFile); - } else if (controlPort) { - controlPortArg = "" + controlPort; + if (this.#controlSettings.ipcFile) { + controlPortArg = this.#controlSettings.ipcFile; + } else if (this.#controlSettings.port) { + controlPortArg = this.#controlSettings.host + ? `${this.#controlSettings.host}:${this.#controlSettings.port}` + : this.#controlSettings.port.toString(); } if (controlPortArg) { - this._args.push("+__ControlPort"); - this._args.push(controlPortArg); + this.#args.push("+__ControlPort", controlPortArg); + } + + if (this.#controlSettings.password) { + this.#args.push( + "HashedControlPassword", + this.#hashPassword(this.#controlSettings.password) + ); + } + if (this.#controlSettings.cookieFilePath) { + this.#args.push("CookieAuthentication", "1"); + this.#args.push("CookieAuthFile", this.#controlSettings.cookieFilePath); } } - _addSocksPortArg() { - // Include a SocksPort argument to support switching between - // a TCP port and an IPC port (e.g., a Unix domain socket). We - // include a "+__" prefix so that (1) this SOCKS port is added - // to any SOCKS ports that the user has defined in their torrc - // file and (2) it is never written to torrc. - const socksPortInfo = lazy.TorProtocolService.torGetSOCKSPortInfo(); - if (socksPortInfo) { - let socksPortArg; - if (socksPortInfo.ipcFile) { - socksPortArg = this._ipcPortArg(socksPortInfo.ipcFile); - } else if (socksPortInfo.host && socksPortInfo.port != 0) { - socksPortArg = socksPortInfo.host + ":" + socksPortInfo.port; - } - if (socksPortArg) { - let socksPortFlags = Services.prefs.getCharPref( - "extensions.torlauncher.socks_port_flags", - "IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth" - ); - if (socksPortFlags) { - socksPortArg += " " + socksPortFlags; - } - this._args.push("+__SocksPort"); - this._args.push(socksPortArg); + /** + * Add the argument related to the control port. + * We use the + prefix so that the the port is added to any other port already + * defined in the torrc, and the __ prefix so that it is never written to + * torrc. + */ + #addSocksPortArg() { + let socksPortArg; + if (this.#socksSettings.ipcFile) { + socksPortArg = this.#socksSettings.ipcFile; + } else if (this.#socksSettings.port != 0) { + socksPortArg = this.#socksSettings.host + ? `${this.#socksSettings.host}:${this.#socksSettings.port}` + : this.#socksSettings.port.toString(); + } + if (socksPortArg) { + const socksPortFlags = Services.prefs.getCharPref( + "extensions.torlauncher.socks_port_flags", + "IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth" + ); + if (socksPortFlags) { + socksPortArg += " " + socksPortFlags; } + this.#args.push("+__SocksPort", socksPortArg); + } + } + + // Based on Vidalia's TorSettings::hashPassword(). + #hashPassword(aHexPassword) { + if (!aHexPassword) { + return null; } + + // Generate a random, 8 byte salt value. + const salt = Array.from(crypto.getRandomValues(new Uint8Array(8))); + + // Convert hex-encoded password to an array of bytes. + const password = []; + for (let i = 0; i < aHexPassword.length; i += 2) { + password.push(parseInt(aHexPassword.substring(i, i + 2), 16)); + } + + // Run through the S2K algorithm and convert to a string. + const toHex = v => v.toString(16).padStart(2, "0"); + const arrayToHex = aArray => aArray.map(toHex).join(""); + const kCodedCount = 96; + const hashVal = this.#cryptoSecretToKey(password, salt, kCodedCount); + return "16:" + arrayToHex(salt) + toHex(kCodedCount) + arrayToHex(hashVal); } - // Return a ControlPort or SocksPort argument for aIPCFile (an nsIFile). - // The result is unix:/path or unix:"/path with spaces" with appropriate - // C-style escaping within the path portion. - _ipcPortArg(aIPCFile) { - return "unix:" + TorParsers.escapeString(aIPCFile.path); + // #cryptoSecretToKey() is similar to Vidalia's crypto_secret_to_key(). + // It generates and returns a hash of aPassword by following the iterated + // and salted S2K algorithm (see RFC 2440 section 3.6.1.3). + // See also https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/control-spec.txt…. + // Returns an array of bytes. + #cryptoSecretToKey(aPassword, aSalt, aCodedCount) { + const inputArray = aSalt.concat(aPassword); + + // Subtle crypto only has the final digest, and does not allow incremental + // updates. + const hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + hasher.init(hasher.SHA1); + const kEXPBIAS = 6; + let count = (16 + (aCodedCount & 15)) << ((aCodedCount >> 4) + kEXPBIAS); + while (count > 0) { + if (count > inputArray.length) { + hasher.update(inputArray, inputArray.length); + count -= inputArray.length; + } else { + const finalArray = inputArray.slice(0, count); + hasher.update(finalArray, finalArray.length); + count = 0; + } + } + return hasher + .finish(false) + .split("") + .map(b => b.charCodeAt(0)); } } ===================================== toolkit/components/tor-launcher/TorProtocolService.sys.mjs ===================================== @@ -289,9 +289,8 @@ export const TorProtocolService = { // are also used in torbutton. // Returns Tor password string or null if an error occurs. - torGetPassword(aPleaseHash) { - const pw = this._controlPassword; - return aPleaseHash ? this._hashPassword(pw) : pw; + torGetPassword() { + return this._controlPassword; }, torGetControlIPCFile() { @@ -306,6 +305,24 @@ export const TorProtocolService = { return this._SOCKSPortInfo; }, + get torControlPortInfo() { + const info = { + password: this._controlPassword, + }; + if (this._controlIPCFile) { + info.ipcFile = this._controlIPCFile?.clone(); + } + if (this._controlPort) { + info.host = this._controlHost; + info.port = this._controlPort; + } + return info; + }, + + get torSOCKSPortInfo() { + return this._SOCKSPortInfo; + }, + // Public, but called only internally // Executes a command on the control port. @@ -469,115 +486,8 @@ export const TorProtocolService = { this._controlPassword = this._generateRandomPassword(); } - // Determine what kind of SOCKS port Tor and the browser will use. - // On Windows (where Unix domain sockets are not supported), TCP is - // always used. - // - // The following environment variables are supported and take - // precedence over preferences: - // TOR_SOCKS_IPC_PATH (file system path; ignored on Windows) - // TOR_SOCKS_HOST - // TOR_SOCKS_PORT - // - // The following preferences are consulted: - // network.proxy.socks - // network.proxy.socks_port - // extensions.torlauncher.socks_port_use_ipc (Boolean) - // extensions.torlauncher.socks_ipc_path (file system path) - // If extensions.torlauncher.socks_ipc_path is empty, a default - // path is used (<tor-data-directory>/socks.socket). - // - // When using TCP, if a value is not defined via an env variable it is - // taken from the corresponding browser preference if possible. The - // exceptions are: - // If network.proxy.socks contains a file: URL, a default value of - // "127.0.0.1" is used instead. - // If the network.proxy.socks_port value is 0, a default value of - // 9150 is used instead. - // - // Supported scenarios: - // 1. By default, an IPC object at a default path is used. - // 2. If extensions.torlauncher.socks_port_use_ipc is set to false, - // a TCP socket at 127.0.0.1:9150 is used, unless different values - // are set in network.proxy.socks and network.proxy.socks_port. - // 3. If the TOR_SOCKS_IPC_PATH env var is set, an IPC object at that - // path is used (e.g., a Unix domain socket). - // 4. If the TOR_SOCKS_HOST and/or TOR_SOCKS_PORT env vars are set, TCP - // is used. Values not set via env vars will be taken from the - // network.proxy.socks and network.proxy.socks_port prefs as described - // above. - // 5. If extensions.torlauncher.socks_port_use_ipc is true and - // extensions.torlauncher.socks_ipc_path is set, an IPC object at - // the specified path is used. - // 6. Tor Launcher is disabled. Torbutton will respect the env vars if - // present; if not, the values in network.proxy.socks and - // network.proxy.socks_port are used without modification. - - let useIPC; - this._SOCKSPortInfo = { ipcFile: undefined, host: undefined, port: 0 }; - if (!isWindows && Services.env.exists("TOR_SOCKS_IPC_PATH")) { - let ipcPath = Services.env.get("TOR_SOCKS_IPC_PATH"); - this._SOCKSPortInfo.ipcFile = new lazy.FileUtils.File(ipcPath); - useIPC = true; - } else { - // Check for TCP host and port environment variables. - if (Services.env.exists("TOR_SOCKS_HOST")) { - this._SOCKSPortInfo.host = Services.env.get("TOR_SOCKS_HOST"); - useIPC = false; - } - if (Services.env.exists("TOR_SOCKS_PORT")) { - this._SOCKSPortInfo.port = parseInt( - Services.env.get("TOR_SOCKS_PORT"), - 10 - ); - useIPC = false; - } - } - - if (useIPC === undefined) { - useIPC = - !isWindows && - Services.prefs.getBoolPref( - "extensions.torlauncher.socks_port_use_ipc", - false - ); - } - - // Fill in missing SOCKS info from prefs. - if (useIPC) { - if (!this._SOCKSPortInfo.ipcFile) { - this._SOCKSPortInfo.ipcFile = TorLauncherUtil.getTorFile( - "socks_ipc", - false - ); - } - } else { - if (!this._SOCKSPortInfo.host) { - let socksAddr = Services.prefs.getCharPref( - "network.proxy.socks", - "127.0.0.1" - ); - let socksAddrHasHost = socksAddr && !socksAddr.startsWith("file:"); - this._SOCKSPortInfo.host = socksAddrHasHost ? socksAddr : "127.0.0.1"; - } - - if (!this._SOCKSPortInfo.port) { - let socksPort = Services.prefs.getIntPref( - "network.proxy.socks_port", - 0 - ); - // This pref is set as 0 by default in Firefox, use 9150 if we get 0. - this._SOCKSPortInfo.port = socksPort != 0 ? socksPort : 9150; - } - } - - logger.info("SOCKS port type: " + (useIPC ? "IPC" : "TCP")); - if (useIPC) { - logger.info(`ipcFile: ${this._SOCKSPortInfo.ipcFile.path}`); - } else { - logger.info(`SOCKS host: ${this._SOCKSPortInfo.host}`); - logger.info(`SOCKS port: ${this._SOCKSPortInfo.port}`); - } + this._SOCKSPortInfo = TorLauncherUtil.getPreferredSocksConfiguration(); + TorLauncherUtil.setProxyConfiguration(this._SOCKSPortInfo); // Set the global control port info parameters. // These values may be overwritten by torbutton when it initializes, but @@ -781,38 +691,6 @@ export const TorProtocolService = { return pwd; }, - // Based on Vidalia's TorSettings::hashPassword(). - _hashPassword(aHexPassword) { - if (!aHexPassword) { - return null; - } - - // Generate a random, 8 byte salt value. - const salt = Array.from(crypto.getRandomValues(new Uint8Array(8))); - - // Convert hex-encoded password to an array of bytes. - const password = []; - for (let i = 0; i < aHexPassword.length; i += 2) { - password.push(parseInt(aHexPassword.substring(i, i + 2), 16)); - } - - // Run through the S2K algorithm and convert to a string. - const kCodedCount = 96; - const hashVal = this._cryptoSecretToKey(password, salt, kCodedCount); - if (!hashVal) { - logger.error("_cryptoSecretToKey() failed"); - return null; - } - - const arrayToHex = aArray => - aArray.map(item => this._toHex(item, 2)).join(""); - let rv = "16:"; - rv += arrayToHex(salt); - rv += this._toHex(kCodedCount, 2); - rv += arrayToHex(hashVal); - return rv; - }, - // Returns -1 upon failure. _cryptoRandInt(aMax) { // Based on tor's crypto_rand_int(). @@ -831,43 +709,6 @@ export const TorProtocolService = { return val % aMax; }, - // _cryptoSecretToKey() is similar to Vidalia's crypto_secret_to_key(). - // It generates and returns a hash of aPassword by following the iterated - // and salted S2K algorithm (see RFC 2440 section 3.6.1.3). - // Returns an array of bytes. - _cryptoSecretToKey(aPassword, aSalt, aCodedCount) { - if (!aPassword || !aSalt) { - return null; - } - - const inputArray = aSalt.concat(aPassword); - - // Subtle crypto only has the final digest, and does not allow incremental - // updates. Also, it is async, so we should hash and keep the hash in a - // variable if we wanted to switch to getters. - // So, keeping this implementation should be okay for now. - const hasher = Cc["@mozilla.org/security/hash;1"].createInstance( - Ci.nsICryptoHash - ); - hasher.init(hasher.SHA1); - const kEXPBIAS = 6; - let count = (16 + (aCodedCount & 15)) << ((aCodedCount >> 4) + kEXPBIAS); - while (count > 0) { - if (count > inputArray.length) { - hasher.update(inputArray, inputArray.length); - count -= inputArray.length; - } else { - const finalArray = inputArray.slice(0, count); - hasher.update(finalArray, finalArray.length); - count = 0; - } - } - return hasher - .finish(false) - .split("") - .map(b => b.charCodeAt(0)); - }, - _toHex(aValue, aMinLen) { return aValue.toString(16).padStart(aMinLen, "0"); }, ===================================== toolkit/torbutton/chrome/content/torbutton.js ===================================== @@ -60,7 +60,7 @@ var torbutton_init; } else { try { // Try to get password from Tor Launcher. - m_tb_control_pass = TorProtocolService.torGetPassword(false); + m_tb_control_pass = TorProtocolService.torGetPassword(); } catch (e) {} } ===================================== toolkit/torbutton/components.conf ===================================== @@ -1,12 +1,4 @@ Classes = [ - { - "cid": "{06322def-6fde-4c06-aef6-47ae8e799629}", - "contract_ids": [ - "@torproject.org/startup-observer;1" - ], - "jsm": "resource://torbutton/modules/TorbuttonStartupObserver.jsm", - "constructor": "StartupObserver", - }, { "cid": "{f36d72c9-9718-4134-b550-e109638331d7}", "contract_ids": [ ===================================== toolkit/torbutton/modules/TorbuttonStartupObserver.jsm deleted ===================================== @@ -1,138 +0,0 @@ -// Bug 1506 P1-3: This code is mostly hackish remnants of session store -// support. There are a couple of observer events that *might* be worth -// listening to. Search for 1506 in the code. - -/************************************************************************* - * Startup observer (JavaScript XPCOM component) - * - * Cases tested (each during Tor and Non-Tor, FF4 and FF3.6) - * 1. Crash - * 2. Upgrade - * 3. Fresh install - * - *************************************************************************/ - -var EXPORTED_SYMBOLS = ["StartupObserver"]; - -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); - -const { TorProtocolService } = ChromeUtils.import( - "resource://gre/modules/TorProtocolService.jsm" -); - -const lazy = {}; - -XPCOMUtils.defineLazyModuleGetters(lazy, { - FileUtils: "resource://gre/modules/FileUtils.jsm", -}); - -function StartupObserver() { - this.logger = Cc["@torproject.org/torbutton-logger;1"].getService( - Ci.nsISupports - ).wrappedJSObject; - this._prefs = Services.prefs; - this.logger.log(3, "Startup Observer created"); - - try { - // XXX: We're in a race with HTTPS-Everywhere to update our proxy settings - // before the initial SSL-Observatory test... If we lose the race, Firefox - // caches the old proxy settings for check.tp.o somehwere, and it never loads :( - this.setProxySettings(); - } catch (e) { - this.logger.log( - 4, - "Early proxy change failed. Will try again at profile load. Error: " + e - ); - } -} - -StartupObserver.prototype = { - // Bug 6803: We need to get the env vars early due to - // some weird proxy caching code that showed up in FF15. - // Otherwise, homepage domain loads fail forever. - setProxySettings() { - // Bug 1506: Still want to get these env vars - if (Services.env.exists("TOR_TRANSPROXY")) { - this.logger.log(3, "Resetting Tor settings to transproxy"); - this._prefs.setBoolPref("network.proxy.socks_remote_dns", false); - this._prefs.setIntPref("network.proxy.type", 0); - this._prefs.setIntPref("network.proxy.socks_port", 0); - this._prefs.setCharPref("network.proxy.socks", ""); - } else { - // Try to retrieve SOCKS proxy settings from Tor Launcher. - let socksPortInfo; - try { - socksPortInfo = TorProtocolService.torGetSOCKSPortInfo(); - } catch (e) { - this.logger.log(3, "tor launcher failed " + e); - } - - // If Tor Launcher is not available, check environment variables. - if (!socksPortInfo) { - socksPortInfo = { ipcFile: undefined, host: undefined, port: 0 }; - - let isWindows = Services.appinfo.OS === "WINNT"; - if (!isWindows && Services.env.exists("TOR_SOCKS_IPC_PATH")) { - socksPortInfo.ipcFile = new lazy.FileUtils.File( - Services.env.get("TOR_SOCKS_IPC_PATH") - ); - } else { - if (Services.env.exists("TOR_SOCKS_HOST")) { - socksPortInfo.host = Services.env.get("TOR_SOCKS_HOST"); - } - if (Services.env.exists("TOR_SOCKS_PORT")) { - socksPortInfo.port = parseInt(Services.env.get("TOR_SOCKS_PORT")); - } - } - } - - // Adjust network.proxy prefs. - if (socksPortInfo.ipcFile) { - let fph = Services.io - .getProtocolHandler("file") - .QueryInterface(Ci.nsIFileProtocolHandler); - let fileURI = fph.newFileURI(socksPortInfo.ipcFile); - this.logger.log(3, "Reset socks to " + fileURI.spec); - this._prefs.setCharPref("network.proxy.socks", fileURI.spec); - this._prefs.setIntPref("network.proxy.socks_port", 0); - } else { - if (socksPortInfo.host) { - this._prefs.setCharPref("network.proxy.socks", socksPortInfo.host); - this.logger.log(3, "Reset socks host to " + socksPortInfo.host); - } - if (socksPortInfo.port) { - this._prefs.setIntPref( - "network.proxy.socks_port", - socksPortInfo.port - ); - this.logger.log(3, "Reset socks port to " + socksPortInfo.port); - } - } - - if (socksPortInfo.ipcFile || socksPortInfo.host || socksPortInfo.port) { - this._prefs.setBoolPref("network.proxy.socks_remote_dns", true); - this._prefs.setIntPref("network.proxy.type", 1); - } - } - - // Force prefs to be synced to disk - Services.prefs.savePrefFile(null); - - this.logger.log(3, "Synced network settings to environment."); - }, - - observe(subject, topic, data) { - if (topic == "profile-after-change") { - this.setProxySettings(); - } - - // In all cases, force prefs to be synced to disk - Services.prefs.savePrefFile(null); - }, - - // Hack to get us registered early to observe recovery - _xpcom_categories: [{ category: "profile-after-change" }], -}; ===================================== toolkit/torbutton/moz.build ===================================== @@ -8,7 +8,3 @@ JAR_MANIFESTS += ['jar.mn'] XPCOM_MANIFESTS += [ "components.conf", ] - -EXTRA_COMPONENTS += [ - "torbutton.manifest", -] ===================================== toolkit/torbutton/torbutton.manifest deleted ===================================== @@ -1 +0,0 @@ -category profile-after-change StartupObserver @torproject.org/startup-observer;1 View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f0493f… -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f0493f… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
[Git][tpo/applications/tor-browser][base-browser-115.0.2esr-13.0-1] Deleted 2 commits: Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs
by richard (@richard) 31 Jul '23

31 Jul '23
richard pushed to branch base-browser-115.0.2esr-13.0-1 at The Tor Project / Applications / Tor Browser WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. Deleted commits: 60fefea4 by Edgar Chen at 2023-07-31T17:56:34+00:00 Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs Fullscreen/PointerLock warnings are initialized with hidden=&quot;true&quot;, but change to hidden=&quot;&quot; after being shown and hidden again. I think this started happening when we began using HTML elements instead of XUL as they handle hidden attribute differently. Differential Revision: https://phabricator.services.mozilla.com/D177790 - - - - - 524c7622 by Edgar Chen at 2023-07-31T17:56:38+00:00 Bug 1821884 - Reshow initial fullscreen notification; r=Gijs Depends on D177790 Differential Revision: https://phabricator.services.mozilla.com/D178339 - - - - - 4 changed files: - browser/base/content/browser-fullScreenAndPointerLock.js - browser/base/content/fullscreen-and-pointerlock.inc.xhtml - browser/base/content/test/fullscreen/browser_fullscreen_warning.js - dom/tests/browser/browser_pointerlock_warning.js Changes: ===================================== browser/base/content/browser-fullScreenAndPointerLock.js ===================================== @@ -62,9 +62,14 @@ var PointerlockFsWarning = { this._element = document.getElementById(elementId); // Setup event listeners this._element.addEventListener("transitionend", this); + this._element.addEventListener("transitioncancel", this); window.addEventListener("mousemove", this, true); + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); // The timeout to hide the warning box after a while. this._timeoutHide = new this.Timeout(() => { + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); this._state = "hidden"; }, timeout); // The timeout to show the warning box when the pointer is at the top @@ -116,11 +121,10 @@ var PointerlockFsWarning = { return; } - // Explicitly set the last state to hidden to avoid the warning - // box being hidden immediately because of mousemove. - this._state = "onscreen"; - this._lastState = "hidden"; - this._timeoutHide.start(); + if (Services.focus.activeWindow == window) { + this._state = "onscreen"; + this._timeoutHide.start(); + } }, /** @@ -148,7 +152,10 @@ var PointerlockFsWarning = { this._element.hidden = true; // Remove all event listeners this._element.removeEventListener("transitionend", this); + this._element.removeEventListener("transitioncancel", this); window.removeEventListener("mousemove", this, true); + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); // Clear fields this._element = null; this._timeoutHide = null; @@ -186,7 +193,7 @@ var PointerlockFsWarning = { } if (newState != "hidden") { if (currentState != "hidden") { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } else { // When the previous state is hidden, the display was none, // thus no box was constructed. We need to wait for the new @@ -197,7 +204,7 @@ var PointerlockFsWarning = { requestAnimationFrame(() => { requestAnimationFrame(() => { if (this._element) { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } }); }); @@ -217,7 +224,7 @@ var PointerlockFsWarning = { } else if (this._timeoutShow.delay >= 0) { this._timeoutShow.start(); } - } else { + } else if (state != "onscreen") { let elemRect = this._element.getBoundingClientRect(); if (state == "hiding" && this._lastState != "hidden") { // If we are on the hiding transition, and the pointer @@ -239,12 +246,23 @@ var PointerlockFsWarning = { } break; } - case "transitionend": { + case "transitionend": + case "transitioncancel": { if (this._state == "hiding") { this._element.hidden = true; } break; } + case "activate": { + this._state = "onscreen"; + this._timeoutHide.start(); + break; + } + case "deactivate": { + this._state = "hidden"; + this._timeoutHide.cancel(); + break; + } } }, }; ===================================== browser/base/content/fullscreen-and-pointerlock.inc.xhtml ===================================== @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. <html:div id="fullscreen-and-pointerlock-wrapper"> - <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> @@ -20,7 +20,7 @@ </html:button> </html:div> - <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> ===================================== browser/base/content/test/fullscreen/browser_fullscreen_warning.js ===================================== @@ -3,14 +3,35 @@ "use strict"; -add_task(async function test_fullscreen_display_none() { +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + +add_setup(async function init() { await SpecialPowers.pushPrefEnv({ set: [ ["full-screen-api.enabled", true], ["full-screen-api.allow-trusted-requests-only", false], ], }); +}); +add_task(async function test_fullscreen_display_none() { await BrowserTestUtils.withNewTab( { gBrowser, @@ -30,11 +51,13 @@ add_task(async function test_fullscreen_display_none() { }, async function (browser) { let warning = document.getElementById("fullscreen-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( warning, - "true" + "hidden", + "Should not show full screen warning initially" ); + + let warningShownPromise = waitForWarningState(warning, "onscreen"); // Enter fullscreen await SpecialPowers.spawn(browser, [], async () => { let frame = content.document.querySelector("iframe"); @@ -54,39 +77,33 @@ add_task(async function test_fullscreen_display_none() { ); document.getElementById("fullscreen-exit-button").click(); await exitFullscreenPromise; + + checkWarningState( + warning, + "hidden", + "Should hide fullscreen warning after exiting fullscreen" + ); } ); }); add_task(async function test_fullscreen_pointerlock_conflict() { - await SpecialPowers.pushPrefEnv({ - set: [ - ["full-screen-api.enabled", true], - ["full-screen-api.allow-trusted-requests-only", false], - ], - }); - await BrowserTestUtils.withNewTab("https://example.com", async browser => { let fsWarning = document.getElementById("fullscreen-warning"); let plWarning = document.getElementById("pointerlock-warning"); - is( - fsWarning.getAttribute("onscreen"), - null, - "Should not show full screen warning initially." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning initially." - ); - - let fsWarningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( fsWarning, - "true" + "hidden", + "Should not show full screen warning initially" + ); + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning initially" ); + let fsWarningShownPromise = waitForWarningState(fsWarning, "onscreen"); info("Entering full screen and pointer lock."); await SpecialPowers.spawn(browser, [], async () => { await content.document.body.requestFullscreen(); @@ -94,15 +111,10 @@ add_task(async function test_fullscreen_pointerlock_conflict() { }); await fsWarningShownPromise; - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should show full screen warning." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); info("Exiting pointerlock"); @@ -110,18 +122,19 @@ add_task(async function test_fullscreen_pointerlock_conflict() { await content.document.exitPointerLock(); }); - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should still show full screen warning." + checkWarningState( + fsWarning, + "onscreen", + "Should still show full screen warning" ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); // Cleanup + info("Exiting fullscreen"); await document.exitFullscreen(); }); }); ===================================== dom/tests/browser/browser_pointerlock_warning.js ===================================== @@ -15,6 +15,25 @@ const FRAME_TEST_URL = encodeURI(BODY_URL) + '"></iframe></body>'; +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + // Make sure the pointerlock warning is shown and exited with the escape key add_task(async function show_pointerlock_warning_escape() { let urls = [TEST_URL, FRAME_TEST_URL]; @@ -24,11 +43,7 @@ add_task(async function show_pointerlock_warning_escape() { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); let warning = document.getElementById("pointerlock-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", - warning, - "true" - ); + let warningShownPromise = waitForWarningState(warning, "onscreen"); let expectedWarningText; @@ -49,11 +64,7 @@ add_task(async function show_pointerlock_warning_escape() { ok(true, "Pointerlock warning shown"); - let warningHiddenPromise = BrowserTestUtils.waitForAttribute( - "hidden", - warning, - "" - ); + let warningHiddenPromise = waitForWarningState(warning, "hidden"); await BrowserTestUtils.waitForCondition( () => warning.innerText == expectedWarningText, View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f3faeb… -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f3faeb… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
[Git][tpo/applications/tor-browser][tor-browser-115.0.2esr-13.0-1] Deleted 2 commits: Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs
by richard (@richard) 31 Jul '23

31 Jul '23
richard pushed to branch tor-browser-115.0.2esr-13.0-1 at The Tor Project / Applications / Tor Browser WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. Deleted commits: 6a6c68cf by Edgar Chen at 2023-07-31T17:56:02+00:00 Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs Fullscreen/PointerLock warnings are initialized with hidden=&quot;true&quot;, but change to hidden=&quot;&quot; after being shown and hidden again. I think this started happening when we began using HTML elements instead of XUL as they handle hidden attribute differently. Differential Revision: https://phabricator.services.mozilla.com/D177790 - - - - - 5e8c1b08 by Edgar Chen at 2023-07-31T17:56:07+00:00 Bug 1821884 - Reshow initial fullscreen notification; r=Gijs Depends on D177790 Differential Revision: https://phabricator.services.mozilla.com/D178339 - - - - - 4 changed files: - browser/base/content/browser-fullScreenAndPointerLock.js - browser/base/content/fullscreen-and-pointerlock.inc.xhtml - browser/base/content/test/fullscreen/browser_fullscreen_warning.js - dom/tests/browser/browser_pointerlock_warning.js Changes: ===================================== browser/base/content/browser-fullScreenAndPointerLock.js ===================================== @@ -62,9 +62,14 @@ var PointerlockFsWarning = { this._element = document.getElementById(elementId); // Setup event listeners this._element.addEventListener("transitionend", this); + this._element.addEventListener("transitioncancel", this); window.addEventListener("mousemove", this, true); + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); // The timeout to hide the warning box after a while. this._timeoutHide = new this.Timeout(() => { + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); this._state = "hidden"; }, timeout); // The timeout to show the warning box when the pointer is at the top @@ -116,11 +121,10 @@ var PointerlockFsWarning = { return; } - // Explicitly set the last state to hidden to avoid the warning - // box being hidden immediately because of mousemove. - this._state = "onscreen"; - this._lastState = "hidden"; - this._timeoutHide.start(); + if (Services.focus.activeWindow == window) { + this._state = "onscreen"; + this._timeoutHide.start(); + } }, /** @@ -148,7 +152,10 @@ var PointerlockFsWarning = { this._element.hidden = true; // Remove all event listeners this._element.removeEventListener("transitionend", this); + this._element.removeEventListener("transitioncancel", this); window.removeEventListener("mousemove", this, true); + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); // Clear fields this._element = null; this._timeoutHide = null; @@ -186,7 +193,7 @@ var PointerlockFsWarning = { } if (newState != "hidden") { if (currentState != "hidden") { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } else { // When the previous state is hidden, the display was none, // thus no box was constructed. We need to wait for the new @@ -197,7 +204,7 @@ var PointerlockFsWarning = { requestAnimationFrame(() => { requestAnimationFrame(() => { if (this._element) { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } }); }); @@ -217,7 +224,7 @@ var PointerlockFsWarning = { } else if (this._timeoutShow.delay >= 0) { this._timeoutShow.start(); } - } else { + } else if (state != "onscreen") { let elemRect = this._element.getBoundingClientRect(); if (state == "hiding" && this._lastState != "hidden") { // If we are on the hiding transition, and the pointer @@ -239,12 +246,23 @@ var PointerlockFsWarning = { } break; } - case "transitionend": { + case "transitionend": + case "transitioncancel": { if (this._state == "hiding") { this._element.hidden = true; } break; } + case "activate": { + this._state = "onscreen"; + this._timeoutHide.start(); + break; + } + case "deactivate": { + this._state = "hidden"; + this._timeoutHide.cancel(); + break; + } } }, }; ===================================== browser/base/content/fullscreen-and-pointerlock.inc.xhtml ===================================== @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. <html:div id="fullscreen-and-pointerlock-wrapper"> - <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> @@ -20,7 +20,7 @@ </html:button> </html:div> - <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> ===================================== browser/base/content/test/fullscreen/browser_fullscreen_warning.js ===================================== @@ -3,14 +3,35 @@ "use strict"; -add_task(async function test_fullscreen_display_none() { +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + +add_setup(async function init() { await SpecialPowers.pushPrefEnv({ set: [ ["full-screen-api.enabled", true], ["full-screen-api.allow-trusted-requests-only", false], ], }); +}); +add_task(async function test_fullscreen_display_none() { await BrowserTestUtils.withNewTab( { gBrowser, @@ -30,11 +51,13 @@ add_task(async function test_fullscreen_display_none() { }, async function (browser) { let warning = document.getElementById("fullscreen-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( warning, - "true" + "hidden", + "Should not show full screen warning initially" ); + + let warningShownPromise = waitForWarningState(warning, "onscreen"); // Enter fullscreen await SpecialPowers.spawn(browser, [], async () => { let frame = content.document.querySelector("iframe"); @@ -54,39 +77,33 @@ add_task(async function test_fullscreen_display_none() { ); document.getElementById("fullscreen-exit-button").click(); await exitFullscreenPromise; + + checkWarningState( + warning, + "hidden", + "Should hide fullscreen warning after exiting fullscreen" + ); } ); }); add_task(async function test_fullscreen_pointerlock_conflict() { - await SpecialPowers.pushPrefEnv({ - set: [ - ["full-screen-api.enabled", true], - ["full-screen-api.allow-trusted-requests-only", false], - ], - }); - await BrowserTestUtils.withNewTab("https://example.com", async browser => { let fsWarning = document.getElementById("fullscreen-warning"); let plWarning = document.getElementById("pointerlock-warning"); - is( - fsWarning.getAttribute("onscreen"), - null, - "Should not show full screen warning initially." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning initially." - ); - - let fsWarningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( fsWarning, - "true" + "hidden", + "Should not show full screen warning initially" + ); + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning initially" ); + let fsWarningShownPromise = waitForWarningState(fsWarning, "onscreen"); info("Entering full screen and pointer lock."); await SpecialPowers.spawn(browser, [], async () => { await content.document.body.requestFullscreen(); @@ -94,15 +111,10 @@ add_task(async function test_fullscreen_pointerlock_conflict() { }); await fsWarningShownPromise; - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should show full screen warning." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); info("Exiting pointerlock"); @@ -110,18 +122,19 @@ add_task(async function test_fullscreen_pointerlock_conflict() { await content.document.exitPointerLock(); }); - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should still show full screen warning." + checkWarningState( + fsWarning, + "onscreen", + "Should still show full screen warning" ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); // Cleanup + info("Exiting fullscreen"); await document.exitFullscreen(); }); }); ===================================== dom/tests/browser/browser_pointerlock_warning.js ===================================== @@ -15,6 +15,25 @@ const FRAME_TEST_URL = encodeURI(BODY_URL) + '"></iframe></body>'; +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + // Make sure the pointerlock warning is shown and exited with the escape key add_task(async function show_pointerlock_warning_escape() { let urls = [TEST_URL, FRAME_TEST_URL]; @@ -24,11 +43,7 @@ add_task(async function show_pointerlock_warning_escape() { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); let warning = document.getElementById("pointerlock-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", - warning, - "true" - ); + let warningShownPromise = waitForWarningState(warning, "onscreen"); let expectedWarningText; @@ -49,11 +64,7 @@ add_task(async function show_pointerlock_warning_escape() { ok(true, "Pointerlock warning shown"); - let warningHiddenPromise = BrowserTestUtils.waitForAttribute( - "hidden", - warning, - "" - ); + let warningHiddenPromise = waitForWarningState(warning, "hidden"); await BrowserTestUtils.waitForCondition( () => warning.innerText == expectedWarningText, View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/93ef5b… -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/93ef5b… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
[Git][tpo/applications/tor-browser][tor-browser-115.0.2esr-13.0-1] 2 commits: Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs
by richard (@richard) 31 Jul '23

31 Jul '23
richard pushed to branch tor-browser-115.0.2esr-13.0-1 at The Tor Project / Applications / Tor Browser Commits: 6a6c68cf by Edgar Chen at 2023-07-31T17:56:02+00:00 Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs Fullscreen/PointerLock warnings are initialized with hidden=&quot;true&quot;, but change to hidden=&quot;&quot; after being shown and hidden again. I think this started happening when we began using HTML elements instead of XUL as they handle hidden attribute differently. Differential Revision: https://phabricator.services.mozilla.com/D177790 - - - - - 5e8c1b08 by Edgar Chen at 2023-07-31T17:56:07+00:00 Bug 1821884 - Reshow initial fullscreen notification; r=Gijs Depends on D177790 Differential Revision: https://phabricator.services.mozilla.com/D178339 - - - - - 4 changed files: - browser/base/content/browser-fullScreenAndPointerLock.js - browser/base/content/fullscreen-and-pointerlock.inc.xhtml - browser/base/content/test/fullscreen/browser_fullscreen_warning.js - dom/tests/browser/browser_pointerlock_warning.js Changes: ===================================== browser/base/content/browser-fullScreenAndPointerLock.js ===================================== @@ -62,9 +62,14 @@ var PointerlockFsWarning = { this._element = document.getElementById(elementId); // Setup event listeners this._element.addEventListener("transitionend", this); + this._element.addEventListener("transitioncancel", this); window.addEventListener("mousemove", this, true); + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); // The timeout to hide the warning box after a while. this._timeoutHide = new this.Timeout(() => { + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); this._state = "hidden"; }, timeout); // The timeout to show the warning box when the pointer is at the top @@ -116,11 +121,10 @@ var PointerlockFsWarning = { return; } - // Explicitly set the last state to hidden to avoid the warning - // box being hidden immediately because of mousemove. - this._state = "onscreen"; - this._lastState = "hidden"; - this._timeoutHide.start(); + if (Services.focus.activeWindow == window) { + this._state = "onscreen"; + this._timeoutHide.start(); + } }, /** @@ -148,7 +152,10 @@ var PointerlockFsWarning = { this._element.hidden = true; // Remove all event listeners this._element.removeEventListener("transitionend", this); + this._element.removeEventListener("transitioncancel", this); window.removeEventListener("mousemove", this, true); + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); // Clear fields this._element = null; this._timeoutHide = null; @@ -186,7 +193,7 @@ var PointerlockFsWarning = { } if (newState != "hidden") { if (currentState != "hidden") { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } else { // When the previous state is hidden, the display was none, // thus no box was constructed. We need to wait for the new @@ -197,7 +204,7 @@ var PointerlockFsWarning = { requestAnimationFrame(() => { requestAnimationFrame(() => { if (this._element) { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } }); }); @@ -217,7 +224,7 @@ var PointerlockFsWarning = { } else if (this._timeoutShow.delay >= 0) { this._timeoutShow.start(); } - } else { + } else if (state != "onscreen") { let elemRect = this._element.getBoundingClientRect(); if (state == "hiding" && this._lastState != "hidden") { // If we are on the hiding transition, and the pointer @@ -239,12 +246,23 @@ var PointerlockFsWarning = { } break; } - case "transitionend": { + case "transitionend": + case "transitioncancel": { if (this._state == "hiding") { this._element.hidden = true; } break; } + case "activate": { + this._state = "onscreen"; + this._timeoutHide.start(); + break; + } + case "deactivate": { + this._state = "hidden"; + this._timeoutHide.cancel(); + break; + } } }, }; ===================================== browser/base/content/fullscreen-and-pointerlock.inc.xhtml ===================================== @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. <html:div id="fullscreen-and-pointerlock-wrapper"> - <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> @@ -20,7 +20,7 @@ </html:button> </html:div> - <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> ===================================== browser/base/content/test/fullscreen/browser_fullscreen_warning.js ===================================== @@ -3,14 +3,35 @@ "use strict"; -add_task(async function test_fullscreen_display_none() { +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + +add_setup(async function init() { await SpecialPowers.pushPrefEnv({ set: [ ["full-screen-api.enabled", true], ["full-screen-api.allow-trusted-requests-only", false], ], }); +}); +add_task(async function test_fullscreen_display_none() { await BrowserTestUtils.withNewTab( { gBrowser, @@ -30,11 +51,13 @@ add_task(async function test_fullscreen_display_none() { }, async function (browser) { let warning = document.getElementById("fullscreen-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( warning, - "true" + "hidden", + "Should not show full screen warning initially" ); + + let warningShownPromise = waitForWarningState(warning, "onscreen"); // Enter fullscreen await SpecialPowers.spawn(browser, [], async () => { let frame = content.document.querySelector("iframe"); @@ -54,39 +77,33 @@ add_task(async function test_fullscreen_display_none() { ); document.getElementById("fullscreen-exit-button").click(); await exitFullscreenPromise; + + checkWarningState( + warning, + "hidden", + "Should hide fullscreen warning after exiting fullscreen" + ); } ); }); add_task(async function test_fullscreen_pointerlock_conflict() { - await SpecialPowers.pushPrefEnv({ - set: [ - ["full-screen-api.enabled", true], - ["full-screen-api.allow-trusted-requests-only", false], - ], - }); - await BrowserTestUtils.withNewTab("https://example.com", async browser => { let fsWarning = document.getElementById("fullscreen-warning"); let plWarning = document.getElementById("pointerlock-warning"); - is( - fsWarning.getAttribute("onscreen"), - null, - "Should not show full screen warning initially." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning initially." - ); - - let fsWarningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( fsWarning, - "true" + "hidden", + "Should not show full screen warning initially" + ); + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning initially" ); + let fsWarningShownPromise = waitForWarningState(fsWarning, "onscreen"); info("Entering full screen and pointer lock."); await SpecialPowers.spawn(browser, [], async () => { await content.document.body.requestFullscreen(); @@ -94,15 +111,10 @@ add_task(async function test_fullscreen_pointerlock_conflict() { }); await fsWarningShownPromise; - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should show full screen warning." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); info("Exiting pointerlock"); @@ -110,18 +122,19 @@ add_task(async function test_fullscreen_pointerlock_conflict() { await content.document.exitPointerLock(); }); - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should still show full screen warning." + checkWarningState( + fsWarning, + "onscreen", + "Should still show full screen warning" ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); // Cleanup + info("Exiting fullscreen"); await document.exitFullscreen(); }); }); ===================================== dom/tests/browser/browser_pointerlock_warning.js ===================================== @@ -15,6 +15,25 @@ const FRAME_TEST_URL = encodeURI(BODY_URL) + '"></iframe></body>'; +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + // Make sure the pointerlock warning is shown and exited with the escape key add_task(async function show_pointerlock_warning_escape() { let urls = [TEST_URL, FRAME_TEST_URL]; @@ -24,11 +43,7 @@ add_task(async function show_pointerlock_warning_escape() { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); let warning = document.getElementById("pointerlock-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", - warning, - "true" - ); + let warningShownPromise = waitForWarningState(warning, "onscreen"); let expectedWarningText; @@ -49,11 +64,7 @@ add_task(async function show_pointerlock_warning_escape() { ok(true, "Pointerlock warning shown"); - let warningHiddenPromise = BrowserTestUtils.waitForAttribute( - "hidden", - warning, - "" - ); + let warningHiddenPromise = waitForWarningState(warning, "hidden"); await BrowserTestUtils.waitForCondition( () => warning.innerText == expectedWarningText, View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/93ef5b… -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/93ef5b… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
[Git][tpo/applications/tor-browser][base-browser-115.0.2esr-13.0-1] 2 commits: Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs
by richard (@richard) 31 Jul '23

31 Jul '23
richard pushed to branch base-browser-115.0.2esr-13.0-1 at The Tor Project / Applications / Tor Browser Commits: 60fefea4 by Edgar Chen at 2023-07-31T17:56:34+00:00 Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs Fullscreen/PointerLock warnings are initialized with hidden=&quot;true&quot;, but change to hidden=&quot;&quot; after being shown and hidden again. I think this started happening when we began using HTML elements instead of XUL as they handle hidden attribute differently. Differential Revision: https://phabricator.services.mozilla.com/D177790 - - - - - 524c7622 by Edgar Chen at 2023-07-31T17:56:38+00:00 Bug 1821884 - Reshow initial fullscreen notification; r=Gijs Depends on D177790 Differential Revision: https://phabricator.services.mozilla.com/D178339 - - - - - 4 changed files: - browser/base/content/browser-fullScreenAndPointerLock.js - browser/base/content/fullscreen-and-pointerlock.inc.xhtml - browser/base/content/test/fullscreen/browser_fullscreen_warning.js - dom/tests/browser/browser_pointerlock_warning.js Changes: ===================================== browser/base/content/browser-fullScreenAndPointerLock.js ===================================== @@ -62,9 +62,14 @@ var PointerlockFsWarning = { this._element = document.getElementById(elementId); // Setup event listeners this._element.addEventListener("transitionend", this); + this._element.addEventListener("transitioncancel", this); window.addEventListener("mousemove", this, true); + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); // The timeout to hide the warning box after a while. this._timeoutHide = new this.Timeout(() => { + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); this._state = "hidden"; }, timeout); // The timeout to show the warning box when the pointer is at the top @@ -116,11 +121,10 @@ var PointerlockFsWarning = { return; } - // Explicitly set the last state to hidden to avoid the warning - // box being hidden immediately because of mousemove. - this._state = "onscreen"; - this._lastState = "hidden"; - this._timeoutHide.start(); + if (Services.focus.activeWindow == window) { + this._state = "onscreen"; + this._timeoutHide.start(); + } }, /** @@ -148,7 +152,10 @@ var PointerlockFsWarning = { this._element.hidden = true; // Remove all event listeners this._element.removeEventListener("transitionend", this); + this._element.removeEventListener("transitioncancel", this); window.removeEventListener("mousemove", this, true); + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); // Clear fields this._element = null; this._timeoutHide = null; @@ -186,7 +193,7 @@ var PointerlockFsWarning = { } if (newState != "hidden") { if (currentState != "hidden") { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } else { // When the previous state is hidden, the display was none, // thus no box was constructed. We need to wait for the new @@ -197,7 +204,7 @@ var PointerlockFsWarning = { requestAnimationFrame(() => { requestAnimationFrame(() => { if (this._element) { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } }); }); @@ -217,7 +224,7 @@ var PointerlockFsWarning = { } else if (this._timeoutShow.delay >= 0) { this._timeoutShow.start(); } - } else { + } else if (state != "onscreen") { let elemRect = this._element.getBoundingClientRect(); if (state == "hiding" && this._lastState != "hidden") { // If we are on the hiding transition, and the pointer @@ -239,12 +246,23 @@ var PointerlockFsWarning = { } break; } - case "transitionend": { + case "transitionend": + case "transitioncancel": { if (this._state == "hiding") { this._element.hidden = true; } break; } + case "activate": { + this._state = "onscreen"; + this._timeoutHide.start(); + break; + } + case "deactivate": { + this._state = "hidden"; + this._timeoutHide.cancel(); + break; + } } }, }; ===================================== browser/base/content/fullscreen-and-pointerlock.inc.xhtml ===================================== @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. <html:div id="fullscreen-and-pointerlock-wrapper"> - <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> @@ -20,7 +20,7 @@ </html:button> </html:div> - <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> ===================================== browser/base/content/test/fullscreen/browser_fullscreen_warning.js ===================================== @@ -3,14 +3,35 @@ "use strict"; -add_task(async function test_fullscreen_display_none() { +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + +add_setup(async function init() { await SpecialPowers.pushPrefEnv({ set: [ ["full-screen-api.enabled", true], ["full-screen-api.allow-trusted-requests-only", false], ], }); +}); +add_task(async function test_fullscreen_display_none() { await BrowserTestUtils.withNewTab( { gBrowser, @@ -30,11 +51,13 @@ add_task(async function test_fullscreen_display_none() { }, async function (browser) { let warning = document.getElementById("fullscreen-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( warning, - "true" + "hidden", + "Should not show full screen warning initially" ); + + let warningShownPromise = waitForWarningState(warning, "onscreen"); // Enter fullscreen await SpecialPowers.spawn(browser, [], async () => { let frame = content.document.querySelector("iframe"); @@ -54,39 +77,33 @@ add_task(async function test_fullscreen_display_none() { ); document.getElementById("fullscreen-exit-button").click(); await exitFullscreenPromise; + + checkWarningState( + warning, + "hidden", + "Should hide fullscreen warning after exiting fullscreen" + ); } ); }); add_task(async function test_fullscreen_pointerlock_conflict() { - await SpecialPowers.pushPrefEnv({ - set: [ - ["full-screen-api.enabled", true], - ["full-screen-api.allow-trusted-requests-only", false], - ], - }); - await BrowserTestUtils.withNewTab("https://example.com", async browser => { let fsWarning = document.getElementById("fullscreen-warning"); let plWarning = document.getElementById("pointerlock-warning"); - is( - fsWarning.getAttribute("onscreen"), - null, - "Should not show full screen warning initially." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning initially." - ); - - let fsWarningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( fsWarning, - "true" + "hidden", + "Should not show full screen warning initially" + ); + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning initially" ); + let fsWarningShownPromise = waitForWarningState(fsWarning, "onscreen"); info("Entering full screen and pointer lock."); await SpecialPowers.spawn(browser, [], async () => { await content.document.body.requestFullscreen(); @@ -94,15 +111,10 @@ add_task(async function test_fullscreen_pointerlock_conflict() { }); await fsWarningShownPromise; - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should show full screen warning." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); info("Exiting pointerlock"); @@ -110,18 +122,19 @@ add_task(async function test_fullscreen_pointerlock_conflict() { await content.document.exitPointerLock(); }); - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should still show full screen warning." + checkWarningState( + fsWarning, + "onscreen", + "Should still show full screen warning" ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); // Cleanup + info("Exiting fullscreen"); await document.exitFullscreen(); }); }); ===================================== dom/tests/browser/browser_pointerlock_warning.js ===================================== @@ -15,6 +15,25 @@ const FRAME_TEST_URL = encodeURI(BODY_URL) + '"></iframe></body>'; +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + // Make sure the pointerlock warning is shown and exited with the escape key add_task(async function show_pointerlock_warning_escape() { let urls = [TEST_URL, FRAME_TEST_URL]; @@ -24,11 +43,7 @@ add_task(async function show_pointerlock_warning_escape() { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); let warning = document.getElementById("pointerlock-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", - warning, - "true" - ); + let warningShownPromise = waitForWarningState(warning, "onscreen"); let expectedWarningText; @@ -49,11 +64,7 @@ add_task(async function show_pointerlock_warning_escape() { ok(true, "Pointerlock warning shown"); - let warningHiddenPromise = BrowserTestUtils.waitForAttribute( - "hidden", - warning, - "" - ); + let warningHiddenPromise = waitForWarningState(warning, "hidden"); await BrowserTestUtils.waitForCondition( () => warning.innerText == expectedWarningText, View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f3faeb… -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f3faeb… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
[Git][tpo/applications/tor-browser][tor-browser-102.14.0esr-12.5-1] 2 commits: Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs
by richard (@richard) 31 Jul '23

31 Jul '23
richard pushed to branch tor-browser-102.14.0esr-12.5-1 at The Tor Project / Applications / Tor Browser Commits: f70fa908 by Edgar Chen at 2023-07-28T23:08:31+02:00 Bug 1821884 - Ensure consistent state for fullscreen/pointerlock warnings; r=Gijs Fullscreen/PointerLock warnings are initialized with hidden=&quot;true&quot;, but change to hidden=&quot;&quot; after being shown and hidden again. I think this started happening when we began using HTML elements instead of XUL as they handle hidden attribute differently. Differential Revision: https://phabricator.services.mozilla.com/D177790 - - - - - d27c6a1b by Edgar Chen at 2023-07-28T23:09:46+02:00 Bug 1821884 - Reshow initial fullscreen notification; r=Gijs Depends on D177790 Differential Revision: https://phabricator.services.mozilla.com/D178339 - - - - - 4 changed files: - browser/base/content/browser-fullScreenAndPointerLock.js - browser/base/content/fullscreen-and-pointerlock.inc.xhtml - browser/base/content/test/fullscreen/browser_fullscreen_warning.js - dom/tests/browser/browser_pointerlock_warning.js Changes: ===================================== browser/base/content/browser-fullScreenAndPointerLock.js ===================================== @@ -62,9 +62,14 @@ var PointerlockFsWarning = { this._element = document.getElementById(elementId); // Setup event listeners this._element.addEventListener("transitionend", this); + this._element.addEventListener("transitioncancel", this); window.addEventListener("mousemove", this, true); + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); // The timeout to hide the warning box after a while. this._timeoutHide = new this.Timeout(() => { + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); this._state = "hidden"; }, timeout); // The timeout to show the warning box when the pointer is at the top @@ -116,11 +121,10 @@ var PointerlockFsWarning = { return; } - // Explicitly set the last state to hidden to avoid the warning - // box being hidden immediately because of mousemove. - this._state = "onscreen"; - this._lastState = "hidden"; - this._timeoutHide.start(); + if (Services.focus.activeWindow == window) { + this._state = "onscreen"; + this._timeoutHide.start(); + } }, /** @@ -148,7 +152,10 @@ var PointerlockFsWarning = { this._element.hidden = true; // Remove all event listeners this._element.removeEventListener("transitionend", this); + this._element.removeEventListener("transitioncancel", this); window.removeEventListener("mousemove", this, true); + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); // Clear fields this._element = null; this._timeoutHide = null; @@ -186,7 +193,7 @@ var PointerlockFsWarning = { } if (newState != "hidden") { if (currentState != "hidden") { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } else { // When the previous state is hidden, the display was none, // thus no box was constructed. We need to wait for the new @@ -197,7 +204,7 @@ var PointerlockFsWarning = { requestAnimationFrame(() => { requestAnimationFrame(() => { if (this._element) { - this._element.setAttribute(newState, true); + this._element.setAttribute(newState, ""); } }); }); @@ -217,7 +224,7 @@ var PointerlockFsWarning = { } else if (this._timeoutShow.delay >= 0) { this._timeoutShow.start(); } - } else { + } else if (state != "onscreen") { let elemRect = this._element.getBoundingClientRect(); if (state == "hiding" && this._lastState != "hidden") { // If we are on the hiding transition, and the pointer @@ -239,12 +246,23 @@ var PointerlockFsWarning = { } break; } - case "transitionend": { + case "transitionend": + case "transitioncancel": { if (this._state == "hiding") { this._element.hidden = true; } break; } + case "activate": { + this._state = "onscreen"; + this._timeoutHide.start(); + break; + } + case "deactivate": { + this._state = "hidden"; + this._timeoutHide.cancel(); + break; + } } }, }; ===================================== browser/base/content/fullscreen-and-pointerlock.inc.xhtml ===================================== @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. <html:div id="fullscreen-and-pointerlock-wrapper"> - <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> @@ -20,7 +20,7 @@ </html:button> </html:div> - <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden="true"> + <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden=""> <html:div class="pointerlockfswarning-domain-text"> <html:span class="pointerlockfswarning-domain" data-l10n-name="domain"/> </html:div> ===================================== browser/base/content/test/fullscreen/browser_fullscreen_warning.js ===================================== @@ -3,14 +3,35 @@ "use strict"; -add_task(async function test_fullscreen_display_none() { +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + +add_setup(async function init() { await SpecialPowers.pushPrefEnv({ set: [ ["full-screen-api.enabled", true], ["full-screen-api.allow-trusted-requests-only", false], ], }); +}); +add_task(async function test_fullscreen_display_none() { await BrowserTestUtils.withNewTab( { gBrowser, @@ -30,11 +51,13 @@ add_task(async function test_fullscreen_display_none() { }, async function(browser) { let warning = document.getElementById("fullscreen-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( warning, - "true" + "hidden", + "Should not show full screen warning initially" ); + + let warningShownPromise = waitForWarningState(warning, "onscreen"); // Enter fullscreen await SpecialPowers.spawn(browser, [], async () => { let frame = content.document.querySelector("iframe"); @@ -54,39 +77,33 @@ add_task(async function test_fullscreen_display_none() { ); document.getElementById("fullscreen-exit-button").click(); await exitFullscreenPromise; + + checkWarningState( + warning, + "hidden", + "Should hide fullscreen warning after exiting fullscreen" + ); } ); }); add_task(async function test_fullscreen_pointerlock_conflict() { - await SpecialPowers.pushPrefEnv({ - set: [ - ["full-screen-api.enabled", true], - ["full-screen-api.allow-trusted-requests-only", false], - ], - }); - await BrowserTestUtils.withNewTab("https://example.com", async browser => { let fsWarning = document.getElementById("fullscreen-warning"); let plWarning = document.getElementById("pointerlock-warning"); - is( - fsWarning.getAttribute("onscreen"), - null, - "Should not show full screen warning initially." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning initially." - ); - - let fsWarningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", + checkWarningState( fsWarning, - "true" + "hidden", + "Should not show full screen warning initially" + ); + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning initially" ); + let fsWarningShownPromise = waitForWarningState(fsWarning, "onscreen"); info("Entering full screen and pointer lock."); await SpecialPowers.spawn(browser, [], async () => { await content.document.body.requestFullscreen(); @@ -94,15 +111,10 @@ add_task(async function test_fullscreen_pointerlock_conflict() { }); await fsWarningShownPromise; - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should show full screen warning." - ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); info("Exiting pointerlock"); @@ -110,18 +122,19 @@ add_task(async function test_fullscreen_pointerlock_conflict() { await content.document.exitPointerLock(); }); - is( - fsWarning.getAttribute("onscreen"), - "true", - "Should still show full screen warning." + checkWarningState( + fsWarning, + "onscreen", + "Should still show full screen warning" ); - is( - plWarning.getAttribute("onscreen"), - null, - "Should not show pointer lock warning." + checkWarningState( + plWarning, + "hidden", + "Should not show pointer lock warning" ); // Cleanup + info("Exiting fullscreen"); await document.exitFullscreen(); }); }); ===================================== dom/tests/browser/browser_pointerlock_warning.js ===================================== @@ -15,6 +15,25 @@ const FRAME_TEST_URL = encodeURI(BODY_URL) + '"></iframe></body>'; +function checkWarningState(aWarningElement, aExpectedState, aMsg) { + ["hidden", "ontop", "onscreen"].forEach(state => { + is( + aWarningElement.hasAttribute(state), + state == aExpectedState, + `${aMsg} - check ${state} attribute.` + ); + }); +} + +async function waitForWarningState(aWarningElement, aExpectedState) { + await BrowserTestUtils.waitForAttribute(aExpectedState, aWarningElement, ""); + checkWarningState( + aWarningElement, + aExpectedState, + `Wait for ${aExpectedState} state` + ); +} + // Make sure the pointerlock warning is shown and exited with the escape key add_task(async function show_pointerlock_warning_escape() { let urls = [TEST_URL, FRAME_TEST_URL]; @@ -24,11 +43,7 @@ add_task(async function show_pointerlock_warning_escape() { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); let warning = document.getElementById("pointerlock-warning"); - let warningShownPromise = BrowserTestUtils.waitForAttribute( - "onscreen", - warning, - "true" - ); + let warningShownPromise = waitForWarningState(warning, "onscreen"); let expectedWarningText; @@ -49,11 +64,7 @@ add_task(async function show_pointerlock_warning_escape() { ok(true, "Pointerlock warning shown"); - let warningHiddenPromise = BrowserTestUtils.waitForAttribute( - "hidden", - warning, - "" - ); + let warningHiddenPromise = waitForWarningState(warning, "hidden"); await BrowserTestUtils.waitForCondition( () => warning.innerText == expectedWarningText, View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/855b69… -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/855b69… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
[Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] fixup! Bug 40597: Implement TorSettings module
by richard (@richard) 27 Jul '23

27 Jul '23
richard pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser Commits: f0493f9f by Pier Angelo Vendrame at 2023-07-27T18:20:08+00:00 fixup! Bug 40597: Implement TorSettings module Bug 41907: Change state after the process becomes ready only when we are in the initial state. - - - - - 1 changed file: - browser/modules/TorConnect.jsm Changes: ===================================== browser/modules/TorConnect.jsm ===================================== @@ -874,6 +874,18 @@ const TorConnect = (() => { switch (topic) { /* We need to wait until TorSettings have been loaded and applied before we can Quickstart */ case TorSettingsTopics.Ready: { + // tor-browser#41907: This is only a workaround to avoid users being + // bounced back to the initial panel without any explanation. + // Longer term we should disable the clickable elements, or find a UX + // to prevent this from happening (e.g., allow buttons to be clicked, + // but show an intermediate starting state, or a message that tor is + // starting while the butons are disabled, etc...). + if (this.state !== TorConnectState.Initial) { + console.warn( + "TorConnect: Seen the torsettings:ready after the state has already changed, ignoring the notification." + ); + break; + } if (this.shouldQuickStart) { // Quickstart this._changeState(TorConnectState.Bootstrapping); View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/f0493f9… -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/f0493f9… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
[Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] 7 commits: fixup! Bug 40933: Add tor-launcher functionality
by richard (@richard) 27 Jul '23

27 Jul '23
richard pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser Commits: 66229717 by Pier Angelo Vendrame at 2023-07-27T18:11:55+02:00 fixup! Bug 40933: Add tor-launcher functionality Bug 41844: Added a couple of wrappers for Onion Auth on TorProtocolService. - - - - - 2cde9fc3 by Pier Angelo Vendrame at 2023-07-27T18:11:56+02:00 fixup! Bug 30237: Add v3 onion services client authentication prompt Bug 41844: Stop using the control port directly - - - - - b2cd0ee8 by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00 fixup! Bug 40933: Add tor-launcher functionality Small improvements on event registration. - - - - - 26152fa9 by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00 fixup! Bug 40933: Add tor-launcher functionality Bug 41844: Do not use a the control port directly. Collect the bridge node for the about:preferences#connection page in TorMonitorService. Also, move parts of the circuit display to TorMonitorService and TorProtocolService. - - - - - 749aeaca by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00 fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection Bug 41844: Do not use the control port directly Do not use the controller in the settings frontend. Instead, let TorMonitorService collect the first node&#39;s fingerprint. - - - - - 20641450 by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00 fixup! Bug 3455: Add DomainIsolator, for isolating circuit by domain. Bug 41844: Do not use the control port directly. Use TorDomainIsolator also as a backend for the circuit display. - - - - - 1a8be7b1 by Pier Angelo Vendrame at 2023-07-27T18:11:59+02:00 fixup! Bug 41600: Add a tor circuit display panel. Bug 41844: Have a separate backend for the tor circuits Remove the backend stuff from the circuit display. - - - - - 11 changed files: - browser/base/content/browser.js - browser/components/onionservices/content/authPrompt.js - browser/components/onionservices/content/savedKeysDialog.js - browser/components/torcircuit/content/torCircuitPanel.js - browser/components/torpreferences/content/connectionPane.js - toolkit/components/tor-launcher/TorDomainIsolator.jsm → toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs - toolkit/components/tor-launcher/TorMonitorService.sys.mjs - toolkit/components/tor-launcher/TorParsers.sys.mjs - toolkit/components/tor-launcher/TorProtocolService.sys.mjs - toolkit/components/tor-launcher/TorStartupService.sys.mjs - toolkit/components/tor-launcher/moz.build Changes: ===================================== browser/base/content/browser.js ===================================== @@ -66,6 +66,7 @@ ChromeUtils.defineESModuleGetters(this, { TabsSetupFlowManager: "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs", TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", + TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs", TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", UITour: "resource:///modules/UITour.sys.mjs", UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", @@ -100,7 +101,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { TorConnect: "resource:///modules/TorConnect.jsm", TorConnectState: "resource:///modules/TorConnect.jsm", TorConnectTopics: "resource:///modules/TorConnect.jsm", - TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.jsm", Translation: "resource:///modules/translation/TranslationParent.jsm", webrtcUI: "resource:///modules/webrtcUI.jsm", ZoomUI: "resource:///modules/ZoomUI.jsm", ===================================== browser/components/onionservices/content/authPrompt.js ===================================== @@ -7,6 +7,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { OnionAuthUtil: "chrome://browser/content/onionservices/authUtil.jsm", CommonUtils: "resource://services-common/utils.js", + TorProtocolService: "resource://gre/modules/TorProtocolService.jsm", TorStrings: "resource:///modules/TorStrings.jsm", }); @@ -192,10 +193,6 @@ const OnionAuthPrompt = (function () { let controllerFailureMsg = TorStrings.onionServices.authPrompt.failedToSetKey; try { - let { controller } = ChromeUtils.import( - "resource://torbutton/modules/tor-control-port.js" - ); - let torController = await controller(); // ^(subdomain.)*onionserviceid.onion$ (case-insensitive) const onionServiceIdRegExp = /^(.*\.)*(?<onionServiceId>[a-z2-7]{56})\.onion$/i; @@ -206,8 +203,7 @@ const OnionAuthPrompt = (function () { let checkboxElem = this._getCheckboxElement(); let isPermanent = checkboxElem && checkboxElem.checked; - torController - .onionAuthAdd(onionServiceId, base64key, isPermanent) + TorProtocolService.onionAuthAdd(onionServiceId, base64key, isPermanent) .then(aResponse => { // Success! Reload the page. this._browser.sendMessageToActor( ===================================== browser/components/onionservices/content/savedKeysDialog.js ===================================== @@ -10,8 +10,8 @@ ChromeUtils.defineModuleGetter( ChromeUtils.defineModuleGetter( this, - "controller", - "resource://torbutton/modules/tor-control-port.js" + "TorProtocolService", + "resource://gre/modules/TorProtocolService.jsm" ); var gOnionServicesSavedKeysDialog = { @@ -49,11 +49,9 @@ var gOnionServicesSavedKeysDialog = { const controllerFailureMsg = TorStrings.onionServices.authPreferences.failedToRemoveKey; try { - const torController = await controller(); - // Remove in reverse index order to avoid issues caused by index changes. for (let i = indexesToDelete.length - 1; i >= 0; --i) { - await this._deleteOneKey(torController, indexesToDelete[i]); + await this._deleteOneKey(indexesToDelete[i]); } } catch (e) { if (e.torMessage) { @@ -127,8 +125,7 @@ var gOnionServicesSavedKeysDialog = { try { this._tree.view = this; - const torController = await controller(); - const keyInfoList = await torController.onionAuthViewKeys(); + const keyInfoList = await TorProtocolService.onionAuthViewKeys(); if (keyInfoList) { // Filter out temporary keys. this._keyInfoList = keyInfoList.filter(aKeyInfo => { @@ -165,9 +162,9 @@ var gOnionServicesSavedKeysDialog = { }, // This method may throw; callers should catch errors. - async _deleteOneKey(aTorController, aIndex) { + async _deleteOneKey(aIndex) { const keyInfoObj = this._keyInfoList[aIndex]; - await aTorController.onionAuthRemove(keyInfoObj.hsAddress); + await TorProtocolService.onionAuthRemove(keyInfoObj.hsAddress); this._tree.view.selection.clearRange(aIndex, aIndex); this._keyInfoList.splice(aIndex, 1); this._tree.rowCountChanged(aIndex + 1, -1); ===================================== browser/components/torcircuit/content/torCircuitPanel.js ===================================== @@ -1,18 +1,5 @@ /* eslint-env mozilla/browser-window */ -/** - * Stores the data associated with a circuit node. - * - * @typedef NodeData - * @property {string[]} ipAddrs - The ip addresses associated with this node. - * @property {string?} bridgeType - The bridge type for this node, or "" if the - * node is a bridge but the type is unknown, or null if this is not a bridge - * node. - * @property {string?} regionCode - An upper case 2-letter ISO3166-1 code for - * the first ip address, or null if there is no region. This should also be a - * valid BCP47 Region subtag. - */ - /** * Data about the current domain and circuit for a xul:browser. * @@ -35,29 +22,6 @@ var gTorCircuitPanel = { * @type {Element} */ toolbarButton: null, - /** - * A list of IDs for "mature" circuits (those that have conveyed a stream). - * - * @type {string[]} - */ - _knownCircuitIDs: [], - /** - * Stores the circuit nodes for each SOCKS username/password pair. The keys - * are of the form "<username>|<password>". - * - * @type {Map<string, NodeData[]>} - */ - _credentialsToCircuitNodes: new Map(), - /** - * Browser data for their currently shown page. - * - * This data may be stale for a given browser since we only update this data - * when loading a new page in the currently selected browser, when switching - * tabs, or if we find a new circuit for the current browser. - * - * @type {WeakMap<MozBrowser, BrowserCircuitData>} - */ - _browserData: new WeakMap(), /** * The data for the currently shown browser. * @@ -71,6 +35,13 @@ var gTorCircuitPanel = { */ _isActive: false, + /** + * The topic on which circuit changes are broadcast. + * + * @type {string} + */ + TOR_CIRCUIT_TOPIC: "TorCircuitChange", + /** * Initialize the panel. */ @@ -86,31 +57,6 @@ var gTorCircuitPanel = { maxLogLevelPref: "browser.torcircuitpanel.loglevel", }); - const { wait_for_controller } = ChromeUtils.import( - "resource://torbutton/modules/tor-control-port.js" - ); - wait_for_controller().then( - controller => { - if (!this._isActive) { - // uninit() was called before resolution. - return; - } - // FIXME: We should be using some dedicated integrated back end to - // store circuit information, rather than collecting it all here in the - // front end. See tor-browser#41700. - controller.watchEvent( - "STREAM", - streamEvent => streamEvent.StreamStatus === "SENTCONNECT", - streamEvent => this._collectCircuit(controller, streamEvent) - ); - }, - error => { - this._log.error( - `Not collecting circuits because of an error: ${error.message}` - ); - } - ); - this.panel = document.getElementById("tor-circuit-panel"); this._panelElements = { heading: document.getElementById("tor-circuit-heading"), @@ -245,6 +191,9 @@ var gTorCircuitPanel = { // Notified of new locations for the currently selected browser (tab) *and* // switching selected browser. gBrowser.addProgressListener(this._locationListener); + + // Get notifications for circuit changes. + Services.obs.addObserver(this, this.TOR_CIRCUIT_TOPIC); }, /** @@ -253,6 +202,17 @@ var gTorCircuitPanel = { uninit() { this._isActive = false; gBrowser.removeProgressListener(this._locationListener); + Services.obs.removeObserver(this, this.TOR_CIRCUIT_TOPIC); + }, + + /** + * Observe circuit changes. + */ + observe(subject, topic, data) { + if (topic === this.TOR_CIRCUIT_TOPIC) { + // TODO: Maybe check if we actually need to do something earlier. + this._updateCurrentBrowser(); + } }, /** @@ -286,109 +246,6 @@ var gTorCircuitPanel = { window.openWebLinkIn(this._panelElements.aliasLink.href, where); }, - /** - * Collect circuit data for the found circuits, to be used later for display. - * - * @param {controller} controller - The tor controller. - * @param {object} streamEvent - The streamEvent for the new circuit. - */ - async _collectCircuit(controller, streamEvent) { - const id = streamEvent.CircuitID; - if (this._knownCircuitIDs.includes(id)) { - return; - } - this._log.debug(`New streamEvent.CircuitID: ${id}.`); - // FIXME: This list grows and is never freed. See tor-browser#41700. - this._knownCircuitIDs.push(id); - const circuitStatus = (await controller.getInfo("circuit-status"))?.find( - circuit => circuit.id === id - ); - if (!circuitStatus?.SOCKS_USERNAME || !circuitStatus?.SOCKS_PASSWORD) { - return; - } - const nodes = await Promise.all( - circuitStatus.circuit.map(names => - this._nodeDataForCircuit(controller, names) - ) - ); - // Remove quotes from the strings. - const username = circuitStatus.SOCKS_USERNAME.replace(/^"(.*)"$/, "$1"); - const password = circuitStatus.SOCKS_PASSWORD.replace(/^"(.*)"$/, "$1"); - const credentials = `${username}|${password}`; - // FIXME: This map grows and is never freed. We cannot simply request this - // information when needed because it is no longer available once the - // circuit is dropped, even if the web page is still displayed. - // See tor-browser#41700. - this._credentialsToCircuitNodes.set(credentials, nodes); - // Update the circuit in case the current page gains a new circuit whilst - // the popup is still open. - this._updateCurrentBrowser(credentials); - }, - - /** - * Fetch the node data for the given circuit node. - * - * @param {controller} controller - The tor controller. - * @param {string[]} circuitNodeNames - The names for the circuit node. Only - * the first name, the node id, will be used. - * - * @returns {NodeData} - The data for this circuit node. - */ - async _nodeDataForCircuit(controller, circuitNodeNames) { - // The first "name" in circuitNodeNames is the id. - // Remove the leading '$' if present. - const id = circuitNodeNames[0].replace(/^\$/, ""); - let result = { ipAddrs: [], bridgeType: null, regionCode: null }; - const bridge = (await controller.getConf("bridge"))?.find( - foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase() - ); - const addrRe = /^\[?([^\]]+)\]?:\d+$/; - if (bridge) { - result.bridgeType = bridge.type ?? ""; - // Attempt to get an IP address from bridge address string. - const ip = bridge.address.match(addrRe)?.[1]; - if (ip && !ip.startsWith("0.")) { - result.ipAddrs.push(ip); - } - } else { - // Either dealing with a relay, or a bridge whose fingerprint is not saved - // in torrc. - let statusMap; - try { - statusMap = await controller.getInfo("ns/id/" + id); - } catch { - // getInfo will throw if the given id is not a relay. - // This probably means we are dealing with a user-provided bridge with - // no fingerprint. - // We don't know the ip/ipv6 or type, so leave blank. - result.bridgeType = ""; - return result; - } - if (statusMap.IP && !statusMap.IP.startsWith("0.")) { - result.ipAddrs.push(statusMap.IP); - } - const ip6 = statusMap.IPv6?.match(addrRe)?.[1]; - if (ip6) { - result.ipAddrs.push(ip6); - } - } - if (result.ipAddrs.length) { - // Get the country code for the node's IP address. - let regionCode; - try { - // Expect a 2-letter ISO3166-1 code, which should also be a valid BCP47 - // Region subtag. - regionCode = await controller.getInfo( - "ip-to-country/" + result.ipAddrs[0] - ); - } catch {} - if (regionCode && regionCode !== "??") { - result.regionCode = regionCode.toUpperCase(); - } - } - return result; - }, - /** * A list of schemes to never show the circuit display for. * @@ -398,71 +255,50 @@ var gTorCircuitPanel = { * * @type {string[]} */ - // FIXME: Have a back end that handles this instead. See tor-browser#41700. + // FIXME: Check if we find a UX to handle some of these cases, and if we + // manage to solve some technical issues. + // See tor-browser#41700 and tor-browser!699. _ignoredSchemes: ["about", "file", "chrome", "resource"], /** * Update the current circuit and domain data for the currently selected * browser, possibly changing the UI. - * - * @param {string?} [matchingCredentials=null] - If given, only update the - * current browser data if the current browser's credentials match. */ - _updateCurrentBrowser(matchingCredentials = null) { + _updateCurrentBrowser() { const browser = gBrowser.selectedBrowser; const domain = TorDomainIsolator.getDomainForBrowser(browser); + const nodes = TorDomainIsolator.getCircuit( + browser, + domain, + browser.contentPrincipal.originAttributes.userContextId + ); // We choose the currentURI, which matches what is shown in the URL bar and // will match up with the domain. // In contrast, documentURI corresponds to the shown page. E.g. it could // point to "about:certerror". const scheme = browser.currentURI?.scheme; - let credentials = TorDomainIsolator.getSocksProxyCredentials( - domain, - browser.contentPrincipal.originAttributes.userContextId - ); - if (credentials) { - credentials = `${credentials.username}|${credentials.password}`; - } - - if (matchingCredentials && matchingCredentials !== credentials) { - // This update was triggered by the circuit update for some other browser - // or process. - return; - } - - let nodes = this._credentialsToCircuitNodes.get(credentials) ?? []; - - const prevData = this._browserData.get(browser); - if ( - prevData && - prevData.domain && - prevData.domain === domain && - prevData.scheme === scheme && - prevData.nodes.length && - !nodes.length - ) { - // Since this is the same domain, for the same browser, and we used to - // have circuit nodes, we *assume* we are re-generating a circuit. So we - // keep the old circuit data around for the time being. - // FIXME: Have a back end that makes this explicit, rather than an - // assumption. See tor-browser#41700. - nodes = prevData.nodes; - this._log.debug(`Keeping old circuit for ${domain}.`); - } - - this._browserData.set(browser, { domain, scheme, nodes }); if ( this._currentBrowserData && this._currentBrowserData.domain === domain && this._currentBrowserData.scheme === scheme && - this._currentBrowserData.nodes === nodes + this._currentBrowserData.nodes.length === nodes.length && + // If non-null, the fingerprints of the nodes match. + (!nodes || + nodes.every( + (n, index) => + n.fingerprint === this._currentBrowserData.nodes[index].fingerprint + )) ) { // No change. + this._log.debug( + "Skipping browser update because the data is already up to date." + ); return; } - this._currentBrowserData = this._browserData.get(browser); + this._currentBrowserData = { domain, scheme, nodes }; + this._log.debug("Updating current browser.", this._currentBrowserData); if ( // Schemes where we always want to hide the display. ===================================== browser/components/torpreferences/content/connectionPane.js ===================================== @@ -17,6 +17,9 @@ const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource } = const { TorProtocolService } = ChromeUtils.import( "resource://gre/modules/TorProtocolService.jsm" ); +const { TorMonitorService, TorMonitorTopics } = ChromeUtils.import( + "resource://gre/modules/TorMonitorService.jsm" +); const { TorConnect, TorConnectTopics, TorConnectState, TorCensorshipLevel } = ChromeUtils.import("resource:///modules/TorConnect.jsm"); @@ -144,8 +147,6 @@ const gConnectionPane = (function () { _internetStatus: InternetStatus.Unknown, - _controller: null, - _currentBridgeId: null, // populate xul with strings and cache the relevant elements @@ -727,9 +728,10 @@ const gConnectionPane = (function () { }; // Use a promise to avoid blocking the population of the page // FIXME: Stop using a JSON file, and switch to properties - fetch( + const annotationPromise = fetch( "chrome://browser/content/torpreferences/bridgemoji/annotations.json" - ).then(async res => { + ); + annotationPromise.then(async res => { const annotations = await res.json(); const bcp47 = Services.locale.appLocaleAsBCP47; const dash = bcp47.indexOf("-"); @@ -749,6 +751,7 @@ const gConnectionPane = (function () { ".currently-connected" )) { card.classList.remove("currently-connected"); + card.querySelector(selectors.bridges.cardQrGrid).style.height = ""; } if (!this._currentBridgeId) { return; @@ -769,72 +772,17 @@ const gConnectionPane = (function () { placeholder.replaceWith(...cards); this._checkBridgeCardsHeight(); }; - try { - const { controller } = ChromeUtils.import( - "resource://torbutton/modules/tor-control-port.js" - ); - // Avoid the cache because we set our custom event watcher, and at the - // moment, watchers cannot be removed from a controller. - controller(true).then(aController => { - this._controller = aController; - // Getting the circuits may be enough, if we have bootstrapped for a - // while, but at the beginning it gives many bridges as connected, - // because tor pokes all the bridges to find the best one. - // Also, watching circuit events does not work, at the moment, but in - // any case, checking the stream has the advantage that we can see if - // it really used for a connection, rather than tor having created - // this circuit to check if the bridge can be used. We do this by - // checking if the stream has SOCKS username, which actually contains - // the destination of the stream. - // FIXME: We only know the currentBridge *after* a circuit event, but - // if the circuit event is sent *before* about:torpreferences is - // opened we will miss it. Therefore this approach only works if a - // circuit is created after opening about:torconnect. A dedicated - // backend outside of about:preferences would help, and could be - // shared with gTorCircuitPanel. See tor-browser#41700. - this._controller.watchEvent( - "STREAM", - event => - event.StreamStatus === "SUCCEEDED" && "SOCKS_USERNAME" in event, - async event => { - const circuitStatuses = await this._controller.getInfo( - "circuit-status" - ); - if (!circuitStatuses) { - return; - } - for (const status of circuitStatuses) { - if (status.id === event.CircuitID && status.circuit.length) { - // The id in the circuit begins with a $ sign. - const id = status.circuit[0][0].replace(/^\$/, ""); - if (id !== this._currentBridgeId) { - const bridge = ( - await this._controller.getConf("bridge") - )?.find( - foundBridge => - foundBridge.ID?.toUpperCase() === id.toUpperCase() - ); - if (!bridge) { - // Either there is no bridge, or bridge with no - // fingerprint. - this._currentBridgeId = null; - } else { - this._currentBridgeId = id; - } - this._updateConnectedBridges(); - } - break; - } - } - } - ); - }); - } catch (err) { - console.warn( - "We could not load torbutton, bridge statuses will not be updated", - err - ); - } + this._checkConnectedBridge = () => { + // TODO: We could make sure TorSettings is in sync by monitoring also + // changes of settings. At that point, we could query it, instead of + // doing a query over the control port. + const bridge = TorMonitorService.currentBridge; + if (bridge?.fingerprint !== this._currentBridgeId) { + this._currentBridgeId = bridge?.fingerprint ?? null; + this._updateConnectedBridges(); + } + }; + annotationPromise.then(this._checkConnectedBridge.bind(this)); // Add a new bridge prefpane.querySelector(selectors.bridges.addHeader).textContent = @@ -927,6 +875,7 @@ const gConnectionPane = (function () { }); Services.obs.addObserver(this, TorConnectTopics.StateChange); + Services.obs.addObserver(this, TorMonitorTopics.BridgeChanged); }, init() { @@ -950,11 +899,7 @@ const gConnectionPane = (function () { // unregister our observer topics Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged); Services.obs.removeObserver(this, TorConnectTopics.StateChange); - - if (this._controller !== null) { - this._controller.close(); - this._controller = null; - } + Services.obs.removeObserver(this, TorMonitorTopics.BridgeChanged); }, // whether the page should be present in about:preferences @@ -985,6 +930,12 @@ const gConnectionPane = (function () { this.onStateChange(); break; } + case TorMonitorTopics.BridgeChanged: { + if (data?.fingerprint !== this._currentBridgeId) { + this._checkConnectedBridge(); + } + break; + } } }, @@ -1028,7 +979,7 @@ const gConnectionPane = (function () { onRemoveAllBridges() { TorSettings.bridges.enabled = false; TorSettings.bridges.bridge_strings = ""; - if (TorSettings.bridges.source == TorBridgeSource.BuiltIn) { + if (TorSettings.bridges.source === TorBridgeSource.BuiltIn) { TorSettings.bridges.builtin_type = ""; } TorSettings.saveToPrefs(); ===================================== toolkit/components/tor-launcher/TorDomainIsolator.jsm → toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs ===================================== @@ -1,13 +1,14 @@ -// A component for Tor Browser that puts requests from different -// first party domains on separate Tor circuits. - -var EXPORTED_SYMBOLS = ["TorDomainIsolator"]; +/** + * A component for Tor Browser that puts requests from different first party + * domains on separate Tor circuits. + */ -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); -const { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm"); +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs"; +import { + clearInterval, + setInterval, +} from "resource://gre/modules/Timer.sys.mjs"; const lazy = {}; @@ -18,11 +19,10 @@ XPCOMUtils.defineLazyServiceGetters(lazy, { ], }); -ChromeUtils.defineModuleGetter( - lazy, - "TorProtocolService", - "resource://gre/modules/TorProtocolService.jsm" -); +ChromeUtils.defineESModuleGetters(lazy, { + TorMonitorTopics: "resource://gre/modules/TorMonitorService.sys.mjs", + TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs", +}); const logger = new ConsoleAPI({ prefix: "TorDomainIsolator", @@ -33,6 +33,12 @@ const logger = new ConsoleAPI({ // The string to use instead of the domain when it is not known. const CATCHALL_DOMAIN = "--unknown--"; +// The maximum lifetime for the catch-all circuit in milliseconds. +// When the catch-all circuit is needed, we check if more than this amount of +// time has passed since we last changed it nonce, and in case we change it +// again. +const CATCHALL_MAX_LIFETIME = 600_000; + // The preference to observe, to know whether isolation should be enabled or // disabled. const NON_TOR_PROXY_PREF = "extensions.torbutton.use_nontor_proxy"; @@ -40,23 +46,92 @@ const NON_TOR_PROXY_PREF = "extensions.torbutton.use_nontor_proxy"; // The topic of new identity, to observe to cleanup all the nonces. const NEW_IDENTITY_TOPIC = "new-identity-requested"; +// The topic on which we broacast circuit change notifications. +const TOR_CIRCUIT_TOPIC = "TorCircuitChange"; + +// We have an interval to delete circuits that are not reclaimed by any browser. +const CLEAR_TIMEOUT = 600_000; + +/** + * @typedef {string} CircuitId A string that we use to identify a circuit. + * Currently, it is a string that combines SOCKS credentials, to make it easier + * to use as a map key. + * It is not related to Tor's CircuitIDs. + */ +/** + * @typedef {number} BrowserId + */ +/** + * @typedef {NodeData[]} CircuitData The data about the nodes, ordered from + * guard (or bridge) to exit. + */ +/** + * @typedef BrowserCircuits Circuits related to a certain combination of + * isolators (first-party domain and user context ID, currently). + * @property {CircuitId} current The id of the last known circuit that has been + * used to fetch data for the isolated context. + * @property {CircuitId?} pending The id of the last used circuit for this + * isolation context. We might or might not know data about it, yet. But if we + * know it, we should move this id into current. + */ + class TorDomainIsolatorImpl { - // A mutable map that records what nonce we are using for each domain. + /** + * A mutable map that records what nonce we are using for each domain. + * + * @type {Map<string, string>} + */ #noncesForDomains = new Map(); - // A mutable map that records what nonce we are using for each tab container. + /** + * A mutable map that records what nonce we are using for each tab container. + * + * @type {Map<string, string>} + */ #noncesForUserContextId = new Map(); - // A bool that controls if we use SOCKS auth for isolation or not. + /** + * Tell whether we use SOCKS auth for isolation or not. + * + * @type {boolean} + */ #isolationEnabled = true; - // Specifies when the current catch-all circuit was first used + /** + * Specifies when the current catch-all circuit was first used. + * + * @type {integer} + */ #catchallDirtySince = Date.now(); + /** + * A map that associates circuit ids to the circuit information. + * + * @type {Map<CircuitId, CircuitData>} + */ + #knownCircuits = new Map(); + + /** + * A map that associates a certain browser to all the circuits it used or it + * is going to use. + * The circuits are keyed on the SOCKS username, which we take for granted + * being a combination of the first-party domain and the user context id. + * + * @type {Map<BrowserId, Map<string, BrowserCircuits>>} + */ + #browsers = new Map(); + + /** + * The handle of the interval we use to cleanup old circuit data. + * + * @type {number?} + */ + #cleanupIntervalId = null; + /** * Initialize the domain isolator. - * This function will setup the proxy filter that injects the credentials and - * register some observers. + * This function will setup the proxy filter that injects the credentials, + * register some observers, and setup the cleaning interval. */ init() { logger.info("Setup circuit isolation by domain and user context"); @@ -68,14 +143,25 @@ class TorDomainIsolatorImpl { Services.prefs.addObserver(NON_TOR_PROXY_PREF, this); Services.obs.addObserver(this, NEW_IDENTITY_TOPIC); + Services.obs.addObserver(this, lazy.TorMonitorTopics.StreamSucceeded); + + this.#cleanupIntervalId = setInterval( + this.#clearKnownCircuits.bind(this), + CLEAR_TIMEOUT + ); } /** - * Removes the observers added in the initialization. + * Removes the observers added in the initialization and stops the cleaning + * interval. */ uninit() { Services.prefs.removeObserver(NON_TOR_PROXY_PREF, this); Services.obs.removeObserver(this, NEW_IDENTITY_TOPIC); + Services.obs.removeObserver(this, lazy.TorMonitorTopics.StreamSucceeded); + clearInterval(this.#cleanupIntervalId); + this.#cleanupIntervalId = null; + this.clearIsolation(); } enable() { @@ -89,52 +175,52 @@ class TorDomainIsolatorImpl { } /** - * Return the credentials to use as username and password for the SOCKS proxy, - * given a certain domain and userContextId. Optionally, create them. + * Get the last circuit used in a certain browser. + * The returned data is created when the circuit is first seen, therefore it + * could be stale (i.e., the circuit might not be available anymore). * - * @param {string} firstPartyDomain The first party domain associated to the requests - * @param {string} userContextId The context ID associated to the request - * @param {bool} create Whether to create the nonce, if it is not available - * @returns {object|null} Either the credential, or null if we do not have them and create is - * false. + * @param {MozBrowser} browser The browser to get data for + * @param {string} domain The first party domain we want to get the circuit + * for + * @param {number} userContextId The user context domain we want to get the + * circuit for + * @returns {NodeData[]} The node data, or an empty array if we do not have + * data for the requested key. */ - getSocksProxyCredentials(firstPartyDomain, userContextId, create = false) { - if (!this.#noncesForDomains.has(firstPartyDomain)) { - if (!create) { - return null; - } - const nonce = this.#nonce(); - logger.info(`New nonce for first party ${firstPartyDomain}: ${nonce}`); - this.#noncesForDomains.set(firstPartyDomain, nonce); + getCircuit(browser, domain, userContextId) { + const username = this.#makeUsername(domain, userContextId); + const circuits = this.#browsers.get(browser.browserId)?.get(username); + // This is the only place where circuit data can go out, so the only place + // where it makes a difference to check whether the pending circuit is still + // pending, or it has actually got data. + const pending = this.#knownCircuits.get(circuits?.pending); + if (pending?.length) { + circuits.current = circuits.pending; + circuits.pending = null; + return pending; } - if (!this.#noncesForUserContextId.has(userContextId)) { - if (!create) { - return null; - } - const nonce = this.#nonce(); - logger.info(`New nonce for userContextId ${userContextId}: ${nonce}`); - this.#noncesForUserContextId.set(userContextId, nonce); - } - return { - username: this.#makeUsername(firstPartyDomain, userContextId), - password: - this.#noncesForDomains.get(firstPartyDomain) + - this.#noncesForUserContextId.get(userContextId), - }; + // TODO: At this point we already know if we expect a circuit change for + // this key: (circuit?.pending && !pending). However, we do not consume this + // data yet in the frontend, so do not send it for now. + return this.#knownCircuits.get(circuits?.current) ?? []; } /** * Create a new nonce for the FP domain of the selected browser and reload the * tab with a new circuit. * - * @param {object} browser Should be the gBrowser from the context of the - * caller + * @param {object} globalBrowser Should be the gBrowser from the context of + * the caller */ - newCircuitForBrowser(browser) { - const firstPartyDomain = getDomainForBrowser(browser.selectedBrowser); + newCircuitForBrowser(globalBrowser) { + const browser = globalBrowser.selectedBrowser; + const firstPartyDomain = getDomainForBrowser(browser); this.#newCircuitForDomain(firstPartyDomain); - // TODO: How to properly handle the user context? Should we use - // (domain, userContextId) pairs, instead of concatenating nonces? + const { username, password } = this.#getSocksProxyCredentials( + firstPartyDomain, + browser.contentPrincipal.originAttributes.userContextId + ); + this.#trackBrowser(browser, username, password); browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); } @@ -147,12 +233,15 @@ class TorDomainIsolatorImpl { // Per-domain and per contextId nonces are stored in maps, so simply clear // them. + // Notice that the catch-all circuit is included in #noncesForDomains, so we + // are implicilty cleaning it. Should this change, we should change its + // nonce explicitly here. this.#noncesForDomains.clear(); this.#noncesForUserContextId.clear(); + this.#catchallDirtySince = Date.now(); - // Force a rotation on the next catch-all circuit use by setting the - // creation time to the epoch. - this.#catchallDirtySince = 0; + this.#knownCircuits.clear(); + this.#browsers.clear(); } async observe(subject, topic, data) { @@ -173,55 +262,20 @@ class TorDomainIsolatorImpl { logger.error("Could not send the newnym command", e); // TODO: What UX to use here? See tor-browser#41708 } + } else if (topic === lazy.TorMonitorTopics.StreamSucceeded) { + const { username, password, circuit } = subject.wrappedJSObject; + this.#updateCircuit(username, password, circuit); } } /** - * Setup a filter that for every HTTPChannel, replaces the default SOCKS proxy - * with one that authenticates to the SOCKS server (the tor client process) - * with a username (the first party domain and userContextId) and a nonce - * password. - * Tor provides a separate circuit for each username+password combination. + * Setup a filter that for every HTTPChannel. */ #setupProxyFilter() { - const filterFunction = (aChannel, aProxy) => { - if (!this.#isolationEnabled) { - return aProxy; - } - try { - const channel = aChannel.QueryInterface(Ci.nsIChannel); - let firstPartyDomain = - channel.loadInfo.originAttributes.firstPartyDomain; - const userContextId = channel.loadInfo.originAttributes.userContextId; - if (firstPartyDomain === "") { - firstPartyDomain = CATCHALL_DOMAIN; - if (Date.now() - this.#catchallDirtySince > 1000 * 10 * 60) { - logger.info( - "tor catchall circuit has been dirty for over 10 minutes. Rotating." - ); - this.#newCircuitForDomain(CATCHALL_DOMAIN); - this.#catchallDirtySince = Date.now(); - } - } - const replacementProxy = this.#applySocksProxyCredentials( - aProxy, - firstPartyDomain, - userContextId - ); - logger.debug( - `Requested ${channel.URI.spec} via ${replacementProxy.username}:${replacementProxy.password}` - ); - return replacementProxy; - } catch (e) { - logger.error("Error while setting a new proxy", e); - return null; - } - }; - lazy.ProtocolProxyService.registerChannelFilter( { - applyFilter(aChannel, aProxy, aCallback) { - aCallback.onProxyFilterResult(filterFunction(aChannel, aProxy)); + applyFilter: (aChannel, aProxy, aCallback) => { + aCallback.onProxyFilterResult(this.#proxyFilter(aChannel, aProxy)); }, }, 0 @@ -229,33 +283,96 @@ class TorDomainIsolatorImpl { } /** - * Takes a proxyInfo object (originalProxy) and returns a new proxyInfo - * object with the same properties, except the username is set to the - * the domain and userContextId, and the password is a nonce. + * Replaces the default SOCKS proxy with one that authenticates to the SOCKS + * server (the tor client process) with a username (the first party domain and + * userContextId) and a nonce password. + * Tor provides a separate circuit for each username+password combination. + * + * @param {nsIChannel} aChannel The channel we are setting the proxy for + * @param {nsIProxyInfo} aProxy The original proxy + * @returns {nsIProxyInfo} The new proxy to use */ - #applySocksProxyCredentials(originalProxy, domain, userContextId) { - const proxy = originalProxy.QueryInterface(Ci.nsIProxyInfo); - const { username, password } = this.getSocksProxyCredentials( - domain, - userContextId, - true - ); - return lazy.ProtocolProxyService.newProxyInfoWithAuth( - "socks", - proxy.host, - proxy.port, - username, - password, - "", // aProxyAuthorizationHeader - "", // aConnectionIsolationKey - proxy.flags, - proxy.failoverTimeout, - proxy.failoverProxy - ); + #proxyFilter(aChannel, aProxy) { + if (!this.#isolationEnabled) { + return aProxy; + } + try { + const channel = aChannel.QueryInterface(Ci.nsIChannel); + let firstPartyDomain = channel.loadInfo.originAttributes.firstPartyDomain; + const userContextId = channel.loadInfo.originAttributes.userContextId; + if (!firstPartyDomain) { + firstPartyDomain = CATCHALL_DOMAIN; + if (Date.now() - this.#catchallDirtySince > CATCHALL_MAX_LIFETIME) { + logger.info( + "tor catchall circuit has reached its maximum lifetime. Rotating." + ); + this.#newCircuitForDomain(CATCHALL_DOMAIN); + } + } + const { username, password } = this.#getSocksProxyCredentials( + firstPartyDomain, + userContextId + ); + const browser = this.#getBrowserForChannel(channel); + if (browser) { + this.#trackBrowser(browser, username, password); + } + logger.debug(`Requested ${channel.URI.spec} via ${username}:${password}`); + const proxy = aProxy.QueryInterface(Ci.nsIProxyInfo); + return lazy.ProtocolProxyService.newProxyInfoWithAuth( + "socks", + proxy.host, + proxy.port, + username, + password, + "", // aProxyAuthorizationHeader + "", // aConnectionIsolationKey + proxy.flags, + proxy.failoverTimeout, + proxy.failoverProxy + ); + } catch (e) { + logger.error("Error while setting a new proxy", e); + return null; + } + } + + /** + * Return the credentials to use as username and password for the SOCKS proxy, + * given a certain domain and userContextId. + * A new random password will be created if not available yet. + * + * @param {string} firstPartyDomain The first party domain associated to the + * requests + * @param {number} userContextId The context ID associated to the request + * @returns {object} The credentials + */ + #getSocksProxyCredentials(firstPartyDomain, userContextId) { + if (!this.#noncesForDomains.has(firstPartyDomain)) { + const nonce = this.#nonce(); + logger.info(`New nonce for first party ${firstPartyDomain}: ${nonce}`); + this.#noncesForDomains.set(firstPartyDomain, nonce); + } + if (!this.#noncesForUserContextId.has(userContextId)) { + const nonce = this.#nonce(); + logger.info(`New nonce for userContextId ${userContextId}: ${nonce}`); + this.#noncesForUserContextId.set(userContextId, nonce); + } + // TODO: How to properly handle the user-context? Should we use + // (domain, userContextId) pairs, instead of concatenating nonces? + return { + username: this.#makeUsername(firstPartyDomain, userContextId), + password: + this.#noncesForDomains.get(firstPartyDomain) + + this.#noncesForUserContextId.get(userContextId), + }; } /** * Combine the needed data into a username for the proxy. + * + * @param {string} domain The first-party domain associated to the request + * @param {integer} userContextId The userContextId associated to the request */ #makeUsername(domain, userContextId) { if (!domain) { @@ -264,12 +381,26 @@ class TorDomainIsolatorImpl { return `${domain}:${userContextId}`; } + /** + * Combine SOCKS username and password into a string to use as ID. + * + * @param {string} username The SOCKS username + * @param {string} password The SOCKS password + * @returns {CircuitId} A string that combines username and password and can + * be used for map lookups. + */ + #credentialsToId(username, password) { + return `${username}|${password}`; + } + /** * Generate a new 128 bit random tag. * * Strictly speaking both using a cryptographic entropy source and using 128 * bits of entropy for the tag are likely overkill, as correct behavior only * depends on how unlikely it is for there to be a collision. + * + * @returns {string} The random nonce */ #nonce() { return Array.from(crypto.getRandomValues(new Uint8Array(16)), byte => @@ -279,12 +410,18 @@ class TorDomainIsolatorImpl { /** * Re-generate the nonce for a certain domain. + * + * @param {string?} domain The first-party domain to re-create the nonce for. + * If empty or null, the catchall domain will be used. */ #newCircuitForDomain(domain) { if (!domain) { domain = CATCHALL_DOMAIN; } this.#noncesForDomains.set(domain, this.#nonce()); + if (domain === CATCHALL_DOMAIN) { + this.#catchallDirtySince = Date.now(); + } logger.info( `New domain isolation for ${domain}: ${this.#noncesForDomains.get( domain @@ -296,6 +433,8 @@ class TorDomainIsolatorImpl { * Re-generate the nonce for a userContextId. * * Currently, this function is not hooked to anything. + * + * @param {integer} userContextId The userContextId to re-create the nonce for */ #newCircuitForUserContextId(userContextId) { this.#noncesForUserContextId.set(userContextId, this.#nonce()); @@ -305,13 +444,182 @@ class TorDomainIsolatorImpl { )}` ); } + + /** + * Try to extract a browser from a channel. + * + * @param {nsIChannel} channel The channel to extract the browser from + * @returns {MozBrowser?} The browser the channel is associated to + */ + #getBrowserForChannel(channel) { + const browsers = + channel.loadInfo.browsingContext?.topChromeWindow?.gBrowser.browsers; + if (!browsers || !channel.loadInfo.browsingContext?.browserId) { + return null; + } + for (const browser of browsers) { + if (browser.browserId === channel.loadInfo.browsingContext.browserId) { + logger.debug( + "Matched browser with browserId", + channel.loadInfo.browsingContext.browserId + ); + return browser; + } + } + // Expected to arrive here for example for the update checker. + // If we find a way to check that, we could raise the level to a warn. + logger.debug("Browser not matched", channel); + return null; + } + + /** + * Associate the SOCKS credentials to a browser. + * If needed (the browser is associated for the first time, or it was already + * known but its credential changed), notify the related circuit display. + * + * @param {MozBrowser} browser The browser to track + * @param {string} username The SOCKS username + * @param {string} password The SOCKS password + */ + #trackBrowser(browser, username, password) { + let browserCircuits = this.#browsers.get(browser.browserId); + if (!browserCircuits) { + browserCircuits = new Map(); + this.#browsers.set(browser.browserId, browserCircuits); + } + const circuitIds = browserCircuits.get(username) ?? {}; + const id = this.#credentialsToId(username, password); + if (circuitIds.current === id) { + // The circuit with these credentials was already built (we already knew + // its nodes, or we would not have promoted it to the current circuit). + // We do not need to do anything else, because we cannot detect a change + // of nodes here. + return; + } + + logger.debug( + `Found new credentials ${username} ${password} for browser`, + browser + ); + const circuit = this.#knownCircuits.get(id); + if (circuit?.length) { + circuitIds.current = id; + if (circuitIds.pending === id) { + circuitIds.pending = null; + } + browserCircuits.set(username, circuitIds); + // FIXME: We only notify the circuit display when we have a change that + // involves circuits whose nodes are known, for now. We need to resolve a + // few other techical problems (e.g., associate the circuit to the + // document?) and develop a UX with some animation to notify the circuit + // display more often. + // See tor-browser#41700 and tor-browser!699. + // In any case, notify the circuit display only after the internal map has + // been updated. + this.#notifyCircuitDisplay(); + } else if (circuitIds.pending !== id) { + // We do not have node data, so we store that we might need to track this. + // Otherwise, when a circuit is ready, we do not know which browser was it + // used for. + circuitIds.pending = id; + browserCircuits.set(username, circuitIds); + } + } + + /** + * Update a circuit, and notify the related circuit displays if it changed. + * + * This function is called when a certain stream has succeeded and so we can + * associate its SOCKS credential to the circuit it is using. + * We receive only the fingerprints of the circuit nodes, but they are enough + * to check if the circuit has changed. If it has, we also get the nodes' + * information through the control port. + * + * @param {string} username The SOCKS username + * @param {string} password The SOCKS password + * @param {NodeFingerprint[]} circuit The fingerprints of the nodes that + * compose the circuit + */ + async #updateCircuit(username, password, circuit) { + const id = this.#credentialsToId(username, password); + let data = this.#knownCircuits.get(id) ?? []; + // Should we modify the lower layer to send a circuit identifier, instead? + if ( + circuit.length === data.length && + circuit.every((id, index) => id === data[index].fingerprint) + ) { + return; + } + + data = await Promise.all( + circuit.map(fingerprint => + lazy.TorProtocolService.getNodeInfo(fingerprint) + ) + ); + this.#knownCircuits.set(id, data); + // We know that something changed, but we cannot know if anyone is + // interested in this change. So, we have to notify all the possible + // consumers of the data in any case. + // Not being specific and let them check if they need to do something allows + // us to keep a simpler structure. + this.#notifyCircuitDisplay(); + } + + /** + * Broadcast a notification when a circuit changed, or a browser is changing + * circuit (which might happen also in case of navigation). + */ + #notifyCircuitDisplay() { + Services.obs.notifyObservers(null, TOR_CIRCUIT_TOPIC); + } + + /** + * Clear the known circuit information, when they are not needed anymore. + * + * We keep circuit data around for a while. We decouple it from the underlying + * tor circuit management in case the user clicks on the circuit display when + * circuit has long gone. + * However, data accumulate during a session. So, since we store all the + * browsers that used a circuit anyway, every now and then we check if we + * still know browsers using a certain circuits. If there are not, we forget + * about it. + * + * This function is run by an interval. + */ + #clearKnownCircuits() { + logger.info("Running the circuit cleanup"); + const windows = []; + const enumerator = Services.wm.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + windows.push(enumerator.getNext()); + } + const browsers = windows + .flatMap(win => win.gBrowser.browsers.map(b => b.browserId)) + .filter(id => this.#browsers.has(id)); + this.#browsers = new Map(browsers.map(id => [id, this.#browsers.get(id)])); + this.#knownCircuits = new Map( + Array.from(this.#browsers.values(), circuits => + Array.from(circuits.values(), ids => { + const r = []; + const current = this.#knownCircuits.get(ids.current); + if (current) { + r.push([ids.current, current]); + } + const pending = this.#knownCircuits.get(ids.pending); + if (pending) { + r.push([ids.pending, pending]); + } + return r; + }) + ).flat(2) + ); + } } /** * Get the first party domain for a certain browser. * - * @param browser The browser to get the FP-domain for. - * + * @param {MozBrowser} browser The browser to get the FP-domain for. * Please notice that it should be gBrowser.selectedBrowser, because * browser.documentURI is the actual shown page, and might be an error page. * In this case, we rely on currentURI, which for gBrowser is an alias of @@ -358,6 +666,6 @@ function getDomainForBrowser(browser) { return fpd; } -const TorDomainIsolator = new TorDomainIsolatorImpl(); +export const TorDomainIsolator = new TorDomainIsolatorImpl(); // Reduce global vars pollution TorDomainIsolator.getDomainForBrowser = getDomainForBrowser; ===================================== toolkit/components/tor-launcher/TorMonitorService.sys.mjs ===================================== @@ -19,6 +19,10 @@ ChromeUtils.defineModuleGetter( "resource://torbutton/modules/tor-control-port.js" ); +ChromeUtils.defineESModuleGetters(lazy, { + TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs", +}); + const logger = new ConsoleAPI({ maxLogLevel: "warn", maxLogLevelPref: "browser.tor_monitor_service.log_level", @@ -37,12 +41,34 @@ const TorTopics = Object.freeze({ ProcessRestarted: "TorProcessRestarted", }); +export const TorMonitorTopics = Object.freeze({ + BridgeChanged: "TorBridgeChanged", + StreamSucceeded: "TorStreamSucceeded", +}); + const ControlConnTimings = Object.freeze({ initialDelayMS: 25, // Wait 25ms after the process has started, before trying to connect maxRetryMS: 10000, // Retry at most every 10 seconds timeoutMS: 5 * 60 * 1000, // Wait at most 5 minutes for tor to start }); +/** + * From control-spec.txt: + * CircuitID = 1*16 IDChar + * IDChar = ALPHA / DIGIT + * Currently, Tor only uses digits, but this may change. + * + * @typedef {string} CircuitID + */ +/** + * The fingerprint of a node. + * From control-spec.txt: + * Fingerprint = "$" 40*HEXDIG + * However, we do not keep the $ in our structures. + * + * @typedef {string} NodeFingerprint + */ + /** * This service monitors an existing Tor instance, or starts one, if needed, and * then starts monitoring it. @@ -52,7 +78,7 @@ const ControlConnTimings = Object.freeze({ */ export const TorMonitorService = { _connection: null, - _eventsToMonitor: Object.freeze(["STATUS_CLIENT", "NOTICE", "WARN", "ERR"]), + _eventHandlers: {}, _torLog: [], // Array of objects with date, type, and msg properties. _startTimeout: null, @@ -64,6 +90,28 @@ export const TorMonitorService = { _inited: false, + /** + * Stores the nodes of a circuit. Keys are cicuit IDs, and values are the node + * fingerprints. + * + * Theoretically, we could hook this map up to the new identity notification, + * but in practice it does not work. Tor pre-builds circuits, and the NEWNYM + * signal does not affect them. So, we might end up using a circuit that was + * built before the new identity but not yet used. If we cleaned the map, we + * risked of not having the data about it. + * + * @type {Map<CircuitID, NodeFingerprint[]>} + */ + _circuits: new Map(), + /** + * The last used bridge, or null if bridges are not in use or if it was not + * possible to detect the bridge. This needs the user to have specified bridge + * lines with fingerprints to work. + * + * @type {NodeFingerprint?} + */ + _currentBridge: null, + // Public methods // Starts Tor, if needed, and starts monitoring for events @@ -72,14 +120,28 @@ export const TorMonitorService = { return; } this._inited = true; + + // We always liten to these events, because they are needed for the circuit + // display. + this._eventHandlers = new Map([ + ["CIRC", this._processCircEvent.bind(this)], + ["STREAM", this._processStreamEvent.bind(this)], + ]); + if (this.ownsTorDaemon) { + // When we own the tor daemon, we listen to more events, that are used + // for about:torconnect or for showing the logs in the settings page. + this._eventHandlers.set("STATUS_CLIENT", (_eventType, lines) => + this._processBootstrapStatus(lines[0], false) + ); + this._eventHandlers.set("NOTICE", this._processLog.bind(this)); + this._eventHandlers.set("WARN", this._processLog.bind(this)); + this._eventHandlers.set("ERR", this._processLog.bind(this)); this._controlTor(); } else { - logger.info( - "Not starting the event monitor, as we do not own the Tor daemon." - ); + this._startEventMonitor(); } - logger.debug("TorMonitorService initialized"); + logger.info("TorMonitorService initialized"); }, // Closes the connection that monitors for events. @@ -153,6 +215,18 @@ export const TorMonitorService = { return !!this._connection; }, + /** + * Return the data about the current bridge, if any, or null. + * We can detect bridge only when the configured bridge lines include the + * fingerprints. + * + * @returns {NodeData?} The node information, or null if the first node + * is not a bridge, or no circuit has been opened, yet. + */ + get currentBridge() { + return this._currentBridge; + }, + // Private methods async _startProcess() { @@ -272,7 +346,7 @@ export const TorMonitorService = { // TODO: optionally monitor INFO and DEBUG log messages. let reply = await conn.sendCommand( - "SETEVENTS " + this._eventsToMonitor.join(" ") + "SETEVENTS " + Array.from(this._eventHandlers.keys()).join(" ") ); reply = TorParsers.parseCommandResponse(reply); if (!TorParsers.commandSucceeded(reply)) { @@ -281,14 +355,10 @@ export const TorMonitorService = { return false; } - // FIXME: At the moment it is not possible to start the event monitor - // when we do start the tor process. So, does it make sense to keep this - // control? if (this._torProcess) { this._torProcess.connectionWorked(); } - - if (!TorLauncherUtil.shouldOnlyConfigureTor) { + if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) { try { await this._takeTorOwnership(conn); } catch (e) { @@ -297,7 +367,31 @@ export const TorMonitorService = { } this._connection = conn; - this._waitForEventData(); + + for (const [type, callback] of this._eventHandlers.entries()) { + this._monitorEvent(type, callback); + } + + // Populate the circuit map already, in case we are connecting to an + // external tor daemon. + try { + const reply = await this._connection.sendCommand( + "GETINFO circuit-status" + ); + const lines = reply.split(/\r?\n/); + if (lines.shift() === "250+circuit-status=") { + for (const line of lines) { + if (line === ".") { + break; + } + // _processCircEvent processes only one line at a time + this._processCircEvent("CIRC", [line]); + } + } + } catch (e) { + logger.warn("Could not populate the initial circuit map", e); + } + return true; }, @@ -318,65 +412,49 @@ export const TorMonitorService = { } }, - _waitForEventData() { - if (!this._connection) { - return; - } - logger.debug("Start watching events:", this._eventsToMonitor); + _monitorEvent(type, callback) { + logger.info(`Watching events of type ${type}.`); let replyObj = {}; - for (const torEvent of this._eventsToMonitor) { - this._connection.watchEvent( - torEvent, - null, - line => { - if (!line) { - return; - } - logger.debug("Event response: ", line); - const isComplete = TorParsers.parseReplyLine(line, replyObj); - if (isComplete) { - this._processEventReply(replyObj); - replyObj = {}; - } - }, - true - ); - } + this._connection.watchEvent( + type, + null, + line => { + if (!line) { + return; + } + logger.debug("Event response: ", line); + const isComplete = TorParsers.parseReplyLine(line, replyObj); + if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) { + return; + } + const reply = replyObj; + replyObj = {}; + if (reply.statusCode !== TorStatuses.EventNotification) { + logger.error("Unexpected event status code:", reply.statusCode); + return; + } + if (!reply.lineArray[0].startsWith(`${type} `)) { + logger.error("Wrong format for the first line:", reply.lineArray[0]); + return; + } + reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1); + try { + callback(type, reply.lineArray); + } catch (e) { + logger.error("Exception while handling an event", reply, e); + } + }, + true + ); }, - _processEventReply(aReply) { - if (aReply._parseError || !aReply.lineArray.length) { - return; - } - - if (aReply.statusCode !== TorStatuses.EventNotification) { - logger.warn("Unexpected event status code:", aReply.statusCode); - return; - } - - // TODO: do we need to handle multiple lines? - const s = aReply.lineArray[0]; - const idx = s.indexOf(" "); - if (idx === -1) { - return; - } - const eventType = s.substring(0, idx); - const msg = s.substring(idx + 1).trim(); - - if (eventType === "STATUS_CLIENT") { - this._processBootstrapStatus(msg, false); - return; - } else if (!this._eventsToMonitor.includes(eventType)) { - logger.debug(`Dropping unlistened event ${eventType}`); - return; - } - - if (eventType === "WARN" || eventType === "ERR") { + _processLog(type, lines) { + if (type === "WARN" || type === "ERR") { // Notify so that Copy Log can be enabled. Services.obs.notifyObservers(null, TorTopics.HasWarnOrErr); } - const now = new Date(); + const date = new Date(); const maxEntries = Services.prefs.getIntPref( "extensions.torlauncher.max_tor_log_entries", 1000 @@ -384,8 +462,10 @@ export const TorMonitorService = { if (maxEntries > 0 && this._torLog.length >= maxEntries) { this._torLog.splice(0, 1); } - this._torLog.push({ date: now, type: eventType, msg }); - const logString = `Tor ${eventType}: ${msg}`; + + const msg = lines.join("\n"); + this._torLog.push({ date, type, msg }); + const logString = `Tor ${type}: ${msg}`; logger.info(logString); }, @@ -461,8 +541,108 @@ export const TorMonitorService = { } }, + async _processCircEvent(_type, lines) { + const builtEvent = + /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec( + lines[0] + ); + const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]); + if (builtEvent) { + const fp = /\$([0-9a-fA-F]{40})/g; + const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g => + g[1].toUpperCase() + ); + this._circuits.set(builtEvent.groups.CircuitID, nodes); + // Ignore circuits of length 1, that are used, for example, to probe + // bridges. So, only store them, since we might see streams that use them, + // but then early-return. + if (nodes.length === 1) { + return; + } + // In some cases, we might already receive SOCKS credentials in the line. + // However, this might be a problem with onion services: we get also a + // 4-hop circuit that we likely do not want to show to the user, + // especially because it is used only temporarily, and it would need a + // technical explaination. + // this._checkCredentials(lines[0], nodes); + if (this._currentBridge?.fingerprint !== nodes[0]) { + const nodeInfo = await lazy.TorProtocolService.getNodeInfo(nodes[0]); + let notify = false; + if (nodeInfo?.bridgeType) { + logger.info(`Bridge changed to ${nodes[0]}`); + this._currentBridge = nodeInfo; + notify = true; + } else if (this._currentBridge) { + logger.info("Bridges disabled"); + this._currentBridge = null; + notify = true; + } + if (notify) { + Services.obs.notifyObservers( + null, + TorMonitorTopics.BridgeChanged, + this._currentBridge + ); + } + } + } else if (closedEvent) { + this._circuits.delete(closedEvent.groups.ID); + } + }, + + _processStreamEvent(_type, lines) { + // The first block is the stream ID, which we do not need at the moment. + const succeeedEvent = + /^[a-zA-Z0-9]{1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec( + lines[0] + ); + if (!succeeedEvent) { + return; + } + const circuit = this._circuits.get(succeeedEvent.groups.CircuitID); + if (!circuit) { + logger.error( + "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers.", + lines[0] + ); + return; + } + this._checkCredentials(lines[0], circuit); + }, + + /** + * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and + * SOCKS_PASSWORD. In case, notify observers that we could associate a certain + * circuit to these credentials. + * + * @param {string} line The circ or stream line to check + * @param {NodeFingerprint[]} circuit The fingerprints of the nodes in the + * circuit. + */ + _checkCredentials(line, circuit) { + const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line); + const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line); + if (!username || !password) { + return; + } + Services.obs.notifyObservers( + { + wrappedJSObject: { + username: TorParsers.unescapeString(username[1]), + password: TorParsers.unescapeString(password[1]), + circuit, + }, + }, + TorMonitorTopics.StreamSucceeded + ); + }, + _shutDownEventMonitor() { - this._connection?.close(); + try { + this._connection?.close(); + } catch (e) { + logger.error("Could not close the connection to the control port", e); + } this._connection = null; if (this._startTimeout !== null) { clearTimeout(this._startTimeout); ===================================== toolkit/components/tor-launcher/TorParsers.sys.mjs ===================================== @@ -181,12 +181,12 @@ export const TorParsers = Object.freeze({ return aStr; } const escaped = aStr - .replace("\\", "\\\\") - .replace('"', '\\"') - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t") - .replace(/[^\x20-\x7e]+/g, text => { + .replaceAll("\\", "\\\\") + .replaceAll('"', '\\"') + .replaceAll("\n", "\\n") + .replaceAll("\r", "\\r") + .replaceAll("\t", "\\t") + .replaceAll(/[^\x20-\x7e]+/g, text => { const encoder = new TextEncoder(); return Array.from( encoder.encode(text), ===================================== toolkit/components/tor-launcher/TorProtocolService.sys.mjs ===================================== @@ -40,6 +40,20 @@ const logger = new ConsoleAPI({ prefix: "TorProtocolService", }); +/** + * Stores the data associated with a circuit node. + * + * @typedef NodeData + * @property {string} fingerprint The node fingerprint. + * @property {string[]} ipAddrs - The ip addresses associated with this node. + * @property {string?} bridgeType - The bridge type for this node, or "" if the + * node is a bridge but the type is unknown, or null if this is not a bridge + * node. + * @property {string?} regionCode - An upper case 2-letter ISO3166-1 code for + * the first ip address, or null if there is no region. This should also be a + * valid BCP47 Region subtag. + */ + // Manage the connection to tor's control port, to update its settings and query // other useful information. // @@ -188,6 +202,89 @@ export const TorProtocolService = { return TorParsers.parseReply(cmd, keyword, response); }, + async getBridges() { + // Ideally, we would not need this function, because we should be the one + // setting them with TorSettings. However, TorSettings is not notified of + // change of settings. So, asking tor directly with the control connection + // is the most reliable way of getting the configured bridges, at the + // moment. Also, we are using this for the circuit display, which should + // work also when we are not configuring the tor daemon, but just using it. + return this._withConnection(conn => { + return conn.getConf("bridge"); + }); + }, + + /** + * Returns tha data about a relay or a bridge. + * + * @param {string} id The fingerprint of the node to get data about + * @returns {NodeData} + */ + async getNodeInfo(id) { + return this._withConnection(async conn => { + const node = { + fingerprint: id, + ipAddrs: [], + bridgeType: null, + regionCode: null, + }; + const bridge = (await conn.getConf("bridge"))?.find( + foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase() + ); + const addrRe = /^\[?([^\]]+)\]?:\d+$/; + if (bridge) { + node.bridgeType = bridge.type ?? ""; + // Attempt to get an IP address from bridge address string. + const ip = bridge.address.match(addrRe)?.[1]; + if (ip && !ip.startsWith("0.")) { + node.ipAddrs.push(ip); + } + } else { + // Either dealing with a relay, or a bridge whose fingerprint is not + // saved in torrc. + const info = await conn.getInfo(`ns/id/${id}`); + if (info.IP && !info.IP.startsWith("0.")) { + node.ipAddrs.push(info.IP); + } + const ip6 = info.IPv6?.match(addrRe)?.[1]; + if (ip6) { + node.ipAddrs.push(ip6); + } + } + if (node.ipAddrs.length) { + // Get the country code for the node's IP address. + let regionCode; + try { + // Expect a 2-letter ISO3166-1 code, which should also be a valid + // BCP47 Region subtag. + regionCode = await conn.getInfo("ip-to-country/" + node.ipAddrs[0]); + } catch {} + if (regionCode && regionCode !== "??") { + node.regionCode = regionCode.toUpperCase(); + } + } + return node; + }); + }, + + async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) { + return this._withConnection(conn => { + return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent); + }); + }, + + async onionAuthRemove(hsAddress) { + return this._withConnection(conn => { + return conn.onionAuthRemove(hsAddress); + }); + }, + + async onionAuthViewKeys() { + return this._withConnection(conn => { + return conn.onionAuthViewKeys(); + }); + }, + // TODO: transform the following 4 functions in getters. At the moment they // are also used in torbutton. @@ -630,6 +727,16 @@ export const TorProtocolService = { } }, + async _withConnection(func) { + // TODO: Make more robust? + const conn = await this._getConnection(); + try { + return await func(conn); + } finally { + this._returnConnection(); + } + }, + // If aConn is omitted, the cached connection is closed. _closeConnection() { if (this._controlConnection) { ===================================== toolkit/components/tor-launcher/TorStartupService.sys.mjs ===================================== @@ -3,6 +3,7 @@ const lazy = {}; // We will use the modules only when the profile is loaded, so prefer lazy // loading ChromeUtils.defineESModuleGetters(lazy, { + TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs", TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs", TorMonitorService: "resource://gre/modules/TorMonitorService.sys.mjs", TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs", @@ -19,12 +20,6 @@ ChromeUtils.defineModuleGetter( "resource:///modules/TorSettings.jsm" ); -ChromeUtils.defineModuleGetter( - lazy, - "TorDomainIsolator", - "resource://gre/modules/TorDomainIsolator.jsm" -); - /* Browser observer topis */ const BrowserTopics = Object.freeze({ ProfileAfterChange: "profile-after-change", ===================================== toolkit/components/tor-launcher/moz.build ===================================== @@ -1,6 +1,6 @@ EXTRA_JS_MODULES += [ "TorBootstrapRequest.sys.mjs", - "TorDomainIsolator.jsm", + "TorDomainIsolator.sys.mjs", "TorLauncherUtil.sys.mjs", "TorMonitorService.sys.mjs", "TorParsers.sys.mjs", View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/bdda46… -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/bdda46… You're receiving this email because of your account on gitlab.torproject.org.
1 0
0 0
  • ← Newer
  • 1
  • ...
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • ...
  • 1873
  • Older →

HyperKitty Powered by HyperKitty version 1.3.12.