1 // A script that automatically displays the Tor Circuit used for the
2 // current domain for the currently selected tab.
4 // This file is written in call stack order (later functions
5 // call earlier functions). The file can be processed
6 // with docco.js to produce pretty documentation.
8 // This script is to be embedded in torbutton.xhtml. It defines a single global
9 // function, createTorCircuitDisplay(), which activates the automatic Tor
10 // circuit display for the current tab and any future tabs.
12 // See https://trac.torproject.org/8641
14 /* jshint esnext: true */
15 /* global document, gBrowser, Components */
18 // __createTorCircuitDisplay(enablePrefName)__.
19 // The single function that prepares tor circuit display. Connects to a tor
20 // control port using information provided to the control port module via
21 // a previous call to configureControlPortModule(), and binds to a named
22 // bool pref whose value determines whether the circuit display is enabled
24 let createTorCircuitDisplay
= (function () {
29 const { Services
} = ChromeUtils
.import("resource://gre/modules/Services.jsm");
31 // Import the controller code.
32 let { wait_for_controller
} = ChromeUtils
.import("resource://torbutton/modules/tor-control-port.js", {});
35 let { bindPrefAndInit
, observe
, getLocale
, getDomainForBrowser
, torbutton_get_property_string
} = ChromeUtils
.import("resource://torbutton/modules/utils.js", {});
37 // Make the TorButton logger available.
38 let logger
= Cc
["@torproject.org/torbutton-logger;1"]
39 .getService(Ci
.nsISupports
).wrappedJSObject
;
41 // ## Circuit/stream credentials and node monitoring
43 // A mutable map that stores the current nodes for each
44 // SOCKS username/password pair.
45 let credentialsToNodeDataMap
= new Map(),
46 // A mutable map that reports `true` for IDs of "mature" circuits
47 // (those that have conveyed a stream).
48 knownCircuitIDs
= new Map(),
49 // A mutable map that records the SOCKS credentials for the
50 // latest channels for each browser + domain.
51 browserToCredentialsMap
= new Map();
54 // Removes quotation marks around a quoted string.
55 let trimQuotes
= s
=> s
? s
.match(/^"(.*)"$/)[1] : undefined;
58 // Gets the bridge parameters for a given node ID. If the node
59 // is not currently used as a bridge, returns null.
60 let getBridge
= async
function (controller
, id
) {
61 let bridges
= await controller
.getConf("bridge");
63 for (let bridge
of bridges
) {
64 if (bridge
.ID
&& bridge
.ID
.toUpperCase() === id
.toUpperCase()) {
72 // nodeDataForID(controller, id)__.
73 // Returns the type, IP addresses and country code of a node with given ID.
74 // Example: `nodeDataForID(controller, "20BC91DC525C3DC9974B29FBEAB51230DE024C44")`
75 // => `{ type: "default", ipAddrs: ["12.23.34.45", "2001:db8::"], countryCode: "fr" }`
76 let nodeDataForID
= async
function (controller
, id
) {
77 let result
= {ipAddrs
: []};
78 const bridge
= await
getBridge(controller
, id
); // type, ip, countryCode;
79 const addrRe
= /^\[?([^\]]+)\]?:\d+$/
81 result
.type
= "bridge";
82 result
.bridgeType
= bridge
.type
;
83 // Attempt to get an IP address from bridge address string.
85 const ip
= bridge
.address
.match(addrRe
)[1];
86 if (!ip
.startsWith("0.")) {
87 result
.ipAddrs
= [ip
];
92 // either dealing with a relay, or a bridge whose fingerprint is not saved in torrc
94 const statusMap
= await controller
.getInfo("ns/id/" + id
);
95 result
.type
= "default";
96 if (!statusMap
.IP
.startsWith("0.")) {
97 result
.ipAddrs
.push(statusMap
.IP
);
100 result
.ipAddrs
.push(statusMap
.IPv6
.match(addrRe
)[1]);
104 // getInfo will throw if the given id is not a relay
105 // this probably means we are dealing with a user-provided bridge with no fingerprint
106 result
.type
= "bridge";
107 // we don't know the ip/ipv6 or type, so leave blank
109 result
.bridgeType
= "";
112 if (result
.ipAddrs
.length
> 0) {
113 // Get the country code for the node's IP address.
115 const countryCode
= await controller
.getInfo("ip-to-country/" + result
.ipAddrs
[0]);
116 result
.countryCode
= countryCode
=== "??" ? null : countryCode
;
122 // __nodeDataForCircuit(controller, circuitEvent)__.
123 // Gets the information for a circuit.
124 let nodeDataForCircuit
= async
function (controller
, circuitEvent
) {
125 let rawIDs
= circuitEvent
.circuit
.map(circ
=> circ
[0]),
126 // Remove the leading '$' if present.
127 ids
= rawIDs
.map(id
=> id
[0] === "$" ? id
.substring(1) : id
);
128 // Get the node data for all IDs in circuit.
129 return Promise
.all(ids
.map(id
=> nodeDataForID(controller
, id
)));
132 // __getCircuitStatusByID(aController, circuitID)__
133 // Returns the circuit status for the circuit with the given ID.
134 let getCircuitStatusByID
= async
function (aController
, circuitID
) {
135 let circuitStatuses
= await aController
.getInfo("circuit-status");
136 if (circuitStatuses
) {
137 for (let circuitStatus
of circuitStatuses
) {
138 if (circuitStatus
.id
=== circuitID
) {
139 return circuitStatus
;
146 // __collectIsolationData(aController, updateUI)__.
147 // Watches for STREAM SENTCONNECT events. When a SENTCONNECT event occurs, then
148 // we assume isolation settings (SOCKS username+password) are now fixed for the
149 // corresponding circuit. Whenever the first stream on a new circuit is seen,
150 // looks up u+p and records the node data in the credentialsToNodeDataMap.
151 // We need to update the circuit display immediately after any new node data
152 // is received. So the `updateUI` callback will be called at that point.
153 // See https://trac.torproject.org/projects/tor/ticket/15493
154 let collectIsolationData = function (aController
, updateUI
) {
155 return aController
.watchEvent(
157 streamEvent
=> streamEvent
.StreamStatus
=== "SENTCONNECT",
158 async (streamEvent
) => {
159 if (!knownCircuitIDs
.get(streamEvent
.CircuitID
)) {
160 logger
.eclog(3, "streamEvent.CircuitID: " + streamEvent
.CircuitID
);
161 knownCircuitIDs
.set(streamEvent
.CircuitID
, true);
162 let circuitStatus
= await
getCircuitStatusByID(aController
, streamEvent
.CircuitID
),
163 credentials
= circuitStatus
?
164 (trimQuotes(circuitStatus
.SOCKS_USERNAME
) + "|" +
165 trimQuotes(circuitStatus
.SOCKS_PASSWORD
)) :
168 let nodeData
= await
nodeDataForCircuit(aController
, circuitStatus
);
169 credentialsToNodeDataMap
.set(credentials
, nodeData
);
176 // __browserForChannel(channel)__.
177 // Returns the browser that loaded a given channel.
178 let browserForChannel = function (channel
) {
179 if (!channel
) return null;
180 let chan
= channel
.QueryInterface(Ci
.nsIChannel
);
181 let callbacks
= chan
.notificationCallbacks
;
182 if (!callbacks
) return null;
185 loadContext
= callbacks
.getInterface(Ci
.nsILoadContext
);
190 if (!loadContext
) return null;
191 return loadContext
.topFrameElement
;
194 // __collectBrowserCredentials()__.
195 // Starts observing http channels. Each channel's proxyInfo
196 // username and password is recorded for the channel's browser.
197 let collectBrowserCredentials = function () {
198 return observe("http-on-modify-request", chan
=> {
200 let proxyInfo
= chan
.QueryInterface(Ci
.nsIProxiedChannel
).proxyInfo
;
201 let browser
= browserForChannel(chan
);
202 if (browser
&& proxyInfo
) {
203 if (!browserToCredentialsMap
.has(browser
)) {
204 browserToCredentialsMap
.set(browser
, new Map());
206 let domainMap
= browserToCredentialsMap
.get(browser
);
207 domainMap
.set(proxyInfo
.username
, [proxyInfo
.username
,
208 proxyInfo
.password
]);
211 logger
.eclog(3, `Error collecting browser credentials: ${e.message}, ${chan.URI.spec}`);
219 // Read the localized strings for this UI.
220 let uiString = function (shortName
) {
221 return torbutton_get_property_string("torbutton.circuit_display." + shortName
);
224 // __localizedCountryNameFromCode(countryCode)__.
225 // Convert a country code to a localized country name.
226 // Example: `'de'` -> `'Deutschland'` in German locale.
227 let localizedCountryNameFromCode = function (countryCode
) {
228 if (!countryCode
) return uiString("unknown_country");
230 return Services
.intl
.getRegionDisplayNames(undefined, [countryCode
])[0];
232 return countryCode
.toUpperCase();
236 // __showCircuitDisplay(show)__.
237 // If show === true, makes the circuit display visible.
238 let showCircuitDisplay = function (show
) {
239 document
.getElementById("circuit-display-container").style
.display
= show
?
243 // __xmlTree(ns, data)__.
244 // Takes an xml namespace, ns, and a
245 // data structure representing xml elements like
246 // [tag, { attr-key: attr-value }, ...xml-children]
247 // and returns nested xml element objects.
248 let xmlTree
= function xmlTree (ns
, data
) {
249 let [type
, attrs
, ...children
] = data
;
250 let element
= type
.startsWith("html:")
251 ? document
.createXULElement(type
)
252 : document
.createElementNS(ns
, type
);
253 for (let [key
, val
] of Object
.entries(attrs
)) {
254 element
.setAttribute(key
, val
);
256 for (let child
of children
) {
257 if (child
!== null && child
!== undefined) {
258 element
.append(typeof child
=== "string" ? child
: xmlTree(ns
, child
));
264 // __htmlTree(data)__.
265 // Takes a data structure representing html elements like
266 // [tag, { attr-key: attr-value }, ...html-children]
267 // and returns nested html element objects.
268 let htmlTree
= data
=> xmlTree("http://www.w3.org/1999/xhtml", data
);
270 // __appendHtml(parent, data)__.
271 // Takes a data structure representing html elements like
272 // [tag, { attr-key: attr-value }, ...html-children]
273 // and appends nested html element objects to the parent element.
274 let appendHtml
= (parent
, data
) => parent
.appendChild(htmlTree(data
));
276 // __circuitCircuitData()__.
277 // Obtains the circuit used by the given browser.
278 let currentCircuitData = function (browser
) {
280 let firstPartyDomain
= getDomainForBrowser(browser
);
281 let domain
= firstPartyDomain
|| "--unknown--";
282 let domainMap
= browserToCredentialsMap
.get(browser
);
283 let credentials
= domainMap
&& domainMap
.get(domain
);
285 let [SOCKS_username
, SOCKS_password
] = credentials
;
286 let nodeData
= credentialsToNodeDataMap
.get(`${SOCKS_username}|${SOCKS_password}`);
287 let domain
= SOCKS_username
;
288 if (browser
.documentURI
.host
.endsWith(".tor.onion")) {
289 const service
= Cc
["@torproject.org/onion-alias-service;1"].getService(
290 Ci
.IOnionAliasService
292 domain
= service
.getOnionAlias(browser
.documentURI
.host
);
294 return { domain
, nodeData
};
297 return { domain
: null, nodeData
: null };
300 // __updateCircuitDisplay()__.
301 // Updates the Tor circuit display, showing the current domain
302 // and the relay nodes for that domain.
303 let updateCircuitDisplay = function () {
304 let { domain
, nodeData
} = currentCircuitData(gBrowser
.selectedBrowser
);
305 if (domain
&& nodeData
) {
306 // Update the displayed information for the relay nodes.
307 let nodeHtmlList
= document
.getElementById("circuit-display-nodes");
308 let li
= (...data
) => appendHtml(nodeHtmlList
, ["li", {}, ...data
]);
309 nodeHtmlList
.innerHTML
= "";
310 li(uiString("this_browser"));
311 for (let i
= 0; i
< nodeData
.length
; ++i
) {
313 if (nodeData
[i
].type
=== "bridge") {
314 relayText
= uiString("tor_bridge");
315 let bridgeType
= nodeData
[i
].bridgeType
;
316 if (bridgeType
=== "meek_lite") {
317 relayText
+= ": meek";
319 else if (bridgeType
!== "vanilla" && bridgeType
!== "") {
320 relayText
+= ": " + bridgeType
;
322 } else if (nodeData
[i
].type
== "default") {
323 relayText
= localizedCountryNameFromCode(nodeData
[i
].countryCode
);
325 const ipAddrs
= nodeData
[i
].ipAddrs
.join(", ");
326 li(relayText
, " ", ["span", { class: "circuit-ip-address" }, ipAddrs
], " ",
327 (i
=== 0 && nodeData
[0].type
!== "bridge") ?
328 ["span", { class: "circuit-guard-info" }, uiString("guard")] : null);
331 let domainParts
= [];
332 if (domain
.endsWith(".onion")) {
333 for (let i
= 0; i
< 3; ++i
) {
334 li(uiString("relay"));
336 if (domain
.length
> 22) {
337 domainParts
.push(domain
.slice(0, 7), "…", domain
.slice(-12));
339 domainParts
.push(domain
);
342 domainParts
.push(domain
);
345 // We use a XUL html:span element so that the tooltiptext is displayed.
349 class: "circuit-onion",
351 this.classList.add("circuit-onion-copied");
353 "@mozilla.org/widget/clipboardhelper;1"
354 ].getService(Ci.nsIClipboardHelper).copyString(this.getAttribute("data-onion"))
356 "data-onion": domain
,
357 "data-text-clicktocopy": torbutton_get_property_string("torbutton.circuit_display.click_to_copy"),
358 "data-text-copied": torbutton_get_property_string("torbutton.circuit_display.copied"),
364 // Hide the note about guards if we are using a bridge.
365 document
.getElementById("circuit-guard-note-container").style
.display
=
366 (nodeData
[0].type
=== "bridge") ? "none" : "block";
368 // Only show the Tor circuit if we have credentials and node data.
369 logger
.eclog(4, "no SOCKS credentials found for current document.");
371 showCircuitDisplay(domain
&& nodeData
);
374 // __syncDisplayWithSelectedTab(syncOn)__.
375 // Whenever the user starts to open the popup menu, make sure the display
376 // is the correct one for this tab. It's also possible that a new site
377 // can be loaded while the popup menu is open.
378 // Update the display if this happens.
379 let syncDisplayWithSelectedTab
= (function() {
380 let listener
= { onLocationChange : function (aBrowser
) {
381 if (aBrowser
=== gBrowser
.selectedBrowser
) {
382 updateCircuitDisplay();
385 return function (syncOn
) {
386 let popupMenu
= document
.getElementById("identity-popup");
388 // Update the circuit display just before the popup menu is shown.
389 popupMenu
.addEventListener("popupshowing", updateCircuitDisplay
);
390 // If the currently selected tab has been sent to a new location,
391 // update the circuit to reflect that.
392 gBrowser
.addTabsProgressListener(listener
);
395 gBrowser
.removeTabsProgressListener(listener
);
396 popupMenu
.removeEventListener("popupshowing", updateCircuitDisplay
);
398 showCircuitDisplay(false);
403 // __setupGuardNote()__.
404 // Call once to show the Guard note as intended.
405 let setupGuardNote = function () {
406 let guardNote
= document
.getElementById("circuit-guard-note-container");
407 let guardNoteString
= uiString("guard_note");
408 let learnMoreString
= uiString("learn_more");
409 let [noteBefore
, name
, noteAfter
] = guardNoteString
.split(/[\[\]]/);
410 let localeCode
= getLocale();
411 appendHtml(guardNote
,
413 noteBefore
, ["span", {class: "circuit-guard-name"}, name
],
415 ["span", {onclick
: `gBrowser.selectedTab = gBrowser.addWebTab('https://support.torproject.org/${localeCode}/tbb/tbb-2/');`,
416 class: "circuit-link"},
420 // __ensureCorrectPopupDimensions()__.
421 // Make sure the identity popup always displays with the correct height.
422 let ensureCorrectPopupDimensions = function () {
423 let setDimensions
= () => {
425 let view
= document
.querySelector("#identity-popup-multiView .panel-viewcontainer");
426 let stack
= document
.querySelector("#identity-popup-multiView .panel-viewstack");
427 let view2
= document
.getElementById("identity-popup-mainView");
428 if (view
&& stack
&& view2
) {
429 let newWidth
= Math
.max(...[...view2
.children
].map(el
=> el
.clientWidth
));
430 let newHeight
= stack
.clientHeight
;
431 stack
.setAttribute("width", newWidth
);
432 view2
.style
.minWidth
= view2
.style
.maxWidth
= newWidth
+ "px";
433 view
.setAttribute("width", newWidth
);
434 view
.setAttribute("height", newHeight
);
438 let removeDimensions
= () => {
439 let view
= document
.querySelector("#identity-popup-multiView .panel-viewcontainer");
440 let stack
= document
.querySelector("#identity-popup-multiView .panel-viewstack");
441 let view2
= document
.getElementById("identity-popup-mainView");
442 if (view
&& stack
&& view2
) {
443 view
.removeAttribute("width");
444 view
.removeAttribute("height");
445 stack
.removeAttribute("width");
446 view2
.style
.minWidth
= view2
.style
.maxWidth
= "";
449 let popupMenu
= document
.getElementById("identity-popup");
450 popupMenu
.addEventListener("popupshowing", setDimensions
);
451 popupMenu
.addEventListener("popuphiding", removeDimensions
);
453 popupMenu
.removeEventListener("popupshowing", setDimensions
);
454 popupMenu
.removeEventListener("popuphiding", removeDimensions
);
460 // __setupDisplay(enablePrefName)__.
461 // Once called, the Tor circuit display will be started whenever
462 // the "enablePref" is set to true, and stopped when it is set to false.
463 // A reference to this function (called createTorCircuitDisplay) is exported as a global.
464 let setupDisplay = function (enablePrefName
) {
465 // From 79 on the identity popup is initialized lazily
466 if (gIdentityHandler
._initializePopup
) {
467 gIdentityHandler
._initializePopup();
470 let myController
= null,
471 stopCollectingIsolationData
= null,
472 stopCollectingBrowserCredentials
= null,
473 stopEnsuringCorrectPopupDimensions
= null,
475 syncDisplayWithSelectedTab(false);
477 if (stopCollectingIsolationData
) {
478 stopCollectingIsolationData();
480 if (stopCollectingBrowserCredentials
) {
481 stopCollectingBrowserCredentials();
483 if (stopEnsuringCorrectPopupDimensions
) {
484 stopEnsuringCorrectPopupDimensions();
489 start
= async
function () {
492 myController
= await
wait_for_controller();
493 syncDisplayWithSelectedTab(true);
494 stopCollectingIsolationData
= collectIsolationData(myController
, updateCircuitDisplay
);
495 stopCollectingBrowserCredentials
= collectBrowserCredentials();
496 stopEnsuringCorrectPopupDimensions
= ensureCorrectPopupDimensions();
498 logger
.eclog(5, err
);
499 logger
.eclog(5, "Disabling tor display circuit because of an error.");
500 myController
.close();
506 let unbindPref
= bindPrefAndInit(enablePrefName
, on
=> { if (on
) start(); else stop(); });
507 // When this chrome window is unloaded, we need to unbind the pref.
508 window
.addEventListener("unload", function () {
513 logger
.eclog(5, "Error: " + e
.message
+ "\n" + e
.stack
);
519 // Finish createTorCircuitDisplay()