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() {
28 const { Services } = ChromeUtils.import(
29 "resource://gre/modules/Services.jsm"
32 // Import the controller code.
33 const { wait_for_controller } = ChromeUtils.import(
34 "resource://torbutton/modules/tor-control-port.js"
43 torbutton_get_property_string,
44 } = ChromeUtils.import("resource://torbutton/modules/utils.js");
46 // Make the TorButton logger available.
47 let logger = Cc["@torproject.org/torbutton-logger;1"].getService(
51 // ## Circuit/stream credentials and node monitoring
53 // A mutable map that stores the current nodes for each
54 // SOCKS username/password pair.
55 let credentialsToNodeDataMap = new Map(),
56 // A mutable map that reports `true` for IDs of "mature" circuits
57 // (those that have conveyed a stream).
58 knownCircuitIDs = new Map(),
59 // A mutable map that records the SOCKS credentials for the
60 // latest channels for each browser + domain.
61 browserToCredentialsMap = new Map();
64 // Removes quotation marks around a quoted string.
65 let trimQuotes = s => (s ? s.match(/^"(.*)"$/)[1] : undefined);
68 // Gets the bridge parameters for a given node ID. If the node
69 // is not currently used as a bridge, returns null.
70 let getBridge = async function(controller, id) {
71 let bridges = await controller.getConf("bridge");
73 for (let bridge of bridges) {
74 if (bridge.ID && bridge.ID.toUpperCase() === id.toUpperCase()) {
82 // nodeDataForID(controller, id)__.
83 // Returns the type, IP addresses and country code of a node with given ID.
84 // Example: `nodeDataForID(controller, "20BC91DC525C3DC9974B29FBEAB51230DE024C44")`
85 // => `{ type: "default", ipAddrs: ["12.23.34.45", "2001:db8::"], countryCode: "fr" }`
86 let nodeDataForID = async function(controller, id) {
87 let result = { ipAddrs: [] };
88 const bridge = await getBridge(controller, id); // type, ip, countryCode;
89 const addrRe = /^\[?([^\]]+)\]?:\d+$/;
91 result.type = "bridge";
92 result.bridgeType = bridge.type;
93 // Attempt to get an IP address from bridge address string.
95 const ip = bridge.address.match(addrRe)[1];
96 if (!ip.startsWith("0.")) {
97 result.ipAddrs = [ip];
101 // either dealing with a relay, or a bridge whose fingerprint is not saved in torrc
103 const statusMap = await controller.getInfo("ns/id/" + id);
104 result.type = "default";
105 if (!statusMap.IP.startsWith("0.")) {
106 result.ipAddrs.push(statusMap.IP);
109 result.ipAddrs.push(statusMap.IPv6.match(addrRe)[1]);
112 // getInfo will throw if the given id is not a relay
113 // this probably means we are dealing with a user-provided bridge with no fingerprint
114 result.type = "bridge";
115 // we don't know the ip/ipv6 or type, so leave blank
117 result.bridgeType = "";
120 if (result.ipAddrs.length) {
121 // Get the country code for the node's IP address.
123 const countryCode = await controller.getInfo(
124 "ip-to-country/" + result.ipAddrs[0]
126 result.countryCode = countryCode === "??" ? null : countryCode;
132 // __nodeDataForCircuit(controller, circuitEvent)__.
133 // Gets the information for a circuit.
134 let nodeDataForCircuit = async function(controller, circuitEvent) {
135 let rawIDs = circuitEvent.circuit.map(circ => circ[0]),
136 // Remove the leading '$' if present.
137 ids = rawIDs.map(id => (id[0] === "$" ? id.substring(1) : id));
138 // Get the node data for all IDs in circuit.
139 return Promise.all(ids.map(id => nodeDataForID(controller, id)));
142 // __getCircuitStatusByID(aController, circuitID)__
143 // Returns the circuit status for the circuit with the given ID.
144 let getCircuitStatusByID = async function(aController, circuitID) {
145 let circuitStatuses = await aController.getInfo("circuit-status");
146 if (circuitStatuses) {
147 for (let circuitStatus of circuitStatuses) {
148 if (circuitStatus.id === circuitID) {
149 return circuitStatus;
156 // __collectIsolationData(aController, updateUI)__.
157 // Watches for STREAM SENTCONNECT events. When a SENTCONNECT event occurs, then
158 // we assume isolation settings (SOCKS username+password) are now fixed for the
159 // corresponding circuit. Whenever the first stream on a new circuit is seen,
160 // looks up u+p and records the node data in the credentialsToNodeDataMap.
161 // We need to update the circuit display immediately after any new node data
162 // is received. So the `updateUI` callback will be called at that point.
163 // See https://trac.torproject.org/projects/tor/ticket/15493
164 let collectIsolationData = function(aController, updateUI) {
165 return aController.watchEvent(
167 streamEvent => streamEvent.StreamStatus === "SENTCONNECT",
168 async streamEvent => {
169 if (!knownCircuitIDs.get(streamEvent.CircuitID)) {
170 logger.eclog(3, "streamEvent.CircuitID: " + streamEvent.CircuitID);
171 knownCircuitIDs.set(streamEvent.CircuitID, true);
172 let circuitStatus = await getCircuitStatusByID(
174 streamEvent.CircuitID
176 credentials = circuitStatus
177 ? trimQuotes(circuitStatus.SOCKS_USERNAME) +
179 trimQuotes(circuitStatus.SOCKS_PASSWORD)
182 let nodeData = await nodeDataForCircuit(aController, circuitStatus);
183 credentialsToNodeDataMap.set(credentials, nodeData);
191 // __browserForChannel(channel)__.
192 // Returns the browser that loaded a given channel.
193 let browserForChannel = function(channel) {
197 let chan = channel.QueryInterface(Ci.nsIChannel);
198 let callbacks = chan.notificationCallbacks;
204 loadContext = callbacks.getInterface(Ci.nsILoadContext);
212 return loadContext.topFrameElement;
215 // __collectBrowserCredentials()__.
216 // Starts observing http channels. Each channel's proxyInfo
217 // username and password is recorded for the channel's browser.
218 let collectBrowserCredentials = function() {
219 return observe("http-on-modify-request", chan => {
221 let proxyInfo = chan.QueryInterface(Ci.nsIProxiedChannel).proxyInfo;
222 let browser = browserForChannel(chan);
223 if (browser && proxyInfo) {
224 if (!browserToCredentialsMap.has(browser)) {
225 browserToCredentialsMap.set(browser, new Map());
227 let domainMap = browserToCredentialsMap.get(browser);
228 domainMap.set(proxyInfo.username, [
236 `Error collecting browser credentials: ${e.message}, ${chan.URI.spec}`
245 // Read the localized strings for this UI.
246 let uiString = function(shortName) {
247 return torbutton_get_property_string(
248 "torbutton.circuit_display." + shortName
252 // __localizedCountryNameFromCode(countryCode)__.
253 // Convert a country code to a localized country name.
254 // Example: `'de'` -> `'Deutschland'` in German locale.
255 let localizedCountryNameFromCode = function(countryCode) {
257 return uiString("unknown_country");
260 return Services.intl.getRegionDisplayNames(undefined, [countryCode])[0];
262 return countryCode.toUpperCase();
266 // __showCircuitDisplay(show)__.
267 // If show === true, makes the circuit display visible.
268 let showCircuitDisplay = function(show) {
269 document.getElementById("circuit-display-container").hidden = !show;
272 // __xmlTree(ns, data)__.
273 // Takes an xml namespace, ns, and a
274 // data structure representing xml elements like
275 // [tag, { attr-key: attr-value }, ...xml-children]
276 // and returns nested xml element objects.
277 let xmlTree = function xmlTree(ns, data) {
278 let [type, attrs, ...children] = data;
279 let element = type.startsWith("html:")
280 ? document.createXULElement(type)
281 : document.createElementNS(ns, type);
282 for (let [key, val] of Object.entries(attrs)) {
283 element.setAttribute(key, val);
285 for (let child of children) {
286 if (child !== null && child !== undefined) {
287 element.append(typeof child === "string" ? child : xmlTree(ns, child));
293 // __htmlTree(data)__.
294 // Takes a data structure representing html elements like
295 // [tag, { attr-key: attr-value }, ...html-children]
296 // and returns nested html element objects.
297 let htmlTree = data => xmlTree("http://www.w3.org/1999/xhtml", data);
299 // __appendHtml(parent, data)__.
300 // Takes a data structure representing html elements like
301 // [tag, { attr-key: attr-value }, ...html-children]
302 // and appends nested html element objects to the parent element.
303 let appendHtml = (parent, data) => parent.appendChild(htmlTree(data));
305 // __circuitCircuitData()__.
306 // Obtains the circuit used by the given browser.
307 let currentCircuitData = function(browser) {
309 let firstPartyDomain = getDomainForBrowser(browser);
310 let domain = firstPartyDomain || "--unknown--";
311 let domainMap = browserToCredentialsMap.get(browser);
312 let credentials = domainMap && domainMap.get(domain);
314 let [SOCKS_username, SOCKS_password] = credentials;
315 let nodeData = credentialsToNodeDataMap.get(
316 `${SOCKS_username}|${SOCKS_password}`
318 let domain = SOCKS_username;
319 if (browser.documentURI.host.endsWith(".tor.onion")) {
321 "@torproject.org/onion-alias-service;1"
322 ].getService(Ci.IOnionAliasService);
323 domain = service.getOnionAlias(browser.documentURI.host);
325 return { domain, nodeData };
328 return { domain: null, nodeData: null };
331 // __updateCircuitDisplay()__.
332 // Updates the Tor circuit display, showing the current domain
333 // and the relay nodes for that domain.
334 let updateCircuitDisplay = function() {
335 let { domain, nodeData } = currentCircuitData(gBrowser.selectedBrowser);
336 if (domain && nodeData) {
337 // Update the displayed information for the relay nodes.
338 let nodeHtmlList = document.getElementById("circuit-display-nodes");
339 let li = (...data) => appendHtml(nodeHtmlList, ["li", {}, ...data]);
340 nodeHtmlList.innerHTML = "";
341 li(uiString("this_browser"));
342 for (let i = 0; i < nodeData.length; ++i) {
344 if (nodeData[i].type === "bridge") {
345 relayText = uiString("tor_bridge");
346 let bridgeType = nodeData[i].bridgeType;
347 if (bridgeType === "meek_lite") {
348 relayText += ": meek";
349 } else if (bridgeType !== "vanilla" && bridgeType !== "") {
350 relayText += ": " + bridgeType;
352 } else if (nodeData[i].type == "default") {
353 relayText = localizedCountryNameFromCode(nodeData[i].countryCode);
355 const ipAddrs = nodeData[i].ipAddrs.join(", ");
359 ["span", { class: "circuit-ip-address" }, ipAddrs],
361 i === 0 && nodeData[0].type !== "bridge"
362 ? ["span", { class: "circuit-guard-info" }, uiString("guard")]
367 let domainParts = [];
368 if (domain.endsWith(".onion")) {
369 for (let i = 0; i < 3; ++i) {
370 li(uiString("relay"));
372 if (domain.length > 22) {
373 domainParts.push(domain.slice(0, 7), "…", domain.slice(-12));
375 domainParts.push(domain);
378 domainParts.push(domain);
381 // We use a XUL html:span element so that the tooltiptext is displayed.
385 class: "circuit-onion",
387 this.classList.add("circuit-onion-copied");
389 "@mozilla.org/widget/clipboardhelper;1"
390 ].getService(Ci.nsIClipboardHelper).copyString(this.getAttribute("data-onion"))
392 "data-onion": domain,
393 "data-text-clicktocopy": torbutton_get_property_string(
394 "torbutton.circuit_display.click_to_copy"
396 "data-text-copied": torbutton_get_property_string(
397 "torbutton.circuit_display.copied"
404 // Hide the note about guards if we are using a bridge.
405 document.getElementById("circuit-guard-note-container").hidden =
406 nodeData[0].type === "bridge";
408 // Only show the Tor circuit if we have credentials and node data.
409 logger.eclog(4, "no SOCKS credentials found for current document.");
411 showCircuitDisplay(domain && nodeData);
414 // __syncDisplayWithSelectedTab(syncOn)__.
415 // Whenever the user starts to open the popup menu, make sure the display
416 // is the correct one for this tab. It's also possible that a new site
417 // can be loaded while the popup menu is open.
418 // Update the display if this happens.
419 let syncDisplayWithSelectedTab = (function() {
421 onLocationChange(aBrowser) {
422 if (aBrowser === gBrowser.selectedBrowser) {
423 updateCircuitDisplay();
427 return function(syncOn) {
428 let popupMenu = document.getElementById("identity-popup");
430 // Update the circuit display just before the popup menu is shown.
431 popupMenu.addEventListener("popupshowing", updateCircuitDisplay);
432 // If the currently selected tab has been sent to a new location,
433 // update the circuit to reflect that.
434 gBrowser.addTabsProgressListener(listener);
437 gBrowser.removeTabsProgressListener(listener);
438 popupMenu.removeEventListener("popupshowing", updateCircuitDisplay);
440 showCircuitDisplay(false);
445 // __setupGuardNote()__.
446 // Call once to show the Guard note as intended.
447 let setupGuardNote = function() {
448 let guardNote = document.getElementById("circuit-guard-note-container");
449 let guardNoteString = uiString("guard_note");
450 let learnMoreString = uiString("learn_more");
451 let [noteBefore, name, noteAfter] = guardNoteString.split(/[\[\]]/);
452 let localeCode = getLocale();
453 appendHtml(guardNote, [
457 ["span", { class: "circuit-guard-name" }, name],
463 onclick: `gBrowser.selectedTab = gBrowser.addWebTab('https://support.torproject.org/${localeCode}/tbb/tbb-2/');`,
464 class: "circuit-link",
471 // __ensureCorrectPopupDimensions()__.
472 // Make sure the identity popup always displays with the correct height.
473 let ensureCorrectPopupDimensions = function() {
474 // FIXME: This is hacking with the internals of the panel view, which also
475 // sets the width and height for transitions (see PanelMultiView.jsm), so
476 // this fix is fragile.
477 // Ideally the panel view would start using a non-XUL CSS layout, which
478 // would work regardless of the content.
479 let popupMenu = document.getElementById("identity-popup");
480 let setDimensions = event => {
481 if (event.target !== popupMenu) {
485 let view = document.querySelector(
486 "#identity-popup-multiView .panel-viewcontainer"
488 let stack = document.querySelector(
489 "#identity-popup-multiView .panel-viewstack"
491 let view2 = document.getElementById("identity-popup-mainView");
492 if (view && stack && view2) {
493 let newWidth = Math.max(
494 ...[...view2.children].map(el => el.clientWidth)
496 let newHeight = stack.clientHeight;
497 stack.setAttribute("width", newWidth);
498 view2.style.minWidth = view2.style.maxWidth = newWidth + "px";
499 view.setAttribute("width", newWidth);
500 view.setAttribute("height", newHeight);
504 let removeDimensions = event => {
505 if (event.target !== popupMenu) {
508 let view = document.querySelector(
509 "#identity-popup-multiView .panel-viewcontainer"
511 let stack = document.querySelector(
512 "#identity-popup-multiView .panel-viewstack"
514 let view2 = document.getElementById("identity-popup-mainView");
515 if (view && stack && view2) {
516 view.removeAttribute("width");
517 view.removeAttribute("height");
518 stack.removeAttribute("width");
519 view2.style.minWidth = view2.style.maxWidth = "";
522 popupMenu.addEventListener("popupshowing", setDimensions);
523 popupMenu.addEventListener("popuphiding", removeDimensions);
525 popupMenu.removeEventListener("popupshowing", setDimensions);
526 popupMenu.removeEventListener("popuphiding", removeDimensions);
532 // __setupDisplay(enablePrefName)__.
533 // Once called, the Tor circuit display will be started whenever
534 // the "enablePref" is set to true, and stopped when it is set to false.
535 // A reference to this function (called createTorCircuitDisplay) is exported as a global.
536 let setupDisplay = function(enablePrefName) {
537 // From 79 on the identity popup is initialized lazily
538 if (gIdentityHandler._initializePopup) {
539 gIdentityHandler._initializePopup();
542 let myController = null,
543 stopCollectingIsolationData = null,
544 stopCollectingBrowserCredentials = null,
545 stopEnsuringCorrectPopupDimensions = null,
547 syncDisplayWithSelectedTab(false);
549 if (stopCollectingIsolationData) {
550 stopCollectingIsolationData();
552 if (stopCollectingBrowserCredentials) {
553 stopCollectingBrowserCredentials();
555 if (stopEnsuringCorrectPopupDimensions) {
556 stopEnsuringCorrectPopupDimensions();
561 start = async function() {
564 myController = await wait_for_controller();
565 syncDisplayWithSelectedTab(true);
566 stopCollectingIsolationData = collectIsolationData(
570 stopCollectingBrowserCredentials = collectBrowserCredentials();
571 stopEnsuringCorrectPopupDimensions = ensureCorrectPopupDimensions();
573 logger.eclog(5, err);
576 "Disabling tor display circuit because of an error."
578 myController.close();
584 let unbindPref = bindPrefAndInit(enablePrefName, on => {
591 // When this chrome window is unloaded, we need to unbind the pref.
592 window.addEventListener("unload", function() {
597 logger.eclog(5, "Error: " + e.message + "\n" + e.stack);
603 // Finish createTorCircuitDisplay()