commit 73d57c01c2bfe398c02586ae76dab677d843df22 Author: Richard Pospesel richard@torproject.org Date: Fri Jul 16 17:32:01 2021 +0200
fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser --- browser/actors/NetErrorParent.jsm | 6 +- browser/base/content/browser.js | 5 +- browser/components/BrowserGlue.jsm | 32 +- browser/components/torconnect/TorConnectParent.jsm | 200 +++++---- .../torconnect/content/aboutTorConnect.js | 466 +++++++++----------- .../torconnect/content/aboutTorConnect.xhtml | 9 - .../torconnect/content/torBootstrapUrlbar.js | 195 ++++----- .../components/torpreferences/content/torPane.js | 8 +- browser/components/urlbar/UrlbarInput.jsm | 6 +- browser/modules/TorConnect.jsm | 477 +++++++++++++++++++-- browser/modules/TorProtocolService.jsm | 101 +++-- .../processsingleton/MainProcessSingleton.jsm | 5 + toolkit/modules/RemotePageAccessManager.jsm | 28 +- 13 files changed, 941 insertions(+), 597 deletions(-)
diff --git a/browser/actors/NetErrorParent.jsm b/browser/actors/NetErrorParent.jsm index fa3cbf23fcb7..6dce9af5aad0 100644 --- a/browser/actors/NetErrorParent.jsm +++ b/browser/actors/NetErrorParent.jsm @@ -17,8 +17,8 @@ const { SessionStore } = ChromeUtils.import( ); const { HomePage } = ChromeUtils.import("resource:///modules/HomePage.jsm");
-const { TorProtocolService } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" +const { TorConnect } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" );
const PREF_SSL_IMPACT_ROOTS = [ @@ -324,7 +324,7 @@ class NetErrorParent extends JSWindowActorParent { } break; case "ShouldShowTorConnect": - return TorProtocolService.shouldShowTorConnect(); + return TorConnect.shouldShowTorConnect; } return undefined; } diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 916cd69320cb..996ef6dcdd7f 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -77,7 +77,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { TabModalPrompt: "chrome://global/content/tabprompts.jsm", TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm", TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm", - TorProtocolService: "resource:///modules/TorProtocolService.jsm", + TorConnect: "resource:///modules/TorConnect.jsm", Translation: "resource:///modules/translation/TranslationParent.jsm", OnionAliasStore: "resource:///modules/OnionAliasStore.jsm", UITour: "resource:///modules/UITour.jsm", @@ -2494,7 +2494,8 @@ var gBrowserInit = { let uri = window.arguments[0]; let defaultArgs = BrowserHandler.defaultArgs;
- if (TorProtocolService.shouldShowTorConnect()) { + if (TorConnect.shouldShowTorConnect) { + TorConnect.setURIsToLoad(uri); return "about:torconnect"; }
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index 8735783cee2b..cb77f4d82a3e 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -17,31 +17,6 @@ const { AppConstants } = ChromeUtils.import( "resource://gre/modules/AppConstants.jsm" );
-// TorProtocolService and TorConnect modules need to be lazily-loaded -// here because they will trigger generation of the random password used -// to talk to the tor daemon in tor-launcher. Generating the random -// password will initialize the cryptographic service ( nsNSSComponent ) -// -// If this service is init'd before the profile has been setup, it will -// use the fallback init path which behaves as if security.nocertdb=true -// -// We make these module getters so init happens when they are needed -// (when init'ing the OnionAliasStore). With theze getters, the password -// generation is triggered in torbutton after the 'profile-after-change' -// topic (so after the profile is initialized) - -ChromeUtils.defineModuleGetter( - this, - "TorProtocolService", - "resource:///modules/TorProtocolService.jsm" -); - -ChromeUtils.defineModuleGetter( - this, - "TorConnect", - "resource:///modules/TorConnect.jsm" -); - ChromeUtils.defineModuleGetter( this, "ActorManagerParent", @@ -2531,14 +2506,17 @@ BrowserGlue.prototype = {
{ task: () => { - if (TorProtocolService.isBootstrapDone() || !TorProtocolService.ownsTorDaemon) { + const { TorConnect, TorConnectTopics } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" + ); + if (!TorConnect.shouldShowTorConnect) { // we will take this path when the user is using the legacy tor launcher or // when Tor Browser didn't launch its own tor. OnionAliasStore.init(); } else { // this path is taken when using about:torconnect, we wait to init // after we are bootstrapped and connected to tor - const topic = "torconnect:bootstrap-complete"; + const topic = TorConnectTopics.BootstrapComplete; let bootstrapObserver = { observe(aSubject, aTopic, aData) { if (aTopic === topic) { diff --git a/browser/components/torconnect/TorConnectParent.jsm b/browser/components/torconnect/TorConnectParent.jsm index c34fab76ddbb..3937bf3ebcf8 100644 --- a/browser/components/torconnect/TorConnectParent.jsm +++ b/browser/components/torconnect/TorConnectParent.jsm @@ -3,123 +3,139 @@ var EXPORTED_SYMBOLS = ["TorConnectParent"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const { TorProtocolService } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" -); const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm"); -const { TorLauncherUtil } = ChromeUtils.import( - "resource://torlauncher/modules/tl-util.jsm" -); - -const { TorConnect } = ChromeUtils.import( +const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import( "resource:///modules/TorConnect.jsm" );
-const kTorProcessReadyTopic = "TorProcessIsReady"; -const kTorProcessExitedTopic = "TorProcessExited"; -const kTorProcessDidNotStartTopic = "TorProcessDidNotStart"; -const kTorShowProgressPanelTopic = "TorShowProgressPanel"; -const kTorBootstrapStatusTopic = "TorBootstrapStatus"; -const kTorBootstrapErrorTopic = "TorBootstrapError"; -const kTorLogHasWarnOrErrTopic = "TorLogHasWarnOrErr"; - -const gActiveTopics = [ - kTorProcessReadyTopic, - kTorProcessExitedTopic, - kTorProcessDidNotStartTopic, - kTorShowProgressPanelTopic, - kTorBootstrapStatusTopic, - kTorBootstrapErrorTopic, - kTorLogHasWarnOrErrTopic, - "torconnect:bootstrap-complete", -]; - -const gTorLauncherPrefs = { +const TorLauncherPrefs = Object.freeze({ quickstart: "extensions.torlauncher.quickstart", -} +}); + +/* +This object is basically a marshalling interface between the TorConnect module +and a particular about:torconnect page +*/
class TorConnectParent extends JSWindowActorParent { constructor(...args) { super(...args);
const self = this; - this.gObserver = { - observe(aSubject, aTopic, aData) { - const obj = aSubject?.wrappedJSObject; - if (obj) { - obj.handled = true; - } - self.sendAsyncMessage(aTopic, obj); - }, - };
- for (const topic of gActiveTopics) { - Services.obs.addObserver(this.gObserver, topic); - } + this.state = { + State: TorConnect.state, + ErrorMessage: TorConnect.errorMessage, + ErrorDetails: TorConnect.errorDetails, + BootstrapProgress: TorConnect.bootstrapProgress, + BootstrapStatus: TorConnect.bootstrapStatus, + ShowCopyLog: TorConnect.logHasWarningOrError, + QuickStartEnabled: Services.prefs.getBoolPref(TorLauncherPrefs.quickstart, false), + };
- this.quickstartObserver = { + // JSWindowActiveParent derived objects cannot observe directly, so create a member + // object to do our observing for us + // + // This object converts the various lifecycle events from the TorConnect module, and + // maintains a state object which we pass down to our about:torconnect page, which uses + // the state object to update its UI + this.torConnectObserver = { observe(aSubject, aTopic, aData) { - if (aTopic === "nsPref:changed" && - aData == gTorLauncherPrefs.quickstart) { - self.sendAsyncMessage("TorQuickstartPrefChanged", Services.prefs.getBoolPref(gTorLauncherPrefs.quickstart)); + let obj = aSubject?.wrappedJSObject; + + // update our state struct based on received torconnect topics and forward on + // to aboutTorConnect.js + switch(aTopic) { + case TorConnectTopics.StateChange: { + self.state.State = obj.state; + // clear any previous error information if we are bootstrapping + if (self.state.State === TorConnectState.Bootstrapping) { + self.state.ErrorMessage = null; + self.state.ErrorDetails = null; + } + break; + } + case TorConnectTopics.BootstrapProgress: { + self.state.BootstrapProgress = obj.progress; + self.state.BootstrapStatus = obj.status; + self.state.ShowCopyLog = obj.hasWarnings; + break; + } + case TorConnectTopics.BootstrapComplete: { + // tells about:torconnect pages to close themselves + // this flag will only be set if an about:torconnect page + // reaches the Bootstrapped state, so if a user + // navigates to about:torconnect manually after bootstrap, the page + // will not auto-close on them + self.state.Close = true; + break; + } + case TorConnectTopics.BootstrapError: { + self.state.ErrorMessage = obj.message; + self.state.ErrorDetails = obj.details; + self.state.ShowCopyLog = true; + break; + } + case TorConnectTopics.FatalError: { + // TODO: handle + break; + } + case "nsPref:changed": { + if (aData === TorLauncherPrefs.quickstart) { + self.state.QuickStartEnabled = Services.prefs.getBoolPref(TorLauncherPrefs.quickstart); + } + break; + } + default: { + console.log(`TorConnect: unhandled observe topic '${aTopic}'`); + } } + + self.sendAsyncMessage("torconnect:state-change", self.state); }, + }; + + // observe all of the torconnect:.* topics + for (const key in TorConnectTopics) { + const topic = TorConnectTopics[key]; + Services.obs.addObserver(this.torConnectObserver, topic); } - Services.prefs.addObserver(gTorLauncherPrefs.quickstart, this.quickstartObserver); + Services.prefs.addObserver(TorLauncherPrefs.quickstart, this.torConnectObserver); }
willDestroy() { - for (const topic of gActiveTopics) { - Services.obs.removeObserver(this.gObserver, topic); + // stop observing all of our torconnect:.* topics + for (const key in TorConnectTopics) { + const topic = TorConnectTopics[key]; + Services.obs.removeObserver(this.torConnectObserver, topic); } - } - - - _OpenTorAdvancedPreferences() { - const win = this.browsingContext.top.embedderElement.ownerGlobal; - win.openTrustedLinkIn("about:preferences#tor", "tab"); - } - - _TorCopyLog() { - // Copy tor log messages to the system clipboard. - const chSvc = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( - Ci.nsIClipboardHelper - ); - const countObj = { value: 0 }; - chSvc.copyString(TorProtocolService.getLog(countObj)); - const count = countObj.value; - return TorLauncherUtil.getFormattedLocalizedString( - "copiedNLogMessagesShort", - [count], - 1 - ); + Services.prefs.removeObserver(TorLauncherPrefs.quickstart, this.torConnectObserver); }
receiveMessage(message) { switch (message.name) { - case "TorBootstrapErrorOccurred": - return TorProtocolService.torBootstrapErrorOccurred(); - case "TorRetrieveBootstrapStatus": - return TorProtocolService.retrieveBootstrapStatus(); - case "OpenTorAdvancedPreferences": - return this._OpenTorAdvancedPreferences(); - case "GetLocalizedBootstrapStatus": - const { status, keyword } = message.data; - return TorLauncherUtil.getLocalizedBootstrapStatus(status, keyword); - case "TorCopyLog": - return this._TorCopyLog(); - case "TorIsNetworkDisabled": - return TorProtocolService.isNetworkDisabled(); - case "TorStopBootstrap": - return TorProtocolService.torStopBootstrap(); - case "TorConnect": - return TorProtocolService.connect(); - case "GetDirection": - return Services.locale.isAppLocaleRTL ? "rtl" : "ltr"; - case "GetTorStrings": - return TorStrings; - case "TorLogHasWarnOrErr": - return TorProtocolService.torLogHasWarnOrErr(); + case "torconnect:set-quickstart": + Services.prefs.setBoolPref(TorLauncherPrefs.quickstart, message.data); + break; + case "torconnect:open-tor-preferences": + TorConnect.openTorPreferences(); + break; + case "torconnect:copy-tor-logs": + return TorConnect.copyTorLogs(); + case "torconnect:cancel-bootstrap": + TorConnect.cancelBootstrap(); + break; + case "torconnect:begin-bootstrap": + TorConnect.beginBootstrap(); + break; + case "torconnect:get-init-args": + // called on AboutTorConnect.init(), pass down all state data it needs to init + return { + TorStrings: TorStrings, + TorConnectState: TorConnectState, + Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr", + State: this.state, + }; } return undefined; } diff --git a/browser/components/torconnect/content/aboutTorConnect.js b/browser/components/torconnect/content/aboutTorConnect.js index 19fd335ccd13..8b269d2fc82b 100644 --- a/browser/components/torconnect/content/aboutTorConnect.js +++ b/browser/components/torconnect/content/aboutTorConnect.js @@ -2,299 +2,258 @@
/* eslint-env mozilla/frame-script */
-const kTorProcessReadyTopic = "TorProcessIsReady"; -const kTorProcessExitedTopic = "TorProcessExited"; -const kTorProcessDidNotStartTopic = "TorProcessDidNotStart"; -const kTorBootstrapStatusTopic = "TorBootstrapStatus"; -const kTorBootstrapErrorTopic = "TorBootstrapError"; -const kTorLogHasWarnOrErrTopic = "TorLogHasWarnOrErr"; -const kTorQuickstartPrefChanged = "TorQuickstartPrefChanged"; - -const TorLauncherPrefs = { - quickstart: "extensions.torlauncher.quickstart", - prompt_at_startup: "extensions.torlauncher.prompt_at_startup", -} +// populated in AboutTorConnect.init() +let TorStrings = {}; +let TorConnectState = {};
class AboutTorConnect { - log(...args) { - console.log(...args); - } - - logError(...args) { - console.error(...args); - } + selectors = Object.freeze({ + textContainer: { + title: "div.title", + titleText: "h1.title-text", + }, + progress: { + description: "p#connectShortDescText", + meter: "div#progressBackground", + }, + copyLog: { + link: "span#copyLogLink", + tooltip: "div#copyLogTooltip", + tooltipText: "span#copyLogTooltipText", + }, + quickstart: { + checkbox: "input#quickstartCheckbox", + label: "label#quickstartCheckboxLabel", + }, + buttons: { + connect: "button#connectButton", + cancel: "button#cancelButton", + advanced: "button#advancedButton", + }, + }) + + elements = Object.freeze({ + title: document.querySelector(this.selectors.textContainer.title), + titleText: document.querySelector(this.selectors.textContainer.titleText), + progressDescription: document.querySelector(this.selectors.progress.description), + progressMeter: document.querySelector(this.selectors.progress.meter), + copyLogLink: document.querySelector(this.selectors.copyLog.link), + copyLogTooltip: document.querySelector(this.selectors.copyLog.tooltip), + copyLogTooltipText: document.querySelector(this.selectors.copyLog.tooltipText), + quickstartCheckbox: document.querySelector(this.selectors.quickstart.checkbox), + quickstartLabel: document.querySelector(this.selectors.quickstart.label), + connectButton: document.querySelector(this.selectors.buttons.connect), + cancelButton: document.querySelector(this.selectors.buttons.cancel), + advancedButton: document.querySelector(this.selectors.buttons.advanced), + }) + + beginBootstrap() { + this.hide(this.elements.connectButton); + this.show(this.elements.cancelButton); + this.elements.cancelButton.focus(); + RPMSendAsyncMessage("torconnect:begin-bootstrap"); + } + + cancelBootstrap() { + RPMSendAsyncMessage("torconnect:cancel-bootstrap"); + } + + /* + Element helper methods + */ + + show(element) { + element.removeAttribute("hidden"); + } + + hide(element) { + element.setAttribute("hidden", "true"); + } + + setTitle(title, error) { + this.elements.titleText.textContent = title; + document.title = title;
- logDebug(...args) { - console.debug(...args); + if (error) { + this.elements.title.classList.add("error"); + } else { + this.elements.title.classList.remove("error"); + } }
- getElem(id) { - return document.getElementById(id); - } - get elemProgressContent() { - return this.getElem("progressContent"); - } - get elemProgressDesc() { - return this.getElem("connectShortDescText"); - } - get elemProgressMeter() { - return this.getElem("progressBackground"); - } - get elemCopyLogLink() { - return this.getElem("copyLogLink"); - } - get elemCopyLogTooltip() { - return this.getElem("copyLogTooltip"); - } - get elemCopyLogTooltipText() { - return this.getElem("copyLogTooltipText"); - } - get elemQuickstartCheckbox() { - return this.getElem("quickstartCheckbox"); - } - get elemQuickstartLabel() { - return this.getElem("quickstartCheckboxLabel"); - } - get elemConnectButton() { - return this.getElem("connectButton"); - } - get elemAdvancedButton() { - return this.getElem("advancedButton"); - } - get elemCancelButton() { - return this.getElem("cancelButton"); - } - get elemTextContainer() { - return this.getElem("text-container"); - } - get elemTitle() { - return this.elemTextContainer.getElementsByClassName("title")[0]; + setProgress(description, visible, percent) { + this.elements.progressDescription.textContent = description; + if (visible) { + this.show(this.elements.progressMeter); + this.elements.progressMeter.style.width = `${percent}%`; + } else { + this.hide(this.elements.progressMeter); + } }
- static get STATE_INITIAL() { - return "STATE_INITIAL"; - } + /* + These methods update the UI based on the current TorConnect state + */
- static get STATE_BOOTSTRAPPING() { - return "STATE_BOOTSTRAPPING"; - } + updateUI(state) { + console.log(state);
- static get STATE_BOOTSTRAPPED() { - return "STATE_BOOTSTRAPPED"; - } + // calls update_$state() + this[`update_${state.State}`](state);
- static get STATE_BOOTSTRAP_ERROR() { - return "STATE_BOOTSTRAP_ERROR"; + if (state.ShowCopyLog) { + this.showCopyLog(); + } + this.elements.quickstartCheckbox.checked = state.QuickStartEnabled; }
- get state() { - return this._state; - } + /* Per-state updates */
- setInitialUI() { - this.setTitle(this.torStrings.torConnect.torConnect); - this.elemProgressDesc.textContent = - this.torStrings.settings.torPreferencesDescription; - this.showElem(this.elemConnectButton); - this.elemConnectButton.focus(); - this.showElem(this.elemAdvancedButton); - this.hideElem(this.elemCopyLogLink); - this.hideElem(this.elemCancelButton); - this.hideElem(this.elemProgressContent); - this.hideElem(this.elemProgressMeter); - this.elemTitle.classList.remove("error"); - } + update_Initial(state) { + const hasError = false; + const showProgressbar = false;
- setBootstrappingUI() { - this.setTitle(this.torStrings.torConnect.torConnecting); - this.hideElem(this.elemConnectButton); - this.hideElem(this.elemAdvancedButton); - this.hideElem(this.elemCopyLogLink); - this.showElem(this.elemCancelButton); - this.elemCancelButton.focus(); - this.showElem(this.elemProgressContent); - this.showElem(this.elemProgressMeter); - this.elemTitle.classList.remove("error"); + this.setTitle(TorStrings.torConnect.torConnect, hasError); + this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar); + this.hide(this.elements.copyLogLink); + this.hide(this.elements.connectButton); + this.hide(this.elements.advancedButton); + this.hide(this.elements.cancelButton); }
- setBootstrapErrorUI() { - this.setTitle(this.torStrings.torConnect.torBootstrapFailed); - this.elemConnectButton.textContent = this.torStrings.torConnect.tryAgain; - this.showElem(this.elemConnectButton); - this.hideElem(this.elemCancelButton); - this.showElem(this.elemAdvancedButton); - this.elemAdvancedButton.focus(); - this.showElem(this.elemProgressContent); - this.hideElem(this.elemProgressMeter); - this.elemTitle.classList.add("error"); - } + update_Configuring(state) { + const hasError = state.ErrorMessage != null; + const showProgressbar = false;
- set state(state) { - const oldState = this.state; - if (oldState === state) { - return; - } - this._state = state; - switch (this.state) { - case AboutTorConnect.STATE_INITIAL: - this.setInitialUI(); - break; - case AboutTorConnect.STATE_BOOTSTRAPPING: - this.setBootstrappingUI(); - break; - case AboutTorConnect.STATE_BOOTSTRAP_ERROR: - this.setBootstrapErrorUI(); - break; - case AboutTorConnect.STATE_BOOTSTRAPPED: - window.close(); - break; + if (hasError) { + this.setTitle(state.ErrorMessage, hasError); + this.setProgress(state.ErrorDetails, showProgressbar); + this.show(this.elements.copyLogLink); + this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain; + } else { + this.setTitle(TorStrings.torConnect.torConnect, hasError); + this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar); + this.hide(this.elements.copyLogLink); + this.elements.connectButton.textContent = TorStrings.torConnect.torConnectButton; } + this.show(this.elements.connectButton); + this.elements.connectButton.focus(); + this.show(this.elements.advancedButton); + this.hide(this.elements.cancelButton); }
- async showErrorMessage(aErrorObj) { - if (aErrorObj && aErrorObj.message) { - this.setTitle(aErrorObj.message); - if (aErrorObj.details) { - this.elemProgressDesc.textContent = aErrorObj.details; - } - } - - this.showCopyLog(); - this.showElem(this.elemConnectButton); + update_AutoConfiguring(state) { + // TODO: noop until this state is used }
- showElem(elem) { - elem.removeAttribute("hidden"); - } + update_Bootstrapping(state) { + const hasError = false; + const showProgressbar = true;
- hideElem(elem) { - elem.setAttribute("hidden", "true"); + this.setTitle(state.BootstrapStatus ? state.BootstrapStatus : TorStrings.torConnect.torConnecting, hasError); + this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar, state.BootstrapProgress); + if (state.ShowCopyLog) { + this.show(this.elements.copyLogLink); + } else { + this.hide(this.elements.copyLogLink); + } + this.hide(this.elements.connectButton); + this.hide(this.elements.advancedButton); + this.show(this.elements.cancelButton); + this.elements.cancelButton.focus(); }
- async connect() { - // reset the text to original description - // in case we are trying again after an error (clears out error text) - this.elemProgressDesc.textContent = - this.torStrings.settings.torPreferencesDescription; + update_Error(state) { + const hasError = true; + const showProgressbar = false;
- this.state = AboutTorConnect.STATE_BOOTSTRAPPING; - const error = await RPMSendQuery("TorConnect"); - if (error) { - if (error.details) { - this.showErrorMessage({ message: error.details }, true); - this.showSaveSettingsError(error.details); - } - } + this.setTitle(state.ErrorMessage, hasError); + this.setProgress(state.ErrorDetails, showProgressbar); + this.show(this.elements.copyLogLink); + this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain; + this.show(this.elements.connectButton); + this.show(this.elements.advancedButton); + this.hide(this.elements.cancelButton); }
- showCopyLog() { - this.elemCopyLogLink.removeAttribute("hidden"); + update_FatalError(state) { + // TODO: noop until this state is used }
- async updateBootstrapProgress(status) { - let labelText = await RPMSendQuery("GetLocalizedBootstrapStatus", { - status, - keyword: "TAG", - }); - let percentComplete = status.PROGRESS ? status.PROGRESS : 0; - this.elemProgressMeter.style.width = `${percentComplete}%`; - - if (await RPMSendQuery("TorBootstrapErrorOccurred")) { - this.state = AboutTorConnect.STATE_BOOTSTRAP_ERROR; - return; - } else if (await RPMSendQuery("TorIsNetworkDisabled")) { - // If tor network is not connected, let's go to the initial state, even - // if bootstrap state is greater than 0. - this.state = AboutTorConnect.STATE_INITIAL; - return; - } else if (percentComplete > 0) { - this.state = AboutTorConnect.STATE_BOOTSTRAPPING; - } + update_Bootstrapped(state) { + const hasError = false; + const showProgressbar = true;
- // Due to async, status might have changed. Do not override desc if so. - if (this.state === AboutTorConnect.STATE_BOOTSTRAPPING) { - this.hideElem(this.elemConnectButton); + this.setTitle(TorStrings.torConnect.torConnected, hasError); + this.setProgress(TorStrings.settings.torPreferencesDescription, showProgressbar, 100); + this.hide(this.elements.connectButton); + this.hide(this.elements.advancedButton); + this.hide(this.elements.cancelButton); + + // only close the window if directed + if (state.Close) { + window.close(); } }
- stopTorBootstrap() { - RPMSendAsyncMessage("TorStopBootstrap"); + update_Disabled(state) { + // TODO: we should probably have some UX here if a user goes to about:torconnect when + // it isn't in use (eg using tor-launcher or system tor) }
- setTitle(title) { - const titleElement = document.querySelector(".title-text"); - titleElement.textContent = title; - document.title = title; - } + async initElements(direction, quickstart) {
- async initElements() { - this.elemAdvancedButton.textContent = this.torStrings.torConnect.torConfigure; - this.elemAdvancedButton.addEventListener("click", () => { - RPMSendAsyncMessage("OpenTorAdvancedPreferences"); - }); + document.documentElement.setAttribute("dir", direction);
- // sets the text content while keping the child elements intact - this.elemCopyLogLink.childNodes[0].nodeValue = - this.torStrings.torConnect.copyLog; - this.elemCopyLogLink.addEventListener("click", async (event) => { - const copiedMessage = await RPMSendQuery("TorCopyLog"); - aboutTorConnect.elemCopyLogTooltipText.textContent = copiedMessage; - aboutTorConnect.elemCopyLogTooltip.style.visibility = "visible"; + // sets the text content while keeping the child elements intact + this.elements.copyLogLink.childNodes[0].nodeValue = + TorStrings.torConnect.copyLog; + this.elements.copyLogLink.addEventListener("click", async (event) => { + const copiedMessage = await RPMSendQuery("torconnect:copy-tor-logs"); + this.elements.copyLogTooltipText.textContent = copiedMessage; + this.elements.copyLogTooltipText.style.visibility = "visible";
// clear previous timeout if one already exists - if (aboutTorConnect.copyLogTimeoutId) { - clearTimeout(aboutTorConnect.copyLogTimeoutId); + if (this.copyLogTimeoutId) { + clearTimeout(this.copyLogTimeoutId); }
// hide tooltip after X ms const TOOLTIP_TIMEOUT = 2000; - aboutTorConnect.copyLogTimeoutId = setTimeout(function() { - aboutTorConnect.elemCopyLogTooltip.style.visibility = "hidden"; - aboutTorConnect.copyLogTimeoutId = 0; + this.copyLogTimeoutId = setTimeout(function() { + this.elements.copyLogTooltipText.style.visibility = "hidden"; + this.copyLogTimeoutId = 0; }, TOOLTIP_TIMEOUT); });
+ this.elements.quickstartCheckbox.checked = quickstart + this.elements.quickstartCheckbox.addEventListener("change", () => { + const quickstart = this.elements.quickstartCheckbox.checked; + RPMSendAsyncMessage("torconnect:set-quickstart", quickstart); + }); + this.elements.quickstartLabel.textContent = TorStrings.settings.quickstartCheckbox;
- this.elemQuickstartLabel.textContent = this.torStrings.settings.quickstartCheckbox; - this.elemQuickstartCheckbox.addEventListener("change", () => { - const quickstart = this.elemQuickstartCheckbox.checked; - RPMSetBoolPref(TorLauncherPrefs.quickstart, quickstart); + this.elements.connectButton.textContent = + TorStrings.torConnect.torConnectButton; + this.elements.connectButton.addEventListener("click", () => { + this.beginBootstrap(); }); - this.elemQuickstartCheckbox.checked = await RPMGetBoolPref(TorLauncherPrefs.quickstart);
- this.elemConnectButton.textContent = - this.torStrings.torConnect.torConnectButton; - this.elemConnectButton.addEventListener("click", () => { - this.connect(); + this.elements.advancedButton.textContent = TorStrings.torConnect.torConfigure; + this.elements.advancedButton.addEventListener("click", () => { + RPMSendAsyncMessage("torconnect:open-tor-preferences"); });
- this.elemCancelButton.textContent = this.torStrings.torConnect.cancel; - this.elemCancelButton.addEventListener("click", () => { - this.stopTorBootstrap(); + this.elements.cancelButton.textContent = TorStrings.torConnect.cancel; + this.elements.cancelButton.addEventListener("click", () => { + this.cancelBootstrap(); }); }
initObservers() { - RPMAddMessageListener(kTorBootstrapErrorTopic, ({ data }) => { - this.showCopyLog(); - this.stopTorBootstrap(); - this.showErrorMessage(data); - }); - RPMAddMessageListener(kTorLogHasWarnOrErrTopic, () => { - this.showCopyLog(); - }); - RPMAddMessageListener(kTorProcessDidNotStartTopic, ({ data }) => { - this.showErrorMessage(data); - }); - RPMAddMessageListener(kTorBootstrapStatusTopic, ({ data }) => { - this.updateBootstrapProgress(data); - }); - RPMAddMessageListener(kTorQuickstartPrefChanged, ({ data }) => { - // update checkbox with latest quickstart pref value - this.elemQuickstartCheckbox.checked = data; - }); - RPMAddMessageListener("torconnect:bootstrap-complete", () => { - this.state = AboutTorConnect.STATE_BOOTSTRAPPED; + // TorConnectParent feeds us state blobs to we use to update our UI + RPMAddMessageListener("torconnect:state-change", ({ data }) => { + this.updateUI(data); }); }
@@ -304,34 +263,25 @@ class AboutTorConnect { // integers, so we must resort to a string compare here :( // see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for relevant documentation if (evt.code === "Escape") { - this.stopTorBootstrap(); + this.cancelBootstrap(); } }; }
async init() { - this.torStrings = await RPMSendQuery("GetTorStrings"); - document.documentElement.setAttribute( - "dir", - await RPMSendQuery("GetDirection") - ); - this.initElements(); + + let args = await RPMSendQuery("torconnect:get-init-args"); + + // various constants + TorStrings = Object.freeze(args.TorStrings); + TorConnectState = Object.freeze(args.TorConnectState); + + this.initElements(args.Direction); this.initObservers(); this.initKeyboardShortcuts(); - this.state = AboutTorConnect.STATE_INITIAL; - - // Request the most recent bootstrap status info so that a - // TorBootstrapStatus notification is generated as soon as possible. - RPMSendAsyncMessage("TorRetrieveBootstrapStatus"); - - // quickstart is the user set pref for starting tor automatically - // prompt_at_startup will be set to false after successful bootstrap, and true on error - // by tor-launcher, so we want to keep the connect screen up when prompt_at_startup is true - /// even if quickstart is enabled so user can potentially resolve errors on next launch - if (await RPMGetBoolPref(TorLauncherPrefs.quickstart) && - !await RPMGetBoolPref(TorLauncherPrefs.prompt_at_startup)) { - this.connect(); - } + + // populate UI based on current state + this.updateUI(args.State); } }
diff --git a/browser/components/torconnect/content/aboutTorConnect.xhtml b/browser/components/torconnect/content/aboutTorConnect.xhtml index 0a0721afb7db..595bbdf9a70a 100644 --- a/browser/components/torconnect/content/aboutTorConnect.xhtml +++ b/browser/components/torconnect/content/aboutTorConnect.xhtml @@ -19,15 +19,6 @@ </div> </div>
- <div id="progressContent" hidden="true"> - <div class="tbb-header" pack="center"> - <image class="tbb-logo"/> - </div> - <div flex="1"> - <div id="progressDesc"/> - </div> - </div> - <div id="copyLogContainer"> <span id="copyLogLink" hidden="true"> <div id="copyLogTooltip"> diff --git a/browser/components/torconnect/content/torBootstrapUrlbar.js b/browser/components/torconnect/content/torBootstrapUrlbar.js index 55a595b2dbab..e4fd6f5ab910 100644 --- a/browser/components/torconnect/content/torBootstrapUrlbar.js +++ b/browser/components/torconnect/content/torBootstrapUrlbar.js @@ -2,135 +2,88 @@
"use strict";
- const TorConnectionStatus = { - invalid: -1, - offline: 0, - connecting: 1, - connected: 2, - failure: 3, - }; -var TorBootstrapUrlbar; +const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" +); +const { TorStrings } = ChromeUtils.import( + "resource:///modules/TorStrings.jsm" +);
-{ - const { TorProtocolService } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" - ); - const { TorLauncherUtil } = ChromeUtils.import( - "resource://torlauncher/modules/tl-util.jsm" - ); - const { TorStrings } = ChromeUtils.import( - "resource:///modules/TorStrings.jsm" - ); - - const kTorProcessReadyTopic = "TorProcessIsReady"; - const kTorProcessExitedTopic = "TorProcessExited"; - const kTorProcessDidNotStartTopic = "TorProcessDidNotStart"; - const kTorBootstrapStatusTopic = "TorBootstrapStatus"; - const kTorBootstrapErrorTopic = "TorBootstrapError"; - - const gActiveTopics = [ - kTorProcessReadyTopic, - kTorProcessExitedTopic, - kTorProcessDidNotStartTopic, - kTorBootstrapStatusTopic, - kTorBootstrapErrorTopic, - ]; - - TorBootstrapUrlbar = { - _connectionStatus: TorConnectionStatus.invalid, - get ConnectionStatus() { - return this._connectionStatus; +var TorBootstrapUrlbar = { + selectors: Object.freeze({ + torConnect: { + box: "hbox#torconnect-box", + label: "label#torconnect-label", }, + }),
- _torConnectBox : null, - get TorConnectBox() { - if (!this._torConnectBox) { - this._torConnectBox = - browser.ownerGlobal.document.getElementById("torconnect-box"); - } - return this._torConnectBox; - }, + elements: null,
- _torConnectLabel : null, - get TorConnectLabel() { - if (!this._torConnectLabel) { - this._torConnectLabel = - browser.ownerGlobal.document.getElementById("torconnect-label"); + updateTorConnectBox: function(state) { + switch(state) + { + case TorConnectState.Initial: + case TorConnectState.Configuring: + case TorConnectState.AutoConfiguring: + case TorConnectState.Error: + case TorConnectState.FatalError: { + this.elements.torConnectBox.removeAttribute("hidden"); + this.elements.torConnectLabel.textContent = TorStrings.torConnect.offline; + this.elements.inputContainer.setAttribute("torconnect", "offline"); + break; } - return this._torConnectLabel; - }, - - _updateConnectionStatus(percentComplete = 0) { - if (TorProtocolService.ownsTorDaemon && - !TorLauncherUtil.useLegacyLauncher) { - if (TorProtocolService.isNetworkDisabled()) { - if (TorProtocolService.torBootstrapErrorOccurred()) { - this._connectionStatus = TorConnectionStatus.failure; - } else { - this._connectionStatus = TorConnectionStatus.offline; - } - } else if (percentComplete < 100) { - this._connectionStatus = TorConnectionStatus.connecting; - } else if (percentComplete === 100) { - this._connectionStatus = TorConnectionStatus.connected; - } + case TorConnectState.Bootstrapping: { + this.elements.torConnectBox.removeAttribute("hidden"); + this.elements.torConnectLabel.textContent = + TorStrings.torConnect.torConnectingConcise; + this.elements.inputContainer.setAttribute("torconnect", "connecting"); + break; } - else - { - this._connectionStatus = TorConnectionStatus.invalid; + case TorConnectState.Bootstrapped: { + this.elements.torConnectBox.removeAttribute("hidden"); + this.elements.torConnectLabel.textContent = + TorStrings.torConnect.torConnectedConcise; + this.elements.inputContainer.setAttribute("torconnect", "connected"); + // hide torconnect box after 5 seconds + setTimeout(() => { + this.elements.torConnectBox.setAttribute("hidden", "true"); + }, 5000); + break; } - - switch(this._connectionStatus) - { - case TorConnectionStatus.failure: - case TorConnectionStatus.offline: - this.TorConnectBox.removeAttribute("hidden"); - this.TorConnectLabel.textContent = TorStrings.torConnect.offline; - gURLBar._inputContainer.setAttribute("torconnect", "offline"); - break; - case TorConnectionStatus.connecting: - this.TorConnectLabel.textContent = - TorStrings.torConnect.torConnectingConcise; - gURLBar._inputContainer.setAttribute("torconnect", "connecting"); - break; - case TorConnectionStatus.connected: - this.TorConnectLabel.textContent = - TorStrings.torConnect.torConnectedConcise; - gURLBar._inputContainer.setAttribute("torconnect", "connected"); - // hide torconnect box after 5 seconds - let self = this; - setTimeout(function() { - self.TorConnectBox.setAttribute("hidden", "true"); - }, 5000); - break; + case TorConnectState.Disabled: { + this.elements.torConnectBox.setAttribute("hidden", "true"); + break; } - }, + default: + break; + } + },
- observe(aSubject, aTopic, aData) { + observe: function(aSubject, aTopic, aData) { + if (aTopic === TorConnectTopics.StateChange) { const obj = aSubject?.wrappedJSObject; + this.updateTorConnectBox(obj?.state); + } + }, + + init: function() { + if (TorConnect.shouldShowTorConnect) { + // browser isn't populated until init + this.elements = Object.freeze({ + torConnectBox: browser.ownerGlobal.document.querySelector(this.selectors.torConnect.box), + torConnectLabel: browser.ownerGlobal.document.querySelector(this.selectors.torConnect.label), + inputContainer: gURLBar._inputContainer, + }) + Services.obs.addObserver(this, TorConnectTopics.StateChange); + this.observing = true; + this.updateTorConnectBox(TorConnect.state); + } + }, + + uninit: function() { + if (this.observing) { + Services.obs.removeObserver(this, TorConnectTopics.StateChange); + } + }, +};
- switch (aTopic) { - case kTorProcessReadyTopic: - case kTorProcessExitedTopic: - case kTorProcessDidNotStartTopic: - case kTorBootstrapErrorTopic: - this._updateConnectionStatus(); - break; - case kTorBootstrapStatusTopic: - let percentComplete = obj.PROGRESS ? obj.PROGRESS : 0; - this._updateConnectionStatus(percentComplete); - break; - } - }, - init() { - for (const topic of gActiveTopics) { - Services.obs.addObserver(this, topic); - } - }, - uninit() { - for (const topic of gActiveTopics) { - Services.obs.removeObserver(this, topic); - } - }, - }; -} diff --git a/browser/components/torpreferences/content/torPane.js b/browser/components/torpreferences/content/torPane.js index 01609ddda090..59ecdec6d1d9 100644 --- a/browser/components/torpreferences/content/torPane.js +++ b/browser/components/torpreferences/content/torPane.js @@ -6,6 +6,10 @@ const { TorProtocolService } = ChromeUtils.import( "resource:///modules/TorProtocolService.jsm" );
+const { TorConnect } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" +); + const { TorBridgeSource, TorBridgeSettings, @@ -188,14 +192,14 @@ const gTorPane = (function() { this._messageBoxButton = prefpane.querySelector(selectors.messageBox.button); // wire up connect button this._messageBoxButton.addEventListener("click", () => { - TorProtocolService.connect(); + TorConnect.beginBootstrap(); let win = Services.wm.getMostRecentWindow("navigator:browser"); // switch to existing about:torconnect tab or create a new one win.switchToTabHavingURI("about:torconnect", true); });
let populateMessagebox = () => { - if (TorProtocolService.shouldShowTorConnect()) { + if (TorConnect.shouldShowTorConnect) { // set messagebox style and text if (TorProtocolService.torBootstrapErrorOccurred()) { this._messageBox.className = "error"; diff --git a/browser/components/urlbar/UrlbarInput.jsm b/browser/components/urlbar/UrlbarInput.jsm index f727c386701c..60b5b9163d67 100644 --- a/browser/components/urlbar/UrlbarInput.jsm +++ b/browser/components/urlbar/UrlbarInput.jsm @@ -10,8 +10,8 @@ const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" );
-const { TorProtocolService } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" +const { TorConnect } = ChromeUtils.import( + "resource:///modules/TorConnect.jsm" );
// in certain scenarios we want user input uris to open in a new tab if they do so from the @@ -24,7 +24,7 @@ function maybeUpdateOpenLocationForTorConnect(openUILinkWhere, currentURI, desti // we are trying to open in same tab openUILinkWhere === "current" && // only if user still has not bootstrapped - TorProtocolService.shouldShowTorConnect() && + TorConnect.shouldShowTorConnect && // and user is not just navigating to about:torconnect destinationURI !== "about:torconnect") { return "tab"; diff --git a/browser/modules/TorConnect.jsm b/browser/modules/TorConnect.jsm index 3125c84558db..5d2b826cfa10 100644 --- a/browser/modules/TorConnect.jsm +++ b/browser/modules/TorConnect.jsm @@ -1,6 +1,6 @@ "use strict";
-var EXPORTED_SYMBOLS = ["TorConnect"]; +var EXPORTED_SYMBOLS = ["TorConnect", "TorConnectTopics", "TorConnectState"];
const { Services } = ChromeUtils.import( "resource://gre/modules/Services.jsm" @@ -10,53 +10,476 @@ const { BrowserWindowTracker } = ChromeUtils.import( "resource:///modules/BrowserWindowTracker.jsm" );
-const { TorProtocolService } = ChromeUtils.import( +const { TorProtocolService, TorProcessStatus } = ChromeUtils.import( "resource:///modules/TorProtocolService.jsm" );
-// TODO: move the bootstrap state management out of each of the individual -// about:torconnect pages and stick it here -var TorConnect = (() => { +const { TorLauncherUtil } = ChromeUtils.import( + "resource://torlauncher/modules/tl-util.jsm" +); + +/* Browser observer topis */ +const BrowserTopics = Object.freeze({ + ProfileAfterChange: "profile-after-change", +}); + +/* tor-launcher observer topics */ +const TorTopics = Object.freeze({ + ProcessIsReady: "TorProcessIsReady", + BootstrapStatus: "TorBootstrapStatus", + BootstrapError: "TorBootstrapError", + ProcessExited: "TorProcessExited", + LogHasWarnOrErr: "TorLogHasWarnOrErr", +}); + +/* Relevant prefs used by tor-launcher */ +const TorLauncherPrefs = Object.freeze({ + quickstart: "extensions.torlauncher.quickstart", + prompt_at_startup: "extensions.torlauncher.prompt_at_startup", +}); + +const TorConnectState = Object.freeze({ + /* Our initial state */ + Initial: "Initial", + /* In-between initial boot and bootstrapping, users can change tor network settings during this state */ + Configuring: "Configuring", + /* Geo-location and setting bridges/etc */ + AutoConfiguring: "AutoConfiguring", + /* Tor is bootstrapping */ + Bootstrapping: "Bootstrapping", + /* Passthrough state back to Configuring or Fatal */ + Error: "Error", + /* An unrecoverable error */ + FatalError: "FatalError", + /* Final state, after successful bootstrap */ + Bootstrapped: "Bootstrapped", + /* If we are using System tor or the legacy Tor-Launcher */ + Disabled: "Disabled", +}); + +/* + + TorConnect State Transitions + + ┌──────────────────────┐ + │ Disabled │ + └──────────────────────┘ + ▲ + │ legacyOrSystemTor() + │ + ┌──────────────────────┐ + ┌────────────────────── │ Initial │ ───────────────────────────┐ + │ └──────────────────────┘ │ + │ │ │ + │ │ beginBootstrap() │ + │ ▼ │ +┌────────────────┐ │ bootstrapComplete() ┌────────────────────────────────────────────────┐ │ beginBootstrap() +│ Bootstrapped │ ◀──┼────────────────────── │ Bootstrapping │ ◀┼─────────────────┐ +└────────────────┘ │ └────────────────────────────────────────────────┘ │ │ + │ │ ▲ │ │ │ + │ │ cancelBootstrap() │ beginBootstrap() └────┼─────────────┐ │ + │ ▼ │ │ │ │ + │ beginConfigure() ┌────────────────────────────────────────────────┐ │ │ │ + └─────────────────────▶ │ │ │ │ │ + │ │ │ │ │ + beginConfigure() │ │ │ │ │ + ┌──────────────────────────▶ │ Configuring │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ │ + │ ┌─────────────────────▶ │ │ │ │ │ + │ │ └────────────────────────────────────────────────┘ │ │ │ + │ │ │ │ │ │ │ + │ │ cancelAutoconfigure() │ autoConfigure() │ ┌────┼─────────────┼───┘ + │ │ ▼ │ │ │ │ + │ │ ┌──────────────────────┐ │ │ │ │ + │ └────────────────────── │ AutoConfiguring │ ─┼────────────────────┘ │ │ + │ └──────────────────────┘ │ │ │ + │ │ │ │ onError() │ + │ │ onError() │ onError() │ │ + │ ▼ ▼ │ │ + │ ┌────────────────────────────────────────────────┐ │ │ + └─────────────────────────── │ Error │ ◀┘ │ + └────────────────────────────────────────────────┘ │ + │ ▲ onError() │ + │ onFatalError() └──────────────────┘ + ▼ + ┌──────────────────────┐ + │ FatalError │ + └──────────────────────┘ + +*/ + + +/* Maps allowed state transitions + TorConnectStateTransitions[state] maps to an array of allowed states to transition to +*/ +const TorConnectStateTransitions = + Object.freeze(new Map([ + [TorConnectState.Initial, + [TorConnectState.Disabled, + TorConnectState.Bootstrapping, + TorConnectState.Configuring, + TorConnectState.Error]], + [TorConnectState.Configuring, + [TorConnectState.AutoConfiguring, + TorConnectState.Bootstrapping, + TorConnectState.Error]], + [TorConnectState.AutoConfiguring, + [TorConnectState.Configuring, + TorConnectState.Bootstrapping, + TorConnectState.Error]], + [TorConnectState.Bootstrapping, + [TorConnectState.Configuring, + TorConnectState.Bootstrapped, + TorConnectState.Error]], + [TorConnectState.Error, + [TorConnectState.Configuring, + TorConnectState.FatalError]], + // terminal states + [TorConnectState.FatalError, []], + [TorConnectState.Bootstrapped, []], + [TorConnectState.Disabled, []], + ])); + +/* Topics Notified by the TorConnect module */ +const TorConnectTopics = Object.freeze({ + StateChange: "torconnect:state-change", + BootstrapProgress: "torconnect:bootstrap-progress", + BootstrapComplete: "torconnect:bootstrap-complete", + BootstrapError: "torconnect:bootstrap-error", + FatalError: "torconnect:fatal-error", +}); + +const TorConnect = (() => { let retval = { - init : function() { - let topics = [ - "TorBootstrapStatus", - ];
- for(const topic of topics) { - Services.obs.addObserver(this, topic); + _state: TorConnectState.Initial, + _bootstrapProgress: 0, + _bootstrapStatus: null, + _errorMessage: null, + _errorDetails: null, + _logHasWarningOrError: false, + // init to about:tor as fallback in case setURIsToLoad is somehow never called + _urisToLoad: ["about:tor"], + + /* These functions are called after transitioning to a new state */ + _transitionCallbacks: Object.freeze(new Map([ + /* Initial is never transitioned to */ + [TorConnectState.Initial, null], + /* Configuring */ + [TorConnectState.Configuring, (self) => { + // TODO move this to the transition function + if (this._state === TorConnectState.Bootstrapping) { + TorProtocolService.torStopBootstrap(); + } + }], + /* AutoConfiguring */ + [TorConnectState.AutoConfiguring, (self) => { + + }], + /* Bootstrapping */ + [TorConnectState.Bootstrapping, (self) => { + let error = TorProtocolService.connect(); + if (error) { + self.onError(error.message, error.details); + } else { + self._errorMessage = self._errorDetails = null; + } + }], + /* Bootstrapped */ + [TorConnectState.Bootstrapped, (self) => { + // open home page(s) in new tabs + const win = BrowserWindowTracker.getTopWindow() + + let location="tab"; + for (const uri of self._urisToLoad) { + win.openTrustedLinkIn(uri, location); + // open subsequent tabs behind first tab + location = "tabshifted"; + } + Services.obs.notifyObservers(null, TorConnectTopics.BootstrapComplete); + }], + /* Error */ + [TorConnectState.Error, (self, errorMessage, errorDetails, fatal) => { + self._errorMessage = errorMessage; + self._errorDetails = errorDetails; + + Services.obs.notifyObservers({message: errorMessage, details: errorDetails}, TorConnectTopics.BootstrapError); + if (fatal) { + self.onFatalError(); + } else { + self.beginConfigure(); + } + }], + /* FatalError */ + [TorConnectState.FatalError, (self) => { + Services.obs.notifyObservers(null, TorConnectTopics.FatalError); + }], + /* Disabled */ + [TorConnectState.Disabled, (self) => { + + }], + ])), + + _changeState: function(newState, ...args) { + const oldState = this._state; + + // ensure this is a valid state transition + if (!TorConnectStateTransitions.get(oldState)?.includes(newState)) { + throw Error(`TorConnect: Attempted invalid state transition from ${oldState} to ${newState}`); } + + console.log(`TorConnect: transitioning state from ${oldState} to ${newState}`); + + // call our transition function and forward any args + this._transitionCallbacks.get(newState)(this, ...args); + + // finally, set our new state + this._state = newState; + + Services.obs.notifyObservers({state: newState}, TorConnectTopics.StateChange); + }, + + // init should be called on app-startup in MainProcessingSingleton.jsm + init : function() { + console.log("TorConnect: Init"); + + // delay remaining init until after profile-after-change + Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange); },
observe: function(subject, topic, data) { + console.log(`TorConnect: observed ${topic}`); + switch(topic) { - case "TorBootstrapStatus": - const obj = subject?.wrappedJSObject; - if (obj?.PROGRESS === 100) { - // open home page(s) in new tabs - const win = BrowserWindowTracker.getTopWindow() - const urls = Services.prefs.getStringPref("browser.startup.homepage").split('|'); - - let location="tab"; - for(const url of urls) { - win.openTrustedLinkIn(url, location); - // open subsequent tabs behind first tab - location = "tabshifted"; + + /* Determine which state to move to from Initial */ + case BrowserTopics.ProfileAfterChange: { + if (TorLauncherUtil.useLegacyLauncher || !TorProtocolService.ownsTorDaemon) { + // Disabled + this.legacyOrSystemTor(); + } else { + // register the Tor topics we always care about + for (const topicKey in TorTopics) { + const topic = TorTopics[topicKey]; + Services.obs.addObserver(this, topic); + console.log(`TorConnect: observing topic '${topic}'`); }
- Services.obs.notifyObservers(null, "torconnect:bootstrap-complete"); + if (TorProtocolService.torProcessStatus == TorProcessStatus.Running) { + if (this.shouldQuickStart) { + // Quickstart + this.beginBootstrap(); + } else { + // Configuring + this.beginConfigure(); + } + } } + + Services.obs.removeObserver(this, topic); break; + } + /* Transition out of Initial if Tor daemon wasn't running yet in BrowserTopics.ProfileAfterChange */ + case TorTopics.ProcessIsReady: { + if (this.state === TorConnectState.Initial) + { + if (this.shouldQuickStart) { + // Quickstart + this.beginBootstrap(); + } else { + // Configuring + this.beginConfigure(); + } + } + break; + } + /* Updates our bootstrap status */ + case TorTopics.BootstrapStatus: { + if (this._state != TorConnectState.Bootstrapping) { + console.log(`TorConnect: observed ${TorTopics.BootstrapStatus} topic while in state TorConnectState.${this._state}`); + break; + } + + const obj = subject?.wrappedJSObject; + if (obj) { + this._bootstrapProgress= obj.PROGRESS; + this._bootstrapStatus = TorLauncherUtil.getLocalizedBootstrapStatus(obj, "TAG"); + + console.log(`TorConnect: Bootstrapping ${this._bootstrapProgress}% complete (${this._bootstrapStatus})`); + Services.obs.notifyObservers({ + progress: this._bootstrapProgress, + status: this._bootstrapStatus, + hasWarnings: this._logHasWarningOrError + }, TorConnectTopics.BootstrapProgress); + + if (this._bootstrapProgress === 100) { + this.bootstrapComplete(); + } + } + break; + } + /* Handle bootstrap error*/ + case TorTopics.BootstrapError: { + const obj = subject?.wrappedJSObject; + TorProtocolService.torStopBootstrap(); + this.onError(obj.message, obj.details); + break; + } + case TorTopics.LogHasWarnOrErr: { + this._logHasWarningOrError = true; + break; + } default: // ignore break; } },
- shouldShowTorConnect : function() { - return TorProtocolService.shouldShowTorConnect(); + /* + Various getters + */ + + get shouldShowTorConnect() { + // TorBrowser must control the daemon + return (TorProtocolService.ownsTorDaemon && + // and we're not using the legacy launcher + !TorLauncherUtil.useLegacyLauncher && + // legacy checks, TODO: maybe this should be in terms of our own state? + (TorProtocolService.isNetworkDisabled() || !TorProtocolService.isBootstrapDone())); + }, + + get shouldQuickStart() { + // quickstart must be enabled + return Services.prefs.getBoolPref(TorLauncherPrefs.quickstart, false) && + // and the previous bootstrap attempt must have succeeded + !Services.prefs.getBoolPref(TorLauncherPrefs.prompt_at_startup, true); + }, + + get state() { + return this._state; + }, + + get bootstrapProgress() { + return this._bootstrapProgress; + }, + + get bootstrapStatus() { + return this._bootstrapStatus; + }, + + get errorMessage() { + return this._errorMessage; + }, + + get errorDetails() { + return this._errorDetails; + }, + + get logHasWarningOrError() { + return this._logHasWarningOrError; + }, + + /* + These functions tell TorConnect to transition states + */ + + legacyOrSystemTor: function() { + console.log("TorConnect: legacyOrSystemTor()"); + this._changeState(TorConnectState.Disabled); + }, + + beginBootstrap: function() { + console.log("TorConnect: beginBootstrap()"); + this._changeState(TorConnectState.Bootstrapping); + }, + + beginConfigure: function() { + console.log("TorConnect: beginConfigure()"); + this._changeState(TorConnectState.Configuring); + }, + + autoConfigure: function() { + console.log("TorConnect: autoConfigure()"); + // TODO: implement + throw Error("TorConnect: not implemented"); + }, + + cancelAutoConfigure: function() { + console.log("TorConnect: cancelAutoConfigure()"); + // TODO: implement + throw Error("TorConnect: not implemented"); + }, + + cancelBootstrap: function() { + console.log("TorConnect: cancelBootstrap()"); + this._changeState(TorConnectState.Configuring); + }, + + bootstrapComplete: function() { + console.log("TorConnect: bootstrapComplete()"); + this._changeState(TorConnectState.Bootstrapped); + }, + + onError: function(message, details) { + console.log("TorConnect: onError()"); + this._changeState(TorConnectState.Error, message, details, false); + }, + + onFatalError: function() { + console.log("TorConnect: onFatalError()"); + // TODO: implement + throw Error("TorConnect: not implemented"); + }, + + /* + Further external commands and helper methods + */ + openTorPreferences: function() { + const win = BrowserWindowTracker.getTopWindow() + win.openTrustedLinkIn("about:preferences#tor", "tab"); + }, + + copyTorLogs: function() { + // Copy tor log messages to the system clipboard. + const chSvc = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + const countObj = { value: 0 }; + chSvc.copyString(TorProtocolService.getLog(countObj)); + const count = countObj.value; + return TorLauncherUtil.getFormattedLocalizedString( + "copiedNLogMessagesShort", + [count], + 1 + ); + }, + + // called from browser.js on browser startup, passed in either the user's homepage(s) + // or uris passed via command-line + setURIsToLoad: function(uriVariant) { + // convert the object we get from browser.js + let uris = ((v) => { + if (v instanceof Ci.nsIArray) { + // Transform the nsIArray of nsISupportsString's into a JS Array of + // JS strings. + return Array.from( + v.enumerate(Ci.nsISupportsString), + supportStr => supportStr.data + ); + } else if (v instanceof Ci.nsISupportsString) { + return [v.data]; + } else if (typeof v === "string") { + return v.split("|"); + } + // about:tor as safe fallback + return ["about:tor"]; + })(uriVariant); + + console.log(`TorConnect: will load after bootstrap => ${uris.join(", ")}`); + this._urisToLoad = uris; }, }; retval.init(); return retval; -})(); /* TorConnect */ \ No newline at end of file +})(); /* TorConnect */ diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm index fc7f2c884aa2..e6c78b9a0eb1 100644 --- a/browser/modules/TorProtocolService.jsm +++ b/browser/modules/TorProtocolService.jsm @@ -2,26 +2,59 @@
"use strict";
-var EXPORTED_SYMBOLS = ["TorProtocolService"]; +var EXPORTED_SYMBOLS = ["TorProtocolService", "TorProcessStatus"];
-const { TorLauncherUtil } = ChromeUtils.import( - "resource://torlauncher/modules/tl-util.jsm" +const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" );
-var TorProtocolService = { - _tlps: Cc["@torproject.org/torlauncher-protocol-service;1"].getService( - Ci.nsISupports - ).wrappedJSObject, +// see tl-process.js +const TorProcessStatus = Object.freeze({ + Unknown: 0, + Starting: 1, + Running: 2, + Exited: 3, +}); + +/* Browser observer topis */ +const BrowserTopics = Object.freeze({ + ProfileAfterChange: "profile-after-change", +});
- _tlproc: Cc["@torproject.org/torlauncher-process-service;1"].getService( - Ci.nsISupports - ).wrappedJSObject, +var TorProtocolService = { + _TorLauncherUtil: function() { + let { TorLauncherUtil } = ChromeUtils.import( + "resource://torlauncher/modules/tl-util.jsm" + ); + return TorLauncherUtil; + }(), + _TorLauncherProtocolService: null, + _TorProcessService: null,
// maintain a map of tor settings set by Tor Browser so that we don't // repeatedly set the same key/values over and over // this map contains string keys to primitive or array values _settingsCache: new Map(),
+ init() { + Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange); + }, + + observe(subject, topic, data) { + if (topic === BrowserTopics.ProfileAfterChange) { + // we have to delay init'ing this or else the crypto service inits too early without a profile + // which breaks the password manager + this._TorLauncherProtocolService = Cc["@torproject.org/torlauncher-protocol-service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + this._TorProcessService = Cc["@torproject.org/torlauncher-process-service;1"].getService( + Ci.nsISupports + ).wrappedJSObject, + + Services.obs.removeObserver(this, topic); + } + }, + _typeof(aValue) { switch (typeof aValue) { case "boolean": @@ -124,7 +157,7 @@ var TorProtocolService = { }
let errorObject = {}; - if (!this._tlps.TorSetConfWithReply(settingsObject, errorObject)) { + if (!this._TorLauncherProtocolService.TorSetConfWithReply(settingsObject, errorObject)) { throw new Error(errorObject.details); }
@@ -137,8 +170,8 @@ var TorProtocolService = {
_readSetting(aSetting) { this._assertValidSettingKey(aSetting); - let reply = this._tlps.TorGetConf(aSetting); - if (this._tlps.TorCommandSucceeded(reply)) { + let reply = this._TorLauncherProtocolService.TorGetConf(aSetting); + if (this._TorLauncherProtocolService.TorCommandSucceeded(reply)) { return reply.lineArray; } throw new Error(reply.lineArray.join("\n")); @@ -207,22 +240,22 @@ var TorProtocolService = {
getLog(countObj) { countObj = countObj || { value: 0 }; - let torLog = this._tlps.TorGetLog(countObj); + let torLog = this._TorLauncherProtocolService.TorGetLog(countObj); return torLog; },
// true if we launched and control tor, false if using system tor get ownsTorDaemon() { - return TorLauncherUtil.shouldStartAndOwnTor; + return this._TorLauncherUtil.shouldStartAndOwnTor; },
// Assumes `ownsTorDaemon` is true isNetworkDisabled() { - const reply = TorProtocolService._tlps.TorGetConfBool( + const reply = TorProtocolService._TorLauncherProtocolService.TorGetConfBool( "DisableNetwork", true ); - if (TorProtocolService._tlps.TorCommandSucceeded(reply)) { + if (TorProtocolService._TorLauncherProtocolService.TorCommandSucceeded(reply)) { return reply.retVal; } return true; @@ -232,22 +265,22 @@ var TorProtocolService = { let settings = {}; settings.DisableNetwork = false; let errorObject = {}; - if (!this._tlps.TorSetConfWithReply(settings, errorObject)) { + if (!this._TorLauncherProtocolService.TorSetConfWithReply(settings, errorObject)) { throw new Error(errorObject.details); } },
sendCommand(cmd) { - return this._tlps.TorSendCommand(cmd); + return this._TorLauncherProtocolService.TorSendCommand(cmd); },
retrieveBootstrapStatus() { - return this._tlps.TorRetrieveBootstrapStatus(); + return this._TorLauncherProtocolService.TorRetrieveBootstrapStatus(); },
_GetSaveSettingsErrorMessage(aDetails) { try { - return TorLauncherUtil.getSaveSettingsErrorMessage(aDetails); + return this._TorLauncherUtil.getSaveSettingsErrorMessage(aDetails); } catch (e) { console.log("GetSaveSettingsErrorMessage error", e); return "Unexpected Error"; @@ -258,7 +291,7 @@ var TorProtocolService = { let result = false; const error = {}; try { - result = this._tlps.TorSetConfWithReply(settings, error); + result = this._TorLauncherProtocolService.TorSetConfWithReply(settings, error); } catch (e) { console.log("TorSetConfWithReply error", e); error.details = this._GetSaveSettingsErrorMessage(e.message); @@ -267,23 +300,15 @@ var TorProtocolService = { },
isBootstrapDone() { - return this._tlproc.mIsBootstrapDone; + return this._TorProcessService.mIsBootstrapDone; },
clearBootstrapError() { - return this._tlproc.TorClearBootstrapError(); - }, - - shouldShowTorConnect() { - return ( - this.ownsTorDaemon && - !TorLauncherUtil.useLegacyLauncher && - (this.isNetworkDisabled() || !this.isBootstrapDone()) - ); + return this._TorProcessService.TorClearBootstrapError(); },
torBootstrapErrorOccurred() { - return this._tlproc.TorBootstrapErrorOccurred; + return this._TorProcessService.TorBootstrapErrorOccurred; },
// Resolves to null if ok, or an error otherwise @@ -306,7 +331,7 @@ var TorProtocolService = { },
torLogHasWarnOrErr() { - return this._tlps.TorLogHasWarnOrErr; + return this._TorLauncherProtocolService.TorLogHasWarnOrErr; },
torStopBootstrap() { @@ -327,4 +352,12 @@ var TorProtocolService = { } this.retrieveBootstrapStatus(); }, + + get torProcessStatus() { + if (this._TorProcessService) { + return this._TorProcessService.TorProcessStatus; + } + return TorProcessStatus.Unknown; + }, }; +TorProtocolService.init(); \ No newline at end of file diff --git a/toolkit/components/processsingleton/MainProcessSingleton.jsm b/toolkit/components/processsingleton/MainProcessSingleton.jsm index db1e2dc8f568..ea9288dccbb3 100644 --- a/toolkit/components/processsingleton/MainProcessSingleton.jsm +++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm @@ -24,6 +24,11 @@ MainProcessSingleton.prototype = { null );
+ ChromeUtils.import( + "resource:///modules/TorConnect.jsm", + null + ); + // Load this script early so that console.* is initialized // before other frame scripts. Services.mm.loadFrameScript( diff --git a/toolkit/modules/RemotePageAccessManager.jsm b/toolkit/modules/RemotePageAccessManager.jsm index 0927391c2ba7..54230e1175ec 100644 --- a/toolkit/modules/RemotePageAccessManager.jsm +++ b/toolkit/modules/RemotePageAccessManager.jsm @@ -181,28 +181,18 @@ let RemotePageAccessManager = { RPMRemoveMessageListener: ["*"], }, "about:torconnect": { - RPMAddMessageListener: ["*"], + RPMAddMessageListener: [ + "torconnect:state-change", + ], RPMSendAsyncMessage: [ - "OpenTorAdvancedPreferences", - "TorRetrieveBootstrapStatus", - "TorStopBootstrap", + "torconnect:open-tor-preferences", + "torconnect:begin-bootstrap", + "torconnect:cancel-bootstrap", + "torconnect:set-quickstart", ], RPMSendQuery: [ - "GetDirection", - "GetLocalizedBootstrapStatus", - "GetTorStrings", - "TorBootstrapErrorOccurred", - "TorConnect", - "TorCopyLog", - "TorIsNetworkDisabled", - "TorLogHasWarnOrErr", - ], - RPMGetBoolPref: [ - "extensions.torlauncher.quickstart", - "extensions.torlauncher.prompt_at_startup", - ], - RPMSetBoolPref: [ - "extensions.torlauncher.quickstart", + "torconnect:get-init-args", + "torconnect:copy-tor-logs", ], }, },
tbb-commits@lists.torproject.org