intrigeri pushed to branch main at The Tor Project / Applications / torbrowser-launcher
Commits:
692e28a1 by intrigeri at 2025-03-18T13:49:53+00:00
AppArmor: allow unprivileged user namespaces
Firefox uses userns to set up its own sandboxing.
On Debian, AppArmor was already allowing this by default, until a recent
upload (that is now in Trixie) updated the features pinning to a version that
now mediates usage of userns, so this functionality is now blocked by profiles
that don't explicitly allow it. Let's repair this.
Also reported as Debian#1098845.
- - - - -
91db109a by intrigeri at 2025-03-18T14:02:01+00:00
AppArmor: allow reading cgroups-v2 CPU bandwidth quota information
Firefox uses this info to determine how many CPUs the current thread actually
has access to, which seems like a reasonable thing to do for an app like Firefox
which manages a bunch of child processes. The call chain is: get_num_cpus →
cgroups_num_cpus → init_cgroups → load_cgroups → cpu_quota → max → "cpu.max".
- - - - -
7772a1ea by intrigeri at 2025-03-18T14:15:17+00:00
AppArmor: allow executing Firefox' own VA-API probe utility
This is necessary for Tor Browser to determine if VA-API is supported by the
host system, which in turn is needed to enable video hardware decoding.
- - - - -
9eb8686d by intrigeri at 2025-03-18T14:18:41+00:00
AppArmor: allow reading intel-media-driver feature files
Firefox reads these files when it runs the vaapitest tool and the VAAPI driver
for the Intel GEN8+ Graphics family is installed.
- - - - -
479b8f53 by intrigeri at 2025-03-18T17:00:32+00:00
Merge branch 'AppArmor-updates-for-current-Debian' into 'main'
AppArmor: various updates including 1 important fix for Debian Trixie
See merge request tpo/applications/torbrowser-launcher!24
- - - - -
1 changed file:
- apparmor/torbrowser.Browser.firefox
Changes:
=====================================
apparmor/torbrowser.Browser.firefox
=====================================
@@ -13,6 +13,8 @@ profile torbrowser_firefox @{torbrowser_firefox_executable} {
#include if exists <abstractions/vulkan>
#include if exists <abstractions/dbus-session-strict>
+ userns,
+
deny capability sys_ptrace,
# Uncomment the following lines if you want to give the Tor Browser read-write
@@ -94,6 +96,10 @@ profile torbrowser_firefox @{torbrowser_firefox_executable} {
owner @{torbrowser_home_dir}/TorBrowser/Tor/*.so.* mr,
owner @{torbrowser_home_dir}/TorBrowser/Tor/libstdc++/*.so mr,
owner @{torbrowser_home_dir}/TorBrowser/Tor/libstdc++/*.so.* mr,
+ owner @{torbrowser_home_dir}/vaapitest ix,
+
+ # intel-media-driver
+ /etc/igfx_user_feature*.txt r,
# parent Firefox process when restarting after upgrade, Web Content processes
owner @{torbrowser_firefox_executable} pxmr -> torbrowser_firefox,
@@ -121,6 +127,7 @@ profile torbrowser_firefox @{torbrowser_firefox_executable} {
/sys/devices/system/node/ r,
/sys/devices/system/node/node[0-9]*/meminfo r,
/sys/fs/cgroup/cpu,cpuacct/{,user.slice/}cpu.cfs_quota_us r,
+ /sys/fs/cgroup/user.slice/user-[0-9]*.slice/user(a)[0-9]*.service/app.slice/app-gnome-torbrowser-[0-9]*.scope/cpu.max r,
deny /sys/class/input/ r,
deny /sys/devices/virtual/block/*/uevent r,
View it on GitLab: https://gitlab.torproject.org/tpo/applications/torbrowser-launcher/-/compar…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/torbrowser-launcher/-/compar…
You're receiving this email because of your account on gitlab.torproject.org.
boklm pushed to branch main at The Tor Project / Applications / torbrowser-launcher
Commits:
a734238b by Integral at 2025-03-18T17:55:44+01:00
fix: window icon under wayland
Currently, when running the launcher on Wayland, the window icon
will fallback to the generic Wayland icon. Set desktop filename to
solve this problem.
- - - - -
1 changed file:
- torbrowser_launcher/__init__.py
Changes:
=====================================
torbrowser_launcher/__init__.py
=====================================
@@ -81,6 +81,9 @@ def main():
common = Common(tor_browser_launcher_version)
app = Application()
+ if "WAYLAND_DISPLAY" in os.environ:
+ app.setDesktopFileName("torbrowser")
+
# Open the window
gui = None
View it on GitLab: https://gitlab.torproject.org/tpo/applications/torbrowser-launcher/-/commit…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/torbrowser-launcher/-/commit…
You're receiving this email because of your account on gitlab.torproject.org.
henry pushed to branch tor-browser-128.8.0esr-14.5-1 at The Tor Project / Applications / Tor Browser
Commits:
202a08d2 by Henry Wilkes at 2025-03-18T15:25:24+00:00
fixup! TB 40933: Add tor-launcher functionality
TB 43405: Split TorProvider writeSettings into separate methods for the
proxy, firewall and bridges settings.
We also call TorSettings.setTorProvider instead of
TorProvider.writeSettings so that TorSettings can handle the application
errors.
- - - - -
a88b1fd3 by Henry Wilkes at 2025-03-18T15:25:25+00:00
fixup! TB 40597: Implement TorSettings module
TB 43405: Do not allow string values for proxy and firewall ports. And
do not allow a proxy username without a password or vis versa.
- - - - -
ebfa2591 by Henry Wilkes at 2025-03-18T15:25:26+00:00
fixup! TB 40597: Implement TorSettings module
TB 43405: TorSettings handles failures to apply Tor settings.
We update TorSettings.#applySettings to catch TorProvider write errors
and signal this error with "ApplyError".
We also keep track of which group of settings have failed so that we can
restore them on the user's request.
- - - - -
cd596922 by Henry Wilkes at 2025-03-18T15:25:27+00:00
fixup! TB 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
TB 43405: Add some validation to the Advanced connection settings
dialog.
- - - - -
58ad673d by Henry Wilkes at 2025-03-18T15:25:27+00:00
TB 43405: Show a prompt whenever we fail to apply Tor settings.
- - - - -
eb9525d5 by Henry Wilkes at 2025-03-18T15:25:28+00:00
fixup! Tor Browser strings
TB 43405: Add strings for tor settings error notification.
- - - - -
8 changed files:
- browser/components/BrowserGlue.sys.mjs
- browser/components/torpreferences/content/connectionSettingsDialog.js
- browser/components/torpreferences/content/connectionSettingsDialog.xhtml
- + browser/modules/TorSettingsNotification.sys.mjs
- browser/modules/moz.build
- toolkit/components/tor-launcher/TorProvider.sys.mjs
- toolkit/locales/en-US/toolkit/global/tor-browser.ftl
- toolkit/modules/TorSettings.sys.mjs
Changes:
=====================================
browser/components/BrowserGlue.sys.mjs
=====================================
@@ -96,6 +96,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
TorConnectTopics: "resource://gre/modules/TorConnect.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorSettingsNotification:
+ "resource:///modules/TorSettingsNotification.sys.mjs",
UIState: "resource://services-sync/UIState.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
@@ -2030,6 +2032,8 @@ BrowserGlue.prototype = {
lazy.TorProviderBuilder.firstWindowLoaded();
+ lazy.TorSettingsNotification.ready();
+
ClipboardPrivacy.startup();
this._firstWindowTelemetry(aWindow);
=====================================
browser/components/torpreferences/content/connectionSettingsDialog.js
=====================================
@@ -5,6 +5,7 @@ const { TorSettings, TorProxyType } = ChromeUtils.importESModule(
);
const gConnectionSettingsDialog = {
+ _acceptButton: null,
_useProxyCheckbox: null,
_proxyTypeLabel: null,
_proxyTypeMenulist: null,
@@ -38,25 +39,43 @@ const gConnectionSettingsDialog = {
"input#torPreferences-connection-textboxAllowedPorts",
},
- // disables the provided list of elements
- _setElementsDisabled(elements, disabled) {
- for (let currentElement of elements) {
- currentElement.disabled = disabled;
- }
- },
+ /**
+ * The "proxy" and "firewall" settings to pass on to TorSettings.
+ *
+ * Each group is `null` whilst the inputs are invalid.
+ *
+ * @type {{proxy: ?object, firewall: ?object}}
+ */
+ _settings: { proxy: null, firewall: null },
init() {
+ const currentSettings = TorSettings.getSettings();
+
+ const dialog = document.getElementById("torPreferences-connection-dialog");
+ dialog.addEventListener("dialogaccept", event => {
+ if (!this._settings.proxy || !this._settings.firewall) {
+ // Do not close yet.
+ event.preventDefault();
+ return;
+ }
+ // TODO: Maybe wait for the method to resolve before closing. Although
+ // this can take a few seconds. See tor-browser#43467.
+ TorSettings.changeSettings(this._settings);
+ });
+ this._acceptButton = dialog.getButton("accept");
+
const selectors = this.selectors;
// Local Proxy
this._useProxyCheckbox = document.querySelector(selectors.useProxyCheckbox);
+ this._useProxyCheckbox.checked = currentSettings.proxy.enabled;
this._useProxyCheckbox.addEventListener("command", () => {
- const checked = this._useProxyCheckbox.checked;
- this.onToggleProxy(checked);
+ this.updateProxyType();
});
+
this._proxyTypeLabel = document.querySelector(selectors.proxyTypeLabel);
- let mockProxies = [
+ const mockProxies = [
{
value: TorProxyType.Socks4,
l10nId: "tor-advanced-dialog-proxy-socks4-menuitem",
@@ -72,15 +91,17 @@ const gConnectionSettingsDialog = {
];
this._proxyTypeMenulist = document.querySelector(selectors.proxyTypeList);
this._proxyTypeMenulist.addEventListener("command", () => {
- const value = this._proxyTypeMenulist.value;
- this.onSelectProxyType(value);
+ this.updateProxyType();
});
- for (let currentProxy of mockProxies) {
+ for (const currentProxy of mockProxies) {
let menuEntry = window.document.createXULElement("menuitem");
menuEntry.setAttribute("value", currentProxy.value);
menuEntry.setAttribute("data-l10n-id", currentProxy.l10nId);
this._proxyTypeMenulist.querySelector("menupopup").appendChild(menuEntry);
}
+ this._proxyTypeMenulist.value = currentSettings.proxy.enabled
+ ? currentSettings.proxy.type
+ : "";
this._proxyAddressLabel = document.querySelector(
selectors.proxyAddressLabel
@@ -89,13 +110,15 @@ const gConnectionSettingsDialog = {
selectors.proxyAddressTextbox
);
this._proxyAddressTextbox.addEventListener("blur", () => {
+ // If the address includes a port move it to the port input instead.
let value = this._proxyAddressTextbox.value.trim();
let colon = value.lastIndexOf(":");
if (colon != -1) {
- let maybePort = parseInt(value.substr(colon + 1));
- if (!isNaN(maybePort) && maybePort > 0 && maybePort < 65536) {
+ let maybePort = this.parsePort(value.substr(colon + 1));
+ if (maybePort !== null) {
this._proxyAddressTextbox.value = value.substr(0, colon);
this._proxyPortTextbox.value = maybePort;
+ this.updateProxy();
}
}
});
@@ -114,23 +137,36 @@ const gConnectionSettingsDialog = {
selectors.proxyPasswordTextbox
);
- this.onToggleProxy(false);
- if (TorSettings.proxy.enabled) {
- this.onToggleProxy(true);
- this.onSelectProxyType(TorSettings.proxy.type);
- this._proxyAddressTextbox.value = TorSettings.proxy.address;
- this._proxyPortTextbox.value = TorSettings.proxy.port;
- this._proxyUsernameTextbox.value = TorSettings.proxy.username;
- this._proxyPasswordTextbox.value = TorSettings.proxy.password;
+ if (currentSettings.proxy.enabled) {
+ this._proxyAddressTextbox.value = currentSettings.proxy.address;
+ this._proxyPortTextbox.value = currentSettings.proxy.port;
+ this._proxyUsernameTextbox.value = currentSettings.proxy.username;
+ this._proxyPasswordTextbox.value = currentSettings.proxy.password;
+ } else {
+ this._proxyAddressTextbox.value = "";
+ this._proxyPortTextbox.value = "";
+ this._proxyUsernameTextbox.value = "";
+ this._proxyPasswordTextbox.value = "";
+ }
+
+ for (const el of [
+ this._proxyAddressTextbox,
+ this._proxyPortTextbox,
+ this._proxyUsernameTextbox,
+ this._proxyPasswordTextbox,
+ ]) {
+ el.addEventListener("input", () => {
+ this.updateProxy();
+ });
}
// Local firewall
this._useFirewallCheckbox = document.querySelector(
selectors.useFirewallCheckbox
);
+ this._useFirewallCheckbox.checked = currentSettings.firewall.enabled;
this._useFirewallCheckbox.addEventListener("command", () => {
- const checked = this._useFirewallCheckbox.checked;
- this.onToggleFirewall(checked);
+ this.updateFirewallEnabled();
});
this._allowedPortsLabel = document.querySelector(
selectors.firewallAllowedPortsLabel
@@ -138,182 +174,161 @@ const gConnectionSettingsDialog = {
this._allowedPortsTextbox = document.querySelector(
selectors.firewallAllowedPortsTextbox
);
+ this._allowedPortsTextbox.value = currentSettings.firewall.enabled
+ ? currentSettings.firewall.allowed_ports.join(",")
+ : "80,443";
- this.onToggleFirewall(false);
- if (TorSettings.firewall.enabled) {
- this.onToggleFirewall(true);
- this._allowedPortsTextbox.value =
- TorSettings.firewall.allowed_ports.join(", ");
- }
-
- const dialog = document.getElementById("torPreferences-connection-dialog");
- dialog.addEventListener("dialogaccept", () => {
- this._applySettings();
+ this._allowedPortsTextbox.addEventListener("input", () => {
+ this.updateFirewall();
});
- },
- // callback when proxy is toggled
- onToggleProxy(enabled) {
- this._useProxyCheckbox.checked = enabled;
- let disabled = !enabled;
+ this.updateProxyType();
+ this.updateFirewallEnabled();
+ },
- this._setElementsDisabled(
- [
- this._proxyTypeLabel,
- this._proxyTypeMenulist,
- this._proxyAddressLabel,
- this._proxyAddressTextbox,
- this._proxyPortLabel,
- this._proxyPortTextbox,
- this._proxyUsernameLabel,
- this._proxyUsernameTextbox,
- this._proxyPasswordLabel,
- this._proxyPasswordTextbox,
- ],
- disabled
- );
- if (enabled) {
- this.onSelectProxyType(this._proxyTypeMenulist.value);
+ /**
+ * Convert a string into a port number.
+ *
+ * @param {string} portStr - The string to convert.
+ * @returns {?integer} - The port number, or `null` if the given string could
+ * not be converted.
+ */
+ parsePort(portStr) {
+ const portRegex = /^[1-9][0-9]*$/; // Strictly-positive decimal integer.
+ if (!portRegex.test(portStr)) {
+ return null;
}
+ const port = parseInt(portStr, 10);
+ if (TorSettings.validPort(port)) {
+ return port;
+ }
+ return null;
},
- // callback when proxy type is changed
- onSelectProxyType(value) {
- if (typeof value === "string") {
- value = parseInt(value);
- }
+ /**
+ * Update the disabled state of the accept button.
+ */
+ updateAcceptButton() {
+ this._acceptButton.disabled =
+ !this._settings.proxy || !this._settings.firewall;
+ },
- this._proxyTypeMenulist.value = value;
- switch (value) {
- case TorProxyType.Invalid: {
- this._setElementsDisabled(
- [
- this._proxyAddressLabel,
- this._proxyAddressTextbox,
- this._proxyPortLabel,
- this._proxyPortTextbox,
- this._proxyUsernameLabel,
- this._proxyUsernameTextbox,
- this._proxyPasswordLabel,
- this._proxyPasswordTextbox,
- ],
- true
- ); // DISABLE
+ /**
+ * Update the UI when the proxy setting is enabled or disabled, or the proxy
+ * type changes.
+ */
+ updateProxyType() {
+ const enabled = this._useProxyCheckbox.checked;
+ const haveType = enabled && Boolean(this._proxyTypeMenulist.value);
+ const type = parseInt(this._proxyTypeMenulist.value, 10);
- this._proxyAddressTextbox.value = "";
- this._proxyPortTextbox.value = "";
- this._proxyUsernameTextbox.value = "";
- this._proxyPasswordTextbox.value = "";
- break;
- }
- case TorProxyType.Socks4: {
- this._setElementsDisabled(
- [
- this._proxyAddressLabel,
- this._proxyAddressTextbox,
- this._proxyPortLabel,
- this._proxyPortTextbox,
- ],
- false
- ); // ENABLE
- this._setElementsDisabled(
- [
- this._proxyUsernameLabel,
- this._proxyUsernameTextbox,
- this._proxyPasswordLabel,
- this._proxyPasswordTextbox,
- ],
- true
- ); // DISABLE
+ this._proxyTypeLabel.disabled = !enabled;
+ this._proxyTypeMenulist.disabled = !enabled;
+ this._proxyAddressLabel.disabled = !haveType;
+ this._proxyAddressTextbox.disabled = !haveType;
+ this._proxyPortLabel.disabled = !haveType;
+ this._proxyPortTextbox.disabled = !haveType;
+ this._proxyUsernameTextbox.disabled =
+ !haveType || type === TorProxyType.Socks4;
+ this._proxyPasswordTextbox.disabled =
+ !haveType || type === TorProxyType.Socks4;
+ if (type === TorProxyType.Socks4) {
+ // Clear unused value.
+ this._proxyUsernameTextbox.value = "";
+ this._proxyPasswordTextbox.value = "";
+ }
- this._proxyUsernameTextbox.value = "";
- this._proxyPasswordTextbox.value = "";
- break;
- }
- case TorProxyType.Socks5:
- case TorProxyType.HTTPS: {
- this._setElementsDisabled(
- [
- this._proxyAddressLabel,
- this._proxyAddressTextbox,
- this._proxyPortLabel,
- this._proxyPortTextbox,
- this._proxyUsernameLabel,
- this._proxyUsernameTextbox,
- this._proxyPasswordLabel,
- this._proxyPasswordTextbox,
- ],
- false
- ); // ENABLE
- break;
+ this.updateProxy();
+ },
+
+ /**
+ * Update the dialog's stored proxy values.
+ */
+ updateProxy() {
+ if (this._useProxyCheckbox.checked) {
+ const typeStr = this._proxyTypeMenulist.value;
+ const type = parseInt(typeStr, 10);
+ // TODO: Validate the address. See tor-browser#43467.
+ const address = this._proxyAddressTextbox.value;
+ const portStr = this._proxyPortTextbox.value;
+ const username =
+ type === TorProxyType.Socks4 ? "" : this._proxyUsernameTextbox.value;
+ const password =
+ type === TorProxyType.Socks4 ? "" : this._proxyPasswordTextbox.value;
+ const port = parseInt(portStr, 10);
+ if (
+ !typeStr ||
+ !address ||
+ !portStr ||
+ !TorSettings.validPort(port) ||
+ // SOCKS5 needs either both username and password, or neither.
+ (type === TorProxyType.Socks5 &&
+ !TorSettings.validSocks5Credentials(username, password))
+ ) {
+ // Invalid.
+ this._settings.proxy = null;
+ } else {
+ this._settings.proxy = {
+ enabled: true,
+ type,
+ address,
+ port,
+ username,
+ password,
+ };
}
+ } else {
+ this._settings.proxy = { enabled: false };
}
+
+ this.updateAcceptButton();
},
- // callback when firewall proxy is toggled
- onToggleFirewall(enabled) {
- this._useFirewallCheckbox.checked = enabled;
- let disabled = !enabled;
+ /**
+ * Update the UI when the firewall setting is enabled or disabled.
+ */
+ updateFirewallEnabled() {
+ const enabled = this._useFirewallCheckbox.checked;
+ this._allowedPortsLabel.disabled = !enabled;
+ this._allowedPortsTextbox.disabled = !enabled;
- this._setElementsDisabled(
- [this._allowedPortsLabel, this._allowedPortsTextbox],
- disabled
- );
+ this.updateFirewall();
},
- // pushes settings from UI to tor
- _applySettings() {
- const type = this._useProxyCheckbox.checked
- ? parseInt(this._proxyTypeMenulist.value)
- : TorProxyType.Invalid;
- const address = this._proxyAddressTextbox.value;
- const port = this._proxyPortTextbox.value;
- const username = this._proxyUsernameTextbox.value;
- const password = this._proxyPasswordTextbox.value;
- const settings = { proxy: {}, firewall: {} };
- switch (type) {
- case TorProxyType.Invalid:
- settings.proxy.enabled = false;
- break;
- case TorProxyType.Socks4:
- settings.proxy.enabled = true;
- settings.proxy.type = type;
- settings.proxy.address = address;
- settings.proxy.port = port;
- settings.proxy.username = "";
- settings.proxy.password = "";
- break;
- case TorProxyType.Socks5:
- settings.proxy.enabled = true;
- settings.proxy.type = type;
- settings.proxy.address = address;
- settings.proxy.port = port;
- settings.proxy.username = username;
- settings.proxy.password = password;
- break;
- case TorProxyType.HTTPS:
- settings.proxy.enabled = true;
- settings.proxy.type = type;
- settings.proxy.address = address;
- settings.proxy.port = port;
- settings.proxy.username = username;
- settings.proxy.password = password;
- break;
- }
-
- let portListString = this._useFirewallCheckbox.checked
- ? this._allowedPortsTextbox.value
- : "";
- if (portListString) {
- settings.firewall.enabled = true;
- settings.firewall.allowed_ports = portListString;
+ /**
+ * Update the dialog's stored firewall values.
+ */
+ updateFirewall() {
+ if (this._useFirewallCheckbox.checked) {
+ const portList = [];
+ let listInvalid = false;
+ for (const portStr of this._allowedPortsTextbox.value.split(
+ /(?:\s*,\s*)+/g
+ )) {
+ if (!portStr) {
+ // Trailing or leading comma.
+ continue;
+ }
+ const port = this.parsePort(portStr);
+ if (port === null) {
+ listInvalid = true;
+ break;
+ }
+ portList.push(port);
+ }
+ if (!listInvalid && portList.length) {
+ this._settings.firewall = {
+ enabled: true,
+ allowed_ports: portList,
+ };
+ } else {
+ this._settings.firewall = null;
+ }
} else {
- settings.firewall.enabled = false;
+ this._settings.firewall = { enabled: false };
}
- // FIXME: What if this fails? Should we prevent the dialog to close and show
- // an error?
- TorSettings.changeSettings(settings);
+ this.updateAcceptButton();
},
};
=====================================
browser/components/torpreferences/content/connectionSettingsDialog.xhtml
=====================================
@@ -61,6 +61,7 @@
<html:input
id="torPreferences-localProxy-textboxAddress"
type="text"
+ required="required"
class="torMarginFix"
data-l10n-id="tor-advanced-dialog-proxy-address-input"
/>
@@ -75,7 +76,8 @@
class="proxy-port-input torMarginFix"
hidespinbuttons="true"
type="number"
- min="0"
+ required="required"
+ min="1"
max="65535"
maxlength="5"
/>
@@ -121,11 +123,14 @@
/>
</hbox>
<hbox id="torPreferences-connection-hboxAllowedPorts" align="center">
+ <!-- NOTE: The pattern allows comma-separated strictly positive
+ - integers. In particular "0" is not allowed. -->
<html:input
id="torPreferences-connection-textboxAllowedPorts"
type="text"
+ required="required"
+ pattern="^(\s*,\s*)*[1-9][0-9]*((\s*,\s*)|([1-9][0-9]*))*$"
class="torMarginFix"
- value="80,443"
data-l10n-id="tor-advanced-dialog-firewall-ports-input"
/>
</hbox>
=====================================
browser/modules/TorSettingsNotification.sys.mjs
=====================================
@@ -0,0 +1,167 @@
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
+ TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
+ TorSettingsTopics: "resource://gre/modules/TorSettings.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "NotificationStrings", function () {
+ return new Localization(["toolkit/global/tor-browser.ftl"]);
+});
+
+/**
+ * Shows a notification whenever we get an ApplyError.
+ */
+export const TorSettingsNotification = {
+ /**
+ * Whether we have already been initialised.
+ *
+ * @type {boolean}
+ */
+ _initialized: false,
+
+ /**
+ * Called when the UI is ready to show a notification.
+ */
+ ready() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ Services.obs.addObserver(this, lazy.TorSettingsTopics.ApplyError);
+
+ // Show the notification for each group of settings if they have an error
+ // that was triggered prior to `ready` being called.
+ this.showNotification("bridges");
+ this.showNotification("proxy");
+ this.showNotification("firewall");
+ },
+
+ observe(subject, topic) {
+ if (topic === lazy.TorSettingsTopics.ApplyError) {
+ this.showNotification(subject.wrappedJSObject.group);
+ }
+ },
+
+ /**
+ * A promise for the `showNotification` method to ensure we only show one
+ * notification at a time.
+ *
+ * @type {?Promise}
+ */
+ _notificationPromise: null,
+
+ /**
+ * Show a notification for the given group of settings if `TorSettings` has an
+ * error for them.
+ *
+ * @param {string} group - The settings group to show the notification for.
+ */
+ async showNotification(group) {
+ const prevNotificationPromise = this._notificationPromise;
+ let notificationComplete;
+ ({ promise: this._notificationPromise, resolve: notificationComplete } =
+ Promise.withResolvers());
+ // Only want to show one notification at a time, so queue behind the
+ // previous one.
+ await prevNotificationPromise;
+
+ // NOTE: We only show the notification for a single `group` at a time, even
+ // when TorSettings has errors for multiple groups. This keeps the strings
+ // simple and means we can show different buttons depending on `canUndo` for
+ // each group individually.
+ // If we do have multiple errors the notification for each group will simply
+ // queue behind each other.
+ try {
+ // Grab the latest error value, which may have changed since
+ // showNotification was first called.
+ const error = lazy.TorSettings.getApplyError(group);
+ if (!error) {
+ // No current error for this group.
+ return;
+ }
+
+ const { canUndo } = error;
+
+ let titleId;
+ let introId;
+ switch (group) {
+ case "bridges":
+ titleId = "tor-settings-failed-notification-title-bridges";
+ introId = "tor-settings-failed-notification-cause-bridges";
+ break;
+ case "proxy":
+ titleId = "tor-settings-failed-notification-title-proxy";
+ introId = "tor-settings-failed-notification-cause-proxy";
+ break;
+ case "firewall":
+ titleId = "tor-settings-failed-notification-title-firewall";
+ introId = "tor-settings-failed-notification-cause-firewall";
+ break;
+ }
+
+ const [
+ titleText,
+ introText,
+ bodyText,
+ primaryButtonText,
+ secondaryButtonText,
+ ] = await lazy.NotificationStrings.formatValues([
+ { id: titleId },
+ { id: introId },
+ {
+ id: canUndo
+ ? "tor-settings-failed-notification-body-undo"
+ : "tor-settings-failed-notification-body-default",
+ },
+ {
+ id: canUndo
+ ? "tor-settings-failed-notification-button-undo"
+ : "tor-settings-failed-notification-button-clear",
+ },
+ { id: "tor-settings-failed-notification-button-fix-myself" },
+ ]);
+
+ const propBag = await Services.prompt.asyncConfirmEx(
+ lazy.BrowserWindowTracker.getTopWindow()?.browsingContext ?? null,
+ Services.prompt.MODAL_TYPE_INTERNAL_WINDOW,
+ titleText,
+ // Concatenate the intro text and the body text. Really these should be
+ // separate paragraph elements, but the prompt service does not support
+ // this. We split them with a double newline, which will hopefully avoid
+ // the usual problems with concatenating localised strings.
+ `${introText}\n\n${bodyText}`,
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1,
+ primaryButtonText,
+ secondaryButtonText,
+ null,
+ null,
+ null,
+ {}
+ );
+
+ const buttonNum = propBag.get("buttonNumClicked");
+
+ if (buttonNum === 0) {
+ if (canUndo) {
+ // Wait for these methods in case they resolve the error for a pending
+ // showNotification call.
+ await lazy.TorSettings.undoFailedSettings(group);
+ } else {
+ await lazy.TorSettings.clearFailedSettings(group);
+ }
+ } else if (buttonNum === 1) {
+ let win = lazy.BrowserWindowTracker.getTopWindow();
+ if (!win) {
+ win = await lazy.BrowserWindowTracker.promiseOpenWindow();
+ }
+ // Open the preferences or switch to its tab and highlight the Tor log.
+ win.openPreferences("connection-viewlogs");
+ }
+ } finally {
+ notificationComplete();
+ }
+ },
+};
=====================================
browser/modules/moz.build
=====================================
@@ -129,6 +129,7 @@ EXTRA_JS_MODULES += [
"SelectionChangedMenulist.sys.mjs",
"SiteDataManager.sys.mjs",
"SitePermissions.sys.mjs",
+ "TorSettingsNotification.sys.mjs",
"TorUIUtils.sys.mjs",
"TransientPrefs.sys.mjs",
"URILoadingHelper.sys.mjs",
=====================================
toolkit/components/tor-launcher/TorProvider.sys.mjs
=====================================
@@ -210,7 +210,7 @@ export class TorProvider {
if (this.ownsTorDaemon) {
try {
await lazy.TorSettings.initializedPromise;
- await this.writeSettings();
+ await lazy.TorSettings.setTorProvider(this);
} catch (e) {
logger.warn(
"Failed to initialize TorSettings or to write our initial settings. Continuing the initialization anyway.",
@@ -252,44 +252,65 @@ export class TorProvider {
/**
* Send settings to the tor daemon.
*
- * This should only be called internally or by the TorSettings module.
+ * @param {Map<string, ?string>} torSettings - The key value pairs to pass in.
*/
- async writeSettings() {
- // Fetch the current settings.
- // We set the useTemporary parameter since we want to apply temporary
- // bridges if they are available.
- const settings = lazy.TorSettings.getSettings(true);
- logger.debug("TorProvider.writeSettings", settings);
+ async #writeSettings(torSettings) {
+ logger.debug("Mapped settings object", torSettings);
+
+ // NOTE: The order in which TorProvider.#writeSettings should match the
+ // order in which the configuration is passed onto setConf. In turn,
+ // TorControlPort.setConf should similarly ensure that the configuration
+ // reaches the tor process in the same order.
+ // In particular, we do not want a race where an earlier call to
+ // TorProvider.#writeSettings for overlapping settings can be delayed and
+ // override a later call.
+ await this.#controller.setConf(Array.from(torSettings));
+ }
+
+ /**
+ * Send bridge settings to the tor daemon.
+ *
+ * This should only be called by the `TorSettings` module.
+ *
+ * @param {TorBridgeSettings} bridges - The bridge settings to apply.
+ */
+ async writeBridgeSettings(bridges) {
+ logger.debug("TorProvider.writeBridgeSettings", bridges);
const torSettings = new Map();
// Bridges
- const haveBridges =
- settings.bridges?.enabled && !!settings.bridges.bridge_strings.length;
+ const haveBridges = bridges?.enabled && !!bridges.bridge_strings.length;
torSettings.set(TorConfigKeys.useBridges, haveBridges);
- if (haveBridges) {
- torSettings.set(
- TorConfigKeys.bridgeList,
- settings.bridges.bridge_strings
- );
- } else {
- torSettings.set(TorConfigKeys.bridgeList, null);
- }
+ torSettings.set(
+ TorConfigKeys.bridgeList,
+ haveBridges ? bridges.bridge_strings : null
+ );
+
+ await this.#writeSettings(torSettings);
+ }
+
+ /**
+ * Send proxy settings to the tor daemon.
+ *
+ * This should only be called by the `TorSettings` module.
+ *
+ * @param {TorProxySettings} proxy - The proxy settings to apply.
+ */
+ async writeProxySettings(proxy) {
+ logger.debug("TorProvider.writeProxySettings", proxy);
+ const torSettings = new Map();
- // Proxy
torSettings.set(TorConfigKeys.socks4Proxy, null);
torSettings.set(TorConfigKeys.socks5Proxy, null);
torSettings.set(TorConfigKeys.socks5ProxyUsername, null);
torSettings.set(TorConfigKeys.socks5ProxyPassword, null);
torSettings.set(TorConfigKeys.httpsProxy, null);
torSettings.set(TorConfigKeys.httpsProxyAuthenticator, null);
- if (settings.proxy && !settings.proxy.enabled) {
- settings.proxy.type = null;
- }
- const address = settings.proxy?.address;
- const port = settings.proxy?.port;
- const username = settings.proxy?.username;
- const password = settings.proxy?.password;
- switch (settings.proxy?.type) {
+
+ const type = proxy.enabled ? proxy.type : null;
+ const { address, port, username, password } = proxy;
+
+ switch (type) {
case lazy.TorProxyType.Socks4:
torSettings.set(TorConfigKeys.socks4Proxy, `${address}:${port}`);
break;
@@ -307,27 +328,28 @@ export class TorProvider {
break;
}
- // Firewall
- if (settings.firewall?.enabled) {
- const reachableAddresses = settings.firewall.allowed_ports
- .map(port => `*:${port}`)
- .join(",");
- torSettings.set(TorConfigKeys.reachableAddresses, reachableAddresses);
- } else {
- torSettings.set(TorConfigKeys.reachableAddresses, null);
- }
+ await this.#writeSettings(torSettings);
+ }
- logger.debug("Mapped settings object", settings, torSettings);
+ /**
+ * Send firewall settings to the tor daemon.
+ *
+ * This should only be called by the `TorSettings` module.
+ *
+ * @param {TorFirewallSettings} firewall - The firewall settings to apply.
+ */
+ async writeFirewallSettings(firewall) {
+ logger.debug("TorProvider.writeFirewallSettings", firewall);
+ const torSettings = new Map();
- // Send settings to the tor process.
- // NOTE: Since everything up to this point has been non-async, the order in
- // which TorProvider.writeSettings is called should match the order in which
- // the configuration is passed onto setConf. In turn, TorControlPort.setConf
- // should similarly ensure that the configuration reaches the tor process in
- // the same order.
- // In particular, we do not want a race where an earlier call to
- // TorProvider.writeSettings can be delayed and override a later call.
- await this.#controller.setConf(Array.from(torSettings));
+ torSettings.set(
+ TorConfigKeys.reachableAddresses,
+ firewall.enabled
+ ? firewall.allowed_ports.map(port => `*:${port}`).join(",")
+ : null
+ );
+
+ await this.#writeSettings(torSettings);
}
async flushSettings() {
=====================================
toolkit/locales/en-US/toolkit/global/tor-browser.ftl
=====================================
@@ -467,6 +467,29 @@ tor-advanced-dialog-firewall-ports-input-label = Allowed ports
tor-advanced-dialog-firewall-ports-input =
.placeholder = Comma-separated values
+## Tor settings error notification.
+
+# Shown when the user's Tor bridge settings could not be passed on to the Tor daemon.
+tor-settings-failed-notification-title-bridges = Your Tor bridge settings could not be applied
+# Shown when the user's Tor bridge settings could not be passed on to the Tor daemon.
+tor-settings-failed-notification-cause-bridges = This could be due to an invalid bridge address.
+# Shown when the user's Tor proxy settings could not be passed on to the Tor daemon.
+tor-settings-failed-notification-title-proxy = Your Tor proxy settings could not be applied
+# Shown when the user's Tor proxy settings could not be passed on to the Tor daemon.
+tor-settings-failed-notification-cause-proxy = This could be due to invalid proxy information.
+# Shown when the user's Tor firewall settings could not be passed on to the Tor daemon.
+tor-settings-failed-notification-title-firewall = Your Tor firewall settings could not be applied
+# Shown when the user's Tor firewall settings could not be passed on to the Tor daemon.
+tor-settings-failed-notification-cause-firewall = This could be due to invalid firewall information.
+tor-settings-failed-notification-body-undo = Until fixed, your Tor connection will continue to use your previous settings. You can either undo the latest changes to restore the previous working settings or check the Tor log to find and fix the issue yourself.
+tor-settings-failed-notification-body-default = Until fixed, your Tor connection will continue to use default settings. You can either clear the problematic settings to restore them to default or check the Tor log to find and fix the issue yourself.
+# Button to revert the latest user settings.
+tor-settings-failed-notification-button-undo = Undo changes
+# Button to clear the user settings.
+tor-settings-failed-notification-button-clear = Clear
+# Button for the user to declare that they will fix the problematic settings by themself.
+tor-settings-failed-notification-button-fix-myself = Fix myself
+
## About Tor Browser dialog.
# '<label data-l10n-name="project-link">' and '</label>' should wrap the link text for the Tor Project, and will link to the Tor Project web page.
=====================================
toolkit/modules/TorSettings.sys.mjs
=====================================
@@ -9,7 +9,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
Lox: "resource://gre/modules/Lox.sys.mjs",
LoxTopics: "resource://gre/modules/Lox.sys.mjs",
TorParsers: "resource://gre/modules/TorParsers.sys.mjs",
- TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "logger", () => {
@@ -23,6 +22,7 @@ ChromeUtils.defineLazyGetter(lazy, "logger", () => {
export const TorSettingsTopics = Object.freeze({
Ready: "torsettings:ready",
SettingsChanged: "torsettings:settings-changed",
+ ApplyError: "torsettings:apply-error",
});
/* Prefs used to store settings in TorBrowser prefs */
@@ -162,44 +162,74 @@ function arrayShuffle(array) {
return array;
}
+/**
+ * @typedef {Object} TorBridgeSettings
+ *
+ * Represents the Tor bridge settings.
+ *
+ * @property {boolean} enabled - Whether bridges are enabled.
+ * @property {integer} source - The source of bridges. One of the values in
+ * `TorBridgeSource`.
+ * @property {string} lox_id - The ID of the lox credentials to get bridges for.
+ * Or "" if not using a `Lox` `source`.
+ * @property {string} builtin_type - The name of the built-in bridge type. Or ""
+ * if not using a `BuiltIn` `source`.
+ * @property {string[]} bridge_strings - The bridge lines to be passed to the
+ * provider. Should be empty if and only if the `source` is `Invalid`.
+ */
+
+/**
+ * @typedef {Object} TorProxySettings
+ *
+ * Represents the Tor proxy settings.
+ *
+ * @property {boolean} enabled - Whether the proxy should be enabled.
+ * @property {integer} type - The proxy type. One of the values in
+ * `TorProxyType`.
+ * @property {string} address - The proxy address, or "" if the proxy is not
+ * being used.
+ * @property {integer} port - The proxy port, or 0 if the proxy is not being
+ * used.
+ * @property {string} username - The proxy user name, or "" if this is not
+ * needed.
+ * @property {string} password - The proxy password, or "" if this is not
+ * needed.
+ */
+
+/**
+ * @typedef {Object} TorFirewallSettings
+ *
+ * Represents the Tor firewall settings.
+ *
+ * @property {boolean} enabled - Whether the firewall settings should be
+ * enabled.
+ * @property {integer[]} allowed_ports - The list of ports that are allowed.
+ */
+
+/**
+ * @typedef {Object} TorCombinedSettings
+ *
+ * A combination of Tor settings.
+ *
+ * @property {TorBridgeSettings} bridges - The bridge settings.
+ * @property {TorProxySettings} proxy - The proxy settings.
+ * @property {TorFirewallSettings} firewall - The firewall settings.
+ */
+
/* TorSettings module */
class TorSettingsImpl {
/**
- * The underlying settings values.
+ * The default settings to use.
*
- * @type {object}
+ * @type {TorCombinedSettings}
*/
- #settings = {
+ #defaultSettings = {
bridges: {
- /**
- * Whether the bridges are enabled or not.
- *
- * @type {boolean}
- */
enabled: false,
source: TorBridgeSource.Invalid,
- /**
- * The lox id is used with the Lox "source", and remains set with the
- * stored value when other sources are used.
- *
- * @type {string}
- */
lox_id: "",
- /**
- * The built-in type to use when using the BuiltIn "source", or empty when
- * using any other source.
- *
- * @type {string}
- */
builtin_type: "",
- /**
- * The current bridge strings.
- *
- * Can only be non-empty if the "source" is not Invalid.
- *
- * @type {Array<string>}
- */
bridge_strings: [],
},
proxy: {
@@ -216,10 +246,71 @@ class TorSettingsImpl {
},
};
+ /**
+ * The underlying settings values.
+ *
+ * @type {TorCombinedSettings}
+ */
+ #settings = structuredClone(this.#defaultSettings);
+
+ /**
+ * The last successfully applied settings for the current `TorProvider`, if
+ * any.
+ *
+ * NOTE: Should only be written to within `#applySettings`.
+ *
+ * @type {{
+ * bridges: ?TorBridgeSettings,
+ * proxy: ?TorProxySettings,
+ * firewall: ?TorFirewallSettings
+ * }}
+ */
+ #successfulSettings = { bridges: null, proxy: null, firewall: null };
+
+ /**
+ * Whether temporary bridge settings have been applied to the current
+ * `TorProvider`.
+ *
+ * @type {boolean}
+ */
+ #temporaryBridgesApplied = false;
+
+ /**
+ * @typedef {TorSettingsApplyError}
+ *
+ * @property {boolean} canUndo - Whether the latest error can be "undone".
+ * When this is `false`, the TorProvider will be using its default values
+ * instead.
+ */
+
+ /**
+ * A summary of the latest failures to apply our settings, if any.
+ *
+ * NOTE: Should only be written to within `#applySettings`.
+ *
+ * @type {{
+ * bridges: ?TorSettingsApplyError,
+ * proxy: ?TorSettingsApplyError,
+ * firewall: ?TorSettingsApplyError,
+ * }}
+ */
+ #applyErrors = { bridges: null, proxy: null, firewall: null };
+
+ /**
+ * Get the latest failure for the given setting, if any.
+ *
+ * @param {string} group - The settings to get the error details for.
+ *
+ * @returns {?TorSettingsApplyError} - The error details, if any.
+ */
+ getApplyError(group) {
+ return structuredClone(this.#applyErrors[group]);
+ }
+
/**
* Temporary bridge settings to apply instead of #settings.bridges.
*
- * @type {?Object}
+ * @type {?TorBridgeSettings}
*/
#temporaryBridgeSettings = null;
@@ -342,38 +433,39 @@ class TorSettingsImpl {
}
/**
- * Regular expression for a decimal non-negative integer.
+ * Verify a port number is within bounds.
*
- * @type {RegExp}
+ * @param {integer} val - The value to verify.
+ * @return {boolean} - Whether the port is within range.
*/
- #portRegex = /^[0-9]+$/;
+ validPort(val) {
+ return Number.isInteger(val) && val >= 1 && val <= 65535;
+ }
+
/**
- * Parse a string as a port number.
+ * Verify that some SOCKS5 credentials are valid.
*
- * @param {string|integer} val - The value to parse.
- * @param {boolean} trim - Whether a string value can be stripped of
- * whitespace before parsing.
- *
- * @return {integer?} - The port number, or null if the given value was not
- * valid.
+ * @param {string} username - The SOCKS5 username.
+ * @param {string} password - The SOCKS5 password.
+ * @return {boolean} - Whether the credentials are valid.
*/
- #parsePort(val, trim) {
- if (typeof val === "string") {
- if (trim) {
- val = val.trim();
+ validSocks5Credentials(username, password) {
+ if (!username && !password) {
+ // Both empty is valid.
+ return true;
+ }
+ for (const val of [username, password]) {
+ if (typeof val !== "string") {
+ return false;
}
- // ensure port string is a valid positive integer
- if (this.#portRegex.test(val)) {
- val = Number.parseInt(val, 10);
- } else {
- throw new Error(`Invalid port string "${val}"`);
+ const byteLen = new TextEncoder().encode(val).length;
+ if (byteLen < 1 || byteLen > 255) {
+ return false;
}
}
- if (!Number.isInteger(val) || val < 1 || val > 65535) {
- throw new Error(`Port out of range: ${val}`);
- }
- return val;
+ return true;
}
+
/**
* Test whether two arrays have equal members and order.
*
@@ -659,10 +751,11 @@ class TorSettingsImpl {
false
);
if (firewall.enabled) {
- firewall.allowed_ports = Services.prefs.getStringPref(
- TorSettingsPrefs.firewall.allowed_ports,
- ""
- );
+ firewall.allowed_ports = Services.prefs
+ .getStringPref(TorSettingsPrefs.firewall.allowed_ports, "")
+ .split(",")
+ .filter(p => p.trim())
+ .map(p => parseInt(p, 10));
}
try {
this.#fixupFirewallSettings(firewall);
@@ -767,19 +860,221 @@ class TorSettingsImpl {
}
}
+ /**
+ * A blocker promise for the #applySettings method.
+ *
+ * Ensures only one active caller to protect the #applyErrors and
+ * #successfulSettings properties.
+ *
+ * @type {?Promise}
+ */
+ #applySettingsTask = null;
+
/**
* Push our settings down to the tor provider.
*
* Even though this introduces a circular depdency, it makes the API nicer for
* frontend consumers.
*
- * @param {boolean} flush - Whether to also flush the settings to disk.
+ * @param {Object} apply - The list of settings to apply.
+ * @param {boolean} [apply.bridges] - Whether to apply our bridge settings.
+ * @param {boolean} [apply.proxy] - Whether to apply our proxy settings.
+ * @param {boolean} [apply.firewall] - Whether to apply our firewall settings.
+ * @param {boolean} [details] - Optional details.
+ * @param {boolean} [details.useTemporaryBridges] - Whether the caller wants
+ * to apply temporary bridges.
+ * @param {boolean} [details.newProvider] - Whether the caller is initialising
+ * a new `TorProvider`.
*/
- async #applySettings(flush) {
- const provider = await lazy.TorProviderBuilder.build();
- await provider.writeSettings();
- if (flush) {
- provider.flushSettings();
+ async #applySettings(apply, details) {
+ // Grab this provider before awaiting.
+ // In particular, if the provider is changed we do not want to switch to
+ // writing to the new instance.
+ const providerRef = this.#providerRef;
+ const provider = providerRef?.deref();
+ if (!provider) {
+ // Wait until setTorProvider is called.
+ lazy.logger.info("No TorProvider yet");
+ return;
+ }
+
+ // We only want one instance of #applySettings to be running at any given
+ // time, so we await the previous call first.
+ // Read and replace #applySettingsTask before we do any async operations.
+ // I.e. this is effectively an atomic read and replace.
+ const prevTask = this.#applySettingsTask;
+ let taskComplete;
+ ({ promise: this.#applySettingsTask, resolve: taskComplete } =
+ Promise.withResolvers());
+ await prevTask;
+
+ try {
+ let flush = false;
+ const errors = [];
+
+ // Test whether the provider is no longer running or has been replaced.
+ const providerRunning = () => {
+ return providerRef === this.#providerRef && provider.isRunning;
+ };
+
+ lazy.logger.debug("Passing on settings to the provider", apply, details);
+
+ if (details?.newProvider) {
+ // If we have a new provider we clear our successful settings.
+ // In particular, the user may have changed their settings several times
+ // whilst the tor process was not running. In the event of an
+ // "ApplyError", we want to correctly show to the user that they are now
+ // using default settings and we do not want to allow them to "undo"
+ // since the previous successful settings may be out of date.
+ // NOTE: We do not do this within `setTorProvider` since some other
+ // caller's `#applySettingsTask` may still be running and writing to
+ // these values when `setTorProvider` is called.
+ this.#successfulSettings.bridges = null;
+ this.#successfulSettings.proxy = null;
+ this.#successfulSettings.firewall = null;
+ this.#applyErrors.bridges = null;
+ this.#applyErrors.proxy = null;
+ this.#applyErrors.firewall = null;
+ // Temporary bridges are not applied to the new provider.
+ this.#temporaryBridgesApplied = false;
+ }
+
+ for (const group of ["bridges", "proxy", "firewall"]) {
+ if (!apply[group]) {
+ continue;
+ }
+
+ if (!providerRunning()) {
+ lazy.logger.info("The TorProvider is no longer running");
+ // Bail on this task since the old provider should not accept
+ // settings. setTorProvider will be called for the new provider and
+ // will already handle applying the same settings.
+ return;
+ }
+
+ let usingSettings = true;
+ if (group === "bridges") {
+ // Only record successes or failures when using the user settings.
+ if (this.#temporaryBridgeSettings && !details?.useTemporaryBridges) {
+ // #temporaryBridgeSettings were written whilst awaiting the
+ // previous task. Do nothing and allow applyTemporarySettings to
+ // apply the temporary bridges instead.
+ lazy.logger.info(
+ "Not apply bridges since temporary bridges were applied"
+ );
+ continue;
+ }
+ if (!this.#temporaryBridgeSettings && details?.useTemporaryBridges) {
+ // #temporaryBridgeSettings were cleared whilst awaiting the
+ // previous task. Do nothing and allow changeSettings or
+ // clearTemporaryBridges to apply the non-temporary bridges
+ // instead.
+ lazy.logger.info(
+ "Not apply temporary bridges since they were cleared"
+ );
+ continue;
+ }
+ usingSettings = !details?.useTemporaryBridges;
+ }
+
+ try {
+ switch (group) {
+ case "bridges": {
+ const bridges = structuredClone(
+ usingSettings
+ ? this.#settings.bridges
+ : this.#temporaryBridgeSettings
+ );
+
+ try {
+ await provider.writeBridgeSettings(bridges);
+ } catch (e) {
+ if (
+ usingSettings &&
+ this.#temporaryBridgesApplied &&
+ providerRunning()
+ ) {
+ lazy.logger.warn(
+ "Recovering to clear temporary bridges from the provider"
+ );
+ // The TorProvider is still using the temporary bridges. As a
+ // priority we want to try and restore the TorProvider to the
+ // state it was in prior to the temporary bridges being
+ // applied.
+ const prevBridges = structuredClone(
+ this.#successfulSettings.bridges ??
+ this.#defaultSettings.bridges
+ );
+ try {
+ await provider.writeBridgeSettings(prevBridges);
+ this.#temporaryBridgesApplied = false;
+ } catch (e) {
+ lazy.logger.error(
+ "Failed to clear the temporary bridges from the provider",
+ e
+ );
+ }
+ }
+ throw e;
+ }
+
+ if (usingSettings) {
+ this.#successfulSettings.bridges = bridges;
+ this.#temporaryBridgesApplied = false;
+ this.#applyErrors.bridges = null;
+ flush = true;
+ } else {
+ this.#temporaryBridgesApplied = true;
+ // Do not flush the temporary bridge settings until they are
+ // saved.
+ }
+ break;
+ }
+ case "proxy": {
+ const proxy = structuredClone(this.#settings.proxy);
+ await provider.writeProxySettings(proxy);
+ this.#successfulSettings.proxy = proxy;
+ this.#applyErrors.proxy = null;
+ flush = true;
+ break;
+ }
+ case "firewall": {
+ const firewall = structuredClone(this.#settings.firewall);
+ await provider.writeFirewallSettings(firewall);
+ this.#successfulSettings.firewall = firewall;
+ this.#applyErrors.firewall = null;
+ flush = true;
+ break;
+ }
+ }
+ } catch (e) {
+ // Store the error and throw later.
+ errors.push(e);
+ if (usingSettings && providerRunning()) {
+ // Record and signal the error.
+ // NOTE: We do not signal ApplyError when we fail to apply temporary
+ // bridges.
+ this.#applyErrors[group] = {
+ canUndo: Boolean(this.#successfulSettings[group]),
+ };
+ lazy.logger.debug(`Signalling new ApplyError for ${group}`);
+ Services.obs.notifyObservers(
+ { group },
+ TorSettingsTopics.ApplyError
+ );
+ }
+ }
+ }
+ if (flush && providerRunning()) {
+ provider.flushSettings();
+ }
+ if (errors.length) {
+ lazy.logger.error("Failed to apply settings", errors);
+ throw new Error(`Failed to apply settings. ${errors.join(". ")}.`);
+ }
+ } finally {
+ // Allow the next caller to proceed.
+ taskComplete();
}
}
@@ -869,7 +1164,30 @@ class TorSettingsImpl {
if (!Object.values(TorProxyType).includes(proxy.type)) {
throw new Error(`Invalid proxy type: ${proxy.type}`);
}
- proxy.port = this.#parsePort(proxy.port, false);
+ // Do not allow port value to be 0.
+ // Whilst Socks4Proxy, Socks5Proxy and HTTPSProxyPort allow you to pass in
+ // `<address>:0` this will select a default port. Our UI does not indicate
+ // that `0` maps to a different value, so we disallow it.
+ if (!this.validPort(proxy.port)) {
+ throw new Error(`Invalid proxy port: ${proxy.port}`);
+ }
+
+ switch (proxy.type) {
+ case TorProxyType.Socks4:
+ // Never use the username or password.
+ proxy.username = "";
+ proxy.password = "";
+ break;
+ case TorProxyType.Socks5:
+ if (!this.validSocks5Credentials(proxy.username, proxy.password)) {
+ throw new Error("Invalid SOCKS5 credentials");
+ }
+ break;
+ case TorProxyType.HTTPS:
+ // username and password are both optional.
+ break;
+ }
+
proxy.address = String(proxy.address);
proxy.username = String(proxy.username);
proxy.password = String(proxy.password);
@@ -890,22 +1208,82 @@ class TorSettingsImpl {
return;
}
- let allowed_ports = firewall.allowed_ports;
- if (!Array.isArray(allowed_ports)) {
- allowed_ports = allowed_ports === "" ? [] : allowed_ports.split(",");
+ if (!Array.isArray(firewall.allowed_ports)) {
+ throw new Error("allowed_ports should be an array of ports");
}
- // parse and remove duplicates
- const portSet = new Set();
-
- for (const port of allowed_ports) {
- try {
- portSet.add(this.#parsePort(port, true));
- } catch (e) {
- // Do not throw for individual ports.
- lazy.logger.error(`Failed to parse the port ${port}. Ignoring.`, e);
+ for (const port of firewall.allowed_ports) {
+ if (!this.validPort(port)) {
+ throw new Error(`Invalid firewall port: ${port}`);
}
}
- firewall.allowed_ports = [...portSet];
+ // Remove duplicates
+ firewall.allowed_ports = [...new Set(firewall.allowed_ports)];
+ }
+
+ /**
+ * The current `TorProvider` instance we are using, if any.
+ *
+ * @type {?WeakRef<TorProvider>}
+ */
+ #providerRef = null;
+
+ /**
+ * Called whenever we have a new provider to send settings to.
+ *
+ * @param {TorProvider} provider - The provider to apply our settings to.
+ */
+ async setTorProvider(provider) {
+ lazy.logger.debug("Applying settings to new provider");
+ this.#checkIfInitialized();
+
+ // Use a WeakRef to not keep the TorProvider instance alive.
+ this.#providerRef = new WeakRef(provider);
+ // NOTE: We need the caller to pass in the TorProvider instance because
+ // TorProvider's initialisation waits for this method. In particular, we
+ // cannot await TorProviderBuilder.build since it would hang!
+ await this.#applySettings(
+ { bridges: true, proxy: true, firewall: true },
+ { newProvider: true }
+ );
+ }
+
+ /**
+ * Undo settings that have failed to be applied by restoring the last
+ * successfully applied settings instead.
+ *
+ * @param {string} group - The group to undo the settings for.
+ */
+ async undoFailedSettings(group) {
+ if (!this.#applyErrors[group]) {
+ lazy.logger.warn(
+ `${group} settings have already been successfully replaced.`
+ );
+ return;
+ }
+ if (!this.#successfulSettings[group]) {
+ // Unexpected.
+ lazy.logger.warn(
+ `Cannot undo ${group} settings since we have no successful settings.`
+ );
+ return;
+ }
+ await this.changeSettings({ [group]: this.#successfulSettings[group] });
+ }
+
+ /**
+ * Clear settings that have failed to be applied by using the default settings
+ * instead.
+ *
+ * @param {string} group - The group to clear the settings for.
+ */
+ async clearFailedSettings(group) {
+ if (!this.#applyErrors[group]) {
+ lazy.logger.warn(
+ `${group} settings have already been successfully replaced.`
+ );
+ return;
+ }
+ await this.changeSettings({ [group]: this.#defaultSettings[group] });
}
/**
@@ -919,8 +1297,8 @@ class TorSettingsImpl {
* + proxy settings can be set as a group.
* + firewall settings can be set a group.
*
- * @param {object} newValues - The new setting values, a subset of the
- * complete settings that should be changed.
+ * @param {object} newValues - The new setting values that should be changed.
+ * A subset of the `TorCombinedSettings` object.
*/
async changeSettings(newValues) {
lazy.logger.debug("changeSettings()", newValues);
@@ -932,6 +1310,7 @@ class TorSettingsImpl {
const completeSettings = structuredClone(this.#settings);
const changes = [];
+ const apply = {};
/**
* Change the given setting to a new value. Does nothing if the new value
@@ -950,10 +1329,11 @@ class TorSettingsImpl {
}
completeSettings[group][prop] = value;
changes.push(`${group}.${prop}`);
+ // Apply these settings.
+ apply[group] = true;
};
if ("bridges" in newValues) {
- const changesLength = changes.length;
if ("source" in newValues.bridges) {
this.#fixupBridgeSettings(newValues.bridges);
changeSetting("bridges", "source", newValues.bridges.source);
@@ -984,7 +1364,7 @@ class TorSettingsImpl {
changeSetting("bridges", "enabled", newValues.bridges.enabled);
}
- if (this.#temporaryBridgeSettings && changes.length !== changesLength) {
+ if (this.#temporaryBridgeSettings && apply.bridges) {
// A change in the bridges settings.
// We want to clear the temporary bridge settings to ensure that they
// cannot be used to overwrite these user-provided settings.
@@ -1034,34 +1414,22 @@ class TorSettingsImpl {
lazy.logger.debug("setSettings result", this.#settings, changes);
- // After we have sent out the notifications for the changed settings and
- // saved the preferences we send the new settings to TorProvider.
- // Some properties are unread by TorProvider. So if only these values change
- // there is no need to re-apply the settings.
- const unreadProps = ["bridges.builtin_type", "bridges.lox_id"];
- const shouldApply = changes.some(prop => !unreadProps.includes(prop));
- if (shouldApply) {
- await this.#applySettings(true);
+ if (apply.bridges || apply.proxy || apply.firewall) {
+ // After we have sent out the notifications for the changed settings and
+ // saved the preferences we send the new settings to TorProvider.
+ await this.#applySettings(apply);
}
}
/**
* Get a copy of all our settings.
*
- * @param {boolean} [useTemporary=false] - Whether the returned settings
- * should use the temporary bridge settings, if any, instead.
- *
- * @returns {object} A copy of the settings object
+ * @returns {TorCombinedSettings} A copy of the current settings.
*/
- getSettings(useTemporary = false) {
+ getSettings() {
lazy.logger.debug("getSettings()");
this.#checkIfInitialized();
- const settings = structuredClone(this.#settings);
- if (useTemporary && this.#temporaryBridgeSettings) {
- // Override the bridge settings with our temporary ones.
- settings.bridges = structuredClone(this.#temporaryBridgeSettings);
- }
- return settings;
+ return structuredClone(this.#settings);
}
/**
@@ -1109,8 +1477,8 @@ class TorSettingsImpl {
// After checks are complete, we commit them.
this.#temporaryBridgeSettings = bridgeSettings;
- // Do not flush the temporary bridge settings until they are saved.
- await this.#applySettings(false);
+
+ await this.#applySettings({ bridges: true }, { useTemporaryBridges: true });
}
/**
@@ -1137,7 +1505,7 @@ class TorSettingsImpl {
return;
}
this.#temporaryBridgeSettings = null;
- await this.#applySettings();
+ await this.#applySettings({ bridges: true });
}
}
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/0f529e…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/0f529e…
You're receiving this email because of your account on gitlab.torproject.org.
henry pushed to branch tor-browser-128.8.0esr-14.5-1 at The Tor Project / Applications / Tor Browser
Commits:
480957e5 by Henry Wilkes at 2025-03-18T15:21:16+00:00
fixup! TB 40933: Add tor-launcher functionality
TB 42300: Store TorProvider log messages in TorProviderBuilder to be
used between instances and to be available whilst a provider is not
available.
- - - - -
0f529e6f by Henry Wilkes at 2025-03-18T15:21:16+00:00
fixup! TB 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
TB 42300: Fetch tor logs from TorProviderBuilder.
- - - - -
3 changed files:
- browser/components/torpreferences/content/torLogDialog.js
- toolkit/components/tor-launcher/TorProvider.sys.mjs
- toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
Changes:
=====================================
browser/components/torpreferences/content/torLogDialog.js
=====================================
@@ -66,28 +66,22 @@ const gTorLogDialog = {
);
});
- // A waiting state should not be needed at this point.
- // Also, we probably cannot even arrive here if the provider failed to
- // initialize, otherwise we could use a try/catch, and write the exception
- // text in the logs, instead.
- TorProviderBuilder.build().then(provider => {
- Services.obs.addObserver(this, TorProviderTopics.TorLog);
- window.addEventListener(
- "unload",
- () => {
- Services.obs.removeObserver(this, TorProviderTopics.TorLog);
- },
- { once: true }
- );
-
- for (const logEntry of provider.getLog()) {
- this.addLogEntry(logEntry, true);
- }
- // Set the initial scroll to the bottom.
- this._logTable.scrollTo({
- top: this._logTable.scrollTopMax,
- behaviour: "instant",
- });
+ Services.obs.addObserver(this, TorProviderTopics.TorLog);
+ window.addEventListener(
+ "unload",
+ () => {
+ Services.obs.removeObserver(this, TorProviderTopics.TorLog);
+ },
+ { once: true }
+ );
+
+ for (const logEntry of TorProviderBuilder.getLog()) {
+ this.addLogEntry(logEntry, true);
+ }
+ // Set the initial scroll to the bottom.
+ this._logTable.scrollTo({
+ top: this._logTable.scrollTopMax,
+ behaviour: "instant",
});
},
=====================================
toolkit/components/tor-launcher/TorProvider.sys.mjs
=====================================
@@ -45,12 +45,6 @@ const logger = console.createInstance({
* @property {string} [host] The host to connect for a TCP proxy
* @property {number} [port] The port number to use for a TCP proxy
*/
-/**
- * @typedef {object} LogEntry An object with a log message
- * @property {string} timestamp The local date-time stamp at which we received the message
- * @property {string} type The message level
- * @property {string} msg The message
- */
/**
* Stores the data associated with a circuit node.
*
@@ -69,7 +63,6 @@ const Preferences = Object.freeze({
ControlUseIpc: "extensions.torlauncher.control_port_use_ipc",
ControlHost: "extensions.torlauncher.control_host",
ControlPort: "extensions.torlauncher.control_port",
- MaxLogEntries: "extensions.torlauncher.max_tor_log_entries",
});
/* Config Keys used to configure tor daemon */
@@ -141,15 +134,6 @@ export class TorProvider {
*/
#socksSettings = null;
- /**
- * The logs we received over the control port.
- * We store a finite number of log entries which can be configured with
- * extensions.torlauncher.max_tor_log_entries.
- *
- * @type {LogEntry[]}
- */
- #logs = [];
-
#isBootstrapDone = false;
/**
* Keep the last warning to avoid broadcasting an async warning if it is the
@@ -511,15 +495,6 @@ export class TorProvider {
return this.#controller.onionAuthViewKeys();
}
- /**
- * Returns captured log messages.
- *
- * @returns {LogEntry[]} The logs we collected from the tor daemon so far.
- */
- getLog() {
- return structuredClone(this.#logs);
- }
-
/**
* @returns {boolean} true if we launched and control tor, false if we are
* using system tor.
@@ -1033,15 +1008,6 @@ export class TorProvider {
TorProviderTopics.TorLog
);
- const maxEntries = Services.prefs.getIntPref(
- Preferences.MaxLogEntries,
- 1000
- );
- if (maxEntries > 0 && this.#logs.length >= maxEntries) {
- this.#logs.splice(0, 1);
- }
-
- this.#logs.push({ type, msg, timestamp });
switch (type) {
case "ERR":
logger.error(`[Tor error] ${msg}`);
=====================================
toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
=====================================
@@ -23,6 +23,13 @@ export const TorProviders = Object.freeze({
tor: 1,
});
+/**
+ * @typedef {object} LogEntry An object with a log message
+ * @property {string} timestamp The local date-time stamp at which we received the message
+ * @property {string} type The message level
+ * @property {string} msg The message
+ */
+
/**
* The factory to get a Tor provider.
* Currently we support only TorProvider, i.e., the one that interacts with
@@ -36,6 +43,48 @@ export class TorProviderBuilder {
*/
static #provider = null;
+ /**
+ * A record of the log messages from all TorProvider instances.
+ *
+ * @type {LogEntry[]}
+ */
+ static #log = [];
+
+ /**
+ * Get a record of historic log entries.
+ *
+ * @returns {LogEntry[]} - The record of entries.
+ */
+ static getLog() {
+ return structuredClone(this.#log);
+ }
+
+ /**
+ * The limit on the number of log entries we should store.
+ *
+ * @type {integer}
+ */
+ static #logLimit;
+
+ /**
+ * The observer that checks for new TorLog messages.
+ *
+ * @type {Function}
+ */
+ static #logObserver;
+
+ /**
+ * Add a new log message.
+ *
+ * @param {LogEntry} logEntry - The log entry to add.
+ */
+ static #addLogEntry(logEntry) {
+ if (this.#logLimit > 0 && this.#log.length >= this.#logLimit) {
+ this.#log.splice(0, 1);
+ }
+ this.#log.push(logEntry);
+ }
+
/**
* The observer that checks when the tor process exits, and reinitializes the
* provider.
@@ -56,6 +105,15 @@ export class TorProviderBuilder {
* Initialize the provider of choice.
*/
static init() {
+ this.#logLimit = Services.prefs.getIntPref(
+ "extensions.torlauncher.max_tor_log_entries",
+ 1000
+ );
+ this.#logObserver = subject => {
+ this.#addLogEntry(subject.wrappedJSObject);
+ };
+ Services.obs.addObserver(this.#logObserver, TorProviderTopics.TorLog);
+
switch (this.providerType) {
case TorProviders.tor:
// Even though initialization of the initial TorProvider is
@@ -136,6 +194,7 @@ export class TorProviderBuilder {
);
this.#exitObserver = null;
}
+ Services.obs.removeObserver(this.#logObserver, TorProviderTopics.TorLog);
}
/**
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/ad21bd…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/ad21bd…
You're receiving this email because of your account on gitlab.torproject.org.
morgan pushed to branch main at The Tor Project / Applications / tor-browser-build
Commits:
db399f9f by Morgan at 2025-03-18T13:05:10+00:00
Tweak Tails notification in Tor Browser Stable release-prep template
- - - - -
1 changed file:
- .gitlab/issue_templates/Release Prep - Tor Browser Stable.md
Changes:
=====================================
.gitlab/issue_templates/Release Prep - Tor Browser Stable.md
=====================================
@@ -147,28 +147,6 @@ Tor Browser Stable is on the `maint-${TOR_BROWSER_MAJOR}.${TOR_BROWSER_MINOR}` b
```bash
make torbrowser-kick-devmole-build
```
-- [ ] Notify Tails of pending builds
- - **Recipients**
- ```
- tails-dev(a)boum.org
- ```
- - **Subject**
- ```
- Tor Browser ${TOR_BROWSER_VERSION} Building
- ```
- - **Body**
- ```
- Hello,
-
- Tor Browser Stable is being built and should be ready for Tails in the coming hours. Build artifacts and hashes will be available here:
- - ${BUILD_ARTIFACTS_URL}
- - ${BUIDL_HASHES_URL}
-
- ⚠️ WARNING: We have not yet verified the builds match, so make sure you do so before prepping the Tails release!
-
- Changelog:
- # paste changelog as quote here
- ```
</details>
@@ -213,6 +191,25 @@ Tor Browser Stable is on the `maint-${TOR_BROWSER_MAJOR}.${TOR_BROWSER_MINOR}` b
- morgan
- pierov
- [ ] Ensure all builders have matching builds
+- [ ] Notify Tails
+ - **Recipients**
+ ```
+ tails-dev(a)boum.org
+ ```
+ - **Subject**
+ ```
+ Tor Browser ${TOR_BROWSER_VERSION} Ready
+ ```
+ - **Body**
+ ```
+ Hello,
+
+ Tor Browser Stable is ready for Tails. Build artifacts can be found here:
+ - ${BUILD_ARTIFACTS_URL}
+
+ Changelog:
+ # paste changelog as quote here
+ ```
- [ ] Verify the associated legacy `maint-13.5` release has been signed and deployed
- **⚠️ WARNING**: Do not continue if the legacy channel has not been fully signed and published yet; it is needed for update-response generation!
- **NOTE** Stable releases without a corresponding legacy release may ignore this
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/commit/d…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/commit/d…
You're receiving this email because of your account on gitlab.torproject.org.
morgan pushed to branch tor-browser-128.8.0esr-14.5-1 at The Tor Project / Applications / Tor Browser
Commits:
36916036 by Henry Wilkes at 2025-03-17T21:14:23+00:00
fixup! TB 27476: Implement about:torconnect captive portal within Tor Browser
TB 43321: Only focus the about:torconnect buttons under certain
circumstances.
By default, when switching stages we move the focus back to the stage
heading. This is because we want to lead the user back to the top of the
page to show them the new context. This should help improve the
experience when using a screen reader.
If we are in the bootstrapping stage we instead move the focus to the
"Cancel" button since it is likely that the user wants to use this
control.
If the user presses the "Cancel" button we return the focus to the
"Connect" or "Try a bridge" button. I.e. we restore the prior focus.
This allows to user to easily re-try without having to re-read the page
they just saw.
We do a similar thing when the user cancels the automatic startup
bootstrapping.
Finally, on page load we will focus the "Connect" button if the user has
previously interacted with it. We record this interaction in a
preference that persists between sessions.
We also separate out the "Loading" stage from the "Start" stage. It is
unexpected for `about:torconnect` to be opened whilst in the "Loading"
stage, but if it does happen it would be safer to keep the page blank.
The way this is implemented also ensures that the initial page is blank
prior to "get-init-args" resolving.
- - - - -
ad21bdd6 by Henry Wilkes at 2025-03-17T21:14:23+00:00
fixup! TB 40597: Implement TorSettings module
TB 43321: Add a isQuickstart property to the TorConnect.stage.
This is used by `about:torconnect` for determining focus behaviour.
- - - - -
5 changed files:
- toolkit/components/torconnect/TorConnectParent.sys.mjs
- toolkit/components/torconnect/content/aboutTorConnect.css
- toolkit/components/torconnect/content/aboutTorConnect.html
- toolkit/components/torconnect/content/aboutTorConnect.js
- toolkit/modules/TorConnect.sys.mjs
Changes:
=====================================
toolkit/components/torconnect/TorConnectParent.sys.mjs
=====================================
@@ -13,6 +13,9 @@ ChromeUtils.defineESModuleGetters(lazy, {
HomePage: "resource:///modules/HomePage.sys.mjs",
});
+const userHasEverClickedConnectPref =
+ "torbrowser.about_torconnect.user_has_ever_clicked_connect";
+
/*
This object is basically a marshalling interface between the TorConnect module
and a particular about:torconnect page
@@ -117,6 +120,9 @@ export class TorConnectParent extends JSWindowActorParent {
TorConnect.chooseRegion();
break;
case "torconnect:begin-bootstrapping":
+ if (message.data.userClickedConnect) {
+ Services.prefs.setBoolPref(userHasEverClickedConnectPref, true);
+ }
TorConnect.beginBootstrapping(message.data.regionCode);
break;
case "torconnect:cancel-bootstrapping":
@@ -130,6 +136,10 @@ export class TorConnectParent extends JSWindowActorParent {
Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr",
CountryNames: TorConnect.countryNames,
stage: TorConnect.stage,
+ userHasEverClickedConnect: Services.prefs.getBoolPref(
+ userHasEverClickedConnectPref,
+ false
+ ),
quickstartEnabled: TorConnect.quickstart,
};
case "torconnect:get-frequent-regions":
=====================================
toolkit/components/torconnect/content/aboutTorConnect.css
=====================================
@@ -15,6 +15,11 @@ html {
height: 100%;
}
+body:not(.loaded) {
+ /* Keep blank whilst loading. */
+ display: none;
+}
+
#breadcrumbs {
display: flex;
align-items: center;
@@ -93,6 +98,11 @@ html {
display: none;
}
+#tor-connect-heading {
+ /* Do not show the focus outline. */
+ outline: none;
+}
+
#connect-to-tor {
margin-inline-start: 0;
}
=====================================
toolkit/components/torconnect/content/aboutTorConnect.html
=====================================
@@ -50,7 +50,7 @@
</div>
<div id="text-container">
<div class="title">
- <h1 class="title-text"></h1>
+ <h1 id="tor-connect-heading" class="title-text" tabindex="-1"></h1>
</div>
<div id="connectLongContent">
<p id="connectLongContentText"></p>
=====================================
toolkit/components/torconnect/content/aboutTorConnect.js
=====================================
@@ -32,7 +32,6 @@ class AboutTorConnect {
selectors = Object.freeze({
textContainer: {
title: "div.title",
- titleText: "h1.title-text",
longContentText: "#connectLongContentText",
},
progress: {
@@ -77,7 +76,7 @@ class AboutTorConnect {
elements = Object.freeze({
title: document.querySelector(this.selectors.textContainer.title),
- titleText: document.querySelector(this.selectors.textContainer.titleText),
+ heading: document.getElementById("tor-connect-heading"),
longContentText: document.querySelector(
this.selectors.textContainer.longContentText
),
@@ -138,18 +137,44 @@ class AboutTorConnect {
locations = {};
- beginBootstrapping() {
- RPMSendAsyncMessage("torconnect:begin-bootstrapping", {});
+ /**
+ * Whether the user requested a cancellation of the bootstrap from *this*
+ * page.
+ *
+ * @type {boolean}
+ */
+ userCancelled = false;
+
+ /**
+ * Start a normal bootstrap attempt.
+ *
+ * @param {boolean} userClickedConnect - Whether this request was triggered by
+ * the user clicking the "Connect" button on the "Start" page.
+ */
+ beginBootstrapping(userClickedConnect) {
+ RPMSendAsyncMessage("torconnect:begin-bootstrapping", {
+ userClickedConnect,
+ });
}
+ /**
+ * Start an auto bootstrap attempt.
+ *
+ * @param {string} regionCode - The region code to use for the bootstrap, or
+ * "automatic".
+ */
beginAutoBootstrapping(regionCode) {
RPMSendAsyncMessage("torconnect:begin-bootstrapping", {
regionCode,
});
}
+ /**
+ * Try and cancel the current bootstrap attempt.
+ */
cancelBootstrapping() {
RPMSendAsyncMessage("torconnect:cancel-bootstrapping");
+ this.userCancelled = true;
}
/*
@@ -260,7 +285,7 @@ class AboutTorConnect {
}
setTitle(title, className) {
- this.elements.titleText.textContent = title;
+ this.elements.heading.textContent = title;
this.elements.title.className = "title";
if (className) {
this.elements.title.classList.add(className);
@@ -349,18 +374,88 @@ class AboutTorConnect {
}
}
+ /**
+ * The connect button that was focused just prior to a bootstrap attempt, if
+ * any.
+ *
+ * @type {?Element}
+ */
+ preBootstrappingFocus = null;
+
+ /**
+ * The stage that was shown on this page just prior to a bootstrap attempt.
+ *
+ * @type {?string}
+ */
+ preBootstrappingStage = null;
+
/*
These methods update the UI based on the current TorConnect state
*/
- updateStage(stage) {
+ /**
+ * Update the shown stage.
+ *
+ * @param {ConnectStage} stage - The new stage to show.
+ * @param {boolean} [focusConnect=false] - Whether to try and focus the
+ * connect button, if we are in the Start stage.
+ */
+ updateStage(stage, focusConnect = false) {
if (stage.name === this.shownStage) {
return;
}
+ const prevStage = this.shownStage;
this.shownStage = stage.name;
this.selectedLocation = stage.defaultRegion;
+ // By default we want to reset the focus to the top of the page when
+ // changing the displayed page since we want a user to read the new page
+ // before activating a control.
+ let moveFocus = this.elements.heading;
+
+ if (stage.name === "Bootstrapping") {
+ this.preBootstrappingStage = prevStage;
+ this.preBootstrappingFocus = null;
+ if (focusConnect && stage.isQuickstart) {
+ // If this is the initial automatic bootstrap triggered by the
+ // quickstart preference, treat as if the previous shown stage was
+ // "Start" and the user clicked the "Connect" button.
+ // Then, if the user cancels, the focus should still move to the
+ // "Connect" button.
+ this.preBootstrappingStage = "Start";
+ this.preBootstrappingFocus = this.elements.connectButton;
+ } else if (this.elements.connectButton.contains(document.activeElement)) {
+ this.preBootstrappingFocus = this.elements.connectButton;
+ } else if (
+ this.elements.tryBridgeButton.contains(document.activeElement)
+ ) {
+ this.preBootstrappingFocus = this.elements.tryBridgeButton;
+ }
+ } else {
+ if (
+ this.userCancelled &&
+ prevStage === "Bootstrapping" &&
+ stage.name === this.preBootstrappingStage &&
+ this.preBootstrappingFocus &&
+ this.elements.cancelButton.contains(document.activeElement)
+ ) {
+ // If returning back to the same stage after the user tried to cancel
+ // bootstrapping from within this page, then we restore the focus to the
+ // connect button to allow the user to quickly re-try.
+ // If the bootstrap was cancelled for any other reason, we reset the
+ // focus as usual.
+ moveFocus = this.preBootstrappingFocus;
+ }
+ // Clear the Bootstrapping variables.
+ this.preBootstrappingStage = null;
+ this.preBootstrappingFocus = null;
+ }
+
+ // Clear the recording of the cancellation request.
+ this.userCancelled = false;
+
+ let isLoaded = true;
let showProgress = false;
let showLog = false;
switch (stage.name) {
@@ -368,14 +463,21 @@ class AboutTorConnect {
console.error("Should not be open when TorConnect is disabled");
break;
case "Loading":
+ // Unexpected for this page to open so early.
+ console.warn("Page opened whilst loading");
+ isLoaded = false;
+ break;
case "Start":
- // Loading is not currnetly handled, treat the same as "Start", but UI
- // will be unresponsive.
this.showStart(stage.tryAgain, stage.potentiallyBlocked);
+ if (focusConnect) {
+ moveFocus = this.elements.connectButton;
+ }
break;
case "Bootstrapping":
showProgress = true;
this.showBootstrapping(stage.bootstrapTrigger, stage.tryAgain);
+ // Always focus the cancel button.
+ moveFocus = this.elements.cancelButton;
break;
case "Offline":
showLog = true;
@@ -419,6 +521,9 @@ class AboutTorConnect {
} else {
this.hide(this.elements.viewLogButton);
}
+
+ document.body.classList.toggle("loaded", isLoaded);
+ moveFocus.focus();
}
updateBootstrappingStatus(data) {
@@ -452,10 +557,9 @@ class AboutTorConnect {
this.show(this.elements.quickstartContainer);
this.show(this.elements.configureButton);
this.show(this.elements.connectButton, true);
- this.elements.connectButton.focus();
- if (tryAgain) {
- this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain;
- }
+ this.elements.connectButton.textContent = tryAgain
+ ? TorStrings.torConnect.tryAgain
+ : TorStrings.torConnect.torConnectButton;
if (potentiallyBlocked) {
this.setBreadcrumbsStatus(
BreadcrumbStatus.Active,
@@ -511,7 +615,6 @@ class AboutTorConnect {
}
this.hideButtons();
this.show(this.elements.cancelButton);
- this.elements.cancelButton.focus();
}
showOffline() {
@@ -541,7 +644,6 @@ class AboutTorConnect {
BreadcrumbStatus.Disabled
);
this.showLocationForm(true, TorStrings.torConnect.tryBridge);
- this.elements.tryBridgeButton.focus();
}
showRegionNotFound() {
@@ -557,7 +659,6 @@ class AboutTorConnect {
BreadcrumbStatus.Disabled
);
this.showLocationForm(false, TorStrings.torConnect.tryBridge);
- this.elements.tryBridgeButton.focus();
}
showConfirmRegion(error) {
@@ -573,7 +674,6 @@ class AboutTorConnect {
BreadcrumbStatus.Active
);
this.showLocationForm(false, TorStrings.torConnect.tryAgain);
- this.elements.tryBridgeButton.focus();
}
showFinalError(error) {
@@ -722,7 +822,8 @@ class AboutTorConnect {
this.elements.connectButton.textContent =
TorStrings.torConnect.torConnectButton;
this.elements.connectButton.addEventListener("click", () => {
- this.beginBootstrapping();
+ // Record as userClickedConnect if we are in the Start stage.
+ this.beginBootstrapping(this.shownStage === "Start");
});
this.populateLocations();
@@ -802,7 +903,13 @@ class AboutTorConnect {
this.initObservers();
this.initKeyboardShortcuts();
- this.updateStage(args.stage);
+ // If we have previously opened about:torconnect and the user tried the
+ // "Connect" button we want to focus the "Connect" button for easy
+ // activation.
+ // Otherwise, we do not want to focus it for first time users so they can
+ // read the full page first.
+ const focusConnect = args.userHasEverClickedConnect;
+ this.updateStage(args.stage, focusConnect);
this.updateQuickstart(args.quickstartEnabled);
}
}
=====================================
toolkit/modules/TorConnect.sys.mjs
=====================================
@@ -680,10 +680,14 @@ export const TorConnectStage = Object.freeze({
* A summary of the user stage.
* (This class is mirrored for Android in TorConnectStage.java. Changes should be mirrored there)
*
- * @property {string} name - The name of the stage.
+ * @property {string} name - The name of the stage. One of the values in
+ * `TorConnectStage`.
* @property {string} defaultRegion - The default region to show in the UI.
- * @property {?string} bootstrapTrigger - The TorConnectStage prior to this
+ * @property {?string} bootstrapTrigger - The name of the stage prior to this
* bootstrap attempt. Only set during the "Bootstrapping" stage.
+ * @property {boolean} isQuickstart - Whether the current bootstrap attempt was
+ * triggered by the `TorConnect.quickstart` preference. Will be `false`
+ * outside of the "Bootstrapping" stage.
* @property {?BootstrapError} error - The last bootstrapping error.
* @property {boolean} tryAgain - Whether a bootstrap attempt has failed, so
* that a normal bootstrap should be shown as "Try Again" instead of
@@ -752,6 +756,14 @@ export const TorConnect = {
*/
_bootstrapTrigger: null,
+ /**
+ * Whether the current bootstrapping attempt was triggered by the quickstart
+ * preference.
+ *
+ * @type {boolean}
+ */
+ _isQuickstart: false,
+
/**
* The alternative stage that we should move to after bootstrapping completes.
*
@@ -807,6 +819,7 @@ export const TorConnect = {
name: this._stageName,
defaultRegion: this._defaultRegion,
bootstrapTrigger: this._bootstrapTrigger,
+ isQuickstart: this._isQuickstart,
error: this._errorDetails
? {
code: this._errorDetails.code,
@@ -935,7 +948,7 @@ export const TorConnect = {
// And the previous bootstrap attempt must have succeeded.
!Services.prefs.getBoolPref(TorConnectPrefs.prompt_at_startup, true)
) {
- this.beginBootstrapping();
+ this._beginBootstrappingInternal(undefined, true);
}
},
@@ -1303,6 +1316,19 @@ export const TorConnect = {
* an auto-bootstrap attempt.
*/
async beginBootstrapping(regionCode) {
+ await this._beginBootstrappingInternal(regionCode, false);
+ },
+
+ /**
+ * Begin a bootstrap attempt.
+ *
+ * @param {string} [regionCode] - An optional region code string to use, or
+ * "automatic" to automatically determine the region. If given, will start
+ * an auto-bootstrap attempt.
+ * @param {boolean} isQuickstart - Whether this was triggered by the
+ * quickstart option.
+ */
+ async _beginBootstrappingInternal(regionCode, isQuickstart) {
lazy.logger.debug("TorConnect.beginBootstrapping()");
if (!this._confirmBootstrapping(regionCode)) {
@@ -1331,6 +1357,7 @@ export const TorConnect = {
}
this._requestedStage = null;
this._bootstrapTrigger = beginStage;
+ this._isQuickstart = isQuickstart;
this._setStage(TorConnectStage.Bootstrapping);
this._bootstrapAttempt = bootstrapAttempt;
@@ -1349,6 +1376,7 @@ export const TorConnect = {
const requestedStage = this._requestedStage;
this._requestedStage = null;
this._bootstrapTrigger = null;
+ this._isQuickstart = false;
this._bootstrapAttempt = null;
if (bootstrapAttempt.detectedRegion) {
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f5c281…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f5c281…
You're receiving this email because of your account on gitlab.torproject.org.