richard pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
-
66229717
by Pier Angelo Vendrame at 2023-07-27T18:11:55+02:00
-
2cde9fc3
by Pier Angelo Vendrame at 2023-07-27T18:11:56+02:00
-
b2cd0ee8
by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00
-
26152fa9
by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00
-
749aeaca
by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00
-
20641450
by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00
-
1a8be7b1
by Pier Angelo Vendrame at 2023-07-27T18:11:59+02:00
11 changed files:
- browser/base/content/browser.js
- browser/components/onionservices/content/authPrompt.js
- browser/components/onionservices/content/savedKeysDialog.js
- browser/components/torcircuit/content/torCircuitPanel.js
- browser/components/torpreferences/content/connectionPane.js
- toolkit/components/tor-launcher/TorDomainIsolator.jsm → toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
- toolkit/components/tor-launcher/TorMonitorService.sys.mjs
- toolkit/components/tor-launcher/TorParsers.sys.mjs
- toolkit/components/tor-launcher/TorProtocolService.sys.mjs
- toolkit/components/tor-launcher/TorStartupService.sys.mjs
- toolkit/components/tor-launcher/moz.build
Changes:
... | ... | @@ -66,6 +66,7 @@ ChromeUtils.defineESModuleGetters(this, { |
66 | 66 | TabsSetupFlowManager:
|
67 | 67 | "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs",
|
68 | 68 | TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
|
69 | + TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
|
|
69 | 70 | TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
|
70 | 71 | UITour: "resource:///modules/UITour.sys.mjs",
|
71 | 72 | UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
|
... | ... | @@ -100,7 +101,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { |
100 | 101 | TorConnect: "resource:///modules/TorConnect.jsm",
|
101 | 102 | TorConnectState: "resource:///modules/TorConnect.jsm",
|
102 | 103 | TorConnectTopics: "resource:///modules/TorConnect.jsm",
|
103 | - TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.jsm",
|
|
104 | 104 | Translation: "resource:///modules/translation/TranslationParent.jsm",
|
105 | 105 | webrtcUI: "resource:///modules/webrtcUI.jsm",
|
106 | 106 | ZoomUI: "resource:///modules/ZoomUI.jsm",
|
... | ... | @@ -7,6 +7,7 @@ |
7 | 7 | XPCOMUtils.defineLazyModuleGetters(this, {
|
8 | 8 | OnionAuthUtil: "chrome://browser/content/onionservices/authUtil.jsm",
|
9 | 9 | CommonUtils: "resource://services-common/utils.js",
|
10 | + TorProtocolService: "resource://gre/modules/TorProtocolService.jsm",
|
|
10 | 11 | TorStrings: "resource:///modules/TorStrings.jsm",
|
11 | 12 | });
|
12 | 13 | |
... | ... | @@ -192,10 +193,6 @@ const OnionAuthPrompt = (function () { |
192 | 193 | let controllerFailureMsg =
|
193 | 194 | TorStrings.onionServices.authPrompt.failedToSetKey;
|
194 | 195 | try {
|
195 | - let { controller } = ChromeUtils.import(
|
|
196 | - "resource://torbutton/modules/tor-control-port.js"
|
|
197 | - );
|
|
198 | - let torController = await controller();
|
|
199 | 196 | // ^(subdomain.)*onionserviceid.onion$ (case-insensitive)
|
200 | 197 | const onionServiceIdRegExp =
|
201 | 198 | /^(.*\.)*(?<onionServiceId>[a-z2-7]{56})\.onion$/i;
|
... | ... | @@ -206,8 +203,7 @@ const OnionAuthPrompt = (function () { |
206 | 203 | |
207 | 204 | let checkboxElem = this._getCheckboxElement();
|
208 | 205 | let isPermanent = checkboxElem && checkboxElem.checked;
|
209 | - torController
|
|
210 | - .onionAuthAdd(onionServiceId, base64key, isPermanent)
|
|
206 | + TorProtocolService.onionAuthAdd(onionServiceId, base64key, isPermanent)
|
|
211 | 207 | .then(aResponse => {
|
212 | 208 | // Success! Reload the page.
|
213 | 209 | this._browser.sendMessageToActor(
|
... | ... | @@ -10,8 +10,8 @@ ChromeUtils.defineModuleGetter( |
10 | 10 | |
11 | 11 | ChromeUtils.defineModuleGetter(
|
12 | 12 | this,
|
13 | - "controller",
|
|
14 | - "resource://torbutton/modules/tor-control-port.js"
|
|
13 | + "TorProtocolService",
|
|
14 | + "resource://gre/modules/TorProtocolService.jsm"
|
|
15 | 15 | );
|
16 | 16 | |
17 | 17 | var gOnionServicesSavedKeysDialog = {
|
... | ... | @@ -49,11 +49,9 @@ var gOnionServicesSavedKeysDialog = { |
49 | 49 | const controllerFailureMsg =
|
50 | 50 | TorStrings.onionServices.authPreferences.failedToRemoveKey;
|
51 | 51 | try {
|
52 | - const torController = await controller();
|
|
53 | - |
|
54 | 52 | // Remove in reverse index order to avoid issues caused by index changes.
|
55 | 53 | for (let i = indexesToDelete.length - 1; i >= 0; --i) {
|
56 | - await this._deleteOneKey(torController, indexesToDelete[i]);
|
|
54 | + await this._deleteOneKey(indexesToDelete[i]);
|
|
57 | 55 | }
|
58 | 56 | } catch (e) {
|
59 | 57 | if (e.torMessage) {
|
... | ... | @@ -127,8 +125,7 @@ var gOnionServicesSavedKeysDialog = { |
127 | 125 | try {
|
128 | 126 | this._tree.view = this;
|
129 | 127 | |
130 | - const torController = await controller();
|
|
131 | - const keyInfoList = await torController.onionAuthViewKeys();
|
|
128 | + const keyInfoList = await TorProtocolService.onionAuthViewKeys();
|
|
132 | 129 | if (keyInfoList) {
|
133 | 130 | // Filter out temporary keys.
|
134 | 131 | this._keyInfoList = keyInfoList.filter(aKeyInfo => {
|
... | ... | @@ -165,9 +162,9 @@ var gOnionServicesSavedKeysDialog = { |
165 | 162 | },
|
166 | 163 | |
167 | 164 | // This method may throw; callers should catch errors.
|
168 | - async _deleteOneKey(aTorController, aIndex) {
|
|
165 | + async _deleteOneKey(aIndex) {
|
|
169 | 166 | const keyInfoObj = this._keyInfoList[aIndex];
|
170 | - await aTorController.onionAuthRemove(keyInfoObj.hsAddress);
|
|
167 | + await TorProtocolService.onionAuthRemove(keyInfoObj.hsAddress);
|
|
171 | 168 | this._tree.view.selection.clearRange(aIndex, aIndex);
|
172 | 169 | this._keyInfoList.splice(aIndex, 1);
|
173 | 170 | this._tree.rowCountChanged(aIndex + 1, -1);
|
1 | 1 | /* eslint-env mozilla/browser-window */
|
2 | 2 | |
3 | -/**
|
|
4 | - * Stores the data associated with a circuit node.
|
|
5 | - *
|
|
6 | - * @typedef NodeData
|
|
7 | - * @property {string[]} ipAddrs - The ip addresses associated with this node.
|
|
8 | - * @property {string?} bridgeType - The bridge type for this node, or "" if the
|
|
9 | - * node is a bridge but the type is unknown, or null if this is not a bridge
|
|
10 | - * node.
|
|
11 | - * @property {string?} regionCode - An upper case 2-letter ISO3166-1 code for
|
|
12 | - * the first ip address, or null if there is no region. This should also be a
|
|
13 | - * valid BCP47 Region subtag.
|
|
14 | - */
|
|
15 | - |
|
16 | 3 | /**
|
17 | 4 | * Data about the current domain and circuit for a xul:browser.
|
18 | 5 | *
|
... | ... | @@ -35,29 +22,6 @@ var gTorCircuitPanel = { |
35 | 22 | * @type {Element}
|
36 | 23 | */
|
37 | 24 | toolbarButton: null,
|
38 | - /**
|
|
39 | - * A list of IDs for "mature" circuits (those that have conveyed a stream).
|
|
40 | - *
|
|
41 | - * @type {string[]}
|
|
42 | - */
|
|
43 | - _knownCircuitIDs: [],
|
|
44 | - /**
|
|
45 | - * Stores the circuit nodes for each SOCKS username/password pair. The keys
|
|
46 | - * are of the form "<username>|<password>".
|
|
47 | - *
|
|
48 | - * @type {Map<string, NodeData[]>}
|
|
49 | - */
|
|
50 | - _credentialsToCircuitNodes: new Map(),
|
|
51 | - /**
|
|
52 | - * Browser data for their currently shown page.
|
|
53 | - *
|
|
54 | - * This data may be stale for a given browser since we only update this data
|
|
55 | - * when loading a new page in the currently selected browser, when switching
|
|
56 | - * tabs, or if we find a new circuit for the current browser.
|
|
57 | - *
|
|
58 | - * @type {WeakMap<MozBrowser, BrowserCircuitData>}
|
|
59 | - */
|
|
60 | - _browserData: new WeakMap(),
|
|
61 | 25 | /**
|
62 | 26 | * The data for the currently shown browser.
|
63 | 27 | *
|
... | ... | @@ -71,6 +35,13 @@ var gTorCircuitPanel = { |
71 | 35 | */
|
72 | 36 | _isActive: false,
|
73 | 37 | |
38 | + /**
|
|
39 | + * The topic on which circuit changes are broadcast.
|
|
40 | + *
|
|
41 | + * @type {string}
|
|
42 | + */
|
|
43 | + TOR_CIRCUIT_TOPIC: "TorCircuitChange",
|
|
44 | + |
|
74 | 45 | /**
|
75 | 46 | * Initialize the panel.
|
76 | 47 | */
|
... | ... | @@ -86,31 +57,6 @@ var gTorCircuitPanel = { |
86 | 57 | maxLogLevelPref: "browser.torcircuitpanel.loglevel",
|
87 | 58 | });
|
88 | 59 | |
89 | - const { wait_for_controller } = ChromeUtils.import(
|
|
90 | - "resource://torbutton/modules/tor-control-port.js"
|
|
91 | - );
|
|
92 | - wait_for_controller().then(
|
|
93 | - controller => {
|
|
94 | - if (!this._isActive) {
|
|
95 | - // uninit() was called before resolution.
|
|
96 | - return;
|
|
97 | - }
|
|
98 | - // FIXME: We should be using some dedicated integrated back end to
|
|
99 | - // store circuit information, rather than collecting it all here in the
|
|
100 | - // front end. See tor-browser#41700.
|
|
101 | - controller.watchEvent(
|
|
102 | - "STREAM",
|
|
103 | - streamEvent => streamEvent.StreamStatus === "SENTCONNECT",
|
|
104 | - streamEvent => this._collectCircuit(controller, streamEvent)
|
|
105 | - );
|
|
106 | - },
|
|
107 | - error => {
|
|
108 | - this._log.error(
|
|
109 | - `Not collecting circuits because of an error: ${error.message}`
|
|
110 | - );
|
|
111 | - }
|
|
112 | - );
|
|
113 | - |
|
114 | 60 | this.panel = document.getElementById("tor-circuit-panel");
|
115 | 61 | this._panelElements = {
|
116 | 62 | heading: document.getElementById("tor-circuit-heading"),
|
... | ... | @@ -245,6 +191,9 @@ var gTorCircuitPanel = { |
245 | 191 | // Notified of new locations for the currently selected browser (tab) *and*
|
246 | 192 | // switching selected browser.
|
247 | 193 | gBrowser.addProgressListener(this._locationListener);
|
194 | + |
|
195 | + // Get notifications for circuit changes.
|
|
196 | + Services.obs.addObserver(this, this.TOR_CIRCUIT_TOPIC);
|
|
248 | 197 | },
|
249 | 198 | |
250 | 199 | /**
|
... | ... | @@ -253,6 +202,17 @@ var gTorCircuitPanel = { |
253 | 202 | uninit() {
|
254 | 203 | this._isActive = false;
|
255 | 204 | gBrowser.removeProgressListener(this._locationListener);
|
205 | + Services.obs.removeObserver(this, this.TOR_CIRCUIT_TOPIC);
|
|
206 | + },
|
|
207 | + |
|
208 | + /**
|
|
209 | + * Observe circuit changes.
|
|
210 | + */
|
|
211 | + observe(subject, topic, data) {
|
|
212 | + if (topic === this.TOR_CIRCUIT_TOPIC) {
|
|
213 | + // TODO: Maybe check if we actually need to do something earlier.
|
|
214 | + this._updateCurrentBrowser();
|
|
215 | + }
|
|
256 | 216 | },
|
257 | 217 | |
258 | 218 | /**
|
... | ... | @@ -286,109 +246,6 @@ var gTorCircuitPanel = { |
286 | 246 | window.openWebLinkIn(this._panelElements.aliasLink.href, where);
|
287 | 247 | },
|
288 | 248 | |
289 | - /**
|
|
290 | - * Collect circuit data for the found circuits, to be used later for display.
|
|
291 | - *
|
|
292 | - * @param {controller} controller - The tor controller.
|
|
293 | - * @param {object} streamEvent - The streamEvent for the new circuit.
|
|
294 | - */
|
|
295 | - async _collectCircuit(controller, streamEvent) {
|
|
296 | - const id = streamEvent.CircuitID;
|
|
297 | - if (this._knownCircuitIDs.includes(id)) {
|
|
298 | - return;
|
|
299 | - }
|
|
300 | - this._log.debug(`New streamEvent.CircuitID: ${id}.`);
|
|
301 | - // FIXME: This list grows and is never freed. See tor-browser#41700.
|
|
302 | - this._knownCircuitIDs.push(id);
|
|
303 | - const circuitStatus = (await controller.getInfo("circuit-status"))?.find(
|
|
304 | - circuit => circuit.id === id
|
|
305 | - );
|
|
306 | - if (!circuitStatus?.SOCKS_USERNAME || !circuitStatus?.SOCKS_PASSWORD) {
|
|
307 | - return;
|
|
308 | - }
|
|
309 | - const nodes = await Promise.all(
|
|
310 | - circuitStatus.circuit.map(names =>
|
|
311 | - this._nodeDataForCircuit(controller, names)
|
|
312 | - )
|
|
313 | - );
|
|
314 | - // Remove quotes from the strings.
|
|
315 | - const username = circuitStatus.SOCKS_USERNAME.replace(/^"(.*)"$/, "$1");
|
|
316 | - const password = circuitStatus.SOCKS_PASSWORD.replace(/^"(.*)"$/, "$1");
|
|
317 | - const credentials = `${username}|${password}`;
|
|
318 | - // FIXME: This map grows and is never freed. We cannot simply request this
|
|
319 | - // information when needed because it is no longer available once the
|
|
320 | - // circuit is dropped, even if the web page is still displayed.
|
|
321 | - // See tor-browser#41700.
|
|
322 | - this._credentialsToCircuitNodes.set(credentials, nodes);
|
|
323 | - // Update the circuit in case the current page gains a new circuit whilst
|
|
324 | - // the popup is still open.
|
|
325 | - this._updateCurrentBrowser(credentials);
|
|
326 | - },
|
|
327 | - |
|
328 | - /**
|
|
329 | - * Fetch the node data for the given circuit node.
|
|
330 | - *
|
|
331 | - * @param {controller} controller - The tor controller.
|
|
332 | - * @param {string[]} circuitNodeNames - The names for the circuit node. Only
|
|
333 | - * the first name, the node id, will be used.
|
|
334 | - *
|
|
335 | - * @returns {NodeData} - The data for this circuit node.
|
|
336 | - */
|
|
337 | - async _nodeDataForCircuit(controller, circuitNodeNames) {
|
|
338 | - // The first "name" in circuitNodeNames is the id.
|
|
339 | - // Remove the leading '$' if present.
|
|
340 | - const id = circuitNodeNames[0].replace(/^\$/, "");
|
|
341 | - let result = { ipAddrs: [], bridgeType: null, regionCode: null };
|
|
342 | - const bridge = (await controller.getConf("bridge"))?.find(
|
|
343 | - foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase()
|
|
344 | - );
|
|
345 | - const addrRe = /^\[?([^\]]+)\]?:\d+$/;
|
|
346 | - if (bridge) {
|
|
347 | - result.bridgeType = bridge.type ?? "";
|
|
348 | - // Attempt to get an IP address from bridge address string.
|
|
349 | - const ip = bridge.address.match(addrRe)?.[1];
|
|
350 | - if (ip && !ip.startsWith("0.")) {
|
|
351 | - result.ipAddrs.push(ip);
|
|
352 | - }
|
|
353 | - } else {
|
|
354 | - // Either dealing with a relay, or a bridge whose fingerprint is not saved
|
|
355 | - // in torrc.
|
|
356 | - let statusMap;
|
|
357 | - try {
|
|
358 | - statusMap = await controller.getInfo("ns/id/" + id);
|
|
359 | - } catch {
|
|
360 | - // getInfo will throw if the given id is not a relay.
|
|
361 | - // This probably means we are dealing with a user-provided bridge with
|
|
362 | - // no fingerprint.
|
|
363 | - // We don't know the ip/ipv6 or type, so leave blank.
|
|
364 | - result.bridgeType = "";
|
|
365 | - return result;
|
|
366 | - }
|
|
367 | - if (statusMap.IP && !statusMap.IP.startsWith("0.")) {
|
|
368 | - result.ipAddrs.push(statusMap.IP);
|
|
369 | - }
|
|
370 | - const ip6 = statusMap.IPv6?.match(addrRe)?.[1];
|
|
371 | - if (ip6) {
|
|
372 | - result.ipAddrs.push(ip6);
|
|
373 | - }
|
|
374 | - }
|
|
375 | - if (result.ipAddrs.length) {
|
|
376 | - // Get the country code for the node's IP address.
|
|
377 | - let regionCode;
|
|
378 | - try {
|
|
379 | - // Expect a 2-letter ISO3166-1 code, which should also be a valid BCP47
|
|
380 | - // Region subtag.
|
|
381 | - regionCode = await controller.getInfo(
|
|
382 | - "ip-to-country/" + result.ipAddrs[0]
|
|
383 | - );
|
|
384 | - } catch {}
|
|
385 | - if (regionCode && regionCode !== "??") {
|
|
386 | - result.regionCode = regionCode.toUpperCase();
|
|
387 | - }
|
|
388 | - }
|
|
389 | - return result;
|
|
390 | - },
|
|
391 | - |
|
392 | 249 | /**
|
393 | 250 | * A list of schemes to never show the circuit display for.
|
394 | 251 | *
|
... | ... | @@ -398,71 +255,50 @@ var gTorCircuitPanel = { |
398 | 255 | *
|
399 | 256 | * @type {string[]}
|
400 | 257 | */
|
401 | - // FIXME: Have a back end that handles this instead. See tor-browser#41700.
|
|
258 | + // FIXME: Check if we find a UX to handle some of these cases, and if we
|
|
259 | + // manage to solve some technical issues.
|
|
260 | + // See tor-browser#41700 and tor-browser!699.
|
|
402 | 261 | _ignoredSchemes: ["about", "file", "chrome", "resource"],
|
403 | 262 | |
404 | 263 | /**
|
405 | 264 | * Update the current circuit and domain data for the currently selected
|
406 | 265 | * browser, possibly changing the UI.
|
407 | - *
|
|
408 | - * @param {string?} [matchingCredentials=null] - If given, only update the
|
|
409 | - * current browser data if the current browser's credentials match.
|
|
410 | 266 | */
|
411 | - _updateCurrentBrowser(matchingCredentials = null) {
|
|
267 | + _updateCurrentBrowser() {
|
|
412 | 268 | const browser = gBrowser.selectedBrowser;
|
413 | 269 | const domain = TorDomainIsolator.getDomainForBrowser(browser);
|
270 | + const nodes = TorDomainIsolator.getCircuit(
|
|
271 | + browser,
|
|
272 | + domain,
|
|
273 | + browser.contentPrincipal.originAttributes.userContextId
|
|
274 | + );
|
|
414 | 275 | // We choose the currentURI, which matches what is shown in the URL bar and
|
415 | 276 | // will match up with the domain.
|
416 | 277 | // In contrast, documentURI corresponds to the shown page. E.g. it could
|
417 | 278 | // point to "about:certerror".
|
418 | 279 | const scheme = browser.currentURI?.scheme;
|
419 | 280 | |
420 | - let credentials = TorDomainIsolator.getSocksProxyCredentials(
|
|
421 | - domain,
|
|
422 | - browser.contentPrincipal.originAttributes.userContextId
|
|
423 | - );
|
|
424 | - if (credentials) {
|
|
425 | - credentials = `${credentials.username}|${credentials.password}`;
|
|
426 | - }
|
|
427 | - |
|
428 | - if (matchingCredentials && matchingCredentials !== credentials) {
|
|
429 | - // This update was triggered by the circuit update for some other browser
|
|
430 | - // or process.
|
|
431 | - return;
|
|
432 | - }
|
|
433 | - |
|
434 | - let nodes = this._credentialsToCircuitNodes.get(credentials) ?? [];
|
|
435 | - |
|
436 | - const prevData = this._browserData.get(browser);
|
|
437 | - if (
|
|
438 | - prevData &&
|
|
439 | - prevData.domain &&
|
|
440 | - prevData.domain === domain &&
|
|
441 | - prevData.scheme === scheme &&
|
|
442 | - prevData.nodes.length &&
|
|
443 | - !nodes.length
|
|
444 | - ) {
|
|
445 | - // Since this is the same domain, for the same browser, and we used to
|
|
446 | - // have circuit nodes, we *assume* we are re-generating a circuit. So we
|
|
447 | - // keep the old circuit data around for the time being.
|
|
448 | - // FIXME: Have a back end that makes this explicit, rather than an
|
|
449 | - // assumption. See tor-browser#41700.
|
|
450 | - nodes = prevData.nodes;
|
|
451 | - this._log.debug(`Keeping old circuit for ${domain}.`);
|
|
452 | - }
|
|
453 | - |
|
454 | - this._browserData.set(browser, { domain, scheme, nodes });
|
|
455 | 281 | if (
|
456 | 282 | this._currentBrowserData &&
|
457 | 283 | this._currentBrowserData.domain === domain &&
|
458 | 284 | this._currentBrowserData.scheme === scheme &&
|
459 | - this._currentBrowserData.nodes === nodes
|
|
285 | + this._currentBrowserData.nodes.length === nodes.length &&
|
|
286 | + // If non-null, the fingerprints of the nodes match.
|
|
287 | + (!nodes ||
|
|
288 | + nodes.every(
|
|
289 | + (n, index) =>
|
|
290 | + n.fingerprint === this._currentBrowserData.nodes[index].fingerprint
|
|
291 | + ))
|
|
460 | 292 | ) {
|
461 | 293 | // No change.
|
294 | + this._log.debug(
|
|
295 | + "Skipping browser update because the data is already up to date."
|
|
296 | + );
|
|
462 | 297 | return;
|
463 | 298 | }
|
464 | 299 | |
465 | - this._currentBrowserData = this._browserData.get(browser);
|
|
300 | + this._currentBrowserData = { domain, scheme, nodes };
|
|
301 | + this._log.debug("Updating current browser.", this._currentBrowserData);
|
|
466 | 302 | |
467 | 303 | if (
|
468 | 304 | // Schemes where we always want to hide the display.
|
... | ... | @@ -17,6 +17,9 @@ const { TorSettings, TorSettingsTopics, TorSettingsData, TorBridgeSource } = |
17 | 17 | const { TorProtocolService } = ChromeUtils.import(
|
18 | 18 | "resource://gre/modules/TorProtocolService.jsm"
|
19 | 19 | );
|
20 | +const { TorMonitorService, TorMonitorTopics } = ChromeUtils.import(
|
|
21 | + "resource://gre/modules/TorMonitorService.jsm"
|
|
22 | +);
|
|
20 | 23 | |
21 | 24 | const { TorConnect, TorConnectTopics, TorConnectState, TorCensorshipLevel } =
|
22 | 25 | ChromeUtils.import("resource:///modules/TorConnect.jsm");
|
... | ... | @@ -144,8 +147,6 @@ const gConnectionPane = (function () { |
144 | 147 | |
145 | 148 | _internetStatus: InternetStatus.Unknown,
|
146 | 149 | |
147 | - _controller: null,
|
|
148 | - |
|
149 | 150 | _currentBridgeId: null,
|
150 | 151 | |
151 | 152 | // populate xul with strings and cache the relevant elements
|
... | ... | @@ -727,9 +728,10 @@ const gConnectionPane = (function () { |
727 | 728 | };
|
728 | 729 | // Use a promise to avoid blocking the population of the page
|
729 | 730 | // FIXME: Stop using a JSON file, and switch to properties
|
730 | - fetch(
|
|
731 | + const annotationPromise = fetch(
|
|
731 | 732 | "chrome://browser/content/torpreferences/bridgemoji/annotations.json"
|
732 | - ).then(async res => {
|
|
733 | + );
|
|
734 | + annotationPromise.then(async res => {
|
|
733 | 735 | const annotations = await res.json();
|
734 | 736 | const bcp47 = Services.locale.appLocaleAsBCP47;
|
735 | 737 | const dash = bcp47.indexOf("-");
|
... | ... | @@ -749,6 +751,7 @@ const gConnectionPane = (function () { |
749 | 751 | ".currently-connected"
|
750 | 752 | )) {
|
751 | 753 | card.classList.remove("currently-connected");
|
754 | + card.querySelector(selectors.bridges.cardQrGrid).style.height = "";
|
|
752 | 755 | }
|
753 | 756 | if (!this._currentBridgeId) {
|
754 | 757 | return;
|
... | ... | @@ -769,72 +772,17 @@ const gConnectionPane = (function () { |
769 | 772 | placeholder.replaceWith(...cards);
|
770 | 773 | this._checkBridgeCardsHeight();
|
771 | 774 | };
|
772 | - try {
|
|
773 | - const { controller } = ChromeUtils.import(
|
|
774 | - "resource://torbutton/modules/tor-control-port.js"
|
|
775 | - );
|
|
776 | - // Avoid the cache because we set our custom event watcher, and at the
|
|
777 | - // moment, watchers cannot be removed from a controller.
|
|
778 | - controller(true).then(aController => {
|
|
779 | - this._controller = aController;
|
|
780 | - // Getting the circuits may be enough, if we have bootstrapped for a
|
|
781 | - // while, but at the beginning it gives many bridges as connected,
|
|
782 | - // because tor pokes all the bridges to find the best one.
|
|
783 | - // Also, watching circuit events does not work, at the moment, but in
|
|
784 | - // any case, checking the stream has the advantage that we can see if
|
|
785 | - // it really used for a connection, rather than tor having created
|
|
786 | - // this circuit to check if the bridge can be used. We do this by
|
|
787 | - // checking if the stream has SOCKS username, which actually contains
|
|
788 | - // the destination of the stream.
|
|
789 | - // FIXME: We only know the currentBridge *after* a circuit event, but
|
|
790 | - // if the circuit event is sent *before* about:torpreferences is
|
|
791 | - // opened we will miss it. Therefore this approach only works if a
|
|
792 | - // circuit is created after opening about:torconnect. A dedicated
|
|
793 | - // backend outside of about:preferences would help, and could be
|
|
794 | - // shared with gTorCircuitPanel. See tor-browser#41700.
|
|
795 | - this._controller.watchEvent(
|
|
796 | - "STREAM",
|
|
797 | - event =>
|
|
798 | - event.StreamStatus === "SUCCEEDED" && "SOCKS_USERNAME" in event,
|
|
799 | - async event => {
|
|
800 | - const circuitStatuses = await this._controller.getInfo(
|
|
801 | - "circuit-status"
|
|
802 | - );
|
|
803 | - if (!circuitStatuses) {
|
|
804 | - return;
|
|
805 | - }
|
|
806 | - for (const status of circuitStatuses) {
|
|
807 | - if (status.id === event.CircuitID && status.circuit.length) {
|
|
808 | - // The id in the circuit begins with a $ sign.
|
|
809 | - const id = status.circuit[0][0].replace(/^\$/, "");
|
|
810 | - if (id !== this._currentBridgeId) {
|
|
811 | - const bridge = (
|
|
812 | - await this._controller.getConf("bridge")
|
|
813 | - )?.find(
|
|
814 | - foundBridge =>
|
|
815 | - foundBridge.ID?.toUpperCase() === id.toUpperCase()
|
|
816 | - );
|
|
817 | - if (!bridge) {
|
|
818 | - // Either there is no bridge, or bridge with no
|
|
819 | - // fingerprint.
|
|
820 | - this._currentBridgeId = null;
|
|
821 | - } else {
|
|
822 | - this._currentBridgeId = id;
|
|
823 | - }
|
|
824 | - this._updateConnectedBridges();
|
|
825 | - }
|
|
826 | - break;
|
|
827 | - }
|
|
828 | - }
|
|
829 | - }
|
|
830 | - );
|
|
831 | - });
|
|
832 | - } catch (err) {
|
|
833 | - console.warn(
|
|
834 | - "We could not load torbutton, bridge statuses will not be updated",
|
|
835 | - err
|
|
836 | - );
|
|
837 | - }
|
|
775 | + this._checkConnectedBridge = () => {
|
|
776 | + // TODO: We could make sure TorSettings is in sync by monitoring also
|
|
777 | + // changes of settings. At that point, we could query it, instead of
|
|
778 | + // doing a query over the control port.
|
|
779 | + const bridge = TorMonitorService.currentBridge;
|
|
780 | + if (bridge?.fingerprint !== this._currentBridgeId) {
|
|
781 | + this._currentBridgeId = bridge?.fingerprint ?? null;
|
|
782 | + this._updateConnectedBridges();
|
|
783 | + }
|
|
784 | + };
|
|
785 | + annotationPromise.then(this._checkConnectedBridge.bind(this));
|
|
838 | 786 | |
839 | 787 | // Add a new bridge
|
840 | 788 | prefpane.querySelector(selectors.bridges.addHeader).textContent =
|
... | ... | @@ -927,6 +875,7 @@ const gConnectionPane = (function () { |
927 | 875 | });
|
928 | 876 | |
929 | 877 | Services.obs.addObserver(this, TorConnectTopics.StateChange);
|
878 | + Services.obs.addObserver(this, TorMonitorTopics.BridgeChanged);
|
|
930 | 879 | },
|
931 | 880 | |
932 | 881 | init() {
|
... | ... | @@ -950,11 +899,7 @@ const gConnectionPane = (function () { |
950 | 899 | // unregister our observer topics
|
951 | 900 | Services.obs.removeObserver(this, TorSettingsTopics.SettingChanged);
|
952 | 901 | Services.obs.removeObserver(this, TorConnectTopics.StateChange);
|
953 | - |
|
954 | - if (this._controller !== null) {
|
|
955 | - this._controller.close();
|
|
956 | - this._controller = null;
|
|
957 | - }
|
|
902 | + Services.obs.removeObserver(this, TorMonitorTopics.BridgeChanged);
|
|
958 | 903 | },
|
959 | 904 | |
960 | 905 | // whether the page should be present in about:preferences
|
... | ... | @@ -985,6 +930,12 @@ const gConnectionPane = (function () { |
985 | 930 | this.onStateChange();
|
986 | 931 | break;
|
987 | 932 | }
|
933 | + case TorMonitorTopics.BridgeChanged: {
|
|
934 | + if (data?.fingerprint !== this._currentBridgeId) {
|
|
935 | + this._checkConnectedBridge();
|
|
936 | + }
|
|
937 | + break;
|
|
938 | + }
|
|
988 | 939 | }
|
989 | 940 | },
|
990 | 941 | |
... | ... | @@ -1028,7 +979,7 @@ const gConnectionPane = (function () { |
1028 | 979 | onRemoveAllBridges() {
|
1029 | 980 | TorSettings.bridges.enabled = false;
|
1030 | 981 | TorSettings.bridges.bridge_strings = "";
|
1031 | - if (TorSettings.bridges.source == TorBridgeSource.BuiltIn) {
|
|
982 | + if (TorSettings.bridges.source === TorBridgeSource.BuiltIn) {
|
|
1032 | 983 | TorSettings.bridges.builtin_type = "";
|
1033 | 984 | }
|
1034 | 985 | TorSettings.saveToPrefs();
|
1 | -// A component for Tor Browser that puts requests from different
|
|
2 | -// first party domains on separate Tor circuits.
|
|
3 | - |
|
4 | -var EXPORTED_SYMBOLS = ["TorDomainIsolator"];
|
|
1 | +/**
|
|
2 | + * A component for Tor Browser that puts requests from different first party
|
|
3 | + * domains on separate Tor circuits.
|
|
4 | + */
|
|
5 | 5 | |
6 | -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
7 | -const { XPCOMUtils } = ChromeUtils.import(
|
|
8 | - "resource://gre/modules/XPCOMUtils.jsm"
|
|
9 | -);
|
|
10 | -const { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
|
|
6 | +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
7 | +import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
|
|
8 | +import {
|
|
9 | + clearInterval,
|
|
10 | + setInterval,
|
|
11 | +} from "resource://gre/modules/Timer.sys.mjs";
|
|
11 | 12 | |
12 | 13 | const lazy = {};
|
13 | 14 | |
... | ... | @@ -18,11 +19,10 @@ XPCOMUtils.defineLazyServiceGetters(lazy, { |
18 | 19 | ],
|
19 | 20 | });
|
20 | 21 | |
21 | -ChromeUtils.defineModuleGetter(
|
|
22 | - lazy,
|
|
23 | - "TorProtocolService",
|
|
24 | - "resource://gre/modules/TorProtocolService.jsm"
|
|
25 | -);
|
|
22 | +ChromeUtils.defineESModuleGetters(lazy, {
|
|
23 | + TorMonitorTopics: "resource://gre/modules/TorMonitorService.sys.mjs",
|
|
24 | + TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
|
|
25 | +});
|
|
26 | 26 | |
27 | 27 | const logger = new ConsoleAPI({
|
28 | 28 | prefix: "TorDomainIsolator",
|
... | ... | @@ -33,6 +33,12 @@ const logger = new ConsoleAPI({ |
33 | 33 | // The string to use instead of the domain when it is not known.
|
34 | 34 | const CATCHALL_DOMAIN = "--unknown--";
|
35 | 35 | |
36 | +// The maximum lifetime for the catch-all circuit in milliseconds.
|
|
37 | +// When the catch-all circuit is needed, we check if more than this amount of
|
|
38 | +// time has passed since we last changed it nonce, and in case we change it
|
|
39 | +// again.
|
|
40 | +const CATCHALL_MAX_LIFETIME = 600_000;
|
|
41 | + |
|
36 | 42 | // The preference to observe, to know whether isolation should be enabled or
|
37 | 43 | // disabled.
|
38 | 44 | const NON_TOR_PROXY_PREF = "extensions.torbutton.use_nontor_proxy";
|
... | ... | @@ -40,23 +46,92 @@ const NON_TOR_PROXY_PREF = "extensions.torbutton.use_nontor_proxy"; |
40 | 46 | // The topic of new identity, to observe to cleanup all the nonces.
|
41 | 47 | const NEW_IDENTITY_TOPIC = "new-identity-requested";
|
42 | 48 | |
49 | +// The topic on which we broacast circuit change notifications.
|
|
50 | +const TOR_CIRCUIT_TOPIC = "TorCircuitChange";
|
|
51 | + |
|
52 | +// We have an interval to delete circuits that are not reclaimed by any browser.
|
|
53 | +const CLEAR_TIMEOUT = 600_000;
|
|
54 | + |
|
55 | +/**
|
|
56 | + * @typedef {string} CircuitId A string that we use to identify a circuit.
|
|
57 | + * Currently, it is a string that combines SOCKS credentials, to make it easier
|
|
58 | + * to use as a map key.
|
|
59 | + * It is not related to Tor's CircuitIDs.
|
|
60 | + */
|
|
61 | +/**
|
|
62 | + * @typedef {number} BrowserId
|
|
63 | + */
|
|
64 | +/**
|
|
65 | + * @typedef {NodeData[]} CircuitData The data about the nodes, ordered from
|
|
66 | + * guard (or bridge) to exit.
|
|
67 | + */
|
|
68 | +/**
|
|
69 | + * @typedef BrowserCircuits Circuits related to a certain combination of
|
|
70 | + * isolators (first-party domain and user context ID, currently).
|
|
71 | + * @property {CircuitId} current The id of the last known circuit that has been
|
|
72 | + * used to fetch data for the isolated context.
|
|
73 | + * @property {CircuitId?} pending The id of the last used circuit for this
|
|
74 | + * isolation context. We might or might not know data about it, yet. But if we
|
|
75 | + * know it, we should move this id into current.
|
|
76 | + */
|
|
77 | + |
|
43 | 78 | class TorDomainIsolatorImpl {
|
44 | - // A mutable map that records what nonce we are using for each domain.
|
|
79 | + /**
|
|
80 | + * A mutable map that records what nonce we are using for each domain.
|
|
81 | + *
|
|
82 | + * @type {Map<string, string>}
|
|
83 | + */
|
|
45 | 84 | #noncesForDomains = new Map();
|
46 | 85 | |
47 | - // A mutable map that records what nonce we are using for each tab container.
|
|
86 | + /**
|
|
87 | + * A mutable map that records what nonce we are using for each tab container.
|
|
88 | + *
|
|
89 | + * @type {Map<string, string>}
|
|
90 | + */
|
|
48 | 91 | #noncesForUserContextId = new Map();
|
49 | 92 | |
50 | - // A bool that controls if we use SOCKS auth for isolation or not.
|
|
93 | + /**
|
|
94 | + * Tell whether we use SOCKS auth for isolation or not.
|
|
95 | + *
|
|
96 | + * @type {boolean}
|
|
97 | + */
|
|
51 | 98 | #isolationEnabled = true;
|
52 | 99 | |
53 | - // Specifies when the current catch-all circuit was first used
|
|
100 | + /**
|
|
101 | + * Specifies when the current catch-all circuit was first used.
|
|
102 | + *
|
|
103 | + * @type {integer}
|
|
104 | + */
|
|
54 | 105 | #catchallDirtySince = Date.now();
|
55 | 106 | |
107 | + /**
|
|
108 | + * A map that associates circuit ids to the circuit information.
|
|
109 | + *
|
|
110 | + * @type {Map<CircuitId, CircuitData>}
|
|
111 | + */
|
|
112 | + #knownCircuits = new Map();
|
|
113 | + |
|
114 | + /**
|
|
115 | + * A map that associates a certain browser to all the circuits it used or it
|
|
116 | + * is going to use.
|
|
117 | + * The circuits are keyed on the SOCKS username, which we take for granted
|
|
118 | + * being a combination of the first-party domain and the user context id.
|
|
119 | + *
|
|
120 | + * @type {Map<BrowserId, Map<string, BrowserCircuits>>}
|
|
121 | + */
|
|
122 | + #browsers = new Map();
|
|
123 | + |
|
124 | + /**
|
|
125 | + * The handle of the interval we use to cleanup old circuit data.
|
|
126 | + *
|
|
127 | + * @type {number?}
|
|
128 | + */
|
|
129 | + #cleanupIntervalId = null;
|
|
130 | + |
|
56 | 131 | /**
|
57 | 132 | * Initialize the domain isolator.
|
58 | - * This function will setup the proxy filter that injects the credentials and
|
|
59 | - * register some observers.
|
|
133 | + * This function will setup the proxy filter that injects the credentials,
|
|
134 | + * register some observers, and setup the cleaning interval.
|
|
60 | 135 | */
|
61 | 136 | init() {
|
62 | 137 | logger.info("Setup circuit isolation by domain and user context");
|
... | ... | @@ -68,14 +143,25 @@ class TorDomainIsolatorImpl { |
68 | 143 | |
69 | 144 | Services.prefs.addObserver(NON_TOR_PROXY_PREF, this);
|
70 | 145 | Services.obs.addObserver(this, NEW_IDENTITY_TOPIC);
|
146 | + Services.obs.addObserver(this, lazy.TorMonitorTopics.StreamSucceeded);
|
|
147 | + |
|
148 | + this.#cleanupIntervalId = setInterval(
|
|
149 | + this.#clearKnownCircuits.bind(this),
|
|
150 | + CLEAR_TIMEOUT
|
|
151 | + );
|
|
71 | 152 | }
|
72 | 153 | |
73 | 154 | /**
|
74 | - * Removes the observers added in the initialization.
|
|
155 | + * Removes the observers added in the initialization and stops the cleaning
|
|
156 | + * interval.
|
|
75 | 157 | */
|
76 | 158 | uninit() {
|
77 | 159 | Services.prefs.removeObserver(NON_TOR_PROXY_PREF, this);
|
78 | 160 | Services.obs.removeObserver(this, NEW_IDENTITY_TOPIC);
|
161 | + Services.obs.removeObserver(this, lazy.TorMonitorTopics.StreamSucceeded);
|
|
162 | + clearInterval(this.#cleanupIntervalId);
|
|
163 | + this.#cleanupIntervalId = null;
|
|
164 | + this.clearIsolation();
|
|
79 | 165 | }
|
80 | 166 | |
81 | 167 | enable() {
|
... | ... | @@ -89,52 +175,52 @@ class TorDomainIsolatorImpl { |
89 | 175 | }
|
90 | 176 | |
91 | 177 | /**
|
92 | - * Return the credentials to use as username and password for the SOCKS proxy,
|
|
93 | - * given a certain domain and userContextId. Optionally, create them.
|
|
178 | + * Get the last circuit used in a certain browser.
|
|
179 | + * The returned data is created when the circuit is first seen, therefore it
|
|
180 | + * could be stale (i.e., the circuit might not be available anymore).
|
|
94 | 181 | *
|
95 | - * @param {string} firstPartyDomain The first party domain associated to the requests
|
|
96 | - * @param {string} userContextId The context ID associated to the request
|
|
97 | - * @param {bool} create Whether to create the nonce, if it is not available
|
|
98 | - * @returns {object|null} Either the credential, or null if we do not have them and create is
|
|
99 | - * false.
|
|
182 | + * @param {MozBrowser} browser The browser to get data for
|
|
183 | + * @param {string} domain The first party domain we want to get the circuit
|
|
184 | + * for
|
|
185 | + * @param {number} userContextId The user context domain we want to get the
|
|
186 | + * circuit for
|
|
187 | + * @returns {NodeData[]} The node data, or an empty array if we do not have
|
|
188 | + * data for the requested key.
|
|
100 | 189 | */
|
101 | - getSocksProxyCredentials(firstPartyDomain, userContextId, create = false) {
|
|
102 | - if (!this.#noncesForDomains.has(firstPartyDomain)) {
|
|
103 | - if (!create) {
|
|
104 | - return null;
|
|
105 | - }
|
|
106 | - const nonce = this.#nonce();
|
|
107 | - logger.info(`New nonce for first party ${firstPartyDomain}: ${nonce}`);
|
|
108 | - this.#noncesForDomains.set(firstPartyDomain, nonce);
|
|
190 | + getCircuit(browser, domain, userContextId) {
|
|
191 | + const username = this.#makeUsername(domain, userContextId);
|
|
192 | + const circuits = this.#browsers.get(browser.browserId)?.get(username);
|
|
193 | + // This is the only place where circuit data can go out, so the only place
|
|
194 | + // where it makes a difference to check whether the pending circuit is still
|
|
195 | + // pending, or it has actually got data.
|
|
196 | + const pending = this.#knownCircuits.get(circuits?.pending);
|
|
197 | + if (pending?.length) {
|
|
198 | + circuits.current = circuits.pending;
|
|
199 | + circuits.pending = null;
|
|
200 | + return pending;
|
|
109 | 201 | }
|
110 | - if (!this.#noncesForUserContextId.has(userContextId)) {
|
|
111 | - if (!create) {
|
|
112 | - return null;
|
|
113 | - }
|
|
114 | - const nonce = this.#nonce();
|
|
115 | - logger.info(`New nonce for userContextId ${userContextId}: ${nonce}`);
|
|
116 | - this.#noncesForUserContextId.set(userContextId, nonce);
|
|
117 | - }
|
|
118 | - return {
|
|
119 | - username: this.#makeUsername(firstPartyDomain, userContextId),
|
|
120 | - password:
|
|
121 | - this.#noncesForDomains.get(firstPartyDomain) +
|
|
122 | - this.#noncesForUserContextId.get(userContextId),
|
|
123 | - };
|
|
202 | + // TODO: At this point we already know if we expect a circuit change for
|
|
203 | + // this key: (circuit?.pending && !pending). However, we do not consume this
|
|
204 | + // data yet in the frontend, so do not send it for now.
|
|
205 | + return this.#knownCircuits.get(circuits?.current) ?? [];
|
|
124 | 206 | }
|
125 | 207 | |
126 | 208 | /**
|
127 | 209 | * Create a new nonce for the FP domain of the selected browser and reload the
|
128 | 210 | * tab with a new circuit.
|
129 | 211 | *
|
130 | - * @param {object} browser Should be the gBrowser from the context of the
|
|
131 | - * caller
|
|
212 | + * @param {object} globalBrowser Should be the gBrowser from the context of
|
|
213 | + * the caller
|
|
132 | 214 | */
|
133 | - newCircuitForBrowser(browser) {
|
|
134 | - const firstPartyDomain = getDomainForBrowser(browser.selectedBrowser);
|
|
215 | + newCircuitForBrowser(globalBrowser) {
|
|
216 | + const browser = globalBrowser.selectedBrowser;
|
|
217 | + const firstPartyDomain = getDomainForBrowser(browser);
|
|
135 | 218 | this.#newCircuitForDomain(firstPartyDomain);
|
136 | - // TODO: How to properly handle the user context? Should we use
|
|
137 | - // (domain, userContextId) pairs, instead of concatenating nonces?
|
|
219 | + const { username, password } = this.#getSocksProxyCredentials(
|
|
220 | + firstPartyDomain,
|
|
221 | + browser.contentPrincipal.originAttributes.userContextId
|
|
222 | + );
|
|
223 | + this.#trackBrowser(browser, username, password);
|
|
138 | 224 | browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
|
139 | 225 | }
|
140 | 226 | |
... | ... | @@ -147,12 +233,15 @@ class TorDomainIsolatorImpl { |
147 | 233 | |
148 | 234 | // Per-domain and per contextId nonces are stored in maps, so simply clear
|
149 | 235 | // them.
|
236 | + // Notice that the catch-all circuit is included in #noncesForDomains, so we
|
|
237 | + // are implicilty cleaning it. Should this change, we should change its
|
|
238 | + // nonce explicitly here.
|
|
150 | 239 | this.#noncesForDomains.clear();
|
151 | 240 | this.#noncesForUserContextId.clear();
|
241 | + this.#catchallDirtySince = Date.now();
|
|
152 | 242 | |
153 | - // Force a rotation on the next catch-all circuit use by setting the
|
|
154 | - // creation time to the epoch.
|
|
155 | - this.#catchallDirtySince = 0;
|
|
243 | + this.#knownCircuits.clear();
|
|
244 | + this.#browsers.clear();
|
|
156 | 245 | }
|
157 | 246 | |
158 | 247 | async observe(subject, topic, data) {
|
... | ... | @@ -173,55 +262,20 @@ class TorDomainIsolatorImpl { |
173 | 262 | logger.error("Could not send the newnym command", e);
|
174 | 263 | // TODO: What UX to use here? See tor-browser#41708
|
175 | 264 | }
|
265 | + } else if (topic === lazy.TorMonitorTopics.StreamSucceeded) {
|
|
266 | + const { username, password, circuit } = subject.wrappedJSObject;
|
|
267 | + this.#updateCircuit(username, password, circuit);
|
|
176 | 268 | }
|
177 | 269 | }
|
178 | 270 | |
179 | 271 | /**
|
180 | - * Setup a filter that for every HTTPChannel, replaces the default SOCKS proxy
|
|
181 | - * with one that authenticates to the SOCKS server (the tor client process)
|
|
182 | - * with a username (the first party domain and userContextId) and a nonce
|
|
183 | - * password.
|
|
184 | - * Tor provides a separate circuit for each username+password combination.
|
|
272 | + * Setup a filter that for every HTTPChannel.
|
|
185 | 273 | */
|
186 | 274 | #setupProxyFilter() {
|
187 | - const filterFunction = (aChannel, aProxy) => {
|
|
188 | - if (!this.#isolationEnabled) {
|
|
189 | - return aProxy;
|
|
190 | - }
|
|
191 | - try {
|
|
192 | - const channel = aChannel.QueryInterface(Ci.nsIChannel);
|
|
193 | - let firstPartyDomain =
|
|
194 | - channel.loadInfo.originAttributes.firstPartyDomain;
|
|
195 | - const userContextId = channel.loadInfo.originAttributes.userContextId;
|
|
196 | - if (firstPartyDomain === "") {
|
|
197 | - firstPartyDomain = CATCHALL_DOMAIN;
|
|
198 | - if (Date.now() - this.#catchallDirtySince > 1000 * 10 * 60) {
|
|
199 | - logger.info(
|
|
200 | - "tor catchall circuit has been dirty for over 10 minutes. Rotating."
|
|
201 | - );
|
|
202 | - this.#newCircuitForDomain(CATCHALL_DOMAIN);
|
|
203 | - this.#catchallDirtySince = Date.now();
|
|
204 | - }
|
|
205 | - }
|
|
206 | - const replacementProxy = this.#applySocksProxyCredentials(
|
|
207 | - aProxy,
|
|
208 | - firstPartyDomain,
|
|
209 | - userContextId
|
|
210 | - );
|
|
211 | - logger.debug(
|
|
212 | - `Requested ${channel.URI.spec} via ${replacementProxy.username}:${replacementProxy.password}`
|
|
213 | - );
|
|
214 | - return replacementProxy;
|
|
215 | - } catch (e) {
|
|
216 | - logger.error("Error while setting a new proxy", e);
|
|
217 | - return null;
|
|
218 | - }
|
|
219 | - };
|
|
220 | - |
|
221 | 275 | lazy.ProtocolProxyService.registerChannelFilter(
|
222 | 276 | {
|
223 | - applyFilter(aChannel, aProxy, aCallback) {
|
|
224 | - aCallback.onProxyFilterResult(filterFunction(aChannel, aProxy));
|
|
277 | + applyFilter: (aChannel, aProxy, aCallback) => {
|
|
278 | + aCallback.onProxyFilterResult(this.#proxyFilter(aChannel, aProxy));
|
|
225 | 279 | },
|
226 | 280 | },
|
227 | 281 | 0
|
... | ... | @@ -229,33 +283,96 @@ class TorDomainIsolatorImpl { |
229 | 283 | }
|
230 | 284 | |
231 | 285 | /**
|
232 | - * Takes a proxyInfo object (originalProxy) and returns a new proxyInfo
|
|
233 | - * object with the same properties, except the username is set to the
|
|
234 | - * the domain and userContextId, and the password is a nonce.
|
|
286 | + * Replaces the default SOCKS proxy with one that authenticates to the SOCKS
|
|
287 | + * server (the tor client process) with a username (the first party domain and
|
|
288 | + * userContextId) and a nonce password.
|
|
289 | + * Tor provides a separate circuit for each username+password combination.
|
|
290 | + *
|
|
291 | + * @param {nsIChannel} aChannel The channel we are setting the proxy for
|
|
292 | + * @param {nsIProxyInfo} aProxy The original proxy
|
|
293 | + * @returns {nsIProxyInfo} The new proxy to use
|
|
235 | 294 | */
|
236 | - #applySocksProxyCredentials(originalProxy, domain, userContextId) {
|
|
237 | - const proxy = originalProxy.QueryInterface(Ci.nsIProxyInfo);
|
|
238 | - const { username, password } = this.getSocksProxyCredentials(
|
|
239 | - domain,
|
|
240 | - userContextId,
|
|
241 | - true
|
|
242 | - );
|
|
243 | - return lazy.ProtocolProxyService.newProxyInfoWithAuth(
|
|
244 | - "socks",
|
|
245 | - proxy.host,
|
|
246 | - proxy.port,
|
|
247 | - username,
|
|
248 | - password,
|
|
249 | - "", // aProxyAuthorizationHeader
|
|
250 | - "", // aConnectionIsolationKey
|
|
251 | - proxy.flags,
|
|
252 | - proxy.failoverTimeout,
|
|
253 | - proxy.failoverProxy
|
|
254 | - );
|
|
295 | + #proxyFilter(aChannel, aProxy) {
|
|
296 | + if (!this.#isolationEnabled) {
|
|
297 | + return aProxy;
|
|
298 | + }
|
|
299 | + try {
|
|
300 | + const channel = aChannel.QueryInterface(Ci.nsIChannel);
|
|
301 | + let firstPartyDomain = channel.loadInfo.originAttributes.firstPartyDomain;
|
|
302 | + const userContextId = channel.loadInfo.originAttributes.userContextId;
|
|
303 | + if (!firstPartyDomain) {
|
|
304 | + firstPartyDomain = CATCHALL_DOMAIN;
|
|
305 | + if (Date.now() - this.#catchallDirtySince > CATCHALL_MAX_LIFETIME) {
|
|
306 | + logger.info(
|
|
307 | + "tor catchall circuit has reached its maximum lifetime. Rotating."
|
|
308 | + );
|
|
309 | + this.#newCircuitForDomain(CATCHALL_DOMAIN);
|
|
310 | + }
|
|
311 | + }
|
|
312 | + const { username, password } = this.#getSocksProxyCredentials(
|
|
313 | + firstPartyDomain,
|
|
314 | + userContextId
|
|
315 | + );
|
|
316 | + const browser = this.#getBrowserForChannel(channel);
|
|
317 | + if (browser) {
|
|
318 | + this.#trackBrowser(browser, username, password);
|
|
319 | + }
|
|
320 | + logger.debug(`Requested ${channel.URI.spec} via ${username}:${password}`);
|
|
321 | + const proxy = aProxy.QueryInterface(Ci.nsIProxyInfo);
|
|
322 | + return lazy.ProtocolProxyService.newProxyInfoWithAuth(
|
|
323 | + "socks",
|
|
324 | + proxy.host,
|
|
325 | + proxy.port,
|
|
326 | + username,
|
|
327 | + password,
|
|
328 | + "", // aProxyAuthorizationHeader
|
|
329 | + "", // aConnectionIsolationKey
|
|
330 | + proxy.flags,
|
|
331 | + proxy.failoverTimeout,
|
|
332 | + proxy.failoverProxy
|
|
333 | + );
|
|
334 | + } catch (e) {
|
|
335 | + logger.error("Error while setting a new proxy", e);
|
|
336 | + return null;
|
|
337 | + }
|
|
338 | + }
|
|
339 | + |
|
340 | + /**
|
|
341 | + * Return the credentials to use as username and password for the SOCKS proxy,
|
|
342 | + * given a certain domain and userContextId.
|
|
343 | + * A new random password will be created if not available yet.
|
|
344 | + *
|
|
345 | + * @param {string} firstPartyDomain The first party domain associated to the
|
|
346 | + * requests
|
|
347 | + * @param {number} userContextId The context ID associated to the request
|
|
348 | + * @returns {object} The credentials
|
|
349 | + */
|
|
350 | + #getSocksProxyCredentials(firstPartyDomain, userContextId) {
|
|
351 | + if (!this.#noncesForDomains.has(firstPartyDomain)) {
|
|
352 | + const nonce = this.#nonce();
|
|
353 | + logger.info(`New nonce for first party ${firstPartyDomain}: ${nonce}`);
|
|
354 | + this.#noncesForDomains.set(firstPartyDomain, nonce);
|
|
355 | + }
|
|
356 | + if (!this.#noncesForUserContextId.has(userContextId)) {
|
|
357 | + const nonce = this.#nonce();
|
|
358 | + logger.info(`New nonce for userContextId ${userContextId}: ${nonce}`);
|
|
359 | + this.#noncesForUserContextId.set(userContextId, nonce);
|
|
360 | + }
|
|
361 | + // TODO: How to properly handle the user-context? Should we use
|
|
362 | + // (domain, userContextId) pairs, instead of concatenating nonces?
|
|
363 | + return {
|
|
364 | + username: this.#makeUsername(firstPartyDomain, userContextId),
|
|
365 | + password:
|
|
366 | + this.#noncesForDomains.get(firstPartyDomain) +
|
|
367 | + this.#noncesForUserContextId.get(userContextId),
|
|
368 | + };
|
|
255 | 369 | }
|
256 | 370 | |
257 | 371 | /**
|
258 | 372 | * Combine the needed data into a username for the proxy.
|
373 | + *
|
|
374 | + * @param {string} domain The first-party domain associated to the request
|
|
375 | + * @param {integer} userContextId The userContextId associated to the request
|
|
259 | 376 | */
|
260 | 377 | #makeUsername(domain, userContextId) {
|
261 | 378 | if (!domain) {
|
... | ... | @@ -264,12 +381,26 @@ class TorDomainIsolatorImpl { |
264 | 381 | return `${domain}:${userContextId}`;
|
265 | 382 | }
|
266 | 383 | |
384 | + /**
|
|
385 | + * Combine SOCKS username and password into a string to use as ID.
|
|
386 | + *
|
|
387 | + * @param {string} username The SOCKS username
|
|
388 | + * @param {string} password The SOCKS password
|
|
389 | + * @returns {CircuitId} A string that combines username and password and can
|
|
390 | + * be used for map lookups.
|
|
391 | + */
|
|
392 | + #credentialsToId(username, password) {
|
|
393 | + return `${username}|${password}`;
|
|
394 | + }
|
|
395 | + |
|
267 | 396 | /**
|
268 | 397 | * Generate a new 128 bit random tag.
|
269 | 398 | *
|
270 | 399 | * Strictly speaking both using a cryptographic entropy source and using 128
|
271 | 400 | * bits of entropy for the tag are likely overkill, as correct behavior only
|
272 | 401 | * depends on how unlikely it is for there to be a collision.
|
402 | + *
|
|
403 | + * @returns {string} The random nonce
|
|
273 | 404 | */
|
274 | 405 | #nonce() {
|
275 | 406 | return Array.from(crypto.getRandomValues(new Uint8Array(16)), byte =>
|
... | ... | @@ -279,12 +410,18 @@ class TorDomainIsolatorImpl { |
279 | 410 | |
280 | 411 | /**
|
281 | 412 | * Re-generate the nonce for a certain domain.
|
413 | + *
|
|
414 | + * @param {string?} domain The first-party domain to re-create the nonce for.
|
|
415 | + * If empty or null, the catchall domain will be used.
|
|
282 | 416 | */
|
283 | 417 | #newCircuitForDomain(domain) {
|
284 | 418 | if (!domain) {
|
285 | 419 | domain = CATCHALL_DOMAIN;
|
286 | 420 | }
|
287 | 421 | this.#noncesForDomains.set(domain, this.#nonce());
|
422 | + if (domain === CATCHALL_DOMAIN) {
|
|
423 | + this.#catchallDirtySince = Date.now();
|
|
424 | + }
|
|
288 | 425 | logger.info(
|
289 | 426 | `New domain isolation for ${domain}: ${this.#noncesForDomains.get(
|
290 | 427 | domain
|
... | ... | @@ -296,6 +433,8 @@ class TorDomainIsolatorImpl { |
296 | 433 | * Re-generate the nonce for a userContextId.
|
297 | 434 | *
|
298 | 435 | * Currently, this function is not hooked to anything.
|
436 | + *
|
|
437 | + * @param {integer} userContextId The userContextId to re-create the nonce for
|
|
299 | 438 | */
|
300 | 439 | #newCircuitForUserContextId(userContextId) {
|
301 | 440 | this.#noncesForUserContextId.set(userContextId, this.#nonce());
|
... | ... | @@ -305,13 +444,182 @@ class TorDomainIsolatorImpl { |
305 | 444 | )}`
|
306 | 445 | );
|
307 | 446 | }
|
447 | + |
|
448 | + /**
|
|
449 | + * Try to extract a browser from a channel.
|
|
450 | + *
|
|
451 | + * @param {nsIChannel} channel The channel to extract the browser from
|
|
452 | + * @returns {MozBrowser?} The browser the channel is associated to
|
|
453 | + */
|
|
454 | + #getBrowserForChannel(channel) {
|
|
455 | + const browsers =
|
|
456 | + channel.loadInfo.browsingContext?.topChromeWindow?.gBrowser.browsers;
|
|
457 | + if (!browsers || !channel.loadInfo.browsingContext?.browserId) {
|
|
458 | + return null;
|
|
459 | + }
|
|
460 | + for (const browser of browsers) {
|
|
461 | + if (browser.browserId === channel.loadInfo.browsingContext.browserId) {
|
|
462 | + logger.debug(
|
|
463 | + "Matched browser with browserId",
|
|
464 | + channel.loadInfo.browsingContext.browserId
|
|
465 | + );
|
|
466 | + return browser;
|
|
467 | + }
|
|
468 | + }
|
|
469 | + // Expected to arrive here for example for the update checker.
|
|
470 | + // If we find a way to check that, we could raise the level to a warn.
|
|
471 | + logger.debug("Browser not matched", channel);
|
|
472 | + return null;
|
|
473 | + }
|
|
474 | + |
|
475 | + /**
|
|
476 | + * Associate the SOCKS credentials to a browser.
|
|
477 | + * If needed (the browser is associated for the first time, or it was already
|
|
478 | + * known but its credential changed), notify the related circuit display.
|
|
479 | + *
|
|
480 | + * @param {MozBrowser} browser The browser to track
|
|
481 | + * @param {string} username The SOCKS username
|
|
482 | + * @param {string} password The SOCKS password
|
|
483 | + */
|
|
484 | + #trackBrowser(browser, username, password) {
|
|
485 | + let browserCircuits = this.#browsers.get(browser.browserId);
|
|
486 | + if (!browserCircuits) {
|
|
487 | + browserCircuits = new Map();
|
|
488 | + this.#browsers.set(browser.browserId, browserCircuits);
|
|
489 | + }
|
|
490 | + const circuitIds = browserCircuits.get(username) ?? {};
|
|
491 | + const id = this.#credentialsToId(username, password);
|
|
492 | + if (circuitIds.current === id) {
|
|
493 | + // The circuit with these credentials was already built (we already knew
|
|
494 | + // its nodes, or we would not have promoted it to the current circuit).
|
|
495 | + // We do not need to do anything else, because we cannot detect a change
|
|
496 | + // of nodes here.
|
|
497 | + return;
|
|
498 | + }
|
|
499 | + |
|
500 | + logger.debug(
|
|
501 | + `Found new credentials ${username} ${password} for browser`,
|
|
502 | + browser
|
|
503 | + );
|
|
504 | + const circuit = this.#knownCircuits.get(id);
|
|
505 | + if (circuit?.length) {
|
|
506 | + circuitIds.current = id;
|
|
507 | + if (circuitIds.pending === id) {
|
|
508 | + circuitIds.pending = null;
|
|
509 | + }
|
|
510 | + browserCircuits.set(username, circuitIds);
|
|
511 | + // FIXME: We only notify the circuit display when we have a change that
|
|
512 | + // involves circuits whose nodes are known, for now. We need to resolve a
|
|
513 | + // few other techical problems (e.g., associate the circuit to the
|
|
514 | + // document?) and develop a UX with some animation to notify the circuit
|
|
515 | + // display more often.
|
|
516 | + // See tor-browser#41700 and tor-browser!699.
|
|
517 | + // In any case, notify the circuit display only after the internal map has
|
|
518 | + // been updated.
|
|
519 | + this.#notifyCircuitDisplay();
|
|
520 | + } else if (circuitIds.pending !== id) {
|
|
521 | + // We do not have node data, so we store that we might need to track this.
|
|
522 | + // Otherwise, when a circuit is ready, we do not know which browser was it
|
|
523 | + // used for.
|
|
524 | + circuitIds.pending = id;
|
|
525 | + browserCircuits.set(username, circuitIds);
|
|
526 | + }
|
|
527 | + }
|
|
528 | + |
|
529 | + /**
|
|
530 | + * Update a circuit, and notify the related circuit displays if it changed.
|
|
531 | + *
|
|
532 | + * This function is called when a certain stream has succeeded and so we can
|
|
533 | + * associate its SOCKS credential to the circuit it is using.
|
|
534 | + * We receive only the fingerprints of the circuit nodes, but they are enough
|
|
535 | + * to check if the circuit has changed. If it has, we also get the nodes'
|
|
536 | + * information through the control port.
|
|
537 | + *
|
|
538 | + * @param {string} username The SOCKS username
|
|
539 | + * @param {string} password The SOCKS password
|
|
540 | + * @param {NodeFingerprint[]} circuit The fingerprints of the nodes that
|
|
541 | + * compose the circuit
|
|
542 | + */
|
|
543 | + async #updateCircuit(username, password, circuit) {
|
|
544 | + const id = this.#credentialsToId(username, password);
|
|
545 | + let data = this.#knownCircuits.get(id) ?? [];
|
|
546 | + // Should we modify the lower layer to send a circuit identifier, instead?
|
|
547 | + if (
|
|
548 | + circuit.length === data.length &&
|
|
549 | + circuit.every((id, index) => id === data[index].fingerprint)
|
|
550 | + ) {
|
|
551 | + return;
|
|
552 | + }
|
|
553 | + |
|
554 | + data = await Promise.all(
|
|
555 | + circuit.map(fingerprint =>
|
|
556 | + lazy.TorProtocolService.getNodeInfo(fingerprint)
|
|
557 | + )
|
|
558 | + );
|
|
559 | + this.#knownCircuits.set(id, data);
|
|
560 | + // We know that something changed, but we cannot know if anyone is
|
|
561 | + // interested in this change. So, we have to notify all the possible
|
|
562 | + // consumers of the data in any case.
|
|
563 | + // Not being specific and let them check if they need to do something allows
|
|
564 | + // us to keep a simpler structure.
|
|
565 | + this.#notifyCircuitDisplay();
|
|
566 | + }
|
|
567 | + |
|
568 | + /**
|
|
569 | + * Broadcast a notification when a circuit changed, or a browser is changing
|
|
570 | + * circuit (which might happen also in case of navigation).
|
|
571 | + */
|
|
572 | + #notifyCircuitDisplay() {
|
|
573 | + Services.obs.notifyObservers(null, TOR_CIRCUIT_TOPIC);
|
|
574 | + }
|
|
575 | + |
|
576 | + /**
|
|
577 | + * Clear the known circuit information, when they are not needed anymore.
|
|
578 | + *
|
|
579 | + * We keep circuit data around for a while. We decouple it from the underlying
|
|
580 | + * tor circuit management in case the user clicks on the circuit display when
|
|
581 | + * circuit has long gone.
|
|
582 | + * However, data accumulate during a session. So, since we store all the
|
|
583 | + * browsers that used a circuit anyway, every now and then we check if we
|
|
584 | + * still know browsers using a certain circuits. If there are not, we forget
|
|
585 | + * about it.
|
|
586 | + *
|
|
587 | + * This function is run by an interval.
|
|
588 | + */
|
|
589 | + #clearKnownCircuits() {
|
|
590 | + logger.info("Running the circuit cleanup");
|
|
591 | + const windows = [];
|
|
592 | + const enumerator = Services.wm.getEnumerator("navigator:browser");
|
|
593 | + while (enumerator.hasMoreElements()) {
|
|
594 | + windows.push(enumerator.getNext());
|
|
595 | + }
|
|
596 | + const browsers = windows
|
|
597 | + .flatMap(win => win.gBrowser.browsers.map(b => b.browserId))
|
|
598 | + .filter(id => this.#browsers.has(id));
|
|
599 | + this.#browsers = new Map(browsers.map(id => [id, this.#browsers.get(id)]));
|
|
600 | + this.#knownCircuits = new Map(
|
|
601 | + Array.from(this.#browsers.values(), circuits =>
|
|
602 | + Array.from(circuits.values(), ids => {
|
|
603 | + const r = [];
|
|
604 | + const current = this.#knownCircuits.get(ids.current);
|
|
605 | + if (current) {
|
|
606 | + r.push([ids.current, current]);
|
|
607 | + }
|
|
608 | + const pending = this.#knownCircuits.get(ids.pending);
|
|
609 | + if (pending) {
|
|
610 | + r.push([ids.pending, pending]);
|
|
611 | + }
|
|
612 | + return r;
|
|
613 | + })
|
|
614 | + ).flat(2)
|
|
615 | + );
|
|
616 | + }
|
|
308 | 617 | }
|
309 | 618 | |
310 | 619 | /**
|
311 | 620 | * Get the first party domain for a certain browser.
|
312 | 621 | *
|
313 | - * @param browser The browser to get the FP-domain for.
|
|
314 | - *
|
|
622 | + * @param {MozBrowser} browser The browser to get the FP-domain for.
|
|
315 | 623 | * Please notice that it should be gBrowser.selectedBrowser, because
|
316 | 624 | * browser.documentURI is the actual shown page, and might be an error page.
|
317 | 625 | * In this case, we rely on currentURI, which for gBrowser is an alias of
|
... | ... | @@ -358,6 +666,6 @@ function getDomainForBrowser(browser) { |
358 | 666 | return fpd;
|
359 | 667 | }
|
360 | 668 | |
361 | -const TorDomainIsolator = new TorDomainIsolatorImpl();
|
|
669 | +export const TorDomainIsolator = new TorDomainIsolatorImpl();
|
|
362 | 670 | // Reduce global vars pollution
|
363 | 671 | TorDomainIsolator.getDomainForBrowser = getDomainForBrowser; |
... | ... | @@ -19,6 +19,10 @@ ChromeUtils.defineModuleGetter( |
19 | 19 | "resource://torbutton/modules/tor-control-port.js"
|
20 | 20 | );
|
21 | 21 | |
22 | +ChromeUtils.defineESModuleGetters(lazy, {
|
|
23 | + TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
|
|
24 | +});
|
|
25 | + |
|
22 | 26 | const logger = new ConsoleAPI({
|
23 | 27 | maxLogLevel: "warn",
|
24 | 28 | maxLogLevelPref: "browser.tor_monitor_service.log_level",
|
... | ... | @@ -37,12 +41,34 @@ const TorTopics = Object.freeze({ |
37 | 41 | ProcessRestarted: "TorProcessRestarted",
|
38 | 42 | });
|
39 | 43 | |
44 | +export const TorMonitorTopics = Object.freeze({
|
|
45 | + BridgeChanged: "TorBridgeChanged",
|
|
46 | + StreamSucceeded: "TorStreamSucceeded",
|
|
47 | +});
|
|
48 | + |
|
40 | 49 | const ControlConnTimings = Object.freeze({
|
41 | 50 | initialDelayMS: 25, // Wait 25ms after the process has started, before trying to connect
|
42 | 51 | maxRetryMS: 10000, // Retry at most every 10 seconds
|
43 | 52 | timeoutMS: 5 * 60 * 1000, // Wait at most 5 minutes for tor to start
|
44 | 53 | });
|
45 | 54 | |
55 | +/**
|
|
56 | + * From control-spec.txt:
|
|
57 | + * CircuitID = 1*16 IDChar
|
|
58 | + * IDChar = ALPHA / DIGIT
|
|
59 | + * Currently, Tor only uses digits, but this may change.
|
|
60 | + *
|
|
61 | + * @typedef {string} CircuitID
|
|
62 | + */
|
|
63 | +/**
|
|
64 | + * The fingerprint of a node.
|
|
65 | + * From control-spec.txt:
|
|
66 | + * Fingerprint = "$" 40*HEXDIG
|
|
67 | + * However, we do not keep the $ in our structures.
|
|
68 | + *
|
|
69 | + * @typedef {string} NodeFingerprint
|
|
70 | + */
|
|
71 | + |
|
46 | 72 | /**
|
47 | 73 | * This service monitors an existing Tor instance, or starts one, if needed, and
|
48 | 74 | * then starts monitoring it.
|
... | ... | @@ -52,7 +78,7 @@ const ControlConnTimings = Object.freeze({ |
52 | 78 | */
|
53 | 79 | export const TorMonitorService = {
|
54 | 80 | _connection: null,
|
55 | - _eventsToMonitor: Object.freeze(["STATUS_CLIENT", "NOTICE", "WARN", "ERR"]),
|
|
81 | + _eventHandlers: {},
|
|
56 | 82 | _torLog: [], // Array of objects with date, type, and msg properties.
|
57 | 83 | _startTimeout: null,
|
58 | 84 | |
... | ... | @@ -64,6 +90,28 @@ export const TorMonitorService = { |
64 | 90 | |
65 | 91 | _inited: false,
|
66 | 92 | |
93 | + /**
|
|
94 | + * Stores the nodes of a circuit. Keys are cicuit IDs, and values are the node
|
|
95 | + * fingerprints.
|
|
96 | + *
|
|
97 | + * Theoretically, we could hook this map up to the new identity notification,
|
|
98 | + * but in practice it does not work. Tor pre-builds circuits, and the NEWNYM
|
|
99 | + * signal does not affect them. So, we might end up using a circuit that was
|
|
100 | + * built before the new identity but not yet used. If we cleaned the map, we
|
|
101 | + * risked of not having the data about it.
|
|
102 | + *
|
|
103 | + * @type {Map<CircuitID, NodeFingerprint[]>}
|
|
104 | + */
|
|
105 | + _circuits: new Map(),
|
|
106 | + /**
|
|
107 | + * The last used bridge, or null if bridges are not in use or if it was not
|
|
108 | + * possible to detect the bridge. This needs the user to have specified bridge
|
|
109 | + * lines with fingerprints to work.
|
|
110 | + *
|
|
111 | + * @type {NodeFingerprint?}
|
|
112 | + */
|
|
113 | + _currentBridge: null,
|
|
114 | + |
|
67 | 115 | // Public methods
|
68 | 116 | |
69 | 117 | // Starts Tor, if needed, and starts monitoring for events
|
... | ... | @@ -72,14 +120,28 @@ export const TorMonitorService = { |
72 | 120 | return;
|
73 | 121 | }
|
74 | 122 | this._inited = true;
|
123 | + |
|
124 | + // We always liten to these events, because they are needed for the circuit
|
|
125 | + // display.
|
|
126 | + this._eventHandlers = new Map([
|
|
127 | + ["CIRC", this._processCircEvent.bind(this)],
|
|
128 | + ["STREAM", this._processStreamEvent.bind(this)],
|
|
129 | + ]);
|
|
130 | + |
|
75 | 131 | if (this.ownsTorDaemon) {
|
132 | + // When we own the tor daemon, we listen to more events, that are used
|
|
133 | + // for about:torconnect or for showing the logs in the settings page.
|
|
134 | + this._eventHandlers.set("STATUS_CLIENT", (_eventType, lines) =>
|
|
135 | + this._processBootstrapStatus(lines[0], false)
|
|
136 | + );
|
|
137 | + this._eventHandlers.set("NOTICE", this._processLog.bind(this));
|
|
138 | + this._eventHandlers.set("WARN", this._processLog.bind(this));
|
|
139 | + this._eventHandlers.set("ERR", this._processLog.bind(this));
|
|
76 | 140 | this._controlTor();
|
77 | 141 | } else {
|
78 | - logger.info(
|
|
79 | - "Not starting the event monitor, as we do not own the Tor daemon."
|
|
80 | - );
|
|
142 | + this._startEventMonitor();
|
|
81 | 143 | }
|
82 | - logger.debug("TorMonitorService initialized");
|
|
144 | + logger.info("TorMonitorService initialized");
|
|
83 | 145 | },
|
84 | 146 | |
85 | 147 | // Closes the connection that monitors for events.
|
... | ... | @@ -153,6 +215,18 @@ export const TorMonitorService = { |
153 | 215 | return !!this._connection;
|
154 | 216 | },
|
155 | 217 | |
218 | + /**
|
|
219 | + * Return the data about the current bridge, if any, or null.
|
|
220 | + * We can detect bridge only when the configured bridge lines include the
|
|
221 | + * fingerprints.
|
|
222 | + *
|
|
223 | + * @returns {NodeData?} The node information, or null if the first node
|
|
224 | + * is not a bridge, or no circuit has been opened, yet.
|
|
225 | + */
|
|
226 | + get currentBridge() {
|
|
227 | + return this._currentBridge;
|
|
228 | + },
|
|
229 | + |
|
156 | 230 | // Private methods
|
157 | 231 | |
158 | 232 | async _startProcess() {
|
... | ... | @@ -272,7 +346,7 @@ export const TorMonitorService = { |
272 | 346 | |
273 | 347 | // TODO: optionally monitor INFO and DEBUG log messages.
|
274 | 348 | let reply = await conn.sendCommand(
|
275 | - "SETEVENTS " + this._eventsToMonitor.join(" ")
|
|
349 | + "SETEVENTS " + Array.from(this._eventHandlers.keys()).join(" ")
|
|
276 | 350 | );
|
277 | 351 | reply = TorParsers.parseCommandResponse(reply);
|
278 | 352 | if (!TorParsers.commandSucceeded(reply)) {
|
... | ... | @@ -281,14 +355,10 @@ export const TorMonitorService = { |
281 | 355 | return false;
|
282 | 356 | }
|
283 | 357 | |
284 | - // FIXME: At the moment it is not possible to start the event monitor
|
|
285 | - // when we do start the tor process. So, does it make sense to keep this
|
|
286 | - // control?
|
|
287 | 358 | if (this._torProcess) {
|
288 | 359 | this._torProcess.connectionWorked();
|
289 | 360 | }
|
290 | - |
|
291 | - if (!TorLauncherUtil.shouldOnlyConfigureTor) {
|
|
361 | + if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) {
|
|
292 | 362 | try {
|
293 | 363 | await this._takeTorOwnership(conn);
|
294 | 364 | } catch (e) {
|
... | ... | @@ -297,7 +367,31 @@ export const TorMonitorService = { |
297 | 367 | }
|
298 | 368 | |
299 | 369 | this._connection = conn;
|
300 | - this._waitForEventData();
|
|
370 | + |
|
371 | + for (const [type, callback] of this._eventHandlers.entries()) {
|
|
372 | + this._monitorEvent(type, callback);
|
|
373 | + }
|
|
374 | + |
|
375 | + // Populate the circuit map already, in case we are connecting to an
|
|
376 | + // external tor daemon.
|
|
377 | + try {
|
|
378 | + const reply = await this._connection.sendCommand(
|
|
379 | + "GETINFO circuit-status"
|
|
380 | + );
|
|
381 | + const lines = reply.split(/\r?\n/);
|
|
382 | + if (lines.shift() === "250+circuit-status=") {
|
|
383 | + for (const line of lines) {
|
|
384 | + if (line === ".") {
|
|
385 | + break;
|
|
386 | + }
|
|
387 | + // _processCircEvent processes only one line at a time
|
|
388 | + this._processCircEvent("CIRC", [line]);
|
|
389 | + }
|
|
390 | + }
|
|
391 | + } catch (e) {
|
|
392 | + logger.warn("Could not populate the initial circuit map", e);
|
|
393 | + }
|
|
394 | + |
|
301 | 395 | return true;
|
302 | 396 | },
|
303 | 397 | |
... | ... | @@ -318,65 +412,49 @@ export const TorMonitorService = { |
318 | 412 | }
|
319 | 413 | },
|
320 | 414 | |
321 | - _waitForEventData() {
|
|
322 | - if (!this._connection) {
|
|
323 | - return;
|
|
324 | - }
|
|
325 | - logger.debug("Start watching events:", this._eventsToMonitor);
|
|
415 | + _monitorEvent(type, callback) {
|
|
416 | + logger.info(`Watching events of type ${type}.`);
|
|
326 | 417 | let replyObj = {};
|
327 | - for (const torEvent of this._eventsToMonitor) {
|
|
328 | - this._connection.watchEvent(
|
|
329 | - torEvent,
|
|
330 | - null,
|
|
331 | - line => {
|
|
332 | - if (!line) {
|
|
333 | - return;
|
|
334 | - }
|
|
335 | - logger.debug("Event response: ", line);
|
|
336 | - const isComplete = TorParsers.parseReplyLine(line, replyObj);
|
|
337 | - if (isComplete) {
|
|
338 | - this._processEventReply(replyObj);
|
|
339 | - replyObj = {};
|
|
340 | - }
|
|
341 | - },
|
|
342 | - true
|
|
343 | - );
|
|
344 | - }
|
|
418 | + this._connection.watchEvent(
|
|
419 | + type,
|
|
420 | + null,
|
|
421 | + line => {
|
|
422 | + if (!line) {
|
|
423 | + return;
|
|
424 | + }
|
|
425 | + logger.debug("Event response: ", line);
|
|
426 | + const isComplete = TorParsers.parseReplyLine(line, replyObj);
|
|
427 | + if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) {
|
|
428 | + return;
|
|
429 | + }
|
|
430 | + const reply = replyObj;
|
|
431 | + replyObj = {};
|
|
432 | + if (reply.statusCode !== TorStatuses.EventNotification) {
|
|
433 | + logger.error("Unexpected event status code:", reply.statusCode);
|
|
434 | + return;
|
|
435 | + }
|
|
436 | + if (!reply.lineArray[0].startsWith(`${type} `)) {
|
|
437 | + logger.error("Wrong format for the first line:", reply.lineArray[0]);
|
|
438 | + return;
|
|
439 | + }
|
|
440 | + reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1);
|
|
441 | + try {
|
|
442 | + callback(type, reply.lineArray);
|
|
443 | + } catch (e) {
|
|
444 | + logger.error("Exception while handling an event", reply, e);
|
|
445 | + }
|
|
446 | + },
|
|
447 | + true
|
|
448 | + );
|
|
345 | 449 | },
|
346 | 450 | |
347 | - _processEventReply(aReply) {
|
|
348 | - if (aReply._parseError || !aReply.lineArray.length) {
|
|
349 | - return;
|
|
350 | - }
|
|
351 | - |
|
352 | - if (aReply.statusCode !== TorStatuses.EventNotification) {
|
|
353 | - logger.warn("Unexpected event status code:", aReply.statusCode);
|
|
354 | - return;
|
|
355 | - }
|
|
356 | - |
|
357 | - // TODO: do we need to handle multiple lines?
|
|
358 | - const s = aReply.lineArray[0];
|
|
359 | - const idx = s.indexOf(" ");
|
|
360 | - if (idx === -1) {
|
|
361 | - return;
|
|
362 | - }
|
|
363 | - const eventType = s.substring(0, idx);
|
|
364 | - const msg = s.substring(idx + 1).trim();
|
|
365 | - |
|
366 | - if (eventType === "STATUS_CLIENT") {
|
|
367 | - this._processBootstrapStatus(msg, false);
|
|
368 | - return;
|
|
369 | - } else if (!this._eventsToMonitor.includes(eventType)) {
|
|
370 | - logger.debug(`Dropping unlistened event ${eventType}`);
|
|
371 | - return;
|
|
372 | - }
|
|
373 | - |
|
374 | - if (eventType === "WARN" || eventType === "ERR") {
|
|
451 | + _processLog(type, lines) {
|
|
452 | + if (type === "WARN" || type === "ERR") {
|
|
375 | 453 | // Notify so that Copy Log can be enabled.
|
376 | 454 | Services.obs.notifyObservers(null, TorTopics.HasWarnOrErr);
|
377 | 455 | }
|
378 | 456 | |
379 | - const now = new Date();
|
|
457 | + const date = new Date();
|
|
380 | 458 | const maxEntries = Services.prefs.getIntPref(
|
381 | 459 | "extensions.torlauncher.max_tor_log_entries",
|
382 | 460 | 1000
|
... | ... | @@ -384,8 +462,10 @@ export const TorMonitorService = { |
384 | 462 | if (maxEntries > 0 && this._torLog.length >= maxEntries) {
|
385 | 463 | this._torLog.splice(0, 1);
|
386 | 464 | }
|
387 | - this._torLog.push({ date: now, type: eventType, msg });
|
|
388 | - const logString = `Tor ${eventType}: ${msg}`;
|
|
465 | + |
|
466 | + const msg = lines.join("\n");
|
|
467 | + this._torLog.push({ date, type, msg });
|
|
468 | + const logString = `Tor ${type}: ${msg}`;
|
|
389 | 469 | logger.info(logString);
|
390 | 470 | },
|
391 | 471 | |
... | ... | @@ -461,8 +541,108 @@ export const TorMonitorService = { |
461 | 541 | }
|
462 | 542 | },
|
463 | 543 | |
544 | + async _processCircEvent(_type, lines) {
|
|
545 | + const builtEvent =
|
|
546 | + /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
|
|
547 | + lines[0]
|
|
548 | + );
|
|
549 | + const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]);
|
|
550 | + if (builtEvent) {
|
|
551 | + const fp = /\$([0-9a-fA-F]{40})/g;
|
|
552 | + const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
|
|
553 | + g[1].toUpperCase()
|
|
554 | + );
|
|
555 | + this._circuits.set(builtEvent.groups.CircuitID, nodes);
|
|
556 | + // Ignore circuits of length 1, that are used, for example, to probe
|
|
557 | + // bridges. So, only store them, since we might see streams that use them,
|
|
558 | + // but then early-return.
|
|
559 | + if (nodes.length === 1) {
|
|
560 | + return;
|
|
561 | + }
|
|
562 | + // In some cases, we might already receive SOCKS credentials in the line.
|
|
563 | + // However, this might be a problem with onion services: we get also a
|
|
564 | + // 4-hop circuit that we likely do not want to show to the user,
|
|
565 | + // especially because it is used only temporarily, and it would need a
|
|
566 | + // technical explaination.
|
|
567 | + // this._checkCredentials(lines[0], nodes);
|
|
568 | + if (this._currentBridge?.fingerprint !== nodes[0]) {
|
|
569 | + const nodeInfo = await lazy.TorProtocolService.getNodeInfo(nodes[0]);
|
|
570 | + let notify = false;
|
|
571 | + if (nodeInfo?.bridgeType) {
|
|
572 | + logger.info(`Bridge changed to ${nodes[0]}`);
|
|
573 | + this._currentBridge = nodeInfo;
|
|
574 | + notify = true;
|
|
575 | + } else if (this._currentBridge) {
|
|
576 | + logger.info("Bridges disabled");
|
|
577 | + this._currentBridge = null;
|
|
578 | + notify = true;
|
|
579 | + }
|
|
580 | + if (notify) {
|
|
581 | + Services.obs.notifyObservers(
|
|
582 | + null,
|
|
583 | + TorMonitorTopics.BridgeChanged,
|
|
584 | + this._currentBridge
|
|
585 | + );
|
|
586 | + }
|
|
587 | + }
|
|
588 | + } else if (closedEvent) {
|
|
589 | + this._circuits.delete(closedEvent.groups.ID);
|
|
590 | + }
|
|
591 | + },
|
|
592 | + |
|
593 | + _processStreamEvent(_type, lines) {
|
|
594 | + // The first block is the stream ID, which we do not need at the moment.
|
|
595 | + const succeeedEvent =
|
|
596 | + /^[a-zA-Z0-9]{1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec(
|
|
597 | + lines[0]
|
|
598 | + );
|
|
599 | + if (!succeeedEvent) {
|
|
600 | + return;
|
|
601 | + }
|
|
602 | + const circuit = this._circuits.get(succeeedEvent.groups.CircuitID);
|
|
603 | + if (!circuit) {
|
|
604 | + logger.error(
|
|
605 | + "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers.",
|
|
606 | + lines[0]
|
|
607 | + );
|
|
608 | + return;
|
|
609 | + }
|
|
610 | + this._checkCredentials(lines[0], circuit);
|
|
611 | + },
|
|
612 | + |
|
613 | + /**
|
|
614 | + * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and
|
|
615 | + * SOCKS_PASSWORD. In case, notify observers that we could associate a certain
|
|
616 | + * circuit to these credentials.
|
|
617 | + *
|
|
618 | + * @param {string} line The circ or stream line to check
|
|
619 | + * @param {NodeFingerprint[]} circuit The fingerprints of the nodes in the
|
|
620 | + * circuit.
|
|
621 | + */
|
|
622 | + _checkCredentials(line, circuit) {
|
|
623 | + const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line);
|
|
624 | + const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line);
|
|
625 | + if (!username || !password) {
|
|
626 | + return;
|
|
627 | + }
|
|
628 | + Services.obs.notifyObservers(
|
|
629 | + {
|
|
630 | + wrappedJSObject: {
|
|
631 | + username: TorParsers.unescapeString(username[1]),
|
|
632 | + password: TorParsers.unescapeString(password[1]),
|
|
633 | + circuit,
|
|
634 | + },
|
|
635 | + },
|
|
636 | + TorMonitorTopics.StreamSucceeded
|
|
637 | + );
|
|
638 | + },
|
|
639 | + |
|
464 | 640 | _shutDownEventMonitor() {
|
465 | - this._connection?.close();
|
|
641 | + try {
|
|
642 | + this._connection?.close();
|
|
643 | + } catch (e) {
|
|
644 | + logger.error("Could not close the connection to the control port", e);
|
|
645 | + }
|
|
466 | 646 | this._connection = null;
|
467 | 647 | if (this._startTimeout !== null) {
|
468 | 648 | clearTimeout(this._startTimeout);
|
... | ... | @@ -181,12 +181,12 @@ export const TorParsers = Object.freeze({ |
181 | 181 | return aStr;
|
182 | 182 | }
|
183 | 183 | const escaped = aStr
|
184 | - .replace("\\", "\\\\")
|
|
185 | - .replace('"', '\\"')
|
|
186 | - .replace("\n", "\\n")
|
|
187 | - .replace("\r", "\\r")
|
|
188 | - .replace("\t", "\\t")
|
|
189 | - .replace(/[^\x20-\x7e]+/g, text => {
|
|
184 | + .replaceAll("\\", "\\\\")
|
|
185 | + .replaceAll('"', '\\"')
|
|
186 | + .replaceAll("\n", "\\n")
|
|
187 | + .replaceAll("\r", "\\r")
|
|
188 | + .replaceAll("\t", "\\t")
|
|
189 | + .replaceAll(/[^\x20-\x7e]+/g, text => {
|
|
190 | 190 | const encoder = new TextEncoder();
|
191 | 191 | return Array.from(
|
192 | 192 | encoder.encode(text),
|
... | ... | @@ -40,6 +40,20 @@ const logger = new ConsoleAPI({ |
40 | 40 | prefix: "TorProtocolService",
|
41 | 41 | });
|
42 | 42 | |
43 | +/**
|
|
44 | + * Stores the data associated with a circuit node.
|
|
45 | + *
|
|
46 | + * @typedef NodeData
|
|
47 | + * @property {string} fingerprint The node fingerprint.
|
|
48 | + * @property {string[]} ipAddrs - The ip addresses associated with this node.
|
|
49 | + * @property {string?} bridgeType - The bridge type for this node, or "" if the
|
|
50 | + * node is a bridge but the type is unknown, or null if this is not a bridge
|
|
51 | + * node.
|
|
52 | + * @property {string?} regionCode - An upper case 2-letter ISO3166-1 code for
|
|
53 | + * the first ip address, or null if there is no region. This should also be a
|
|
54 | + * valid BCP47 Region subtag.
|
|
55 | + */
|
|
56 | + |
|
43 | 57 | // Manage the connection to tor's control port, to update its settings and query
|
44 | 58 | // other useful information.
|
45 | 59 | //
|
... | ... | @@ -188,6 +202,89 @@ export const TorProtocolService = { |
188 | 202 | return TorParsers.parseReply(cmd, keyword, response);
|
189 | 203 | },
|
190 | 204 | |
205 | + async getBridges() {
|
|
206 | + // Ideally, we would not need this function, because we should be the one
|
|
207 | + // setting them with TorSettings. However, TorSettings is not notified of
|
|
208 | + // change of settings. So, asking tor directly with the control connection
|
|
209 | + // is the most reliable way of getting the configured bridges, at the
|
|
210 | + // moment. Also, we are using this for the circuit display, which should
|
|
211 | + // work also when we are not configuring the tor daemon, but just using it.
|
|
212 | + return this._withConnection(conn => {
|
|
213 | + return conn.getConf("bridge");
|
|
214 | + });
|
|
215 | + },
|
|
216 | + |
|
217 | + /**
|
|
218 | + * Returns tha data about a relay or a bridge.
|
|
219 | + *
|
|
220 | + * @param {string} id The fingerprint of the node to get data about
|
|
221 | + * @returns {NodeData}
|
|
222 | + */
|
|
223 | + async getNodeInfo(id) {
|
|
224 | + return this._withConnection(async conn => {
|
|
225 | + const node = {
|
|
226 | + fingerprint: id,
|
|
227 | + ipAddrs: [],
|
|
228 | + bridgeType: null,
|
|
229 | + regionCode: null,
|
|
230 | + };
|
|
231 | + const bridge = (await conn.getConf("bridge"))?.find(
|
|
232 | + foundBridge => foundBridge.ID?.toUpperCase() === id.toUpperCase()
|
|
233 | + );
|
|
234 | + const addrRe = /^\[?([^\]]+)\]?:\d+$/;
|
|
235 | + if (bridge) {
|
|
236 | + node.bridgeType = bridge.type ?? "";
|
|
237 | + // Attempt to get an IP address from bridge address string.
|
|
238 | + const ip = bridge.address.match(addrRe)?.[1];
|
|
239 | + if (ip && !ip.startsWith("0.")) {
|
|
240 | + node.ipAddrs.push(ip);
|
|
241 | + }
|
|
242 | + } else {
|
|
243 | + // Either dealing with a relay, or a bridge whose fingerprint is not
|
|
244 | + // saved in torrc.
|
|
245 | + const info = await conn.getInfo(`ns/id/${id}`);
|
|
246 | + if (info.IP && !info.IP.startsWith("0.")) {
|
|
247 | + node.ipAddrs.push(info.IP);
|
|
248 | + }
|
|
249 | + const ip6 = info.IPv6?.match(addrRe)?.[1];
|
|
250 | + if (ip6) {
|
|
251 | + node.ipAddrs.push(ip6);
|
|
252 | + }
|
|
253 | + }
|
|
254 | + if (node.ipAddrs.length) {
|
|
255 | + // Get the country code for the node's IP address.
|
|
256 | + let regionCode;
|
|
257 | + try {
|
|
258 | + // Expect a 2-letter ISO3166-1 code, which should also be a valid
|
|
259 | + // BCP47 Region subtag.
|
|
260 | + regionCode = await conn.getInfo("ip-to-country/" + node.ipAddrs[0]);
|
|
261 | + } catch {}
|
|
262 | + if (regionCode && regionCode !== "??") {
|
|
263 | + node.regionCode = regionCode.toUpperCase();
|
|
264 | + }
|
|
265 | + }
|
|
266 | + return node;
|
|
267 | + });
|
|
268 | + },
|
|
269 | + |
|
270 | + async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) {
|
|
271 | + return this._withConnection(conn => {
|
|
272 | + return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent);
|
|
273 | + });
|
|
274 | + },
|
|
275 | + |
|
276 | + async onionAuthRemove(hsAddress) {
|
|
277 | + return this._withConnection(conn => {
|
|
278 | + return conn.onionAuthRemove(hsAddress);
|
|
279 | + });
|
|
280 | + },
|
|
281 | + |
|
282 | + async onionAuthViewKeys() {
|
|
283 | + return this._withConnection(conn => {
|
|
284 | + return conn.onionAuthViewKeys();
|
|
285 | + });
|
|
286 | + },
|
|
287 | + |
|
191 | 288 | // TODO: transform the following 4 functions in getters. At the moment they
|
192 | 289 | // are also used in torbutton.
|
193 | 290 | |
... | ... | @@ -630,6 +727,16 @@ export const TorProtocolService = { |
630 | 727 | }
|
631 | 728 | },
|
632 | 729 | |
730 | + async _withConnection(func) {
|
|
731 | + // TODO: Make more robust?
|
|
732 | + const conn = await this._getConnection();
|
|
733 | + try {
|
|
734 | + return await func(conn);
|
|
735 | + } finally {
|
|
736 | + this._returnConnection();
|
|
737 | + }
|
|
738 | + },
|
|
739 | + |
|
633 | 740 | // If aConn is omitted, the cached connection is closed.
|
634 | 741 | _closeConnection() {
|
635 | 742 | if (this._controlConnection) {
|
... | ... | @@ -3,6 +3,7 @@ const lazy = {}; |
3 | 3 | // We will use the modules only when the profile is loaded, so prefer lazy
|
4 | 4 | // loading
|
5 | 5 | ChromeUtils.defineESModuleGetters(lazy, {
|
6 | + TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
|
|
6 | 7 | TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
|
7 | 8 | TorMonitorService: "resource://gre/modules/TorMonitorService.sys.mjs",
|
8 | 9 | TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
|
... | ... | @@ -19,12 +20,6 @@ ChromeUtils.defineModuleGetter( |
19 | 20 | "resource:///modules/TorSettings.jsm"
|
20 | 21 | );
|
21 | 22 | |
22 | -ChromeUtils.defineModuleGetter(
|
|
23 | - lazy,
|
|
24 | - "TorDomainIsolator",
|
|
25 | - "resource://gre/modules/TorDomainIsolator.jsm"
|
|
26 | -);
|
|
27 | - |
|
28 | 23 | /* Browser observer topis */
|
29 | 24 | const BrowserTopics = Object.freeze({
|
30 | 25 | ProfileAfterChange: "profile-after-change",
|
1 | 1 | EXTRA_JS_MODULES += [
|
2 | 2 | "TorBootstrapRequest.sys.mjs",
|
3 | - "TorDomainIsolator.jsm",
|
|
3 | + "TorDomainIsolator.sys.mjs",
|
|
4 | 4 | "TorLauncherUtil.sys.mjs",
|
5 | 5 | "TorMonitorService.sys.mjs",
|
6 | 6 | "TorParsers.sys.mjs",
|