tor-commits
Threads by month
- ----- 2025 -----
- 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
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
August 2023
- 1 participants
- 164 discussions

[Git][tpo/applications/tor-browser][base-browser-115.1.0esr-13.0-1] 2 commits: amend! Bug 32308: use direct browser sizing for letterboxing.
by ma1 (@ma1) 08 Aug '23
by ma1 (@ma1) 08 Aug '23
08 Aug '23
ma1 pushed to branch base-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
dc2274d0 by hackademix at 2023-08-08T09:18:01+02:00
amend! Bug 32308: use direct browser sizing for letterboxing.
Bug 32308: use direct browser sizing for letterboxing.
Bug 30556: align letterboxing with 200x100 new win width stepping
- - - - -
9ac98dab by hackademix at 2023-08-08T09:18:02+02:00
fixup! Firefox preference overrides.
Bug 33282: Redefine the dimensions of new RFP windows
- - - - -
2 changed files:
- browser/app/profile/001-base-profile.js
- toolkit/components/resistfingerprinting/RFPHelper.sys.mjs
Changes:
=====================================
browser/app/profile/001-base-profile.js
=====================================
@@ -291,6 +291,9 @@ pref("dom.use_components_shim", false);
pref("privacy.resistFingerprinting.letterboxing", true);
// tor-browser#41695: how many warnings we show if user closes them without restoring the window size
pref("privacy.resistFingerprinting.resizeWarnings", 3);
+// tor-browser#33282: new windows start at 1400x900 when there's enough screen space, otherwise down by 200x100 blocks
+pref("privacy.window.maxInnerWidth", 1400);
+pref("privacy.window.maxInnerHeight", 900);
// Enforce Network Information API as disabled
pref("dom.netinfo.enabled", false);
pref("network.http.referer.defaultPolicy", 2); // Bug 32948: Make referer behavior consistent regardless of private browing mode status
=====================================
toolkit/components/resistfingerprinting/RFPHelper.sys.mjs
=====================================
@@ -475,14 +475,14 @@ class _RFPHelper {
/**
* Given a width or height, rounds it with the proper stepping.
*/
- steppedSize(aDimension) {
+ steppedSize(aDimension, isWidth = false) {
let stepping;
if (aDimension <= 50) {
return 0;
} else if (aDimension <= 500) {
stepping = 50;
} else if (aDimension <= 1600) {
- stepping = 100;
+ stepping = isWidth ? 200 : 100;
} else {
stepping = 200;
}
@@ -569,7 +569,7 @@ class _RFPHelper {
// If the set is empty, we will round the content with the default
// stepping size.
if (!this._letterboxingDimensions.length) {
- result = r(this.steppedSize(aWidth), this.steppedSize(aHeight));
+ result = r(this.steppedSize(aWidth, true), this.steppedSize(aHeight));
log(
`${logPrefix} roundDimensions(${aWidth}, ${aHeight}) = ${result.width} x ${result.height}`
);
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/65183e…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/65183e…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] fixup! Firefox preference overrides.
by ma1 (@ma1) 08 Aug '23
by ma1 (@ma1) 08 Aug '23
08 Aug '23
ma1 pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
e0e763c0 by hackademix at 2023-08-08T09:11:30+02:00
fixup! Firefox preference overrides.
Bug 33282: Redefine the dimensions of new RFP windows
- - - - -
1 changed file:
- browser/app/profile/001-base-profile.js
Changes:
=====================================
browser/app/profile/001-base-profile.js
=====================================
@@ -291,6 +291,9 @@ pref("dom.use_components_shim", false);
pref("privacy.resistFingerprinting.letterboxing", true);
// tor-browser#41695: how many warnings we show if user closes them without restoring the window size
pref("privacy.resistFingerprinting.resizeWarnings", 3);
+// tor-browser#33282: new windows start at 1400x900 when there's enough screen space, otherwise down by 200x100 blocks
+pref("privacy.window.maxInnerWidth", 1400);
+pref("privacy.window.maxInnerHeight", 900);
// Enforce Network Information API as disabled
pref("dom.netinfo.enabled", false);
pref("network.http.referer.defaultPolicy", 2); // Bug 32948: Make referer behavior consistent regardless of private browing mode status
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/e0e763c…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/e0e763c…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] amend! Bug 32308: use direct browser sizing for letterboxing.
by ma1 (@ma1) 08 Aug '23
by ma1 (@ma1) 08 Aug '23
08 Aug '23
ma1 pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
63eecb92 by hackademix at 2023-08-08T08:08:43+02:00
amend! Bug 32308: use direct browser sizing for letterboxing.
Bug 32308: use direct browser sizing for letterboxing.
Bug 30556: align letterboxing with 200x100 new win width stepping
- - - - -
1 changed file:
- toolkit/components/resistfingerprinting/RFPHelper.sys.mjs
Changes:
=====================================
toolkit/components/resistfingerprinting/RFPHelper.sys.mjs
=====================================
@@ -475,14 +475,14 @@ class _RFPHelper {
/**
* Given a width or height, rounds it with the proper stepping.
*/
- steppedSize(aDimension) {
+ steppedSize(aDimension, isWidth = false) {
let stepping;
if (aDimension <= 50) {
return 0;
} else if (aDimension <= 500) {
stepping = 50;
} else if (aDimension <= 1600) {
- stepping = 100;
+ stepping = isWidth ? 200 : 100;
} else {
stepping = 200;
}
@@ -569,7 +569,7 @@ class _RFPHelper {
// If the set is empty, we will round the content with the default
// stepping size.
if (!this._letterboxingDimensions.length) {
- result = r(this.steppedSize(aWidth), this.steppedSize(aHeight));
+ result = r(this.steppedSize(aWidth, true), this.steppedSize(aHeight));
log(
`${logPrefix} roundDimensions(${aWidth}, ${aHeight}) = ${result.width} x ${result.height}`
);
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/63eecb9…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/63eecb9…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser] Pushed new tag tor-browser-115.1.0esr-13.0-1-build3
by richard (@richard) 07 Aug '23
by richard (@richard) 07 Aug '23
07 Aug '23
richard pushed new tag tor-browser-115.1.0esr-13.0-1-build3 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

[Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] 8 commits: fixup! Bug 40933: Add tor-launcher functionality
by richard (@richard) 07 Aug '23
by richard (@richard) 07 Aug '23
07 Aug '23
richard pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
d339153b by Pier Angelo Vendrame at 2023-08-07T18:36:05+02:00
fixup! Bug 40933: Add tor-launcher functionality
Use the new functions whenever possible, adjust some property names and
other minor fixes.
- - - - -
e8d3c0b8 by Pier Angelo Vendrame at 2023-08-07T18:36:14+02:00
fixup! Bug 40597: Implement TorSettings module
Changes needed for the new control port implementation.
Also, moved to ES modules and done some refactors on Moat.
- - - - -
c1b05a65 by Pier Angelo Vendrame at 2023-08-07T18:36:15+02:00
fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Changes for the new control port implementation and following
ESMification.
- - - - -
d23cb300 by Pier Angelo Vendrame at 2023-08-07T18:36:15+02:00
fixup! Bug 30237: Add v3 onion services client authentication prompt
Changes for the new control port implementation.
- - - - -
c801d620 by Pier Angelo Vendrame at 2023-08-07T18:36:16+02:00
fixup! Bug 7494: Create local home page for TBB.
Use the TorProvider in TorCheckService
- - - - -
fa105e4f by Pier Angelo Vendrame at 2023-08-07T18:36:16+02:00
fixup! Bug 3455: Add DomainIsolator, for isolating circuit by domain.
Remove TorMonitorService and TorProtocolService references.
- - - - -
cd92f30e by Pier Angelo Vendrame at 2023-08-07T18:36:17+02:00
fixup! Bug 41668: Tweaks to the Base Browser updater for Tor Browser
Removed TorMonitorService reference
- - - - -
384dff97 by Pier Angelo Vendrame at 2023-08-07T18:36:17+02:00
fixup! Bug 40933: Add tor-launcher functionality
Remove the final references to TorMonitorService and TorProtocolService.
- - - - -
24 changed files:
- browser/components/abouttor/TorCheckService.sys.mjs
- browser/components/onionservices/content/authPrompt.js
- browser/components/onionservices/content/savedKeysDialog.js
- browser/components/torpreferences/content/builtinBridgeDialog.jsm
- browser/components/torpreferences/content/connectionPane.js
- browser/components/torpreferences/content/connectionSettingsDialog.jsm
- browser/components/torpreferences/content/provideBridgeDialog.jsm
- browser/components/torpreferences/content/requestBridgeDialog.jsm
- browser/components/torpreferences/content/torLogDialog.jsm
- browser/modules/BridgeDB.jsm → browser/modules/BridgeDB.sys.mjs
- browser/modules/Moat.jsm → browser/modules/Moat.sys.mjs
- browser/modules/TorConnect.jsm → browser/modules/TorConnect.sys.mjs
- browser/modules/TorSettings.jsm → browser/modules/TorSettings.sys.mjs
- browser/modules/moz.build
- toolkit/components/tor-launcher/TorBootstrapRequest.sys.mjs
- toolkit/components/tor-launcher/TorControlPort.sys.mjs
- 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/TorProvider.sys.mjs
- toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
- toolkit/components/tor-launcher/TorStartupService.sys.mjs
- toolkit/components/tor-launcher/moz.build
- toolkit/mozapps/update/UpdateService.sys.mjs
Changes:
=====================================
browser/components/abouttor/TorCheckService.sys.mjs
=====================================
@@ -11,14 +11,9 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ConsoleAPI: "resource://gre/modules/Console.sys.mjs",
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});
-ChromeUtils.defineModuleGetter(
- lazy,
- "TorProtocolService",
- "resource://gre/modules/TorProtocolService.jsm"
-);
-
export const TorCheckService = {
kCheckNotInitiated: 0, // Possible values for status.
kCheckSuccessful: 1,
@@ -109,7 +104,7 @@ export const TorCheckService = {
let listeners;
try {
- listeners = await lazy.TorProtocolService.getSocksListeners();
+ listeners = await lazy.TorProviderBuilder.build().getSocksListeners();
} catch (e) {
this._logger.error("Failed to get the SOCKS listerner addresses.", e);
return false;
=====================================
browser/components/onionservices/content/authPrompt.js
=====================================
@@ -4,10 +4,13 @@
/* globals gBrowser, PopupNotifications, Services, XPCOMUtils */
+ChromeUtils.defineESModuleGetters(this, {
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+});
+
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",
});
@@ -203,7 +206,8 @@ const OnionAuthPrompt = (function () {
let checkboxElem = this._getCheckboxElement();
let isPermanent = checkboxElem && checkboxElem.checked;
- TorProtocolService.onionAuthAdd(onionServiceId, base64key, isPermanent)
+ TorProviderBuilder.build()
+ .onionAuthAdd(onionServiceId, base64key, isPermanent)
.then(aResponse => {
// Success! Reload the page.
this._browser.sendMessageToActor(
=====================================
browser/components/onionservices/content/savedKeysDialog.js
=====================================
@@ -8,11 +8,9 @@ ChromeUtils.defineModuleGetter(
"resource:///modules/TorStrings.jsm"
);
-ChromeUtils.defineModuleGetter(
- this,
- "TorProtocolService",
- "resource://gre/modules/TorProtocolService.jsm"
-);
+ChromeUtils.defineESModuleGetters(this, {
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+});
var gOnionServicesSavedKeysDialog = {
selector: {
@@ -54,6 +52,7 @@ var gOnionServicesSavedKeysDialog = {
await this._deleteOneKey(indexesToDelete[i]);
}
} catch (e) {
+ console.error("Removing a saved key failed", e);
if (e.torMessage) {
this._showError(e.torMessage);
} else {
@@ -125,22 +124,16 @@ var gOnionServicesSavedKeysDialog = {
try {
this._tree.view = this;
- const keyInfoList = await TorProtocolService.onionAuthViewKeys();
+ const keyInfoList = await TorProviderBuilder.build().onionAuthViewKeys();
if (keyInfoList) {
// Filter out temporary keys.
- this._keyInfoList = keyInfoList.filter(aKeyInfo => {
- if (!aKeyInfo.Flags) {
- return false;
- }
-
- const flags = aKeyInfo.Flags.split(",");
- return flags.includes("Permanent");
- });
-
+ this._keyInfoList = keyInfoList.filter(aKeyInfo =>
+ aKeyInfo.flags?.includes("Permanent")
+ );
// Sort by the .onion address.
this._keyInfoList.sort((aObj1, aObj2) => {
- const hsAddr1 = aObj1.hsAddress.toLowerCase();
- const hsAddr2 = aObj2.hsAddress.toLowerCase();
+ const hsAddr1 = aObj1.address.toLowerCase();
+ const hsAddr2 = aObj2.address.toLowerCase();
if (hsAddr1 < hsAddr2) {
return -1;
}
@@ -164,7 +157,7 @@ var gOnionServicesSavedKeysDialog = {
// This method may throw; callers should catch errors.
async _deleteOneKey(aIndex) {
const keyInfoObj = this._keyInfoList[aIndex];
- await TorProtocolService.onionAuthRemove(keyInfoObj.hsAddress);
+ await TorProviderBuilder.build().onionAuthRemove(keyInfoObj.address);
this._tree.view.selection.clearRange(aIndex, aIndex);
this._keyInfoList.splice(aIndex, 1);
this._tree.rowCountChanged(aIndex + 1, -1);
@@ -193,26 +186,20 @@ var gOnionServicesSavedKeysDialog = {
// XUL tree widget view implementation.
get rowCount() {
- return this._keyInfoList ? this._keyInfoList.length : 0;
+ return this._keyInfoList?.length ?? 0;
},
getCellText(aRow, aCol) {
- let val = "";
if (this._keyInfoList && aRow < this._keyInfoList.length) {
const keyInfo = this._keyInfoList[aRow];
if (aCol.id.endsWith("-siteCol")) {
- val = keyInfo.hsAddress;
+ return keyInfo.address;
} else if (aCol.id.endsWith("-keyCol")) {
- val = keyInfo.typeAndKey;
- // Omit keyType because it is always "x25519".
- const idx = val.indexOf(":");
- if (idx > 0) {
- val = val.substring(idx + 1);
- }
+ // keyType is always "x25519", so do not show it.
+ return keyInfo.keyBlob;
}
}
-
- return val;
+ return "";
},
isSeparator(index) {
=====================================
browser/components/torpreferences/content/builtinBridgeDialog.jsm
=====================================
@@ -7,10 +7,10 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
const { TorSettings, TorBridgeSource, TorBuiltinBridgeTypes } =
- ChromeUtils.import("resource:///modules/TorSettings.jsm");
+ ChromeUtils.importESModule("resource:///modules/TorSettings.sys.mjs");
-const { TorConnect, TorConnectTopics } = ChromeUtils.import(
- "resource:///modules/TorConnect.jsm"
+const { TorConnect, TorConnectTopics } = ChromeUtils.importESModule(
+ "resource:///modules/TorConnect.sys.mjs"
);
class BuiltinBridgeDialog {
=====================================
browser/components/torpreferences/content/connectionPane.js
=====================================
@@ -12,20 +12,17 @@ const { setTimeout, clearTimeout } = ChromeUtils.import(
);
const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource } =
- ChromeUtils.import("resource:///modules/TorSettings.jsm");
+ ChromeUtils.importESModule("resource:///modules/TorSettings.sys.mjs");
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"
+const { TorProviderBuilder, TorProviderTopics } = ChromeUtils.importESModule(
+ "resource://gre/modules/TorProviderBuilder.sys.mjs"
);
const { TorConnect, TorConnectTopics, TorConnectState, TorCensorshipLevel } =
- ChromeUtils.import("resource:///modules/TorConnect.jsm");
+ ChromeUtils.importESModule("resource:///modules/TorConnect.sys.mjs");
const { TorLogDialog } = ChromeUtils.import(
"chrome://browser/content/torpreferences/torLogDialog.jsm"
@@ -51,7 +48,9 @@ const { ProvideBridgeDialog } = ChromeUtils.import(
"chrome://browser/content/torpreferences/provideBridgeDialog.jsm"
);
-const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm");
+const { MoatRPC } = ChromeUtils.importESModule(
+ "resource:///modules/Moat.sys.mjs"
+);
const { QRCode } = ChromeUtils.import("resource://gre/modules/QRCode.jsm");
@@ -156,7 +155,7 @@ const gConnectionPane = (function () {
_populateXUL() {
// saves tor settings to disk when navigate away from about:preferences
window.addEventListener("blur", val => {
- TorProtocolService.flushSettings();
+ TorProviderBuilder.build().flushSettings();
});
document
@@ -751,7 +750,7 @@ const gConnectionPane = (function () {
// 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;
+ const bridge = TorProviderBuilder.build().currentBridge;
if (bridge?.fingerprint !== this._currentBridgeId) {
this._currentBridgeId = bridge?.fingerprint ?? null;
this._updateConnectedBridges();
@@ -850,7 +849,7 @@ const gConnectionPane = (function () {
});
Services.obs.addObserver(this, TorConnectTopics.StateChange);
- Services.obs.addObserver(this, TorMonitorTopics.BridgeChanged);
+ Services.obs.addObserver(this, TorProviderTopics.BridgeChanged);
Services.obs.addObserver(this, "intl:app-locales-changed");
},
@@ -875,7 +874,7 @@ const gConnectionPane = (function () {
// unregister our observer topics
Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged);
Services.obs.removeObserver(this, TorConnectTopics.StateChange);
- Services.obs.removeObserver(this, TorMonitorTopics.BridgeChanged);
+ Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged);
Services.obs.removeObserver(this, "intl:app-locales-changed");
},
@@ -907,7 +906,7 @@ const gConnectionPane = (function () {
this.onStateChange();
break;
}
- case TorMonitorTopics.BridgeChanged: {
+ case TorProviderTopics.BridgeChanged: {
if (data?.fingerprint !== this._currentBridgeId) {
this._checkConnectedBridge();
}
=====================================
browser/components/torpreferences/content/connectionSettingsDialog.jsm
=====================================
@@ -2,8 +2,8 @@
var EXPORTED_SYMBOLS = ["ConnectionSettingsDialog"];
-const { TorSettings, TorProxyType } = ChromeUtils.import(
- "resource:///modules/TorSettings.jsm"
+const { TorSettings, TorProxyType } = ChromeUtils.importESModule(
+ "resource:///modules/TorSettings.sys.mjs"
);
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
=====================================
browser/components/torpreferences/content/provideBridgeDialog.jsm
=====================================
@@ -6,12 +6,12 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
-const { TorSettings, TorBridgeSource } = ChromeUtils.import(
- "resource:///modules/TorSettings.jsm"
+const { TorSettings, TorBridgeSource } = ChromeUtils.importESModule(
+ "resource:///modules/TorSettings.sys.mjs"
);
-const { TorConnect, TorConnectTopics } = ChromeUtils.import(
- "resource:///modules/TorConnect.jsm"
+const { TorConnect, TorConnectTopics } = ChromeUtils.importESModule(
+ "resource:///modules/TorConnect.sys.mjs"
);
class ProvideBridgeDialog {
=====================================
browser/components/torpreferences/content/requestBridgeDialog.jsm
=====================================
@@ -4,11 +4,13 @@ var EXPORTED_SYMBOLS = ["RequestBridgeDialog"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const { BridgeDB } = ChromeUtils.import("resource:///modules/BridgeDB.jsm");
+const { BridgeDB } = ChromeUtils.importESModule(
+ "resource:///modules/BridgeDB.sys.mjs"
+);
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
-const { TorConnect, TorConnectTopics } = ChromeUtils.import(
- "resource:///modules/TorConnect.jsm"
+const { TorConnect, TorConnectTopics } = ChromeUtils.importESModule(
+ "resource:///modules/TorConnect.sys.mjs"
);
class RequestBridgeDialog {
=====================================
browser/components/torpreferences/content/torLogDialog.jsm
=====================================
@@ -2,12 +2,12 @@
var EXPORTED_SYMBOLS = ["TorLogDialog"];
-const { setTimeout, clearTimeout } = ChromeUtils.import(
- "resource://gre/modules/Timer.jsm"
+const { setTimeout, clearTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
);
-const { TorMonitorService } = ChromeUtils.import(
- "resource://gre/modules/TorMonitorService.jsm"
+const { TorProviderBuilder } = ChromeUtils.importESModule(
+ "resource://gre/modules/TorProviderBuilder.sys.mjs"
);
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
@@ -56,7 +56,7 @@ class TorLogDialog {
}, RESTORE_TIME);
});
- this._logTextarea.value = TorMonitorService.getLog();
+ this._logTextarea.value = TorProviderBuilder.build().getLog();
}
init(window, aDialog) {
=====================================
browser/modules/BridgeDB.jsm → browser/modules/BridgeDB.sys.mjs
=====================================
@@ -1,10 +1,14 @@
-"use strict";
+/* 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/. */
-var EXPORTED_SYMBOLS = ["BridgeDB"];
+const lazy = {};
-const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm");
+ChromeUtils.defineESModuleGetters(lazy, {
+ MoatRPC: "resource:///modules/Moat.sys.mjs",
+});
-var BridgeDB = {
+export var BridgeDB = {
_moatRPC: null,
_challenge: null,
_image: null,
@@ -20,7 +24,7 @@ var BridgeDB = {
async submitCaptchaGuess(solution) {
if (!this._moatRPC) {
- this._moatRPC = new MoatRPC();
+ this._moatRPC = new lazy.MoatRPC();
await this._moatRPC.init();
}
@@ -37,7 +41,7 @@ var BridgeDB = {
async requestNewCaptchaImage() {
try {
if (!this._moatRPC) {
- this._moatRPC = new MoatRPC();
+ this._moatRPC = new lazy.MoatRPC();
await this._moatRPC.init();
}
=====================================
browser/modules/Moat.jsm → browser/modules/Moat.sys.mjs
=====================================
@@ -1,24 +1,19 @@
-"use strict";
+/* 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/. */
-var EXPORTED_SYMBOLS = ["MoatRPC"];
+import {
+ TorSettings,
+ TorBridgeSource,
+} from "resource:///modules/TorSettings.sys.mjs";
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const lazy = {};
-const { Subprocess } = ChromeUtils.import(
- "resource://gre/modules/Subprocess.jsm"
-);
-
-const { TorLauncherUtil } = ChromeUtils.import(
- "resource://gre/modules/TorLauncherUtil.jsm"
-);
-
-const { TorProtocolService } = ChromeUtils.import(
- "resource://gre/modules/TorProtocolService.jsm"
-);
-
-const { TorSettings, TorBridgeSource } = ChromeUtils.import(
- "resource:///modules/TorSettings.jsm"
-);
+ChromeUtils.defineESModuleGetters(lazy, {
+ Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
+ TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+});
const TorLauncherPrefs = Object.freeze({
bridgedb_front: "extensions.torlauncher.bridgedb_front",
@@ -26,73 +21,54 @@ const TorLauncherPrefs = Object.freeze({
moat_service: "extensions.torlauncher.moat_service",
});
-// Config keys used to query tor daemon properties
-const TorConfigKeys = Object.freeze({
- clientTransportPlugin: "ClientTransportPlugin",
-});
-
//
// Launches and controls the PT process lifetime
//
class MeekTransport {
- constructor() {
- this._inited = false;
- this._meekClientProcess = null;
- this._meekProxyType = null;
- this._meekProxyAddress = null;
- this._meekProxyPort = 0;
- this._meekProxyUsername = null;
- this._meekProxyPassword = null;
- }
+ // These members are used by consumers to setup the proxy to do requests over
+ // meek. They are passed to newProxyInfoWithAuth.
+ proxyType = null;
+ proxyAddress = null;
+ proxyPort = 0;
+ proxyUsername = null;
+ proxyPassword = null;
+
+ #inited = false;
+ #meekClientProcess = null;
// launches the meekprocess
async init() {
// ensure we haven't already init'd
- if (this._inited) {
+ if (this.#inited) {
throw new Error("MeekTransport: Already initialized");
}
- // cleanup function for killing orphaned pt process
- let onException = () => {};
try {
// figure out which pluggable transport to use
const supportedTransports = ["meek", "meek_lite"];
- let transportPlugins = await TorProtocolService.readStringArraySetting(
- TorConfigKeys.clientTransportPlugin
+ const proxy = (
+ await lazy.TorProviderBuilder.build().getPluggableTransports()
+ ).find(
+ pt =>
+ pt.type === "exec" &&
+ supportedTransports.some(t => pt.transports.includes(t))
);
+ if (!proxy) {
+ throw new Error("No supported transport found.");
+ }
- let { meekTransport, meekClientPath, meekClientArgs } = (() => {
- for (const line of transportPlugins) {
- let tokens = line.split(" ");
- if (tokens.length > 2 && tokens[1] == "exec") {
- let transportArray = tokens[0].split(",").map(aStr => aStr.trim());
- let transport = transportArray.find(aTransport =>
- supportedTransports.includes(aTransport)
- );
-
- if (transport != undefined) {
- return {
- meekTransport: transport,
- meekClientPath: tokens[2],
- meekClientArgs: tokens.slice(3),
- };
- }
- }
- }
-
- return {
- meekTransport: null,
- meekClientPath: null,
- meekClientArgs: null,
- };
- })();
-
+ const meekTransport = proxy.transports.find(t =>
+ supportedTransports.includes(t)
+ );
// Convert meek client path to absolute path if necessary
- let meekWorkDir = TorLauncherUtil.getTorFile("pt-startup-dir", false);
- if (TorLauncherUtil.isPathRelative(meekClientPath)) {
- let meekPath = meekWorkDir.clone();
- meekPath.appendRelativePath(meekClientPath);
- meekClientPath = meekPath.path;
+ const meekWorkDir = lazy.TorLauncherUtil.getTorFile(
+ "pt-startup-dir",
+ false
+ );
+ if (lazy.TorLauncherUtil.isPathRelative(proxy.pathToBinary)) {
+ const meekPath = meekWorkDir.clone();
+ meekPath.appendRelativePath(proxy.pathToBinary);
+ proxy.pathToBinary = meekPath.path;
}
// Construct the per-connection arguments.
@@ -105,16 +81,13 @@ class MeekTransport {
// First the "<Key>=<Value>" formatted arguments MUST be escaped,
// such that all backslash, equal sign, and semicolon characters
// are escaped with a backslash.
- let escapeArgValue = aValue => {
- if (!aValue) {
- return "";
- }
-
- let rv = aValue.replace(/\\/g, "\\\\");
- rv = rv.replace(/=/g, "\\=");
- rv = rv.replace(/;/g, "\\;");
- return rv;
- };
+ const escapeArgValue = aValue =>
+ aValue
+ ? aValue
+ .replaceAll("\\", "\\\\")
+ .replaceAll("=", "\\=")
+ .replaceAll(";", "\\;")
+ : "";
if (meekReflector) {
meekClientEscapedArgs += "url=";
@@ -132,10 +105,10 @@ class MeekTransport {
}
// Setup env and start meek process
- let ptStateDir = TorLauncherUtil.getTorFile("tordatadir", false);
+ const ptStateDir = lazy.TorLauncherUtil.getTorFile("tordatadir", false);
ptStateDir.append("pt_state"); // Match what tor uses.
- let envAdditions = {
+ const envAdditions = {
TOR_PT_MANAGED_TRANSPORT_VER: "1",
TOR_PT_STATE_LOCATION: ptStateDir.path,
TOR_PT_EXIT_ON_STDIN_CLOSE: "1",
@@ -145,9 +118,9 @@ class MeekTransport {
envAdditions.TOR_PT_PROXY = TorSettings.proxy.uri;
}
- let opts = {
- command: meekClientPath,
- arguments: meekClientArgs,
+ const opts = {
+ command: proxy.pathToBinary,
+ arguments: proxy.options.split(/s+/),
workdir: meekWorkDir.path,
environmentAppend: true,
environment: envAdditions,
@@ -155,27 +128,23 @@ class MeekTransport {
};
// Launch meek client
- let meekClientProcess = await Subprocess.call(opts);
- // kill our process if exception is thrown
- onException = () => {
- meekClientProcess.kill();
- };
+ this.#meekClientProcess = await lazy.Subprocess.call(opts);
// Callback chain for reading stderr
- let stderrLogger = async () => {
- if (this._meekClientProcess) {
- let errString = await this._meekClientProcess.stderr.readString();
- console.log(`MeekTransport: stderr => ${errString}`);
- await stderrLogger();
+ const stderrLogger = async () => {
+ while (this.#meekClientProcess) {
+ const errString = await this.#meekClientProcess.stderr.readString();
+ if (errString) {
+ console.log(`MeekTransport: stderr => ${errString}`);
+ }
}
};
stderrLogger();
// Read pt's stdout until terminal (CMETHODS DONE) is reached
// returns array of lines for parsing
- let getInitLines = async (stdout = "") => {
- let string = await meekClientProcess.stdout.readString();
- stdout += string;
+ const getInitLines = async (stdout = "") => {
+ stdout += await this.#meekClientProcess.stdout.readString();
// look for the final message
const CMETHODS_DONE = "CMETHODS DONE";
@@ -188,20 +157,16 @@ class MeekTransport {
};
// read our lines from pt's stdout
- let meekInitLines = await getInitLines();
+ const meekInitLines = await getInitLines();
// tokenize our pt lines
- let meekInitTokens = meekInitLines.map(line => {
- let tokens = line.split(" ");
+ const meekInitTokens = meekInitLines.map(line => {
+ const tokens = line.split(" ");
return {
keyword: tokens[0],
args: tokens.slice(1),
};
});
- let meekProxyType = null;
- let meekProxyAddr = null;
- let meekProxyPort = 0;
-
// parse our pt tokens
for (const { keyword, args } of meekInitTokens) {
const argsJoined = args.join(" ");
@@ -251,9 +216,9 @@ class MeekTransport {
}
// convert proxy type to strings used by protocol-proxy-servce
- meekProxyType = proxyType === "socks5" ? "socks" : "socks4";
- meekProxyAddr = addr;
- meekProxyPort = port;
+ this.proxyType = proxyType === "socks5" ? "socks" : "socks4";
+ this.proxyAddress = addr;
+ this.proxyPort = port;
break;
}
@@ -278,49 +243,47 @@ class MeekTransport {
}
}
- this._meekClientProcess = meekClientProcess;
// register callback to cleanup on process exit
- this._meekClientProcess.wait().then(exitObj => {
- this._meekClientProcess = null;
+ this.#meekClientProcess.wait().then(exitObj => {
+ this.#meekClientProcess = null;
this.uninit();
});
- this._meekProxyType = meekProxyType;
- this._meekProxyAddress = meekProxyAddr;
- this._meekProxyPort = meekProxyPort;
-
// socks5
- if (meekProxyType === "socks") {
+ if (this.proxyType === "socks") {
if (meekClientEscapedArgs.length <= 255) {
- this._meekProxyUsername = meekClientEscapedArgs;
- this._meekProxyPassword = "\x00";
+ this.proxyUsername = meekClientEscapedArgs;
+ this.proxyPassword = "\x00";
} else {
- this._meekProxyUsername = meekClientEscapedArgs.substring(0, 255);
- this._meekProxyPassword = meekClientEscapedArgs.substring(255);
+ this.proxyUsername = meekClientEscapedArgs.substring(0, 255);
+ this.proxyPassword = meekClientEscapedArgs.substring(255);
}
// socks4
} else {
- this._meekProxyUsername = meekClientEscapedArgs;
- this._meekProxyPassword = undefined;
+ this.proxyUsername = meekClientEscapedArgs;
+ this.proxyPassword = undefined;
}
- this._inited = true;
+ this.#inited = true;
} catch (ex) {
- onException();
+ if (this.#meekClientProcess) {
+ this.#meekClientProcess.kill();
+ this.#meekClientProcess = null;
+ }
throw ex;
}
}
async uninit() {
- this._inited = false;
-
- await this._meekClientProcess?.kill();
- this._meekClientProcess = null;
- this._meekProxyType = null;
- this._meekProxyAddress = null;
- this._meekProxyPort = 0;
- this._meekProxyUsername = null;
- this._meekProxyPassword = null;
+ this.#inited = false;
+
+ await this.#meekClientProcess?.kill();
+ this.#meekClientProcess = null;
+ this.proxyType = null;
+ this.proxyAddress = null;
+ this.proxyPort = 0;
+ this.proxyUsername = null;
+ this.proxyPassword = null;
}
}
@@ -328,21 +291,25 @@ class MeekTransport {
// Callback object with a cached promise for the returned Moat data
//
class MoatResponseListener {
+ #response = "";
+ #responsePromise;
+ #resolve;
+ #reject;
constructor() {
- this._response = "";
+ this.#response = "";
// we need this promise here because await nsIHttpChannel::asyncOpen does
// not return only once the request is complete, it seems to return
// after it begins, so we have to get the result from this listener object.
// This promise is only resolved once onStopRequest is called
- this._responsePromise = new Promise((resolve, reject) => {
- this._resolve = resolve;
- this._reject = reject;
+ this.#responsePromise = new Promise((resolve, reject) => {
+ this.#resolve = resolve;
+ this.#reject = reject;
});
}
// callers wait on this for final response
response() {
- return this._responsePromise;
+ return this.#responsePromise;
}
// noop
@@ -352,16 +319,17 @@ class MoatResponseListener {
onStopRequest(request, status) {
try {
if (!Components.isSuccessCode(status)) {
- const errorMessage = TorLauncherUtil.getLocalizedStringForError(status);
- this._reject(new Error(errorMessage));
+ const errorMessage =
+ lazy.TorLauncherUtil.getLocalizedStringForError(status);
+ this.#reject(new Error(errorMessage));
}
if (request.responseStatus != 200) {
- this._reject(new Error(request.responseStatusText));
+ this.#reject(new Error(request.responseStatusText));
}
} catch (err) {
- this._reject(err);
+ this.#reject(err);
}
- this._resolve(this._response);
+ this.#resolve(this.#response);
}
// read response data
@@ -370,30 +338,32 @@ class MoatResponseListener {
"@mozilla.org/scriptableinputstream;1"
].createInstance(Ci.nsIScriptableInputStream);
scriptableStream.init(stream);
- this._response += scriptableStream.read(length);
+ this.#response += scriptableStream.read(length);
}
}
class InternetTestResponseListener {
+ #promise;
+ #resolve;
+ #reject;
constructor() {
- this._promise = new Promise((resolve, reject) => {
- this._resolve = resolve;
- this._reject = reject;
+ this.#promise = new Promise((resolve, reject) => {
+ this.#resolve = resolve;
+ this.#reject = reject;
});
}
// callers wait on this for final response
get status() {
- return this._promise;
+ return this.#promise;
}
onStartRequest(request) {}
// resolve or reject our Promise
onStopRequest(request, status) {
- let statuses = {};
try {
- statuses = {
+ const statuses = {
components: status,
successful: Components.isSuccessCode(status),
};
@@ -408,56 +378,51 @@ class InternetTestResponseListener {
err
);
}
+ this.#resolve(statuses);
} catch (err) {
- this._reject(err);
+ this.#reject(err);
}
- this._resolve(statuses);
}
onDataAvailable(request, stream, offset, length) {
- // We do not care of the actual data, as long as we have a successful
+ // We do not care of the actual data, as long as we have a successful
// connection
}
}
// constructs the json objects and sends the request over moat
-class MoatRPC {
- constructor() {
- this._meekTransport = null;
- this._inited = false;
- }
+export class MoatRPC {
+ #inited = false;
+ #meekTransport = null;
get inited() {
- return this._inited;
+ return this.#inited;
}
async init() {
- if (this._inited) {
+ if (this.#inited) {
throw new Error("MoatRPC: Already initialized");
}
let meekTransport = new MeekTransport();
await meekTransport.init();
- this._meekTransport = meekTransport;
- this._inited = true;
+ this.#meekTransport = meekTransport;
+ this.#inited = true;
}
async uninit() {
- await this._meekTransport?.uninit();
- this._meekTransport = null;
- this._inited = false;
+ await this.#meekTransport?.uninit();
+ this.#meekTransport = null;
+ this.#inited = false;
}
- _makeHttpHandler(uriString) {
- if (!this._inited) {
+ #makeHttpHandler(uriString) {
+ if (!this.#inited) {
throw new Error("MoatRPC: Not initialized");
}
- const proxyType = this._meekTransport._meekProxyType;
- const proxyAddress = this._meekTransport._meekProxyAddress;
- const proxyPort = this._meekTransport._meekProxyPort;
- const proxyUsername = this._meekTransport._meekProxyUsername;
- const proxyPassword = this._meekTransport._meekProxyPassword;
+ const { proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword } =
+ this.#meekTransport;
const proxyPS = Cc[
"@mozilla.org/network/protocol-proxy-service;1"
@@ -511,11 +476,11 @@ class MoatRPC {
return ch;
}
- async _makeRequest(procedure, args) {
+ async #makeRequest(procedure, args) {
const procedureURIString = `${Services.prefs.getStringPref(
TorLauncherPrefs.moat_service
)}/${procedure}`;
- const ch = this._makeHttpHandler(procedureURIString);
+ const ch = this.#makeHttpHandler(procedureURIString);
// Arrange for the POST data to be sent.
const argsJson = JSON.stringify(args);
@@ -544,7 +509,7 @@ class MoatRPC {
const uri = `${Services.prefs.getStringPref(
TorLauncherPrefs.moat_service
)}/circumvention/countries`;
- const ch = this._makeHttpHandler(uri);
+ const ch = this.#makeHttpHandler(uri);
ch.requestMethod = "HEAD";
const listener = new InternetTestResponseListener();
@@ -582,7 +547,7 @@ class MoatRPC {
},
],
};
- const response = await this._makeRequest("fetch", args);
+ const response = await this.#makeRequest("fetch", args);
if ("errors" in response) {
const code = response.errors[0].code;
const detail = response.errors[0].detail;
@@ -623,7 +588,7 @@ class MoatRPC {
},
],
};
- const response = await this._makeRequest("check", args);
+ const response = await this.#makeRequest("check", args);
if ("errors" in response) {
const code = response.errors[0].code;
const detail = response.errors[0].detail;
@@ -642,7 +607,7 @@ class MoatRPC {
// Convert received settings object to format used by TorSettings module
// In the event of error, just return null
- _fixupSettings(settings) {
+ #fixupSettings(settings) {
try {
let retval = TorSettings.defaultSettings();
if ("bridges" in settings) {
@@ -691,11 +656,11 @@ class MoatRPC {
// Converts a list of settings objects received from BridgeDB to a list of settings objects
// understood by the TorSettings module
// In the event of error, returns and empty list
- _fixupSettingsList(settingsList) {
+ #fixupSettingsList(settingsList) {
try {
let retval = [];
for (let settings of settingsList) {
- settings = this._fixupSettings(settings);
+ settings = this.#fixupSettings(settings);
if (settings != null) {
retval.push(settings);
}
@@ -724,7 +689,7 @@ class MoatRPC {
transports: transports ? transports : [],
country,
};
- const response = await this._makeRequest("circumvention/settings", args);
+ const response = await this.#makeRequest("circumvention/settings", args);
let settings = {};
if ("errors" in response) {
const code = response.errors[0].code;
@@ -739,7 +704,7 @@ class MoatRPC {
throw new Error(`MoatRPC: ${detail} (${code})`);
} else if ("settings" in response) {
- settings.settings = this._fixupSettingsList(response.settings);
+ settings.settings = this.#fixupSettingsList(response.settings);
}
if ("country" in response) {
settings.country = response.country;
@@ -753,7 +718,7 @@ class MoatRPC {
// for
async circumvention_countries() {
const args = {};
- return this._makeRequest("circumvention/countries", args);
+ return this.#makeRequest("circumvention/countries", args);
}
// Request a copy of the builtin bridges, takes the following parameters:
@@ -766,7 +731,7 @@ class MoatRPC {
const args = {
transports: transports ? transports : [],
};
- const response = await this._makeRequest("circumvention/builtin", args);
+ const response = await this.#makeRequest("circumvention/builtin", args);
if ("errors" in response) {
const code = response.errors[0].code;
const detail = response.errors[0].detail;
@@ -791,13 +756,13 @@ class MoatRPC {
const args = {
transports: transports ? transports : [],
};
- const response = await this._makeRequest("circumvention/defaults", args);
+ const response = await this.#makeRequest("circumvention/defaults", args);
if ("errors" in response) {
const code = response.errors[0].code;
const detail = response.errors[0].detail;
throw new Error(`MoatRPC: ${detail} (${code})`);
} else if ("settings" in response) {
- return this._fixupSettingsList(response.settings);
+ return this.#fixupSettingsList(response.settings);
}
return [];
}
=====================================
browser/modules/TorConnect.jsm → browser/modules/TorConnect.sys.mjs
=====================================
@@ -1,36 +1,32 @@
-"use strict";
+/* 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/. */
-var EXPORTED_SYMBOLS = [
- "InternetStatus",
- "TorConnect",
- "TorConnectTopics",
- "TorConnectState",
-];
+import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const lazy = {};
-const { setTimeout, clearTimeout } = ChromeUtils.import(
- "resource://gre/modules/Timer.jsm"
-);
+ChromeUtils.defineESModuleGetters(lazy, {
+ MoatRPC: "resource:///modules/Moat.sys.mjs",
+ TorBootstrapRequest: "resource://gre/modules/TorBootstrapRequest.sys.mjs",
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+});
-const { BrowserWindowTracker } = ChromeUtils.import(
+// TODO: Should we move this to the about:torconnect actor?
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "BrowserWindowTracker",
"resource:///modules/BrowserWindowTracker.jsm"
);
-const { TorMonitorService } = ChromeUtils.import(
- "resource://gre/modules/TorMonitorService.jsm"
-);
-const { TorBootstrapRequest } = ChromeUtils.import(
- "resource://gre/modules/TorBootstrapRequest.jsm"
-);
-
-const { TorSettings, TorSettingsTopics, TorBuiltinBridgeTypes } =
- ChromeUtils.import("resource:///modules/TorSettings.jsm");
+import {
+ TorSettings,
+ TorSettingsTopics,
+ TorBuiltinBridgeTypes,
+} from "resource:///modules/TorSettings.sys.mjs";
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
-const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm");
-
const TorTopics = Object.freeze({
LogHasWarnOrErr: "TorLogHasWarnOrErr",
ProcessExited: "TorProcessExited",
@@ -46,7 +42,7 @@ const TorConnectPrefs = Object.freeze({
allow_internet_test: "torbrowser.bootstrap.allow_internet_test",
});
-const TorConnectState = Object.freeze({
+export const TorConnectState = Object.freeze({
/* Our initial state */
Initial: "Initial",
/* In-between initial boot and bootstrapping, users can change tor network settings during this state */
@@ -156,7 +152,7 @@ const TorConnectStateTransitions = Object.freeze(
);
/* Topics Notified by the TorConnect module */
-const TorConnectTopics = Object.freeze({
+export const TorConnectTopics = Object.freeze({
StateChange: "torconnect:state-change",
BootstrapProgress: "torconnect:bootstrap-progress",
BootstrapComplete: "torconnect:bootstrap-complete",
@@ -238,7 +234,7 @@ const debug_sleep = async ms => {
});
};
-const InternetStatus = Object.freeze({
+export const InternetStatus = Object.freeze({
Unknown: -1,
Offline: 0,
Online: 1,
@@ -302,7 +298,7 @@ class InternetTest {
// waiting both for the bootstrap, and for the Internet test.
// However, managing Moat with async/await is much easier as it avoids a
// callback hell, and it makes extra explicit that we are uniniting it.
- const mrpc = new MoatRPC();
+ const mrpc = new lazy.MoatRPC();
let status = null;
let error = null;
try {
@@ -340,7 +336,7 @@ class InternetTest {
}
}
-const TorConnect = (() => {
+export const TorConnect = (() => {
let retval = {
_state: TorConnectState.Initial,
_bootstrapProgress: 0,
@@ -459,7 +455,7 @@ const TorConnect = (() => {
return;
}
- const tbr = new TorBootstrapRequest();
+ const tbr = new lazy.TorBootstrapRequest();
const internetTest = new InternetTest();
let cancelled = false;
@@ -604,7 +600,7 @@ const TorConnect = (() => {
// lookup user's potential censorship circumvention settings from Moat service
try {
- this.mrpc = new MoatRPC();
+ this.mrpc = new lazy.MoatRPC();
await this.mrpc.init();
if (this.transitioning) {
@@ -678,7 +674,7 @@ const TorConnect = (() => {
await TorSettings.applySettings();
// build out our bootstrap request
- const tbr = new TorBootstrapRequest();
+ const tbr = new lazy.TorBootstrapRequest();
tbr.onbootstrapstatus = (progress, status) => {
TorConnect._updateBootstrapStatus(progress, status);
};
@@ -915,7 +911,7 @@ const TorConnect = (() => {
* @type {boolean}
*/
get enabled() {
- return TorMonitorService.ownsTorDaemon;
+ return lazy.TorProviderBuilder.build().ownsTorDaemon;
},
get shouldShowTorConnect() {
@@ -1053,7 +1049,7 @@ const TorConnect = (() => {
Further external commands and helper methods
*/
openTorPreferences() {
- const win = BrowserWindowTracker.getTopWindow();
+ const win = lazy.BrowserWindowTracker.getTopWindow();
win.switchToTabHavingURI("about:preferences#connection", true);
},
@@ -1073,7 +1069,7 @@ const TorConnect = (() => {
* begin AutoBootstrapping, if possible.
*/
openTorConnect(options) {
- const win = BrowserWindowTracker.getTopWindow();
+ const win = lazy.BrowserWindowTracker.getTopWindow();
win.switchToTabHavingURI("about:torconnect", true, {
ignoreQueryString: true,
});
@@ -1094,7 +1090,7 @@ const TorConnect = (() => {
},
viewTorLogs() {
- const win = BrowserWindowTracker.getTopWindow();
+ const win = lazy.BrowserWindowTracker.getTopWindow();
win.switchToTabHavingURI("about:preferences#connection-viewlogs", true);
},
@@ -1104,7 +1100,7 @@ const TorConnect = (() => {
if (this._countryCodes.length) {
return this._countryCodes;
}
- const mrpc = new MoatRPC();
+ const mrpc = new lazy.MoatRPC();
try {
await mrpc.init();
this._countryCodes = await mrpc.circumvention_countries();
=====================================
browser/modules/TorSettings.jsm → browser/modules/TorSettings.sys.mjs
=====================================
@@ -1,36 +1,22 @@
-"use strict";
+/* 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/. */
-var EXPORTED_SYMBOLS = [
- "TorSettings",
- "TorSettingsTopics",
- "TorSettingsData",
- "TorBridgeSource",
- "TorBuiltinBridgeTypes",
- "TorProxyType",
-];
+const lazy = {};
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-const { TorMonitorService } = ChromeUtils.import(
- "resource://gre/modules/TorMonitorService.jsm"
-);
-const { TorProtocolService } = ChromeUtils.import(
- "resource://gre/modules/TorProtocolService.jsm"
-);
-
-/* tor-launcher observer topics */
-const TorTopics = Object.freeze({
- ProcessIsReady: "TorProcessIsReady",
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});
/* TorSettings observer topics */
-const TorSettingsTopics = Object.freeze({
+export const TorSettingsTopics = Object.freeze({
Ready: "torsettings:ready",
SettingChanged: "torsettings:setting-changed",
});
/* TorSettings observer data (for SettingChanged topic) */
-const TorSettingsData = Object.freeze({
+export const TorSettingsData = Object.freeze({
QuickStartEnabled: "torsettings:quickstart_enabled",
});
@@ -98,21 +84,21 @@ const TorConfigKeys = Object.freeze({
clientTransportPlugin: "ClientTransportPlugin",
});
-const TorBridgeSource = Object.freeze({
+export const TorBridgeSource = Object.freeze({
Invalid: -1,
BuiltIn: 0,
BridgeDB: 1,
UserProvided: 2,
});
-const TorProxyType = Object.freeze({
+export const TorProxyType = Object.freeze({
Invalid: -1,
Socks4: 0,
Socks5: 1,
HTTPS: 2,
});
-const TorBuiltinBridgeTypes = Object.freeze(
+export const TorBuiltinBridgeTypes = Object.freeze(
(() => {
const bridgeListBranch = Services.prefs.getBranch(
TorLauncherPrefs.default_bridge
@@ -254,7 +240,7 @@ const arrayCopy = function (array) {
/* TorSettings module */
-const TorSettings = (() => {
+export const TorSettings = (() => {
const self = {
_settings: null,
@@ -288,7 +274,8 @@ const TorSettings = (() => {
/* load or init our settings, and register observers */
init() {
- if (TorMonitorService.ownsTorDaemon) {
+ const provider = lazy.TorProviderBuilder.build();
+ if (provider.ownsTorDaemon) {
// if the settings branch exists, load settings from prefs
if (Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)) {
this.loadFromPrefs();
@@ -296,9 +283,9 @@ const TorSettings = (() => {
// otherwise load defaults
this._settings = this.defaultSettings();
}
- Services.obs.addObserver(this, TorTopics.ProcessIsReady);
+ Services.obs.addObserver(this, lazy.TorProviderTopics.ProcessIsReady);
- if (TorMonitorService.isRunning) {
+ if (provider.isRunning) {
this.handleProcessReady();
}
}
@@ -309,8 +296,11 @@ const TorSettings = (() => {
console.log(`TorSettings: Observed ${topic}`);
switch (topic) {
- case TorTopics.ProcessIsReady:
- Services.obs.removeObserver(this, TorTopics.ProcessIsReady);
+ case lazy.TorProviderTopics.ProcessIsReady:
+ Services.obs.removeObserver(
+ this,
+ lazy.TorProviderTopics.ProcessIsReady
+ );
await this.handleProcessReady();
break;
}
@@ -569,7 +559,7 @@ const TorSettings = (() => {
}
/* Push to Tor */
- await TorProtocolService.writeSettings(settingsMap);
+ await lazy.TorProviderBuilder.build().writeSettings(settingsMap);
return this;
},
=====================================
browser/modules/moz.build
=====================================
@@ -123,7 +123,7 @@ XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
EXTRA_JS_MODULES += [
"AboutNewTab.jsm",
"AsyncTabSwitcher.jsm",
- "BridgeDB.jsm",
+ "BridgeDB.sys.mjs",
"BrowserUIUtils.jsm",
"BrowserUsageTelemetry.jsm",
"BrowserWindowTracker.jsm",
@@ -135,7 +135,7 @@ EXTRA_JS_MODULES += [
"FeatureCallout.sys.mjs",
"HomePage.jsm",
"LaterRun.jsm",
- 'Moat.jsm',
+ "Moat.sys.mjs",
"NewTabPagePreloading.jsm",
"OpenInTabsUtils.jsm",
"PageActions.jsm",
@@ -149,8 +149,8 @@ EXTRA_JS_MODULES += [
"SitePermissions.sys.mjs",
"TabsList.jsm",
"TabUnloader.jsm",
- "TorConnect.jsm",
- "TorSettings.jsm",
+ "TorConnect.sys.mjs",
+ "TorSettings.sys.mjs",
"TorStrings.jsm",
"TransientPrefs.jsm",
"URILoadingHelper.sys.mjs",
=====================================
toolkit/components/tor-launcher/TorBootstrapRequest.sys.mjs
=====================================
@@ -1,6 +1,6 @@
import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
-import { TorProtocolService } from "resource://gre/modules/TorProtocolService.sys.mjs";
+import { TorProviderBuilder } from "resource://gre/modules/TorProviderBuilder.sys.mjs";
import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
/* tor-launcher observer topics */
@@ -13,19 +13,23 @@ export const TorTopics = Object.freeze({
// modeled after XMLHttpRequest
// nicely encapsulates the observer register/unregister logic
export class TorBootstrapRequest {
+ // number of ms to wait before we abandon the bootstrap attempt
+ // a value of 0 implies we never wait
+ timeout = 0;
+
+ // callbacks for bootstrap process status updates
+ onbootstrapstatus = (progress, status) => {};
+ onbootstrapcomplete = () => {};
+ onbootstraperror = (message, details) => {};
+
+ // internal resolve() method for bootstrap
+ #bootstrapPromiseResolve = null;
+ #bootstrapPromise = null;
+ #timeoutID = null;
+ #provider = null;
+
constructor() {
- // number of ms to wait before we abandon the bootstrap attempt
- // a value of 0 implies we never wait
- this.timeout = 0;
- // callbacks for bootstrap process status updates
- this.onbootstrapstatus = (progress, status) => {};
- this.onbootstrapcomplete = () => {};
- this.onbootstraperror = (message, details) => {};
-
- // internal resolve() method for bootstrap
- this._bootstrapPromiseResolve = null;
- this._bootstrapPromise = null;
- this._timeoutID = null;
+ this.#provider = TorProviderBuilder.build();
}
observe(subject, topic, data) {
@@ -41,15 +45,16 @@ export class TorBootstrapRequest {
if (this.onbootstrapcomplete) {
this.onbootstrapcomplete();
}
- this._bootstrapPromiseResolve(true);
- clearTimeout(this._timeoutID);
+ this.#bootstrapPromiseResolve(true);
+ clearTimeout(this.#timeoutID);
+ this.#timeoutID = null;
}
break;
}
case TorTopics.BootstrapError: {
console.info("TorBootstrapRequest: observerd TorBootstrapError", obj);
- this._stop(obj?.message, obj?.details);
+ this.#stop(obj?.message, obj?.details);
break;
}
}
@@ -57,12 +62,12 @@ export class TorBootstrapRequest {
// resolves 'true' if bootstrap succeeds, false otherwise
bootstrap() {
- if (this._bootstrapPromise) {
- return this._bootstrapPromise;
+ if (this.#bootstrapPromise) {
+ return this.#bootstrapPromise;
}
- this._bootstrapPromise = new Promise((resolve, reject) => {
- this._bootstrapPromiseResolve = resolve;
+ this.#bootstrapPromise = new Promise((resolve, reject) => {
+ this.#bootstrapPromiseResolve = resolve;
// register ourselves to listen for bootstrap events
Services.obs.addObserver(this, TorTopics.BootstrapStatus);
@@ -70,10 +75,10 @@ export class TorBootstrapRequest {
// optionally cancel bootstrap after a given timeout
if (this.timeout > 0) {
- this._timeoutID = setTimeout(async () => {
- this._timeoutID = null;
+ this.#timeoutID = setTimeout(async () => {
+ this.#timeoutID = null;
// TODO: Translate, if really used
- await this._stop(
+ await this.#stop(
"Tor Bootstrap process timed out",
`Bootstrap attempt abandoned after waiting ${this.timeout} ms`
);
@@ -81,38 +86,45 @@ export class TorBootstrapRequest {
}
// wait for bootstrapping to begin and maybe handle error
- TorProtocolService.connect().catch(err => {
- this._stop(err.message, "");
+ this.#provider.connect().catch(err => {
+ this.#stop(err.message, "");
});
}).finally(() => {
// and remove ourselves once bootstrap is resolved
Services.obs.removeObserver(this, TorTopics.BootstrapStatus);
Services.obs.removeObserver(this, TorTopics.BootstrapError);
- this._bootstrapPromise = null;
+ this.#bootstrapPromise = null;
});
- return this._bootstrapPromise;
+ return this.#bootstrapPromise;
}
async cancel() {
- await this._stop();
+ await this.#stop();
}
// Internal implementation. Do not use directly, but call cancel, instead.
- async _stop(message, details) {
+ async #stop(message, details) {
// first stop our bootstrap timeout before handling the error
- if (this._timeoutID !== null) {
- clearTimeout(this._timeoutID);
- this._timeoutID = null;
+ if (this.#timeoutID !== null) {
+ clearTimeout(this.#timeoutID);
+ this.#timeoutID = null;
}
- // stopBootstrap never throws
- await TorProtocolService.stopBootstrap();
+ try {
+ await this.#provider.stopBootstrap();
+ } catch (e) {
+ console.error("Failed to stop the bootstrap.", e);
+ if (!message) {
+ message = e.message;
+ details = "";
+ }
+ }
if (this.onbootstraperror && message) {
this.onbootstraperror(message, details);
}
- this._bootstrapPromiseResolve(false);
+ this.#bootstrapPromiseResolve(false);
}
}
=====================================
toolkit/components/tor-launcher/TorControlPort.sys.mjs
=====================================
@@ -274,6 +274,44 @@ class AsyncSocket {
* the command
*/
+/**
+ * @typedef {object} Bridge
+ * @property {string} transport The transport of the bridge, or vanilla if not
+ * specified.
+ * @property {string} addr The IP address and port of the bridge
+ * @property {string} id The fingerprint of the bridge
+ * @property {string} args Optional arguments passed to the bridge
+ */
+/**
+ * @typedef {object} PTInfo The information about a pluggable transport
+ * @property {string[]} transports An array with all the transports supported by
+ * this configuration.
+ * @property {string} type Either socks4, socks5 or exec
+ * @property {string} [ip] The IP address of the proxy (only for socks4 and
+ * socks5)
+ * @property {integer} [port] The port of the proxy (only for socks4 and socks5)
+ * @property {string} [pathToBinary] Path to the binary that is run (only for
+ * exec)
+ * @property {string} [options] Optional options passed to the binary (only for
+ * exec)
+ */
+/**
+ * @typedef {object} OnionAuthKeyInfo
+ * @property {string} address The address of the onion service
+ * @property {string} typeAndKey Onion service key and type of key, as
+ * `type:base64-private-key`
+ * @property {string} Flags Additional flags, such as Permanent
+ */
+/**
+ * @callback EventFilterCallback
+ * @param {any} data Either a raw string, or already parsed data
+ * @returns {boolean}
+ */
+/**
+ * @callback EventCallback
+ * @param {any} data Either a raw string, or already parsed data
+ */
+
class TorError extends Error {
constructor(command, reply) {
super(`${command} -> ${reply}`);
@@ -584,319 +622,6 @@ class ControlSocket {
}
}
-// ## utils
-// A namespace for utility functions
-let utils = {};
-
-// __utils.identity(x)__.
-// Returns its argument unchanged.
-utils.identity = function (x) {
- return x;
-};
-
-// __utils.capture(string, regex)__.
-// Takes a string and returns an array of capture items, where regex must have a single
-// capturing group and use the suffix /.../g to specify a global search.
-utils.capture = function (string, regex) {
- let matches = [];
- // Special trick to use string.replace for capturing multiple matches.
- string.replace(regex, function (a, captured) {
- matches.push(captured);
- });
- return matches;
-};
-
-// __utils.extractor(regex)__.
-// Returns a function that takes a string and returns an array of regex matches. The
-// regex must use the suffix /.../g to specify a global search.
-utils.extractor = function (regex) {
- return function (text) {
- return utils.capture(text, regex);
- };
-};
-
-// __utils.splitLines(string)__.
-// Splits a string into an array of strings, each corresponding to a line.
-utils.splitLines = function (string) {
- return string.split(/\r?\n/);
-};
-
-// __utils.splitAtSpaces(string)__.
-// Splits a string into chunks between spaces. Does not split at spaces
-// inside pairs of quotation marks.
-utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);
-
-// __utils.splitAtFirst(string, regex)__.
-// Splits a string at the first instance of regex match. If no match is
-// found, returns the whole string.
-utils.splitAtFirst = function (string, regex) {
- let match = string.match(regex);
- return match
- ? [
- string.substring(0, match.index),
- string.substring(match.index + match[0].length),
- ]
- : string;
-};
-
-// __utils.splitAtEquals(string)__.
-// Splits a string into chunks between equals. Does not split at equals
-// inside pairs of quotation marks.
-utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g);
-
-// __utils.mergeObjects(arrayOfObjects)__.
-// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object.
-// Pure function.
-utils.mergeObjects = function (arrayOfObjects) {
- let result = {};
- for (let obj of arrayOfObjects) {
- for (let key in obj) {
- result[key] = obj[key];
- }
- }
- return result;
-};
-
-// __utils.listMapData(parameterString, listNames)__.
-// Takes a list of parameters separated by spaces, of which the first several are
-// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames
-// to the unnamed parameters, and combine them in a map with the named parameters.
-// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH`
-//
-// utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH",
-// ["streamID", "event", "circuitID", "IP"])
-// // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0",
-// // "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}"
-utils.listMapData = function (parameterString, listNames) {
- // Split out the space-delimited parameters.
- let parameters = utils.splitAtSpaces(parameterString),
- dataMap = {};
- // Assign listNames to the first n = listNames.length parameters.
- for (let i = 0; i < listNames.length; ++i) {
- dataMap[listNames[i]] = parameters[i];
- }
- // Read key-value pairs and copy these to the dataMap.
- for (let i = listNames.length; i < parameters.length; ++i) {
- let [key, value] = utils.splitAtEquals(parameters[i]);
- if (key && value) {
- dataMap[key] = value;
- }
- }
- return dataMap;
-};
-
-// ## info
-// A namespace for functions related to tor's GETINFO and GETCONF command.
-let info = {};
-
-// __info.keyValueStringsFromMessage(messageText)__.
-// Takes a message (text) response to GETINFO or GETCONF and provides
-// a series of key-value strings, which are either multiline (with a `250+` prefix):
-//
-// 250+config/defaults=
-// AccountingMax "0 bytes"
-// AllowDotExit "0"
-// .
-//
-// or single-line (with a `250-` or `250 ` prefix):
-//
-// 250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
-info.keyValueStringsFromMessage = utils.extractor(
- /^(250\+[\s\S]+?^\.|250[- ].+?)$/gim
-);
-
-// __info.applyPerLine(transformFunction)__.
-// Returns a function that splits text into lines,
-// and applies transformFunction to each line.
-info.applyPerLine = function (transformFunction) {
- return function (text) {
- return utils.splitLines(text.trim()).map(transformFunction);
- };
-};
-
-// __info.routerStatusParser(valueString)__.
-// Parses a router status entry as, described in
-// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
-// (search for "router status entry")
-info.routerStatusParser = function (valueString) {
- let lines = utils.splitLines(valueString),
- objects = [];
- for (let line of lines) {
- // Drop first character and grab data following it.
- let myData = line.substring(2),
- // Accumulate more maps with data, depending on the first character in the line.
- dataFun = {
- r: data =>
- utils.listMapData(data, [
- "nickname",
- "identity",
- "digest",
- "publicationDate",
- "publicationTime",
- "IP",
- "ORPort",
- "DirPort",
- ]),
- a: data => ({ IPv6: data }),
- s: data => ({ statusFlags: utils.splitAtSpaces(data) }),
- v: data => ({ version: data }),
- w: data => utils.listMapData(data, []),
- p: data => ({ portList: data.split(",") }),
- }[line.charAt(0)];
- if (dataFun !== undefined) {
- objects.push(dataFun(myData));
- }
- }
- return utils.mergeObjects(objects);
-};
-
-// __info.circuitStatusParser(line)__.
-// Parse the output of a circuit status line.
-info.circuitStatusParser = function (line) {
- let data = utils.listMapData(line, ["id", "status", "circuit"]),
- circuit = data.circuit;
- // Parse out the individual circuit IDs and names.
- if (circuit) {
- data.circuit = circuit.split(",").map(function (x) {
- return x.split(/~|=/);
- });
- }
- return data;
-};
-
-// __info.streamStatusParser(line)__.
-// Parse the output of a stream status line.
-info.streamStatusParser = function (text) {
- return utils.listMapData(text, [
- "StreamID",
- "StreamStatus",
- "CircuitID",
- "Target",
- ]);
-};
-
-// TODO: fix this parsing logic to handle bridgeLine correctly
-// fingerprint/id is an optional parameter
-// __info.bridgeParser(bridgeLine)__.
-// Takes a single line from a `getconf bridge` result and returns
-// a map containing the bridge's type, address, and ID.
-info.bridgeParser = function (bridgeLine) {
- let result = {},
- tokens = bridgeLine.split(/\s+/);
- // First check if we have a "vanilla" bridge:
- if (tokens[0].match(/^\d+\.\d+\.\d+\.\d+/)) {
- result.type = "vanilla";
- [result.address, result.ID] = tokens;
- // Several bridge types have a similar format:
- } else {
- result.type = tokens[0];
- if (
- [
- "flashproxy",
- "fte",
- "meek",
- "meek_lite",
- "obfs3",
- "obfs4",
- "scramblesuit",
- "snowflake",
- ].includes(result.type)
- ) {
- [result.address, result.ID] = tokens.slice(1);
- }
- }
- return result.type ? result : null;
-};
-
-// __info.parsers__.
-// A map of GETINFO and GETCONF keys to parsing function, which convert
-// result strings to JavaScript data.
-info.parsers = {
- "ns/id/": info.routerStatusParser,
- "ip-to-country/": utils.identity,
- "circuit-status": info.applyPerLine(info.circuitStatusParser),
- bridge: info.bridgeParser,
- // Currently unused parsers:
- // "ns/name/" : info.routerStatusParser,
- // "stream-status" : info.applyPerLine(info.streamStatusParser),
- // "version" : utils.identity,
- // "config-file" : utils.identity,
-};
-
-// __info.getParser(key)__.
-// Takes a key and determines the parser function that should be used to
-// convert its corresponding valueString to JavaScript data.
-info.getParser = function (key) {
- return (
- info.parsers[key] ||
- info.parsers[key.substring(0, key.lastIndexOf("/") + 1)]
- );
-};
-
-// __info.stringToValue(string)__.
-// Converts a key-value string as from GETINFO or GETCONF to a value.
-info.stringToValue = function (string) {
- // key should look something like `250+circuit-status=` or `250-circuit-status=...`
- // or `250 circuit-status=...`
- let matchForKey = string.match(/^250[ +-](.+?)=/),
- key = matchForKey ? matchForKey[1] : null;
- if (key === null) {
- return null;
- }
- // matchResult finds a single-line result for `250-` or `250 `,
- // or a multi-line one for `250+`.
- let matchResult =
- string.match(/^250[ -].+?=(.*)$/) ||
- string.match(/^250\+.+?=([\s\S]*?)^\.$/m),
- // Retrieve the captured group (the text of the value in the key-value pair)
- valueString = matchResult ? matchResult[1] : null,
- // Get the parser function for the key found.
- parse = info.getParser(key.toLowerCase());
- if (parse === undefined) {
- throw new Error("No parser found for '" + key + "'");
- }
- // Return value produced by the parser.
- return parse(valueString);
-};
-
-/**
- * @typedef {object} Bridge
- * @property {string} transport The transport of the bridge, or vanilla if not
- * specified.
- * @property {string} addr The IP address and port of the bridge
- * @property {string} id The fingerprint of the bridge
- * @property {string} args Optional arguments passed to the bridge
- */
-/**
- * @typedef {object} PTInfo The information about a pluggable transport
- * @property {string[]} transports An array with all the transports supported by
- * this configuration.
- * @property {string} type Either socks4, socks5 or exec
- * @property {string} [ip] The IP address of the proxy (only for socks4 and
- * socks5)
- * @property {integer} [port] The port of the proxy (only for socks4 and socks5)
- * @property {string} [pathToBinary] Path to the binary that is run (only for
- * exec)
- * @property {string} [options] Optional options passed to the binary (only for
- * exec)
- */
-/**
- * @typedef {object} OnionAuthKeyInfo
- * @property {string} address The address of the onion service
- * @property {string} typeAndKey Onion service key and type of key, as
- * `type:base64-private-key`
- * @property {string} Flags Additional flags, such as Permanent
- */
-/**
- * @callback EventFilterCallback
- * @param {any} data Either a raw string, or already parsed data
- * @returns {boolean}
- */
-/**
- * @callback EventCallback
- * @param {any} data Either a raw string, or already parsed data
- */
-
class TorController {
/**
* The control socket
@@ -905,16 +630,6 @@ class TorController {
*/
#socket;
- /**
- * A map of EVENT keys to parsing functions, which convert result strings to
- * JavaScript data.
- */
- #eventParsers = {
- stream: info.streamStatusParser,
- // Currently unused:
- // "circ" : info.circuitStatusParser,
- };
-
/**
* Builds a new TorController.
*
@@ -981,18 +696,6 @@ class TorController {
await this.#sendCommandSimple(`authenticate ${password || ""}`);
}
- /**
- * Sends a GETINFO for a single key.
- *
- * @param {string} key The key to get value for
- * @returns {any} The return value depends on the requested key
- */
- async getInfo(key) {
- this.#expectString(key, "key");
- const response = await this.sendCommand(`getinfo ${key}`);
- return this.#getMultipleResponseValues(response)[0];
- }
-
/**
* Sends a GETINFO for a single key.
* control-spec.txt says "one ReplyLine is sent for each requested value", so,
@@ -1054,9 +757,7 @@ class TorController {
const addresses = [v4[5]];
// a address:port
// dir-spec.txt also states only the first one should be taken
- // TODO: The consumers do not care about the port or the square brackets
- // either. Remove them when integrating this function with the rest
- const v6 = reply.match(/^a\s+(\[[0-9a-fA-F:]+\]:[0-9]{1,5})$/m);
+ const v6 = reply.match(/^a\s+\[([0-9a-fA-F:]+)\]:\d{1,5}$/m);
if (v6) {
addresses.push(v6[1]);
}
@@ -1091,23 +792,6 @@ class TorController {
// Configuration
- /**
- * Sends a GETCONF for a single key.
- * GETCONF with a single argument returns results with one or more lines that
- * look like `250[- ]key=value`.
- * Any GETCONF lines that contain a single keyword only are currently dropped.
- * So we can use similar parsing to that for getInfo.
- *
- * @param {string} key The key to get value for
- * @returns {any} A parsed config value (it depends if a parser is known)
- */
- async getConf(key) {
- this.#expectString(key, "key");
- return this.#getMultipleResponseValues(
- await this.sendCommand(`getconf ${key}`)
- );
- }
-
/**
* Sends a GETCONF for a single key.
* The function could be easily generalized to get multiple keys at once, but
@@ -1264,12 +948,14 @@ class TorController {
// TODO: Change the consumer and make the fields more consistent with what
// we get (e.g., separate key and type, and use a boolen for permanent).
const info = {
- hsAddress: match.groups.HSAddress,
- typeAndKey: `${match.groups.KeyType}:${match.groups.PrivateKeyBlob}`,
+ address: match.groups.HSAddress,
+ keyType: match.groups.KeyType,
+ keyBlob: match.groups.PrivateKeyBlob,
+ flags: [],
};
const maybeFlags = match.groups.other?.match(/Flags=(\S+)/);
if (maybeFlags) {
- info.Flags = maybeFlags[1];
+ info.flags = maybeFlags[1].split(",");
}
return info;
});
@@ -1369,28 +1055,12 @@ class TorController {
* first.
*
* @param {string} type The event type to catch
- * @param {EventFilterCallback?} filter An optional callback to filter
- * events for which the callback will be called. If null, all events will be
- * passed.
* @param {EventCallback} callback The callback that will handle the event
- * @param {boolean} raw Tell whether to ignore the data parser, even if
- * supported
*/
- watchEvent(type, filter, callback, raw = false) {
+ watchEvent(type, callback) {
this.#expectString(type, "type");
const start = `650 ${type}`;
- this.#socket.addNotificationCallback(new RegExp(`^${start}`), message => {
- // Remove also the initial text
- const dataText = message.substring(start.length + 1);
- const parser = this.#eventParsers[type.toLowerCase()];
- const data = dataText && parser ? parser(dataText) : null;
- // FIXME: This is the original code, but we risk of not filtering on the
- // data, if we ask for raw data (which we always do at the moment, but we
- // do not use a filter either...)
- if (filter === null || filter(data)) {
- callback(data && !raw ? data : message);
- }
- });
+ this.#socket.addNotificationCallback(new RegExp(`^${start}`), callback);
}
// Other helpers
@@ -1453,19 +1123,6 @@ class TorController {
)
);
}
-
- /**
- * Process multiple responses to a GETINFO or GETCONF request.
- *
- * @param {string} message The message to process
- * @returns {object[]} The keys depend on the message
- */
- #getMultipleResponseValues(message) {
- return info
- .keyValueStringsFromMessage(message)
- .map(info.stringToValue)
- .filter(x => x);
- }
}
const controlPortInfo = {};
=====================================
toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
=====================================
@@ -12,6 +12,11 @@ import {
const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+});
+
XPCOMUtils.defineLazyServiceGetters(lazy, {
ProtocolProxyService: [
"@mozilla.org/network/protocol-proxy-service;1",
@@ -19,11 +24,6 @@ XPCOMUtils.defineLazyServiceGetters(lazy, {
],
});
-ChromeUtils.defineESModuleGetters(lazy, {
- TorMonitorTopics: "resource://gre/modules/TorMonitorService.sys.mjs",
- TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
-});
-
const logger = new ConsoleAPI({
prefix: "TorDomainIsolator",
maxLogLevel: "warn",
@@ -143,7 +143,7 @@ class TorDomainIsolatorImpl {
Services.prefs.addObserver(NON_TOR_PROXY_PREF, this);
Services.obs.addObserver(this, NEW_IDENTITY_TOPIC);
- Services.obs.addObserver(this, lazy.TorMonitorTopics.StreamSucceeded);
+ Services.obs.addObserver(this, lazy.TorProviderTopics.StreamSucceeded);
this.#cleanupIntervalId = setInterval(
this.#clearKnownCircuits.bind(this),
@@ -158,7 +158,7 @@ class TorDomainIsolatorImpl {
uninit() {
Services.prefs.removeObserver(NON_TOR_PROXY_PREF, this);
Services.obs.removeObserver(this, NEW_IDENTITY_TOPIC);
- Services.obs.removeObserver(this, lazy.TorMonitorTopics.StreamSucceeded);
+ Services.obs.removeObserver(this, lazy.TorProviderTopics.StreamSucceeded);
clearInterval(this.#cleanupIntervalId);
this.#cleanupIntervalId = null;
this.clearIsolation();
@@ -257,12 +257,12 @@ class TorDomainIsolatorImpl {
);
this.clearIsolation();
try {
- await lazy.TorProtocolService.newnym();
+ await lazy.TorProviderBuilder.build().newnym();
} catch (e) {
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) {
+ } else if (topic === lazy.TorProviderTopics.StreamSucceeded) {
const { username, password, circuit } = subject.wrappedJSObject;
this.#updateCircuit(username, password, circuit);
}
@@ -553,7 +553,7 @@ class TorDomainIsolatorImpl {
data = await Promise.all(
circuit.map(fingerprint =>
- lazy.TorProtocolService.getNodeInfo(fingerprint)
+ lazy.TorProviderBuilder.build().getNodeInfo(fingerprint)
)
);
this.#knownCircuits.set(id, data);
=====================================
toolkit/components/tor-launcher/TorMonitorService.sys.mjs deleted
=====================================
@@ -1,42 +0,0 @@
-// Copyright (c) 2022, The Tor Project, Inc.
-
-import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs";
-
-const lazy = {};
-ChromeUtils.defineESModuleGetters(lazy, {
- TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
-});
-
-export const TorMonitorTopics = Object.freeze({
- BridgeChanged: TorProviderTopics.BridgeChanged,
- StreamSucceeded: TorProviderTopics.StreamSucceeded,
-});
-
-/**
- * This service monitors an existing Tor instance, or starts one, if needed, and
- * then starts monitoring it.
- *
- * This is the service which should be queried to know information about the
- * status of the bootstrap, the logs, etc...
- */
-export const TorMonitorService = {
- get currentBridge() {
- return lazy.TorProtocolService.currentBridge;
- },
-
- get ownsTorDaemon() {
- return lazy.TorProtocolService.ownsTorDaemon;
- },
-
- get isRunning() {
- return lazy.TorProtocolService.isRunning;
- },
-
- get isBootstrapDone() {
- return lazy.TorProtocolService.isBootstrapDone;
- },
-
- getLog() {
- return lazy.TorProtocolService.getLog();
- },
-};
=====================================
toolkit/components/tor-launcher/TorParsers.sys.mjs
=====================================
@@ -269,11 +269,14 @@ export const TorParsers = Object.freeze({
},
parseBridgeLine(line) {
+ if (!line) {
+ return null;
+ }
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.");
+ throw new Error(`Invalid bridge line: ${line}.`);
}
const bridge = match.groups;
if (!bridge.transport) {
=====================================
toolkit/components/tor-launcher/TorProtocolService.sys.mjs → toolkit/components/tor-launcher/TorProvider.sys.mjs
=====================================
@@ -1,4 +1,6 @@
-// Copyright (c) 2021, The Tor Project, Inc.
+/* 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, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
@@ -11,7 +13,6 @@ import {
import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs";
const lazy = {};
-
ChromeUtils.defineESModuleGetters(lazy, {
controller: "resource://gre/modules/TorControlPort.sys.mjs",
configureControlPortModule: "resource://gre/modules/TorControlPort.sys.mjs",
@@ -21,7 +22,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
const logger = new ConsoleAPI({
maxLogLevel: "warn",
- prefix: "TorProtocolService",
+ maxLogLevelPref: "browser.tor_provider.log_level",
+ prefix: "TorProvider",
});
/**
@@ -70,7 +72,7 @@ const ControlConnTimings = Object.freeze({
* It can start a new tor instance, or connect to an existing one.
* In the former case, it also takes its ownership by default.
*/
-class TorProvider {
+export class TorProvider {
#inited = false;
// Maintain a map of tor settings set by Tor Browser so that we don't
@@ -85,7 +87,6 @@ class TorProvider {
#SOCKSPortInfo = null; // An object that contains ipcFile, host, port.
#controlConnection = null; // This is cached and reused.
- #connectionQueue = [];
// Public methods
@@ -123,39 +124,34 @@ class TorProvider {
// takes a Map containing tor settings
// throws on error
async writeSettings(aSettingsObj) {
+ const entries =
+ aSettingsObj instanceof Map
+ ? Array.from(aSettingsObj.entries())
+ : Object.entries(aSettingsObj);
// only write settings that have changed
- const newSettings = Array.from(aSettingsObj).filter(([setting, value]) => {
- // make sure we have valid data here
- this.#assertValidSetting(setting, value);
-
+ const newSettings = entries.filter(([setting, value]) => {
if (!this.#settingsCache.has(setting)) {
// no cached setting, so write
return true;
}
const cachedValue = this.#settingsCache.get(setting);
- if (value === cachedValue) {
- return false;
- } else if (Array.isArray(value) && Array.isArray(cachedValue)) {
- // compare arrays member-wise
- if (value.length !== cachedValue.length) {
- return true;
- }
- for (let i = 0; i < value.length; i++) {
- if (value[i] !== cachedValue[i]) {
- return true;
- }
- }
- return false;
+ // Arrays are the only special case for which === could fail.
+ // The other values we accept (strings, booleans, numbers, null and
+ // undefined) work correctly with ===.
+ if (Array.isArray(value) && Array.isArray(cachedValue)) {
+ return (
+ value.length !== cachedValue.length ||
+ value.some((val, idx) => val !== cachedValue[idx])
+ );
}
- // some other different values
- return true;
+ return value !== cachedValue;
});
// only write if new setting to save
if (newSettings.length) {
- const settingsObject = Object.fromEntries(newSettings);
- await this.setConfWithReply(settingsObject);
+ const conn = await this.#getConnection();
+ await conn.setConf(Object.fromEntries(newSettings));
// save settings to cache after successfully writing to Tor
for (const [setting, value] of newSettings) {
@@ -164,23 +160,15 @@ class TorProvider {
}
}
- async readStringArraySetting(aSetting) {
- const value = await this.#readSetting(aSetting);
- this.#settingsCache.set(aSetting, value);
- return value;
- }
-
// writes current tor settings to disk
async flushSettings() {
- await this.sendCommand("SAVECONF");
+ const conn = await this.#getConnection();
+ await conn.flushSettings();
}
async connect() {
- const kTorConfKeyDisableNetwork = "DisableNetwork";
- const settings = {};
- settings[kTorConfKeyDisableNetwork] = false;
- await this.setConfWithReply(settings);
- await this.sendCommand("SAVECONF");
+ const conn = await this.#getConnection();
+ await conn.setNetworkEnabled(true);
this.clearBootstrapError();
this.retrieveBootstrapStatus();
}
@@ -188,12 +176,8 @@ class TorProvider {
async stopBootstrap() {
// Tell tor to disable use of the network; this should stop the bootstrap
// process.
- try {
- const settings = { DisableNetwork: true };
- await this.setConfWithReply(settings);
- } catch (e) {
- logger.error("Error stopping bootstrap", e);
- }
+ const conn = await this.#getConnection();
+ await conn.setNetworkEnabled(false);
// We are not interested in waiting for this, nor in **catching its error**,
// so we do not await this. We just want to be notified when the bootstrap
// status is actually updated through observers.
@@ -201,28 +185,31 @@ class TorProvider {
}
async newnym() {
- return this.sendCommand("SIGNAL NEWNYM");
+ const conn = await this.#getConnection();
+ await conn.newnym();
}
// Ask tor which ports it is listening to for SOCKS connections.
// At the moment this is used only in TorCheckService.
async getSocksListeners() {
- const cmd = "GETINFO";
- const keyword = "net/listeners/socks";
- const response = await this.sendCommand(cmd, keyword);
- return TorParsers.parseReply(cmd, keyword, response);
+ const conn = await this.#getConnection();
+ return conn.getSocksListeners();
}
async getBridges() {
+ const conn = await this.#getConnection();
// 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");
- });
+ return conn.getBridges();
+ }
+
+ async getPluggableTransports() {
+ const conn = await this.#getConnection();
+ return conn.getPluggableTransports();
}
/**
@@ -232,68 +219,55 @@ class TorProvider {
* @returns {Promise<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);
- }
+ const conn = await this.#getConnection();
+ const node = {
+ fingerprint: id,
+ ipAddrs: [],
+ bridgeType: null,
+ regionCode: null,
+ };
+ const bridge = (await conn.getBridges())?.find(
+ foundBridge => foundBridge.id?.toUpperCase() === id.toUpperCase()
+ );
+ if (bridge) {
+ node.bridgeType = bridge.transport ?? "";
+ // Attempt to get an IP address from bridge address string.
+ const ip = bridge.addr.match(/^\[?([^\]]+)\]?:\d+$/)?.[1];
+ if (ip && !ip.startsWith("0.")) {
+ node.ipAddrs.push(ip);
}
- 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 {}
+ } else {
+ node.ipAddrs = await conn.getNodeAddresses(id);
+ }
+ if (node.ipAddrs.length) {
+ // Get the country code for the node's IP address.
+ try {
+ // Expect a 2-letter ISO3166-1 code, which should also be a valid
+ // BCP47 Region subtag.
+ const regionCode = await conn.getIPCountry(node.ipAddrs[0]);
if (regionCode && regionCode !== "??") {
node.regionCode = regionCode.toUpperCase();
}
+ } catch (e) {
+ logger.warn(`Cannot get a country for IP ${node.ipAddrs[0]}`, e);
}
- return node;
- });
+ }
+ return node;
}
- async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) {
- return this.#withConnection(conn => {
- return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent);
- });
+ async onionAuthAdd(address, b64PrivateKey, isPermanent) {
+ const conn = await this.#getConnection();
+ return conn.onionAuthAdd(address, b64PrivateKey, isPermanent);
}
- async onionAuthRemove(hsAddress) {
- return this.#withConnection(conn => {
- return conn.onionAuthRemove(hsAddress);
- });
+ async onionAuthRemove(address) {
+ const conn = await this.#getConnection();
+ return conn.onionAuthRemove(address);
}
async onionAuthViewKeys() {
- return this.#withConnection(conn => {
- return conn.onionAuthViewKeys();
- });
+ const conn = await this.#getConnection();
+ return conn.onionAuthViewKeys();
}
// TODO: transform the following 4 functions in getters.
@@ -333,106 +307,6 @@ class TorProvider {
return this.#SOCKSPortInfo;
}
- // Public, but called only internally
-
- // Executes a command on the control port.
- // Return a reply object or null if a fatal error occurs.
- async sendCommand(cmd, args) {
- const maxTimeout = 1000;
- let leftConnAttempts = 5;
- let timeout = 250;
- let reply;
- while (leftConnAttempts-- > 0) {
- const response = await this.#trySend(cmd, args, leftConnAttempts === 0);
- if (response.connected) {
- reply = response.reply;
- break;
- }
- // We failed to acquire the controller after multiple attempts.
- // Try again after some time.
- logger.warn(
- "sendCommand: Acquiring control connection failed, trying again later.",
- cmd,
- args
- );
- await new Promise(resolve => setTimeout(() => resolve(), timeout));
- timeout = Math.min(2 * timeout, maxTimeout);
- }
-
- // We sent the command, but we still got an empty response.
- // Something must be busted elsewhere.
- if (!reply) {
- throw new Error(`${cmd} sent an empty response`);
- }
-
- // TODO: Move the parsing of the reply to the controller, because anyone
- // calling sendCommand on it actually wants a parsed reply.
-
- reply = TorParsers.parseCommandResponse(reply);
- if (!TorParsers.commandSucceeded(reply)) {
- if (reply?.lineArray) {
- throw new Error(reply.lineArray.join("\n"));
- }
- throw new Error(`${cmd} failed with code ${reply.statusCode}`);
- }
-
- return reply;
- }
-
- // Perform a SETCONF command.
- // aSettingsObj should be a JavaScript object with keys (property values)
- // that correspond to tor config. keys. The value associated with each
- // key should be a simple string, a string array, or a Boolean value.
- // If an associated value is undefined or null, a key with no value is
- // passed in the SETCONF command.
- // Throws in case of error, or returns a reply object.
- async setConfWithReply(settings) {
- if (!settings) {
- throw new Error("Empty settings object");
- }
- const args = Object.entries(settings)
- .map(([key, val]) => {
- if (val === undefined || val === null) {
- return key;
- }
- const valType = typeof val;
- let rv = `${key}=`;
- if (valType === "boolean") {
- rv += val ? "1" : "0";
- } else if (Array.isArray(val)) {
- rv += val.map(TorParsers.escapeString).join(` ${key}=`);
- } else if (valType === "string") {
- rv += TorParsers.escapeString(val);
- } else {
- logger.error(`Got unsupported type for ${key}`, val);
- throw new Error(`Unsupported type ${valType} (key ${key})`);
- }
- return rv;
- })
- .filter(arg => arg);
- if (!args.length) {
- throw new Error("No settings to set");
- }
-
- await this.sendCommand("SETCONF", args.join(" "));
- }
-
- // Public, never called?
-
- async readBoolSetting(aSetting) {
- let value = await this.#readBoolSetting(aSetting);
- this.#settingsCache.set(aSetting, value);
- return value;
- }
-
- async readStringSetting(aSetting) {
- let value = await this.#readStringSetting(aSetting);
- this.#settingsCache.set(aSetting, value);
- return value;
- }
-
- // Private
-
async #setSockets() {
try {
const isWindows = TorLauncherUtil.isWindows;
@@ -511,167 +385,24 @@ class TorProvider {
}
}
- #assertValidSettingKey(aSetting) {
- // ensure the 'key' is a string
- if (typeof aSetting !== "string") {
- throw new Error(
- `Expected setting of type string but received ${typeof aSetting}`
- );
- }
- }
-
- #assertValidSetting(aSetting, aValue) {
- this.#assertValidSettingKey(aSetting);
- switch (typeof aValue) {
- case "boolean":
- case "string":
- return;
- case "object":
- if (aValue === null) {
- return;
- } else if (Array.isArray(aValue)) {
- for (const element of aValue) {
- if (typeof element !== "string") {
- throw new Error(
- `Setting '${aSetting}' array contains value of invalid type '${typeof element}'`
- );
- }
- }
- return;
- }
- // fall through
- default:
- throw new Error(
- `Invalid object type received for setting '${aSetting}'`
- );
- }
- }
-
- // Perform a GETCONF command.
- async #readSetting(aSetting) {
- this.#assertValidSettingKey(aSetting);
-
- const cmd = "GETCONF";
- let reply = await this.sendCommand(cmd, aSetting);
- return TorParsers.parseReply(cmd, aSetting, reply);
- }
-
- async #readStringSetting(aSetting) {
- let lineArray = await this.#readSetting(aSetting);
- if (lineArray.length !== 1) {
- throw new Error(
- `Expected an array with length 1 but received array of length ${lineArray.length}`
- );
- }
- return lineArray[0];
- }
-
- async #readBoolSetting(aSetting) {
- const value = this.#readStringSetting(aSetting);
- switch (value) {
- case "0":
- return false;
- case "1":
- return true;
- default:
- throw new Error(`Expected boolean (1 or 0) but received '${value}'`);
- }
- }
-
- async #trySend(cmd, args, rethrow) {
- let connected = false;
- let reply;
- let leftAttempts = 2;
- while (leftAttempts-- > 0) {
- let conn;
- try {
- conn = await this.#getConnection();
- } catch (e) {
- logger.error("Cannot get a connection to the control port", e);
- if (leftAttempts == 0 && rethrow) {
- throw e;
- }
- }
- if (!conn) {
- continue;
- }
- // If we _ever_ got a connection, the caller should not try again
- connected = true;
- try {
- reply = await conn.sendCommand(cmd + (args ? " " + args : ""));
- if (reply) {
- // Return for reuse.
- this.#returnConnection();
- } else {
- // Connection is bad.
- logger.warn(
- "sendCommand returned an empty response, taking the connection as broken and closing it."
- );
- this.#closeConnection();
- }
- } catch (e) {
- logger.error(`Cannot send the command ${cmd}`, e);
- this.#closeConnection();
- if (leftAttempts == 0 && rethrow) {
- throw e;
- }
- }
- }
- return { connected, reply };
- }
-
- // Opens an authenticated connection, sets it to this.#controlConnection, and
- // return it.
async #getConnection() {
- if (!this.#controlConnection) {
+ if (!this.#controlConnection?.isOpen) {
this.#controlConnection = await lazy.controller();
}
- if (this.#controlConnection.inUse) {
- await new Promise((resolve, reject) =>
- this.#connectionQueue.push({ resolve, reject })
- );
- } else {
- this.#controlConnection.inUse = true;
- }
return this.#controlConnection;
}
- #returnConnection() {
- if (this.#connectionQueue.length) {
- this.#connectionQueue.shift().resolve();
- } else {
- this.#controlConnection.inUse = false;
- }
- }
-
- 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) {
logger.info("Closing the control connection");
this.#controlConnection.close();
this.#controlConnection = null;
}
- for (const promise of this.#connectionQueue) {
- promise.reject("Connection closed");
- }
- this.#connectionQueue = [];
}
async #reconnect() {
this.#closeConnection();
- const conn = await this.#getConnection();
- logger.debug("Reconnected to the control port.");
- this.#returnConnection(conn);
+ await this.#getConnection();
}
async #readAuthenticationCookie(aPath) {
@@ -777,8 +508,9 @@ class TorProvider {
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(
+ "STATUS_CLIENT",
+ this._processStatusClient.bind(this)
);
this._eventHandlers.set("NOTICE", this._processLog.bind(this));
this._eventHandlers.set("WARN", this._processLog.bind(this));
@@ -809,23 +541,10 @@ class TorProvider {
throw new Error("Event monitor connection not available");
}
- // TODO: Unify with TorProtocolService.sendCommand and put everything in the
- // reviewed torbutton replacement.
- const cmd = "GETINFO";
- const key = "status/bootstrap-phase";
- let reply = await this._connection.sendCommand(`${cmd} ${key}`);
-
- // A typical reply looks like:
- // 250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
- // 250 OK
- reply = TorParsers.parseCommandResponse(reply);
- if (!TorParsers.commandSucceeded(reply)) {
- throw new Error(`${cmd} failed`);
- }
- reply = TorParsers.parseReply(cmd, key, reply);
- if (reply.length) {
- this._processBootstrapStatus(reply[0], true);
- }
+ this._processBootstrapStatus(
+ await this._connection.getBootstrapPhase(),
+ true
+ );
}
// Returns captured log message as a text string (one message per line).
@@ -1058,37 +777,32 @@ class TorProvider {
_monitorEvent(type, callback) {
logger.info(`Watching events of type ${type}.`);
let replyObj = {};
- 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
- );
+ this._connection.watchEvent(type, 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);
+ }
+ });
}
_processLog(type, lines) {
@@ -1116,15 +830,12 @@ class TorProvider {
// to TorBootstrapStatus observers.
// If aSuppressErrors is true, errors are ignored. This is used when we
// are handling the response to a "GETINFO status/bootstrap-phase" command.
- _processBootstrapStatus(aStatusMsg, aSuppressErrors) {
- const statusObj = TorParsers.parseBootstrapStatus(aStatusMsg);
- if (!statusObj) {
- return;
- }
-
+ _processBootstrapStatus(statusObj, suppressErrors) {
// Notify observers
- statusObj.wrappedJSObject = statusObj;
- Services.obs.notifyObservers(statusObj, "TorBootstrapStatus");
+ Services.obs.notifyObservers(
+ { wrappedJSObject: statusObj },
+ "TorBootstrapStatus"
+ );
if (statusObj.PROGRESS === 100) {
this._isBootstrapDone = true;
@@ -1141,7 +852,7 @@ class TorProvider {
if (
statusObj.TYPE === "WARN" &&
statusObj.RECOMMENDATION !== "ignore" &&
- !aSuppressErrors
+ !suppressErrors
) {
this._notifyBootstrapError(statusObj);
}
@@ -1184,6 +895,15 @@ class TorProvider {
}
}
+ _processStatusClient(_type, lines) {
+ const statusObj = TorParsers.parseBootstrapStatus(lines[0]);
+ if (!statusObj) {
+ // No `BOOTSTRAP` in the line
+ return;
+ }
+ this._processBootstrapStatus(statusObj, false);
+ }
+
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(
@@ -1295,7 +1015,3 @@ class TorProvider {
this.clearBootstrapError();
}
}
-
-// TODO: Stop defining TorProtocolService, make the builder instance the
-// TorProvider.
-export const TorProtocolService = new TorProvider();
=====================================
toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
=====================================
@@ -4,7 +4,7 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
+ TorProvider: "resource://gre/modules/TorProvider.sys.mjs",
});
export const TorProviderTopics = Object.freeze({
@@ -19,16 +19,25 @@ export const TorProviderTopics = Object.freeze({
});
export class TorProviderBuilder {
+ static #provider = null;
+
static async init() {
- await lazy.TorProtocolService.init();
+ const provider = new lazy.TorProvider();
+ await provider.init();
+ // Assign it only when initialization succeeds.
+ TorProviderBuilder.#provider = provider;
}
static uninit() {
- lazy.TorProtocolService.uninit();
+ TorProviderBuilder.#provider.uninit();
+ TorProviderBuilder.#provider = null;
}
// TODO: Switch to an async build?
static build() {
- return lazy.TorProtocolService;
+ if (!TorProviderBuilder.#provider) {
+ throw new Error("TorProviderBuilder has not been initialized yet.");
+ }
+ return TorProviderBuilder.#provider;
}
}
=====================================
toolkit/components/tor-launcher/TorStartupService.sys.mjs
=====================================
@@ -3,22 +3,13 @@ const lazy = {};
// We will use the modules only when the profile is loaded, so prefer lazy
// loading
ChromeUtils.defineESModuleGetters(lazy, {
+ TorConnect: "resource:///modules/TorConnect.sys.mjs",
TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorSettings: "resource:///modules/TorSettings.sys.mjs",
});
-ChromeUtils.defineModuleGetter(
- lazy,
- "TorConnect",
- "resource:///modules/TorConnect.jsm"
-);
-ChromeUtils.defineModuleGetter(
- lazy,
- "TorSettings",
- "resource:///modules/TorSettings.jsm"
-);
-
/* Browser observer topis */
const BrowserTopics = Object.freeze({
ProfileAfterChange: "profile-after-change",
=====================================
toolkit/components/tor-launcher/moz.build
=====================================
@@ -3,10 +3,9 @@ EXTRA_JS_MODULES += [
"TorControlPort.sys.mjs",
"TorDomainIsolator.sys.mjs",
"TorLauncherUtil.sys.mjs",
- "TorMonitorService.sys.mjs",
"TorParsers.sys.mjs",
"TorProcess.sys.mjs",
- "TorProtocolService.sys.mjs",
+ "TorProvider.sys.mjs",
"TorProviderBuilder.sys.mjs",
"TorStartupService.sys.mjs",
]
=====================================
toolkit/mozapps/update/UpdateService.sys.mjs
=====================================
@@ -23,18 +23,13 @@ ChromeUtils.defineESModuleGetters(lazy, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
CertUtils: "resource://gre/modules/CertUtils.sys.mjs",
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
ctypes: "resource://gre/modules/ctypes.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
-ChromeUtils.defineModuleGetter(
- lazy,
- "TorMonitorService",
- "resource://gre/modules/TorMonitorService.jsm"
-);
-
XPCOMUtils.defineLazyServiceGetter(
lazy,
"AUS",
@@ -394,10 +389,11 @@ XPCOMUtils.defineLazyGetter(
);
function _shouldRegisterBootstrapObserver(errorCode) {
+ const provider = lazy.TorProviderBuilder.build();
return (
errorCode == PROXY_SERVER_CONNECTION_REFUSED &&
- !lazy.TorMonitorService.isBootstrapDone &&
- lazy.TorMonitorService.ownsTorDaemon
+ !provider.isBootstrapDone &&
+ provider.ownsTorDaemon
);
}
@@ -5833,10 +5829,7 @@ Downloader.prototype = {
// we choose to compute these hashes.
hash = hash.finish(false);
digest = Array.from(hash, (c, i) =>
- hash
- .charCodeAt(i)
- .toString(16)
- .padStart(2, "0")
+ hash.charCodeAt(i).toString(16).padStart(2, "0")
).join("");
} catch (e) {
LOG(
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/ab8a15…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/ab8a15…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser-build][main] 3 commits: Bug 40855: Update toolchains for Firefox 115 (Android part)
by Pier Angelo Vendrame (@pierov) 07 Aug '23
by Pier Angelo Vendrame (@pierov) 07 Aug '23
07 Aug '23
Pier Angelo Vendrame pushed to branch main at The Tor Project / Applications / tor-browser-build
Commits:
a65bfce8 by Pier Angelo Vendrame at 2023-08-07T17:44:54+02:00
Bug 40855: Update toolchains for Firefox 115 (Android part)
Android-only toolchain updates.
- - - - -
301a540c by Pier Angelo Vendrame at 2023-08-07T17:44:56+02:00
Bug 40855: Updates for Firefox 115 (Application Services)
Application services needs NSS and SQLCipher.
We had two projects for them, but they are used only by AS.
So, our build scripts were a copy of Mozilla's, and we applied the same
patches.
This meant we needed to keep the build scripts up to date, with all the
additional changes for RBM.
Since no other project depended on them, we can build these libraries
here with Mozilla's scripts, without the need to keep theirs and ours
in sync.
In addition to that, this commit updates the list of Java dependencies.
- - - - -
b7d80c1d by Pier Angelo Vendrame at 2023-08-07T17:44:56+02:00
Bug 40867: Add a project for firefox-android.
This project replaces Android Components and Fenix.
- - - - -
13 changed files:
- Makefile
- − projects/android-components/build
- − projects/android-components/config
- − projects/android-components/gradle-dependencies-list.txt
- − projects/android-components/list_toolchain_updates_checks
- − projects/android-components/mavenLocal.patch
- projects/android-toolchain/build
- projects/android-toolchain/config
- + projects/application-services/apply-bug-13028.diff
- projects/application-services/bug40485.patch → projects/application-services/bug40485.diff
- projects/nss/bug_13028.patch → projects/application-services/bug_13028.patch
- projects/application-services/build
- projects/application-services/config
The diff was not included because it is too large.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/compare/…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/compare/…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser-build][main] Bug 40880 (fix): Add zstd back again
by Pier Angelo Vendrame (@pierov) 07 Aug '23
by Pier Angelo Vendrame (@pierov) 07 Aug '23
07 Aug '23
Pier Angelo Vendrame pushed to branch main at The Tor Project / Applications / tor-browser-build
Commits:
d6d756ac by Pier Angelo Vendrame at 2023-08-04T08:19:45+02:00
Bug 40880 (fix): Add zstd back again
The previous commit removed zstd from the list of the dependencies in
the README.
This commit restores it and sorts the dependencies.
- - - - -
1 changed file:
- README
Changes:
=====================================
README
=====================================
@@ -39,14 +39,14 @@ You also need a few perl modules installed:
If you are running Debian or Ubuntu, you can install them with:
-# apt-get install libyaml-libyaml-perl libtemplate-perl libdatetime-perl \
- libio-handle-util-perl libio-all-perl \
- libio-captureoutput-perl libjson-perl libpath-tiny-perl \
- libstring-shellquote-perl libsort-versions-perl \
- libdigest-sha-perl libdata-uuid-perl libdata-dump-perl \
- libfile-copy-recursive-perl libfile-slurp-perl git \
- mercurial uidmap libxml-writer-perl \
- libparallel-forkmanager-perl libxml-libxml-perl
+# apt-get install libdata-dump-perl libdata-uuid-perl libdatetime-perl \
+ libdigest-sha-perl libfile-copy-recursive-perl \
+ libfile-slurp-perl libio-all-perl libio-captureoutput-perl \
+ libio-handle-util-perl libjson-perl \
+ libparallel-forkmanager-perl libpath-tiny-perl \
+ libsort-versions-perl libstring-shellquote-perl \
+ libtemplate-perl libxml-libxml-perl libxml-writer-perl \
+ libyaml-libyaml-perl git mercurial uidmap zstd
If you are running an Arch based system, you should be able to install them with:
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.
1
0

[Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] 2 commits: fixup! Bug 40933: Add tor-launcher functionality
by Pier Angelo Vendrame (@pierov) 05 Aug '23
by Pier Angelo Vendrame (@pierov) 05 Aug '23
05 Aug '23
Pier Angelo Vendrame pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
d5283d94 by Pier Angelo Vendrame at 2023-08-04T20:03:25+02:00
fixup! Bug 40933: Add tor-launcher functionality
Make TorProtocolService an ES class, and change _ with actual private
stuff.
- - - - -
ab8a15b7 by Pier Angelo Vendrame at 2023-08-04T20:03:26+02:00
fixup! Bug 40933: Add tor-launcher functionality
Merged TorMonitorService into TorProtocolService.
- - - - -
5 changed files:
- toolkit/components/tor-launcher/TorMonitorService.sys.mjs
- toolkit/components/tor-launcher/TorProtocolService.sys.mjs
- + toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
- toolkit/components/tor-launcher/TorStartupService.sys.mjs
- toolkit/components/tor-launcher/moz.build
Changes:
=====================================
toolkit/components/tor-launcher/TorMonitorService.sys.mjs
=====================================
@@ -1,73 +1,17 @@
// Copyright (c) 2022, The Tor Project, Inc.
-import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
-import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
-
-import {
- TorParsers,
- TorStatuses,
-} from "resource://gre/modules/TorParsers.sys.mjs";
-import { TorProcess } from "resource://gre/modules/TorProcess.sys.mjs";
-
-import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
+import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs";
const lazy = {};
-
-ChromeUtils.defineESModuleGetters(lazy, {
- TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
- controller: "resource://gre/modules/TorControlPort.sys.mjs",
-});
-
ChromeUtils.defineESModuleGetters(lazy, {
TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
});
-const logger = new ConsoleAPI({
- maxLogLevel: "warn",
- maxLogLevelPref: "browser.tor_monitor_service.log_level",
- prefix: "TorMonitorService",
-});
-
-const Preferences = Object.freeze({
- PromptAtStartup: "extensions.torlauncher.prompt_at_startup",
-});
-
-const TorTopics = Object.freeze({
- BootstrapError: "TorBootstrapError",
- HasWarnOrErr: "TorLogHasWarnOrErr",
- ProcessExited: "TorProcessExited",
- ProcessIsReady: "TorProcessIsReady",
- 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
+ BridgeChanged: TorProviderTopics.BridgeChanged,
+ StreamSucceeded: TorProviderTopics.StreamSucceeded,
});
-/**
- * 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.
@@ -76,575 +20,23 @@ const ControlConnTimings = Object.freeze({
* status of the bootstrap, the logs, etc...
*/
export const TorMonitorService = {
- _connection: null,
- _eventHandlers: {},
- _torLog: [], // Array of objects with date, type, and msg properties.
- _startTimeout: null,
-
- _isBootstrapDone: false,
- _lastWarningPhase: null,
- _lastWarningReason: null,
-
- _torProcess: null,
-
- _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
- init() {
- if (this._inited) {
- 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 {
- this._startEventMonitor();
- }
- logger.info("TorMonitorService initialized");
- },
-
- // Closes the connection that monitors for events.
- // When Tor is started by Tor Browser, it is configured to exit when the
- // control connection is closed. Therefore, as a matter of facts, calling this
- // function also makes the child Tor instance stop.
- uninit() {
- if (this._torProcess) {
- this._torProcess.forget();
- this._torProcess.onExit = null;
- this._torProcess.onRestart = null;
- this._torProcess = null;
- }
- this._shutDownEventMonitor();
- },
-
- async retrieveBootstrapStatus() {
- if (!this._connection) {
- throw new Error("Event monitor connection not available");
- }
-
- // TODO: Unify with TorProtocolService.sendCommand and put everything in the
- // reviewed torbutton replacement.
- const cmd = "GETINFO";
- const key = "status/bootstrap-phase";
- let reply = await this._connection.sendCommand(`${cmd} ${key}`);
-
- // A typical reply looks like:
- // 250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
- // 250 OK
- reply = TorParsers.parseCommandResponse(reply);
- if (!TorParsers.commandSucceeded(reply)) {
- throw new Error(`${cmd} failed`);
- }
- reply = TorParsers.parseReply(cmd, key, reply);
- if (reply.length) {
- this._processBootstrapStatus(reply[0], true);
- }
- },
-
- // Returns captured log message as a text string (one message per line).
- getLog() {
- return this._torLog
- .map(logObj => {
- const timeStr = logObj.date
- .toISOString()
- .replace("T", " ")
- .replace("Z", "");
- return `${timeStr} [${logObj.type}] ${logObj.msg}`;
- })
- .join(TorLauncherUtil.isWindows ? "\r\n" : "\n");
+ get currentBridge() {
+ return lazy.TorProtocolService.currentBridge;
},
- // true if we launched and control tor, false if using system tor
get ownsTorDaemon() {
- return TorLauncherUtil.shouldStartAndOwnTor;
- },
-
- get isBootstrapDone() {
- return this._isBootstrapDone;
- },
-
- clearBootstrapError() {
- this._lastWarningPhase = null;
- this._lastWarningReason = null;
+ return lazy.TorProtocolService.ownsTorDaemon;
},
get isRunning() {
- return !!this._connection;
+ return lazy.TorProtocolService.isRunning;
},
- /**
- * 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() {
- // 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(
- lazy.TorProtocolService.torControlPortInfo,
- lazy.TorProtocolService.torSOCKSPortInfo
- );
- this._torProcess.onExit = () => {
- this._shutDownEventMonitor();
- Services.obs.notifyObservers(null, TorTopics.ProcessExited);
- };
- this._torProcess.onRestart = async () => {
- this._shutDownEventMonitor();
- await this._controlTor();
- Services.obs.notifyObservers(null, TorTopics.ProcessRestarted);
- };
- }
-
- // Already running, but we did not start it
- if (this._torProcess.isRunning) {
- return false;
- }
-
- try {
- await this._torProcess.start();
- if (this._torProcess.isRunning) {
- logger.info("tor started");
- this._torProcessStartTime = Date.now();
- }
- } catch (e) {
- // TorProcess already logs the error.
- this._lastWarningPhase = "startup";
- this._lastWarningReason = e.toString();
- }
- return this._torProcess.isRunning;
- },
-
- async _controlTor() {
- if (!this._torProcess?.isRunning && !(await this._startProcess())) {
- logger.error("Tor not running, not starting to monitor it.");
- return;
- }
-
- let delayMS = ControlConnTimings.initialDelayMS;
- const callback = async () => {
- if (await this._startEventMonitor()) {
- this.retrieveBootstrapStatus().catch(e => {
- logger.warn("Could not get the initial bootstrap status", e);
- });
-
- // FIXME: TorProcess is misleading here. We should use a topic related
- // to having a control port connection, instead.
- logger.info(`Notifying ${TorTopics.ProcessIsReady}`);
- Services.obs.notifyObservers(null, TorTopics.ProcessIsReady);
-
- // We reset this here hoping that _shutDownEventMonitor can interrupt
- // the current monitor, either by calling clearTimeout and preventing it
- // from starting, or by closing the control port connection.
- if (this._startTimeout === null) {
- logger.warn("Someone else reset _startTimeout!");
- }
- this._startTimeout = null;
- } else if (
- Date.now() - this._torProcessStartTime >
- ControlConnTimings.timeoutMS
- ) {
- let s = TorLauncherUtil.getLocalizedString("tor_controlconn_failed");
- this._lastWarningPhase = "startup";
- this._lastWarningReason = s;
- logger.info(s);
- if (this._startTimeout === null) {
- logger.warn("Someone else reset _startTimeout!");
- }
- this._startTimeout = null;
- } else {
- delayMS *= 2;
- if (delayMS > ControlConnTimings.maxRetryMS) {
- delayMS = ControlConnTimings.maxRetryMS;
- }
- this._startTimeout = setTimeout(() => {
- logger.debug(`Control port not ready, waiting ${delayMS / 1000}s.`);
- callback();
- }, delayMS);
- }
- };
- // Check again, in the unfortunate case in which the execution was alrady
- // queued, but was waiting network code.
- if (this._startTimeout === null) {
- this._startTimeout = setTimeout(callback, delayMS);
- } else {
- logger.error("Possible race? Refusing to start the timeout again");
- }
- },
-
- async _startEventMonitor() {
- if (this._connection) {
- return true;
- }
-
- let conn;
- try {
- conn = await lazy.controller();
- } catch (e) {
- logger.error("Cannot open a control port connection", e);
- if (conn) {
- try {
- conn.close();
- } catch (e) {
- logger.error(
- "Also, the connection is not null but cannot be closed",
- e
- );
- }
- }
- return false;
- }
-
- // TODO: optionally monitor INFO and DEBUG log messages.
- try {
- await conn.setEvents(Array.from(this._eventHandlers.keys()));
- } catch (e) {
- logger.error("SETEVENTS failed", e);
- conn.close();
- return false;
- }
-
- if (this._torProcess) {
- this._torProcess.connectionWorked();
- }
- if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) {
- try {
- await this._takeTorOwnership(conn);
- } catch (e) {
- logger.warn("Could not take ownership of the Tor daemon", e);
- }
- }
-
- this._connection = conn;
-
- 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;
- },
-
- // Try to become the primary controller (TAKEOWNERSHIP).
- async _takeTorOwnership(conn) {
- try {
- conn.takeOwnership();
- } catch (e) {
- logger.warn("Take ownership failed", e);
- return;
- }
- try {
- conn.resetOwningControllerProcess();
- } catch (e) {
- logger.warn("Clear owning controller process failed", e);
- }
- },
-
- _monitorEvent(type, callback) {
- logger.info(`Watching events of type ${type}.`);
- let replyObj = {};
- 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
- );
- },
-
- _processLog(type, lines) {
- if (type === "WARN" || type === "ERR") {
- // Notify so that Copy Log can be enabled.
- Services.obs.notifyObservers(null, TorTopics.HasWarnOrErr);
- }
-
- const date = new Date();
- const maxEntries = Services.prefs.getIntPref(
- "extensions.torlauncher.max_tor_log_entries",
- 1000
- );
- if (maxEntries > 0 && this._torLog.length >= maxEntries) {
- this._torLog.splice(0, 1);
- }
-
- const msg = lines.join("\n");
- this._torLog.push({ date, type, msg });
- const logString = `Tor ${type}: ${msg}`;
- logger.info(logString);
- },
-
- // Process a bootstrap status to update the current state, and broadcast it
- // to TorBootstrapStatus observers.
- // If aSuppressErrors is true, errors are ignored. This is used when we
- // are handling the response to a "GETINFO status/bootstrap-phase" command.
- _processBootstrapStatus(aStatusMsg, aSuppressErrors) {
- const statusObj = TorParsers.parseBootstrapStatus(aStatusMsg);
- if (!statusObj) {
- return;
- }
-
- // Notify observers
- statusObj.wrappedJSObject = statusObj;
- Services.obs.notifyObservers(statusObj, "TorBootstrapStatus");
-
- if (statusObj.PROGRESS === 100) {
- this._isBootstrapDone = true;
- try {
- Services.prefs.setBoolPref(Preferences.PromptAtStartup, false);
- } catch (e) {
- logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
- }
- return;
- }
-
- this._isBootstrapDone = false;
-
- if (
- statusObj.TYPE === "WARN" &&
- statusObj.RECOMMENDATION !== "ignore" &&
- !aSuppressErrors
- ) {
- this._notifyBootstrapError(statusObj);
- }
- },
-
- _notifyBootstrapError(statusObj) {
- try {
- Services.prefs.setBoolPref(Preferences.PromptAtStartup, true);
- } catch (e) {
- logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
- }
- const phase = TorLauncherUtil.getLocalizedBootstrapStatus(statusObj, "TAG");
- const reason = TorLauncherUtil.getLocalizedBootstrapStatus(
- statusObj,
- "REASON"
- );
- const details = TorLauncherUtil.getFormattedLocalizedString(
- "tor_bootstrap_failed_details",
- [phase, reason],
- 2
- );
- logger.error(
- `Tor bootstrap error: [${statusObj.TAG}/${statusObj.REASON}] ${details}`
- );
-
- if (
- statusObj.TAG !== this._lastWarningPhase ||
- statusObj.REASON !== this._lastWarningReason
- ) {
- this._lastWarningPhase = statusObj.TAG;
- this._lastWarningReason = statusObj.REASON;
-
- const message = TorLauncherUtil.getLocalizedString(
- "tor_bootstrap_failed"
- );
- Services.obs.notifyObservers(
- { message, details },
- TorTopics.BootstrapError
- );
- }
- },
-
- 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
- );
+ get isBootstrapDone() {
+ return lazy.TorProtocolService.isBootstrapDone;
},
- _shutDownEventMonitor() {
- 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);
- this._startTimeout = null;
- }
- this._isBootstrapDone = false;
- this.clearBootstrapError();
+ getLog() {
+ return lazy.TorProtocolService.getLog();
},
};
=====================================
toolkit/components/tor-launcher/TorProtocolService.sys.mjs
=====================================
@@ -1,32 +1,22 @@
// Copyright (c) 2021, The Tor Project, Inc.
-import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
-import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
+import {
+ TorParsers,
+ TorStatuses,
+} from "resource://gre/modules/TorParsers.sys.mjs";
+import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs";
const lazy = {};
-ChromeUtils.defineModuleGetter(
- lazy,
- "FileUtils",
- "resource://gre/modules/FileUtils.jsm"
-);
-
-ChromeUtils.defineModuleGetter(
- lazy,
- "TorMonitorService",
- "resource://gre/modules/TorMonitorService.jsm"
-);
ChromeUtils.defineESModuleGetters(lazy, {
controller: "resource://gre/modules/TorControlPort.sys.mjs",
configureControlPortModule: "resource://gre/modules/TorControlPort.sys.mjs",
-});
-
-const TorTopics = Object.freeze({
- ProcessExited: "TorProcessExited",
- ProcessRestarted: "TorProcessRestarted",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ TorProcess: "resource://gre/modules/TorProcess.sys.mjs",
});
const logger = new ConsoleAPI({
@@ -34,11 +24,27 @@ const logger = new ConsoleAPI({
prefix: "TorProtocolService",
});
+/**
+ * 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
+ */
/**
* Stores the data associated with a circuit node.
*
* @typedef NodeData
- * @property {string} fingerprint The node fingerprint.
+ * @property {NodeFingerprint} 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
@@ -48,60 +54,71 @@ const logger = new ConsoleAPI({
* valid BCP47 Region subtag.
*/
-// Manage the connection to tor's control port, to update its settings and query
-// other useful information.
-//
-// NOTE: Many Tor protocol functions return a reply object, which is a
-// a JavaScript object that has the following fields:
-// reply.statusCode -- integer, e.g., 250
-// reply.lineArray -- an array of strings returned by tor
-// For GetConf calls, the aKey prefix is removed from the lineArray strings.
-export const TorProtocolService = {
- _inited: false,
+const Preferences = Object.freeze({
+ PromptAtStartup: "extensions.torlauncher.prompt_at_startup",
+});
+
+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
+});
+
+/**
+ * This is a Tor provider for the C Tor daemon.
+ *
+ * It can start a new tor instance, or connect to an existing one.
+ * In the former case, it also takes its ownership by default.
+ */
+class TorProvider {
+ #inited = false;
// Maintain a map of tor settings set by Tor Browser so that we don't
// repeatedly set the same key/values over and over.
// This map contains string keys to primitives or array values.
- _settingsCache: new Map(),
+ #settingsCache = new Map();
- _controlPort: null,
- _controlHost: null,
- _controlIPCFile: null, // An nsIFile if using IPC for control port.
- _controlPassword: null, // JS string that contains hex-encoded password.
- _SOCKSPortInfo: null, // An object that contains ipcFile, host, port.
+ #controlPort = null;
+ #controlHost = null;
+ #controlIPCFile = null; // An nsIFile if using IPC for control port.
+ #controlPassword = null; // JS string that contains hex-encoded password.
+ #SOCKSPortInfo = null; // An object that contains ipcFile, host, port.
- _controlConnection: null, // This is cached and reused.
- _connectionQueue: [],
+ #controlConnection = null; // This is cached and reused.
+ #connectionQueue = [];
// Public methods
async init() {
- if (this._inited) {
+ if (this.#inited) {
return;
}
- this._inited = true;
+ this.#inited = true;
+
+ Services.obs.addObserver(this, TorProviderTopics.ProcessExited);
+ Services.obs.addObserver(this, TorProviderTopics.ProcessRestarted);
- Services.obs.addObserver(this, TorTopics.ProcessExited);
- Services.obs.addObserver(this, TorTopics.ProcessRestarted);
+ await this.#setSockets();
- await this._setSockets();
+ this._monitorInit();
- logger.debug("TorProtocolService initialized");
- },
+ logger.debug("TorProvider initialized");
+ }
uninit() {
- Services.obs.removeObserver(this, TorTopics.ProcessExited);
- Services.obs.removeObserver(this, TorTopics.ProcessRestarted);
- this._closeConnection();
- },
+ Services.obs.removeObserver(this, TorProviderTopics.ProcessExited);
+ Services.obs.removeObserver(this, TorProviderTopics.ProcessRestarted);
+ this.#closeConnection();
+ this._monitorUninit();
+ }
observe(subject, topic, data) {
- if (topic === TorTopics.ProcessExited) {
- this._closeConnection();
- } else if (topic === TorTopics.ProcessRestarted) {
- this._reconnect();
+ if (topic === TorProviderTopics.ProcessExited) {
+ this.#closeConnection();
+ } else if (topic === TorProviderTopics.ProcessRestarted) {
+ this.#reconnect();
}
- },
+ }
// takes a Map containing tor settings
// throws on error
@@ -109,14 +126,14 @@ export const TorProtocolService = {
// only write settings that have changed
const newSettings = Array.from(aSettingsObj).filter(([setting, value]) => {
// make sure we have valid data here
- this._assertValidSetting(setting, value);
+ this.#assertValidSetting(setting, value);
- if (!this._settingsCache.has(setting)) {
+ if (!this.#settingsCache.has(setting)) {
// no cached setting, so write
return true;
}
- const cachedValue = this._settingsCache.get(setting);
+ const cachedValue = this.#settingsCache.get(setting);
if (value === cachedValue) {
return false;
} else if (Array.isArray(value) && Array.isArray(cachedValue)) {
@@ -142,21 +159,21 @@ export const TorProtocolService = {
// save settings to cache after successfully writing to Tor
for (const [setting, value] of newSettings) {
- this._settingsCache.set(setting, value);
+ this.#settingsCache.set(setting, value);
}
}
- },
+ }
async readStringArraySetting(aSetting) {
- const value = await this._readSetting(aSetting);
- this._settingsCache.set(aSetting, value);
+ const value = await this.#readSetting(aSetting);
+ this.#settingsCache.set(aSetting, value);
return value;
- },
+ }
// writes current tor settings to disk
async flushSettings() {
await this.sendCommand("SAVECONF");
- },
+ }
async connect() {
const kTorConfKeyDisableNetwork = "DisableNetwork";
@@ -164,9 +181,9 @@ export const TorProtocolService = {
settings[kTorConfKeyDisableNetwork] = false;
await this.setConfWithReply(settings);
await this.sendCommand("SAVECONF");
- lazy.TorMonitorService.clearBootstrapError();
- lazy.TorMonitorService.retrieveBootstrapStatus();
- },
+ this.clearBootstrapError();
+ this.retrieveBootstrapStatus();
+ }
async stopBootstrap() {
// Tell tor to disable use of the network; this should stop the bootstrap
@@ -180,12 +197,12 @@ export const TorProtocolService = {
// We are not interested in waiting for this, nor in **catching its error**,
// so we do not await this. We just want to be notified when the bootstrap
// status is actually updated through observers.
- lazy.TorMonitorService.retrieveBootstrapStatus();
- },
+ this.retrieveBootstrapStatus();
+ }
async newnym() {
return this.sendCommand("SIGNAL NEWNYM");
- },
+ }
// Ask tor which ports it is listening to for SOCKS connections.
// At the moment this is used only in TorCheckService.
@@ -194,7 +211,7 @@ export const TorProtocolService = {
const keyword = "net/listeners/socks";
const response = await this.sendCommand(cmd, keyword);
return TorParsers.parseReply(cmd, keyword, response);
- },
+ }
async getBridges() {
// Ideally, we would not need this function, because we should be the one
@@ -203,19 +220,19 @@ export const TorProtocolService = {
// 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 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}
+ * @returns {Promise<NodeData>}
*/
async getNodeInfo(id) {
- return this._withConnection(async conn => {
+ return this.#withConnection(async conn => {
const node = {
fingerprint: id,
ipAddrs: [],
@@ -259,62 +276,62 @@ export const TorProtocolService = {
}
return node;
});
- },
+ }
async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) {
- return this._withConnection(conn => {
+ return this.#withConnection(conn => {
return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent);
});
- },
+ }
async onionAuthRemove(hsAddress) {
- return this._withConnection(conn => {
+ return this.#withConnection(conn => {
return conn.onionAuthRemove(hsAddress);
});
- },
+ }
async onionAuthViewKeys() {
- return this._withConnection(conn => {
+ return this.#withConnection(conn => {
return conn.onionAuthViewKeys();
});
- },
+ }
// TODO: transform the following 4 functions in getters.
// Returns Tor password string or null if an error occurs.
torGetPassword() {
- return this._controlPassword;
- },
+ return this.#controlPassword;
+ }
torGetControlIPCFile() {
- return this._controlIPCFile?.clone();
- },
+ return this.#controlIPCFile?.clone();
+ }
torGetControlPort() {
- return this._controlPort;
- },
+ return this.#controlPort;
+ }
torGetSOCKSPortInfo() {
- return this._SOCKSPortInfo;
- },
+ return this.#SOCKSPortInfo;
+ }
get torControlPortInfo() {
const info = {
- password: this._controlPassword,
+ password: this.#controlPassword,
};
- if (this._controlIPCFile) {
- info.ipcFile = this._controlIPCFile?.clone();
+ if (this.#controlIPCFile) {
+ info.ipcFile = this.#controlIPCFile?.clone();
}
- if (this._controlPort) {
- info.host = this._controlHost;
- info.port = this._controlPort;
+ if (this.#controlPort) {
+ info.host = this.#controlHost;
+ info.port = this.#controlPort;
}
return info;
- },
+ }
get torSOCKSPortInfo() {
- return this._SOCKSPortInfo;
- },
+ return this.#SOCKSPortInfo;
+ }
// Public, but called only internally
@@ -326,7 +343,7 @@ export const TorProtocolService = {
let timeout = 250;
let reply;
while (leftConnAttempts-- > 0) {
- const response = await this._trySend(cmd, args, leftConnAttempts == 0);
+ const response = await this.#trySend(cmd, args, leftConnAttempts === 0);
if (response.connected) {
reply = response.reply;
break;
@@ -360,7 +377,7 @@ export const TorProtocolService = {
}
return reply;
- },
+ }
// Perform a SETCONF command.
// aSettingsObj should be a JavaScript object with keys (property values)
@@ -398,39 +415,39 @@ export const TorProtocolService = {
}
await this.sendCommand("SETCONF", args.join(" "));
- },
+ }
// Public, never called?
async readBoolSetting(aSetting) {
- let value = await this._readBoolSetting(aSetting);
- this._settingsCache.set(aSetting, value);
+ let value = await this.#readBoolSetting(aSetting);
+ this.#settingsCache.set(aSetting, value);
return value;
- },
+ }
async readStringSetting(aSetting) {
- let value = await this._readStringSetting(aSetting);
- this._settingsCache.set(aSetting, value);
+ let value = await this.#readStringSetting(aSetting);
+ this.#settingsCache.set(aSetting, value);
return value;
- },
+ }
// Private
- async _setSockets() {
+ async #setSockets() {
try {
const isWindows = TorLauncherUtil.isWindows;
// Determine how Tor Launcher will connect to the Tor control port.
// Environment variables get top priority followed by preferences.
if (!isWindows && Services.env.exists("TOR_CONTROL_IPC_PATH")) {
const ipcPath = Services.env.get("TOR_CONTROL_IPC_PATH");
- this._controlIPCFile = new lazy.FileUtils.File(ipcPath);
+ this.#controlIPCFile = new lazy.FileUtils.File(ipcPath);
} else {
// Check for TCP host and port environment variables.
if (Services.env.exists("TOR_CONTROL_HOST")) {
- this._controlHost = Services.env.get("TOR_CONTROL_HOST");
+ this.#controlHost = Services.env.get("TOR_CONTROL_HOST");
}
if (Services.env.exists("TOR_CONTROL_PORT")) {
- this._controlPort = parseInt(
+ this.#controlPort = parseInt(
Services.env.get("TOR_CONTROL_PORT"),
10
);
@@ -442,20 +459,20 @@ export const TorProtocolService = {
"extensions.torlauncher.control_port_use_ipc",
false
);
- if (!this._controlHost && !this._controlPort && useIPC) {
- this._controlIPCFile = TorLauncherUtil.getTorFile(
+ if (!this.#controlHost && !this.#controlPort && useIPC) {
+ this.#controlIPCFile = TorLauncherUtil.getTorFile(
"control_ipc",
false
);
} else {
- if (!this._controlHost) {
- this._controlHost = Services.prefs.getCharPref(
+ if (!this.#controlHost) {
+ this.#controlHost = Services.prefs.getCharPref(
"extensions.torlauncher.control_host",
"127.0.0.1"
);
}
- if (!this._controlPort) {
- this._controlPort = Services.prefs.getIntPref(
+ if (!this.#controlPort) {
+ this.#controlPort = Services.prefs.getIntPref(
"extensions.torlauncher.control_port",
9151
);
@@ -465,46 +482,46 @@ export const TorProtocolService = {
// Populate _controlPassword so it is available when starting tor.
if (Services.env.exists("TOR_CONTROL_PASSWD")) {
- this._controlPassword = Services.env.get("TOR_CONTROL_PASSWD");
+ this.#controlPassword = Services.env.get("TOR_CONTROL_PASSWD");
} else if (Services.env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
// TODO: test this code path (TOR_CONTROL_COOKIE_AUTH_FILE).
const cookiePath = Services.env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
if (cookiePath) {
- this._controlPassword = await this._readAuthenticationCookie(
+ this.#controlPassword = await this.#readAuthenticationCookie(
cookiePath
);
}
}
- if (!this._controlPassword) {
- this._controlPassword = this._generateRandomPassword();
+ if (!this.#controlPassword) {
+ this.#controlPassword = this.#generateRandomPassword();
}
- this._SOCKSPortInfo = TorLauncherUtil.getPreferredSocksConfiguration();
- TorLauncherUtil.setProxyConfiguration(this._SOCKSPortInfo);
+ this.#SOCKSPortInfo = TorLauncherUtil.getPreferredSocksConfiguration();
+ TorLauncherUtil.setProxyConfiguration(this.#SOCKSPortInfo);
// Set the global control port info parameters.
lazy.configureControlPortModule(
- this._controlIPCFile,
- this._controlHost,
- this._controlPort,
- this._controlPassword
+ this.#controlIPCFile,
+ this.#controlHost,
+ this.#controlPort,
+ this.#controlPassword
);
} catch (e) {
logger.error("Failed to get environment variables", e);
}
- },
+ }
- _assertValidSettingKey(aSetting) {
+ #assertValidSettingKey(aSetting) {
// ensure the 'key' is a string
if (typeof aSetting !== "string") {
throw new Error(
`Expected setting of type string but received ${typeof aSetting}`
);
}
- },
+ }
- _assertValidSetting(aSetting, aValue) {
- this._assertValidSettingKey(aSetting);
+ #assertValidSetting(aSetting, aValue) {
+ this.#assertValidSettingKey(aSetting);
switch (typeof aValue) {
case "boolean":
case "string":
@@ -528,29 +545,29 @@ export const TorProtocolService = {
`Invalid object type received for setting '${aSetting}'`
);
}
- },
+ }
// Perform a GETCONF command.
- async _readSetting(aSetting) {
- this._assertValidSettingKey(aSetting);
+ async #readSetting(aSetting) {
+ this.#assertValidSettingKey(aSetting);
const cmd = "GETCONF";
let reply = await this.sendCommand(cmd, aSetting);
return TorParsers.parseReply(cmd, aSetting, reply);
- },
+ }
- async _readStringSetting(aSetting) {
- let lineArray = await this._readSetting(aSetting);
+ async #readStringSetting(aSetting) {
+ let lineArray = await this.#readSetting(aSetting);
if (lineArray.length !== 1) {
throw new Error(
`Expected an array with length 1 but received array of length ${lineArray.length}`
);
}
return lineArray[0];
- },
+ }
- async _readBoolSetting(aSetting) {
- const value = this._readStringSetting(aSetting);
+ async #readBoolSetting(aSetting) {
+ const value = this.#readStringSetting(aSetting);
switch (value) {
case "0":
return false;
@@ -559,16 +576,16 @@ export const TorProtocolService = {
default:
throw new Error(`Expected boolean (1 or 0) but received '${value}'`);
}
- },
+ }
- async _trySend(cmd, args, rethrow) {
+ async #trySend(cmd, args, rethrow) {
let connected = false;
let reply;
let leftAttempts = 2;
while (leftAttempts-- > 0) {
let conn;
try {
- conn = await this._getConnection();
+ conn = await this.#getConnection();
} catch (e) {
logger.error("Cannot get a connection to the control port", e);
if (leftAttempts == 0 && rethrow) {
@@ -584,105 +601,105 @@ export const TorProtocolService = {
reply = await conn.sendCommand(cmd + (args ? " " + args : ""));
if (reply) {
// Return for reuse.
- this._returnConnection();
+ this.#returnConnection();
} else {
// Connection is bad.
logger.warn(
"sendCommand returned an empty response, taking the connection as broken and closing it."
);
- this._closeConnection();
+ this.#closeConnection();
}
} catch (e) {
logger.error(`Cannot send the command ${cmd}`, e);
- this._closeConnection();
+ this.#closeConnection();
if (leftAttempts == 0 && rethrow) {
throw e;
}
}
}
return { connected, reply };
- },
+ }
- // Opens an authenticated connection, sets it to this._controlConnection, and
+ // Opens an authenticated connection, sets it to this.#controlConnection, and
// return it.
- async _getConnection() {
- if (!this._controlConnection) {
- this._controlConnection = await lazy.controller();
+ async #getConnection() {
+ if (!this.#controlConnection) {
+ this.#controlConnection = await lazy.controller();
}
- if (this._controlConnection.inUse) {
+ if (this.#controlConnection.inUse) {
await new Promise((resolve, reject) =>
- this._connectionQueue.push({ resolve, reject })
+ this.#connectionQueue.push({ resolve, reject })
);
} else {
- this._controlConnection.inUse = true;
+ this.#controlConnection.inUse = true;
}
- return this._controlConnection;
- },
+ return this.#controlConnection;
+ }
- _returnConnection() {
- if (this._connectionQueue.length) {
- this._connectionQueue.shift().resolve();
+ #returnConnection() {
+ if (this.#connectionQueue.length) {
+ this.#connectionQueue.shift().resolve();
} else {
- this._controlConnection.inUse = false;
+ this.#controlConnection.inUse = false;
}
- },
+ }
- async _withConnection(func) {
+ async #withConnection(func) {
// TODO: Make more robust?
- const conn = await this._getConnection();
+ const conn = await this.#getConnection();
try {
return await func(conn);
} finally {
- this._returnConnection();
+ this.#returnConnection();
}
- },
+ }
// If aConn is omitted, the cached connection is closed.
- _closeConnection() {
- if (this._controlConnection) {
+ #closeConnection() {
+ if (this.#controlConnection) {
logger.info("Closing the control connection");
- this._controlConnection.close();
- this._controlConnection = null;
+ this.#controlConnection.close();
+ this.#controlConnection = null;
}
- for (const promise of this._connectionQueue) {
+ for (const promise of this.#connectionQueue) {
promise.reject("Connection closed");
}
- this._connectionQueue = [];
- },
+ this.#connectionQueue = [];
+ }
- async _reconnect() {
- this._closeConnection();
- const conn = await this._getConnection();
+ async #reconnect() {
+ this.#closeConnection();
+ const conn = await this.#getConnection();
logger.debug("Reconnected to the control port.");
- this._returnConnection(conn);
- },
+ this.#returnConnection(conn);
+ }
- async _readAuthenticationCookie(aPath) {
+ async #readAuthenticationCookie(aPath) {
const bytes = await IOUtils.read(aPath);
- return Array.from(bytes, b => this._toHex(b, 2)).join("");
- },
+ return Array.from(bytes, b => this.#toHex(b, 2)).join("");
+ }
// Returns a random 16 character password, hex-encoded.
- _generateRandomPassword() {
+ #generateRandomPassword() {
// Similar to Vidalia's crypto_rand_string().
const kPasswordLen = 16;
const kMinCharCode = "!".charCodeAt(0);
const kMaxCharCode = "~".charCodeAt(0);
let pwd = "";
for (let i = 0; i < kPasswordLen; ++i) {
- const val = this._cryptoRandInt(kMaxCharCode - kMinCharCode + 1);
+ const val = this.#cryptoRandInt(kMaxCharCode - kMinCharCode + 1);
if (val < 0) {
logger.error("_cryptoRandInt() failed");
return null;
}
- pwd += this._toHex(kMinCharCode + val, 2);
+ pwd += this.#toHex(kMinCharCode + val, 2);
}
return pwd;
- },
+ }
// Returns -1 upon failure.
- _cryptoRandInt(aMax) {
+ #cryptoRandInt(aMax) {
// Based on tor's crypto_rand_int().
const maxUInt = 0xffffffff;
if (aMax <= 0 || aMax > maxUInt) {
@@ -697,9 +714,588 @@ export const TorProtocolService = {
val = uint32[0];
}
return val % aMax;
- },
+ }
- _toHex(aValue, aMinLen) {
+ #toHex(aValue, aMinLen) {
return aValue.toString(16).padStart(aMinLen, "0");
- },
-};
+ }
+
+ // Former TorMonitorService implementation.
+ // FIXME: Refactor and integrate more with the rest of the class.
+
+ _connection = null;
+ _eventHandlers = {};
+ _torLog = []; // Array of objects with date, type, and msg properties
+ _startTimeout = null;
+
+ _isBootstrapDone = false;
+ _lastWarningPhase = null;
+ _lastWarningReason = null;
+
+ _torProcess = null;
+
+ _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
+ _monitorInit() {
+ if (this._inited) {
+ 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 {
+ this._startEventMonitor();
+ }
+ logger.info("TorMonitorService initialized");
+ }
+
+ // Closes the connection that monitors for events.
+ // When Tor is started by Tor Browser, it is configured to exit when the
+ // control connection is closed. Therefore, as a matter of facts, calling this
+ // function also makes the child Tor instance stop.
+ _monitorUninit() {
+ if (this._torProcess) {
+ this._torProcess.forget();
+ this._torProcess.onExit = null;
+ this._torProcess.onRestart = null;
+ this._torProcess = null;
+ }
+ this._shutDownEventMonitor();
+ }
+
+ async retrieveBootstrapStatus() {
+ if (!this._connection) {
+ throw new Error("Event monitor connection not available");
+ }
+
+ // TODO: Unify with TorProtocolService.sendCommand and put everything in the
+ // reviewed torbutton replacement.
+ const cmd = "GETINFO";
+ const key = "status/bootstrap-phase";
+ let reply = await this._connection.sendCommand(`${cmd} ${key}`);
+
+ // A typical reply looks like:
+ // 250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
+ // 250 OK
+ reply = TorParsers.parseCommandResponse(reply);
+ if (!TorParsers.commandSucceeded(reply)) {
+ throw new Error(`${cmd} failed`);
+ }
+ reply = TorParsers.parseReply(cmd, key, reply);
+ if (reply.length) {
+ this._processBootstrapStatus(reply[0], true);
+ }
+ }
+
+ // Returns captured log message as a text string (one message per line).
+ getLog() {
+ return this._torLog
+ .map(logObj => {
+ const timeStr = logObj.date
+ .toISOString()
+ .replace("T", " ")
+ .replace("Z", "");
+ return `${timeStr} [${logObj.type}] ${logObj.msg}`;
+ })
+ .join(TorLauncherUtil.isWindows ? "\r\n" : "\n");
+ }
+
+ // true if we launched and control tor, false if using system tor
+ get ownsTorDaemon() {
+ return TorLauncherUtil.shouldStartAndOwnTor;
+ }
+
+ get isBootstrapDone() {
+ return this._isBootstrapDone;
+ }
+
+ clearBootstrapError() {
+ this._lastWarningPhase = null;
+ this._lastWarningReason = null;
+ }
+
+ get isRunning() {
+ 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() {
+ // 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 lazy.TorProcess(
+ this.torControlPortInfo,
+ this.torSOCKSPortInfo
+ );
+ this._torProcess.onExit = () => {
+ this._shutDownEventMonitor();
+ Services.obs.notifyObservers(null, TorProviderTopics.ProcessExited);
+ };
+ this._torProcess.onRestart = async () => {
+ this._shutDownEventMonitor();
+ await this._controlTor();
+ Services.obs.notifyObservers(null, TorProviderTopics.ProcessRestarted);
+ };
+ }
+
+ // Already running, but we did not start it
+ if (this._torProcess.isRunning) {
+ return false;
+ }
+
+ try {
+ await this._torProcess.start();
+ if (this._torProcess.isRunning) {
+ logger.info("tor started");
+ this._torProcessStartTime = Date.now();
+ }
+ } catch (e) {
+ // TorProcess already logs the error.
+ this._lastWarningPhase = "startup";
+ this._lastWarningReason = e.toString();
+ }
+ return this._torProcess.isRunning;
+ }
+
+ async _controlTor() {
+ if (!this._torProcess?.isRunning && !(await this._startProcess())) {
+ logger.error("Tor not running, not starting to monitor it.");
+ return;
+ }
+
+ let delayMS = ControlConnTimings.initialDelayMS;
+ const callback = async () => {
+ if (await this._startEventMonitor()) {
+ this.retrieveBootstrapStatus().catch(e => {
+ logger.warn("Could not get the initial bootstrap status", e);
+ });
+
+ // FIXME: TorProcess is misleading here. We should use a topic related
+ // to having a control port connection, instead.
+ logger.info(`Notifying ${TorProviderTopics.ProcessIsReady}`);
+ Services.obs.notifyObservers(null, TorProviderTopics.ProcessIsReady);
+
+ // We reset this here hoping that _shutDownEventMonitor can interrupt
+ // the current monitor, either by calling clearTimeout and preventing it
+ // from starting, or by closing the control port connection.
+ if (this._startTimeout === null) {
+ logger.warn("Someone else reset _startTimeout!");
+ }
+ this._startTimeout = null;
+ } else if (
+ Date.now() - this._torProcessStartTime >
+ ControlConnTimings.timeoutMS
+ ) {
+ let s = TorLauncherUtil.getLocalizedString("tor_controlconn_failed");
+ this._lastWarningPhase = "startup";
+ this._lastWarningReason = s;
+ logger.info(s);
+ if (this._startTimeout === null) {
+ logger.warn("Someone else reset _startTimeout!");
+ }
+ this._startTimeout = null;
+ } else {
+ delayMS *= 2;
+ if (delayMS > ControlConnTimings.maxRetryMS) {
+ delayMS = ControlConnTimings.maxRetryMS;
+ }
+ this._startTimeout = setTimeout(() => {
+ logger.debug(`Control port not ready, waiting ${delayMS / 1000}s.`);
+ callback();
+ }, delayMS);
+ }
+ };
+ // Check again, in the unfortunate case in which the execution was alrady
+ // queued, but was waiting network code.
+ if (this._startTimeout === null) {
+ this._startTimeout = setTimeout(callback, delayMS);
+ } else {
+ logger.error("Possible race? Refusing to start the timeout again");
+ }
+ }
+
+ async _startEventMonitor() {
+ if (this._connection) {
+ return true;
+ }
+
+ let conn;
+ try {
+ conn = await lazy.controller();
+ } catch (e) {
+ logger.error("Cannot open a control port connection", e);
+ if (conn) {
+ try {
+ conn.close();
+ } catch (e) {
+ logger.error(
+ "Also, the connection is not null but cannot be closed",
+ e
+ );
+ }
+ }
+ return false;
+ }
+
+ // TODO: optionally monitor INFO and DEBUG log messages.
+ try {
+ await conn.setEvents(Array.from(this._eventHandlers.keys()));
+ } catch (e) {
+ logger.error("SETEVENTS failed", e);
+ conn.close();
+ return false;
+ }
+
+ if (this._torProcess) {
+ this._torProcess.connectionWorked();
+ }
+ if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) {
+ try {
+ await this._takeTorOwnership(conn);
+ } catch (e) {
+ logger.warn("Could not take ownership of the Tor daemon", e);
+ }
+ }
+
+ this._connection = conn;
+
+ 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;
+ }
+
+ // Try to become the primary controller (TAKEOWNERSHIP).
+ async _takeTorOwnership(conn) {
+ try {
+ conn.takeOwnership();
+ } catch (e) {
+ logger.warn("Take ownership failed", e);
+ return;
+ }
+ try {
+ conn.resetOwningControllerProcess();
+ } catch (e) {
+ logger.warn("Clear owning controller process failed", e);
+ }
+ }
+
+ _monitorEvent(type, callback) {
+ logger.info(`Watching events of type ${type}.`);
+ let replyObj = {};
+ 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
+ );
+ }
+
+ _processLog(type, lines) {
+ if (type === "WARN" || type === "ERR") {
+ // Notify so that Copy Log can be enabled.
+ Services.obs.notifyObservers(null, TorProviderTopics.HasWarnOrErr);
+ }
+
+ const date = new Date();
+ const maxEntries = Services.prefs.getIntPref(
+ "extensions.torlauncher.max_tor_log_entries",
+ 1000
+ );
+ if (maxEntries > 0 && this._torLog.length >= maxEntries) {
+ this._torLog.splice(0, 1);
+ }
+
+ const msg = lines.join("\n");
+ this._torLog.push({ date, type, msg });
+ const logString = `Tor ${type}: ${msg}`;
+ logger.info(logString);
+ }
+
+ // Process a bootstrap status to update the current state, and broadcast it
+ // to TorBootstrapStatus observers.
+ // If aSuppressErrors is true, errors are ignored. This is used when we
+ // are handling the response to a "GETINFO status/bootstrap-phase" command.
+ _processBootstrapStatus(aStatusMsg, aSuppressErrors) {
+ const statusObj = TorParsers.parseBootstrapStatus(aStatusMsg);
+ if (!statusObj) {
+ return;
+ }
+
+ // Notify observers
+ statusObj.wrappedJSObject = statusObj;
+ Services.obs.notifyObservers(statusObj, "TorBootstrapStatus");
+
+ if (statusObj.PROGRESS === 100) {
+ this._isBootstrapDone = true;
+ try {
+ Services.prefs.setBoolPref(Preferences.PromptAtStartup, false);
+ } catch (e) {
+ logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
+ }
+ return;
+ }
+
+ this._isBootstrapDone = false;
+
+ if (
+ statusObj.TYPE === "WARN" &&
+ statusObj.RECOMMENDATION !== "ignore" &&
+ !aSuppressErrors
+ ) {
+ this._notifyBootstrapError(statusObj);
+ }
+ }
+
+ _notifyBootstrapError(statusObj) {
+ try {
+ Services.prefs.setBoolPref(Preferences.PromptAtStartup, true);
+ } catch (e) {
+ logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
+ }
+ const phase = TorLauncherUtil.getLocalizedBootstrapStatus(statusObj, "TAG");
+ const reason = TorLauncherUtil.getLocalizedBootstrapStatus(
+ statusObj,
+ "REASON"
+ );
+ const details = TorLauncherUtil.getFormattedLocalizedString(
+ "tor_bootstrap_failed_details",
+ [phase, reason],
+ 2
+ );
+ logger.error(
+ `Tor bootstrap error: [${statusObj.TAG}/${statusObj.REASON}] ${details}`
+ );
+
+ if (
+ statusObj.TAG !== this._lastWarningPhase ||
+ statusObj.REASON !== this._lastWarningReason
+ ) {
+ this._lastWarningPhase = statusObj.TAG;
+ this._lastWarningReason = statusObj.REASON;
+
+ const message = TorLauncherUtil.getLocalizedString(
+ "tor_bootstrap_failed"
+ );
+ Services.obs.notifyObservers(
+ { message, details },
+ TorProviderTopics.BootstrapError
+ );
+ }
+ }
+
+ 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 this.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,
+ TorProviderTopics.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,
+ },
+ },
+ TorProviderTopics.StreamSucceeded
+ );
+ }
+
+ _shutDownEventMonitor() {
+ 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);
+ this._startTimeout = null;
+ }
+ this._isBootstrapDone = false;
+ this.clearBootstrapError();
+ }
+}
+
+// TODO: Stop defining TorProtocolService, make the builder instance the
+// TorProvider.
+export const TorProtocolService = new TorProvider();
=====================================
toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
=====================================
@@ -0,0 +1,34 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
+});
+
+export const TorProviderTopics = Object.freeze({
+ ProcessIsReady: "TorProcessIsReady",
+ ProcessExited: "TorProcessExited",
+ ProcessRestarted: "TorProcessRestarted",
+ BootstrapStatus: "TorBootstrapStatus",
+ BootstrapError: "TorBootstrapError",
+ HasWarnOrErr: "TorLogHasWarnOrErr",
+ BridgeChanged: "TorBridgeChanged",
+ StreamSucceeded: "TorStreamSucceeded",
+});
+
+export class TorProviderBuilder {
+ static async init() {
+ await lazy.TorProtocolService.init();
+ }
+
+ static uninit() {
+ lazy.TorProtocolService.uninit();
+ }
+
+ // TODO: Switch to an async build?
+ static build() {
+ return lazy.TorProtocolService;
+ }
+}
=====================================
toolkit/components/tor-launcher/TorStartupService.sys.mjs
=====================================
@@ -5,8 +5,7 @@ const lazy = {};
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",
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});
ChromeUtils.defineModuleGetter(
@@ -33,24 +32,18 @@ let gInited = false;
// When it observes profile-after-change, it initializes whatever is needed to
// launch Tor.
export class TorStartupService {
- _defaultPreferencesAreLoaded = false;
-
observe(aSubject, aTopic, aData) {
if (aTopic === BrowserTopics.ProfileAfterChange && !gInited) {
- this._init();
+ this.#init();
} else if (aTopic === BrowserTopics.QuitApplicationGranted) {
- this._uninit();
+ this.#uninit();
}
}
- async _init() {
+ async #init() {
Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
- // Starts TorProtocolService first, because it configures the controller
- // factory, too.
- await lazy.TorProtocolService.init();
- lazy.TorMonitorService.init();
-
+ await lazy.TorProviderBuilder.init();
lazy.TorSettings.init();
lazy.TorConnect.init();
@@ -59,17 +52,11 @@ export class TorStartupService {
gInited = true;
}
- _uninit() {
+ #uninit() {
Services.obs.removeObserver(this, BrowserTopics.QuitApplicationGranted);
lazy.TorDomainIsolator.uninit();
-
- // Close any helper connection first...
- lazy.TorProtocolService.uninit();
- // ... and only then closes the event monitor connection, which will cause
- // Tor to stop.
- lazy.TorMonitorService.uninit();
-
+ lazy.TorProviderBuilder.uninit();
lazy.TorLauncherUtil.cleanupTempDirectories();
}
}
=====================================
toolkit/components/tor-launcher/moz.build
=====================================
@@ -7,6 +7,7 @@ EXTRA_JS_MODULES += [
"TorParsers.sys.mjs",
"TorProcess.sys.mjs",
"TorProtocolService.sys.mjs",
+ "TorProviderBuilder.sys.mjs",
"TorStartupService.sys.mjs",
]
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/9722ca…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/9722ca…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] 2 commits: squash! Bug 40933: Add tor-launcher functionality
by Pier Angelo Vendrame (@pierov) 04 Aug '23
by Pier Angelo Vendrame (@pierov) 04 Aug '23
04 Aug '23
Pier Angelo Vendrame pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
57b25177 by Pier Angelo Vendrame at 2023-08-04T20:02:03+02:00
squash! Bug 40933: Add tor-launcher functionality
Bug 41926: Reimplement the control port
- - - - -
9722ca26 by Pier Angelo Vendrame at 2023-08-04T20:02:04+02:00
fixup! Bug 10760: Integrate TorButton to TorBrowser core
Removed torbutton.js, tor-control-port.js and utils.js.
- - - - -
13 changed files:
- browser/base/content/browser.xhtml
- + toolkit/components/tor-launcher/TorControlPort.sys.mjs
- toolkit/components/tor-launcher/TorMonitorService.sys.mjs
- toolkit/components/tor-launcher/TorProtocolService.sys.mjs
- toolkit/components/tor-launcher/moz.build
- − toolkit/torbutton/chrome/content/torbutton.js
- − toolkit/torbutton/components.conf
- toolkit/torbutton/jar.mn
- − toolkit/torbutton/modules/TorbuttonLogger.jsm
- − toolkit/torbutton/modules/tor-control-port.js
- − toolkit/torbutton/modules/utils.js
- toolkit/torbutton/moz.build
- tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
Changes:
=====================================
browser/base/content/browser.xhtml
=====================================
@@ -130,17 +130,11 @@
Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/languageNotification.js", this);
- Services.scriptloader.loadSubScript("chrome://torbutton/content/torbutton.js", this);
window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
window.onclose = WindowIsClosing;
- //onLoad Handler
- try {
- window.addEventListener("load", torbutton_init);
- } catch (e) {}
-
window.addEventListener("MozBeforeInitialXULLayout",
gBrowserInit.onBeforeInitialXULLayout.bind(gBrowserInit), { once: true });
=====================================
toolkit/components/tor-launcher/TorControlPort.sys.mjs
=====================================
@@ -0,0 +1,1534 @@
+import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
+
+/**
+ * @callback MessageCallback A callback to receive messages from the control
+ * port.
+ * @param {string} message The message to handle
+ */
+/**
+ * @callback RemoveCallback A function used to remove a previously registered
+ * callback.
+ */
+
+class CallbackDispatcher {
+ #callbackPairs = [];
+
+ /**
+ * Register a callback to handle a certain type of responses.
+ *
+ * @param {RegExp} regex The regex that tells which messages the callback
+ * wants to handle.
+ * @param {MessageCallback} callback The function to call
+ * @returns {RemoveCallback} A function to remove the just added callback
+ */
+ addCallback(regex, callback) {
+ this.#callbackPairs.push([regex, callback]);
+ }
+
+ /**
+ * Push a certain message to all the callbacks whose regex matches it.
+ *
+ * @param {string} message The message to push to the callbacks
+ */
+ pushMessage(message) {
+ for (const [regex, callback] of this.#callbackPairs) {
+ if (message.match(regex)) {
+ callback(message);
+ }
+ }
+ }
+}
+
+/**
+ * A wrapper around XPCOM sockets and buffers to handle streams in a standard
+ * async JS fashion.
+ * This class can handle both Unix sockets and TCP sockets.
+ */
+class AsyncSocket {
+ /**
+ * The output stream used for write operations.
+ *
+ * @type {nsIAsyncOutputStream}
+ */
+ #outputStream;
+ /**
+ * The output stream can only have one registered callback at a time, so
+ * multiple writes need to be queued up (see nsIAsyncOutputStream.idl).
+ * Every item is associated with a promise we returned in write, and it will
+ * resolve it or reject it when called by the output stream.
+ *
+ * @type {nsIOutputStreamCallback[]}
+ */
+ #outputQueue = [];
+ /**
+ * The input stream.
+ *
+ * @type {nsIAsyncInputStream}
+ */
+ #inputStream;
+ /**
+ * An input stream adapter that makes reading from scripts easier.
+ *
+ * @type {nsIScriptableInputStream}
+ */
+ #scriptableInputStream;
+ /**
+ * The queue of callbacks to be used when we receive data.
+ * Every item is associated with a promise we returned in read, and it will
+ * resolve it or reject it when called by the input stream.
+ *
+ * @type {nsIInputStreamCallback[]}
+ */
+ #inputQueue = [];
+
+ /**
+ * Connect to a Unix socket. Not available on Windows.
+ *
+ * @param {nsIFile} ipcFile The path to the Unix socket to connect to.
+ */
+ static fromIpcFile(ipcFile) {
+ const sts = Cc[
+ "@mozilla.org/network/socket-transport-service;1"
+ ].getService(Ci.nsISocketTransportService);
+ const socket = new AsyncSocket();
+ const transport = sts.createUnixDomainTransport(ipcFile);
+ socket.#createStreams(transport);
+ return socket;
+ }
+
+ /**
+ * Connect to a TCP socket.
+ *
+ * @param {string} host The hostname to connect the TCP socket to.
+ * @param {number} port The port to connect the TCP socket to.
+ */
+ static fromSocketAddress(host, port) {
+ const sts = Cc[
+ "@mozilla.org/network/socket-transport-service;1"
+ ].getService(Ci.nsISocketTransportService);
+ const socket = new AsyncSocket();
+ const transport = sts.createTransport([], host, port, null, null);
+ socket.#createStreams(transport);
+ return socket;
+ }
+
+ #createStreams(socketTransport) {
+ const OPEN_UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED;
+ this.#outputStream = socketTransport
+ .openOutputStream(OPEN_UNBUFFERED, 1, 1)
+ .QueryInterface(Ci.nsIAsyncOutputStream);
+
+ this.#inputStream = socketTransport
+ .openInputStream(OPEN_UNBUFFERED, 1, 1)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ this.#scriptableInputStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ this.#scriptableInputStream.init(this.#inputStream);
+ }
+
+ /**
+ * Asynchronously write string to underlying socket.
+ *
+ * When write is called, we create a new promise and queue it on the output
+ * queue. If it is the only element in the queue, we ask the output stream to
+ * run it immediately.
+ * Otherwise, the previous item of the queue will run it after it finishes.
+ *
+ * @param {string} str The string to write to the socket. The underlying
+ * implementation shoulw convert JS strings (UTF-16) into UTF-8 strings.
+ * See also write nsIOutputStream (the first argument is a string, not a
+ * wstring).
+ * @returns {Promise<number>} The number of written bytes
+ */
+ async write(str) {
+ return new Promise((resolve, reject) => {
+ // asyncWait next write request
+ const tryAsyncWait = () => {
+ if (this.#outputQueue.length) {
+ this.#outputStream.asyncWait(
+ this.#outputQueue.at(0), // next request
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ }
+ };
+
+ // Implement an nsIOutputStreamCallback: write the string once possible,
+ // and then start running the following queue item, if any.
+ this.#outputQueue.push({
+ onOutputStreamReady: () => {
+ try {
+ const bytesWritten = this.#outputStream.write(str, str.length);
+
+ // remove this callback object from queue as it is now completed
+ this.#outputQueue.shift();
+
+ // request next wait if there is one
+ tryAsyncWait();
+
+ // finally resolve promise
+ resolve(bytesWritten);
+ } catch (err) {
+ // reject promise on error
+ reject(err);
+ }
+ },
+ });
+
+ // Length 1 imples that there is no in-flight asyncWait, so we may
+ // immediately follow through on this write.
+ if (this.#outputQueue.length === 1) {
+ tryAsyncWait();
+ }
+ });
+ }
+
+ /**
+ * Asynchronously read string from underlying socket and return it.
+ *
+ * When read is called, we create a new promise and queue it on the input
+ * queue. If it is the only element in the queue, we ask the input stream to
+ * run it immediately.
+ * Otherwise, the previous item of the queue will run it after it finishes.
+ *
+ * This function is expected to throw when the underlying socket has been
+ * closed.
+ *
+ * @returns {Promise<string>} The read string
+ */
+ async read() {
+ return new Promise((resolve, reject) => {
+ const tryAsyncWait = () => {
+ if (this.#inputQueue.length) {
+ this.#inputStream.asyncWait(
+ this.#inputQueue.at(0), // next input request
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ }
+ };
+
+ this.#inputQueue.push({
+ onInputStreamReady: stream => {
+ try {
+ if (!this.#scriptableInputStream.available()) {
+ // This means EOF, but not closed yet. However, arriving at EOF
+ // should be an error condition for us, since we are in a socket,
+ // and EOF should mean peer disconnected.
+ // If the stream has been closed, this function itself should
+ // throw.
+ reject(
+ new Error("onInputStreamReady called without available bytes.")
+ );
+ return;
+ }
+
+ // Read our string from input stream.
+ const str = this.#scriptableInputStream.read(
+ this.#scriptableInputStream.available()
+ );
+
+ // Remove this callback object from queue now that we have read.
+ this.#inputQueue.shift();
+
+ // Start waiting for incoming data again if the reading queue is not
+ // empty.
+ tryAsyncWait();
+
+ // Finally resolve the promise.
+ resolve(str);
+ } catch (err) {
+ // E.g., we received a NS_BASE_STREAM_CLOSED because the socket was
+ // closed.
+ reject(err);
+ }
+ },
+ });
+
+ // Length 1 imples that there is no in-flight asyncWait, so we may
+ // immediately follow through on this read.
+ if (this.#inputQueue.length === 1) {
+ tryAsyncWait();
+ }
+ });
+ }
+
+ /**
+ * Close the streams.
+ */
+ close() {
+ this.#outputStream.close();
+ this.#inputStream.close();
+ }
+}
+
+/**
+ * @typedef Command
+ * @property {string} commandString The string to send over the control port
+ * @property {Function} resolve The function to resolve the promise with the
+ * response we got on the control port
+ * @property {Function} reject The function to reject the promise associated to
+ * the command
+ */
+
+class TorError extends Error {
+ constructor(command, reply) {
+ super(`${command} -> ${reply}`);
+ this.name = "TorError";
+ const info = reply.match(/(?<code>\d{3})(?:\s(?<message>.+))?/);
+ this.torStatusCode = info.groups.code;
+ if (info.groups.message) {
+ this.torMessage = info.groups.message;
+ }
+ }
+}
+
+class ControlSocket {
+ /**
+ * The socket to write to the control port.
+ *
+ * @type {AsyncSocket}
+ */
+ #socket;
+
+ /**
+ * The dispatcher used for the data we receive over the control port.
+ *
+ * @type {CallbackDispatcher}
+ */
+ #mainDispatcher = new CallbackDispatcher();
+ /**
+ * A secondary dispatcher used only to dispatch aynchronous events.
+ *
+ * @type {CallbackDispatcher}
+ */
+ #notificationDispatcher = new CallbackDispatcher();
+
+ /**
+ * Data we received on a read but that was not a complete line (missing a
+ * final CRLF). We will prepend it to the next read.
+ *
+ * @type {string}
+ */
+ #pendingData = "";
+ /**
+ * The lines we received and are still queued for being evaluated.
+ *
+ * @type {string[]}
+ */
+ #pendingLines = [];
+ /**
+ * The commands that need to be run or receive a response.
+ *
+ * @type {Command[]}
+ */
+ #commandQueue = [];
+
+ constructor(asyncSocket) {
+ this.#socket = asyncSocket;
+
+ // #mainDispatcher pushes only async notifications (650) to
+ // #notificationDispatcher
+ this.#mainDispatcher.addCallback(
+ /^650/,
+ this.#handleNotification.bind(this)
+ );
+ // callback for handling responses and errors
+ this.#mainDispatcher.addCallback(
+ /^[245]\d\d/,
+ this.#handleCommandReply.bind(this)
+ );
+
+ this.#startMessagePump();
+ }
+
+ /**
+ * Return the next line in the queue. If there is not any, block until one is
+ * read (or until a communication error happens, including the underlying
+ * socket being closed while it was still waiting for data).
+ * Any letfovers will be prepended to the next read.
+ *
+ * @returns {Promise<string>} A line read over the socket
+ */
+ async #readLine() {
+ // Keep reading from socket until we have at least a full line to return.
+ while (!this.#pendingLines.length) {
+ if (!this.#socket) {
+ throw new Error(
+ "Read interrupted because the control socket is not available anymore"
+ );
+ }
+ // Read data from our socket and split on newline tokens.
+ // This might still throw when the socket has been closed.
+ this.#pendingData += await this.#socket.read();
+ const lines = this.#pendingData.split("\r\n");
+ // The last line will either be empty string, or a partial read of a
+ // response/event so save it off for the next socket read.
+ this.#pendingData = lines.pop();
+ // Copy remaining full lines to our pendingLines list.
+ this.#pendingLines = this.#pendingLines.concat(lines);
+ }
+ return this.#pendingLines.shift();
+ }
+
+ /**
+ * Blocks until an entire message is ready and returns it.
+ * This function does a rudimentary parsing of the data only to handle
+ * multi-line responses.
+ *
+ * @returns {Promise<string>} The read message (without the final CRLF)
+ */
+ async #readMessage() {
+ // whether we are searching for the end of a multi-line values
+ // See control-spec section 3.9
+ let handlingMultlineValue = false;
+ let endOfMessageFound = false;
+ const message = [];
+
+ do {
+ const line = await this.#readLine();
+ message.push(line);
+
+ if (handlingMultlineValue) {
+ // look for end of multiline
+ if (line === ".") {
+ handlingMultlineValue = false;
+ }
+ } else {
+ // 'Multiline values' are possible. We avoid interrupting one by
+ // detecting it and waiting for a terminating "." on its own line.
+ // (See control-spec section 3.9 and
+ // https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/16990#n…)
+ // Ensure this is the first line of a new message
+ // eslint-disable-next-line no-lonely-if
+ if (message.length === 1 && line.match(/^\d\d\d\+.+?=$/)) {
+ handlingMultlineValue = true;
+ }
+ // look for end of message (notice the space character at end of the
+ // regex!)
+ else if (line.match(/^\d\d\d /)) {
+ if (message.length === 1) {
+ endOfMessageFound = true;
+ } else {
+ const firstReplyCode = message[0].substring(0, 3);
+ const lastReplyCode = line.substring(0, 3);
+ endOfMessageFound = firstReplyCode === lastReplyCode;
+ }
+ }
+ }
+ } while (!endOfMessageFound);
+
+ // join our lines back together to form one message
+ return message.join("\r\n");
+ }
+
+ /**
+ * Read messages on the socket and routed them to a dispatcher until the
+ * socket is open or some error happens (including the underlying socket being
+ * closed).
+ */
+ async #startMessagePump() {
+ try {
+ // This while is inside the try block because it is very likely that it
+ // will be broken by a NS_BASE_STREAM_CLOSED exception, rather than by its
+ // condition becoming false.
+ while (this.#socket) {
+ const message = await this.#readMessage();
+ // log("controlPort >> " + message);
+ this.#mainDispatcher.pushMessage(message);
+ }
+ } catch (err) {
+ try {
+ this.#close(err);
+ } catch (ec) {
+ console.error(
+ "Caught another error while closing the control socket.",
+ ec
+ );
+ }
+ }
+ }
+
+ /**
+ * Start running the first available command in the queue.
+ * To be called when the previous one has finished running.
+ * This makes sure to avoid conflicts when using the control port.
+ */
+ #writeNextCommand() {
+ const cmd = this.#commandQueue[0];
+ // log("controlPort << " + cmd.commandString);
+ this.#socket.write(`${cmd.commandString}\r\n`).catch(cmd.reject);
+ }
+
+ /**
+ * Send a command over the control port.
+ * This function returns only when it receives a complete message over the
+ * control port. This class does some rudimentary parsing to check wheter it
+ * needs to handle multi-line messages.
+ *
+ * @param {string} commandString
+ * @returns {Promise<string>} The message sent by the control port. It will
+ * always start with 2xx. In case of other codes the function will throw,
+ * instead. This means that the return value will never be an empty string
+ * (even though it will not include the final CRLF).
+ */
+ async sendCommand(commandString) {
+ if (!this.#socket) {
+ throw new Error("ControlSocket not open");
+ }
+
+ // this promise is resolved either in #handleCommandReply, or in
+ // #startMessagePump (on stream error)
+ return new Promise((resolve, reject) => {
+ const command = {
+ commandString,
+ resolve,
+ reject,
+ };
+ this.#commandQueue.push(command);
+ if (this.#commandQueue.length === 1) {
+ this.#writeNextCommand();
+ }
+ });
+ }
+
+ /**
+ * Handles a message starting with 2xx, 4xx, or 5xx.
+ * This function should be used only as a callback for the main dispatcher.
+ *
+ * @param {string} message The message to handle
+ */
+ #handleCommandReply(message) {
+ const cmd = this.#commandQueue.shift();
+ if (message[0] === "2") {
+ cmd.resolve(message);
+ } else if (message.match(/^[45]/)) {
+ cmd.reject(new TorError(cmd.commandString, message));
+ } else {
+ // This should never happen, as the dispatcher should filter the messages
+ // already.
+ cmd.reject(
+ new Error(`Received unexpected message:\n----\n${message}\n----`)
+ );
+ }
+
+ // send next command if one is available
+ if (this.#commandQueue.length) {
+ this.#writeNextCommand();
+ }
+ }
+
+ /**
+ * Re-route an event message to the notification dispatcher.
+ * This function should be used only as a callback for the main dispatcher.
+ *
+ * @param {string} message The message received on the control port
+ */
+ #handleNotification(message) {
+ try {
+ this.#notificationDispatcher.pushMessage(message);
+ } catch (e) {
+ console.error("An event watcher threw", e);
+ }
+ }
+
+ /**
+ * Reject all the commands that are still in queue and close the control
+ * socket.
+ *
+ * @param {object?} reason An error object used to pass a more specific
+ * rejection reason to the commands that are still queued.
+ */
+ #close(reason) {
+ const error = new Error(
+ "The control socket has been closed" +
+ (reason ? `: ${reason.message}` : "")
+ );
+ const commands = this.#commandQueue;
+ this.#commandQueue = [];
+ for (const cmd of commands) {
+ cmd.reject(error);
+ }
+ try {
+ this.#socket?.close();
+ } finally {
+ this.#socket = null;
+ }
+ }
+
+ /**
+ * Closes the socket connected to the control port.
+ */
+ close() {
+ this.#close(null);
+ }
+
+ /**
+ * Register an event watcher.
+ *
+ * @param {RegExp} regex The regex to filter on messages to receive
+ * @param {MessageCallback} callback The callback for the messages
+ */
+ addNotificationCallback(regex, callback) {
+ this.#notificationDispatcher.addCallback(regex, callback);
+ }
+
+ /**
+ * Tells whether the underlying socket is still open.
+ */
+ get isOpen() {
+ return !!this.#socket;
+ }
+}
+
+// ## utils
+// A namespace for utility functions
+let utils = {};
+
+// __utils.identity(x)__.
+// Returns its argument unchanged.
+utils.identity = function (x) {
+ return x;
+};
+
+// __utils.capture(string, regex)__.
+// Takes a string and returns an array of capture items, where regex must have a single
+// capturing group and use the suffix /.../g to specify a global search.
+utils.capture = function (string, regex) {
+ let matches = [];
+ // Special trick to use string.replace for capturing multiple matches.
+ string.replace(regex, function (a, captured) {
+ matches.push(captured);
+ });
+ return matches;
+};
+
+// __utils.extractor(regex)__.
+// Returns a function that takes a string and returns an array of regex matches. The
+// regex must use the suffix /.../g to specify a global search.
+utils.extractor = function (regex) {
+ return function (text) {
+ return utils.capture(text, regex);
+ };
+};
+
+// __utils.splitLines(string)__.
+// Splits a string into an array of strings, each corresponding to a line.
+utils.splitLines = function (string) {
+ return string.split(/\r?\n/);
+};
+
+// __utils.splitAtSpaces(string)__.
+// Splits a string into chunks between spaces. Does not split at spaces
+// inside pairs of quotation marks.
+utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);
+
+// __utils.splitAtFirst(string, regex)__.
+// Splits a string at the first instance of regex match. If no match is
+// found, returns the whole string.
+utils.splitAtFirst = function (string, regex) {
+ let match = string.match(regex);
+ return match
+ ? [
+ string.substring(0, match.index),
+ string.substring(match.index + match[0].length),
+ ]
+ : string;
+};
+
+// __utils.splitAtEquals(string)__.
+// Splits a string into chunks between equals. Does not split at equals
+// inside pairs of quotation marks.
+utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g);
+
+// __utils.mergeObjects(arrayOfObjects)__.
+// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object.
+// Pure function.
+utils.mergeObjects = function (arrayOfObjects) {
+ let result = {};
+ for (let obj of arrayOfObjects) {
+ for (let key in obj) {
+ result[key] = obj[key];
+ }
+ }
+ return result;
+};
+
+// __utils.listMapData(parameterString, listNames)__.
+// Takes a list of parameters separated by spaces, of which the first several are
+// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames
+// to the unnamed parameters, and combine them in a map with the named parameters.
+// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH`
+//
+// utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH",
+// ["streamID", "event", "circuitID", "IP"])
+// // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0",
+// // "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}"
+utils.listMapData = function (parameterString, listNames) {
+ // Split out the space-delimited parameters.
+ let parameters = utils.splitAtSpaces(parameterString),
+ dataMap = {};
+ // Assign listNames to the first n = listNames.length parameters.
+ for (let i = 0; i < listNames.length; ++i) {
+ dataMap[listNames[i]] = parameters[i];
+ }
+ // Read key-value pairs and copy these to the dataMap.
+ for (let i = listNames.length; i < parameters.length; ++i) {
+ let [key, value] = utils.splitAtEquals(parameters[i]);
+ if (key && value) {
+ dataMap[key] = value;
+ }
+ }
+ return dataMap;
+};
+
+// ## info
+// A namespace for functions related to tor's GETINFO and GETCONF command.
+let info = {};
+
+// __info.keyValueStringsFromMessage(messageText)__.
+// Takes a message (text) response to GETINFO or GETCONF and provides
+// a series of key-value strings, which are either multiline (with a `250+` prefix):
+//
+// 250+config/defaults=
+// AccountingMax "0 bytes"
+// AllowDotExit "0"
+// .
+//
+// or single-line (with a `250-` or `250 ` prefix):
+//
+// 250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
+info.keyValueStringsFromMessage = utils.extractor(
+ /^(250\+[\s\S]+?^\.|250[- ].+?)$/gim
+);
+
+// __info.applyPerLine(transformFunction)__.
+// Returns a function that splits text into lines,
+// and applies transformFunction to each line.
+info.applyPerLine = function (transformFunction) {
+ return function (text) {
+ return utils.splitLines(text.trim()).map(transformFunction);
+ };
+};
+
+// __info.routerStatusParser(valueString)__.
+// Parses a router status entry as, described in
+// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
+// (search for "router status entry")
+info.routerStatusParser = function (valueString) {
+ let lines = utils.splitLines(valueString),
+ objects = [];
+ for (let line of lines) {
+ // Drop first character and grab data following it.
+ let myData = line.substring(2),
+ // Accumulate more maps with data, depending on the first character in the line.
+ dataFun = {
+ r: data =>
+ utils.listMapData(data, [
+ "nickname",
+ "identity",
+ "digest",
+ "publicationDate",
+ "publicationTime",
+ "IP",
+ "ORPort",
+ "DirPort",
+ ]),
+ a: data => ({ IPv6: data }),
+ s: data => ({ statusFlags: utils.splitAtSpaces(data) }),
+ v: data => ({ version: data }),
+ w: data => utils.listMapData(data, []),
+ p: data => ({ portList: data.split(",") }),
+ }[line.charAt(0)];
+ if (dataFun !== undefined) {
+ objects.push(dataFun(myData));
+ }
+ }
+ return utils.mergeObjects(objects);
+};
+
+// __info.circuitStatusParser(line)__.
+// Parse the output of a circuit status line.
+info.circuitStatusParser = function (line) {
+ let data = utils.listMapData(line, ["id", "status", "circuit"]),
+ circuit = data.circuit;
+ // Parse out the individual circuit IDs and names.
+ if (circuit) {
+ data.circuit = circuit.split(",").map(function (x) {
+ return x.split(/~|=/);
+ });
+ }
+ return data;
+};
+
+// __info.streamStatusParser(line)__.
+// Parse the output of a stream status line.
+info.streamStatusParser = function (text) {
+ return utils.listMapData(text, [
+ "StreamID",
+ "StreamStatus",
+ "CircuitID",
+ "Target",
+ ]);
+};
+
+// TODO: fix this parsing logic to handle bridgeLine correctly
+// fingerprint/id is an optional parameter
+// __info.bridgeParser(bridgeLine)__.
+// Takes a single line from a `getconf bridge` result and returns
+// a map containing the bridge's type, address, and ID.
+info.bridgeParser = function (bridgeLine) {
+ let result = {},
+ tokens = bridgeLine.split(/\s+/);
+ // First check if we have a "vanilla" bridge:
+ if (tokens[0].match(/^\d+\.\d+\.\d+\.\d+/)) {
+ result.type = "vanilla";
+ [result.address, result.ID] = tokens;
+ // Several bridge types have a similar format:
+ } else {
+ result.type = tokens[0];
+ if (
+ [
+ "flashproxy",
+ "fte",
+ "meek",
+ "meek_lite",
+ "obfs3",
+ "obfs4",
+ "scramblesuit",
+ "snowflake",
+ ].includes(result.type)
+ ) {
+ [result.address, result.ID] = tokens.slice(1);
+ }
+ }
+ return result.type ? result : null;
+};
+
+// __info.parsers__.
+// A map of GETINFO and GETCONF keys to parsing function, which convert
+// result strings to JavaScript data.
+info.parsers = {
+ "ns/id/": info.routerStatusParser,
+ "ip-to-country/": utils.identity,
+ "circuit-status": info.applyPerLine(info.circuitStatusParser),
+ bridge: info.bridgeParser,
+ // Currently unused parsers:
+ // "ns/name/" : info.routerStatusParser,
+ // "stream-status" : info.applyPerLine(info.streamStatusParser),
+ // "version" : utils.identity,
+ // "config-file" : utils.identity,
+};
+
+// __info.getParser(key)__.
+// Takes a key and determines the parser function that should be used to
+// convert its corresponding valueString to JavaScript data.
+info.getParser = function (key) {
+ return (
+ info.parsers[key] ||
+ info.parsers[key.substring(0, key.lastIndexOf("/") + 1)]
+ );
+};
+
+// __info.stringToValue(string)__.
+// Converts a key-value string as from GETINFO or GETCONF to a value.
+info.stringToValue = function (string) {
+ // key should look something like `250+circuit-status=` or `250-circuit-status=...`
+ // or `250 circuit-status=...`
+ let matchForKey = string.match(/^250[ +-](.+?)=/),
+ key = matchForKey ? matchForKey[1] : null;
+ if (key === null) {
+ return null;
+ }
+ // matchResult finds a single-line result for `250-` or `250 `,
+ // or a multi-line one for `250+`.
+ let matchResult =
+ string.match(/^250[ -].+?=(.*)$/) ||
+ string.match(/^250\+.+?=([\s\S]*?)^\.$/m),
+ // Retrieve the captured group (the text of the value in the key-value pair)
+ valueString = matchResult ? matchResult[1] : null,
+ // Get the parser function for the key found.
+ parse = info.getParser(key.toLowerCase());
+ if (parse === undefined) {
+ throw new Error("No parser found for '" + key + "'");
+ }
+ // Return value produced by the parser.
+ return parse(valueString);
+};
+
+/**
+ * @typedef {object} Bridge
+ * @property {string} transport The transport of the bridge, or vanilla if not
+ * specified.
+ * @property {string} addr The IP address and port of the bridge
+ * @property {string} id The fingerprint of the bridge
+ * @property {string} args Optional arguments passed to the bridge
+ */
+/**
+ * @typedef {object} PTInfo The information about a pluggable transport
+ * @property {string[]} transports An array with all the transports supported by
+ * this configuration.
+ * @property {string} type Either socks4, socks5 or exec
+ * @property {string} [ip] The IP address of the proxy (only for socks4 and
+ * socks5)
+ * @property {integer} [port] The port of the proxy (only for socks4 and socks5)
+ * @property {string} [pathToBinary] Path to the binary that is run (only for
+ * exec)
+ * @property {string} [options] Optional options passed to the binary (only for
+ * exec)
+ */
+/**
+ * @typedef {object} OnionAuthKeyInfo
+ * @property {string} address The address of the onion service
+ * @property {string} typeAndKey Onion service key and type of key, as
+ * `type:base64-private-key`
+ * @property {string} Flags Additional flags, such as Permanent
+ */
+/**
+ * @callback EventFilterCallback
+ * @param {any} data Either a raw string, or already parsed data
+ * @returns {boolean}
+ */
+/**
+ * @callback EventCallback
+ * @param {any} data Either a raw string, or already parsed data
+ */
+
+class TorController {
+ /**
+ * The control socket
+ *
+ * @type {ControlSocket}
+ */
+ #socket;
+
+ /**
+ * A map of EVENT keys to parsing functions, which convert result strings to
+ * JavaScript data.
+ */
+ #eventParsers = {
+ stream: info.streamStatusParser,
+ // Currently unused:
+ // "circ" : info.circuitStatusParser,
+ };
+
+ /**
+ * Builds a new TorController.
+ *
+ * @param {AsyncSocket} socket The socket to communicate to the control port
+ */
+ constructor(socket) {
+ this.#socket = new ControlSocket(socket);
+ }
+
+ /**
+ * Tells whether the underlying socket is open.
+ *
+ * @returns {boolean}
+ */
+ get isOpen() {
+ return this.#socket.isOpen;
+ }
+
+ /**
+ * Close the underlying socket.
+ */
+ close() {
+ this.#socket.close();
+ }
+
+ /**
+ * Send a command over the control port.
+ * TODO: Make this function private, and force the operations to go through
+ * specialized methods.
+ *
+ * @param {string} cmd The command to send
+ * @returns {Promise<string>} A 2xx response obtained from the control port.
+ * For other codes, this function will throw. The returned string will never
+ * be empty.
+ */
+ async sendCommand(cmd) {
+ return this.#socket.sendCommand(cmd);
+ }
+
+ /**
+ * Send a simple command whose response is expected to be simply a "250 OK".
+ * The function will not return a reply, but will throw if an unexpected one
+ * is received.
+ *
+ * @param {string} command The command to send
+ */
+ async #sendCommandSimple(command) {
+ const reply = await this.sendCommand(command);
+ if (!/^250 OK\s*$/i.test(reply)) {
+ throw new TorError(command, reply);
+ }
+ }
+
+ /**
+ * Authenticate to the tor daemon.
+ * Notice that a failure in the authentication makes the connection close.
+ *
+ * @param {string} password The password for the control port.
+ */
+ async authenticate(password) {
+ if (password) {
+ this.#expectString(password, "password");
+ }
+ await this.#sendCommandSimple(`authenticate ${password || ""}`);
+ }
+
+ /**
+ * Sends a GETINFO for a single key.
+ *
+ * @param {string} key The key to get value for
+ * @returns {any} The return value depends on the requested key
+ */
+ async getInfo(key) {
+ this.#expectString(key, "key");
+ const response = await this.sendCommand(`getinfo ${key}`);
+ return this.#getMultipleResponseValues(response)[0];
+ }
+
+ /**
+ * Sends a GETINFO for a single key.
+ * control-spec.txt says "one ReplyLine is sent for each requested value", so,
+ * we expect to receive only one line starting with `250-keyword=`, or one
+ * line starting with `250+keyword=` (in which case we will match until a
+ * period).
+ * This function could be possibly extended to handle several keys at once,
+ * but we currently do not need this functionality, so we preferred keeping
+ * the function simpler.
+ *
+ * @param {string} key The key to get value for
+ * @returns {Promise<string>} The string we received (only the value, without
+ * the key). We do not do any additional parsing on it.
+ */
+ async #getInfo(key) {
+ this.#expectString(key);
+ const cmd = `GETINFO ${key}`;
+ const reply = await this.sendCommand(cmd);
+ const match =
+ reply.match(/^250-([^=]+)=(.*)$/m) ||
+ reply.match(/^250\+([^=]+)=([\s\S]*?)^\.\r?\n^250 OK\s*$/m);
+ if (!match || match[1] !== key) {
+ throw new TorError(cmd, reply);
+ }
+ return match[2];
+ }
+
+ /**
+ * Ask Tor its bootstrap phase.
+ *
+ * @returns {object} An object with the bootstrap information received from
+ * Tor. Its keys might vary, depending on the input
+ */
+ async getBootstrapPhase() {
+ return this.#parseBootstrapStatus(
+ await this.#getInfo("status/bootstrap-phase")
+ );
+ }
+
+ /**
+ * Get the IPv4 and optionally IPv6 addresses of an onion router.
+ *
+ * @param {NodeFingerprint} id The fingerprint of the node the caller is
+ * interested in
+ * @returns {string[]} The IP addresses (one IPv4 and optionally an IPv6)
+ */
+ async getNodeAddresses(id) {
+ this.#expectString(id, "id");
+ const reply = await this.#getInfo(`ns/id/${id}`);
+ // See dir-spec.txt.
+ // r nickname identity digest publication IP OrPort DirPort
+ const rLine = reply.match(/^r\s+(.*)$/m);
+ const v4 = rLine ? rLine[1].split(/\s+/) : [];
+ // Tor should already reply with a 552 when a relay cannot be found.
+ // Also, publication is a date with a space inside, so it is counted twice.
+ if (!rLine || v4.length !== 8) {
+ throw new Error(`Received an invalid node information: ${reply}`);
+ }
+ const addresses = [v4[5]];
+ // a address:port
+ // dir-spec.txt also states only the first one should be taken
+ // TODO: The consumers do not care about the port or the square brackets
+ // either. Remove them when integrating this function with the rest
+ const v6 = reply.match(/^a\s+(\[[0-9a-fA-F:]+\]:[0-9]{1,5})$/m);
+ if (v6) {
+ addresses.push(v6[1]);
+ }
+ return addresses;
+ }
+
+ /**
+ * Maps IP addresses to 2-letter country codes, or ?? if unknown.
+ *
+ * @param {string} ip The IP address to look for
+ * @returns {Promise<string>} A promise with the country code. If unknown, the
+ * promise is resolved with "??". It is rejected only when the underlying
+ * GETINFO command fails or if an exception is thrown
+ */
+ async getIPCountry(ip) {
+ this.#expectString(ip, "ip");
+ return this.#getInfo(`ip-to-country/${ip}`);
+ }
+
+ /**
+ * Ask tor which ports it is listening to for SOCKS connections.
+ *
+ * @returns {Promise<string[]>} An array of addresses. It might be empty
+ * (e.g., when DisableNetwork is set)
+ */
+ async getSocksListeners() {
+ const listeners = await this.#getInfo("net/listeners/socks");
+ return Array.from(listeners.matchAll(/\s*("(?:[^"\\]|\\.)*"|\S+)\s*/g), m =>
+ TorParsers.unescapeString(m[1])
+ );
+ }
+
+ // Configuration
+
+ /**
+ * Sends a GETCONF for a single key.
+ * GETCONF with a single argument returns results with one or more lines that
+ * look like `250[- ]key=value`.
+ * Any GETCONF lines that contain a single keyword only are currently dropped.
+ * So we can use similar parsing to that for getInfo.
+ *
+ * @param {string} key The key to get value for
+ * @returns {any} A parsed config value (it depends if a parser is known)
+ */
+ async getConf(key) {
+ this.#expectString(key, "key");
+ return this.#getMultipleResponseValues(
+ await this.sendCommand(`getconf ${key}`)
+ );
+ }
+
+ /**
+ * Sends a GETCONF for a single key.
+ * The function could be easily generalized to get multiple keys at once, but
+ * we do not need this functionality, at the moment.
+ *
+ * @param {string} key The keys to get info for
+ * @returns {Promise<string[]>} The values obtained from the control port.
+ * The key is removed, and the values unescaped, but they are not parsed.
+ * The array might contain an empty string, which means that the default value
+ * is used.
+ */
+ async #getConf(key) {
+ this.#expectString(key, "key");
+ // GETCONF expects a `keyword`, which should be only alpha characters,
+ // according to the definition in control-port.txt. But as a matter of fact,
+ // several configuration keys include numbers (e.g., Socks4Proxy). So, we
+ // accept also numbers in this regular expression. One of the reason to
+ // sanitize the input is that we then use it to create a regular expression.
+ // Sadly, JavaScript does not provide a function to escape/quote a string
+ // for inclusion in a regex. Should we remove this limitation, we should
+ // also implement a regex sanitizer, or switch to another pattern, like
+ // `([^=])` and then filter on the keyword.
+ if (!/^[A-Za-z0-9]+$/.test(key)) {
+ throw new Error("The key can be composed only of letters and numbers.");
+ }
+ const cmd = `GETCONF ${key}`;
+ const reply = await this.sendCommand(cmd);
+ // From control-spec.txt: a 'default' value semantically different from an
+ // empty string will not have an equal sign, just `250 $key`.
+ const defaultRe = new RegExp(`^250[-\\s]${key}$`, "gim");
+ if (reply.match(defaultRe)) {
+ return [];
+ }
+ const re = new RegExp(`^250[-\\s]${key}=(.*)$`, "gim");
+ const values = Array.from(reply.matchAll(re), m =>
+ TorParsers.unescapeString(m[1])
+ );
+ if (!values.length) {
+ throw new TorError(cmd, reply);
+ }
+ return values;
+ }
+
+ /**
+ * Get the bridges Tor has been configured with.
+ *
+ * @returns {Bridge[]} The configured bridges
+ */
+ async getBridges() {
+ return (await this.#getConf("BRIDGE")).map(TorParsers.parseBridgeLine);
+ }
+
+ /**
+ * Get the configured pluggable transports.
+ *
+ * @returns {PTInfo[]} An array with the info of all the configured pluggable
+ * transports.
+ */
+ async getPluggableTransports() {
+ return (await this.#getConf("ClientTransportPlugin")).map(ptLine => {
+ // man 1 tor: ClientTransportPlugin transport socks4|socks5 IP:PORT
+ const socksLine = ptLine.match(
+ /(\S+)\s+(socks[45])\s+([\d.]{7,15}|\[[\da-fA-F:]+\]):(\d{1,5})/i
+ );
+ // man 1 tor: transport exec path-to-binary [options]
+ const execLine = ptLine.match(
+ /(\S+)\s+(exec)\s+("(?:[^"\\]|\\.)*"|\S+)\s*(.*)/i
+ );
+ if (socksLine) {
+ return {
+ transports: socksLine[1].split(","),
+ type: socksLine[2].toLowerCase(),
+ ip: socksLine[3],
+ port: parseInt(socksLine[4], 10),
+ };
+ } else if (execLine) {
+ return {
+ transports: execLine[1].split(","),
+ type: execLine[2].toLowerCase(),
+ pathToBinary: TorParsers.unescapeString(execLine[3]),
+ options: execLine[4],
+ };
+ }
+ throw new Error(
+ `Received an invalid ClientTransportPlugin line: ${ptLine}`
+ );
+ });
+ }
+
+ /**
+ * Send multiple configuration values to tor.
+ *
+ * @param {object} values The values to set
+ */
+ async setConf(values) {
+ const args = Object.entries(values)
+ .flatMap(([key, value]) => {
+ if (value === undefined || value === null) {
+ return [key];
+ }
+ if (Array.isArray(value)) {
+ return value.length
+ ? value.map(v => `${key}=${TorParsers.escapeString(v)}`)
+ : key;
+ } else if (typeof value === "string" || value instanceof String) {
+ return `${key}=${TorParsers.escapeString(value)}`;
+ } else if (typeof value === "boolean") {
+ return `${key}=${value ? "1" : "0"}`;
+ } else if (typeof value === "number") {
+ return `${key}=${value}`;
+ }
+ throw new Error(`Unsupported type ${typeof value} (key ${key})`);
+ })
+ .join(" ");
+ return this.#sendCommandSimple(`SETCONF ${args}`);
+ }
+
+ /**
+ * Enable or disable the network.
+ * Notice: switching from network disabled to network enabled will trigger a
+ * bootstrap on C tor! (Or stop the current one).
+ *
+ * @param {boolean} enabled Tell whether the network should be enabled
+ */
+ async setNetworkEnabled(enabled) {
+ return this.setConf({ DisableNetwork: !enabled });
+ }
+
+ /**
+ * Ask Tor to write out its config options into its torrc.
+ */
+ async flushSettings() {
+ return this.#sendCommandSimple("SAVECONF");
+ }
+
+ // Onion service authentication
+
+ /**
+ * Sends a ONION_CLIENT_AUTH_VIEW command to retrieve the list of private
+ * keys.
+ *
+ * @returns {OnionAuthKeyInfo[]}
+ */
+ async onionAuthViewKeys() {
+ const cmd = "onion_client_auth_view";
+ const message = await this.sendCommand(cmd);
+ // Either `250-CLIENT`, or `250 OK` if no keys are available.
+ if (!message.startsWith("250")) {
+ throw new TorError(cmd, message);
+ }
+ const re =
+ /^250-CLIENT\s+(?<HSAddress>[A-Za-z2-7]+)\s+(?<KeyType>[^:]+):(?<PrivateKeyBlob>\S+)(?:\s(?<other>.+))?$/gim;
+ return Array.from(message.matchAll(re), match => {
+ // TODO: Change the consumer and make the fields more consistent with what
+ // we get (e.g., separate key and type, and use a boolen for permanent).
+ const info = {
+ hsAddress: match.groups.HSAddress,
+ typeAndKey: `${match.groups.KeyType}:${match.groups.PrivateKeyBlob}`,
+ };
+ const maybeFlags = match.groups.other?.match(/Flags=(\S+)/);
+ if (maybeFlags) {
+ info.Flags = maybeFlags[1];
+ }
+ return info;
+ });
+ }
+
+ /**
+ * Sends an ONION_CLIENT_AUTH_ADD command to add a private key to the Tor
+ * configuration.
+ *
+ * @param {string} address The address of the onion service
+ * @param {string} b64PrivateKey The private key of the service, in base64
+ * @param {boolean} isPermanent Tell whether the key should be saved forever
+ */
+ async onionAuthAdd(address, b64PrivateKey, isPermanent) {
+ this.#expectString(address, "address");
+ this.#expectString(b64PrivateKey, "b64PrivateKey");
+ const keyType = "x25519";
+ let cmd = `onion_client_auth_add ${address} ${keyType}:${b64PrivateKey}`;
+ if (isPermanent) {
+ cmd += " Flags=Permanent";
+ }
+ const reply = await this.sendCommand(cmd);
+ const status = reply.substring(0, 3);
+ if (status !== "250" && status !== "251" && status !== "252") {
+ throw new TorError(cmd, reply);
+ }
+ }
+
+ /**
+ * Sends an ONION_CLIENT_AUTH_REMOVE command to remove a private key from the
+ * Tor configuration.
+ *
+ * @param {string} address The address of the onion service
+ */
+ async onionAuthRemove(address) {
+ this.#expectString(address, "address");
+ const cmd = `onion_client_auth_remove ${address}`;
+ const reply = await this.sendCommand(cmd);
+ const status = reply.substring(0, 3);
+ if (status !== "250" && status !== "251") {
+ throw new TorError(cmd, reply);
+ }
+ }
+
+ // Daemon ownership
+
+ /**
+ * Instructs Tor to shut down when this control connection is closed.
+ * If multiple connection sends this request, Tor will shut dwon when any of
+ * them is closed.
+ */
+ async takeOwnership() {
+ return this.#sendCommandSimple("TAKEOWNERSHIP");
+ }
+
+ /**
+ * The __OwningControllerProcess argument can be used to make Tor periodically
+ * check if a certain PID is still present, or terminate itself otherwise.
+ * When switching to the ownership tied to the control port, this mechanism
+ * should be stopped by calling this function.
+ */
+ async resetOwningControllerProcess() {
+ return this.#sendCommandSimple("RESETCONF __OwningControllerProcess");
+ }
+
+ // Signals
+
+ /**
+ * Ask Tor to swtich to new circuits and clear the DNS cache.
+ */
+ async newnym() {
+ return this.#sendCommandSimple("SIGNAL NEWNYM");
+ }
+
+ // Events monitoring
+
+ /**
+ * Enable receiving certain events.
+ * As per control-spec.txt, any events turned on in previous calls but not
+ * included in this one will be turned off.
+ *
+ * @param {string[]} types The events to enable. If empty, no events will be
+ * watched.
+ */
+ setEvents(types) {
+ if (!types.every(t => typeof t === "string" || t instanceof String)) {
+ throw new Error("Event types must be strings");
+ }
+ return this.#sendCommandSimple("SETEVENTS " + types.join(" "));
+ }
+
+ /**
+ * Watches for a particular type of asynchronous event.
+ * Notice: we only observe `"650" SP...` events, currently (no `650+...` or
+ * `650-...` events).
+ * Also, you need to enable the events in the control port with SETEVENTS,
+ * first.
+ *
+ * @param {string} type The event type to catch
+ * @param {EventFilterCallback?} filter An optional callback to filter
+ * events for which the callback will be called. If null, all events will be
+ * passed.
+ * @param {EventCallback} callback The callback that will handle the event
+ * @param {boolean} raw Tell whether to ignore the data parser, even if
+ * supported
+ */
+ watchEvent(type, filter, callback, raw = false) {
+ this.#expectString(type, "type");
+ const start = `650 ${type}`;
+ this.#socket.addNotificationCallback(new RegExp(`^${start}`), message => {
+ // Remove also the initial text
+ const dataText = message.substring(start.length + 1);
+ const parser = this.#eventParsers[type.toLowerCase()];
+ const data = dataText && parser ? parser(dataText) : null;
+ // FIXME: This is the original code, but we risk of not filtering on the
+ // data, if we ask for raw data (which we always do at the moment, but we
+ // do not use a filter either...)
+ if (filter === null || filter(data)) {
+ callback(data && !raw ? data : message);
+ }
+ });
+ }
+
+ // Other helpers
+
+ /**
+ * Parse a bootstrap status line.
+ *
+ * @param {string} line The line to parse, without the command/notification
+ * prefix
+ * @returns {object} An object with the bootstrap information received from
+ * Tor. Its keys might vary, depending on the input
+ */
+ #parseBootstrapStatus(line) {
+ const match = line.match(/^(NOTICE|WARN) BOOTSTRAP\s*(.*)/);
+ if (!match) {
+ throw Error(
+ `Received an invalid response for the bootstrap phase: ${line}`
+ );
+ }
+ const status = {
+ TYPE: match[1],
+ ...this.#getKeyValues(match[2]),
+ };
+ if (status.PROGRESS !== undefined) {
+ status.PROGRESS = parseInt(status.PROGRESS, 10);
+ }
+ if (status.COUNT !== undefined) {
+ status.COUNT = parseInt(status.COUNT, 10);
+ }
+ return status;
+ }
+
+ /**
+ * Throw an exception when value is not a string.
+ *
+ * @param {any} value The value to check
+ * @param {string} name The name of the `value` argument
+ */
+ #expectString(value, name) {
+ if (typeof value !== "string" && !(value instanceof String)) {
+ throw new Error(`The ${name} argument is expected to be a string.`);
+ }
+ }
+
+ /**
+ * Return an object with all the matches that are in the form `key="value"` or
+ * `key=value`. The values will be unescaped, but no additional parsing will
+ * be done (e.g., numbers will be returned as strings).
+ * If keys are repeated, only the last one will be taken.
+ *
+ * @param {string} str The string to match tokens in
+ * @returns {object} An object with all the various tokens. If none is found,
+ * an empty object is returned.
+ */
+ #getKeyValues(str) {
+ return Object.fromEntries(
+ Array.from(
+ str.matchAll(/\s*([^=]+)=("(?:[^"\\]|\\.)*"|\S+)\s*/g) || [],
+ pair => [pair[1], TorParsers.unescapeString(pair[2])]
+ )
+ );
+ }
+
+ /**
+ * Process multiple responses to a GETINFO or GETCONF request.
+ *
+ * @param {string} message The message to process
+ * @returns {object[]} The keys depend on the message
+ */
+ #getMultipleResponseValues(message) {
+ return info
+ .keyValueStringsFromMessage(message)
+ .map(info.stringToValue)
+ .filter(x => x);
+ }
+}
+
+const controlPortInfo = {};
+
+/**
+ * Sets Tor control port connection parameters to be used in future calls to
+ * the controller() function.
+ *
+ * Example:
+ * configureControlPortModule(undefined, "127.0.0.1", 9151, "MyPassw0rd");
+ *
+ * @param {nsIFile?} ipcFile An optional file to use to communicate to the
+ * control port on Unix platforms
+ * @param {string?} host The hostname to connect to the control port. Mutually
+ * exclusive with ipcFile
+ * @param {integer?} port The port number of the control port. To be used only
+ * with host. The default is 9151.
+ * @param {string} password The password of the control port in clear text.
+ */
+export function configureControlPortModule(ipcFile, host, port, password) {
+ controlPortInfo.ipcFile = ipcFile;
+ controlPortInfo.host = host;
+ controlPortInfo.port = port || 9151;
+ controlPortInfo.password = password;
+}
+
+/**
+ * Instantiates and returns a controller object that is connected and
+ * authenticated to a Tor ControlPort using the connection parameters
+ * provided in the most recent call to configureControlPortModule().
+ *
+ * Example:
+ * // Get a new controller
+ * let c = await controller();
+ * // Send command and receive a `250` reply or an error message:
+ * let replyPromise = await c.getInfo("ip-to-country/16.16.16.16");
+ * // Close the controller permanently
+ * c.close();
+ */
+export async function controller() {
+ if (!controlPortInfo.ipcFile && !controlPortInfo.host) {
+ throw new Error("Please call configureControlPortModule first");
+ }
+ let socket;
+ if (controlPortInfo.ipcFile) {
+ socket = AsyncSocket.fromIpcFile(controlPortInfo.ipcFile);
+ } else {
+ socket = AsyncSocket.fromSocketAddress(
+ controlPortInfo.host,
+ controlPortInfo.port
+ );
+ }
+ const controller = new TorController(socket);
+ try {
+ await controller.authenticate(controlPortInfo.password);
+ } catch (e) {
+ try {
+ controller.close();
+ } catch (ec) {
+ // TODO: Use a custom logger?
+ console.error("Cannot close the socket", ec);
+ }
+ throw e;
+ }
+ return controller;
+}
=====================================
toolkit/components/tor-launcher/TorMonitorService.sys.mjs
=====================================
@@ -15,14 +15,9 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
+ controller: "resource://gre/modules/TorControlPort.sys.mjs",
});
-ChromeUtils.defineModuleGetter(
- lazy,
- "controller",
- "resource://torbutton/modules/tor-control-port.js"
-);
-
ChromeUtils.defineESModuleGetters(lazy, {
TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
});
@@ -172,9 +167,7 @@ export const TorMonitorService = {
const cmd = "GETINFO";
const key = "status/bootstrap-phase";
let reply = await this._connection.sendCommand(`${cmd} ${key}`);
- if (!reply) {
- throw new Error("We received an empty reply");
- }
+
// A typical reply looks like:
// 250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
// 250 OK
@@ -335,8 +328,7 @@ export const TorMonitorService = {
let conn;
try {
- const avoidCache = true;
- conn = await lazy.controller(avoidCache);
+ conn = await lazy.controller();
} catch (e) {
logger.error("Cannot open a control port connection", e);
if (conn) {
@@ -353,12 +345,10 @@ export const TorMonitorService = {
}
// TODO: optionally monitor INFO and DEBUG log messages.
- let reply = await conn.sendCommand(
- "SETEVENTS " + Array.from(this._eventHandlers.keys()).join(" ")
- );
- reply = TorParsers.parseCommandResponse(reply);
- if (!TorParsers.commandSucceeded(reply)) {
- logger.error("SETEVENTS failed");
+ try {
+ await conn.setEvents(Array.from(this._eventHandlers.keys()));
+ } catch (e) {
+ logger.error("SETEVENTS failed", e);
conn.close();
return false;
}
@@ -405,18 +395,16 @@ export const TorMonitorService = {
// Try to become the primary controller (TAKEOWNERSHIP).
async _takeTorOwnership(conn) {
- const takeOwnership = "TAKEOWNERSHIP";
- let reply = await conn.sendCommand(takeOwnership);
- reply = TorParsers.parseCommandResponse(reply);
- if (!TorParsers.commandSucceeded(reply)) {
- logger.warn("Take ownership failed");
- } else {
- const resetConf = "RESETCONF __OwningControllerProcess";
- reply = await conn.sendCommand(resetConf);
- reply = TorParsers.parseCommandResponse(reply);
- if (!TorParsers.commandSucceeded(reply)) {
- logger.warn("Clear owning controller process failed");
- }
+ try {
+ conn.takeOwnership();
+ } catch (e) {
+ logger.warn("Take ownership failed", e);
+ return;
+ }
+ try {
+ conn.resetOwningControllerProcess();
+ } catch (e) {
+ logger.warn("Clear owning controller process failed", e);
}
},
=====================================
toolkit/components/tor-launcher/TorProtocolService.sys.mjs
=====================================
@@ -19,16 +19,10 @@ ChromeUtils.defineModuleGetter(
"TorMonitorService",
"resource://gre/modules/TorMonitorService.jsm"
);
-ChromeUtils.defineModuleGetter(
- lazy,
- "configureControlPortModule",
- "resource://torbutton/modules/tor-control-port.js"
-);
-ChromeUtils.defineModuleGetter(
- lazy,
- "controller",
- "resource://torbutton/modules/tor-control-port.js"
-);
+ChromeUtils.defineESModuleGetters(lazy, {
+ controller: "resource://gre/modules/TorControlPort.sys.mjs",
+ configureControlPortModule: "resource://gre/modules/TorControlPort.sys.mjs",
+});
const TorTopics = Object.freeze({
ProcessExited: "TorProcessExited",
@@ -285,8 +279,7 @@ export const TorProtocolService = {
});
},
- // TODO: transform the following 4 functions in getters. At the moment they
- // are also used in torbutton.
+ // TODO: transform the following 4 functions in getters.
// Returns Tor password string or null if an error occurs.
torGetPassword() {
@@ -490,8 +483,6 @@ export const TorProtocolService = {
TorLauncherUtil.setProxyConfiguration(this._SOCKSPortInfo);
// Set the global control port info parameters.
- // These values may be overwritten by torbutton when it initializes, but
- // torbutton's values *should* be identical.
lazy.configureControlPortModule(
this._controlIPCFile,
this._controlHost,
@@ -616,8 +607,7 @@ export const TorProtocolService = {
// return it.
async _getConnection() {
if (!this._controlConnection) {
- const avoidCache = true;
- this._controlConnection = await lazy.controller(avoidCache);
+ this._controlConnection = await lazy.controller();
}
if (this._controlConnection.inUse) {
await new Promise((resolve, reject) =>
=====================================
toolkit/components/tor-launcher/moz.build
=====================================
@@ -1,5 +1,6 @@
EXTRA_JS_MODULES += [
"TorBootstrapRequest.sys.mjs",
+ "TorControlPort.sys.mjs",
"TorDomainIsolator.sys.mjs",
"TorLauncherUtil.sys.mjs",
"TorMonitorService.sys.mjs",
=====================================
toolkit/torbutton/chrome/content/torbutton.js deleted
=====================================
@@ -1,148 +0,0 @@
-// window globals
-var torbutton_init;
-
-(() => {
- // Bug 1506 P1-P5: This is the main Torbutton overlay file. Much needs to be
- // preserved here, but in an ideal world, most of this code should perhaps be
- // moved into an XPCOM service, and much can also be tossed. See also
- // individual 1506 comments for details.
-
- // TODO: check for leaks: http://www.mozilla.org/scriptable/avoiding-leaks.html
- // TODO: Double-check there are no strange exploits to defeat:
- // http://kb.mozillazine.org/Links_to_local_pages_don%27t_work
-
- /* global gBrowser, Services, AppConstants */
-
- let { torbutton_log } = ChromeUtils.import(
- "resource://torbutton/modules/utils.js"
- );
- let { configureControlPortModule } = ChromeUtils.import(
- "resource://torbutton/modules/tor-control-port.js"
- );
-
- const { TorProtocolService } = ChromeUtils.import(
- "resource://gre/modules/TorProtocolService.jsm"
- );
-
- var m_tb_prefs = Services.prefs;
-
- // status
- var m_tb_wasinited = false;
-
- var m_tb_control_ipc_file = null; // Set if using IPC (UNIX domain socket).
- var m_tb_control_port = null; // Set if using TCP.
- var m_tb_control_host = null; // Set if using TCP.
- var m_tb_control_pass = null;
-
- // Bug 1506 P2-P4: This code sets some version variables that are irrelevant.
- // It does read out some important environment variables, though. It is
- // called once per browser window.. This might belong in a component.
- torbutton_init = function () {
- torbutton_log(3, "called init()");
-
- if (m_tb_wasinited) {
- return;
- }
- m_tb_wasinited = true;
-
- // Bug 1506 P4: These vars are very important for New Identity
- if (Services.env.exists("TOR_CONTROL_PASSWD")) {
- m_tb_control_pass = Services.env.get("TOR_CONTROL_PASSWD");
- } else if (Services.env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
- var cookie_path = Services.env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
- try {
- if ("" != cookie_path) {
- m_tb_control_pass = torbutton_read_authentication_cookie(cookie_path);
- }
- } catch (e) {
- torbutton_log(4, "unable to read authentication cookie");
- }
- } else {
- try {
- // Try to get password from Tor Launcher.
- m_tb_control_pass = TorProtocolService.torGetPassword();
- } catch (e) {}
- }
-
- // Try to get the control port IPC file (an nsIFile) from Tor Launcher,
- // since Tor Launcher knows how to handle its own preferences and how to
- // resolve relative paths.
- try {
- m_tb_control_ipc_file = TorProtocolService.torGetControlIPCFile();
- } catch (e) {}
-
- if (!m_tb_control_ipc_file) {
- if (Services.env.exists("TOR_CONTROL_PORT")) {
- m_tb_control_port = Services.env.get("TOR_CONTROL_PORT");
- } else {
- try {
- const kTLControlPortPref = "extensions.torlauncher.control_port";
- m_tb_control_port = m_tb_prefs.getIntPref(kTLControlPortPref);
- } catch (e) {
- // Since we want to disable some features when Tor Launcher is
- // not installed (e.g., New Identity), we do not set a default
- // port value here.
- }
- }
-
- if (Services.env.exists("TOR_CONTROL_HOST")) {
- m_tb_control_host = Services.env.get("TOR_CONTROL_HOST");
- } else {
- try {
- const kTLControlHostPref = "extensions.torlauncher.control_host";
- m_tb_control_host = m_tb_prefs.getCharPref(kTLControlHostPref);
- } catch (e) {
- m_tb_control_host = "127.0.0.1";
- }
- }
- }
-
- configureControlPortModule(
- m_tb_control_ipc_file,
- m_tb_control_host,
- m_tb_control_port,
- m_tb_control_pass
- );
-
- torbutton_log(3, "init completed");
- };
-
- // Bug 1506 P4: Control port interaction. Needed for New Identity.
- function torbutton_read_authentication_cookie(path) {
- var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.initWithPath(path);
- var fileStream = Cc[
- "@mozilla.org/network/file-input-stream;1"
- ].createInstance(Ci.nsIFileInputStream);
- fileStream.init(file, 1, 0, false);
- var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
- Ci.nsIBinaryInputStream
- );
- binaryStream.setInputStream(fileStream);
- var array = binaryStream.readByteArray(fileStream.available());
- binaryStream.close();
- fileStream.close();
- return torbutton_array_to_hexdigits(array);
- }
-
- // Bug 1506 P4: Control port interaction. Needed for New Identity.
- function torbutton_array_to_hexdigits(array) {
- return array
- .map(function (c) {
- return String("0" + c.toString(16)).slice(-2);
- })
- .join("");
- }
-
- // ---------------------- Event handlers -----------------
-
- // Bug 1506 P3: This is needed pretty much only for the window resizing.
- // See comments for individual functions for details
- function torbutton_new_window(event) {
- torbutton_log(3, "New window");
- if (!m_tb_wasinited) {
- torbutton_init();
- }
- }
- window.addEventListener("load", torbutton_new_window);
-})();
=====================================
toolkit/torbutton/components.conf deleted
=====================================
@@ -1,10 +0,0 @@
-Classes = [
- {
- "cid": "{f36d72c9-9718-4134-b550-e109638331d7}",
- "contract_ids": [
- "@torproject.org/torbutton-logger;1"
- ],
- "jsm": "resource://torbutton/modules/TorbuttonLogger.jsm",
- "constructor": "TorbuttonLogger",
- },
-]
=====================================
toolkit/torbutton/jar.mn
=====================================
@@ -1,19 +1,12 @@
#filter substitution
torbutton.jar:
-
-% content torbutton %content/
-
- content/torbutton.js (chrome/content/torbutton.js)
-
- modules/ (modules/*)
-
% resource torbutton %
+% category l10n-registry torbutton resource://torbutton/locale/{locale}/
# browser branding
% override chrome://branding/locale/brand.dtd chrome://torbutton/locale/brand.dtd
% override chrome://branding/locale/brand.properties chrome://torbutton/locale/brand.properties
-% category l10n-registry torbutton resource://torbutton/locale/{locale}/
# Strings for the about:tbupdate page
% override chrome://browser/locale/aboutTBUpdate.dtd chrome://torbutton/locale/aboutTBUpdate.dtd
=====================================
toolkit/torbutton/modules/TorbuttonLogger.jsm deleted
=====================================
@@ -1,147 +0,0 @@
-// Bug 1506 P1: This is just a handy logger. If you have a better one, toss
-// this in the trash.
-
-/*************************************************************************
- * TBLogger (JavaScript XPCOM component)
- *
- * Allows loglevel-based logging to different logging mechanisms.
- *
- *************************************************************************/
-
-var EXPORTED_SYMBOLS = ["TorbuttonLogger"];
-
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-function TorbuttonLogger() {
- // Register observer
- Services.prefs.addObserver("extensions.torbutton", this);
-
- this.loglevel = Services.prefs.getIntPref("extensions.torbutton.loglevel", 4);
- this.logmethod = Services.prefs.getIntPref(
- "extensions.torbutton.logmethod",
- 1
- );
-
- try {
- var logMngr = Cc["@mozmonkey.com/debuglogger/manager;1"].getService(
- Ci.nsIDebugLoggerManager
- );
- this._debuglog = logMngr.registerLogger("torbutton");
- } catch (exErr) {
- this._debuglog = false;
- }
- this._console = Services.console;
-
- // This JSObject is exported directly to chrome
- this.wrappedJSObject = this;
- this.log(3, "Torbutton debug output ready");
-}
-
-/**
- * JS XPCOM component registration goop:
- *
- * Everything below is boring boilerplate and can probably be ignored.
- */
-
-TorbuttonLogger.prototype = {
- QueryInterface: ChromeUtils.generateQI([Ci.nsIClassInfo]),
-
- wrappedJSObject: null, // Initialized by constructor
-
- formatLog(str, level) {
- const padInt = n => String(n).padStart(2, "0");
- const logString = { 1: "VERB", 2: "DBUG", 3: "INFO", 4: "NOTE", 5: "WARN" };
- const d = new Date();
- const now =
- padInt(d.getUTCMonth() + 1) +
- "-" +
- padInt(d.getUTCDate()) +
- " " +
- padInt(d.getUTCHours()) +
- ":" +
- padInt(d.getUTCMinutes()) +
- ":" +
- padInt(d.getUTCSeconds());
- return `${now} Torbutton ${logString[level]}: ${str}`;
- },
-
- // error console log
- eclog(level, str) {
- switch (this.logmethod) {
- case 0: // stderr
- if (this.loglevel <= level) {
- dump(this.formatLog(str, level) + "\n");
- }
- break;
- default:
- // errorconsole
- if (this.loglevel <= level) {
- this._console.logStringMessage(this.formatLog(str, level));
- }
- break;
- }
- },
-
- safe_log(level, str, scrub) {
- if (this.loglevel < 4) {
- this.eclog(level, str + scrub);
- } else {
- this.eclog(level, str + " [scrubbed]");
- }
- },
-
- log(level, str) {
- switch (this.logmethod) {
- case 2: // debuglogger
- if (this._debuglog) {
- this._debuglog.log(6 - level, this.formatLog(str, level));
- break;
- }
- // fallthrough
- case 0: // stderr
- if (this.loglevel <= level) {
- dump(this.formatLog(str, level) + "\n");
- }
- break;
- case 1: // errorconsole
- if (this.loglevel <= level) {
- this._console.logStringMessage(this.formatLog(str, level));
- }
- break;
- default:
- dump("Bad log method: " + this.logmethod);
- }
- },
-
- // Pref observer interface implementation
-
- // topic: what event occurred
- // subject: what nsIPrefBranch we're observing
- // data: which pref has been changed (relative to subject)
- observe(subject, topic, data) {
- if (topic != "nsPref:changed") {
- return;
- }
- switch (data) {
- case "extensions.torbutton.logmethod":
- this.logmethod = Services.prefs.getIntPref(
- "extensions.torbutton.logmethod"
- );
- if (this.logmethod === 0) {
- Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
- } else if (
- Services.prefs.getIntPref("extensions.torlauncher.logmethod", 3) !== 0
- ) {
- // If Tor Launcher is not available or its log method is not 0
- // then let's reset the dump pref.
- Services.prefs.setBoolPref("browser.dom.window.dump.enabled", false);
- }
- break;
- case "extensions.torbutton.loglevel":
- this.loglevel = Services.prefs.getIntPref(
- "extensions.torbutton.loglevel"
- );
- break;
- }
- },
-};
=====================================
toolkit/torbutton/modules/tor-control-port.js deleted
=====================================
@@ -1,1002 +0,0 @@
-// A module for TorBrowser that provides an asynchronous controller for
-// Tor, through its ControlPort.
-//
-// This file is written in call stack order (later functions
-// call earlier functions). The file can be processed
-// with docco.js to produce pretty documentation.
-//
-// To import the module, use
-//
-// let { configureControlPortModule, controller, wait_for_controller } =
-// Components.utils.import("path/to/tor-control-port.js", {});
-//
-// See the third-to-last function defined in this file:
-// configureControlPortModule(ipcFile, host, port, password)
-// for usage of the configureControlPortModule function.
-//
-// See the last functions defined in this file:
-// controller(avoidCache), wait_for_controller(avoidCache)
-// for usage of the controller functions.
-
-/* jshint esnext: true */
-/* jshint -W097 */
-/* global console */
-"use strict";
-
-const { XPCOMUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/XPCOMUtils.sys.mjs"
-);
-
-ChromeUtils.defineModuleGetter(
- this,
- "TorMonitorService",
- "resource://gre/modules/TorMonitorService.jsm"
-);
-
-XPCOMUtils.defineLazyServiceGetter(
- this,
- "logger",
- "@torproject.org/torbutton-logger;1",
- "nsISupports"
-);
-
-// tor-launcher observer topics
-const TorTopics = Object.freeze({
- ProcessIsReady: "TorProcessIsReady",
-});
-
-// __log__.
-// Logging function
-let log = x =>
- logger.wrappedJSObject.eclog(3, x.trimRight().replace(/\r\n/g, "\n"));
-
-// ### announce this file
-log("Loading tor-control-port.js\n");
-
-class AsyncSocket {
- constructor(ipcFile, host, port) {
- let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
- Ci.nsISocketTransportService
- );
- const OPEN_UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED;
-
- let socketTransport = ipcFile
- ? sts.createUnixDomainTransport(ipcFile)
- : sts.createTransport([], host, port, null, null);
-
- this.outputStream = socketTransport
- .openOutputStream(OPEN_UNBUFFERED, 1, 1)
- .QueryInterface(Ci.nsIAsyncOutputStream);
- this.outputQueue = [];
-
- this.inputStream = socketTransport
- .openInputStream(OPEN_UNBUFFERED, 1, 1)
- .QueryInterface(Ci.nsIAsyncInputStream);
- this.scriptableInputStream = Cc[
- "@mozilla.org/scriptableinputstream;1"
- ].createInstance(Ci.nsIScriptableInputStream);
- this.scriptableInputStream.init(this.inputStream);
- this.inputQueue = [];
- }
-
- // asynchronously write string to underlying socket and return number of bytes written
- async write(str) {
- return new Promise((resolve, reject) => {
- // asyncWait next write request
- const tryAsyncWait = () => {
- if (this.outputQueue.length) {
- this.outputStream.asyncWait(
- this.outputQueue.at(0), // next request
- 0,
- 0,
- Services.tm.currentThread
- );
- }
- };
-
- // output stream can only have 1 registered callback at a time, so multiple writes
- // need to be queued up (see nsIAsyncOutputStream.idl)
- this.outputQueue.push({
- // Implement an nsIOutputStreamCallback:
- onOutputStreamReady: () => {
- try {
- let bytesWritten = this.outputStream.write(str, str.length);
-
- // remove this callback object from queue as it is now completed
- this.outputQueue.shift();
-
- // request next wait if there is one
- tryAsyncWait();
-
- // finally resolve promise
- resolve(bytesWritten);
- } catch (err) {
- // reject promise on error
- reject(err);
- }
- },
- });
-
- // length 1 imples that there is no in-flight asyncWait, so we may immediately
- // follow through on this write
- if (this.outputQueue.length == 1) {
- tryAsyncWait();
- }
- });
- }
-
- // asynchronously read string from underlying socket and return it
- async read() {
- return new Promise((resolve, reject) => {
- const tryAsyncWait = () => {
- if (this.inputQueue.length) {
- this.inputStream.asyncWait(
- this.inputQueue.at(0), // next input request
- 0,
- 0,
- Services.tm.currentThread
- );
- }
- };
-
- this.inputQueue.push({
- onInputStreamReady: stream => {
- try {
- if (!this.scriptableInputStream.available()) {
- // This means EOF, but not closed yet. However, arriving at EOF
- // should be an error condition for us, since we are in a socket,
- // and EOF should mean peer disconnected.
- // If the stream has been closed, this function itself should
- // throw.
- reject(
- new Error("onInputStreamReady called without available bytes.")
- );
- return;
- }
-
- // read our string from input stream
- let str = this.scriptableInputStream.read(
- this.scriptableInputStream.available()
- );
-
- // remove this callback object from queue now that we have read
- this.inputQueue.shift();
-
- // request next wait if there is one
- tryAsyncWait();
-
- // finally resolve promise
- resolve(str);
- } catch (err) {
- reject(err);
- }
- },
- });
-
- // length 1 imples that there is no in-flight asyncWait, so we may immediately
- // follow through on this read
- if (this.inputQueue.length == 1) {
- tryAsyncWait();
- }
- });
- }
-
- close() {
- this.outputStream.close();
- this.inputStream.close();
- }
-}
-
-class ControlSocket {
- constructor(asyncSocket) {
- this.socket = asyncSocket;
- this._isOpen = true;
- this.pendingData = "";
- this.pendingLines = [];
-
- this.mainDispatcher = io.callbackDispatcher();
- this.notificationDispatcher = io.callbackDispatcher();
- // mainDispatcher pushes only async notifications (650) to notificationDispatcher
- this.mainDispatcher.addCallback(
- /^650/,
- this._handleNotification.bind(this)
- );
- // callback for handling responses and errors
- this.mainDispatcher.addCallback(
- /^[245]\d\d/,
- this._handleCommandReply.bind(this)
- );
-
- this.commandQueue = [];
-
- this._startMessagePump();
- }
-
- // blocks until an entire line is read and returns it
- // immediately returns next line in queue (pendingLines) if present
- async _readLine() {
- // keep reading from socket until we have a full line to return
- while (!this.pendingLines.length) {
- // read data from our socket and spit on newline tokens
- this.pendingData += await this.socket.read();
- let lines = this.pendingData.split("\r\n");
-
- // the last line will either be empty string, or a partial read of a response/event
- // so save it off for the next socket read
- this.pendingData = lines.pop();
-
- // copy remaining full lines to our pendingLines list
- this.pendingLines = this.pendingLines.concat(lines);
- }
- return this.pendingLines.shift();
- }
-
- // blocks until an entire message is ready and returns it
- async _readMessage() {
- // whether we are searching for the end of a multi-line values
- // See control-spec section 3.9
- let handlingMultlineValue = false;
- let endOfMessageFound = false;
- const message = [];
-
- do {
- const line = await this._readLine();
- message.push(line);
-
- if (handlingMultlineValue) {
- // look for end of multiline
- if (line.match(/^\.$/)) {
- handlingMultlineValue = false;
- }
- } else {
- // 'Multiline values' are possible. We avoid interrupting one by detecting it
- // and waiting for a terminating "." on its own line.
- // (See control-spec section 3.9 and https://trac.torproject.org/16990#comment:28
- // Ensure this is the first line of a new message
- // eslint-disable-next-line no-lonely-if
- if (message.length === 1 && line.match(/^\d\d\d\+.+?=$/)) {
- handlingMultlineValue = true;
- }
- // look for end of message (note the space character at end of the regex)
- else if (line.match(/^\d\d\d /)) {
- if (message.length == 1) {
- endOfMessageFound = true;
- } else {
- let firstReplyCode = message[0].substring(0, 3);
- let lastReplyCode = line.substring(0, 3);
- if (firstReplyCode == lastReplyCode) {
- endOfMessageFound = true;
- }
- }
- }
- }
- } while (!endOfMessageFound);
-
- // join our lines back together to form one message
- return message.join("\r\n");
- }
-
- async _startMessagePump() {
- try {
- while (true) {
- let message = await this._readMessage();
- log("controlPort >> " + message);
- this.mainDispatcher.pushMessage(message);
- }
- } catch (err) {
- this._isOpen = false;
- for (const cmd of this.commandQueue) {
- cmd.reject(err);
- }
- this.commandQueue = [];
- }
- }
-
- _writeNextCommand() {
- let cmd = this.commandQueue[0];
- log("controlPort << " + cmd.commandString);
- this.socket.write(`${cmd.commandString}\r\n`).catch(cmd.reject);
- }
-
- async sendCommand(commandString) {
- if (!this.isOpen()) {
- throw new Error("ControlSocket not open");
- }
-
- // this promise is resolved either in _handleCommandReply, or
- // in _startMessagePump (on stream error)
- return new Promise((resolve, reject) => {
- let command = {
- commandString,
- resolve,
- reject,
- };
-
- this.commandQueue.push(command);
- if (this.commandQueue.length == 1) {
- this._writeNextCommand();
- }
- });
- }
-
- _handleCommandReply(message) {
- let cmd = this.commandQueue.shift();
- if (message.match(/^2/)) {
- cmd.resolve(message);
- } else if (message.match(/^[45]/)) {
- let myErr = new Error(cmd.commandString + " -> " + message);
- // Add Tor-specific information to the Error object.
- let idx = message.indexOf(" ");
- if (idx > 0) {
- myErr.torStatusCode = message.substring(0, idx);
- myErr.torMessage = message.substring(idx);
- } else {
- myErr.torStatusCode = message;
- }
- cmd.reject(myErr);
- } else {
- cmd.reject(
- new Error(
- `ControlSocket::_handleCommandReply received unexpected message:\n----\n${message}\n----`
- )
- );
- }
-
- // send next command if one is available
- if (this.commandQueue.length) {
- this._writeNextCommand();
- }
- }
-
- _handleNotification(message) {
- this.notificationDispatcher.pushMessage(message);
- }
-
- close() {
- this.socket.close();
- this._isOpen = false;
- }
-
- addNotificationCallback(regex, callback) {
- this.notificationDispatcher.addCallback(regex, callback);
- }
-
- isOpen() {
- return this._isOpen;
- }
-}
-
-// ## io
-// I/O utilities namespace
-
-let io = {};
-
-// __io.callbackDispatcher()__.
-// Returns dispatcher object with three member functions:
-// dispatcher.addCallback(regex, callback), dispatcher.removeCallback(callback),
-// and dispatcher.pushMessage(message).
-// Pass pushMessage to another function that needs a callback with a single string
-// argument. Whenever dispatcher.pushMessage receives a string, the dispatcher will
-// check for any regex matches and pass the string on to the corresponding callback(s).
-io.callbackDispatcher = function () {
- let callbackPairs = [],
- removeCallback = function (aCallback) {
- callbackPairs = callbackPairs.filter(function ([regex, callback]) {
- return callback !== aCallback;
- });
- },
- addCallback = function (regex, callback) {
- if (callback) {
- callbackPairs.push([regex, callback]);
- }
- return function () {
- removeCallback(callback);
- };
- },
- pushMessage = function (message) {
- for (let [regex, callback] of callbackPairs) {
- if (message.match(regex)) {
- callback(message);
- }
- }
- };
- return {
- pushMessage,
- removeCallback,
- addCallback,
- };
-};
-
-// __io.controlSocket(ipcFile, host, port, password)__.
-// Instantiates and returns a socket to a tor ControlPort at ipcFile or
-// host:port, authenticating with the given password. Example:
-//
-// // Open the socket
-// let socket = await io.controlSocket(undefined, "127.0.0.1", 9151, "MyPassw0rd");
-// // Send command and receive "250" response reply or error is thrown
-// await socket.sendCommand(commandText);
-// // Register or deregister for "650" notifications
-// // that match regex
-// socket.addNotificationCallback(regex, callback);
-// socket.removeNotificationCallback(callback);
-// // Close the socket permanently
-// socket.close();
-io.controlSocket = async function (ipcFile, host, port, password) {
- let socket = new AsyncSocket(ipcFile, host, port);
- let controlSocket = new ControlSocket(socket);
-
- // Log in to control port.
- await controlSocket.sendCommand("authenticate " + (password || ""));
- // Activate needed events.
- await controlSocket.sendCommand("setevents stream");
-
- return controlSocket;
-};
-
-// ## utils
-// A namespace for utility functions
-let utils = {};
-
-// __utils.identity(x)__.
-// Returns its argument unchanged.
-utils.identity = function (x) {
- return x;
-};
-
-// __utils.isString(x)__.
-// Returns true iff x is a string.
-utils.isString = function (x) {
- return typeof x === "string" || x instanceof String;
-};
-
-// __utils.capture(string, regex)__.
-// Takes a string and returns an array of capture items, where regex must have a single
-// capturing group and use the suffix /.../g to specify a global search.
-utils.capture = function (string, regex) {
- let matches = [];
- // Special trick to use string.replace for capturing multiple matches.
- string.replace(regex, function (a, captured) {
- matches.push(captured);
- });
- return matches;
-};
-
-// __utils.extractor(regex)__.
-// Returns a function that takes a string and returns an array of regex matches. The
-// regex must use the suffix /.../g to specify a global search.
-utils.extractor = function (regex) {
- return function (text) {
- return utils.capture(text, regex);
- };
-};
-
-// __utils.splitLines(string)__.
-// Splits a string into an array of strings, each corresponding to a line.
-utils.splitLines = function (string) {
- return string.split(/\r?\n/);
-};
-
-// __utils.splitAtSpaces(string)__.
-// Splits a string into chunks between spaces. Does not split at spaces
-// inside pairs of quotation marks.
-utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);
-
-// __utils.splitAtFirst(string, regex)__.
-// Splits a string at the first instance of regex match. If no match is
-// found, returns the whole string.
-utils.splitAtFirst = function (string, regex) {
- let match = string.match(regex);
- return match
- ? [
- string.substring(0, match.index),
- string.substring(match.index + match[0].length),
- ]
- : string;
-};
-
-// __utils.splitAtEquals(string)__.
-// Splits a string into chunks between equals. Does not split at equals
-// inside pairs of quotation marks.
-utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g);
-
-// __utils.mergeObjects(arrayOfObjects)__.
-// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object.
-// Pure function.
-utils.mergeObjects = function (arrayOfObjects) {
- let result = {};
- for (let obj of arrayOfObjects) {
- for (let key in obj) {
- result[key] = obj[key];
- }
- }
- return result;
-};
-
-// __utils.listMapData(parameterString, listNames)__.
-// Takes a list of parameters separated by spaces, of which the first several are
-// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames
-// to the unnamed parameters, and combine them in a map with the named parameters.
-// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH`
-//
-// utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH",
-// ["streamID", "event", "circuitID", "IP"])
-// // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0",
-// // "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}"
-utils.listMapData = function (parameterString, listNames) {
- // Split out the space-delimited parameters.
- let parameters = utils.splitAtSpaces(parameterString),
- dataMap = {};
- // Assign listNames to the first n = listNames.length parameters.
- for (let i = 0; i < listNames.length; ++i) {
- dataMap[listNames[i]] = parameters[i];
- }
- // Read key-value pairs and copy these to the dataMap.
- for (let i = listNames.length; i < parameters.length; ++i) {
- let [key, value] = utils.splitAtEquals(parameters[i]);
- if (key && value) {
- dataMap[key] = value;
- }
- }
- return dataMap;
-};
-
-// __utils.rejectPromise(errorMessage)__.
-// Returns a rejected promise with the given error message.
-utils.rejectPromise = errorMessage => Promise.reject(new Error(errorMessage));
-
-// ## info
-// A namespace for functions related to tor's GETINFO and GETCONF command.
-let info = {};
-
-// __info.keyValueStringsFromMessage(messageText)__.
-// Takes a message (text) response to GETINFO or GETCONF and provides
-// a series of key-value strings, which are either multiline (with a `250+` prefix):
-//
-// 250+config/defaults=
-// AccountingMax "0 bytes"
-// AllowDotExit "0"
-// .
-//
-// or single-line (with a `250-` or `250 ` prefix):
-//
-// 250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
-info.keyValueStringsFromMessage = utils.extractor(
- /^(250\+[\s\S]+?^\.|250[- ].+?)$/gim
-);
-
-// __info.applyPerLine(transformFunction)__.
-// Returns a function that splits text into lines,
-// and applies transformFunction to each line.
-info.applyPerLine = function (transformFunction) {
- return function (text) {
- return utils.splitLines(text.trim()).map(transformFunction);
- };
-};
-
-// __info.routerStatusParser(valueString)__.
-// Parses a router status entry as, described in
-// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
-// (search for "router status entry")
-info.routerStatusParser = function (valueString) {
- let lines = utils.splitLines(valueString),
- objects = [];
- for (let line of lines) {
- // Drop first character and grab data following it.
- let myData = line.substring(2),
- // Accumulate more maps with data, depending on the first character in the line.
- dataFun = {
- r: data =>
- utils.listMapData(data, [
- "nickname",
- "identity",
- "digest",
- "publicationDate",
- "publicationTime",
- "IP",
- "ORPort",
- "DirPort",
- ]),
- a: data => ({ IPv6: data }),
- s: data => ({ statusFlags: utils.splitAtSpaces(data) }),
- v: data => ({ version: data }),
- w: data => utils.listMapData(data, []),
- p: data => ({ portList: data.split(",") }),
- }[line.charAt(0)];
- if (dataFun !== undefined) {
- objects.push(dataFun(myData));
- }
- }
- return utils.mergeObjects(objects);
-};
-
-// __info.circuitStatusParser(line)__.
-// Parse the output of a circuit status line.
-info.circuitStatusParser = function (line) {
- let data = utils.listMapData(line, ["id", "status", "circuit"]),
- circuit = data.circuit;
- // Parse out the individual circuit IDs and names.
- if (circuit) {
- data.circuit = circuit.split(",").map(function (x) {
- return x.split(/~|=/);
- });
- }
- return data;
-};
-
-// __info.streamStatusParser(line)__.
-// Parse the output of a stream status line.
-info.streamStatusParser = function (text) {
- return utils.listMapData(text, [
- "StreamID",
- "StreamStatus",
- "CircuitID",
- "Target",
- ]);
-};
-
-// TODO: fix this parsing logic to handle bridgeLine correctly
-// fingerprint/id is an optional parameter
-// __info.bridgeParser(bridgeLine)__.
-// Takes a single line from a `getconf bridge` result and returns
-// a map containing the bridge's type, address, and ID.
-info.bridgeParser = function (bridgeLine) {
- let result = {},
- tokens = bridgeLine.split(/\s+/);
- // First check if we have a "vanilla" bridge:
- if (tokens[0].match(/^\d+\.\d+\.\d+\.\d+/)) {
- result.type = "vanilla";
- [result.address, result.ID] = tokens;
- // Several bridge types have a similar format:
- } else {
- result.type = tokens[0];
- if (
- [
- "flashproxy",
- "fte",
- "meek",
- "meek_lite",
- "obfs3",
- "obfs4",
- "scramblesuit",
- "snowflake",
- ].includes(result.type)
- ) {
- [result.address, result.ID] = tokens.slice(1);
- }
- }
- return result.type ? result : null;
-};
-
-// __info.parsers__.
-// A map of GETINFO and GETCONF keys to parsing function, which convert
-// result strings to JavaScript data.
-info.parsers = {
- "ns/id/": info.routerStatusParser,
- "ip-to-country/": utils.identity,
- "circuit-status": info.applyPerLine(info.circuitStatusParser),
- bridge: info.bridgeParser,
- // Currently unused parsers:
- // "ns/name/" : info.routerStatusParser,
- // "stream-status" : info.applyPerLine(info.streamStatusParser),
- // "version" : utils.identity,
- // "config-file" : utils.identity,
-};
-
-// __info.getParser(key)__.
-// Takes a key and determines the parser function that should be used to
-// convert its corresponding valueString to JavaScript data.
-info.getParser = function (key) {
- return (
- info.parsers[key] ||
- info.parsers[key.substring(0, key.lastIndexOf("/") + 1)]
- );
-};
-
-// __info.stringToValue(string)__.
-// Converts a key-value string as from GETINFO or GETCONF to a value.
-info.stringToValue = function (string) {
- // key should look something like `250+circuit-status=` or `250-circuit-status=...`
- // or `250 circuit-status=...`
- let matchForKey = string.match(/^250[ +-](.+?)=/),
- key = matchForKey ? matchForKey[1] : null;
- if (key === null) {
- return null;
- }
- // matchResult finds a single-line result for `250-` or `250 `,
- // or a multi-line one for `250+`.
- let matchResult =
- string.match(/^250[ -].+?=(.*)$/) ||
- string.match(/^250\+.+?=([\s\S]*?)^\.$/m),
- // Retrieve the captured group (the text of the value in the key-value pair)
- valueString = matchResult ? matchResult[1] : null,
- // Get the parser function for the key found.
- parse = info.getParser(key.toLowerCase());
- if (parse === undefined) {
- throw new Error("No parser found for '" + key + "'");
- }
- // Return value produced by the parser.
- return parse(valueString);
-};
-
-// __info.getMultipleResponseValues(message)__.
-// Process multiple responses to a GETINFO or GETCONF request.
-info.getMultipleResponseValues = function (message) {
- return info
- .keyValueStringsFromMessage(message)
- .map(info.stringToValue)
- .filter(utils.identity);
-};
-
-// __info.getInfo(controlSocket, key)__.
-// Sends GETINFO for a single key. Returns a promise with the result.
-info.getInfo = function (aControlSocket, key) {
- if (!utils.isString(key)) {
- return utils.rejectPromise("key argument should be a string");
- }
- return aControlSocket
- .sendCommand("getinfo " + key)
- .then(response => info.getMultipleResponseValues(response)[0]);
-};
-
-// __info.getConf(aControlSocket, key)__.
-// Sends GETCONF for a single key. Returns a promise with the result.
-info.getConf = function (aControlSocket, key) {
- // GETCONF with a single argument returns results with
- // one or more lines that look like `250[- ]key=value`.
- // Any GETCONF lines that contain a single keyword only are currently dropped.
- // So we can use similar parsing to that for getInfo.
- if (!utils.isString(key)) {
- return utils.rejectPromise("key argument should be a string");
- }
- return aControlSocket
- .sendCommand("getconf " + key)
- .then(info.getMultipleResponseValues);
-};
-
-// ## onionAuth
-// A namespace for functions related to tor's ONION_CLIENT_AUTH_* commands.
-let onionAuth = {};
-
-onionAuth.keyInfoStringsFromMessage = utils.extractor(/^250-CLIENT\s+(.+)$/gim);
-
-onionAuth.keyInfoObjectsFromMessage = function (message) {
- let keyInfoStrings = onionAuth.keyInfoStringsFromMessage(message);
- return keyInfoStrings.map(infoStr =>
- utils.listMapData(infoStr, ["hsAddress", "typeAndKey"])
- );
-};
-
-// __onionAuth.viewKeys()__.
-// Sends a ONION_CLIENT_AUTH_VIEW command to retrieve the list of private keys.
-// Returns a promise that is fulfilled with an array of key info objects which
-// contain the following properties:
-// hsAddress
-// typeAndKey
-// Flags (e.g., "Permanent")
-onionAuth.viewKeys = function (aControlSocket) {
- let cmd = "onion_client_auth_view";
- return aControlSocket
- .sendCommand(cmd)
- .then(onionAuth.keyInfoObjectsFromMessage);
-};
-
-// __onionAuth.add(controlSocket, hsAddress, b64PrivateKey, isPermanent)__.
-// Sends a ONION_CLIENT_AUTH_ADD command to add a private key to the
-// Tor configuration.
-onionAuth.add = function (
- aControlSocket,
- hsAddress,
- b64PrivateKey,
- isPermanent
-) {
- if (!utils.isString(hsAddress)) {
- return utils.rejectPromise("hsAddress argument should be a string");
- }
-
- if (!utils.isString(b64PrivateKey)) {
- return utils.rejectPromise("b64PrivateKey argument should be a string");
- }
-
- const keyType = "x25519";
- let cmd = `onion_client_auth_add ${hsAddress} ${keyType}:${b64PrivateKey}`;
- if (isPermanent) {
- cmd += " Flags=Permanent";
- }
- return aControlSocket.sendCommand(cmd);
-};
-
-// __onionAuth.remove(controlSocket, hsAddress)__.
-// Sends a ONION_CLIENT_AUTH_REMOVE command to remove a private key from the
-// Tor configuration.
-onionAuth.remove = function (aControlSocket, hsAddress) {
- if (!utils.isString(hsAddress)) {
- return utils.rejectPromise("hsAddress argument should be a string");
- }
-
- let cmd = `onion_client_auth_remove ${hsAddress}`;
- return aControlSocket.sendCommand(cmd);
-};
-
-// ## event
-// Handlers for events
-
-let event = {};
-
-// __event.parsers__.
-// A map of EVENT keys to parsing functions, which convert result strings to JavaScript
-// data.
-event.parsers = {
- stream: info.streamStatusParser,
- // Currently unused:
- // "circ" : info.circuitStatusParser,
-};
-
-// __event.messageToData(type, message)__.
-// Extract the data from an event. Note, at present
-// we only extract streams that look like `"650" SP...`
-event.messageToData = function (type, message) {
- let dataText = message.match(/^650 \S+?\s(.*)/m)[1];
- return dataText && type.toLowerCase() in event.parsers
- ? event.parsers[type.toLowerCase()](dataText)
- : null;
-};
-
-// __event.watchEvent(controlSocket, type, filter, onData)__.
-// Watches for a particular type of event. If filter(data) returns true, the event's
-// data is passed to the onData callback. Returns a zero arg function that
-// stops watching the event. Note: we only observe `"650" SP...` events
-// currently (no `650+...` or `650-...` events).
-event.watchEvent = function (controlSocket, type, filter, onData, raw = false) {
- controlSocket.addNotificationCallback(
- new RegExp("^650 " + type),
- function (message) {
- let data = event.messageToData(type, message);
- if (filter === null || filter(data)) {
- if (raw || !data) {
- onData(message);
- return;
- }
- onData(data);
- }
- }
- );
-};
-
-// ## tor
-// Things related to the main controller.
-let tor = {};
-
-// __tor.controllerCache__.
-// A map from "unix:socketpath" or "host:port" to controller objects. Prevents
-// redundant instantiation of control sockets.
-tor.controllerCache = new Map();
-
-// __tor.controller(ipcFile, host, port, password)__.
-// Creates a tor controller at the given ipcFile or host and port, with the
-// given password.
-tor.controller = async function (ipcFile, host, port, password) {
- let socket = await io.controlSocket(ipcFile, host, port, password);
- return {
- getInfo: key => info.getInfo(socket, key),
- getConf: key => info.getConf(socket, key),
- onionAuthViewKeys: () => onionAuth.viewKeys(socket),
- onionAuthAdd: (hsAddress, b64PrivateKey, isPermanent) =>
- onionAuth.add(socket, hsAddress, b64PrivateKey, isPermanent),
- onionAuthRemove: hsAddress => onionAuth.remove(socket, hsAddress),
- watchEvent: (type, filter, onData, raw = false) => {
- event.watchEvent(socket, type, filter, onData, raw);
- },
- isOpen: () => socket.isOpen(),
- close: () => {
- socket.close();
- },
- sendCommand: cmd => socket.sendCommand(cmd),
- };
-};
-
-// ## Export
-
-let controlPortInfo = {};
-
-// __configureControlPortModule(ipcFile, host, port, password)__.
-// Sets Tor control port connection parameters to be used in future calls to
-// the controller() function. Example:
-// configureControlPortModule(undefined, "127.0.0.1", 9151, "MyPassw0rd");
-var configureControlPortModule = function (ipcFile, host, port, password) {
- controlPortInfo.ipcFile = ipcFile;
- controlPortInfo.host = host;
- controlPortInfo.port = port || 9151;
- controlPortInfo.password = password;
-};
-
-// __controller(avoidCache)__.
-// Instantiates and returns a controller object that is connected and
-// authenticated to a Tor ControlPort using the connection parameters
-// provided in the most recent call to configureControlPortModule(), if
-// the controller doesn't yet exist. Otherwise returns the existing
-// controller to the given ipcFile or host:port. Throws on error.
-//
-// Example:
-//
-// // Get a new controller
-// const avoidCache = true;
-// let c = controller(avoidCache);
-// // Send command and receive `250` reply or error message in a promise:
-// let replyPromise = c.getInfo("ip-to-country/16.16.16.16");
-// // Close the controller permanently
-// c.close();
-var controller = async function (avoidCache) {
- if (!controlPortInfo.ipcFile && !controlPortInfo.host) {
- throw new Error("Please call configureControlPortModule first");
- }
-
- const dest = controlPortInfo.ipcFile
- ? `unix:${controlPortInfo.ipcFile.path}`
- : `${controlPortInfo.host}:${controlPortInfo.port}`;
-
- // constructor shorthand
- const newTorController = async () => {
- return tor.controller(
- controlPortInfo.ipcFile,
- controlPortInfo.host,
- controlPortInfo.port,
- controlPortInfo.password
- );
- };
-
- // avoid cache so always return a new controller
- if (avoidCache) {
- return newTorController();
- }
-
- // first check our cache and see if we already have one
- let cachedController = tor.controllerCache.get(dest);
- if (cachedController && cachedController.isOpen()) {
- return cachedController;
- }
-
- // create a new one and store in the map
- cachedController = await newTorController();
- // overwrite the close() function to prevent consumers from closing a shared/cached controller
- cachedController.close = () => {
- throw new Error("May not close cached Tor Controller as it may be in use");
- };
-
- tor.controllerCache.set(dest, cachedController);
- return cachedController;
-};
-
-// __wait_for_controller(avoidCache)
-// Same as controller() function, but explicitly waits until there is a tor daemon
-// to connect to (either launched by tor-launcher, or if we have an existing system
-// tor daemon)
-var wait_for_controller = function (avoidCache) {
- // if tor process is running (either ours or system) immediately return controller
- if (!TorMonitorService.ownsTorDaemon || TorMonitorService.isRunning) {
- return controller(avoidCache);
- }
-
- // otherwise we must wait for tor to finish launching before resolving
- return new Promise((resolve, reject) => {
- let observer = {
- observe: async (subject, topic, data) => {
- if (topic === TorTopics.ProcessIsReady) {
- try {
- resolve(await controller(avoidCache));
- } catch (err) {
- reject(err);
- }
- Services.obs.removeObserver(observer, TorTopics.ProcessIsReady);
- }
- },
- };
- Services.obs.addObserver(observer, TorTopics.ProcessIsReady);
- });
-};
-
-// Export functions for external use.
-var EXPORTED_SYMBOLS = [
- "configureControlPortModule",
- "controller",
- "wait_for_controller",
-];
=====================================
toolkit/torbutton/modules/utils.js deleted
=====================================
@@ -1,276 +0,0 @@
-// # Utils.js
-// Various helpful utility functions.
-
-// ### Import Mozilla Services
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-// ## Pref utils
-
-// __prefs__. A shortcut to Mozilla Services.prefs.
-let prefs = Services.prefs;
-
-// __getPrefValue(prefName)__
-// Returns the current value of a preference, regardless of its type.
-var getPrefValue = function (prefName) {
- switch (prefs.getPrefType(prefName)) {
- case prefs.PREF_BOOL:
- return prefs.getBoolPref(prefName);
- case prefs.PREF_INT:
- return prefs.getIntPref(prefName);
- case prefs.PREF_STRING:
- return prefs.getCharPref(prefName);
- default:
- return null;
- }
-};
-
-// __bindPref(prefName, prefHandler, init)__
-// Applies prefHandler whenever the value of the pref changes.
-// If init is true, applies prefHandler to the current value.
-// Returns a zero-arg function that unbinds the pref.
-var bindPref = function (prefName, prefHandler, init = false) {
- let update = () => {
- prefHandler(getPrefValue(prefName));
- },
- observer = {
- observe(subject, topic, data) {
- if (data === prefName) {
- update();
- }
- },
- };
- prefs.addObserver(prefName, observer);
- if (init) {
- update();
- }
- return () => {
- prefs.removeObserver(prefName, observer);
- };
-};
-
-// __bindPrefAndInit(prefName, prefHandler)__
-// Applies prefHandler to the current value of pref specified by prefName.
-// Re-applies prefHandler whenever the value of the pref changes.
-// Returns a zero-arg function that unbinds the pref.
-var bindPrefAndInit = (prefName, prefHandler) =>
- bindPref(prefName, prefHandler, true);
-
-// ## Observers
-
-// __observe(topic, callback)__.
-// Observe the given topic. When notification of that topic
-// occurs, calls callback(subject, data). Returns a zero-arg
-// function that stops observing.
-var observe = function (topic, callback) {
- let observer = {
- observe(aSubject, aTopic, aData) {
- if (topic === aTopic) {
- callback(aSubject, aData);
- }
- },
- };
- Services.obs.addObserver(observer, topic);
- return () => Services.obs.removeObserver(observer, topic);
-};
-
-// ## Environment variables
-
-// __getEnv(name)__.
-// Reads the environment variable of the given name.
-var getEnv = function (name) {
- return Services.env.exists(name) ? Services.env.get(name) : undefined;
-};
-
-// __getLocale
-// Returns the app locale to be used in tor-related urls.
-var getLocale = function () {
- const locale = Services.locale.appLocaleAsBCP47;
- if (locale === "ja-JP-macos") {
- // We don't want to distinguish the mac locale.
- return "ja";
- }
- return locale;
-};
-
-// ## Windows
-
-// __dialogsByName__.
-// Map of window names to dialogs.
-let dialogsByName = {};
-
-// __showDialog(parent, url, name, features, arg1, arg2, ...)__.
-// Like window.openDialog, but if the window is already
-// open, just focuses it instead of opening a new one.
-var showDialog = function (parent, url, name, features) {
- let existingDialog = dialogsByName[name];
- if (existingDialog && !existingDialog.closed) {
- existingDialog.focus();
- return existingDialog;
- }
- let newDialog = parent.openDialog.apply(parent, Array.slice(arguments, 1));
- dialogsByName[name] = newDialog;
- return newDialog;
-};
-
-// ## Tor control protocol utility functions
-
-let _torControl = {
- // Unescape Tor Control string aStr (removing surrounding "" and \ escapes).
- // Based on Vidalia's src/common/stringutil.cpp:string_unescape().
- // Returns the unescaped string. Throws upon failure.
- // Within Tor Launcher, the file components/tl-protocol.js also contains a
- // copy of _strUnescape().
- _strUnescape(aStr) {
- if (!aStr) {
- return aStr;
- }
-
- var len = aStr.length;
- if (len < 2 || '"' != aStr.charAt(0) || '"' != aStr.charAt(len - 1)) {
- return aStr;
- }
-
- const kHexRE = /[0-9A-Fa-f]{2}/;
- const kOctalRE = /[0-7]{3}/;
- var rv = "";
- var i = 1;
- var lastCharIndex = len - 2;
- while (i <= lastCharIndex) {
- var c = aStr.charAt(i);
- if ("\\" == c) {
- if (++i > lastCharIndex) {
- throw new Error("missing character after \\");
- }
-
- c = aStr.charAt(i);
- if ("n" == c) {
- rv += "\n";
- } else if ("r" == c) {
- rv += "\r";
- } else if ("t" == c) {
- rv += "\t";
- } else if ("x" == c) {
- if (i + 2 > lastCharIndex) {
- throw new Error("not enough hex characters");
- }
-
- let s = aStr.substr(i + 1, 2);
- if (!kHexRE.test(s)) {
- throw new Error("invalid hex characters");
- }
-
- let val = parseInt(s, 16);
- rv += String.fromCharCode(val);
- i += 3;
- } else if (this._isDigit(c)) {
- let s = aStr.substr(i, 3);
- if (i + 2 > lastCharIndex) {
- throw new Error("not enough octal characters");
- }
-
- if (!kOctalRE.test(s)) {
- throw new Error("invalid octal characters");
- }
-
- let val = parseInt(s, 8);
- rv += String.fromCharCode(val);
- i += 3;
- } // "\\" and others
- else {
- rv += c;
- ++i;
- }
- } else if ('"' == c) {
- throw new Error('unescaped " within string');
- } else {
- rv += c;
- ++i;
- }
- }
-
- // Convert from UTF-8 to Unicode. TODO: is UTF-8 always used in protocol?
- return decodeURIComponent(escape(rv));
- }, // _strUnescape()
-
- // Within Tor Launcher, the file components/tl-protocol.js also contains a
- // copy of _isDigit().
- _isDigit(aChar) {
- const kRE = /^\d$/;
- return aChar && kRE.test(aChar);
- },
-}; // _torControl
-
-// __unescapeTorString(str, resultObj)__.
-// Unescape Tor Control string str (removing surrounding "" and \ escapes).
-// Returns the unescaped string. Throws upon failure.
-var unescapeTorString = function (str) {
- return _torControl._strUnescape(str);
-};
-
-var m_tb_torlog = Cc["@torproject.org/torbutton-logger;1"].getService(
- Ci.nsISupports
-).wrappedJSObject;
-
-var m_tb_string_bundle = torbutton_get_stringbundle();
-
-function torbutton_safelog(nLevel, sMsg, scrub) {
- m_tb_torlog.safe_log(nLevel, sMsg, scrub);
- return true;
-}
-
-function torbutton_log(nLevel, sMsg) {
- m_tb_torlog.log(nLevel, sMsg);
-
- // So we can use it in boolean expressions to determine where the
- // short-circuit is..
- return true;
-}
-
-// load localization strings
-function torbutton_get_stringbundle() {
- var o_stringbundle = false;
-
- try {
- var oBundle = Services.strings;
- o_stringbundle = oBundle.createBundle(
- "chrome://torbutton/locale/torbutton.properties"
- );
- } catch (err) {
- o_stringbundle = false;
- }
- if (!o_stringbundle) {
- torbutton_log(5, "ERROR (init): failed to find torbutton-bundle");
- }
-
- return o_stringbundle;
-}
-
-function torbutton_get_property_string(propertyname) {
- try {
- if (!m_tb_string_bundle) {
- m_tb_string_bundle = torbutton_get_stringbundle();
- }
-
- return m_tb_string_bundle.GetStringFromName(propertyname);
- } catch (e) {
- torbutton_log(4, "Unlocalized string " + propertyname);
- }
-
- return propertyname;
-}
-
-// Export utility functions for external use.
-let EXPORTED_SYMBOLS = [
- "bindPref",
- "bindPrefAndInit",
- "getEnv",
- "getLocale",
- "getPrefValue",
- "observe",
- "showDialog",
- "show_torbrowser_manual",
- "unescapeTorString",
- "torbutton_safelog",
- "torbutton_log",
- "torbutton_get_property_string",
-];
=====================================
toolkit/torbutton/moz.build
=====================================
@@ -3,8 +3,4 @@
# 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/.
-JAR_MANIFESTS += ['jar.mn']
-
-XPCOM_MANIFESTS += [
- "components.conf",
-]
+JAR_MANIFESTS += ["jar.mn"]
=====================================
tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
=====================================
@@ -90,11 +90,7 @@ function getGlobalScriptIncludes(scriptPath) {
"browser/components/screenshots/content/"
)
.replace("chrome://browser/content/", "browser/base/content/")
- .replace("chrome://global/content/", "toolkit/content/")
- .replace(
- "chrome://torbutton/content/",
- "toolkit/torbutton/chrome/content/"
- );
+ .replace("chrome://global/content/", "toolkit/content/");
for (let mapping of Object.getOwnPropertyNames(MAPPINGS)) {
if (sourceFile.includes(mapping)) {
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f67d72…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f67d72…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in...
by Pier Angelo Vendrame (@pierov) 04 Aug '23
by Pier Angelo Vendrame (@pierov) 04 Aug '23
04 Aug '23
Pier Angelo Vendrame pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
f67d72dd by Henry Wilkes at 2023-08-04T15:47:04+01:00
fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 41964: Wait for both emoji resources before setting bridge emoji
attributes.
Also change annotations in response to a change in locale.
- - - - -
1 changed file:
- browser/components/torpreferences/content/connectionPane.js
Changes:
=====================================
browser/components/torpreferences/content/connectionPane.js
=====================================
@@ -448,10 +448,6 @@ const gConnectionPane = (function () {
const bridgeCards = prefpane.querySelector(selectors.bridges.cards);
const bridgeMenu = prefpane.querySelector(selectors.bridges.cardMenu);
- let emojiAnnotations;
- const emojiListPromise = fetch(
- "chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json"
- ).then(response => response.json());
this._addBridgeCard = bridgeString => {
const card = bridgeTemplate.cloneNode(true);
card.removeAttribute("id");
@@ -484,16 +480,8 @@ const gConnectionPane = (function () {
const emojis = makeBridgeId(bridgeString).map(emojiIndex => {
const img = document.createElement("img");
img.classList.add("emoji");
- emojiListPromise.then(emojiList => {
- const emoji = emojiList[emojiIndex];
- const cp = emoji.codePointAt(0).toString(16);
- img.setAttribute(
- "src",
- `chrome://browser/content/torpreferences/bridgemoji/svgs/${cp}.svg`
- );
- img.setAttribute("alt", emoji);
- img.setAttribute("title", emojiAnnotations[cp]);
- });
+ // Image is set in _updateBridgeEmojis.
+ img.dataset.emojiIndex = emojiIndex;
return img;
});
const idString = TorStrings.settings.bridgeId;
@@ -699,6 +687,9 @@ const gConnectionPane = (function () {
shownCards--;
}
+ // Newly added emojis.
+ this._updateBridgeEmojis();
+
// And finally update the buttons
removeAll.hidden = false;
showAll.classList.toggle("primary", TorSettings.bridges.enabled);
@@ -729,26 +720,7 @@ const gConnectionPane = (function () {
bridgeCards.classList.remove("list-collapsed");
}
};
- // Use a promise to avoid blocking the population of the page
- // FIXME: Stop using a JSON file, and switch to properties
- const annotationPromise = fetch(
- "chrome://browser/content/torpreferences/bridgemoji/annotations.json"
- );
- annotationPromise.then(async res => {
- const annotations = await res.json();
- const bcp47 = Services.locale.appLocaleAsBCP47;
- const dash = bcp47.indexOf("-");
- const lang = dash !== -1 ? bcp47.substring(0, dash) : bcp47;
- if (bcp47 in annotations) {
- emojiAnnotations = annotations[bcp47];
- } else if (lang in annotations) {
- emojiAnnotations = annotations[lang];
- } else {
- // At the moment, nb does not have annotations!
- emojiAnnotations = annotations.en;
- }
- this._populateBridgeCards();
- });
+ this._populateBridgeCards();
this._updateConnectedBridges = () => {
for (const card of bridgeCards.querySelectorAll(
".currently-connected"
@@ -785,7 +757,7 @@ const gConnectionPane = (function () {
this._updateConnectedBridges();
}
};
- annotationPromise.then(this._checkConnectedBridge.bind(this));
+ this._checkConnectedBridge();
// Add a new bridge
prefpane.querySelector(selectors.bridges.addHeader).textContent =
@@ -879,6 +851,7 @@ const gConnectionPane = (function () {
Services.obs.addObserver(this, TorConnectTopics.StateChange);
Services.obs.addObserver(this, TorMonitorTopics.BridgeChanged);
+ Services.obs.addObserver(this, "intl:app-locales-changed");
},
init() {
@@ -903,6 +876,7 @@ const gConnectionPane = (function () {
Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged);
Services.obs.removeObserver(this, TorConnectTopics.StateChange);
Services.obs.removeObserver(this, TorMonitorTopics.BridgeChanged);
+ Services.obs.removeObserver(this, "intl:app-locales-changed");
},
// whether the page should be present in about:preferences
@@ -939,6 +913,60 @@ const gConnectionPane = (function () {
}
break;
}
+ case "intl:app-locales-changed": {
+ this._updateBridgeEmojis();
+ break;
+ }
+ }
+ },
+
+ /**
+ * Update the bridge emojis to show their corresponding emoji with an
+ * annotation that matches the current locale.
+ */
+ async _updateBridgeEmojis() {
+ if (!this._emojiPromise) {
+ this._emojiPromise = Promise.all([
+ fetch(
+ "chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json"
+ ).then(response => response.json()),
+ fetch(
+ "chrome://browser/content/torpreferences/bridgemoji/annotations.json"
+ ).then(response => response.json()),
+ ]);
+ }
+ const [emojiList, emojiAnnotations] = await this._emojiPromise;
+ let langCode;
+ // Find the first desired locale we have annotations for.
+ // Add "en" as a fallback.
+ for (const bcp47 of [...Services.locale.appLocalesAsBCP47, "en"]) {
+ langCode = bcp47;
+ if (langCode in emojiAnnotations) {
+ break;
+ }
+ // Remove everything after the dash, if there is one.
+ langCode = bcp47.replace(/-.*/, "");
+ if (langCode in emojiAnnotations) {
+ break;
+ }
+ }
+ for (const img of document.querySelectorAll(".emoji[data-emoji-index]")) {
+ const emoji = emojiList[img.dataset.emojiIndex];
+ if (!emoji) {
+ // Unexpected.
+ console.error(`No emoji for index ${img.dataset.emojiIndex}`);
+ img.removeAttribute("src");
+ img.removeAttribute("alt");
+ img.removeAttribute("title");
+ continue;
+ }
+ const cp = emoji.codePointAt(0).toString(16);
+ img.setAttribute(
+ "src",
+ `chrome://browser/content/torpreferences/bridgemoji/svgs/${cp}.svg`
+ );
+ img.setAttribute("alt", emoji);
+ img.setAttribute("title", emojiAnnotations[langCode][cp]);
}
},
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/f67d72d…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/f67d72d…
You're receiving this email because of your account on gitlab.torproject.org.
1
0