Bug 40580: Add support for uk (ukranian) locale
[torbutton.git] / chrome / content / tor-circuit-display.js
blob14e3da5b85029ca4d23cda75e133a65466f63184
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 { wait_for_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;
69 return null;
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+$/
80 if (bridge) {
81 result.type = "bridge";
82 result.bridgeType = bridge.type;
83 // Attempt to get an IP address from bridge address string.
84 try {
85 const ip = bridge.address.match(addrRe)[1];
86 if (!ip.startsWith("0.")) {
87 result.ipAddrs = [ip];
89 } catch (e) {
91 } else {
92 // either dealing with a relay, or a bridge whose fingerprint is not saved in torrc
93 try {
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);
99 try {
100 result.ipAddrs.push(statusMap.IPv6.match(addrRe)[1]);
101 } catch (e) {
103 } catch (e) {
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
108 result.ipAddrs = [];
109 result.bridgeType = "";
112 if (result.ipAddrs.length > 0) {
113 // Get the country code for the node's IP address.
114 try {
115 const countryCode = await controller.getInfo("ip-to-country/" + result.ipAddrs[0]);
116 result.countryCode = countryCode === "??" ? null : countryCode;
117 } catch (e) { }
119 return result;
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;
143 return null;
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(
156 "STREAM",
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)) :
166 null;
167 if (credentials) {
168 let nodeData = await nodeDataForCircuit(aController, circuitStatus);
169 credentialsToNodeDataMap.set(credentials, nodeData);
170 updateUI();
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;
183 let loadContext;
184 try {
185 loadContext = callbacks.getInterface(Ci.nsILoadContext);
186 } catch (e) {
187 // Ignore
188 return null;
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 => {
199 try {
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]);
210 } catch (e) {
211 logger.eclog(3, `Error collecting browser credentials: ${e.message}, ${chan.URI.spec}`);
216 // ## User interface
218 // __uiString__.
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");
229 try {
230 return Services.intl.getRegionDisplayNames(undefined, [countryCode])[0];
231 } catch (e) {
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 ?
240 'block' : 'none';
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));
261 return element;
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) {
279 if (browser) {
280 let firstPartyDomain = getDomainForBrowser(browser);
281 let domain = firstPartyDomain || "--unknown--";
282 let domainMap = browserToCredentialsMap.get(browser);
283 let credentials = domainMap && domainMap.get(domain);
284 if (credentials) {
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) {
312 let relayText;
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));
338 } else {
339 domainParts.push(domain);
341 } else {
342 domainParts.push(domain);
345 // We use a XUL html:span element so that the tooltiptext is displayed.
346 li([
347 "html:span",
349 class: "circuit-onion",
350 onclick: `
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"),
359 tooltiptext: domain,
361 ...domainParts,
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";
367 } else {
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();
384 } };
385 return function (syncOn) {
386 let popupMenu = document.getElementById("identity-popup");
387 if (syncOn) {
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);
393 } else {
394 // Stop syncing.
395 gBrowser.removeTabsProgressListener(listener);
396 popupMenu.removeEventListener("popupshowing", updateCircuitDisplay);
397 // Hide the display.
398 showCircuitDisplay(false);
401 })();
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,
412 ["div", {},
413 noteBefore, ["span", {class: "circuit-guard-name"}, name],
414 noteAfter, " ",
415 ["span", {onclick: `gBrowser.selectedTab = gBrowser.addWebTab('https://support.torproject.org/${localeCode}/tbb/tbb-2/');`,
416 class: "circuit-link"},
417 learnMoreString]]);
420 // __ensureCorrectPopupDimensions()__.
421 // Make sure the identity popup always displays with the correct height.
422 let ensureCorrectPopupDimensions = function () {
423 let setDimensions = () => {
424 setTimeout(() => {
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);
436 }, 0);
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);
452 return () => {
453 popupMenu.removeEventListener("popupshowing", setDimensions);
454 popupMenu.removeEventListener("popuphiding", removeDimensions);
458 // ## Main function
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();
469 setupGuardNote();
470 let myController = null,
471 stopCollectingIsolationData = null,
472 stopCollectingBrowserCredentials = null,
473 stopEnsuringCorrectPopupDimensions = null,
474 stop = function() {
475 syncDisplayWithSelectedTab(false);
476 if (myController) {
477 if (stopCollectingIsolationData) {
478 stopCollectingIsolationData();
480 if (stopCollectingBrowserCredentials) {
481 stopCollectingBrowserCredentials();
483 if (stopEnsuringCorrectPopupDimensions) {
484 stopEnsuringCorrectPopupDimensions();
486 myController = null;
489 start = async function () {
490 if (!myController) {
491 try {
492 myController = await wait_for_controller();
493 syncDisplayWithSelectedTab(true);
494 stopCollectingIsolationData = collectIsolationData(myController, updateCircuitDisplay);
495 stopCollectingBrowserCredentials = collectBrowserCredentials();
496 stopEnsuringCorrectPopupDimensions = ensureCorrectPopupDimensions();
497 } catch (err) {
498 logger.eclog(5, err);
499 logger.eclog(5, "Disabling tor display circuit because of an error.");
500 myController.close();
501 stop();
505 try {
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 () {
509 unbindPref();
510 stop();
512 } catch (e) {
513 logger.eclog(5, "Error: " + e.message + "\n" + e.stack);
517 return setupDisplay;
519 // Finish createTorCircuitDisplay()
520 })();