fixup! Bug 40051: Implement 2021 Year End Campaign look in about:tor
[torbutton.git] / chrome / content / tor-circuit-display.js
blob4e0088f0abfdfea02311f5c0a483e0856bd2e097
1 // A script that automatically displays the Tor Circuit used for the
2 // current domain for the currently selected tab.
3 //
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.
7 //
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 */
17 // ### Main function
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
23 // or disabled.
24 let createTorCircuitDisplay = (function () {
26 "use strict";
28 // Mozilla utilities
29 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
31 // Import the controller code.
32 let { controller } = ChromeUtils.import("resource://torbutton/modules/tor-control-port.js", {});
34 // Utility functions
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();
53 // __trimQuotes(s)__.
54 // Removes quotation marks around a quoted string.
55 let trimQuotes = s => s ? s.match(/^"(.*)"$/)[1] : undefined;
57 // __getBridge(id)__.
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");
62   if (bridges) {
63     for (let bridge of bridges) {
64       if (bridge.ID && bridge.ID.toUpperCase() === id.toUpperCase()) {
65         return bridge;
66       }
67     }
68   }
69   return null;
72 // nodeDataForID(controller, id)__.
73 // Returns the type, IP and country code of a node with given ID.
74 // Example: `nodeDataForID(controller, "20BC91DC525C3DC9974B29FBEAB51230DE024C44")`
75 // => `{ type : "default", ip : "12.23.34.45", countryCode : "fr" }`
76 let nodeDataForID = async function (controller, id) {
77   let result = {},
78       bridge = await getBridge(controller, id); // type, ip, countryCode;
79   if (bridge) {
80     result.type = "bridge";
81     result.bridgeType = bridge.type;
82     // Attempt to get an IP address from bridge address string.
83     try {
84       result.ip = bridge.address.split(":")[0];
85     } catch (e) { }
86   } else {
87     // either dealing with a relay, or a bridge whose fingerprint is not saved in torrc
88     try {
89       let statusMap = await controller.getInfo("ns/id/" + id);
90       result.type = "default";
91       result.ip = statusMap.IP;
92     } catch (e) {
93       // getInfo will throw if the given id is not a relay
94       // this probably means we are dealing with a user-provided bridge with no fingerprint
95       result.type = "bridge";
96       // we don't know the ip or type, so leave blank
97       result.ip = "";
98       result.bridgeType = "";
99     }
100   }
101   if (result.ip) {
102     // Get the country code for the node's IP address.
103     try {
104       let countryCode = await controller.getInfo("ip-to-country/" + result.ip);
105       result.countryCode = countryCode === "??" ? null : countryCode;
106     } catch (e) { }
107   }
108   return result;
111 // __nodeDataForCircuit(controller, circuitEvent)__.
112 // Gets the information for a circuit.
113 let nodeDataForCircuit = async function (controller, circuitEvent) {
114   let rawIDs = circuitEvent.circuit.map(circ => circ[0]),
115       // Remove the leading '$' if present.
116       ids = rawIDs.map(id => id[0] === "$" ? id.substring(1) : id);
117   // Get the node data for all IDs in circuit.
118   return Promise.all(ids.map(id => nodeDataForID(controller, id)));
121 // __getCircuitStatusByID(aController, circuitID)__
122 // Returns the circuit status for the circuit with the given ID.
123 let getCircuitStatusByID = async function (aController, circuitID) {
124   let circuitStatuses = await aController.getInfo("circuit-status");
125   if (circuitStatuses) {
126     for (let circuitStatus of circuitStatuses) {
127       if (circuitStatus.id === circuitID) {
128         return circuitStatus;
129       }
130     }
131   }
132   return null;
135 // __collectIsolationData(aController, updateUI)__.
136 // Watches for STREAM SENTCONNECT events. When a SENTCONNECT event occurs, then
137 // we assume isolation settings (SOCKS username+password) are now fixed for the
138 // corresponding circuit. Whenever the first stream on a new circuit is seen,
139 // looks up u+p and records the node data in the credentialsToNodeDataMap.
140 // We need to update the circuit display immediately after any new node data
141 // is received. So the `updateUI` callback will be called at that point.
142 // See https://trac.torproject.org/projects/tor/ticket/15493
143 let collectIsolationData = function (aController, updateUI) {
144   return aController.watchEvent(
145     "STREAM",
146     streamEvent => streamEvent.StreamStatus === "SENTCONNECT",
147     async (streamEvent) => {
148       if (!knownCircuitIDs.get(streamEvent.CircuitID)) {
149         logger.eclog(3, "streamEvent.CircuitID: " + streamEvent.CircuitID);
150         knownCircuitIDs.set(streamEvent.CircuitID, true);
151         let circuitStatus = await getCircuitStatusByID(aController, streamEvent.CircuitID),
152             credentials = circuitStatus ?
153                             (trimQuotes(circuitStatus.SOCKS_USERNAME) + "|" +
154                              trimQuotes(circuitStatus.SOCKS_PASSWORD)) :
155                             null;
156         if (credentials) {
157           let nodeData = await nodeDataForCircuit(aController, circuitStatus);
158           credentialsToNodeDataMap.set(credentials, nodeData);
159           updateUI();
160         }
161       }
162     });
165 // __browserForChannel(channel)__.
166 // Returns the browser that loaded a given channel.
167 let browserForChannel = function (channel) {
168   if (!channel) return null;
169   let chan = channel.QueryInterface(Ci.nsIChannel);
170   let callbacks = chan.notificationCallbacks;
171   if (!callbacks) return null;
172   let loadContext;
173   try {
174     loadContext = callbacks.getInterface(Ci.nsILoadContext);
175   } catch (e) {
176     // Ignore
177     return null;
178   }
179   if (!loadContext) return null;
180   return loadContext.topFrameElement;
183 // __collectBrowserCredentials()__.
184 // Starts observing http channels. Each channel's proxyInfo
185 // username and password is recorded for the channel's browser.
186 let collectBrowserCredentials = function () {
187   return observe("http-on-modify-request", chan => {
188     try {
189       let proxyInfo = chan.QueryInterface(Ci.nsIProxiedChannel).proxyInfo;
190       let browser = browserForChannel(chan);
191       if (browser && proxyInfo) {
192         if (!browserToCredentialsMap.has(browser)) {
193           browserToCredentialsMap.set(browser, new Map());
194         }
195         let domainMap = browserToCredentialsMap.get(browser);
196         domainMap.set(proxyInfo.username, [proxyInfo.username,
197                                           proxyInfo.password]);
198       }
199     } catch (e) {
200       logger.eclog(3, `Error collecting browser credentials: ${e.message}, ${chan.URI.spec}`);
201     }
202   });
205 // ## User interface
207 // __uiString__.
208 // Read the localized strings for this UI.
209 let uiString = function (shortName) {
210   return torbutton_get_property_string("torbutton.circuit_display." + shortName);
213 // __localizedCountryNameFromCode(countryCode)__.
214 // Convert a country code to a localized country name.
215 // Example: `'de'` -> `'Deutschland'` in German locale.
216 let localizedCountryNameFromCode = function (countryCode) {
217   if (!countryCode) return uiString("unknown_country");
218   try {
219     return Services.intl.getRegionDisplayNames(undefined, [countryCode])[0];
220   } catch (e) {
221     return countryCode.toUpperCase();
222   }
225 // __showCircuitDisplay(show)__.
226 // If show === true, makes the circuit display visible.
227 let showCircuitDisplay = function (show) {
228   document.getElementById("circuit-display-container").style.display = show ?
229                                                             'block' : 'none';
232 // __xmlTree(ns, data)__.
233 // Takes an xml namespace, ns, and a
234 // data structure representing xml elements like
235 // [tag, { attr-key: attr-value }, ...xml-children]
236 // and returns nested xml element objects.
237 let xmlTree = function xmlTree (ns, data) {
238   let [type, attrs, ...children] = data;
239   let element = type.startsWith("html:")
240     ? document.createXULElement(type)
241     : document.createElementNS(ns, type);
242   for (let [key, val] of Object.entries(attrs)) {
243     element.setAttribute(key, val);
244   }
245   for (let child of children) {
246     if (child !== null && child !== undefined) {
247       element.append(typeof child === "string" ? child : xmlTree(ns, child));
248     }
249   }
250   return element;
253 // __htmlTree(data)__.
254 // Takes a data structure representing html elements like
255 // [tag, { attr-key: attr-value }, ...html-children]
256 // and returns nested html element objects.
257 let htmlTree = data => xmlTree("http://www.w3.org/1999/xhtml", data);
259 // __appendHtml(parent, data)__.
260 // Takes a data structure representing html elements like
261 // [tag, { attr-key: attr-value }, ...html-children]
262 // and appends nested html element objects to the parent element.
263 let appendHtml = (parent, data) => parent.appendChild(htmlTree(data));
265 // __circuitCircuitData()__.
266 // Obtains the circuit used by the given browser.
267 let currentCircuitData = function (browser) {
268   if (browser) {
269     let firstPartyDomain = getDomainForBrowser(browser);
270     let domain = firstPartyDomain || "--unknown--";
271     let domainMap = browserToCredentialsMap.get(browser);
272     let credentials = domainMap && domainMap.get(domain);
273     if (credentials) {
274       let [SOCKS_username, SOCKS_password] = credentials;
275       let nodeData = credentialsToNodeDataMap.get(`${SOCKS_username}|${SOCKS_password}`);
276       let domain = SOCKS_username;
277       return { domain, nodeData };
278     }
279   }
280   return { domain: null, nodeData: null };
283 // __updateCircuitDisplay()__.
284 // Updates the Tor circuit display, showing the current domain
285 // and the relay nodes for that domain.
286 let updateCircuitDisplay = function () {
287   let { domain, nodeData } = currentCircuitData(gBrowser.selectedBrowser);
288   if (domain && nodeData) {
289     // Update the displayed information for the relay nodes.
290     let nodeHtmlList = document.getElementById("circuit-display-nodes");
291     let li = (...data) => appendHtml(nodeHtmlList, ["li", {}, ...data]);
292     nodeHtmlList.innerHTML = "";
293     li(uiString("this_browser"));
294     for (let i = 0; i < nodeData.length; ++i) {
295       let relayText;
296       if (nodeData[i].type === "bridge") {
297         relayText = uiString("tor_bridge");
298         let bridgeType = nodeData[i].bridgeType;
299         if (bridgeType === "meek_lite") {
300           relayText += ": meek";
301         }
302         else if (bridgeType !== "vanilla" && bridgeType !== "") {
303           relayText += ": " + bridgeType;
304         }
305       } else if (nodeData[i].type == "default") {
306         relayText = localizedCountryNameFromCode(nodeData[i].countryCode);
307       }
308       let ip = nodeData[i].ip.startsWith("0.") ? "" : nodeData[i].ip;
309       li(relayText, " ", ["span", { class: "circuit-ip-address" }, ip], " ",
310          (i === 0 && nodeData[0].type !== "bridge") ?
311            ["span", { class: "circuit-guard-info" }, uiString("guard")] : null);
312     }
314     let domainParts = [];
315     if (domain.endsWith(".onion")) {
316       for (let i = 0; i < 3; ++i) {
317         li(uiString("relay"));
318       }
319       if (domain.length > 22) {
320         domainParts.push(domain.slice(0, 7), "…", domain.slice(-12));
321       } else {
322         domainParts.push(domain);
323       }
324     } else {
325       domainParts.push(domain);
326     }
328     // We use a XUL html:span element so that the tooltiptext is displayed.
329     li([
330       "html:span",
331       {
332         class: "circuit-onion",
333         onclick: `
334           this.classList.add("circuit-onion-copied");
335           Cc[
336             "@mozilla.org/widget/clipboardhelper;1"
337           ].getService(Ci.nsIClipboardHelper).copyString(this.getAttribute("data-onion"))
338         `,
339         "data-onion": domain,
340         "data-text-clicktocopy": torbutton_get_property_string("torbutton.circuit_display.click_to_copy"),
341         "data-text-copied": torbutton_get_property_string("torbutton.circuit_display.copied"),
342         tooltiptext: domain,
343       },
344       ...domainParts,
345     ]);
347     // Hide the note about guards if we are using a bridge.
348     document.getElementById("circuit-guard-note-container").style.display =
349       (nodeData[0].type === "bridge") ? "none" : "block";
350   } else {
351     // Only show the Tor circuit if we have credentials and node data.
352     logger.eclog(4, "no SOCKS credentials found for current document.");
353   }
354   showCircuitDisplay(domain && nodeData);
357 // __syncDisplayWithSelectedTab(syncOn)__.
358 // Whenever the user starts to open the popup menu, make sure the display
359 // is the correct one for this tab. It's also possible that a new site
360 // can be loaded while the popup menu is open.
361 // Update the display if this happens.
362 let syncDisplayWithSelectedTab = (function() {
363   let listener = { onLocationChange : function (aBrowser) {
364                       if (aBrowser === gBrowser.selectedBrowser) {
365                         updateCircuitDisplay();
366                       }
367                     } };
368   return function (syncOn) {
369     let popupMenu = document.getElementById("identity-popup");
370     if (syncOn) {
371       // Update the circuit display just before the popup menu is shown.
372       popupMenu.addEventListener("popupshowing", updateCircuitDisplay);
373       // If the currently selected tab has been sent to a new location,
374       // update the circuit to reflect that.
375       gBrowser.addTabsProgressListener(listener);
376     } else {
377       // Stop syncing.
378       gBrowser.removeTabsProgressListener(listener);
379       popupMenu.removeEventListener("popupshowing", updateCircuitDisplay);
380       // Hide the display.
381       showCircuitDisplay(false);
382     }
383   };
384 })();
386 // __setupGuardNote()__.
387 // Call once to show the Guard note as intended.
388 let setupGuardNote = function () {
389   let guardNote = document.getElementById("circuit-guard-note-container");
390   let guardNoteString = uiString("guard_note");
391   let learnMoreString = uiString("learn_more");
392   let [noteBefore, name, noteAfter] = guardNoteString.split(/[\[\]]/);
393   let localeCode = getLocale();
394   appendHtml(guardNote,
395              ["div", {},
396               noteBefore, ["span", {class: "circuit-guard-name"}, name],
397               noteAfter, " ",
398               ["span", {onclick: `gBrowser.selectedTab = gBrowser.addWebTab('https://support.torproject.org/${localeCode}/tbb/tbb-2/');`,
399                         class: "circuit-link"},
400                learnMoreString]]);
403 // __ensureCorrectPopupDimensions()__.
404 // Make sure the identity popup always displays with the correct height.
405 let ensureCorrectPopupDimensions = function () {
406   let setDimensions = () => {
407     setTimeout(() => {
408       let view = document.querySelector("#identity-popup-multiView .panel-viewcontainer");
409       let stack = document.querySelector("#identity-popup-multiView .panel-viewstack");
410       let view2 = document.getElementById("identity-popup-mainView");
411       if (view && stack && view2) {
412         let newWidth = Math.max(...[...view2.children].map(el => el.clientWidth));
413         let newHeight = stack.clientHeight;
414         stack.setAttribute("width", newWidth);
415         view2.style.minWidth = view2.style.maxWidth = newWidth + "px";
416         view.setAttribute("width", newWidth);
417         view.setAttribute("height", newHeight);
418       }
419     }, 0);
420   };
421   let removeDimensions = () => {
422     let view = document.querySelector("#identity-popup-multiView .panel-viewcontainer");
423     let stack = document.querySelector("#identity-popup-multiView .panel-viewstack");
424     let view2 = document.getElementById("identity-popup-mainView");
425     if (view && stack && view2) {
426       view.removeAttribute("width");
427       view.removeAttribute("height");
428       stack.removeAttribute("width");
429       view2.style.minWidth = view2.style.maxWidth = "";
430     }
431   };
432   let popupMenu = document.getElementById("identity-popup");
433   popupMenu.addEventListener("popupshowing", setDimensions);
434   popupMenu.addEventListener("popuphiding", removeDimensions);
435   return () => {
436     popupMenu.removeEventListener("popupshowing", setDimensions);
437     popupMenu.removeEventListener("popuphiding", removeDimensions);
438   };
441 // ## Main function
443 // __setupDisplay(enablePrefName)__.
444 // Once called, the Tor circuit display will be started whenever
445 // the "enablePref" is set to true, and stopped when it is set to false.
446 // A reference to this function (called createTorCircuitDisplay) is exported as a global.
447 let setupDisplay = function (enablePrefName) {
448   // From 79 on the identity popup is initialized lazily
449   if (gIdentityHandler._initializePopup) {
450     gIdentityHandler._initializePopup();
451   }
452   setupGuardNote();
453   let myController = null,
454       stopCollectingIsolationData = null,
455       stopCollectingBrowserCredentials = null,
456       stopEnsuringCorrectPopupDimensions = null,
457       stop = function() {
458         syncDisplayWithSelectedTab(false);
459         if (myController) {
460           if (stopCollectingIsolationData) {
461             stopCollectingIsolationData();
462           }
463           if (stopCollectingBrowserCredentials) {
464             stopCollectingBrowserCredentials();
465           }
466           if (stopEnsuringCorrectPopupDimensions) {
467             stopEnsuringCorrectPopupDimensions();
468           }
469           myController = null;
470         }
471       },
472       start = function () {
473         if (!myController) {
474           myController = controller(function (err) {
475             // An error has occurred.
476             logger.eclog(5, err);
477             logger.eclog(5, "Disabling tor display circuit because of an error.");
478             myController.close();
479             stop();
480           });
481           syncDisplayWithSelectedTab(true);
482           stopCollectingIsolationData = collectIsolationData(myController, updateCircuitDisplay);
483           stopCollectingBrowserCredentials = collectBrowserCredentials();
484           stopEnsuringCorrectPopupDimensions = ensureCorrectPopupDimensions();
485        }
486      };
487   try {
488     let unbindPref = bindPrefAndInit(enablePrefName, on => { if (on) start(); else stop(); });
489     // When this chrome window is unloaded, we need to unbind the pref.
490     window.addEventListener("unload", function () {
491       unbindPref();
492       stop();
493     });
494   } catch (e) {
495     logger.eclog(5, "Error: " + e.message + "\n" + e.stack);
496   }
499 return setupDisplay;
501 // Finish createTorCircuitDisplay()
502 })();