Commits:
-
66229717
by Pier Angelo Vendrame at 2023-07-27T18:11:55+02:00
fixup! Bug 40933: Add tor-launcher functionality
Bug 41844: Added a couple of wrappers for Onion Auth on
TorProtocolService.
-
2cde9fc3
by Pier Angelo Vendrame at 2023-07-27T18:11:56+02:00
fixup! Bug 30237: Add v3 onion services client authentication prompt
Bug 41844: Stop using the control port directly
-
b2cd0ee8
by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00
fixup! Bug 40933: Add tor-launcher functionality
Small improvements on event registration.
-
26152fa9
by Pier Angelo Vendrame at 2023-07-27T18:11:57+02:00
fixup! Bug 40933: Add tor-launcher functionality
Bug 41844: Do not use a the control port directly.
Collect the bridge node for the about:preferences#connection page in
TorMonitorService.
Also, move parts of the circuit display to TorMonitorService and
TorProtocolService.
-
749aeaca
by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00
fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 41844: Do not use the control port directly
Do not use the controller in the settings frontend.
Instead, let TorMonitorService collect the first node's fingerprint.
-
20641450
by Pier Angelo Vendrame at 2023-07-27T18:11:58+02:00
fixup! Bug 3455: Add DomainIsolator, for isolating circuit by domain.
Bug 41844: Do not use the control port directly.
Use TorDomainIsolator also as a backend for the circuit display.
-
1a8be7b1
by Pier Angelo Vendrame at 2023-07-27T18:11:59+02:00
fixup! Bug 41600: Add a tor circuit display panel.
Bug 41844: Have a separate backend for the tor circuits
Remove the backend stuff from the circuit display.
11 changed files:
Changes:
browser/base/content/browser.js
... |
... |
@@ -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",
|
browser/components/onionservices/content/authPrompt.js
... |
... |
@@ -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(
|
browser/components/onionservices/content/savedKeysDialog.js
... |
... |
@@ -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);
|
browser/components/torcircuit/content/torCircuitPanel.js
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.
|
browser/components/torpreferences/content/connectionPane.js
... |
... |
@@ -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();
|
toolkit/components/tor-launcher/TorDomainIsolator.jsm
→
toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
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; |
toolkit/components/tor-launcher/TorMonitorService.sys.mjs
... |
... |
@@ -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);
|
toolkit/components/tor-launcher/TorParsers.sys.mjs
... |
... |
@@ -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),
|
toolkit/components/tor-launcher/TorProtocolService.sys.mjs
... |
... |
@@ -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) {
|
toolkit/components/tor-launcher/TorStartupService.sys.mjs
... |
... |
@@ -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",
|
toolkit/components/tor-launcher/moz.build
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",
|
|