This is an automated email from the git hooks/post-receive script.
pierov pushed a commit to branch tor-browser-102.5.0esr-12.5-1 in repository tor-browser.
commit e1eb5ce81dc8f0bd786597415a0069c3f603c74c Author: Alex Catarineu acat@torproject.org AuthorDate: Wed Feb 19 23:05:08 2020 +0100
Bug 10760: Integrate TorButton to TorBrowser core
Because of the non-restartless nature of Torbutton, it required a two-stage installation process. On mobile, it was a problem, because it was not loading when the user opened the browser for the first time.
Moving it to tor-browser and making it a system extension allows it to load when the user opens the browser for first time.
Additionally, this patch also fixes Bug 27611.
Bug 26321: New Circuit and New Identity menu items
Bug 14392: Make about:tor behave like other initial pages.
Bug 25013: Add torbutton as a tor-browser submodule
Bug 31575: Replace Firefox Home (newtab) with about:tor
Avoid loading AboutNewTab in BrowserGlue.jsm in order to avoid several network requests that we do not need. Besides, about:newtab will now point to about:blank or about:tor (depending on browser.newtabpage.enabled) and about:home will point to about:tor. --- browser/base/content/aboutDialog.xhtml | 40 +- browser/base/content/appmenu-viewcache.inc.xhtml | 11 +- browser/base/content/browser-doctype.inc | 8 + browser/base/content/browser-menubar.inc | 24 +- browser/base/content/browser-sets.inc | 1 + browser/base/content/browser.js | 1 + browser/base/content/browser.xhtml | 13 + browser/base/content/hiddenWindowMac.xhtml | 4 + browser/base/content/navigator-toolbox.inc.xhtml | 5 + browser/base/content/pageinfo/pageInfo.xhtml | 6 + browser/components/BrowserGlue.jsm | 33 +- .../controlcenter/content/identityPanel.inc.xhtml | 22 + browser/components/newtab/AboutNewTabService.jsm | 15 +- browser/components/places/content/places.xhtml | 3 + browser/components/preferences/home.inc.xhtml | 4 +- browser/components/preferences/preferences.xhtml | 5 +- .../shell/content/setDesktopBackground.xhtml | 6 + browser/installer/package-manifest.in | 2 + browser/modules/HomePage.jsm | 2 +- browser/themes/shared/icons/new_circuit.svg | 6 + browser/themes/shared/jar.inc.mn | 2 + browser/themes/shared/toolbarbutton-icons.css | 4 + docshell/base/nsAboutRedirector.cpp | 6 +- docshell/build/components.conf | 1 + mobile/android/installer/package-manifest.in | 4 + toolkit/moz.build | 1 + toolkit/torbutton/.gitignore | 1 + toolkit/torbutton/CREDITS | 5 + toolkit/torbutton/LICENSE | 29 + .../chrome/content/aboutTor/aboutTor-content.js | 139 +++ .../chrome/content/aboutTor/aboutTor.xhtml | 112 +++ .../chrome/content/aboutTor/resources/aboutTor.js | 11 + .../torbutton/chrome/content/preferences-mobile.js | 59 ++ toolkit/torbutton/chrome/content/preferences.xhtml | 84 ++ .../chrome/content/tor-circuit-display.js | 604 +++++++++++ toolkit/torbutton/chrome/content/torbutton.js | 1044 ++++++++++++++++++++ toolkit/torbutton/chrome/skin/about-wordmark.png | Bin 0 -> 4609 bytes toolkit/torbutton/chrome/skin/aboutDialog.css | 34 + toolkit/torbutton/chrome/skin/aboutTor.css | 313 ++++++ toolkit/torbutton/chrome/skin/banner-warning.svg | 1 + toolkit/torbutton/chrome/skin/dax-logo.svg | 1 + toolkit/torbutton/chrome/skin/icon-newsletter.png | Bin 0 -> 649 bytes .../torbutton/chrome/skin/preferences-mobile.css | 47 + toolkit/torbutton/chrome/skin/preferences.css | 7 + .../torbutton/chrome/skin/tor-circuit-display.css | 193 ++++ .../chrome/skin/tor-circuit-line-first.svg | 6 + .../chrome/skin/tor-circuit-line-last.svg | 6 + toolkit/torbutton/chrome/skin/tor-circuit-line.svg | 7 + toolkit/torbutton/chrome/skin/tor.png | Bin 0 -> 2073 bytes .../chrome/skin/torbrowser_mobile_logo.png | Bin 0 -> 9345 bytes toolkit/torbutton/chrome/skin/torbutton.css | 14 + toolkit/torbutton/chrome/skin/torbutton.svg | 3 + toolkit/torbutton/components/domain-isolator.js | 228 +++++ toolkit/torbutton/components/dragDropFilter.js | 134 +++ .../torbutton/components/external-app-blocker.js | 160 +++ toolkit/torbutton/components/startup-observer.js | 196 ++++ toolkit/torbutton/components/torCheckService.js | 140 +++ toolkit/torbutton/components/torbutton-logger.js | 185 ++++ toolkit/torbutton/jar.mn | 46 + toolkit/torbutton/modules/tor-control-port.js | 982 ++++++++++++++++++ toolkit/torbutton/modules/utils.js | 318 ++++++ toolkit/torbutton/moz.build | 6 + .../lib/environments/browser-window.js | 6 +- 63 files changed, 5284 insertions(+), 66 deletions(-)
diff --git a/browser/base/content/aboutDialog.xhtml b/browser/base/content/aboutDialog.xhtml index 90a568a17dd6..60b1e15b637c 100644 --- a/browser/base/content/aboutDialog.xhtml +++ b/browser/base/content/aboutDialog.xhtml @@ -7,6 +7,12 @@ <?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/content/aboutDialog.css" type="text/css"?> <?xml-stylesheet href="chrome://branding/content/aboutDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://torbutton/skin/aboutDialog.css" type="text/css"?> + +<!-- We need to include the localization DTDs until we migrate to Fluent --> +<!DOCTYPE window [ +#include browser-doctype.inc +]>
<window xmlns:html="http://www.w3.org/1999/xhtml" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" @@ -22,7 +28,7 @@ data-l10n-id="aboutDialog-title" #endif role="dialog" - aria-describedby="version distribution distributionId communityDesc contributeDesc trademark" + aria-describedby="version distribution distributionId projectDesc helpDesc trademark trademarkTor" > #ifdef XP_MACOSX #include macWindow.inc.xhtml @@ -140,24 +146,36 @@ <label is="text-link" useoriginprincipal="true" href="about:credits" data-l10n-name="community-exp-creditsLink"/> </description> </vbox> - <description class="text-blurb" id="communityDesc" data-l10n-id="community-2"> - <label is="text-link" href="https://www.mozilla.org/?utm_source=firefox-browser&utm_medium=firefox-desktop&utm_campaign=about-dialog" data-l10n-name="community-mozillaLink"/> - <label is="text-link" useoriginprincipal="true" href="about:credits" data-l10n-name="community-creditsLink"/> + <!-- Keep communityDesc and contributeDesc to avoid JS errors trying to hide them --> + <description class="text-blurb" id="communityDesc" data-l10n-id="community-2" hidden="true"></description> + <description class="text-blurb" id="contributeDesc" data-l10n-id="helpus" hidden="true"></description> + <description class="text-blurb" id="projectDesc"> + &project.start; + <label is="text-link" href="https://www.torproject.org/"> + &project.tpoLink; + </label>&project.end; </description> - <description class="text-blurb" id="contributeDesc" data-l10n-id="helpus"> - <label is="text-link" href="https://donate.mozilla.org/?utm_source=firefox&utm_medium=referral&utm_campaign=firefox_about&utm_content=firefox_about" data-l10n-name="helpus-donateLink"/> - <label is="text-link" href="https://www.mozilla.org/contribute/?utm_source=firefox-browser&utm_medium=firefox-desktop&utm_campaign=about-dialog" data-l10n-name="helpus-getInvolvedLink"/> + <description class="text-blurb" id="helpDesc"> + &help.start; + <label is="text-link" href="https://donate.torproject.org/"> + &help.donateLink; + </label> + &help.or; + <label is="text-link" href="https://community.torproject.org/"> + &help.getInvolvedLink; + </label>&help.end; </description> </vbox> </vbox> </hbox> <vbox id="bottomBox"> - <hbox pack="center"> - <label is="text-link" class="bottom-link" useoriginprincipal="true" href="about:license" data-l10n-id="bottomLinks-license"/> - <label is="text-link" class="bottom-link" useoriginprincipal="true" href="about:rights" data-l10n-id="bottomLinks-rights"/> - <label is="text-link" class="bottom-link" href="https://www.mozilla.org/privacy/?utm_source=firefox-browser&utm_medium=firefox-desktop&utm_campaign=about-dialog" data-l10n-id="bottomLinks-privacy"/> + <hbox id="newBottom" pack="center" position="1"> + <label is="text-link" class="bottom-link" href="https://support.torproject.org/">&bottomLinks.questions;</label> + <label is="text-link" class="bottom-link" href="https://community.torproject.org/relay/">&bottomLinks.grow;</label> + <label is="text-link" class="bottom-link" useoriginprincipal="true" href="about:license">&bottomLinks.license;</label> </hbox> <description id="trademark" data-l10n-id="trademarkInfo"></description> + <description id="trademarkTor">&tor.TrademarkStatement;</description> </vbox> </vbox>
diff --git a/browser/base/content/appmenu-viewcache.inc.xhtml b/browser/base/content/appmenu-viewcache.inc.xhtml index c138be3ee86c..cb685a3d0f52 100644 --- a/browser/base/content/appmenu-viewcache.inc.xhtml +++ b/browser/base/content/appmenu-viewcache.inc.xhtml @@ -55,11 +55,17 @@ class="subviewbutton" data-l10n-id="appmenuitem-new-private-window" key="key_privatebrowsing" - command="Tools:PrivateBrowsing"/> + command="Tools:PrivateBrowsing" + hidden="true"/> <toolbarseparator/> <toolbarbutton id="appMenu-new-identity" class="subviewbutton" key="new-identity-key"/> + <toolbarbutton id="appMenuNewCircuit" + class="subviewbutton" + key="torbutton-new-circuit-key" + label="&torbutton.context_menu.new_circuit_sentence_case;" + oncommand="torbutton_new_circuit();"/> <toolbarseparator/> <toolbarbutton id="appMenu-bookmarks-button" class="subviewbutton subviewbutton-nav" @@ -179,7 +185,8 @@ <toolbarbutton id="appMenu-restoreSession" data-l10n-id="appmenu-restore-session" class="subviewbutton" - command="Browser:RestoreLastSession"/> + command="Browser:RestoreLastSession" + hidden="true"/> <toolbarseparator/> <toolbarbutton id="appMenuClearRecentHistory" data-l10n-id="appmenu-clear-history" diff --git a/browser/base/content/browser-doctype.inc b/browser/base/content/browser-doctype.inc new file mode 100644 index 000000000000..fdc302e79de3 --- /dev/null +++ b/browser/base/content/browser-doctype.inc @@ -0,0 +1,8 @@ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % torbuttonDTD SYSTEM "chrome://torbutton/locale/torbutton.dtd"> +%torbuttonDTD; +<!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd"> +%aboutTorDTD; +<!ENTITY % aboutDialogDTD SYSTEM "chrome://torbutton/locale/aboutDialog.dtd"> +%aboutDialogDTD; diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc index 7c3dd4fe6d25..cde4d205b763 100644 --- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -31,6 +31,11 @@ <menuseparator/> <menuitem id="menu_newIdentity" key="new-identity-key"/> + <menuitem id="menu_newCircuit" + accesskey="&torbutton.context_menu.new_circuit_key;" + key="torbutton-new-circuit-key" + label="&torbutton.context_menu.new_circuit;" + oncommand="torbutton_new_circuit();"/> <menuseparator/> <menuitem id="menu_openLocation" hidden="true" @@ -456,8 +461,20 @@ <menupopup id="menu_HelpPopup" onpopupshowing="buildHelpMenu();"> <!-- Note: Items under here are cloned to the AppMenu Help submenu. The cloned items have their strings defined by appmenu-data-l10n-id. --> + <!-- dummy elements to avoid 'getElementById' errors --> + <box id="feedbackPage"/> + <box id="helpSafeMode"/> + <box id="menu_HelpPopup_reportPhishingtoolmenu"/> + <box id="menu_HelpPopup_reportPhishingErrortoolmenu"/> + <!-- Add Tor Browser manual link --> + <menuitem id="torBrowserUserManual" + oncommand="gBrowser.selectedTab = gBrowser.addTab('about:manual', {triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal()});" + label="&aboutTor.torbrowser_user_manual.label;" + accesskey="&aboutTor.torbrowser_user_manual.accesskey;"/> + <!-- Bug 18905: Hide unused help menu items --> <menuitem id="menu_openHelp" oncommand="openHelpLink('firefox-help')" + hidden="true" data-l10n-id="menu-get-help" appmenu-data-l10n-id="appmenu-get-help" #ifdef XP_MACOSX @@ -467,14 +484,17 @@ #endif <menuitem id="feedbackPage" oncommand="openFeedbackPage()" - data-l10n-id="menu-help-share-ideas" - appmenu-data-l10n-id="appmenu-help-share-ideas"/> + hidden="true" + data-l10n-id="menu-help-feedback-page" + appmenu-data-l10n-id="appmenu-help-feedback-page"/> <menuitem id="helpSafeMode" oncommand="safeModeRestart();" + hidden="true" data-l10n-id="menu-help-enter-troubleshoot-mode2" appmenu-data-l10n-id="appmenu-help-enter-troubleshoot-mode2"/> <menuitem id="troubleShooting" oncommand="openTroubleshootingPage()" + hidden="true" data-l10n-id="menu-help-more-troubleshooting-info" appmenu-data-l10n-id="appmenu-help-more-troubleshooting-info"/> <menuitem id="help_reportSiteIssue" diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc index 17edb35baf6a..dc5d93978950 100644 --- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -389,4 +389,5 @@ internal="true"/> #endif <key id="new-identity-key" modifiers="accel shift" key="U" oncommand="NewIdentityButton.onCommand(event)"/> + <key id="torbutton-new-circuit-key" modifiers="accel shift" key="L" oncommand="torbutton_new_circuit()"/> </keyset> diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index ee392d55b572..597bff687349 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -645,6 +645,7 @@ var gPageIcons = { };
var gInitialPages = [ + "about:tor", "about:blank", "about:home", ...(AppConstants.NIGHTLY_BUILD ? ["about:firefoxview"] : []), diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml index df33143a7365..5959615f9dc1 100644 --- a/browser/base/content/browser.xhtml +++ b/browser/base/content/browser.xhtml @@ -35,6 +35,12 @@ <?xml-stylesheet href="chrome://browser/skin/searchbar.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/places/editBookmark.css" type="text/css"?> +<?xml-stylesheet href="chrome://torbutton/skin/tor-circuit-display.css" type="text/css"?> +<?xml-stylesheet href="chrome://torbutton/skin/torbutton.css" type="text/css"?> + +<!DOCTYPE window [ +#include browser-doctype.inc +]>
<html id="main-window" xmlns:html="http://www.w3.org/1999/xhtml" @@ -116,11 +122,18 @@ Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/languageNotification.js", this); + Services.scriptloader.loadSubScript("chrome://torbutton/content/tor-circuit-display.js", this); + Services.scriptloader.loadSubScript("chrome://torbutton/content/torbutton.js", this);
window.onload = gBrowserInit.onLoad.bind(gBrowserInit); window.onunload = gBrowserInit.onUnload.bind(gBrowserInit); window.onclose = WindowIsClosing;
+ //onLoad Handler + try { + window.addEventListener("load", torbutton_init, false); + } catch (e) {} + window.addEventListener("MozBeforeInitialXULLayout", gBrowserInit.onBeforeInitialXULLayout.bind(gBrowserInit), { once: true });
diff --git a/browser/base/content/hiddenWindowMac.xhtml b/browser/base/content/hiddenWindowMac.xhtml index c27d394d3707..b272a076bb30 100644 --- a/browser/base/content/hiddenWindowMac.xhtml +++ b/browser/base/content/hiddenWindowMac.xhtml @@ -8,6 +8,10 @@
<?xml-stylesheet href="chrome://browser/skin/webRTC-menubar-indicator.css" type="text/css"?>
+<!DOCTYPE window [ +#include browser-doctype.inc +]> + <window id="main-window" xmlns:html="http://www.w3.org/1999/xhtml" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml index cadf68c91679..6123e1336aed 100644 --- a/browser/base/content/navigator-toolbox.inc.xhtml +++ b/browser/base/content/navigator-toolbox.inc.xhtml @@ -539,6 +539,11 @@
<toolbarbutton id="new-identity-button" class="toolbarbutton-1 chromeclass-toolbar-additional"/>
+ <toolbarbutton id="new-circuit-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&torbutton.context_menu.new_circuit;" + oncommand="torbutton_new_circuit();" + tooltiptext="&torbutton.context_menu.new_circuit;"/> + <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional" observes="View:FullScreen" type="checkbox" diff --git a/browser/base/content/pageinfo/pageInfo.xhtml b/browser/base/content/pageinfo/pageInfo.xhtml index 4fe7f4909180..b2b09f753902 100644 --- a/browser/base/content/pageinfo/pageInfo.xhtml +++ b/browser/base/content/pageinfo/pageInfo.xhtml @@ -6,6 +6,12 @@ <?xml-stylesheet href="chrome://browser/content/pageinfo/pageInfo.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/pageInfo.css" type="text/css"?>
+<!DOCTYPE window [ +#ifdef XP_MACOSX +#include ../browser-doctype.inc +#endif +]> + <window id="main-window" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index 99b289bd934a..6d0947a464ba 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -18,7 +18,6 @@ const { AppConstants } = ChromeUtils.import( );
XPCOMUtils.defineLazyModuleGetters(this, { - AboutNewTab: "resource:///modules/AboutNewTab.jsm", ActorManagerParent: "resource://gre/modules/ActorManagerParent.jsm", AddonManager: "resource://gre/modules/AddonManager.jsm", AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm", @@ -225,28 +224,6 @@ let JSWINDOWACTORS = { remoteTypes: ["privilegedabout"], },
- AboutNewTab: { - parent: { - moduleURI: "resource:///actors/AboutNewTabParent.jsm", - }, - child: { - moduleURI: "resource:///actors/AboutNewTabChild.jsm", - events: { - DOMContentLoaded: {}, - pageshow: {}, - visibilitychange: {}, - }, - }, - // The wildcard on about:newtab is for the ?endpoint query parameter - // that is used for snippets debugging. The wildcard for about:home - // is similar, and also allows for falling back to loading the - // about:home document dynamically if an attempt is made to load - // about:home?jscache from the AboutHomeStartupCache as a top-level - // load. - matches: ["about:home*", "about:welcome", "about:newtab*"], - remoteTypes: ["privilegedabout"], - }, - AboutPlugins: { parent: { moduleURI: "resource:///actors/AboutPluginsParent.jsm", @@ -1599,8 +1576,6 @@ BrowserGlue.prototype = {
// the first browser window has finished initializing _onFirstWindowLoaded: function BG__onFirstWindowLoaded(aWindow) { - AboutNewTab.init(); - TabCrashHandler.init();
ProcessHangMonitor.init(); @@ -5792,12 +5767,8 @@ var AboutHomeStartupCache = { return { pageInputStream: null, scriptInputStream: null }; }
- let state = AboutNewTab.activityStream.store.getState(); - return new Promise(resolve => { - this._cacheDeferred = resolve; - this.log.trace("Parent is requesting cache streams."); - this._procManager.sendAsyncMessage(this.CACHE_REQUEST_MESSAGE, { state }); - }); + this.log.error("Activity Stream is disabled in Tor Browser."); + return { pageInputStream: null, scriptInputStream: null }; },
/** diff --git a/browser/components/controlcenter/content/identityPanel.inc.xhtml b/browser/components/controlcenter/content/identityPanel.inc.xhtml index ad6f9db340ef..498f374ffde8 100644 --- a/browser/components/controlcenter/content/identityPanel.inc.xhtml +++ b/browser/components/controlcenter/content/identityPanel.inc.xhtml @@ -92,6 +92,28 @@ </vbox> </hbox>
+ <!-- Circuit display section --> + + <vbox id="circuit-display-container" class="identity-popup-section"> + <toolbarseparator/> + <vbox id="circuit-display-header" flex="1" role="group" + aria-labelledby="circuit-display-headline"> + <hbox flex="1"> + <label id="circuit-display-headline" + role="heading" aria-level="2">&torbutton.circuit_display.title;</label> + </hbox> + </vbox> + <vbox id="circuit-display-content"> + <html:ul id="circuit-display-nodes" dir="auto"/> + <hbox id="circuit-guard-note-container"/> + <hbox id="circuit-reload-button-container"> + <html:button id="circuit-reload-button" + onclick="torbutton_new_circuit()" + default="true">&torbutton.circuit_display.new_circuit;</html:button> + </hbox> + </vbox> + </vbox> + <!-- Clear Site Data Button --> <vbox hidden="true" id="identity-popup-clear-sitedata-footer"> diff --git a/browser/components/newtab/AboutNewTabService.jsm b/browser/components/newtab/AboutNewTabService.jsm index f3bc40019f8f..471a3139baa7 100644 --- a/browser/components/newtab/AboutNewTabService.jsm +++ b/browser/components/newtab/AboutNewTabService.jsm @@ -420,20 +420,7 @@ class BaseAboutNewTabService { * the newtab page has no effect on the result of this function. */ get defaultURL() { - // Generate the desired activity stream resource depending on state, e.g., - // "resource://activity-stream/prerendered/activity-stream.html" - // "resource://activity-stream/prerendered/activity-stream-debug.html" - // "resource://activity-stream/prerendered/activity-stream-noscripts.html" - return [ - "resource://activity-stream/prerendered/", - "activity-stream", - // Debug version loads dev scripts but noscripts separately loads scripts - this.activityStreamDebug && !this.privilegedAboutProcessEnabled - ? "-debug" - : "", - this.privilegedAboutProcessEnabled ? "-noscripts" : "", - ".html", - ].join(""); + return "about:tor"; }
get welcomeURL() { diff --git a/browser/components/places/content/places.xhtml b/browser/components/places/content/places.xhtml index e19a58029828..f55d813664f6 100644 --- a/browser/components/places/content/places.xhtml +++ b/browser/components/places/content/places.xhtml @@ -18,6 +18,9 @@ <!DOCTYPE window [ <!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd"> %editMenuOverlayDTD; +#ifdef XP_MACOSX +#include ../../../base/content/browser-doctype.inc +#endif ]>
<window id="places" diff --git a/browser/components/preferences/home.inc.xhtml b/browser/components/preferences/home.inc.xhtml index 5bb936782ed9..e812d969837e 100644 --- a/browser/components/preferences/home.inc.xhtml +++ b/browser/components/preferences/home.inc.xhtml @@ -33,7 +33,7 @@ class="check-home-page-controlled" data-preference-related="browser.startup.homepage"> <menupopup> - <menuitem value="0" data-l10n-id="home-mode-choice-default" /> + <menuitem value="0" label="&aboutTor.title;" /> <menuitem value="2" data-l10n-id="home-mode-choice-custom" /> <menuitem value="1" data-l10n-id="home-mode-choice-blank" /> </menupopup> @@ -84,7 +84,7 @@ Preferences so we need to handle setting the pref manually.--> <menulist id="newTabMode" flex="1" data-preference-related="browser.newtabpage.enabled"> <menupopup> - <menuitem value="0" data-l10n-id="home-mode-choice-default" /> + <menuitem value="0" label="&aboutTor.title;" /> <menuitem value="1" data-l10n-id="home-mode-choice-blank" /> </menupopup> </menulist> diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml index 8706870466fa..f1a8115843a3 100644 --- a/browser/components/preferences/preferences.xhtml +++ b/browser/components/preferences/preferences.xhtml @@ -15,7 +15,10 @@ <?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?> <?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?>
-<!DOCTYPE html> +<!DOCTYPE html [ +<!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd"> + %aboutTorDTD; +]>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:html="http://www.w3.org/1999/xhtml" diff --git a/browser/components/shell/content/setDesktopBackground.xhtml b/browser/components/shell/content/setDesktopBackground.xhtml index a7d4ced792b6..ef102450ff2c 100644 --- a/browser/components/shell/content/setDesktopBackground.xhtml +++ b/browser/components/shell/content/setDesktopBackground.xhtml @@ -7,6 +7,12 @@ <?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/setDesktopBackground.css" type="text/css"?>
+<!DOCTYPE window [ +#ifdef XP_MACOSX +#include ../../../base/content/browser-doctype.inc +#endif +]> + <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" windowtype="Shell:SetDesktopBackground" diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index cb7891d5b43e..04acc38fa5cf 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -235,6 +235,8 @@ @RESPATH@/chrome/pdfjs.manifest @RESPATH@/chrome/pdfjs/* @RESPATH@/components/tor-launcher.manifest +@RESPATH@/chrome/torbutton.manifest +@RESPATH@/chrome/torbutton/* @RESPATH@/chrome/toolkit@JAREXT@ @RESPATH@/chrome/toolkit.manifest #ifdef MOZ_GTK diff --git a/browser/modules/HomePage.jsm b/browser/modules/HomePage.jsm index f73b0f3e6c8c..26618374df3a 100644 --- a/browser/modules/HomePage.jsm +++ b/browser/modules/HomePage.jsm @@ -21,7 +21,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { });
const kPrefName = "browser.startup.homepage"; -const kDefaultHomePage = "about:home"; +const kDefaultHomePage = "about:tor"; const kExtensionControllerPref = "browser.startup.homepage_override.extensionControlled"; const kHomePageIgnoreListId = "homepage-urls"; diff --git a/browser/themes/shared/icons/new_circuit.svg b/browser/themes/shared/icons/new_circuit.svg new file mode 100644 index 000000000000..ddc819946818 --- /dev/null +++ b/browser/themes/shared/icons/new_circuit.svg @@ -0,0 +1,6 @@ +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g stroke="none" stroke-width="1" fill="context-fill" fill-rule="evenodd" opacity="context-fill-opacity"> + <path d="m10.707 6h3.993l.3-.3v-3.993c.0002-.09902-.0291-.19586-.084-.27825s-.1331-.14661-.2245-.18453c-.0915-.03792-.1922-.04782-.2893-.02845-.0971.01936-.1863.06713-.2562.13723l-1.459 1.459c-1.2817-1.16743-2.95335-1.813714-4.687-1.812-3.859 0-7 3.141-7 7s3.141 7 7 7c1.74123.007 3.422-.6379 4.7116-1.8079 1.2896-1.1701 2.0945-2.7804 2.2564-4.5141.0156-.1649-.0348-.32927-.1401-.4571s-.2571-.2087-.4219-.2249c-.1644-.01324-.3275.03801-.4548.1429s-.2088.2552-.2272.4191c-.1334 1.42392 [...] + <path d="m8 12.5c-2.48528 0-4.5-2.0147-4.5-4.5 0-2.48528 2.01472-4.5 4.5-4.5z"/> + </g> +</svg> diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn index d04f95e59ea0..95fe3fdbe299 100644 --- a/browser/themes/shared/jar.inc.mn +++ b/browser/themes/shared/jar.inc.mn @@ -266,3 +266,5 @@ skin/classic/browser/syncedtabs/sidebar.css (../shared/syncedtabs/sidebar.css)
skin/classic/browser/new_identity.svg (../shared/icons/new_identity.svg) + + skin/classic/browser/new_circuit.svg (../shared/icons/new_circuit.svg) diff --git a/browser/themes/shared/toolbarbutton-icons.css b/browser/themes/shared/toolbarbutton-icons.css index 8e285fdfd7c2..b2cb4d277e1e 100644 --- a/browser/themes/shared/toolbarbutton-icons.css +++ b/browser/themes/shared/toolbarbutton-icons.css @@ -267,6 +267,10 @@ toolbar { list-style-image: url("chrome://browser/skin/new_identity.svg"); }
+#new-circuit-button { + list-style-image: url("chrome://browser/skin/new_circuit.svg"); +} + #privatebrowsing-button { list-style-image: url("chrome://browser/skin/privateBrowsing.svg"); } diff --git a/docshell/base/nsAboutRedirector.cpp b/docshell/base/nsAboutRedirector.cpp index e28bec9bd2c2..232104214844 100644 --- a/docshell/base/nsAboutRedirector.cpp +++ b/docshell/base/nsAboutRedirector.cpp @@ -174,7 +174,11 @@ static const RedirEntry kRedirMap[] = { nsIAboutModule::HIDE_FROM_ABOUTABOUT | nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::URI_MUST_LOAD_IN_CHILD}, - {"crashgpu", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT}}; + {"crashgpu", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT}, + {"tor", "chrome://torbutton/content/aboutTor/aboutTor.xhtml", + nsIAboutModule::URI_MUST_LOAD_IN_CHILD | + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}}; static const int kRedirTotal = mozilla::ArrayLength(kRedirMap);
NS_IMETHODIMP diff --git a/docshell/build/components.conf b/docshell/build/components.conf index 5f11df641e37..6bc8617c8f0a 100644 --- a/docshell/build/components.conf +++ b/docshell/build/components.conf @@ -29,6 +29,7 @@ about_pages = [ 'srcdoc', 'support', 'telemetry', + 'tor', 'url-classifier', 'webrtc', ] diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in index d9e8407ade7a..ead4a138d927 100644 --- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -137,6 +137,10 @@ ; Base Browser @BINPATH@/components/SecurityLevel.manifest
+; Torbutton +@BINPATH@/chrome/torbutton@JAREXT@ +@BINPATH@/chrome/torbutton.manifest + ; [Default Preferences] ; All the pref files must be part of base to prevent migration bugs #ifndef MOZ_ANDROID_FAT_AAR_ARCHITECTURES diff --git a/toolkit/moz.build b/toolkit/moz.build index d464f7eb8092..cf9f7202ece0 100644 --- a/toolkit/moz.build +++ b/toolkit/moz.build @@ -22,6 +22,7 @@ DIRS += [ "mozapps/preferences", "profile", "themes", + "torbutton", ]
if CONFIG["OS_ARCH"] == "WINNT" and CONFIG["MOZ_DEFAULT_BROWSER_AGENT"]: diff --git a/toolkit/torbutton/.gitignore b/toolkit/torbutton/.gitignore new file mode 100644 index 000000000000..753ac2e10e5a --- /dev/null +++ b/toolkit/torbutton/.gitignore @@ -0,0 +1 @@ +translation diff --git a/toolkit/torbutton/CREDITS b/toolkit/torbutton/CREDITS new file mode 100644 index 000000000000..c2a0be95196a --- /dev/null +++ b/toolkit/torbutton/CREDITS @@ -0,0 +1,5 @@ +TorButton was adapted by Scott Squires from ProxyButton +(proxybutton.mozdev.com), which was written by Oleg Ivanov. + +Security components by Mike Perry with History blocking and cookie jar code +from Collin Jackson. diff --git a/toolkit/torbutton/LICENSE b/toolkit/torbutton/LICENSE new file mode 100644 index 000000000000..783d6a4705a1 --- /dev/null +++ b/toolkit/torbutton/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2020, The Tor Project, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + (1) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + (3)The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/toolkit/torbutton/chrome/content/aboutTor/aboutTor-content.js b/toolkit/torbutton/chrome/content/aboutTor/aboutTor-content.js new file mode 100644 index 000000000000..6c53dff832e5 --- /dev/null +++ b/toolkit/torbutton/chrome/content/aboutTor/aboutTor-content.js @@ -0,0 +1,139 @@ +/************************************************************************* + * Copyright (c) 2019, The Tor Project, Inc. + * See LICENSE for licensing information. + * + * vim: set sw=2 sts=2 ts=8 et syntax=javascript: + * + * about:tor content script + *************************************************************************/ + +/* + * The following about:tor IPC messages are exchanged by this code and + * the code in torbutton.js: + * AboutTor:Loaded page loaded content -> chrome + * AboutTor:ChromeData privileged data chrome -> content + */ + +/* globals content, addMessageListener, sendAsyncMessage, + removeMessageListener */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { bindPrefAndInit, getLocale } = ChromeUtils.import( + "resource://torbutton/modules/utils.js" +); + +var AboutTorListener = { + kAboutTorLoadedMessage: "AboutTor:Loaded", + kAboutTorChromeDataMessage: "AboutTor:ChromeData", + + get isAboutTor() { + return content.document.documentURI.toLowerCase() == "about:tor"; + }, + + init(aChromeGlobal) { + aChromeGlobal.addEventListener("AboutTorLoad", this, false, true); + }, + + handleEvent(aEvent) { + if (!this.isAboutTor) { + return; + } + + switch (aEvent.type) { + case "AboutTorLoad": + this.onPageLoad(); + break; + case "pagehide": + this.onPageHide(); + break; + } + }, + + receiveMessage(aMessage) { + if (!this.isAboutTor) { + return; + } + + switch (aMessage.name) { + case this.kAboutTorChromeDataMessage: + this.onChromeDataUpdate(aMessage.data); + break; + } + }, + + onPageLoad() { + // Arrange to update localized text and links. + bindPrefAndInit("intl.locale.requested", () => { + this.onLocaleChange(); + }); + + // Add message and event listeners. + addMessageListener(this.kAboutTorChromeDataMessage, this); + addEventListener("pagehide", this, false); + addEventListener("resize", this, false); + + sendAsyncMessage(this.kAboutTorLoadedMessage); + }, + + onPageHide() { + removeEventListener("resize", this, false); + removeEventListener("pagehide", this, false); + removeMessageListener(this.kAboutTorChromeDataMessage, this); + }, + + onChromeDataUpdate(aData) { + let body = content.document.body; + + // Update status: tor on/off, Tor Browser manual shown. + if (aData.torOn) { + body.setAttribute("toron", "yes"); + } else { + body.removeAttribute("toron"); + } + + if (aData.updateChannel) { + body.setAttribute("updatechannel", aData.updateChannel); + } else { + body.removeAttribute("updatechannel"); + } + + if (aData.hasBeenUpdated) { + body.setAttribute("hasbeenupdated", "yes"); + content.document + .getElementById("update-infolink") + .setAttribute("href", aData.updateMoreInfoURL); + } + + if (aData.mobile) { + body.setAttribute("mobile", "yes"); + } + + // Setting body.initialized="yes" displays the body. + body.setAttribute("initialized", "yes"); + }, + + onLocaleChange() { + // Set localized "Get Involved" link. + content.document.getElementById("getInvolvedLink").href = + `https://community.torproject.org/$%7BgetLocale()%7D%60; + + // Display the Tor Browser product name and version. + try { + const kBrandBundle = "chrome://branding/locale/brand.properties"; + let brandBundle = Services.strings.createBundle(kBrandBundle); + let productName = brandBundle.GetStringFromName("brandFullName"); + let tbbVersion = Services.prefs.getCharPref("torbrowser.version"); + let elem = content.document.getElementById("torbrowser-version"); + + while (elem.firstChild) { + elem.firstChild.remove(); + } + elem.appendChild( + content.document.createTextNode(productName + " " + tbbVersion) + ); + } catch (e) {} + }, +}; + +AboutTorListener.init(this); diff --git a/toolkit/torbutton/chrome/content/aboutTor/aboutTor.xhtml b/toolkit/torbutton/chrome/content/aboutTor/aboutTor.xhtml new file mode 100644 index 000000000000..e48c22630150 --- /dev/null +++ b/toolkit/torbutton/chrome/content/aboutTor/aboutTor.xhtml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + - Copyright (c) 2020, The Tor Project, Inc. + - See LICENSE for licensing information. + - vim: set sw=2 sts=2 ts=8 et syntax=xml: + --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd"> + %aboutTorDTD; + <!ENTITY % tbUpdateDTD SYSTEM "chrome://browser/locale/aboutTBUpdate.dtd"> + %tbUpdateDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Security-Policy" content="default-src resource:; object-src 'none'" /> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title>&aboutTor.title;</title> + <link rel="stylesheet" href="chrome://browser/skin/onionPattern.css" type="text/css" media="all" /> + <link rel="stylesheet" href="resource://torbutton-assets/aboutTor.css" type="text/css" media="all" /> + <script type="text/javascript" src="resource://torbutton-abouttor/aboutTor.js"></script> +</head> +<body dir="&locale.dir;"> + <div class="torcontent-container"> + <div id="torbrowser-info"> + <div id="torbrowser-version"/> + <div id="torbrowser-changelog-link"><a href="about:tbupdate">&aboutTor.viewChangelog.label;</a></div> + </div> + <img class="torcontent-logo" src="resource://torbutton-assets/torbrowser_mobile_logo.png"/> + <div id="torstatus" class="top"> + <div class="hideIfTorOff hideIfHasBeenUpdated torstatus-container"> +#if MOZ_UPDATE_CHANNEL == alpha + <div class="heading1">&aboutTor.alpha.ready.label;</div> + <br/> + <div class="heading2">&aboutTor.alpha.ready2.label;</div> +#elif MOZ_UPDATE_CHANNEL == nightly + <div class="heading1">&aboutTor.nightly.ready.label;</div> + <br/> + <div class="heading2">&aboutTor.nightly.ready2.label;</div> +#else + <div class="heading1">&aboutTor.ready.label;</div> + <br/> + <div class="heading2">&aboutTor.ready2.label;</div> +#endif + </div> + <div class="showIfHasBeenUpdated torstatus-container"> + <div class="heading1">&aboutTBUpdate.updated;</div> + <br/> + <div class="heading2">&aboutTBUpdate.linkPrefix;<a id="update-infolink">&aboutTBUpdate.linkLabel;</a>&aboutTBUpdate.linkSuffix;</div> + </div> + <div class="hideIfTorOn torstatus-container"> + <div class="heading1">&aboutTor.failure.label;</div> + <br/> + <div class="heading2">&aboutTor.failure2.label;</div> + </div> + </div> + + <div class="searchbox hideIfTorOff"> <!-- begin form based search --> + <form action="&aboutTor.searchDDGPost.link;" method="get"> + <div class="searchwrapper"> + <label class="searchlabel" for="search-text"></label> + <input name="q" id="search-text" placeholder="&aboutTor.search.label;" + autocomplete="off" type="text"/> + <input id="search-button" value="" + title="&aboutTor.search.label;" + alt="&aboutTor.search.label;" type="submit"/> + </div> + </form> + </div> + +#if MOZ_UPDATE_CHANNEL != release + <div id="bannerRoot"> + <img id="bannerImg" src="resource://torbutton-assets/banner-warning.svg" /> + <div id="bannerTextCol"> + <!--<div id="bannerHeader"></div> + <br />--> +#if MOZ_UPDATE_CHANNEL == alpha + <div id="bannerDescription">&aboutTor.alpha.bannerDescription;</div> + <br /> + <a id="bannerLink" target="_blank" href="https://forum.torproject.net/">&aboutTor.alpha.bannerLink;</a> +#else + <div id="bannerDescription">&aboutTor.nightly.bannerDescription;</div> + <br /> + <a id="bannerLink" target="_blank" href="https://forum.torproject.net/">&aboutTor.nightly.bannerLink;</a> +#endif + </div> + </div> +#endif + + <div id="bottom"> + <p id="donate" class="moreInfoLink">&aboutTor.donationBanner.freeToUse; + <a href="https://donate.torproject.org/" target="_blank">&aboutTor.donationBanner.buttonA; »</a> + </p> + <p id="manual" class="showForManual moreInfoLink">&aboutTor.torbrowser_user_manual_questions.label; + <a id="manualLink" href="about:manual" target="_blank">&aboutTor.torbrowser_user_manual_link.label;</a></p> + <p id="newsletter" class="moreInfoLink"><img class="imageStyle" src="chrome://browser/skin/mail.svg"/>&aboutTor.newsletter.tagline;<br/> + <a href="https://newsletter.torproject.org">&aboutTor.newsletter.link_text; »</a> + </p> + <p id="mission">&aboutTor.tor_mission.label; + <a id="getInvolvedLink">&aboutTor.getInvolved.label;</a></p> + </div> + </div> +#include ../../../../../browser/themes/shared/onionPattern.inc.xhtml +</body> +</html> diff --git a/toolkit/torbutton/chrome/content/aboutTor/resources/aboutTor.js b/toolkit/torbutton/chrome/content/aboutTor/resources/aboutTor.js new file mode 100644 index 000000000000..6687390b6aca --- /dev/null +++ b/toolkit/torbutton/chrome/content/aboutTor/resources/aboutTor.js @@ -0,0 +1,11 @@ +/************************************************************************* + * Copyright (c) 2020, The Tor Project, Inc. + * See LICENSE for licensing information. + * + * vim: set sw=2 sts=2 ts=8 et syntax=javascript: + *************************************************************************/ + +window.addEventListener("pageshow", function() { + let evt = new CustomEvent("AboutTorLoad", { bubbles: true }); + document.dispatchEvent(evt); +}); diff --git a/toolkit/torbutton/chrome/content/preferences-mobile.js b/toolkit/torbutton/chrome/content/preferences-mobile.js new file mode 100644 index 000000000000..b696ef17fd32 --- /dev/null +++ b/toolkit/torbutton/chrome/content/preferences-mobile.js @@ -0,0 +1,59 @@ +// # Security Settings User Interface for Mobile + +// Utilities +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { + getBoolPref, + getIntPref, + setBoolPref, + setIntPref, + getCharPref, +} = Services.prefs; + +// Description elements have the follow names. +const descNames = ["", "desc_standard", "desc_safer", "desc_safest"]; +// "Learn-more"-elements have the follow names. +const linkNames = ["", "link_standard", "link_safer", "link_safest"]; +// A single `state` object that reflects the user settings in this UI. + +let state = { slider: 0, custom: false }; + +// Utility functions to convert between the legacy 4-value pref index +// and the 3-valued security slider. +let sliderPositionToPrefSetting = pos => [0, 4, 2, 1][pos]; +let prefSettingToSliderPosition = pref => [0, 3, 2, 2, 1][pref]; + +// Set the desired slider value and update UI. +function torbutton_set_slider(sliderValue) { + state.slider = sliderValue; + let slider = document.getElementById("torbutton_sec_slider"); + slider.value = sliderValue.toString(); + let descs = descNames.map(name => document.getElementById(name)); + descs.forEach((desc, i) => { + if (state.slider !== i) { + desc.style.display = "none"; + } else { + desc.style.display = "block"; + } + }); + torbutton_save_security_settings(); +} + +// Read prefs 'browser.security_level.security_slider' and +// 'browser.security_level.security_custom', and initialize the UI. +function torbutton_init_security_ui() { + torbutton_set_slider( + prefSettingToSliderPosition( + getIntPref("browser.security_level.security_slider") + ) + ); +} + +// Write the two prefs from the current settings. +function torbutton_save_security_settings() { + setIntPref( + "browser.security_level.security_slider", + sliderPositionToPrefSetting(state.slider) + ); + setBoolPref("browser.security_level.security_custom", state.custom); +} diff --git a/toolkit/torbutton/chrome/content/preferences.xhtml b/toolkit/torbutton/chrome/content/preferences.xhtml new file mode 100644 index 000000000000..eafbf01e7d6c --- /dev/null +++ b/toolkit/torbutton/chrome/content/preferences.xhtml @@ -0,0 +1,84 @@ +<!DOCTYPE overlay SYSTEM "chrome://torbutton/locale/torbutton.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> + <meta charset="UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <head> + <title>&torbutton.prefs.security_settings;</title> + <link type="text/css" rel="stylesheet" charset="UTF-8" href="chrome://torbutton/skin/preferences.css"/> + <link type="text/css" rel="stylesheet" charset="UTF-8" href="chrome://torbutton/skin/preferences-mobile.css"/> + <script type="text/javascript" src="preferences-mobile.js"></script> + <style> + </style> + </head> + <body onload="torbutton_init_security_ui()"> + <div class="wrapper outer-wrapper"> + <div class="title"> + &torbutton.prefs.sec_caption; + </div> + <div class="wrapper inner-wrapper"> + <input id="torbutton_sec_slider" type="range" min="1" max="3" list="datalist" onchange="torbutton_set_slider(parseInt(this.value, 10))"/> + <datalist id="datalist"> + <option onclick="torbutton_set_slider(1)"> + &torbutton.prefs.sec_standard_label; + </option> + <option onclick="torbutton_set_slider(2)"> + &torbutton.prefs.sec_safer_label; + </option> + <option onclick="torbutton_set_slider(3)"> + &torbutton.prefs.sec_safest_label; + </option> + </datalist> + <div class="description-wrapper"> + <div id="desc_safest" class="description"> + <p class="slider-text-size slider-text-weight"> + &torbutton.prefs.sec_safest_description; + </p> + <p class="slider-text-size slider-text-weight"> + &torbutton.prefs.sec_safest_list_label; + </p> + <p class="slider-text-size"> + &torbutton.prefs.sec_js_disabled; + </p> + <p class="slider-text-size"> + &torbutton.prefs.sec_limit_graphics_and_typography; + </p> + <p class="slider-text-size"> + &torbutton.prefs.sec_click_to_play_media; + </p> + <a id="link_safest" class="text-link"> + &torbutton.prefs.sec_learn_more_label; + </a> + </div> + <div id="desc_safer" class="description"> + <p class="slider-text-size slider-text-weight"> + &torbutton.prefs.sec_safer_description; + </p> + <p class="slider-text-size slider-text-weight"> + &torbutton.prefs.sec_safer_list_label; + </p> + <p class="slider-text-size"> + &torbutton.prefs.sec_js_on_https_sites_only; + </p> + <p class="slider-text-size"> + &torbutton.prefs.sec_limit_typography; + </p> + <p class="slider-text-size"> + &torbutton.prefs.sec_click_to_play_media; + </p> + <a id="link_safer" class="text-link"> + &torbutton.prefs.sec_learn_more_label; + </a> + </div> + <div id="desc_standard" class="description"> + <p class="slider-text-size slider-text-weight"> + &torbutton.prefs.sec_standard_description; + </p> + <a id="link_standard" class="text-link"> + &torbutton.prefs.sec_learn_more_label; + </a> + </div> + </div> + </div> + </div> + </body> +</html> diff --git a/toolkit/torbutton/chrome/content/tor-circuit-display.js b/toolkit/torbutton/chrome/content/tor-circuit-display.js new file mode 100644 index 000000000000..76fb74546ac6 --- /dev/null +++ b/toolkit/torbutton/chrome/content/tor-circuit-display.js @@ -0,0 +1,604 @@ +// A script that automatically displays the Tor Circuit used for the +// current domain for the currently selected tab. +// +// This file is written in call stack order (later functions +// call earlier functions). The file can be processed +// with docco.js to produce pretty documentation. +// +// This script is to be embedded in torbutton.xhtml. It defines a single global +// function, createTorCircuitDisplay(), which activates the automatic Tor +// circuit display for the current tab and any future tabs. +// +// See https://trac.torproject.org/8641 + +/* jshint esnext: true */ +/* global document, gBrowser, Components */ + +// ### Main function +// __createTorCircuitDisplay(enablePrefName)__. +// The single function that prepares tor circuit display. Connects to a tor +// control port using information provided to the control port module via +// a previous call to configureControlPortModule(), and binds to a named +// bool pref whose value determines whether the circuit display is enabled +// or disabled. +let createTorCircuitDisplay = (function() { + "use strict"; + + // Mozilla utilities + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + // Import the controller code. + const { wait_for_controller } = ChromeUtils.import( + "resource://torbutton/modules/tor-control-port.js" + ); + + // Utility functions + let { + bindPrefAndInit, + observe, + getLocale, + getDomainForBrowser, + torbutton_get_property_string, + } = ChromeUtils.import("resource://torbutton/modules/utils.js"); + + // Make the TorButton logger available. + let logger = Cc["@torproject.org/torbutton-logger;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + + // ## Circuit/stream credentials and node monitoring + + // A mutable map that stores the current nodes for each + // SOCKS username/password pair. + let credentialsToNodeDataMap = new Map(), + // A mutable map that reports `true` for IDs of "mature" circuits + // (those that have conveyed a stream). + knownCircuitIDs = new Map(), + // A mutable map that records the SOCKS credentials for the + // latest channels for each browser + domain. + browserToCredentialsMap = new Map(); + + // __trimQuotes(s)__. + // Removes quotation marks around a quoted string. + let trimQuotes = s => (s ? s.match(/^"(.*)"$/)[1] : undefined); + + // __getBridge(id)__. + // Gets the bridge parameters for a given node ID. If the node + // is not currently used as a bridge, returns null. + let getBridge = async function(controller, id) { + let bridges = await controller.getConf("bridge"); + if (bridges) { + for (let bridge of bridges) { + if (bridge.ID && bridge.ID.toUpperCase() === id.toUpperCase()) { + return bridge; + } + } + } + return null; + }; + + // nodeDataForID(controller, id)__. + // Returns the type, IP addresses and country code of a node with given ID. + // Example: `nodeDataForID(controller, "20BC91DC525C3DC9974B29FBEAB51230DE024C44")` + // => `{ type: "default", ipAddrs: ["12.23.34.45", "2001:db8::"], countryCode: "fr" }` + let nodeDataForID = async function(controller, id) { + let result = { ipAddrs: [] }; + const bridge = await getBridge(controller, id); // type, ip, countryCode; + const addrRe = /^[?([^]]+)]?:\d+$/; + if (bridge) { + result.type = "bridge"; + result.bridgeType = bridge.type; + // Attempt to get an IP address from bridge address string. + try { + const ip = bridge.address.match(addrRe)[1]; + if (!ip.startsWith("0.")) { + result.ipAddrs = [ip]; + } + } catch (e) {} + } else { + // either dealing with a relay, or a bridge whose fingerprint is not saved in torrc + try { + const statusMap = await controller.getInfo("ns/id/" + id); + result.type = "default"; + if (!statusMap.IP.startsWith("0.")) { + result.ipAddrs.push(statusMap.IP); + } + try { + result.ipAddrs.push(statusMap.IPv6.match(addrRe)[1]); + } catch (e) {} + } catch (e) { + // getInfo will throw if the given id is not a relay + // this probably means we are dealing with a user-provided bridge with no fingerprint + result.type = "bridge"; + // we don't know the ip/ipv6 or type, so leave blank + result.ipAddrs = []; + result.bridgeType = ""; + } + } + if (result.ipAddrs.length) { + // Get the country code for the node's IP address. + try { + const countryCode = await controller.getInfo( + "ip-to-country/" + result.ipAddrs[0] + ); + result.countryCode = countryCode === "??" ? null : countryCode; + } catch (e) {} + } + return result; + }; + + // __nodeDataForCircuit(controller, circuitEvent)__. + // Gets the information for a circuit. + let nodeDataForCircuit = async function(controller, circuitEvent) { + let rawIDs = circuitEvent.circuit.map(circ => circ[0]), + // Remove the leading '$' if present. + ids = rawIDs.map(id => (id[0] === "$" ? id.substring(1) : id)); + // Get the node data for all IDs in circuit. + return Promise.all(ids.map(id => nodeDataForID(controller, id))); + }; + + // __getCircuitStatusByID(aController, circuitID)__ + // Returns the circuit status for the circuit with the given ID. + let getCircuitStatusByID = async function(aController, circuitID) { + let circuitStatuses = await aController.getInfo("circuit-status"); + if (circuitStatuses) { + for (let circuitStatus of circuitStatuses) { + if (circuitStatus.id === circuitID) { + return circuitStatus; + } + } + } + return null; + }; + + // __collectIsolationData(aController, updateUI)__. + // Watches for STREAM SENTCONNECT events. When a SENTCONNECT event occurs, then + // we assume isolation settings (SOCKS username+password) are now fixed for the + // corresponding circuit. Whenever the first stream on a new circuit is seen, + // looks up u+p and records the node data in the credentialsToNodeDataMap. + // We need to update the circuit display immediately after any new node data + // is received. So the `updateUI` callback will be called at that point. + // See https://trac.torproject.org/projects/tor/ticket/15493 + let collectIsolationData = function(aController, updateUI) { + return aController.watchEvent( + "STREAM", + streamEvent => streamEvent.StreamStatus === "SENTCONNECT", + async streamEvent => { + if (!knownCircuitIDs.get(streamEvent.CircuitID)) { + logger.eclog(3, "streamEvent.CircuitID: " + streamEvent.CircuitID); + knownCircuitIDs.set(streamEvent.CircuitID, true); + let circuitStatus = await getCircuitStatusByID( + aController, + streamEvent.CircuitID + ), + credentials = circuitStatus + ? trimQuotes(circuitStatus.SOCKS_USERNAME) + + "|" + + trimQuotes(circuitStatus.SOCKS_PASSWORD) + : null; + if (credentials) { + let nodeData = await nodeDataForCircuit(aController, circuitStatus); + credentialsToNodeDataMap.set(credentials, nodeData); + updateUI(); + } + } + } + ); + }; + + // __browserForChannel(channel)__. + // Returns the browser that loaded a given channel. + let browserForChannel = function(channel) { + if (!channel) { + return null; + } + let chan = channel.QueryInterface(Ci.nsIChannel); + let callbacks = chan.notificationCallbacks; + if (!callbacks) { + return null; + } + let loadContext; + try { + loadContext = callbacks.getInterface(Ci.nsILoadContext); + } catch (e) { + // Ignore + return null; + } + if (!loadContext) { + return null; + } + return loadContext.topFrameElement; + }; + + // __collectBrowserCredentials()__. + // Starts observing http channels. Each channel's proxyInfo + // username and password is recorded for the channel's browser. + let collectBrowserCredentials = function() { + return observe("http-on-modify-request", chan => { + try { + let proxyInfo = chan.QueryInterface(Ci.nsIProxiedChannel).proxyInfo; + let browser = browserForChannel(chan); + if (browser && proxyInfo) { + if (!browserToCredentialsMap.has(browser)) { + browserToCredentialsMap.set(browser, new Map()); + } + let domainMap = browserToCredentialsMap.get(browser); + domainMap.set(proxyInfo.username, [ + proxyInfo.username, + proxyInfo.password, + ]); + } + } catch (e) { + logger.eclog( + 3, + `Error collecting browser credentials: ${e.message}, ${chan.URI.spec}` + ); + } + }); + }; + + // ## User interface + + // __uiString__. + // Read the localized strings for this UI. + let uiString = function(shortName) { + return torbutton_get_property_string( + "torbutton.circuit_display." + shortName + ); + }; + + // __localizedCountryNameFromCode(countryCode)__. + // Convert a country code to a localized country name. + // Example: `'de'` -> `'Deutschland'` in German locale. + let localizedCountryNameFromCode = function(countryCode) { + if (!countryCode) { + return uiString("unknown_country"); + } + try { + return Services.intl.getRegionDisplayNames(undefined, [countryCode])[0]; + } catch (e) { + return countryCode.toUpperCase(); + } + }; + + // __showCircuitDisplay(show)__. + // If show === true, makes the circuit display visible. + let showCircuitDisplay = function(show) { + document.getElementById("circuit-display-container").hidden = !show; + }; + + // __xmlTree(ns, data)__. + // Takes an xml namespace, ns, and a + // data structure representing xml elements like + // [tag, { attr-key: attr-value }, ...xml-children] + // and returns nested xml element objects. + let xmlTree = function xmlTree(ns, data) { + let [type, attrs, ...children] = data; + let element = type.startsWith("html:") + ? document.createXULElement(type) + : document.createElementNS(ns, type); + for (let [key, val] of Object.entries(attrs)) { + element.setAttribute(key, val); + } + for (let child of children) { + if (child !== null && child !== undefined) { + element.append(typeof child === "string" ? child : xmlTree(ns, child)); + } + } + return element; + }; + + // __htmlTree(data)__. + // Takes a data structure representing html elements like + // [tag, { attr-key: attr-value }, ...html-children] + // and returns nested html element objects. + let htmlTree = data => xmlTree("http://www.w3.org/1999/xhtml", data); + + // __appendHtml(parent, data)__. + // Takes a data structure representing html elements like + // [tag, { attr-key: attr-value }, ...html-children] + // and appends nested html element objects to the parent element. + let appendHtml = (parent, data) => parent.appendChild(htmlTree(data)); + + // __circuitCircuitData()__. + // Obtains the circuit used by the given browser. + let currentCircuitData = function(browser) { + if (browser) { + let firstPartyDomain = getDomainForBrowser(browser); + let domain = firstPartyDomain || "--unknown--"; + let domainMap = browserToCredentialsMap.get(browser); + let credentials = domainMap && domainMap.get(domain); + if (credentials) { + let [SOCKS_username, SOCKS_password] = credentials; + let nodeData = credentialsToNodeDataMap.get( + `${SOCKS_username}|${SOCKS_password}` + ); + let domain = SOCKS_username; + if (browser.documentURI.host.endsWith(".tor.onion")) { + const service = Cc[ + "@torproject.org/onion-alias-service;1" + ].getService(Ci.IOnionAliasService); + domain = service.getOnionAlias(browser.documentURI.host); + } + return { domain, nodeData }; + } + } + return { domain: null, nodeData: null }; + }; + + // __updateCircuitDisplay()__. + // Updates the Tor circuit display, showing the current domain + // and the relay nodes for that domain. + let updateCircuitDisplay = function() { + let { domain, nodeData } = currentCircuitData(gBrowser.selectedBrowser); + if (domain && nodeData) { + // Update the displayed information for the relay nodes. + let nodeHtmlList = document.getElementById("circuit-display-nodes"); + let li = (...data) => appendHtml(nodeHtmlList, ["li", {}, ...data]); + nodeHtmlList.innerHTML = ""; + li(uiString("this_browser")); + for (let i = 0; i < nodeData.length; ++i) { + let relayText; + if (nodeData[i].type === "bridge") { + relayText = uiString("tor_bridge"); + let bridgeType = nodeData[i].bridgeType; + if (bridgeType === "meek_lite") { + relayText += ": meek"; + } else if (bridgeType !== "vanilla" && bridgeType !== "") { + relayText += ": " + bridgeType; + } + } else if (nodeData[i].type == "default") { + relayText = localizedCountryNameFromCode(nodeData[i].countryCode); + } + const ipAddrs = nodeData[i].ipAddrs.join(", "); + li( + relayText, + " ", + ["span", { class: "circuit-ip-address" }, ipAddrs], + " ", + i === 0 && nodeData[0].type !== "bridge" + ? ["span", { class: "circuit-guard-info" }, uiString("guard")] + : null + ); + } + + let domainParts = []; + if (domain.endsWith(".onion")) { + for (let i = 0; i < 3; ++i) { + li(uiString("relay")); + } + if (domain.length > 22) { + domainParts.push(domain.slice(0, 7), "…", domain.slice(-12)); + } else { + domainParts.push(domain); + } + } else { + domainParts.push(domain); + } + + // We use a XUL html:span element so that the tooltiptext is displayed. + li([ + "html:span", + { + class: "circuit-onion", + onclick: ` + this.classList.add("circuit-onion-copied"); + Cc[ + "@mozilla.org/widget/clipboardhelper;1" + ].getService(Ci.nsIClipboardHelper).copyString(this.getAttribute("data-onion")) + `, + "data-onion": domain, + "data-text-clicktocopy": torbutton_get_property_string( + "torbutton.circuit_display.click_to_copy" + ), + "data-text-copied": torbutton_get_property_string( + "torbutton.circuit_display.copied" + ), + tooltiptext: domain, + }, + ...domainParts, + ]); + + // Hide the note about guards if we are using a bridge. + document.getElementById("circuit-guard-note-container").hidden = + nodeData[0].type === "bridge"; + } else { + // Only show the Tor circuit if we have credentials and node data. + logger.eclog(4, "no SOCKS credentials found for current document."); + } + showCircuitDisplay(domain && nodeData); + }; + + // __syncDisplayWithSelectedTab(syncOn)__. + // Whenever the user starts to open the popup menu, make sure the display + // is the correct one for this tab. It's also possible that a new site + // can be loaded while the popup menu is open. + // Update the display if this happens. + let syncDisplayWithSelectedTab = (function() { + let listener = { + onLocationChange(aBrowser) { + if (aBrowser === gBrowser.selectedBrowser) { + updateCircuitDisplay(); + } + }, + }; + return function(syncOn) { + let popupMenu = document.getElementById("identity-popup"); + if (syncOn) { + // Update the circuit display just before the popup menu is shown. + popupMenu.addEventListener("popupshowing", updateCircuitDisplay); + // If the currently selected tab has been sent to a new location, + // update the circuit to reflect that. + gBrowser.addTabsProgressListener(listener); + } else { + // Stop syncing. + gBrowser.removeTabsProgressListener(listener); + popupMenu.removeEventListener("popupshowing", updateCircuitDisplay); + // Hide the display. + showCircuitDisplay(false); + } + }; + })(); + + // __setupGuardNote()__. + // Call once to show the Guard note as intended. + let setupGuardNote = function() { + let guardNote = document.getElementById("circuit-guard-note-container"); + let guardNoteString = uiString("guard_note"); + let learnMoreString = uiString("learn_more"); + let [noteBefore, name, noteAfter] = guardNoteString.split(/[[]]/); + let localeCode = getLocale(); + appendHtml(guardNote, [ + "div", + {}, + noteBefore, + ["span", { class: "circuit-guard-name" }, name], + noteAfter, + " ", + [ + "span", + { + onclick: `gBrowser.selectedTab = gBrowser.addWebTab('https://support.torproject.org/$%7BlocaleCode%7D/tbb/tbb-2/%27);%60, + class: "circuit-link", + }, + learnMoreString, + ], + ]); + }; + + // __ensureCorrectPopupDimensions()__. + // Make sure the identity popup always displays with the correct height. + let ensureCorrectPopupDimensions = function() { + // FIXME: This is hacking with the internals of the panel view, which also + // sets the width and height for transitions (see PanelMultiView.jsm), so + // this fix is fragile. + // Ideally the panel view would start using a non-XUL CSS layout, which + // would work regardless of the content. + let popupMenu = document.getElementById("identity-popup"); + let setDimensions = event => { + if (event.target !== popupMenu) { + return; + } + setTimeout(() => { + let view = document.querySelector( + "#identity-popup-multiView .panel-viewcontainer" + ); + let stack = document.querySelector( + "#identity-popup-multiView .panel-viewstack" + ); + let view2 = document.getElementById("identity-popup-mainView"); + if (view && stack && view2) { + let newWidth = Math.max( + ...[...view2.children].map(el => el.clientWidth) + ); + let newHeight = stack.clientHeight; + stack.setAttribute("width", newWidth); + view2.style.minWidth = view2.style.maxWidth = newWidth + "px"; + view.setAttribute("width", newWidth); + view.setAttribute("height", newHeight); + } + }); + }; + let removeDimensions = event => { + if (event.target !== popupMenu) { + return; + } + let view = document.querySelector( + "#identity-popup-multiView .panel-viewcontainer" + ); + let stack = document.querySelector( + "#identity-popup-multiView .panel-viewstack" + ); + let view2 = document.getElementById("identity-popup-mainView"); + if (view && stack && view2) { + view.removeAttribute("width"); + view.removeAttribute("height"); + stack.removeAttribute("width"); + view2.style.minWidth = view2.style.maxWidth = ""; + } + }; + popupMenu.addEventListener("popupshowing", setDimensions); + popupMenu.addEventListener("popuphiding", removeDimensions); + return () => { + popupMenu.removeEventListener("popupshowing", setDimensions); + popupMenu.removeEventListener("popuphiding", removeDimensions); + }; + }; + + // ## Main function + + // __setupDisplay(enablePrefName)__. + // Once called, the Tor circuit display will be started whenever + // the "enablePref" is set to true, and stopped when it is set to false. + // A reference to this function (called createTorCircuitDisplay) is exported as a global. + let setupDisplay = function(enablePrefName) { + // From 79 on the identity popup is initialized lazily + if (gIdentityHandler._initializePopup) { + gIdentityHandler._initializePopup(); + } + setupGuardNote(); + let myController = null, + stopCollectingIsolationData = null, + stopCollectingBrowserCredentials = null, + stopEnsuringCorrectPopupDimensions = null, + stop = function() { + syncDisplayWithSelectedTab(false); + if (myController) { + if (stopCollectingIsolationData) { + stopCollectingIsolationData(); + } + if (stopCollectingBrowserCredentials) { + stopCollectingBrowserCredentials(); + } + if (stopEnsuringCorrectPopupDimensions) { + stopEnsuringCorrectPopupDimensions(); + } + myController = null; + } + }, + start = async function() { + if (!myController) { + try { + myController = await wait_for_controller(); + syncDisplayWithSelectedTab(true); + stopCollectingIsolationData = collectIsolationData( + myController, + updateCircuitDisplay + ); + stopCollectingBrowserCredentials = collectBrowserCredentials(); + stopEnsuringCorrectPopupDimensions = ensureCorrectPopupDimensions(); + } catch (err) { + logger.eclog(5, err); + logger.eclog( + 5, + "Disabling tor display circuit because of an error." + ); + myController.close(); + stop(); + } + } + }; + try { + let unbindPref = bindPrefAndInit(enablePrefName, on => { + if (on) { + start(); + } else { + stop(); + } + }); + // When this chrome window is unloaded, we need to unbind the pref. + window.addEventListener("unload", function() { + unbindPref(); + stop(); + }); + } catch (e) { + logger.eclog(5, "Error: " + e.message + "\n" + e.stack); + } + }; + + return setupDisplay; + + // Finish createTorCircuitDisplay() +})(); diff --git a/toolkit/torbutton/chrome/content/torbutton.js b/toolkit/torbutton/chrome/content/torbutton.js new file mode 100644 index 000000000000..0959efc4fd31 --- /dev/null +++ b/toolkit/torbutton/chrome/content/torbutton.js @@ -0,0 +1,1044 @@ +// window globals +var torbutton_init; +var torbutton_new_circuit; + +(() => { + // Bug 1506 P1-P5: This is the main Torbutton overlay file. Much needs to be + // preserved here, but in an ideal world, most of this code should perhaps be + // moved into an XPCOM service, and much can also be tossed. See also + // individual 1506 comments for details. + + // TODO: check for leaks: http://www.mozilla.org/scriptable/avoiding-leaks.html + // TODO: Double-check there are no strange exploits to defeat: + // http://kb.mozillazine.org/Links_to_local_pages_don%27t_work + + /* global gBrowser, CustomizableUI, + createTorCircuitDisplay, gFindBarInitialized, + gFindBar, OpenBrowserWindow, PrivateBrowsingUtils, + Services, AppConstants + */ + + let { + unescapeTorString, + bindPrefAndInit, + getDomainForBrowser, + torbutton_log, + torbutton_get_property_string, + } = ChromeUtils.import("resource://torbutton/modules/utils.js"); + let { configureControlPortModule, wait_for_controller } = ChromeUtils.import( + "resource://torbutton/modules/tor-control-port.js" + ); + + const { TorProtocolService } = ChromeUtils.import( + "resource://gre/modules/TorProtocolService.jsm" + ); + + const k_tb_tor_check_failed_topic = "Torbutton:TorCheckFailed"; + + var m_tb_prefs = Services.prefs; + + // status + var m_tb_wasinited = false; + var m_tb_is_main_window = false; + + var m_tb_control_ipc_file = null; // Set if using IPC (UNIX domain socket). + var m_tb_control_port = null; // Set if using TCP. + var m_tb_control_host = null; // Set if using TCP. + var m_tb_control_pass = null; + + // Bug 1506 P2: This object keeps Firefox prefs in sync with Torbutton prefs. + // It probably could stand some simplification (See #3100). It also belongs + // in a component, not the XUL overlay. + var torbutton_unique_pref_observer = { + register() { + this.forced_ua = false; + m_tb_prefs.addObserver("extensions.torbutton", this); + m_tb_prefs.addObserver("browser.privatebrowsing.autostart", this); + m_tb_prefs.addObserver("javascript", this); + m_tb_prefs.addObserver("privacy.resistFingerprinting", this); + m_tb_prefs.addObserver("privacy.resistFingerprinting.letterboxing", this); + }, + + unregister() { + m_tb_prefs.removeObserver("extensions.torbutton", this); + m_tb_prefs.removeObserver("browser.privatebrowsing.autostart", this); + m_tb_prefs.removeObserver("javascript", this); + m_tb_prefs.removeObserver("privacy.resistFingerprinting", this); + m_tb_prefs.removeObserver( + "privacy.resistFingerprinting.letterboxing", + this + ); + }, + + // topic: what event occurred + // subject: what nsIPrefBranch we're observing + // data: which pref has been changed (relative to subject) + observe(subject, topic, data) { + if (topic !== "nsPref:changed") { + return; + } + switch (data) { + case "browser.privatebrowsing.autostart": + torbutton_update_disk_prefs(); + break; + case "extensions.torbutton.use_nontor_proxy": + torbutton_use_nontor_proxy(); + break; + case "privacy.resistFingerprinting": + case "privacy.resistFingerprinting.letterboxing": + torbutton_update_fingerprinting_prefs(); + break; + } + }, + }; + + var torbutton_tor_check_observer = { + register() { + this._obsSvc = Services.obs; + this._obsSvc.addObserver(this, k_tb_tor_check_failed_topic); + }, + + unregister() { + if (this._obsSvc) { + this._obsSvc.removeObserver(this, k_tb_tor_check_failed_topic); + } + }, + + observe(subject, topic, data) { + if (topic === k_tb_tor_check_failed_topic) { + // Update all open about:tor pages. + torbutton_abouttor_message_handler.updateAllOpenPages(); + + // If the user does not have an about:tor tab open in the front most + // window, open one. + var wm = Services.wm; + var win = wm.getMostRecentWindow("navigator:browser"); + if (win == window) { + let foundTab = false; + let tabBrowser = top.gBrowser; + for (let i = 0; !foundTab && i < tabBrowser.browsers.length; ++i) { + let b = tabBrowser.getBrowserAtIndex(i); + foundTab = b.currentURI.spec.toLowerCase() == "about:tor"; + } + + if (!foundTab) { + gBrowser.selectedTab = gBrowser.addTrustedTab("about:tor"); + } + } + } + }, + }; + + var torbutton_new_identity_observers = { + register() { + Services.obs.addObserver(this, "new-identity-requested"); + }, + + observe(aSubject, aTopic, aData) { + if (aTopic !== "new-identity-requested") { + return; + } + + // Clear the domain isolation state. + torbutton_log(3, "Clearing domain isolator"); + const domainIsolator = Cc["@torproject.org/domain-isolator;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + domainIsolator.clearIsolation(); + + torbutton_log(3, "New Identity: Sending NEWNYM"); + // We only support TBB for newnym. + if ( + !m_tb_control_pass || + (!m_tb_control_ipc_file && !m_tb_control_port) + ) { + const warning = torbutton_get_property_string( + "torbutton.popup.no_newnym" + ); + torbutton_log( + 5, + "Torbutton cannot safely newnym. It does not have access to the Tor Control Port." + ); + window.alert(warning); + } else { + const warning = torbutton_get_property_string( + "torbutton.popup.no_newnym" + ); + torbutton_send_ctrl_cmd("SIGNAL NEWNYM") + .then(res => { + if (!res) { + torbutton_log( + 5, + "Torbutton was unable to request a new circuit from Tor" + ); + window.alert(warning); + } + }) + .catch(e => { + torbutton_log( + 5, + "Torbutton was unable to request a new circuit from Tor " + e + ); + window.alert(warning); + }); + } + }, + }; + + function torbutton_is_mobile() { + return Services.appinfo.OS === "Android"; + } + + // Bug 1506 P2-P4: This code sets some version variables that are irrelevant. + // It does read out some important environment variables, though. It is + // called once per browser window.. This might belong in a component. + torbutton_init = function() { + torbutton_log(3, "called init()"); + + if (m_tb_wasinited) { + return; + } + m_tb_wasinited = true; + + // Bug 1506 P4: These vars are very important for New Identity + var environ = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + if (environ.exists("TOR_CONTROL_PASSWD")) { + m_tb_control_pass = environ.get("TOR_CONTROL_PASSWD"); + } else if (environ.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) { + var cookie_path = environ.get("TOR_CONTROL_COOKIE_AUTH_FILE"); + try { + if ("" != cookie_path) { + m_tb_control_pass = torbutton_read_authentication_cookie(cookie_path); + } + } catch (e) { + torbutton_log(4, "unable to read authentication cookie"); + } + } else { + try { + // Try to get password from Tor Launcher. + m_tb_control_pass = TorProtocolService.torGetPassword(false); + } catch (e) {} + } + + // Try to get the control port IPC file (an nsIFile) from Tor Launcher, + // since Tor Launcher knows how to handle its own preferences and how to + // resolve relative paths. + try { + m_tb_control_ipc_file = TorProtocolService.torGetControlIPCFile(); + } catch (e) {} + + if (!m_tb_control_ipc_file) { + if (environ.exists("TOR_CONTROL_PORT")) { + m_tb_control_port = environ.get("TOR_CONTROL_PORT"); + } else { + try { + const kTLControlPortPref = "extensions.torlauncher.control_port"; + m_tb_control_port = m_tb_prefs.getIntPref(kTLControlPortPref); + } catch (e) { + // Since we want to disable some features when Tor Launcher is + // not installed (e.g., New Identity), we do not set a default + // port value here. + } + } + + if (environ.exists("TOR_CONTROL_HOST")) { + m_tb_control_host = environ.get("TOR_CONTROL_HOST"); + } else { + try { + const kTLControlHostPref = "extensions.torlauncher.control_host"; + m_tb_control_host = m_tb_prefs.getCharPref(kTLControlHostPref); + } catch (e) { + m_tb_control_host = "127.0.0.1"; + } + } + } + + configureControlPortModule( + m_tb_control_ipc_file, + m_tb_control_host, + m_tb_control_port, + m_tb_control_pass + ); + + // Add about:tor IPC message listener. + window.messageManager.addMessageListener( + "AboutTor:Loaded", + torbutton_abouttor_message_handler + ); + + setupPreferencesForMobile(); + + torbutton_log(1, "registering Tor check observer"); + torbutton_tor_check_observer.register(); + + // Create the circuit display even though the control port might not be + // ready yet, as the circuit display will wait for the controller to be + // available anyway. + try { + createTorCircuitDisplay("extensions.torbutton.display_circuit"); + circuitDisplayCreated = true; + } catch (e) { + torbutton_log(4, "Error creating the tor circuit display " + e); + } + + // Arrange for our about:tor content script to be loaded in each frame. + window.messageManager.loadFrameScript( + "chrome://torbutton/content/aboutTor/aboutTor-content.js", + true + ); + + torbutton_new_identity_observers.register(); + + torbutton_log(3, "init completed"); + }; + + var torbutton_abouttor_message_handler = { + // Receive IPC messages from the about:tor content script. + async receiveMessage(aMessage) { + switch (aMessage.name) { + case "AboutTor:Loaded": + aMessage.target.messageManager.sendAsyncMessage( + "AboutTor:ChromeData", + await this.getChromeData(true) + ); + break; + } + }, + + // Send privileged data to all of the about:tor content scripts. + async updateAllOpenPages() { + window.messageManager.broadcastAsyncMessage( + "AboutTor:ChromeData", + await this.getChromeData(false) + ); + }, + + // The chrome data contains all of the data needed by the about:tor + // content process that is only available here (in the chrome process). + // It is sent to the content process when an about:tor window is opened + // and in response to events such as the browser noticing that Tor is + // not working. + async getChromeData(aIsRespondingToPageLoad) { + let dataObj = { + mobile: torbutton_is_mobile(), + updateChannel: AppConstants.MOZ_UPDATE_CHANNEL, + torOn: await torbutton_tor_check_ok(), + }; + + if (aIsRespondingToPageLoad) { + const kShouldNotifyPref = "torbrowser.post_update.shouldNotify"; + if (m_tb_prefs.getBoolPref(kShouldNotifyPref, false)) { + m_tb_prefs.clearUserPref(kShouldNotifyPref); + dataObj.hasBeenUpdated = true; + dataObj.updateMoreInfoURL = this.getUpdateMoreInfoURL(); + } + } + + return dataObj; + }, + + getUpdateMoreInfoURL() { + try { + return Services.prefs.getCharPref("torbrowser.post_update.url"); + } catch (e) {} + + // Use the default URL as a fallback. + return Services.urlFormatter.formatURLPref( + "startup.homepage_override_url" + ); + }, + }; + + // Bug 1506 P4: Control port interaction. Needed for New Identity. + function torbutton_read_authentication_cookie(path) { + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(path); + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binaryStream.setInputStream(fileStream); + var array = binaryStream.readByteArray(fileStream.available()); + binaryStream.close(); + fileStream.close(); + return torbutton_array_to_hexdigits(array); + } + + // Bug 1506 P4: Control port interaction. Needed for New Identity. + function torbutton_array_to_hexdigits(array) { + return array + .map(function(c) { + return String("0" + c.toString(16)).slice(-2); + }) + .join(""); + } + + // Bug 1506 P4: Control port interaction. Needed for New Identity. + // + // Asynchronously executes a command on the control port. + // returns the response as a string, or null on error + async function torbutton_send_ctrl_cmd(command) { + const getErrorMessage = e => (e && (e.torMessage || e.message)) || ""; + let response = null; + try { + const avoidCache = true; + let torController = await wait_for_controller(avoidCache); + + let bytes = await torController.sendCommand(command); + if (!bytes.startsWith("250")) { + throw new Error( + `Unexpected command response on control port '${bytes}'` + ); + } + response = bytes.slice(4); + + torController.close(); + } catch (err) { + let msg = getErrorMessage(err); + torbutton_log(4, `Error: ${msg}`); + } + return response; + } + + // Bug 1506 P4: Needed for New IP Address + torbutton_new_circuit = function() { + let firstPartyDomain = getDomainForBrowser(gBrowser.selectedBrowser); + + let domainIsolator = Cc["@torproject.org/domain-isolator;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + + domainIsolator.newCircuitForDomain(firstPartyDomain); + + gBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); + }; + + /* Called when we switch the use_nontor_proxy pref in either direction. + * + * Enables/disables domain isolation and then does new identity + */ + function torbutton_use_nontor_proxy() { + let domainIsolator = Cc["@torproject.org/domain-isolator;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + + if (m_tb_prefs.getBoolPref("extensions.torbutton.use_nontor_proxy")) { + // Disable domain isolation + domainIsolator.disableIsolation(); + } else { + domainIsolator.enableIsolation(); + } + } + + async function torbutton_do_tor_check() { + let checkSvc = Cc["@torproject.org/torbutton-torCheckService;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + if ( + m_tb_prefs.getBoolPref("extensions.torbutton.use_nontor_proxy") || + !m_tb_prefs.getBoolPref("extensions.torbutton.test_enabled") + ) { + return; + } // Only do the check once. + + // If we have a tor control port and transparent torification is off, + // perform a check via the control port. + const kEnvSkipControlPortTest = "TOR_SKIP_CONTROLPORTTEST"; + const kEnvUseTransparentProxy = "TOR_TRANSPROXY"; + var env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + if ( + (m_tb_control_ipc_file || m_tb_control_port) && + !env.exists(kEnvUseTransparentProxy) && + !env.exists(kEnvSkipControlPortTest) && + m_tb_prefs.getBoolPref("extensions.torbutton.local_tor_check") + ) { + if (await torbutton_local_tor_check()) { + checkSvc.statusOfTorCheck = checkSvc.kCheckSuccessful; + } else { + // The check failed. Update toolbar icon and tooltip. + checkSvc.statusOfTorCheck = checkSvc.kCheckFailed; + } + } else { + // A local check is not possible, so perform a remote check. + torbutton_initiate_remote_tor_check(); + } + } + + async function torbutton_local_tor_check() { + let didLogError = false; + + let proxyType = m_tb_prefs.getIntPref("network.proxy.type"); + if (0 == proxyType) { + return false; + } + + // Ask tor for its SOCKS listener address and port and compare to the + // browser preferences. + const kCmdArg = "net/listeners/socks"; + let resp = await torbutton_send_ctrl_cmd("GETINFO " + kCmdArg); + if (!resp) { + return false; + } + + function logUnexpectedResponse() { + if (!didLogError) { + didLogError = true; + torbutton_log( + 5, + "Local Tor check: unexpected GETINFO response: " + resp + ); + } + } + + function removeBrackets(aStr) { + // Remove enclosing square brackets if present. + if (aStr.startsWith("[") && aStr.endsWith("]")) { + return aStr.substr(1, aStr.length - 2); + } + + return aStr; + } + + // Sample response: net/listeners/socks="127.0.0.1:9149" "127.0.0.1:9150" + // First, check for and remove the command argument prefix. + if (0 != resp.indexOf(kCmdArg + "=")) { + logUnexpectedResponse(); + return false; + } + resp = resp.substr(kCmdArg.length + 1); + + // Retrieve configured proxy settings and check each listener against them. + // When the SOCKS prefs are set to use IPC (e.g., a Unix domain socket), a + // file URL should be present in network.proxy.socks. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1211567 + let socksAddr = m_tb_prefs.getCharPref("network.proxy.socks"); + let socksPort = m_tb_prefs.getIntPref("network.proxy.socks_port"); + let socksIPCPath; + if (socksAddr && socksAddr.startsWith("file:")) { + // Convert the file URL to a file path. + try { + let ioService = Services.io; + let fph = ioService + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + socksIPCPath = fph.getFileFromURLSpec(socksAddr).path; + } catch (e) { + torbutton_log(5, "Local Tor check: IPC file error: " + e); + return false; + } + } else { + socksAddr = removeBrackets(socksAddr); + } + + // Split into quoted strings. This code is adapted from utils.splitAtSpaces() + // within tor-control-port.js; someday this code should use the entire + // tor-control-port.js framework. + let addrArray = []; + resp.replace(/((\S*?"(.*?)")+\S*|\S+)/g, function(a, captured) { + addrArray.push(captured); + }); + + let foundSocksListener = false; + for (let i = 0; !foundSocksListener && i < addrArray.length; ++i) { + let addr; + try { + addr = unescapeTorString(addrArray[i]); + } catch (e) {} + if (!addr) { + continue; + } + + // Remove double quotes if present. + let len = addr.length; + if (len > 2 && '"' == addr.charAt(0) && '"' == addr.charAt(len - 1)) { + addr = addr.substring(1, len - 1); + } + + if (addr.startsWith("unix:")) { + if (!socksIPCPath) { + continue; + } + + // Check against the configured UNIX domain socket proxy. + let path = addr.substring(5); + torbutton_log(2, "Tor socks listener (Unix domain socket): " + path); + foundSocksListener = socksIPCPath === path; + } else if (!socksIPCPath) { + // Check against the configured TCP proxy. We expect addr:port where addr + // may be an IPv6 address; that is, it may contain colon characters. + // Also, we remove enclosing square brackets before comparing addresses + // because tor requires them but Firefox does not. + let idx = addr.lastIndexOf(":"); + if (idx < 0) { + logUnexpectedResponse(); + } else { + let torSocksAddr = removeBrackets(addr.substring(0, idx)); + let torSocksPort = parseInt(addr.substring(idx + 1), 10); + if (torSocksAddr.length < 1 || isNaN(torSocksPort)) { + logUnexpectedResponse(); + } else { + torbutton_log( + 2, + "Tor socks listener: " + torSocksAddr + ":" + torSocksPort + ); + foundSocksListener = + socksAddr === torSocksAddr && socksPort === torSocksPort; + } + } + } + } + + return foundSocksListener; + } // torbutton_local_tor_check + + function torbutton_initiate_remote_tor_check() { + let obsSvc = Services.obs; + try { + let checkSvc = Cc[ + "@torproject.org/torbutton-torCheckService;1" + ].getService(Ci.nsISupports).wrappedJSObject; + let req = checkSvc.createCheckRequest(true); // async + req.onreadystatechange = function(aEvent) { + if (req.readyState === 4) { + let ret = checkSvc.parseCheckResponse(req); + + // If we received an error response from check.torproject.org, + // set the status of the tor check to failure (we don't want + // to indicate failure if we didn't receive a response). + if ( + ret == 2 || + ret == 3 || + ret == 5 || + ret == 6 || + ret == 7 || + ret == 8 + ) { + checkSvc.statusOfTorCheck = checkSvc.kCheckFailed; + obsSvc.notifyObservers(null, k_tb_tor_check_failed_topic); + } else if (ret == 4) { + checkSvc.statusOfTorCheck = checkSvc.kCheckSuccessful; + } // Otherwise, redo the check later + + torbutton_log(3, "Tor remote check done. Result: " + ret); + } + }; + + torbutton_log(3, "Sending async Tor remote check"); + req.send(null); + } catch (e) { + if (e.result == 0x80004005) { + // NS_ERROR_FAILURE + torbutton_log(5, "Tor check failed! Is tor running?"); + } else { + torbutton_log(5, "Tor check failed! Tor internal error: " + e); + } + + obsSvc.notifyObservers(null, k_tb_tor_check_failed_topic); + } + } // torbutton_initiate_remote_tor_check() + + async function torbutton_tor_check_ok() { + await torbutton_do_tor_check(); + let checkSvc = Cc["@torproject.org/torbutton-torCheckService;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + return checkSvc.kCheckFailed != checkSvc.statusOfTorCheck; + } + + function torbutton_update_disk_prefs() { + var mode = m_tb_prefs.getBoolPref("browser.privatebrowsing.autostart"); + + m_tb_prefs.setBoolPref("browser.cache.disk.enable", !mode); + m_tb_prefs.setBoolPref("places.history.enabled", !mode); + + m_tb_prefs.setBoolPref("security.nocertdb", mode); + + // No way to clear this beast during New Identity. Leave it off. + //m_tb_prefs.setBoolPref("dom.indexedDB.enabled", !mode); + + m_tb_prefs.setBoolPref("permissions.memory_only", mode); + + // Third party abuse. Leave it off for now. + //m_tb_prefs.setBoolPref("browser.cache.offline.enable", !mode); + + // Force prefs to be synced to disk + Services.prefs.savePrefFile(null); + } + + function torbutton_update_fingerprinting_prefs() { + var mode = m_tb_prefs.getBoolPref("privacy.resistFingerprinting"); + var letterboxing = m_tb_prefs.getBoolPref( + "privacy.resistFingerprinting.letterboxing", + false + ); + m_tb_prefs.setBoolPref( + "extensions.torbutton.resize_new_windows", + mode && !letterboxing + ); + + // Force prefs to be synced to disk + Services.prefs.savePrefFile(null); + } + + // Bug 1506 P1: This function just cleans up prefs that got set badly in previous releases + function torbutton_fixup_old_prefs() { + if (m_tb_prefs.getIntPref("extensions.torbutton.pref_fixup_version") < 1) { + // TBB 5.0a3 had bad Firefox code that silently flipped this pref on us + if (m_tb_prefs.prefHasUserValue("browser.newtabpage.enhanced")) { + m_tb_prefs.clearUserPref("browser.newtabpage.enhanced"); + // TBB 5.0a3 users had all the necessary data cached in + // directoryLinks.json. This meant that resetting the pref above + // alone was not sufficient as the tiles features uses the cache + // even if the pref indicates that feature should be disabled. + // We flip the preference below as this forces a refetching which + // effectively results in an empty JSON file due to our spoofed + // URLs. + let matchOS = m_tb_prefs.getBoolPref("intl.locale.matchOS"); + m_tb_prefs.setBoolPref("intl.locale.matchOS", !matchOS); + m_tb_prefs.setBoolPref("intl.locale.matchOS", matchOS); + } + + // For some reason, the Share This Page button also survived the + // TBB 5.0a4 update's attempt to remove it. + if (m_tb_prefs.prefHasUserValue("browser.uiCustomization.state")) { + m_tb_prefs.clearUserPref("browser.uiCustomization.state"); + } + + m_tb_prefs.setIntPref("extensions.torbutton.pref_fixup_version", 1); + } + } + + // ---------------------- Event handlers ----------------- + + // Bug 1506 P1-P3: Most of these observers aren't very important. + // See their comments for details + function torbutton_do_main_window_startup() { + torbutton_log(3, "Torbutton main window startup"); + m_tb_is_main_window = true; + torbutton_unique_pref_observer.register(); + } + + // Bug 1506 P4: Most of this function is now useless, save + // for the very important SOCKS environment vars at the end. + // Those could probably be rolled into a function with the + // control port vars, though. See 1506 comments inside. + function torbutton_do_startup() { + if (m_tb_prefs.getBoolPref("extensions.torbutton.startup")) { + // Bug 1506: Should probably be moved to an XPCOM component + torbutton_do_main_window_startup(); + + // For charsets + torbutton_update_fingerprinting_prefs(); + + // Bug 30565: sync browser.privatebrowsing.autostart with security.nocertdb + torbutton_update_disk_prefs(); + + // For general pref fixups to handle pref damage in older versions + torbutton_fixup_old_prefs(); + + m_tb_prefs.setBoolPref("extensions.torbutton.startup", false); + } + } + + // Bug 1506 P3: Used to decide if we should resize the window. + // + // Returns true if the window wind is neither maximized, full screen, + // ratpoisioned/evilwmed, nor minimized. + function torbutton_is_windowed(wind) { + torbutton_log( + 3, + "Window: (" + + wind.outerWidth + + "," + + wind.outerHeight + + ") ?= (" + + wind.screen.availWidth + + "," + + wind.screen.availHeight + + ")" + ); + if ( + wind.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED || + wind.windowState == Ci.nsIDOMChromeWindow.STATE_MAXIMIZED + ) { + torbutton_log(2, "Window is minimized/maximized"); + return false; + } + if ("fullScreen" in wind && wind.fullScreen) { + torbutton_log(2, "Window is fullScreen"); + return false; + } + if ( + wind.outerHeight == wind.screen.availHeight && + wind.outerWidth == wind.screen.availWidth + ) { + torbutton_log(3, "Window is ratpoisoned/evilwm'ed"); + return false; + } + + torbutton_log(2, "Window is normal"); + return true; + } + + function showSecurityPreferencesPanel(chromeWindow) { + const tabBrowser = chromeWindow.BrowserApp; + let settingsTab = null; + + const SECURITY_PREFERENCES_URI = + "chrome://torbutton/content/preferences.xhtml"; + + tabBrowser.tabs.some(function(tab) { + // If the security prefs tab is opened, send the user to it + if (tab.browser.currentURI.spec === SECURITY_PREFERENCES_URI) { + settingsTab = tab; + return true; + } + return false; + }); + + if (settingsTab === null) { + // Open up the settings panel in a new tab. + tabBrowser.addTab(SECURITY_PREFERENCES_URI, { + selected: true, + parentId: tabBrowser.selectedTab.id, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + } else { + // Activate an existing settings panel tab. + tabBrowser.selectTab(settingsTab); + } + } + + function setupPreferencesForMobile() { + if (!torbutton_is_mobile()) { + return; + } + + torbutton_log(4, "Setting up settings preferences for Android."); + + const chromeWindow = Services.wm.getMostRecentWindow("navigator:browser"); + + // Add the extension's chrome menu item to the main browser menu. + chromeWindow.NativeWindow.menu.add({ + name: torbutton_get_property_string( + "torbutton.security_settings.menu.title" + ), + callback: showSecurityPreferencesPanel.bind(this, chromeWindow), + }); + } + + // Bug 1506 P3: This is needed pretty much only for the window resizing. + // See comments for individual functions for details + function torbutton_new_window(event) { + torbutton_log(3, "New window"); + var browser = window.gBrowser; + + if (!browser) { + torbutton_log(5, "No browser for new window."); + return; + } + + if (!m_tb_wasinited) { + torbutton_init(); + } + + torbutton_do_startup(); + + let progress = Cc["@mozilla.org/docloaderservice;1"].getService( + Ci.nsIWebProgress + ); + + if (torbutton_is_windowed(window)) { + progress.addProgressListener( + torbutton_resizelistener, + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT + ); + } + } + + // Bug 1506 P2: This is only needed because we have observers + // in XUL that should be in an XPCOM component + function torbutton_close_window(event) { + torbutton_tor_check_observer.unregister(); + + window.removeEventListener("sizemodechange", m_tb_resize_handler); + + // TODO: This is a real ghetto hack.. When the original window + // closes, we need to find another window to handle observing + // unique events... The right way to do this is to move the + // majority of torbutton functionality into a XPCOM component.. + // But that is a major overhaul.. + if (m_tb_is_main_window) { + torbutton_log(3, "Original window closed. Searching for another"); + var wm = Services.wm; + var enumerator = wm.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + var win = enumerator.getNext(); + // For some reason, when New Identity is called from a pref + // observer (ex: torbutton_use_nontor_proxy) on an ASAN build, + // we sometimes don't have this symbol set in the new window yet. + // However, the new window will run this init later in that case, + // as it does in the OSX case. + if (win != window && "torbutton_do_main_window_startup" in win) { + torbutton_log(3, "Found another window"); + win.torbutton_do_main_window_startup(); + m_tb_is_main_window = false; + break; + } + } + + torbutton_unique_pref_observer.unregister(); + + if (m_tb_is_main_window) { + // main window not reset above + // This happens on Mac OS because they allow firefox + // to still persist without a navigator window + torbutton_log(3, "Last window closed. None remain."); + m_tb_prefs.setBoolPref("extensions.torbutton.startup", true); + m_tb_is_main_window = false; + } + } + } + + window.addEventListener("load", torbutton_new_window); + window.addEventListener("unload", torbutton_close_window); + + var m_tb_resize_handler = null; + var m_tb_resize_date = null; + + // Bug 1506 P1/P3: Setting a fixed window size is important, but + // probably not for android. + var torbutton_resizelistener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + onLocationChange(aProgress, aRequest, aURI) {}, + onStateChange(aProgress, aRequest, aFlag, aStatus) { + if (aFlag & Ci.nsIWebProgressListener.STATE_STOP) { + m_tb_resize_handler = async function() { + // Wait for end of execution queue to ensure we have correct windowState. + await new Promise(resolve => setTimeout(resolve, 0)); + if ( + window.windowState === window.STATE_MAXIMIZED || + window.windowState === window.STATE_FULLSCREEN + ) { + if ( + m_tb_prefs.getBoolPref( + "extensions.torbutton.resize_new_windows" + ) && + m_tb_prefs.getIntPref( + "extensions.torbutton.maximize_warnings_remaining" + ) > 0 + ) { + // Do not add another notification if one is already showing. + const kNotificationName = "torbutton-maximize-notification"; + let box = gBrowser.getNotificationBox(); + if (box.getNotificationWithValue(kNotificationName)) { + return; + } + + // Rate-limit showing our notification if needed. + if (m_tb_resize_date === null) { + m_tb_resize_date = Date.now(); + } else { + // We wait at least another second before we show a new + // notification. Should be enough to rule out OSes that call our + // handler rapidly due to internal workings. + if (Date.now() - m_tb_resize_date < 1000) { + return; + } + // Resizing but we need to reset |m_tb_resize_date| now. + m_tb_resize_date = Date.now(); + } + + // No need to get "OK" translated again. + let sbSvc = Services.strings; + let bundle = sbSvc.createBundle( + "chrome://global/locale/commonDialogs.properties" + ); + let button_label = bundle.GetStringFromName("OK"); + + let buttons = [ + { + label: button_label, + accessKey: "O", + popup: null, + callback() { + m_tb_prefs.setIntPref( + "extensions.torbutton.maximize_warnings_remaining", + m_tb_prefs.getIntPref( + "extensions.torbutton.maximize_warnings_remaining" + ) - 1 + ); + }, + }, + ]; + + let priority = box.PRIORITY_WARNING_LOW; + let message = torbutton_get_property_string( + "torbutton.maximize_warning" + ); + + box.appendNotification( + message, + kNotificationName, + null, + priority, + buttons + ); + } + } + }; // m_tb_resize_handler + + // We need to handle OSes that auto-maximize windows depending on user + // settings and/or screen resolution in the start-up phase and users that + // try to shoot themselves in the foot by maximizing the window manually. + // We add a listener which is triggerred as soon as the window gets + // maximized (windowState = 1). We are resizing during start-up but not + // later as the user should see only a warning there as a stopgap before + // #14229 lands. + // Alas, the Firefox window code is handling the event not itself: + // "// Note the current implementation of SetSizeMode just stores + // // the new state; it doesn't actually resize. So here we store + // // the state and pass the event on to the OS." + // (See: https://mxr.mozilla.org/mozilla-esr31/source/xpfe/appshell/src/ + // nsWebShellWindow.cpp#348) + // This means we have to cope with race conditions and resizing in the + // sizemodechange listener is likely to fail. Thus, we add a specific + // resize listener that is doing the work for us. It seems (at least on + // Ubuntu) to be the case that maximizing (and then again normalizing) of + // the window triggers more than one resize event the first being not the + // one we need. Thus we can't remove the listener after the first resize + // event got fired. Thus, we have the rather klunky setTimeout() call. + window.addEventListener("sizemodechange", m_tb_resize_handler); + + let progress = Cc["@mozilla.org/docloaderservice;1"].getService( + Ci.nsIWebProgress + ); + progress.removeProgressListener(this); + } + }, // onStateChange + + onProgressChange( + aProgress, + aRequest, + curSelfProgress, + maxSelfProgress, + curTotalProgress, + maxTotalProgress + ) {}, + onStatusChange(aProgress, aRequest, stat, message) {}, + onSecurityChange() {}, + }; +})(); +//vim:set ts=4 diff --git a/toolkit/torbutton/chrome/skin/about-wordmark.png b/toolkit/torbutton/chrome/skin/about-wordmark.png new file mode 100644 index 000000000000..1becd875f9ac Binary files /dev/null and b/toolkit/torbutton/chrome/skin/about-wordmark.png differ diff --git a/toolkit/torbutton/chrome/skin/aboutDialog.css b/toolkit/torbutton/chrome/skin/aboutDialog.css new file mode 100644 index 000000000000..981d68e22698 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/aboutDialog.css @@ -0,0 +1,34 @@ +#leftBox { + background-image: url('chrome://branding/content/icon256.png'); + background-position: right top; + background-size: 180px; +} + +#rightBox { + background-image: url('chrome://torbutton/skin/about-wordmark.png'); + background-position: left top; +} + +#contributeDesc { + display: none; +} + +#communityDesc { + display: none; +} + +#trademark { + display: none; +} + +#trademarkTor { + font-size: xx-small; + text-align: center; + color: #999999; + margin-top: 10px; + margin-bottom: 10px; +} + +#bottomBox > hbox:not(#newBottom) { + display: none; +} diff --git a/toolkit/torbutton/chrome/skin/aboutTor.css b/toolkit/torbutton/chrome/skin/aboutTor.css new file mode 100644 index 000000000000..007c46d390b4 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/aboutTor.css @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2019, The Tor Project, Inc. + * See LICENSE for licensing information. + * + * vim: set sw=2 sts=2 ts=8 et syntax=css: + */ + +:root { + --abouttor-text-color: white; + --abouttor-bg-toron-color: #420C5D; + --abouttor-bg-toroff-color: #A4000F; + --onion-opacity: 0.2; + --onion-color: #fff; + --onion-radius: 75px; +} + +* { + padding: 0px; + margin: 0px; +} + +html { + height: 100%; +} + +body { + display: flex; + flex-direction: column; + justify-content: space-between; + width: 100%; + height: 100%; + margin: 0px auto; + padding: 0px 0px; + font-family: Helvetica, Arial, sans-serif; + color: var(--abouttor-text-color); + background-color: var(--abouttor-bg-toroff-color); + background-attachment: fixed; + background-size: 100% 100%; +} + +body[toron] { + background-color: var(--abouttor-bg-toron-color); +} + +/* Hide the entire document by default to avoid showing the incorrect + * Tor on / off status (that info must be retrieved from the chrome + * process, which involves IPC when multiprocess mode is enabled). An + * initialized attribute will be added as soon as the status is known. + */ +body:not([initialized]) { + display: none; +} + +.torcontent-container { + margin: 40px 20px 28px 20px; + display: flex; + flex-direction: column; +} + +@media only screen and (min-width: 900px) { + .torcontent-container { + margin: 40px auto 28px auto; + max-width: 1000px; + } +} + +#torbrowser-info { + position: absolute; + top: 16px; + offset-inline-end: 16px; + inset-inline-end: 16px; + height: 36px; + width: 200px; +} + +#torbrowser-info div { + font-size: 14px; + white-space: pre-wrap; + text-align: end; + margin-bottom: 6px; +} + +/* Hide "View Changelog" link if update channel is not release or alpha. */ +body:not([updatechannel="release"]):not([updatechannel="alpha"]) #torbrowser-changelog-link { + display: none; +} + +a { + color: var(--abouttor-text-color); +} + +#torstatus { + margin-top: 135px; + display: flex; + flex-direction: column; + align-content: center; + justify-content: flex-end; + min-height: 92px; +} + +.top { + white-space: nowrap; +} + +.torstatus-container { + text-align: center; +} + +body[hasbeenupdated] .hideIfHasBeenUpdated, +body[toron] .hideIfTorOn, +body:not([toron]) .hideIfTorOff { + display: none; +} + +body:not([hasbeenupdated]) .showIfHasBeenUpdated { + display: none; +} + +.torstatus-container * { + text-align: center; +} + +.top div.hideIfTorOff .heading1 { + margin-top: 20px; +} + +.top .heading1 { + font-size: 50px; + font-weight: 300; + margin-bottom: 5px; +} + +.top .heading2 { + font-size: 15px; + margin-bottom: 20px; +} + +#bottom { + margin-top: 10vh; + margin-inline: auto; + max-width: 730px; +} + +#bottom p { + font-size: 10px; + text-align: start; + padding-bottom: 20px; +} + +#bottom p:last-child { + padding-bottom: 0px; +} + +#bottom p.moreInfoLink { + font-size: 15px; +} + +#bottom img.imageStyle { + padding-inline-end: 0.5em; + height: 1.5em; + vertical-align: bottom; + -moz-context-properties: fill; + fill: white; +} + +/* Hide the linebreaks on large enough screens (desktops, laptops, and + * tablets). + */ +@media only screen and (min-width: 768px) { + #bottom br { + display: none; + } +} + +.searchbox form { + max-width: 575px; + width: auto; + margin-block: 0; + margin-inline: auto; + text-align: left; +} + +.searchwrapper { + display: flex; + height: 46px; + border: 1px solid black; + border-radius: 8px; + background-color: white; +} + +.searchlabel { + height: auto; + width: 50px; + display: inline-block; + background: url('dax-logo.svg') no-repeat center center; + background-size: 30px 30px; +} + +#search-button { + height: auto; + width: 36px; + border: 0; + background: url('chrome://browser/skin/forward.svg') no-repeat center center; + background-size: 16px 16px; + cursor: pointer; +} + +#search-button:dir(rtl) { + transform: scaleX(-1); +} + +#search-button:hover { + background-color: rgba(12, 12, 13, 0.1); /* same as Firefox's about:home */ +} + +#bannerRoot { + width: 690px; + padding: 20px 25px 25px; + background: rgba(0, 0, 0, 0.3); + border-radius: 6px; + margin: 35px auto 0px; + display: flex; + flex-direction: row; + align-items: flex-start; + position: static; +} + +#bannerTextCol { + padding-top: 8px; + padding-inline-start: 15px; + font-size: 15px; + line-height: normal; + color: #FFFFFF; + width: 633px; +} + +#bannerHeader { + font-weight: bold; +} + +#bannerImg { + width: 32px; + position: static; +} + +a#bannerLink { + text-decoration: underline; +} + +a#bannerLink:hover { + text-decoration: none; +} + +/* #search-text is the search input field */ +#search-text { + flex: 1; + border: none; + padding: 4px 2px 5px 2px; + margin: 0; + font-size: 15px; +} + +#search-text:focus-visible { + outline: none; +} + +#search-button:focus-visible { + outline: 1px dotted black; + outline-offset: -6px; +} + + +/* + * Mobile specific css + */ + +.torcontent-logo { + display: none; +} + +body[mobile] #torbrowser-changelog-link, +body[mobile] .searchbox, +body[mobile] .top .heading2, +body[mobile] #manual, +body[mobile] #mission { + display: none; +} + +body[mobile] .top { + white-space: normal; + margin-top: 0px !important; +} + +body[mobile] .torcontent-logo { + display: block; + margin-top: 20px; + width: 50%; + height: auto; + margin-left: auto; + margin-right: auto; +} + +body[mobile] .top .heading1 { + font-size: 62px; + line-height: 1.1 +} + +body[mobile] .onion-pattern-container { + flex: none; + z-index: -1; /* ensure that circles do not block access to clickable links */ + position: absolute; + bottom: 0px; +} diff --git a/toolkit/torbutton/chrome/skin/banner-warning.svg b/toolkit/torbutton/chrome/skin/banner-warning.svg new file mode 100644 index 000000000000..2f4bf1b887d8 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/banner-warning.svg @@ -0,0 +1 @@ +<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><circle cx="16" cy="16" fill="#ffa436" r="16"/><g fill="#0c0c0d"><path d="m14.2303 23.393c-3.5375-.6033-6.2303-3.6837-6.2303-7.393 0-4.1421 3.3579-7.5 7.5-7.5s7.5 3.3579 7.5 7.5c0 .669-.0876 1.3176-.252 1.9349l-.9277-1.6176c.0052-.1051.0078-.2109.0078-.3173 0-3.4949-2.8332-6.32812-6.3281-6.32812s-6.32812 2.83322-6.32812 6.32812c0 2.9851 2.06682 5.4874 4.84752 6.154-.06.4351.0216.8644.2109 1.239 [...] \ No newline at end of file diff --git a/toolkit/torbutton/chrome/skin/dax-logo.svg b/toolkit/torbutton/chrome/skin/dax-logo.svg new file mode 100644 index 000000000000..94ad7c356329 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/dax-logo.svg @@ -0,0 +1 @@ +<svg width="120" height="120" viewbox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="50.833" cy="50.833" r="50.25"/><linearGradient x1="3.084%" y1="49.368%" x2="100.592%" y2="49.368%" id="c"><stop stop-color="#6176B9" offset=".56%"/><stop stop-color="#394A9F" offset="69.1%"/></linearGradient><linearGradient x1="-.006%" y1="49.006%" x2="98.932%" y2="49.006%" id="d"><stop stop-color="#6176B9" offset=".56%"/><stop stop-co [...] \ No newline at end of file diff --git a/toolkit/torbutton/chrome/skin/icon-newsletter.png b/toolkit/torbutton/chrome/skin/icon-newsletter.png new file mode 100644 index 000000000000..7532ba9c273f Binary files /dev/null and b/toolkit/torbutton/chrome/skin/icon-newsletter.png differ diff --git a/toolkit/torbutton/chrome/skin/preferences-mobile.css b/toolkit/torbutton/chrome/skin/preferences-mobile.css new file mode 100644 index 000000000000..b6d54e8dff63 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/preferences-mobile.css @@ -0,0 +1,47 @@ +.wrapper { + display: flex; +} + +.outer-wrapper { + display: flex; + flex-direction: column; + align-items: center; +} + +.inner-wrapper { + height: 350px; + width: 100%; + max-width: 600px; + flex-direction: column; + align-items: center; +} + +#torbutton_sec_slider { + align-self: stretch; + width: auto; + height: auto; +} + +datalist { + display: flex; + flex-direction: row; + justify-content: space-between; + align-self: stretch; +} + +p { + text-align: justify; +} + +div > p { + margin-top: 0px; +} + +.title { + font-weight: bold; + line-height: 30px; +} + +.wrapper .inner-wrapper * { + margin-top: 10px; +} diff --git a/toolkit/torbutton/chrome/skin/preferences.css b/toolkit/torbutton/chrome/skin/preferences.css new file mode 100644 index 000000000000..013b36771aea --- /dev/null +++ b/toolkit/torbutton/chrome/skin/preferences.css @@ -0,0 +1,7 @@ +.slider-text-weight { + font-weight: bold; +} + +.slider-text-size { + font-size: 95%; +} diff --git a/toolkit/torbutton/chrome/skin/tor-circuit-display.css b/toolkit/torbutton/chrome/skin/tor-circuit-display.css new file mode 100644 index 000000000000..4f03b8824464 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/tor-circuit-display.css @@ -0,0 +1,193 @@ +/* +This CSS file is for styling the tor circuit display, which consists of a title, +a domain, and a bulleted list. + +Each bullet in the circuit node list is supposed to represent a Tor circuit node, +and lines drawn between them to represent Tor network inter-relay connections. +*/ + +#circuit-display-container { + margin-inline: 8px; + margin-top: 0px; + margin-bottom: 4px; + padding: 0px 3px; +} + +#circuit-display-container > toolbarseparator { + margin-block: 0px; + margin-inline: -3px; + width: calc(100% + 6px); +} + +#circuit-display-headline { + margin-inline-start: 8px; +} + +#circuit-display-header { + background-image: url(chrome://torbutton/skin/torbutton.svg); + background-repeat: no-repeat; + background-position: 4px 50%; + background-size: 16px auto; + min-height: 16px; + -moz-context-properties: fill, fill-opacity; + fill: currentColor; + margin-block: 8px; +} + +#circuit-display-header:-moz-locale-dir(rtl) { + background-position-x: right; +} + +#circuit-display-header > hbox { + margin-inline-start: 16px; +} + +#circuit-display-content { + width: 100%; +} + +#circuit-display-content > * { + margin-inline: 0px; + margin-inline-start: 16px; + margin-top: 0px; + margin-bottom: 8px; +} + +#circuit-display-content > hbox:not([hidden]) { + display: flex; +} + +#circuit-reload-button-container { + padding-top: 8px; + justify-content: flex-end; +} + +#circuit-reload-content { + cursor: default; + width: 100%; +} + +/* Format the domain text. */ +#circuit-display-domain { + opacity: 0.8; +} + +#circuit-div { + position: relative; + margin-inline-start: 6px; +} + +/* Format the circuit node list. */ +ul#circuit-display-nodes { + line-height:32px; + padding-inline-start: 8px; +} + +/* Hide default bullets, and draw our own bullets */ +ul#circuit-display-nodes li { + background-image: url(chrome://torbutton/skin/tor-circuit-line.svg); + background-position: left center; + background-repeat: no-repeat; + list-style: none; + /* tor circuit svg are 16px wide */ + padding-inline-start: calc(16px + 0.5em); + white-space: nowrap; + max-height: 32px; + -moz-context-properties: stroke, stroke-opacity; + stroke: currentColor; + stroke-opacity: 100%; +} + +ul#circuit-display-nodes li:dir(rtl) { + background-position: right center; +} + +ul#circuit-display-nodes li:first-child { + background-image: url(chrome://torbutton/skin/tor-circuit-line-first.svg); +} + +ul#circuit-display-nodes li:last-child { + background-image: url(chrome://torbutton/skin/tor-circuit-line-last.svg); +} + +.circuit-ip-address { + font-size: 80%; + opacity: 0.7; + padding-inline-start: 3px; +} + +.circuit-guard-info { + font-size: 80%; + color: var(--button-primary-bgcolor); + font-weight: bold; + padding-inline-start: 3px; +} + +#circuit-reload-button { + background-color: var(--button-primary-bgcolor); + border-radius: 4px; + border-width: 0px; + color: var(--button-primary-color); + font-weight: 600; + padding: 8px 16px; + margin: 0; +} +#circuit-reload-button:hover { + background-color: var(--button-primary-hover-bgcolor); +} + +#circuit-reload-button:active { + background-color: var(--button-primary-active-bgcolor); +} + +#circuit-guard-note-container div { + margin-inline-start: 8px; +} + +.circuit-guard-name { + font-weight: bold; +} + +.circuit-link { + cursor: pointer; + color: var(--urlbar-popup-url-color); +} + +.circuit-link:hover { + text-decoration: underline; +} + +.circuit-onion { + cursor: pointer; +} + +.circuit-onion:hover::after { + opacity: 1; +} + +.circuit-onion::after { + background-color: var(--button-bgcolor);; + border-radius: 4px; + color: var(--button-color); + content: attr(data-text-clicktocopy); + font-size: 80%; + opacity: 0; + padding: 5px 10px; + margin: 10px; + text-align: center; + transition: opacity 0.3s cubic-bezier(0.07, 0.95, 0, 1); +} + +.circuit-onion:hover::after { + background-color: var(--button-hover-bgcolor); +} + +.circuit-onion:active::after { + background-color: var(--button-active-bgcolor); +} + + +.circuit-onion-copied::after { + content: attr(data-text-copied); + opacity: 1; +} diff --git a/toolkit/torbutton/chrome/skin/tor-circuit-line-first.svg b/toolkit/torbutton/chrome/skin/tor-circuit-line-first.svg new file mode 100644 index 000000000000..1981ee875aeb --- /dev/null +++ b/toolkit/torbutton/chrome/skin/tor-circuit-line-first.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="32"> + <g stroke="context-stroke" opacity="context-stroke-opacity" stroke-width="2" fill="transparent"> + <circle cx="50%" cy="50%" r="6"/> + <line x1="50%" y1="22" x2="50%" y2="100%"/> + </g> +</svg> diff --git a/toolkit/torbutton/chrome/skin/tor-circuit-line-last.svg b/toolkit/torbutton/chrome/skin/tor-circuit-line-last.svg new file mode 100644 index 000000000000..19c4adaae2d2 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/tor-circuit-line-last.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="32"> + <g stroke="context-stroke" opacity="context-stroke-opacity" stroke-width="2" fill="transparent"> + <line x1="50%" y1="0%" x2="50%" y2="10"/> + <circle cx="50%" cy="50%" r="6"/> + </g> +</svg> diff --git a/toolkit/torbutton/chrome/skin/tor-circuit-line.svg b/toolkit/torbutton/chrome/skin/tor-circuit-line.svg new file mode 100644 index 000000000000..00717f6c2054 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/tor-circuit-line.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="32"> + <g stroke="context-stroke" opacity="context-stroke-opacity" stroke-width="2" fill="transparent"> + <line x1="50%" y1="0%" x2="50%" y2="10"/> + <circle cx="50%" cy="50%" r="6"/> + <line x1="50%" y1="22" x2="50%" y2="100%"/> + </g> +</svg> diff --git a/toolkit/torbutton/chrome/skin/tor.png b/toolkit/torbutton/chrome/skin/tor.png new file mode 100644 index 000000000000..75b86bfb940a Binary files /dev/null and b/toolkit/torbutton/chrome/skin/tor.png differ diff --git a/toolkit/torbutton/chrome/skin/torbrowser_mobile_logo.png b/toolkit/torbutton/chrome/skin/torbrowser_mobile_logo.png new file mode 100644 index 000000000000..9d8317f02b2c Binary files /dev/null and b/toolkit/torbutton/chrome/skin/torbrowser_mobile_logo.png differ diff --git a/toolkit/torbutton/chrome/skin/torbutton.css b/toolkit/torbutton/chrome/skin/torbutton.css new file mode 100644 index 000000000000..fff1714bcee5 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/torbutton.css @@ -0,0 +1,14 @@ +svg.circuit text { + font-family: Arial; +} + +svg#tor-circuit text.node-text { + dominant-baseline: central; + font-size: 14px; +} + +svg#tor-circuit circle.node-circle { + stroke: #195021; + stroke-width: 2px; + fill: white; +} \ No newline at end of file diff --git a/toolkit/torbutton/chrome/skin/torbutton.svg b/toolkit/torbutton/chrome/skin/torbutton.svg new file mode 100644 index 000000000000..30cd52ba5c51 --- /dev/null +++ b/toolkit/torbutton/chrome/skin/torbutton.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <path fill="context-fill" fill-opacity="context-fill-opacity" d="M12.0246161,21.8174863 L12.0246161,20.3628098 C16.6324777,20.3495038 20.3634751,16.6108555 20.3634751,11.9996673 C20.3634751,7.38881189 16.6324777,3.65016355 12.0246161,3.63685757 L12.0246161,2.18218107 C17.4358264,2.1958197 21.8178189,6.58546322 21.8178189,11.9996673 C21.8178189,17.4142042 17.4358264,21.8041803 12.0246161,21.8174863 L12.0246161,21.8174863 Z M12.0246161,16.7259522 C14.623607,16.7123136 16.7272828,14.60231 [...] +</svg> \ No newline at end of file diff --git a/toolkit/torbutton/components/domain-isolator.js b/toolkit/torbutton/components/domain-isolator.js new file mode 100644 index 000000000000..1c77b5776f0f --- /dev/null +++ b/toolkit/torbutton/components/domain-isolator.js @@ -0,0 +1,228 @@ +// # domain-isolator.js +// A component for TorBrowser that puts requests from different +// first party domains on separate tor circuits. + +// This file is written in call stack order (later functions +// call earlier functions). The code file can be processed +// with docco.js to provide clear documentation. + +// ### Abbreviations + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + ComponentUtils: "resource://gre/modules/ComponentUtils.jsm", +}); + +// Make the logger available. +let logger = Cc["@torproject.org/torbutton-logger;1"].getService(Ci.nsISupports) + .wrappedJSObject; + +// Import crypto object (FF 37+). +Cu.importGlobalProperties(["crypto"]); + +// ## mozilla namespace. +// Useful functionality for interacting with Mozilla services. +let mozilla = {}; + +// __mozilla.protocolProxyService__. +// Mozilla's protocol proxy service, useful for managing proxy connections made +// by the browser. +mozilla.protocolProxyService = Cc[ + "@mozilla.org/network/protocol-proxy-service;1" +].getService(Ci.nsIProtocolProxyService); + +// __mozilla.registerProxyChannelFilter(filterFunction, positionIndex)__. +// Registers a proxy channel filter with the Mozilla Protocol Proxy Service, +// which will help to decide the proxy to be used for a given channel. +// The filterFunction should expect two arguments, (aChannel, aProxy), +// where aProxy is the proxy or list of proxies that would be used by default +// for the given channel, and should return a new Proxy or list of Proxies. +mozilla.registerProxyChannelFilter = function(filterFunction, positionIndex) { + let proxyFilter = { + applyFilter(aChannel, aProxy, aCallback) { + aCallback.onProxyFilterResult(filterFunction(aChannel, aProxy)); + }, + }; + mozilla.protocolProxyService.registerChannelFilter( + proxyFilter, + positionIndex + ); +}; + +// ## tor functionality. +let tor = {}; + +// __tor.noncesForDomains__. +// A mutable map that records what nonce we are using for each domain. +tor.noncesForDomains = {}; + +// __tor.isolationEabled__. +// A bool that controls if we use SOCKS auth for isolation or not. +tor.isolationEnabled = true; + +// __tor.unknownDirtySince__. +// Specifies when the current catch-all circuit was first used +tor.unknownDirtySince = Date.now(); + +// __tor.socksProxyCredentials(originalProxy, domain)__. +// Takes a proxyInfo object (originalProxy) and returns a new proxyInfo +// object with the same properties, except the username is set to the +// the domain, and the password is a nonce. +tor.socksProxyCredentials = function(originalProxy, domain) { + // Check if we already have a nonce. If not, create + // one for this domain. + if (!tor.noncesForDomains.hasOwnProperty(domain)) { + tor.noncesForDomains[domain] = tor.nonce(); + } + let proxy = originalProxy.QueryInterface(Ci.nsIProxyInfo); + return mozilla.protocolProxyService.newProxyInfoWithAuth( + "socks", + proxy.host, + proxy.port, + domain, // username + tor.noncesForDomains[domain], // password + "", // aProxyAuthorizationHeader + "", // aConnectionIsolationKey + proxy.flags, + proxy.failoverTimeout, + proxy.failoverProxy + ); +}; + +tor.nonce = function() { + // Generate a new 128 bit random tag. Strictly speaking both using a + // cryptographic entropy source and using 128 bits of entropy for the + // tag are likely overkill, as correct behavior only depends on how + // unlikely it is for there to be a collision. + let tag = new Uint8Array(16); + crypto.getRandomValues(tag); + + // Convert the tag to a hex string. + let tagStr = ""; + for (let i = 0; i < tag.length; i++) { + tagStr += (tag[i] >>> 4).toString(16); + tagStr += (tag[i] & 0x0f).toString(16); + } + + return tagStr; +}; + +tor.newCircuitForDomain = function(domain) { + // Re-generate the nonce for the domain. + if (domain === "") { + domain = "--unknown--"; + } + tor.noncesForDomains[domain] = tor.nonce(); + logger.eclog( + 3, + "New domain isolation for " + domain + ": " + tor.noncesForDomains[domain] + ); +}; + +// __tor.clearIsolation()_. +// Clear the isolation state cache, forcing new circuits to be used for all +// subsequent requests. +tor.clearIsolation = function() { + // Per-domain nonces are stored in a map, so simply re-initialize the map. + tor.noncesForDomains = {}; + + // Force a rotation on the next catch-all circuit use by setting the creation + // time to the epoch. + tor.unknownDirtySince = 0; +}; + +// __tor.isolateCircuitsByDomain()__. +// For every HTTPChannel, replaces the default SOCKS proxy with one that authenticates +// to the SOCKS server (the tor client process) with a username (the first party domain) +// and a nonce password. Tor provides a separate circuit for each username+password +// combination. +tor.isolateCircuitsByDomain = function() { + mozilla.registerProxyChannelFilter(function(aChannel, aProxy) { + if (!tor.isolationEnabled) { + return aProxy; + } + try { + let channel = aChannel.QueryInterface(Ci.nsIChannel), + firstPartyDomain = channel.loadInfo.originAttributes.firstPartyDomain; + if (firstPartyDomain === "") { + firstPartyDomain = "--unknown--"; + if (Date.now() - tor.unknownDirtySince > 1000 * 10 * 60) { + logger.eclog( + 3, + "tor catchall circuit has been dirty for over 10 minutes. Rotating." + ); + tor.newCircuitForDomain("--unknown--"); + tor.unknownDirtySince = Date.now(); + } + } + let replacementProxy = tor.socksProxyCredentials( + aProxy, + firstPartyDomain + ); + logger.eclog( + 3, + `tor SOCKS: ${channel.URI.spec} via + ${replacementProxy.username}:${replacementProxy.password}` + ); + return replacementProxy; + } catch (e) { + logger.eclog(4, `tor domain isolator error: ${e.message}`); + return null; + } + }, 0); +}; + +// ## XPCOM component construction. +// Module specific constants +const kMODULE_NAME = "TorBrowser Domain Isolator"; +const kMODULE_CONTRACTID = "@torproject.org/domain-isolator;1"; +const kMODULE_CID = Components.ID("e33fd6d4-270f-475f-a96f-ff3140279f68"); + +// DomainIsolator object. +function DomainIsolator() { + this.wrappedJSObject = this; +} + +// Firefox component requirements +DomainIsolator.prototype = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]), + classDescription: kMODULE_NAME, + classID: kMODULE_CID, + contractID: kMODULE_CONTRACTID, + observe(subject, topic, data) { + if (topic === "profile-after-change") { + logger.eclog(3, "domain isolator: set up isolating circuits by domain"); + + if (Services.prefs.getBoolPref("extensions.torbutton.use_nontor_proxy")) { + tor.isolationEnabled = false; + } + tor.isolateCircuitsByDomain(); + } + }, + newCircuitForDomain(domain) { + tor.newCircuitForDomain(domain); + }, + + enableIsolation() { + tor.isolationEnabled = true; + }, + + disableIsolation() { + tor.isolationEnabled = false; + }, + + clearIsolation() { + tor.clearIsolation(); + }, + + wrappedJSObject: null, +}; + +// Assign factory to global object. +const NSGetFactory = XPCOMUtils.generateNSGetFactory + ? XPCOMUtils.generateNSGetFactory([DomainIsolator]) + : ComponentUtils.generateNSGetFactory([DomainIsolator]); diff --git a/toolkit/torbutton/components/dragDropFilter.js b/toolkit/torbutton/components/dragDropFilter.js new file mode 100644 index 000000000000..f763595be5e6 --- /dev/null +++ b/toolkit/torbutton/components/dragDropFilter.js @@ -0,0 +1,134 @@ +/************************************************************************* + * Drag and Drop Handler. + * + * Implements an observer that filters drag events to prevent OS + * access to URLs (a potential proxy bypass vector). + *************************************************************************/ + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + ComponentUtils: "resource://gre/modules/ComponentUtils.jsm", +}); + +// Module specific constants +const kMODULE_NAME = "Torbutton Drag and Drop Handler"; +const kCONTRACT_ID = "@torproject.org/torbutton-dragDropFilter;1"; +const kMODULE_CID = Components.ID("f605ec27-d867-44b5-ad97-2a29276642c3"); + +const kInterfaces = [Ci.nsIObserver, Ci.nsIClassInfo]; + +const URLISH_TYPES = Object.freeze([ + "text/x-moz-url", + "text/x-moz-url-data", + "text/uri-list", + "application/x-moz-file-promise-url", +]); + +/* + Returns true if the text resembles a URL or even just a hostname + in a way that may prompt the O.S. or other applications to send out a + validation DNS query, if not a full request (e.g. " torproject.org", + even with the leading whitespace). +*/ +function isURLish(text) { + // Ignore leading whitespace. + text = text.trim(); + + // Without any protocol or dot in the first chunk, this is unlikely + // to be considered URLish (exception: localhost, but we don't care). + if (!/^[a-z][a-z0-9+-]*:///i.test(text)) { + // no protocol + if (!/^[^.\s/]+.[^.\s/]/.test(text)) { + // no dot + return false; + } + // Prepare for hostname validation via relative URL building. + text = `//${text}`; + } + // Validate URL or hostname. + try { + new URL(text, "https://localhost"); + return true; + } catch (e) { + // invalid URL, bail out + } + return false; +} + +// Returns true if any chunk of text is URLish +const hasURLish = text => text.split(/[^\p{L}_.-:/%~@$-]+/u).some(isURLish); + +function DragDropFilter() { + this.logger = Cc["@torproject.org/torbutton-logger;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + this.logger.log(3, "Component Load 0: New DragDropFilter."); + + try { + Services.obs.addObserver(this, "on-datatransfer-available"); + } catch (e) { + this.logger.log(5, "Failed to register drag observer"); + } +} + +DragDropFilter.prototype = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]), + + // make this an nsIClassInfo object + flags: Ci.nsIClassInfo.DOM_OBJECT, + classDescription: kMODULE_NAME, + contractID: kCONTRACT_ID, + classID: kMODULE_CID, + + // method of nsIClassInfo + getInterfaces(count) { + count.value = kInterfaces.length; + return kInterfaces; + }, + + // method of nsIClassInfo + getHelperForLanguage(count) { + return null; + }, + + // method of nsIObserver + observe(subject, topic, data) { + if (topic === "on-datatransfer-available") { + this.logger.log(3, "The DataTransfer is available"); + this.filterDataTransferURLs(subject); + } + }, + + filterDataTransferURLs(aDataTransfer) { + for (let i = 0, count = aDataTransfer.mozItemCount; i < count; ++i) { + this.logger.log(3, `Inspecting the data transfer: ${i}.`); + const types = aDataTransfer.mozTypesAt(i); + for (const type of types) { + this.logger.log(3, `Type is: ${type}.`); + if ( + URLISH_TYPES.includes(type) || + ((type === "text/plain" || type === "text/html") && + hasURLish(aDataTransfer.getData(type))) + ) { + this.logger.log( + 3, + `Removing transfer data ${aDataTransfer.getData(type)}` + ); + for (const type of types) { + aDataTransfer.clearData(type); + } + break; + } + } + } + }, +}; + +// Assign factory to global object. +const NSGetFactory = XPCOMUtils.generateNSGetFactory + ? XPCOMUtils.generateNSGetFactory([DragDropFilter]) + : ComponentUtils.generateNSGetFactory([DragDropFilter]); diff --git a/toolkit/torbutton/components/external-app-blocker.js b/toolkit/torbutton/components/external-app-blocker.js new file mode 100644 index 000000000000..6a53fc01eaf9 --- /dev/null +++ b/toolkit/torbutton/components/external-app-blocker.js @@ -0,0 +1,160 @@ +// Bug 1506 Android P1/TBB P5: This code provides users with notification +// in the event of external app launch. We want it to exist in the desktop +// port, but it is probably useless for Android. + +/************************************************************************* + * External App Handler. + * Handles displaying confirmation dialogs for external apps and protocols + * due to Firefox Bug https://bugzilla.mozilla.org/show_bug.cgi?id=440892 + * + * An instance of this module is created each time the browser starts to + * download a file and when an external application may be invoked to + * handle an URL (e.g., when the user clicks on a mailto: URL). + *************************************************************************/ + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { PromptUtils } = ChromeUtils.import( + "resource://gre/modules/SharedPromptUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + ComponentUtils: "resource://gre/modules/ComponentUtils.jsm", +}); + +let { torbutton_get_property_string } = ChromeUtils.import( + "resource://torbutton/modules/utils.js" +); + +// Module specific constants +const kMODULE_NAME = "Torbutton External App Handler"; +const kCONTRACT_ID = "@torproject.org/torbutton-extAppBlocker;1"; +const kMODULE_CID = Components.ID("3da0269f-fc29-4e9e-a678-c3b1cafcf13f"); + +const kInterfaces = [Ci.nsIObserver, Ci.nsIClassInfo]; + +function ExternalAppBlocker() { + this.logger = Cc["@torproject.org/torbutton-logger;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + this.logger.log(3, "Component Load 0: New ExternalAppBlocker."); +} + +ExternalAppBlocker.prototype = { + _helperAppLauncher: undefined, + + QueryInterface: ChromeUtils.generateQI([ + Ci.nsIObserver, + Ci.nsIHelperAppWarningDialog, + ]), + + // make this an nsIClassInfo object + flags: Ci.nsIClassInfo.DOM_OBJECT, + classDescription: kMODULE_NAME, + contractID: kCONTRACT_ID, + classID: kMODULE_CID, + + // method of nsIClassInfo + getInterfaces(count) { + count.value = kInterfaces.length; + return kInterfaces; + }, + + // method of nsIClassInfo + getHelperForLanguage(count) { + return null; + }, + + // method of nsIHelperAppWarningDialog + maybeShow(aLauncher, aWindowContext) { + // Hold a reference to the object that called this component. This is + // important not just because we need to later invoke the + // continueRequest() or cancelRequest() callback on aLauncher, but also + // so that the launcher object (which is a reference counted object) is + // not released too soon. + this._helperAppLauncher = aLauncher; + + if (!Services.prefs.getBoolPref("extensions.torbutton.launch_warning")) { + this._helperAppLauncher.continueRequest(); + return; + } + + this._showPrompt(aWindowContext); + }, + + /* + * The _showPrompt() implementation uses some XUL and JS that is part of the + * browser's confirmEx() implementation. Specifically, _showPrompt() depends + * on chrome://global/content/commonDialog.xhtml as well as some of the code + * in resource://gre/modules/SharedPromptUtils.jsm. + */ + _showPrompt(aWindowContext) { + let parentWin; + try { + parentWin = aWindowContext.getInterface(Ci.nsIDOMWindow); + } catch (e) { + parentWin = Services.wm.getMostRecentWindow("navigator:browser"); + } + + let title = torbutton_get_property_string("torbutton.popup.external.title"); + let app = torbutton_get_property_string("torbutton.popup.external.app"); + let note = torbutton_get_property_string("torbutton.popup.external.note"); + let suggest = torbutton_get_property_string( + "torbutton.popup.external.suggest" + ); + let launch = torbutton_get_property_string("torbutton.popup.launch"); + let cancel = torbutton_get_property_string("torbutton.popup.cancel"); + let dontask = torbutton_get_property_string("torbutton.popup.dontask"); + + let args = { + promptType: "confirmEx", + title, + text: app + note + suggest + " ", + checkLabel: dontask, + checked: false, + ok: false, + button0Label: launch, + button1Label: cancel, + defaultButtonNum: 1, // Cancel + buttonNumClicked: 1, // Cancel + enableDelay: true, + }; + + let propBag = PromptUtils.objectToPropBag(args); + let uri = "chrome://global/content/commonDialog.xhtml"; + let promptWin = Services.ww.openWindow( + parentWin, + uri, + "_blank", + "centerscreen,chrome,titlebar", + propBag + ); + promptWin.addEventListener("load", aEvent => { + promptWin.addEventListener("unload", aEvent => { + PromptUtils.propBagToObject(propBag, args); + + if (0 == args.buttonNumClicked) { + // Save the checkbox value and tell the browser's external helper app + // module about the user's choice. + if (args.checked) { + Services.prefs.setBoolPref( + "extensions.torbutton.launch_warning", + false + ); + } + + this._helperAppLauncher.continueRequest(); + } else { + this._helperAppLauncher.cancelRequest(Cr.NS_BINDING_ABORTED); + } + }); + }); + }, +}; + +// Assign factory to global object. +const NSGetFactory = XPCOMUtils.generateNSGetFactory + ? XPCOMUtils.generateNSGetFactory([ExternalAppBlocker]) + : ComponentUtils.generateNSGetFactory([ExternalAppBlocker]); diff --git a/toolkit/torbutton/components/startup-observer.js b/toolkit/torbutton/components/startup-observer.js new file mode 100644 index 000000000000..8f9b8cc24bc3 --- /dev/null +++ b/toolkit/torbutton/components/startup-observer.js @@ -0,0 +1,196 @@ +// Bug 1506 P1-3: This code is mostly hackish remnants of session store +// support. There are a couple of observer events that *might* be worth +// listening to. Search for 1506 in the code. + +/************************************************************************* + * Startup observer (JavaScript XPCOM component) + * + * Cases tested (each during Tor and Non-Tor, FF4 and FF3.6) + * 1. Crash + * 2. Upgrade + * 3. Fresh install + * + *************************************************************************/ + +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +const { TorProtocolService } = ChromeUtils.import( + "resource://gre/modules/TorProtocolService.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + ComponentUtils: "resource://gre/modules/ComponentUtils.jsm", +}); + +// Module specific constants +const kMODULE_NAME = "Startup"; +const kMODULE_CONTRACTID = "@torproject.org/startup-observer;1"; +const kMODULE_CID = Components.ID("06322def-6fde-4c06-aef6-47ae8e799629"); + +function cleanupCookies() { + const migratedPref = "extensions.torbutton.cookiejar_migrated"; + if (!Services.prefs.getBoolPref(migratedPref, false)) { + // Cleanup stored cookie-jar-selector json files + const profileFolder = Services.dirsvc.get("ProfD", Ci.nsIFile).clone(); + for (const file of profileFolder.directoryEntries) { + if (file.leafName.match(/^(cookies|protected)-.*[.]json$/)) { + try { + file.remove(false); + } catch (e) {} + } + } + Services.prefs.setBoolPref(migratedPref, true); + } +} + +function StartupObserver() { + this.logger = Cc["@torproject.org/torbutton-logger;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + this._prefs = Services.prefs; + this.logger.log(3, "Startup Observer created"); + + var env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + var prefName = "browser.startup.homepage"; + if (env.exists("TOR_DEFAULT_HOMEPAGE")) { + // if the user has set this value in a previous installation, don't override it + if (!this._prefs.prefHasUserValue(prefName)) { + this._prefs.setCharPref(prefName, env.get("TOR_DEFAULT_HOMEPAGE")); + } + } + + this.is_tbb = true; + + try { + // XXX: We're in a race with HTTPS-Everywhere to update our proxy settings + // before the initial SSL-Observatory test... If we lose the race, Firefox + // caches the old proxy settings for check.tp.o somehwere, and it never loads :( + this.setProxySettings(); + } catch (e) { + this.logger.log( + 4, + "Early proxy change failed. Will try again at profile load. Error: " + e + ); + } + + cleanupCookies(); +} + +StartupObserver.prototype = { + // Bug 6803: We need to get the env vars early due to + // some weird proxy caching code that showed up in FF15. + // Otherwise, homepage domain loads fail forever. + setProxySettings() { + if (!this.is_tbb) { + return; + } + + // Bug 1506: Still want to get these env vars + let environ = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + if (environ.exists("TOR_TRANSPROXY")) { + this.logger.log(3, "Resetting Tor settings to transproxy"); + this._prefs.setBoolPref("network.proxy.socks_remote_dns", false); + this._prefs.setIntPref("network.proxy.type", 0); + this._prefs.setIntPref("network.proxy.socks_port", 0); + this._prefs.setCharPref("network.proxy.socks", ""); + } else { + // Try to retrieve SOCKS proxy settings from Tor Launcher. + let socksPortInfo; + try { + socksPortInfo = TorProtocolService.torGetSOCKSPortInfo(); + } catch (e) { + this.logger.log(3, "tor launcher failed " + e); + } + + // If Tor Launcher is not available, check environment variables. + if (!socksPortInfo) { + socksPortInfo = { ipcFile: undefined, host: undefined, port: 0 }; + + let isWindows = Services.appinfo.OS === "WINNT"; + if (!isWindows && environ.exists("TOR_SOCKS_IPC_PATH")) { + socksPortInfo.ipcFile = new FileUtils.File( + environ.get("TOR_SOCKS_IPC_PATH") + ); + } else { + if (environ.exists("TOR_SOCKS_HOST")) { + socksPortInfo.host = environ.get("TOR_SOCKS_HOST"); + } + if (environ.exists("TOR_SOCKS_PORT")) { + socksPortInfo.port = parseInt(environ.get("TOR_SOCKS_PORT")); + } + } + } + + // Adjust network.proxy prefs. + if (socksPortInfo.ipcFile) { + let fph = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + let fileURI = fph.newFileURI(socksPortInfo.ipcFile); + this.logger.log(3, "Reset socks to " + fileURI.spec); + this._prefs.setCharPref("network.proxy.socks", fileURI.spec); + this._prefs.setIntPref("network.proxy.socks_port", 0); + } else { + if (socksPortInfo.host) { + this._prefs.setCharPref("network.proxy.socks", socksPortInfo.host); + this.logger.log(3, "Reset socks host to " + socksPortInfo.host); + } + if (socksPortInfo.port) { + this._prefs.setIntPref( + "network.proxy.socks_port", + socksPortInfo.port + ); + this.logger.log(3, "Reset socks port to " + socksPortInfo.port); + } + } + + if (socksPortInfo.ipcFile || socksPortInfo.host || socksPortInfo.port) { + this._prefs.setBoolPref("network.proxy.socks_remote_dns", true); + this._prefs.setIntPref("network.proxy.type", 1); + } + } + + // Force prefs to be synced to disk + Services.prefs.savePrefFile(null); + + this.logger.log(3, "Synced network settings to environment."); + }, + + observe(subject, topic, data) { + if (topic == "profile-after-change") { + // Bug 1506 P1: We listen to these prefs as signals for startup, + // but only for hackish reasons. + this._prefs.setBoolPref("extensions.torbutton.startup", true); + + this.setProxySettings(); + } + + // In all cases, force prefs to be synced to disk + Services.prefs.savePrefFile(null); + }, + + QueryInterface: ChromeUtils.generateQI([Ci.nsIClassInfo]), + + // method of nsIClassInfo + classDescription: "Torbutton Startup Observer", + classID: kMODULE_CID, + contractID: kMODULE_CONTRACTID, + + // Hack to get us registered early to observe recovery + _xpcom_categories: [{ category: "profile-after-change" }], +}; + +// Assign factory to global object. +const NSGetFactory = XPCOMUtils.generateNSGetFactory + ? XPCOMUtils.generateNSGetFactory([StartupObserver]) + : ComponentUtils.generateNSGetFactory([StartupObserver]); diff --git a/toolkit/torbutton/components/torCheckService.js b/toolkit/torbutton/components/torCheckService.js new file mode 100644 index 000000000000..41d716ff935c --- /dev/null +++ b/toolkit/torbutton/components/torCheckService.js @@ -0,0 +1,140 @@ +/************************************************************************* + * Copyright (c) 2013, The Tor Project, Inc. + * See LICENSE for licensing information. + * + * vim: set sw=2 sts=2 ts=8 et syntax=javascript: + * + * Tor check service + *************************************************************************/ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + ComponentUtils: "resource://gre/modules/ComponentUtils.jsm", +}); + +// Module specific constants +const kMODULE_NAME = "Torbutton Tor Check Service"; +const kMODULE_CONTRACTID = "@torproject.org/torbutton-torCheckService;1"; +const kMODULE_CID = Components.ID("5d57312b-5d8c-4169-b4af-e80d6a28a72e"); + +function TBTorCheckService() { + this._logger = Cc["@torproject.org/torbutton-logger;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + this._logger.log(3, "Torbutton Tor Check Service initialized"); + + this._statusOfTorCheck = this.kCheckNotInitiated; + this.wrappedJSObject = this; +} + +TBTorCheckService.prototype = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIClassInfo]), + + kCheckNotInitiated: 0, // Possible values for statusOfTorCheck. + kCheckSuccessful: 1, + kCheckFailed: 2, + + wrappedJSObject: null, + _logger: null, + _statusOfTorCheck: 0, // this.kCheckNotInitiated, + + // make this an nsIClassInfo object + flags: Ci.nsIClassInfo.DOM_OBJECT, + + // method of nsIClassInfo + classDescription: kMODULE_NAME, + classID: kMODULE_CID, + contractID: kMODULE_CONTRACTID, + + // method of nsIClassInfo + getInterfaces(count) { + var interfaceList = [Ci.nsIClassInfo]; + count.value = interfaceList.length; + return interfaceList; + }, + + // method of nsIClassInfo + getHelperForLanguage(count) { + return null; + }, + + // Public methods. + get statusOfTorCheck() { + return this._statusOfTorCheck; + }, + + set statusOfTorCheck(aStatus) { + this._statusOfTorCheck = aStatus; + }, + + createCheckRequest(aAsync) { + let req = new XMLHttpRequest(); + let url = Services.prefs.getCharPref("extensions.torbutton.test_url"); + req.open("GET", url, aAsync); + req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + req.overrideMimeType("text/xml"); + req.timeout = 120000; // Wait at most two minutes for a response. + return req; + }, + + parseCheckResponse(aReq) { + let ret = 0; + if (aReq.status == 200) { + if (!aReq.responseXML) { + this._logger.log(5, "Check failed! Not text/xml!"); + ret = 1; + } else { + let result = aReq.responseXML.getElementById("TorCheckResult"); + + if (result === null) { + this._logger.log(5, "Test failed! No TorCheckResult element"); + ret = 2; + } else if ( + typeof result.target == "undefined" || + result.target === null + ) { + this._logger.log(5, "Test failed! No target"); + ret = 3; + } else if (result.target === "success") { + this._logger.log(3, "Test Successful"); + ret = 4; + } else if (result.target === "failure") { + this._logger.log(5, "Tor test failed!"); + ret = 5; + } else if (result.target === "unknown") { + this._logger.log(5, "Tor test failed. TorDNSEL Failure?"); + ret = 6; + } else { + this._logger.log(5, "Tor test failed. Strange target."); + ret = 7; + } + } + } else { + if (0 == aReq.status) { + try { + var req = aReq.channel.QueryInterface(Ci.nsIRequest); + if (req.status == Cr.NS_ERROR_PROXY_CONNECTION_REFUSED) { + this._logger.log(5, "Tor test failed. Proxy connection refused"); + ret = 8; + } + } catch (e) {} + } + + if (ret == 0) { + this._logger.log(5, "Tor test failed. HTTP Error: " + aReq.status); + ret = -aReq.status; + } + } + + return ret; + }, +}; + +// Assign factory to global object. +const NSGetFactory = XPCOMUtils.generateNSGetFactory + ? XPCOMUtils.generateNSGetFactory([TBTorCheckService]) + : ComponentUtils.generateNSGetFactory([TBTorCheckService]); diff --git a/toolkit/torbutton/components/torbutton-logger.js b/toolkit/torbutton/components/torbutton-logger.js new file mode 100644 index 000000000000..2fdcd7e6a753 --- /dev/null +++ b/toolkit/torbutton/components/torbutton-logger.js @@ -0,0 +1,185 @@ +// Bug 1506 P1: This is just a handy logger. If you have a better one, toss +// this in the trash. + +/************************************************************************* + * TBLogger (JavaScript XPCOM component) + * + * Allows loglevel-based logging to different logging mechanisms. + * + *************************************************************************/ + +// Module specific constants +const kMODULE_NAME = "Torbutton Logger"; +const kMODULE_CONTRACTID = "@torproject.org/torbutton-logger;1"; +const kMODULE_CID = Components.ID("f36d72c9-9718-4134-b550-e109638331d7"); + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + ComponentUtils: "resource://gre/modules/ComponentUtils.jsm", +}); + +function TorbuttonLogger() { + // Register observer + Services.prefs.addObserver("extensions.torbutton", this); + + this.loglevel = Services.prefs.getIntPref("extensions.torbutton.loglevel"); + this.logmethod = Services.prefs.getIntPref("extensions.torbutton.logmethod"); + + try { + var logMngr = Cc["@mozmonkey.com/debuglogger/manager;1"].getService( + Ci.nsIDebugLoggerManager + ); + this._debuglog = logMngr.registerLogger("torbutton"); + } catch (exErr) { + this._debuglog = false; + } + this._console = Services.console; + + // This JSObject is exported directly to chrome + this.wrappedJSObject = this; + this.log(3, "Torbutton debug output ready"); +} + +/** + * JS XPCOM component registration goop: + * + * Everything below is boring boilerplate and can probably be ignored. + */ + +const nsIClassInfo = Ci.nsIClassInfo; + +const logString = { 1: "VERB", 2: "DBUG", 3: "INFO", 4: "NOTE", 5: "WARN" }; + +function padInt(i) { + return i < 10 ? "0" + i : i; +} + +TorbuttonLogger.prototype = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIClassInfo]), + + wrappedJSObject: null, // Initialized by constructor + + // make this an nsIClassInfo object + flags: nsIClassInfo.DOM_OBJECT, + + // method of nsIClassInfo + classDescription: "TorbuttonLogger", + classID: kMODULE_CID, + contractID: kMODULE_CONTRACTID, + + // method of nsIClassInfo + getInterfaces(count) { + var interfaceList = [nsIClassInfo]; + count.value = interfaceList.length; + return interfaceList; + }, + + // method of nsIClassInfo + getHelperForLanguage(count) { + return null; + }, + + formatLog(str, level) { + var d = new Date(); + var now = + padInt(d.getUTCMonth() + 1) + + "-" + + padInt(d.getUTCDate()) + + " " + + padInt(d.getUTCHours()) + + ":" + + padInt(d.getUTCMinutes()) + + ":" + + padInt(d.getUTCSeconds()); + return "[" + now + "] Torbutton " + logString[level] + ": " + str; + }, + + // error console log + eclog(level, str) { + switch (this.logmethod) { + case 0: // stderr + if (this.loglevel <= level) { + dump(this.formatLog(str, level) + "\n"); + } + break; + default: + // errorconsole + if (this.loglevel <= level) { + this._console.logStringMessage(this.formatLog(str, level)); + } + break; + } + }, + + safe_log(level, str, scrub) { + if (this.loglevel < 4) { + this.eclog(level, str + scrub); + } else { + this.eclog(level, str + " [scrubbed]"); + } + }, + + log(level, str) { + switch (this.logmethod) { + case 2: // debuglogger + if (this._debuglog) { + this._debuglog.log(6 - level, this.formatLog(str, level)); + break; + } + // fallthrough + case 0: // stderr + if (this.loglevel <= level) { + dump(this.formatLog(str, level) + "\n"); + } + break; + case 1: // errorconsole + if (this.loglevel <= level) { + this._console.logStringMessage(this.formatLog(str, level)); + } + break; + default: + dump("Bad log method: " + this.logmethod); + } + }, + + // Pref observer interface implementation + + // topic: what event occurred + // subject: what nsIPrefBranch we're observing + // data: which pref has been changed (relative to subject) + observe(subject, topic, data) { + if (topic != "nsPref:changed") { + return; + } + switch (data) { + case "extensions.torbutton.logmethod": + this.logmethod = Services.prefs.getIntPref( + "extensions.torbutton.logmethod" + ); + if (this.logmethod === 0) { + Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true); + } else if ( + Services.prefs.getIntPref("extensions.torlauncher.logmethod", 3) !== 0 + ) { + // If Tor Launcher is not available or its log method is not 0 + // then let's reset the dump pref. + Services.prefs.setBoolPref("browser.dom.window.dump.enabled", false); + } + break; + case "extensions.torbutton.loglevel": + this.loglevel = Services.prefs.getIntPref( + "extensions.torbutton.loglevel" + ); + break; + } + }, +}; + +// Assign factory to global object. +const NSGetFactory = XPCOMUtils.generateNSGetFactory + ? XPCOMUtils.generateNSGetFactory([TorbuttonLogger]) + : ComponentUtils.generateNSGetFactory([TorbuttonLogger]); diff --git a/toolkit/torbutton/jar.mn b/toolkit/torbutton/jar.mn new file mode 100644 index 000000000000..5e7a11384cc4 --- /dev/null +++ b/toolkit/torbutton/jar.mn @@ -0,0 +1,46 @@ +#filter substitution + +torbutton.jar: + +% content torbutton %content/ + + content/torbutton.js (chrome/content/torbutton.js) + content/tor-circuit-display.js (chrome/content/tor-circuit-display.js) + content/preferences.xhtml (chrome/content/preferences.xhtml) + content/aboutTor/aboutTor-content.js (chrome/content/aboutTor/aboutTor-content.js) +* content/aboutTor/aboutTor.xhtml (chrome/content/aboutTor/aboutTor.xhtml) + content/aboutTor/resources/aboutTor.js (chrome/content/aboutTor/resources/aboutTor.js) + content/preferences-mobile.js (chrome/content/preferences-mobile.js) + + components/ (components/*) + modules/ (modules/*) + skin/ (chrome/skin/*) + +% resource torbutton % +% resource torbutton-abouttor resource://torbutton/content/aboutTor/resources/ contentaccessible=yes +% resource torbutton-assets resource://torbutton/skin/ contentaccessible=yes + +% skin torbutton classic/1.0 %skin/ + +# Firefox 4-style component registration +% component {f605ec27-d867-44b5-ad97-2a29276642c3} %components/dragDropFilter.js +% contract @torproject.org/torbutton-dragDropFilter;1 {f605ec27-d867-44b5-ad97-2a29276642c3} + +% component {3da0269f-fc29-4e9e-a678-c3b1cafcf13f} %components/external-app-blocker.js +% contract @torproject.org/torbutton-extAppBlocker;1 {3da0269f-fc29-4e9e-a678-c3b1cafcf13f} + +% component {06322def-6fde-4c06-aef6-47ae8e799629} %components/startup-observer.js +% contract @torproject.org/startup-observer;1 {06322def-6fde-4c06-aef6-47ae8e799629} + +% component {5d57312b-5d8c-4169-b4af-e80d6a28a72e} %components/torCheckService.js +% contract @torproject.org/torbutton-torCheckService;1 {5d57312b-5d8c-4169-b4af-e80d6a28a72e} + +% component {f36d72c9-9718-4134-b550-e109638331d7} %components/torbutton-logger.js +% contract @torproject.org/torbutton-logger;1 {f36d72c9-9718-4134-b550-e109638331d7} + +% component {e33fd6d4-270f-475f-a96f-ff3140279f68} %components/domain-isolator.js +% contract @torproject.org/domain-isolator;1 {e33fd6d4-270f-475f-a96f-ff3140279f68} + +% category profile-after-change StartupObserver @torproject.org/startup-observer;1 +% category profile-after-change DomainIsolator @torproject.org/domain-isolator;1 +% category profile-after-change DragDropFilter @torproject.org/torbutton-dragDropFilter;1 diff --git a/toolkit/torbutton/modules/tor-control-port.js b/toolkit/torbutton/modules/tor-control-port.js new file mode 100644 index 000000000000..374ff5fd1bfd --- /dev/null +++ b/toolkit/torbutton/modules/tor-control-port.js @@ -0,0 +1,982 @@ +// A module for TorBrowser that provides an asynchronous controller for +// Tor, through its ControlPort. +// +// This file is written in call stack order (later functions +// call earlier functions). The file can be processed +// with docco.js to produce pretty documentation. +// +// To import the module, use +// +// let { configureControlPortModule, controller, wait_for_controller } = +// Components.utils.import("path/to/tor-control-port.js", {}); +// +// See the third-to-last function defined in this file: +// configureControlPortModule(ipcFile, host, port, password) +// for usage of the configureControlPortModule function. +// +// See the last functions defined in this file: +// controller(avoidCache), wait_for_controller(avoidCache) +// for usage of the controller functions. + +/* jshint esnext: true */ +/* jshint -W097 */ +/* global console */ +"use strict"; + +// ### Import Mozilla Services +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "TorMonitorService", + "resource://gre/modules/TorMonitorService.jsm" +); + +// tor-launcher observer topics +const TorTopics = Object.freeze({ + ProcessIsReady: "TorProcessIsReady", +}); + +// __log__. +// Logging function +let logger = Cc["@torproject.org/torbutton-logger;1"].getService(Ci.nsISupports) + .wrappedJSObject; +let log = x => logger.eclog(3, x.trimRight().replace(/\r\n/g, "\n")); + +// ### announce this file +log("Loading tor-control-port.js\n"); + +class AsyncSocket { + constructor(ipcFile, host, port) { + let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService + ); + const OPEN_UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED; + + let socketTransport = ipcFile + ? sts.createUnixDomainTransport(ipcFile) + : sts.createTransport([], host, port, null, null); + + this.outputStream = socketTransport + .openOutputStream(OPEN_UNBUFFERED, 1, 1) + .QueryInterface(Ci.nsIAsyncOutputStream); + this.outputQueue = []; + + this.inputStream = socketTransport + .openInputStream(OPEN_UNBUFFERED, 1, 1) + .QueryInterface(Ci.nsIAsyncInputStream); + this.scriptableInputStream = Cc[ + "@mozilla.org/scriptableinputstream;1" + ].createInstance(Ci.nsIScriptableInputStream); + this.scriptableInputStream.init(this.inputStream); + this.inputQueue = []; + } + + // asynchronously write string to underlying socket and return number of bytes written + async write(str) { + return new Promise((resolve, reject) => { + // asyncWait next write request + const tryAsyncWait = () => { + if (this.outputQueue.length) { + this.outputStream.asyncWait( + this.outputQueue.at(0), // next request + 0, + 0, + Services.tm.currentThread + ); + } + }; + + // output stream can only have 1 registered callback at a time, so multiple writes + // need to be queued up (see nsIAsyncOutputStream.idl) + this.outputQueue.push({ + // Implement an nsIOutputStreamCallback: + onOutputStreamReady: () => { + try { + let bytesWritten = this.outputStream.write(str, str.length); + + // remove this callback object from queue as it is now completed + this.outputQueue.shift(); + + // request next wait if there is one + tryAsyncWait(); + + // finally resolve promise + resolve(bytesWritten); + } catch (err) { + // reject promise on error + reject(err); + } + }, + }); + + // length 1 imples that there is no in-flight asyncWait, so we may immediately + // follow through on this write + if (this.outputQueue.length == 1) { + tryAsyncWait(); + } + }); + } + + // asynchronously read string from underlying socket and return it + async read() { + return new Promise((resolve, reject) => { + const tryAsyncWait = () => { + if (this.inputQueue.length) { + this.inputStream.asyncWait( + this.inputQueue.at(0), // next input request + 0, + 0, + Services.tm.currentThread + ); + } + }; + + this.inputQueue.push({ + onInputStreamReady: stream => { + try { + // read our string from input stream + let str = this.scriptableInputStream.read( + this.scriptableInputStream.available() + ); + + // remove this callback object from queue now that we have read + this.inputQueue.shift(); + + // request next wait if there is one + tryAsyncWait(); + + // finally resolve promise + resolve(str); + } catch (err) { + reject(err); + } + }, + }); + + // length 1 imples that there is no in-flight asyncWait, so we may immediately + // follow through on this read + if (this.inputQueue.length == 1) { + tryAsyncWait(); + } + }); + } + + close() { + this.outputStream.close(); + this.inputStream.close(); + } +} + +class ControlSocket { + constructor(asyncSocket) { + this.socket = asyncSocket; + this._isOpen = true; + this.pendingData = ""; + this.pendingLines = []; + + this.mainDispatcher = io.callbackDispatcher(); + this.notificationDispatcher = io.callbackDispatcher(); + // mainDispatcher pushes only async notifications (650) to notificationDispatcher + this.mainDispatcher.addCallback( + /^650/, + this._handleNotification.bind(this) + ); + // callback for handling responses and errors + this.mainDispatcher.addCallback( + /^[245]\d\d/, + this._handleCommandReply.bind(this) + ); + + this.commandQueue = []; + + this._startMessagePump(); + } + + // blocks until an entire line is read and returns it + // immediately returns next line in queue (pendingLines) if present + async _readLine() { + // keep reading from socket until we have a full line to return + while (!this.pendingLines.length) { + // read data from our socket and spit on newline tokens + this.pendingData += await this.socket.read(); + let lines = this.pendingData.split("\r\n"); + + // the last line will either be empty string, or a partial read of a response/event + // so save it off for the next socket read + this.pendingData = lines.pop(); + + // copy remaining full lines to our pendingLines list + this.pendingLines = this.pendingLines.concat(lines); + } + return this.pendingLines.shift(); + } + + // blocks until an entire message is ready and returns it + async _readMessage() { + // whether we are searching for the end of a multi-line values + // See control-spec section 3.9 + let handlingMultlineValue = false; + let endOfMessageFound = false; + const message = []; + + do { + const line = await this._readLine(); + message.push(line); + + if (handlingMultlineValue) { + // look for end of multiline + if (line.match(/^.$/)) { + handlingMultlineValue = false; + } + } else { + // 'Multiline values' are possible. We avoid interrupting one by detecting it + // and waiting for a terminating "." on its own line. + // (See control-spec section 3.9 and https://trac.torproject.org/16990#comment:28 + // Ensure this is the first line of a new message + // eslint-disable-next-line no-lonely-if + if (message.length === 1 && line.match(/^\d\d\d+.+?=$/)) { + handlingMultlineValue = true; + } + // look for end of message (note the space character at end of the regex) + else if (line.match(/^\d\d\d /)) { + if (message.length == 1) { + endOfMessageFound = true; + } else { + let firstReplyCode = message[0].substring(0, 3); + let lastReplyCode = line.substring(0, 3); + if (firstReplyCode == lastReplyCode) { + endOfMessageFound = true; + } + } + } + } + } while (!endOfMessageFound); + + // join our lines back together to form one message + return message.join("\r\n"); + } + + async _startMessagePump() { + try { + while (true) { + let message = await this._readMessage(); + log("controlPort >> " + message); + this.mainDispatcher.pushMessage(message); + } + } catch (err) { + this._isOpen = false; + for (const cmd of this.commandQueue) { + cmd.reject(err); + } + this.commandQueue = []; + } + } + + _writeNextCommand() { + let cmd = this.commandQueue[0]; + log("controlPort << " + cmd.commandString); + this.socket.write(`${cmd.commandString}\r\n`).catch(cmd.reject); + } + + async sendCommand(commandString) { + if (!this.isOpen()) { + throw new Error("ControlSocket not open"); + } + + // this promise is resolved either in _handleCommandReply, or + // in _startMessagePump (on stream error) + return new Promise((resolve, reject) => { + let command = { + commandString, + resolve, + reject, + }; + + this.commandQueue.push(command); + if (this.commandQueue.length == 1) { + this._writeNextCommand(); + } + }); + } + + _handleCommandReply(message) { + let cmd = this.commandQueue.shift(); + if (message.match(/^2/)) { + cmd.resolve(message); + } else if (message.match(/^[45]/)) { + let myErr = new Error(cmd.commandString + " -> " + message); + // Add Tor-specific information to the Error object. + let idx = message.indexOf(" "); + if (idx > 0) { + myErr.torStatusCode = message.substring(0, idx); + myErr.torMessage = message.substring(idx); + } else { + myErr.torStatusCode = message; + } + cmd.reject(myErr); + } else { + cmd.reject( + new Error( + `ControlSocket::_handleCommandReply received unexpected message:\n----\n${message}\n----` + ) + ); + } + + // send next command if one is available + if (this.commandQueue.length) { + this._writeNextCommand(); + } + } + + _handleNotification(message) { + this.notificationDispatcher.pushMessage(message); + } + + close() { + this.socket.close(); + this._isOpen = false; + } + + addNotificationCallback(regex, callback) { + this.notificationDispatcher.addCallback(regex, callback); + } + + isOpen() { + return this._isOpen; + } +} + +// ## io +// I/O utilities namespace + +let io = {}; + +// __io.callbackDispatcher()__. +// Returns dispatcher object with three member functions: +// dispatcher.addCallback(regex, callback), dispatcher.removeCallback(callback), +// and dispatcher.pushMessage(message). +// Pass pushMessage to another function that needs a callback with a single string +// argument. Whenever dispatcher.pushMessage receives a string, the dispatcher will +// check for any regex matches and pass the string on to the corresponding callback(s). +io.callbackDispatcher = function() { + let callbackPairs = [], + removeCallback = function(aCallback) { + callbackPairs = callbackPairs.filter(function([regex, callback]) { + return callback !== aCallback; + }); + }, + addCallback = function(regex, callback) { + if (callback) { + callbackPairs.push([regex, callback]); + } + return function() { + removeCallback(callback); + }; + }, + pushMessage = function(message) { + for (let [regex, callback] of callbackPairs) { + if (message.match(regex)) { + callback(message); + } + } + }; + return { + pushMessage, + removeCallback, + addCallback, + }; +}; + +// __io.controlSocket(ipcFile, host, port, password)__. +// Instantiates and returns a socket to a tor ControlPort at ipcFile or +// host:port, authenticating with the given password. Example: +// +// // Open the socket +// let socket = await io.controlSocket(undefined, "127.0.0.1", 9151, "MyPassw0rd"); +// // Send command and receive "250" response reply or error is thrown +// await socket.sendCommand(commandText); +// // Register or deregister for "650" notifications +// // that match regex +// socket.addNotificationCallback(regex, callback); +// socket.removeNotificationCallback(callback); +// // Close the socket permanently +// socket.close(); +io.controlSocket = async function(ipcFile, host, port, password) { + let socket = new AsyncSocket(ipcFile, host, port); + let controlSocket = new ControlSocket(socket); + + // Log in to control port. + await controlSocket.sendCommand("authenticate " + (password || "")); + // Activate needed events. + await controlSocket.sendCommand("setevents stream"); + + return controlSocket; +}; + +// ## utils +// A namespace for utility functions +let utils = {}; + +// __utils.identity(x)__. +// Returns its argument unchanged. +utils.identity = function(x) { + return x; +}; + +// __utils.isString(x)__. +// Returns true iff x is a string. +utils.isString = function(x) { + return typeof x === "string" || x instanceof String; +}; + +// __utils.capture(string, regex)__. +// Takes a string and returns an array of capture items, where regex must have a single +// capturing group and use the suffix /.../g to specify a global search. +utils.capture = function(string, regex) { + let matches = []; + // Special trick to use string.replace for capturing multiple matches. + string.replace(regex, function(a, captured) { + matches.push(captured); + }); + return matches; +}; + +// __utils.extractor(regex)__. +// Returns a function that takes a string and returns an array of regex matches. The +// regex must use the suffix /.../g to specify a global search. +utils.extractor = function(regex) { + return function(text) { + return utils.capture(text, regex); + }; +}; + +// __utils.splitLines(string)__. +// Splits a string into an array of strings, each corresponding to a line. +utils.splitLines = function(string) { + return string.split(/\r?\n/); +}; + +// __utils.splitAtSpaces(string)__. +// Splits a string into chunks between spaces. Does not split at spaces +// inside pairs of quotation marks. +utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g); + +// __utils.splitAtFirst(string, regex)__. +// Splits a string at the first instance of regex match. If no match is +// found, returns the whole string. +utils.splitAtFirst = function(string, regex) { + let match = string.match(regex); + return match + ? [ + string.substring(0, match.index), + string.substring(match.index + match[0].length), + ] + : string; +}; + +// __utils.splitAtEquals(string)__. +// Splits a string into chunks between equals. Does not split at equals +// inside pairs of quotation marks. +utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g); + +// __utils.mergeObjects(arrayOfObjects)__. +// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object. +// Pure function. +utils.mergeObjects = function(arrayOfObjects) { + let result = {}; + for (let obj of arrayOfObjects) { + for (let key in obj) { + result[key] = obj[key]; + } + } + return result; +}; + +// __utils.listMapData(parameterString, listNames)__. +// Takes a list of parameters separated by spaces, of which the first several are +// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames +// to the unnamed parameters, and combine them in a map with the named parameters. +// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH` +// +// utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH", +// ["streamID", "event", "circuitID", "IP"]) +// // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0", +// // "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}" +utils.listMapData = function(parameterString, listNames) { + // Split out the space-delimited parameters. + let parameters = utils.splitAtSpaces(parameterString), + dataMap = {}; + // Assign listNames to the first n = listNames.length parameters. + for (let i = 0; i < listNames.length; ++i) { + dataMap[listNames[i]] = parameters[i]; + } + // Read key-value pairs and copy these to the dataMap. + for (let i = listNames.length; i < parameters.length; ++i) { + let [key, value] = utils.splitAtEquals(parameters[i]); + if (key && value) { + dataMap[key] = value; + } + } + return dataMap; +}; + +// __utils.rejectPromise(errorMessage)__. +// Returns a rejected promise with the given error message. +utils.rejectPromise = errorMessage => Promise.reject(new Error(errorMessage)); + +// ## info +// A namespace for functions related to tor's GETINFO and GETCONF command. +let info = {}; + +// __info.keyValueStringsFromMessage(messageText)__. +// Takes a message (text) response to GETINFO or GETCONF and provides +// a series of key-value strings, which are either multiline (with a `250+` prefix): +// +// 250+config/defaults= +// AccountingMax "0 bytes" +// AllowDotExit "0" +// . +// +// or single-line (with a `250-` or `250 ` prefix): +// +// 250-version=0.2.6.0-alpha-dev (git-b408125288ad6943) +info.keyValueStringsFromMessage = utils.extractor( + /^(250+[\s\S]+?^.|250[- ].+?)$/gim +); + +// __info.applyPerLine(transformFunction)__. +// Returns a function that splits text into lines, +// and applies transformFunction to each line. +info.applyPerLine = function(transformFunction) { + return function(text) { + return utils.splitLines(text.trim()).map(transformFunction); + }; +}; + +// __info.routerStatusParser(valueString)__. +// Parses a router status entry as, described in +// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt +// (search for "router status entry") +info.routerStatusParser = function(valueString) { + let lines = utils.splitLines(valueString), + objects = []; + for (let line of lines) { + // Drop first character and grab data following it. + let myData = line.substring(2), + // Accumulate more maps with data, depending on the first character in the line. + dataFun = { + r: data => + utils.listMapData(data, [ + "nickname", + "identity", + "digest", + "publicationDate", + "publicationTime", + "IP", + "ORPort", + "DirPort", + ]), + a: data => ({ IPv6: data }), + s: data => ({ statusFlags: utils.splitAtSpaces(data) }), + v: data => ({ version: data }), + w: data => utils.listMapData(data, []), + p: data => ({ portList: data.split(",") }), + }[line.charAt(0)]; + if (dataFun !== undefined) { + objects.push(dataFun(myData)); + } + } + return utils.mergeObjects(objects); +}; + +// __info.circuitStatusParser(line)__. +// Parse the output of a circuit status line. +info.circuitStatusParser = function(line) { + let data = utils.listMapData(line, ["id", "status", "circuit"]), + circuit = data.circuit; + // Parse out the individual circuit IDs and names. + if (circuit) { + data.circuit = circuit.split(",").map(function(x) { + return x.split(/~|=/); + }); + } + return data; +}; + +// __info.streamStatusParser(line)__. +// Parse the output of a stream status line. +info.streamStatusParser = function(text) { + return utils.listMapData(text, [ + "StreamID", + "StreamStatus", + "CircuitID", + "Target", + ]); +}; + +// TODO: fix this parsing logic to handle bridgeLine correctly +// fingerprint/id is an optional parameter +// __info.bridgeParser(bridgeLine)__. +// Takes a single line from a `getconf bridge` result and returns +// a map containing the bridge's type, address, and ID. +info.bridgeParser = function(bridgeLine) { + let result = {}, + tokens = bridgeLine.split(/\s+/); + // First check if we have a "vanilla" bridge: + if (tokens[0].match(/^\d+.\d+.\d+.\d+/)) { + result.type = "vanilla"; + [result.address, result.ID] = tokens; + // Several bridge types have a similar format: + } else { + result.type = tokens[0]; + if ( + [ + "flashproxy", + "fte", + "meek", + "meek_lite", + "obfs3", + "obfs4", + "scramblesuit", + "snowflake", + ].includes(result.type) + ) { + [result.address, result.ID] = tokens.slice(1); + } + } + return result.type ? result : null; +}; + +// __info.parsers__. +// A map of GETINFO and GETCONF keys to parsing function, which convert +// result strings to JavaScript data. +info.parsers = { + "ns/id/": info.routerStatusParser, + "ip-to-country/": utils.identity, + "circuit-status": info.applyPerLine(info.circuitStatusParser), + bridge: info.bridgeParser, + // Currently unused parsers: + // "ns/name/" : info.routerStatusParser, + // "stream-status" : info.applyPerLine(info.streamStatusParser), + // "version" : utils.identity, + // "config-file" : utils.identity, +}; + +// __info.getParser(key)__. +// Takes a key and determines the parser function that should be used to +// convert its corresponding valueString to JavaScript data. +info.getParser = function(key) { + return ( + info.parsers[key] || + info.parsers[key.substring(0, key.lastIndexOf("/") + 1)] + ); +}; + +// __info.stringToValue(string)__. +// Converts a key-value string as from GETINFO or GETCONF to a value. +info.stringToValue = function(string) { + // key should look something like `250+circuit-status=` or `250-circuit-status=...` + // or `250 circuit-status=...` + let matchForKey = string.match(/^250[ +-](.+?)=/), + key = matchForKey ? matchForKey[1] : null; + if (key === null) { + return null; + } + // matchResult finds a single-line result for `250-` or `250 `, + // or a multi-line one for `250+`. + let matchResult = + string.match(/^250[ -].+?=(.*)$/) || + string.match(/^250+.+?=([\s\S]*?)^.$/m), + // Retrieve the captured group (the text of the value in the key-value pair) + valueString = matchResult ? matchResult[1] : null, + // Get the parser function for the key found. + parse = info.getParser(key.toLowerCase()); + if (parse === undefined) { + throw new Error("No parser found for '" + key + "'"); + } + // Return value produced by the parser. + return parse(valueString); +}; + +// __info.getMultipleResponseValues(message)__. +// Process multiple responses to a GETINFO or GETCONF request. +info.getMultipleResponseValues = function(message) { + return info + .keyValueStringsFromMessage(message) + .map(info.stringToValue) + .filter(utils.identity); +}; + +// __info.getInfo(controlSocket, key)__. +// Sends GETINFO for a single key. Returns a promise with the result. +info.getInfo = function(aControlSocket, key) { + if (!utils.isString(key)) { + return utils.rejectPromise("key argument should be a string"); + } + return aControlSocket + .sendCommand("getinfo " + key) + .then(response => info.getMultipleResponseValues(response)[0]); +}; + +// __info.getConf(aControlSocket, key)__. +// Sends GETCONF for a single key. Returns a promise with the result. +info.getConf = function(aControlSocket, key) { + // GETCONF with a single argument returns results with + // one or more lines that look like `250[- ]key=value`. + // Any GETCONF lines that contain a single keyword only are currently dropped. + // So we can use similar parsing to that for getInfo. + if (!utils.isString(key)) { + return utils.rejectPromise("key argument should be a string"); + } + return aControlSocket + .sendCommand("getconf " + key) + .then(info.getMultipleResponseValues); +}; + +// ## onionAuth +// A namespace for functions related to tor's ONION_CLIENT_AUTH_* commands. +let onionAuth = {}; + +onionAuth.keyInfoStringsFromMessage = utils.extractor(/^250-CLIENT\s+(.+)$/gim); + +onionAuth.keyInfoObjectsFromMessage = function(message) { + let keyInfoStrings = onionAuth.keyInfoStringsFromMessage(message); + return keyInfoStrings.map(infoStr => + utils.listMapData(infoStr, ["hsAddress", "typeAndKey"]) + ); +}; + +// __onionAuth.viewKeys()__. +// Sends a ONION_CLIENT_AUTH_VIEW command to retrieve the list of private keys. +// Returns a promise that is fulfilled with an array of key info objects which +// contain the following properties: +// hsAddress +// typeAndKey +// Flags (e.g., "Permanent") +onionAuth.viewKeys = function(aControlSocket) { + let cmd = "onion_client_auth_view"; + return aControlSocket + .sendCommand(cmd) + .then(onionAuth.keyInfoObjectsFromMessage); +}; + +// __onionAuth.add(controlSocket, hsAddress, b64PrivateKey, isPermanent)__. +// Sends a ONION_CLIENT_AUTH_ADD command to add a private key to the +// Tor configuration. +onionAuth.add = function( + aControlSocket, + hsAddress, + b64PrivateKey, + isPermanent +) { + if (!utils.isString(hsAddress)) { + return utils.rejectPromise("hsAddress argument should be a string"); + } + + if (!utils.isString(b64PrivateKey)) { + return utils.rejectPromise("b64PrivateKey argument should be a string"); + } + + const keyType = "x25519"; + let cmd = `onion_client_auth_add ${hsAddress} ${keyType}:${b64PrivateKey}`; + if (isPermanent) { + cmd += " Flags=Permanent"; + } + return aControlSocket.sendCommand(cmd); +}; + +// __onionAuth.remove(controlSocket, hsAddress)__. +// Sends a ONION_CLIENT_AUTH_REMOVE command to remove a private key from the +// Tor configuration. +onionAuth.remove = function(aControlSocket, hsAddress) { + if (!utils.isString(hsAddress)) { + return utils.rejectPromise("hsAddress argument should be a string"); + } + + let cmd = `onion_client_auth_remove ${hsAddress}`; + return aControlSocket.sendCommand(cmd); +}; + +// ## event +// Handlers for events + +let event = {}; + +// __event.parsers__. +// A map of EVENT keys to parsing functions, which convert result strings to JavaScript +// data. +event.parsers = { + stream: info.streamStatusParser, + // Currently unused: + // "circ" : info.circuitStatusParser, +}; + +// __event.messageToData(type, message)__. +// Extract the data from an event. Note, at present +// we only extract streams that look like `"650" SP...` +event.messageToData = function(type, message) { + let dataText = message.match(/^650 \S+?\s(.*)/m)[1]; + return dataText && type.toLowerCase() in event.parsers + ? event.parsers[type.toLowerCase()](dataText) + : null; +}; + +// __event.watchEvent(controlSocket, type, filter, onData)__. +// Watches for a particular type of event. If filter(data) returns true, the event's +// data is passed to the onData callback. Returns a zero arg function that +// stops watching the event. Note: we only observe `"650" SP...` events +// currently (no `650+...` or `650-...` events). +event.watchEvent = function(controlSocket, type, filter, onData, raw = false) { + return controlSocket.addNotificationCallback( + new RegExp("^650 " + type), + function(message) { + let data = event.messageToData(type, message); + if (filter === null || filter(data)) { + if (raw || !data) { + onData(message); + return; + } + onData(data); + } + } + ); +}; + +// ## tor +// Things related to the main controller. +let tor = {}; + +// __tor.controllerCache__. +// A map from "unix:socketpath" or "host:port" to controller objects. Prevents +// redundant instantiation of control sockets. +tor.controllerCache = new Map(); + +// __tor.controller(ipcFile, host, port, password)__. +// Creates a tor controller at the given ipcFile or host and port, with the +// given password. +tor.controller = async function(ipcFile, host, port, password) { + let socket = await io.controlSocket(ipcFile, host, port, password); + return { + getInfo: key => info.getInfo(socket, key), + getConf: key => info.getConf(socket, key), + onionAuthViewKeys: () => onionAuth.viewKeys(socket), + onionAuthAdd: (hsAddress, b64PrivateKey, isPermanent) => + onionAuth.add(socket, hsAddress, b64PrivateKey, isPermanent), + onionAuthRemove: hsAddress => onionAuth.remove(socket, hsAddress), + watchEvent: (type, filter, onData, raw = false) => + event.watchEvent(socket, type, filter, onData, raw), + isOpen: () => socket.isOpen(), + close: () => { + socket.close(); + }, + sendCommand: cmd => socket.sendCommand(cmd), + }; +}; + +// ## Export + +let controlPortInfo = {}; + +// __configureControlPortModule(ipcFile, host, port, password)__. +// Sets Tor control port connection parameters to be used in future calls to +// the controller() function. Example: +// configureControlPortModule(undefined, "127.0.0.1", 9151, "MyPassw0rd"); +var configureControlPortModule = function(ipcFile, host, port, password) { + controlPortInfo.ipcFile = ipcFile; + controlPortInfo.host = host; + controlPortInfo.port = port || 9151; + controlPortInfo.password = password; +}; + +// __controller(avoidCache)__. +// Instantiates and returns a controller object that is connected and +// authenticated to a Tor ControlPort using the connection parameters +// provided in the most recent call to configureControlPortModule(), if +// the controller doesn't yet exist. Otherwise returns the existing +// controller to the given ipcFile or host:port. Throws on error. +// +// Example: +// +// // Get a new controller +// const avoidCache = true; +// let c = controller(avoidCache); +// // Send command and receive `250` reply or error message in a promise: +// let replyPromise = c.getInfo("ip-to-country/16.16.16.16"); +// // Close the controller permanently +// c.close(); +var controller = async function(avoidCache) { + if (!controlPortInfo.ipcFile && !controlPortInfo.host) { + throw new Error("Please call configureControlPortModule first"); + } + + const dest = controlPortInfo.ipcFile + ? `unix:${controlPortInfo.ipcFile.path}` + : `${controlPortInfo.host}:${controlPortInfo.port}`; + + // constructor shorthand + const newTorController = async () => { + return tor.controller( + controlPortInfo.ipcFile, + controlPortInfo.host, + controlPortInfo.port, + controlPortInfo.password + ); + }; + + // avoid cache so always return a new controller + if (avoidCache) { + return newTorController(); + } + + // first check our cache and see if we already have one + let cachedController = tor.controllerCache.get(dest); + if (cachedController && cachedController.isOpen()) { + return cachedController; + } + + // create a new one and store in the map + cachedController = await newTorController(); + // overwrite the close() function to prevent consumers from closing a shared/cached controller + cachedController.close = () => { + throw new Error("May not close cached Tor Controller as it may be in use"); + }; + + tor.controllerCache.set(dest, cachedController); + return cachedController; +}; + +// __wait_for_controller(avoidCache) +// Same as controller() function, but explicitly waits until there is a tor daemon +// to connect to (either launched by tor-launcher, or if we have an existing system +// tor daemon) +var wait_for_controller = function(avoidCache) { + // if tor process is running (either ours or system) immediately return controller + if (!TorMonitorService.ownsTorDaemon || TorMonitorService.isRunning) { + return controller(avoidCache); + } + + // otherwise we must wait for tor to finish launching before resolving + return new Promise((resolve, reject) => { + let observer = { + observe: async (subject, topic, data) => { + if (topic === TorTopics.ProcessIsReady) { + try { + resolve(await controller(avoidCache)); + } catch (err) { + reject(err); + } + Services.obs.removeObserver(observer, TorTopics.ProcessIsReady); + } + }, + }; + Services.obs.addObserver(observer, TorTopics.ProcessIsReady); + }); +}; + +// Export functions for external use. +var EXPORTED_SYMBOLS = [ + "configureControlPortModule", + "controller", + "wait_for_controller", +]; diff --git a/toolkit/torbutton/modules/utils.js b/toolkit/torbutton/modules/utils.js new file mode 100644 index 000000000000..30947a737cb9 --- /dev/null +++ b/toolkit/torbutton/modules/utils.js @@ -0,0 +1,318 @@ +// # Utils.js +// Various helpful utility functions. + +// ### Import Mozilla Services +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// ## Pref utils + +// __prefs__. A shortcut to Mozilla Services.prefs. +let prefs = Services.prefs; + +// __getPrefValue(prefName)__ +// Returns the current value of a preference, regardless of its type. +var getPrefValue = function(prefName) { + switch (prefs.getPrefType(prefName)) { + case prefs.PREF_BOOL: + return prefs.getBoolPref(prefName); + case prefs.PREF_INT: + return prefs.getIntPref(prefName); + case prefs.PREF_STRING: + return prefs.getCharPref(prefName); + default: + return null; + } +}; + +// __bindPref(prefName, prefHandler, init)__ +// Applies prefHandler whenever the value of the pref changes. +// If init is true, applies prefHandler to the current value. +// Returns a zero-arg function that unbinds the pref. +var bindPref = function(prefName, prefHandler, init = false) { + let update = () => { + prefHandler(getPrefValue(prefName)); + }, + observer = { + observe(subject, topic, data) { + if (data === prefName) { + update(); + } + }, + }; + prefs.addObserver(prefName, observer); + if (init) { + update(); + } + return () => { + prefs.removeObserver(prefName, observer); + }; +}; + +// __bindPrefAndInit(prefName, prefHandler)__ +// Applies prefHandler to the current value of pref specified by prefName. +// Re-applies prefHandler whenever the value of the pref changes. +// Returns a zero-arg function that unbinds the pref. +var bindPrefAndInit = (prefName, prefHandler) => + bindPref(prefName, prefHandler, true); + +// ## Observers + +// __observe(topic, callback)__. +// Observe the given topic. When notification of that topic +// occurs, calls callback(subject, data). Returns a zero-arg +// function that stops observing. +var observe = function(topic, callback) { + let observer = { + observe(aSubject, aTopic, aData) { + if (topic === aTopic) { + callback(aSubject, aData); + } + }, + }; + Services.obs.addObserver(observer, topic); + return () => Services.obs.removeObserver(observer, topic); +}; + +// ## Environment variables + +// __env__. +// Provides access to process environment variables. +let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment +); + +// __getEnv(name)__. +// Reads the environment variable of the given name. +var getEnv = function(name) { + return env.exists(name) ? env.get(name) : undefined; +}; + +// __getLocale +// Returns the app locale to be used in tor-related urls. +var getLocale = function() { + const locale = Services.locale.appLocaleAsBCP47; + if (locale === "ja-JP-macos") { + // We don't want to distinguish the mac locale. + return "ja"; + } + return locale; +}; + +// ## Windows + +// __dialogsByName__. +// Map of window names to dialogs. +let dialogsByName = {}; + +// __showDialog(parent, url, name, features, arg1, arg2, ...)__. +// Like window.openDialog, but if the window is already +// open, just focuses it instead of opening a new one. +var showDialog = function(parent, url, name, features) { + let existingDialog = dialogsByName[name]; + if (existingDialog && !existingDialog.closed) { + existingDialog.focus(); + return existingDialog; + } + let newDialog = parent.openDialog.apply(parent, Array.slice(arguments, 1)); + dialogsByName[name] = newDialog; + return newDialog; +}; + +// ## Tor control protocol utility functions + +let _torControl = { + // Unescape Tor Control string aStr (removing surrounding "" and \ escapes). + // Based on Vidalia's src/common/stringutil.cpp:string_unescape(). + // Returns the unescaped string. Throws upon failure. + // Within Tor Launcher, the file components/tl-protocol.js also contains a + // copy of _strUnescape(). + _strUnescape(aStr) { + if (!aStr) { + return aStr; + } + + var len = aStr.length; + if (len < 2 || '"' != aStr.charAt(0) || '"' != aStr.charAt(len - 1)) { + return aStr; + } + + const kHexRE = /[0-9A-Fa-f]{2}/; + const kOctalRE = /[0-7]{3}/; + var rv = ""; + var i = 1; + var lastCharIndex = len - 2; + while (i <= lastCharIndex) { + var c = aStr.charAt(i); + if ("\" == c) { + if (++i > lastCharIndex) { + throw new Error("missing character after \"); + } + + c = aStr.charAt(i); + if ("n" == c) { + rv += "\n"; + } else if ("r" == c) { + rv += "\r"; + } else if ("t" == c) { + rv += "\t"; + } else if ("x" == c) { + if (i + 2 > lastCharIndex) { + throw new Error("not enough hex characters"); + } + + let s = aStr.substr(i + 1, 2); + if (!kHexRE.test(s)) { + throw new Error("invalid hex characters"); + } + + let val = parseInt(s, 16); + rv += String.fromCharCode(val); + i += 3; + } else if (this._isDigit(c)) { + let s = aStr.substr(i, 3); + if (i + 2 > lastCharIndex) { + throw new Error("not enough octal characters"); + } + + if (!kOctalRE.test(s)) { + throw new Error("invalid octal characters"); + } + + let val = parseInt(s, 8); + rv += String.fromCharCode(val); + i += 3; + } // "\" and others + else { + rv += c; + ++i; + } + } else if ('"' == c) { + throw new Error('unescaped " within string'); + } else { + rv += c; + ++i; + } + } + + // Convert from UTF-8 to Unicode. TODO: is UTF-8 always used in protocol? + return decodeURIComponent(escape(rv)); + }, // _strUnescape() + + // Within Tor Launcher, the file components/tl-protocol.js also contains a + // copy of _isDigit(). + _isDigit(aChar) { + const kRE = /^\d$/; + return aChar && kRE.test(aChar); + }, +}; // _torControl + +// __unescapeTorString(str, resultObj)__. +// Unescape Tor Control string str (removing surrounding "" and \ escapes). +// Returns the unescaped string. Throws upon failure. +var unescapeTorString = function(str) { + return _torControl._strUnescape(str); +}; + +var getFPDFromHost = hostname => { + try { + return Services.eTLD.getBaseDomainFromHost(hostname); + } catch (e) { + if ( + e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS || + e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS + ) { + return hostname; + } + } + return null; +}; + +// Assuming this is called with gBrowser.selectedBrowser +var getDomainForBrowser = browser => { + let fpd = browser.contentPrincipal.originAttributes.firstPartyDomain; + // Bug 31562: For neterror or certerror, get the original URL from + // browser.currentURI and use it to calculate the firstPartyDomain. + let knownErrors = ["about:neterror", "about:certerror"]; + let documentURI = browser.documentURI; + if ( + documentURI && + documentURI.schemeIs("about") && + knownErrors.some(x => documentURI.spec.startsWith(x)) + ) { + let knownSchemes = ["http", "https", "ftp"]; + let currentURI = browser.currentURI; + if (currentURI && knownSchemes.some(x => currentURI.schemeIs(x))) { + fpd = getFPDFromHost(currentURI.host) || fpd; + } + } + return fpd; +}; + +var m_tb_torlog = Cc["@torproject.org/torbutton-logger;1"].getService( + Ci.nsISupports +).wrappedJSObject; + +var m_tb_string_bundle = torbutton_get_stringbundle(); + +function torbutton_safelog(nLevel, sMsg, scrub) { + m_tb_torlog.safe_log(nLevel, sMsg, scrub); + return true; +} + +function torbutton_log(nLevel, sMsg) { + m_tb_torlog.log(nLevel, sMsg); + + // So we can use it in boolean expressions to determine where the + // short-circuit is.. + return true; +} + +// load localization strings +function torbutton_get_stringbundle() { + var o_stringbundle = false; + + try { + var oBundle = Services.strings; + o_stringbundle = oBundle.createBundle( + "chrome://torbutton/locale/torbutton.properties" + ); + } catch (err) { + o_stringbundle = false; + } + if (!o_stringbundle) { + torbutton_log(5, "ERROR (init): failed to find torbutton-bundle"); + } + + return o_stringbundle; +} + +function torbutton_get_property_string(propertyname) { + try { + if (!m_tb_string_bundle) { + m_tb_string_bundle = torbutton_get_stringbundle(); + } + + return m_tb_string_bundle.GetStringFromName(propertyname); + } catch (e) { + torbutton_log(4, "Unlocalized string " + propertyname); + } + + return propertyname; +} + +// Export utility functions for external use. +let EXPORTED_SYMBOLS = [ + "bindPref", + "bindPrefAndInit", + "getEnv", + "getLocale", + "getDomainForBrowser", + "getPrefValue", + "observe", + "showDialog", + "show_torbrowser_manual", + "unescapeTorString", + "torbutton_safelog", + "torbutton_log", + "torbutton_get_property_string", +]; diff --git a/toolkit/torbutton/moz.build b/toolkit/torbutton/moz.build new file mode 100644 index 000000000000..376c99765de4 --- /dev/null +++ b/toolkit/torbutton/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +JAR_MANIFESTS += ['jar.mn'] diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js index c24943e9dee3..de0091ae8d4d 100644 --- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js @@ -83,7 +83,11 @@ function getGlobalScriptIncludes(scriptPath) { "browser/components/screenshots/content/" ) .replace("chrome://browser/content/", "browser/base/content/") - .replace("chrome://global/content/", "toolkit/content/"); + .replace("chrome://global/content/", "toolkit/content/") + .replace( + "chrome://torbutton/content/", + "toolkit/torproject/torbutton/chrome/content/" + );
for (let mapping of Object.getOwnPropertyNames(MAPPINGS)) { if (sourceFile.includes(mapping)) {