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 { 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 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) {
78 bridge = await getBridge(controller, id); // type, ip, countryCode;
80 result.type = "bridge";
81 result.bridgeType = bridge.type;
82 // Attempt to get an IP address from bridge address string.
84 result.ip = bridge.address.split(":")[0];
87 // either dealing with a relay, or a bridge whose fingerprint is not saved in torrc
89 let statusMap = await controller.getInfo("ns/id/" + id);
90 result.type = "default";
91 result.ip = statusMap.IP;
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
98 result.bridgeType = "";
102 // Get the country code for the node's IP address.
104 let countryCode = await controller.getInfo("ip-to-country/" + result.ip);
105 result.countryCode = countryCode === "??" ? null : countryCode;
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;
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(
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)) :
157 let nodeData = await nodeDataForCircuit(aController, circuitStatus);
158 credentialsToNodeDataMap.set(credentials, nodeData);
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;
174 loadContext = callbacks.getInterface(Ci.nsILoadContext);
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 => {
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());
195 let domainMap = browserToCredentialsMap.get(browser);
196 domainMap.set(proxyInfo.username, [proxyInfo.username,
197 proxyInfo.password]);
200 logger.eclog(3, `Error collecting browser credentials: ${e.message}, ${chan.URI.spec}`);
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");
219 return Services.intl.getRegionDisplayNames(undefined, [countryCode])[0];
221 return countryCode.toUpperCase();
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 ?
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);
245 for (let child of children) {
246 if (child !== null && child !== undefined) {
247 element.append(typeof child === "string" ? child : xmlTree(ns, child));
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) {
269 let firstPartyDomain = getDomainForBrowser(browser);
270 let domain = firstPartyDomain || "--unknown--";
271 let domainMap = browserToCredentialsMap.get(browser);
272 let credentials = domainMap && domainMap.get(domain);
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 };
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) {
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";
302 else if (bridgeType !== "vanilla" && bridgeType !== "") {
303 relayText += ": " + bridgeType;
305 } else if (nodeData[i].type == "default") {
306 relayText = localizedCountryNameFromCode(nodeData[i].countryCode);
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);
314 let domainParts = [];
315 if (domain.endsWith(".onion")) {
316 for (let i = 0; i < 3; ++i) {
317 li(uiString("relay"));
319 if (domain.length > 22) {
320 domainParts.push(domain.slice(0, 7), "…", domain.slice(-12));
322 domainParts.push(domain);
325 domainParts.push(domain);
328 // We use a XUL html:span element so that the tooltiptext is displayed.
332 class: "circuit-onion",
334 this.classList.add("circuit-onion-copied");
336 "@mozilla.org/widget/clipboardhelper;1"
337 ].getService(Ci.nsIClipboardHelper).copyString(this.getAttribute("data-onion"))
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"),
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";
351 // Only show the Tor circuit if we have credentials and node data.
352 logger.eclog(4, "no SOCKS credentials found for current document.");
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();
368 return function (syncOn) {
369 let popupMenu = document.getElementById("identity-popup");
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);
378 gBrowser.removeTabsProgressListener(listener);
379 popupMenu.removeEventListener("popupshowing", updateCircuitDisplay);
381 showCircuitDisplay(false);
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,
396 noteBefore, ["span", {class: "circuit-guard-name"}, name],
398 ["span", {onclick: `gBrowser.selectedTab = gBrowser.addWebTab('https://support.torproject.org/${localeCode}/tbb/tbb-2/');`,
399 class: "circuit-link"},
403 // __ensureCorrectPopupDimensions()__.
404 // Make sure the identity popup always displays with the correct height.
405 let ensureCorrectPopupDimensions = function () {
406 let setDimensions = () => {
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);
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 = "";
432 let popupMenu = document.getElementById("identity-popup");
433 popupMenu.addEventListener("popupshowing", setDimensions);
434 popupMenu.addEventListener("popuphiding", removeDimensions);
436 popupMenu.removeEventListener("popupshowing", setDimensions);
437 popupMenu.removeEventListener("popuphiding", removeDimensions);
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();
453 let myController = null,
454 stopCollectingIsolationData = null,
455 stopCollectingBrowserCredentials = null,
456 stopEnsuringCorrectPopupDimensions = null,
458 syncDisplayWithSelectedTab(false);
460 if (stopCollectingIsolationData) {
461 stopCollectingIsolationData();
463 if (stopCollectingBrowserCredentials) {
464 stopCollectingBrowserCredentials();
466 if (stopEnsuringCorrectPopupDimensions) {
467 stopEnsuringCorrectPopupDimensions();
472 start = function () {
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();
481 syncDisplayWithSelectedTab(true);
482 stopCollectingIsolationData = collectIsolationData(myController, updateCircuitDisplay);
483 stopCollectingBrowserCredentials = collectBrowserCredentials();
484 stopEnsuringCorrectPopupDimensions = ensureCorrectPopupDimensions();
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 () {
495 logger.eclog(5, "Error: " + e.message + "\n" + e.stack);
501 // Finish createTorCircuitDisplay()