... |
... |
@@ -2,350 +2,417 @@ |
2
|
2
|
|
3
|
3
|
"use strict";
|
4
|
4
|
|
5
|
|
-const OnionAuthPrompt = (function () {
|
|
5
|
+var OnionAuthPrompt = {
|
6
|
6
|
// Only import to our internal scope, rather than the global scope of
|
7
|
7
|
// browser.xhtml.
|
8
|
|
- const lazy = {};
|
9
|
|
- ChromeUtils.defineESModuleGetters(lazy, {
|
10
|
|
- TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
|
11
|
|
- TorStrings: "resource://gre/modules/TorStrings.sys.mjs",
|
12
|
|
- CommonUtils: "resource://services-common/utils.sys.mjs",
|
13
|
|
- });
|
14
|
|
-
|
15
|
|
- // OnionServicesAuthPrompt objects run within the main/chrome process.
|
16
|
|
- // aReason is the topic passed within the observer notification that is
|
17
|
|
- // causing this auth prompt to be displayed.
|
18
|
|
- function OnionServicesAuthPrompt(aBrowser, aFailedURI, aReason, aOnionName) {
|
19
|
|
- this._browser = aBrowser;
|
20
|
|
- this._failedURI = aFailedURI;
|
21
|
|
- this._reasonForPrompt = aReason;
|
22
|
|
- this._onionHostname = aOnionName;
|
23
|
|
- }
|
24
|
|
-
|
25
|
|
- const topics = {
|
|
8
|
+ _lazy: {},
|
|
9
|
+
|
|
10
|
+ /**
|
|
11
|
+ * The topics to listen to.
|
|
12
|
+ *
|
|
13
|
+ * @type {Object<string, string>}
|
|
14
|
+ */
|
|
15
|
+ _topics: {
|
26
|
16
|
clientAuthMissing: "tor-onion-services-clientauth-missing",
|
27
|
17
|
clientAuthIncorrect: "tor-onion-services-clientauth-incorrect",
|
28
|
|
- };
|
29
|
|
-
|
30
|
|
- OnionServicesAuthPrompt.prototype = {
|
31
|
|
- show(aWarningMessage) {
|
32
|
|
- let mainAction = {
|
33
|
|
- label: lazy.TorStrings.onionServices.authPrompt.done,
|
34
|
|
- accessKey: lazy.TorStrings.onionServices.authPrompt.doneAccessKey,
|
35
|
|
- leaveOpen: true, // Callback is responsible for closing the notification.
|
36
|
|
- callback: this._onDone.bind(this),
|
37
|
|
- };
|
38
|
|
-
|
39
|
|
- let dialogBundle = Services.strings.createBundle(
|
40
|
|
- "chrome://global/locale/dialog.properties"
|
41
|
|
- );
|
42
|
|
-
|
43
|
|
- let cancelAccessKey = dialogBundle.GetStringFromName("accesskey-cancel");
|
44
|
|
- if (!cancelAccessKey) {
|
45
|
|
- cancelAccessKey = "c";
|
46
|
|
- } // required by PopupNotifications.show()
|
47
|
|
-
|
48
|
|
- let cancelAction = {
|
49
|
|
- label: dialogBundle.GetStringFromName("button-cancel"),
|
50
|
|
- accessKey: cancelAccessKey,
|
51
|
|
- callback: this._onCancel.bind(this),
|
52
|
|
- };
|
53
|
|
-
|
54
|
|
- let _this = this;
|
55
|
|
- let options = {
|
56
|
|
- autofocus: true,
|
57
|
|
- hideClose: true,
|
58
|
|
- persistent: true,
|
59
|
|
- removeOnDismissal: false,
|
60
|
|
- eventCallback(aTopic) {
|
61
|
|
- if (aTopic === "showing") {
|
62
|
|
- _this._onPromptShowing(aWarningMessage);
|
63
|
|
- } else if (aTopic === "shown") {
|
64
|
|
- _this._onPromptShown();
|
65
|
|
- } else if (aTopic === "removed") {
|
66
|
|
- _this._onPromptRemoved();
|
67
|
|
- }
|
68
|
|
- },
|
69
|
|
- };
|
70
|
|
-
|
71
|
|
- this._prompt = PopupNotifications.show(
|
72
|
|
- this._browser,
|
73
|
|
- "tor-clientauth",
|
74
|
|
- "",
|
75
|
|
- "tor-clientauth-notification-icon",
|
76
|
|
- mainAction,
|
77
|
|
- [cancelAction],
|
78
|
|
- options
|
79
|
|
- );
|
80
|
|
- },
|
81
|
|
-
|
82
|
|
- _onPromptShowing(aWarningMessage) {
|
83
|
|
- let xulDoc = this._browser.ownerDocument;
|
84
|
|
- let descElem = xulDoc.getElementById("tor-clientauth-notification-desc");
|
85
|
|
- if (descElem) {
|
86
|
|
- // Handle replacement of the onion name within the localized
|
87
|
|
- // string ourselves so we can show the onion name as bold text.
|
88
|
|
- // We do this by splitting the localized string and creating
|
89
|
|
- // several HTML <span> elements.
|
90
|
|
- const fmtString = lazy.TorStrings.onionServices.authPrompt.description;
|
91
|
|
- const [prefix, suffix] = fmtString.split("%S");
|
92
|
|
-
|
93
|
|
- const domainEl = xulDoc.createElement("span");
|
94
|
|
- domainEl.id = "tor-clientauth-notification-onionname";
|
95
|
|
- domainEl.textContent = TorUIUtils.shortenOnionAddress(
|
96
|
|
- this._onionHostname
|
97
|
|
- );
|
98
|
|
-
|
99
|
|
- descElem.replaceChildren(prefix, domainEl, suffix);
|
100
|
|
- }
|
101
|
|
-
|
102
|
|
- // Set "Learn More" label and href.
|
103
|
|
- let learnMoreElem = xulDoc.getElementById(
|
104
|
|
- "tor-clientauth-notification-learnmore"
|
105
|
|
- );
|
106
|
|
- if (learnMoreElem) {
|
107
|
|
- learnMoreElem.setAttribute(
|
108
|
|
- "value",
|
109
|
|
- lazy.TorStrings.onionServices.learnMore
|
110
|
|
- );
|
111
|
|
- learnMoreElem.setAttribute(
|
112
|
|
- "href",
|
113
|
|
- "about:manual#onion-services_onion-service-authentication"
|
114
|
|
- );
|
115
|
|
- learnMoreElem.setAttribute("useoriginprincipal", "true");
|
116
|
|
- }
|
117
|
|
-
|
118
|
|
- this._showWarning(aWarningMessage);
|
119
|
|
- let checkboxElem = this._getCheckboxElement();
|
120
|
|
- if (checkboxElem) {
|
121
|
|
- checkboxElem.checked = false;
|
122
|
|
- }
|
123
|
|
- },
|
124
|
|
-
|
125
|
|
- _onPromptShown() {
|
126
|
|
- let keyElem = this._getKeyElement();
|
127
|
|
- if (keyElem) {
|
128
|
|
- keyElem.setAttribute(
|
129
|
|
- "placeholder",
|
130
|
|
- lazy.TorStrings.onionServices.authPrompt.keyPlaceholder
|
131
|
|
- );
|
132
|
|
- this._boundOnKeyFieldKeyPress = this._onKeyFieldKeyPress.bind(this);
|
133
|
|
- this._boundOnKeyFieldInput = this._onKeyFieldInput.bind(this);
|
134
|
|
- keyElem.addEventListener("keypress", this._boundOnKeyFieldKeyPress);
|
135
|
|
- keyElem.addEventListener("input", this._boundOnKeyFieldInput);
|
136
|
|
- keyElem.focus();
|
137
|
|
- }
|
138
|
|
- },
|
139
|
|
-
|
140
|
|
- _onPromptRemoved() {
|
141
|
|
- if (this._boundOnKeyFieldKeyPress) {
|
142
|
|
- let keyElem = this._getKeyElement();
|
143
|
|
- if (keyElem) {
|
144
|
|
- keyElem.value = "";
|
145
|
|
- keyElem.removeEventListener(
|
146
|
|
- "keypress",
|
147
|
|
- this._boundOnKeyFieldKeyPress
|
148
|
|
- );
|
149
|
|
- this._boundOnKeyFieldKeyPress = undefined;
|
150
|
|
- keyElem.removeEventListener("input", this._boundOnKeyFieldInput);
|
151
|
|
- this._boundOnKeyFieldInput = undefined;
|
|
18
|
+ },
|
|
19
|
+
|
|
20
|
+ /**
|
|
21
|
+ * @typedef {object} PromptDetails
|
|
22
|
+ *
|
|
23
|
+ * @property {Browser} browser - The browser this prompt is for.
|
|
24
|
+ * @property {string} cause - The notification that cause this prompt.
|
|
25
|
+ * @property {string} onionHost - The onion host name.
|
|
26
|
+ * @property {nsIURI} uri - The browser URI when the notification was
|
|
27
|
+ * triggered.
|
|
28
|
+ * @property {string} onionServiceId - The onion service ID for this host.
|
|
29
|
+ * @property {Notification} [notification] - The notification instance for
|
|
30
|
+ * this prompt.
|
|
31
|
+ */
|
|
32
|
+
|
|
33
|
+ /**
|
|
34
|
+ * The currently shown details in the prompt.
|
|
35
|
+ */
|
|
36
|
+ _shownDetails: null,
|
|
37
|
+
|
|
38
|
+ /**
|
|
39
|
+ * Used for logging to represent PromptDetails.
|
|
40
|
+ *
|
|
41
|
+ * @param {PromptDetails} details - The details to represent.
|
|
42
|
+ * @returns {string} - The representation of these details.
|
|
43
|
+ */
|
|
44
|
+ _detailsRepr(details) {
|
|
45
|
+ if (!details) {
|
|
46
|
+ return "none";
|
|
47
|
+ }
|
|
48
|
+ return `${details.browser.browserId}:${details.onionHost}`;
|
|
49
|
+ },
|
|
50
|
+
|
|
51
|
+ /**
|
|
52
|
+ * Show a new prompt, using the given details.
|
|
53
|
+ *
|
|
54
|
+ * @param {PromptDetails} details - The details to show.
|
|
55
|
+ */
|
|
56
|
+ show(details) {
|
|
57
|
+ this._logger.debug(`New Notification: ${this._detailsRepr(details)}`);
|
|
58
|
+
|
|
59
|
+ let mainAction = {
|
|
60
|
+ label: this.TorStrings.onionServices.authPrompt.done,
|
|
61
|
+ accessKey: this.TorStrings.onionServices.authPrompt.doneAccessKey,
|
|
62
|
+ leaveOpen: true, // Callback is responsible for closing the notification.
|
|
63
|
+ callback: this._onDone.bind(this),
|
|
64
|
+ };
|
|
65
|
+
|
|
66
|
+ let dialogBundle = Services.strings.createBundle(
|
|
67
|
+ "chrome://global/locale/dialog.properties"
|
|
68
|
+ );
|
|
69
|
+
|
|
70
|
+ let cancelAccessKey = dialogBundle.GetStringFromName("accesskey-cancel");
|
|
71
|
+ if (!cancelAccessKey) {
|
|
72
|
+ cancelAccessKey = "c";
|
|
73
|
+ } // required by PopupNotifications.show()
|
|
74
|
+
|
|
75
|
+ // The first secondarybuttoncommand (cancelAction) should be triggered when
|
|
76
|
+ // the user presses "Escape".
|
|
77
|
+ let cancelAction = {
|
|
78
|
+ label: dialogBundle.GetStringFromName("button-cancel"),
|
|
79
|
+ accessKey: cancelAccessKey,
|
|
80
|
+ callback: this._onCancel.bind(this),
|
|
81
|
+ };
|
|
82
|
+
|
|
83
|
+ let options = {
|
|
84
|
+ autofocus: true,
|
|
85
|
+ hideClose: true,
|
|
86
|
+ persistent: true,
|
|
87
|
+ removeOnDismissal: false,
|
|
88
|
+ eventCallback: topic => {
|
|
89
|
+ if (topic === "showing") {
|
|
90
|
+ this._onPromptShowing(details);
|
|
91
|
+ } else if (topic === "shown") {
|
|
92
|
+ this._onPromptShown();
|
|
93
|
+ } else if (topic === "removed") {
|
|
94
|
+ this._onPromptRemoved(details);
|
152
|
95
|
}
|
|
96
|
+ },
|
|
97
|
+ };
|
|
98
|
+
|
|
99
|
+ details.notification = PopupNotifications.show(
|
|
100
|
+ details.browser,
|
|
101
|
+ "tor-clientauth",
|
|
102
|
+ "",
|
|
103
|
+ "tor-clientauth-notification-icon",
|
|
104
|
+ mainAction,
|
|
105
|
+ [cancelAction],
|
|
106
|
+ options
|
|
107
|
+ );
|
|
108
|
+ },
|
|
109
|
+
|
|
110
|
+ /**
|
|
111
|
+ * Callback when the prompt is about to be shown.
|
|
112
|
+ *
|
|
113
|
+ * @param {PromptDetails?} details - The details to show, or null to shown
|
|
114
|
+ * none.
|
|
115
|
+ */
|
|
116
|
+ _onPromptShowing(details) {
|
|
117
|
+ if (details === this._shownDetails) {
|
|
118
|
+ // The last shown details match this one exactly.
|
|
119
|
+ // This happens when we switch tabs to a page that has no prompt and then
|
|
120
|
+ // switch back.
|
|
121
|
+ // We don't want to reset the current state in this case.
|
|
122
|
+ // In particular, we keep the current _keyInput value and _persistCheckbox
|
|
123
|
+ // the same.
|
|
124
|
+ this._logger.debug(`Already showing: ${this._detailsRepr(details)}`);
|
|
125
|
+ return;
|
|
126
|
+ }
|
|
127
|
+
|
|
128
|
+ this._logger.debug(`Now showing: ${this._detailsRepr(details)}`);
|
|
129
|
+
|
|
130
|
+ this._shownDetails = details;
|
|
131
|
+
|
|
132
|
+ // Clear the key input.
|
|
133
|
+ // In particular, clear the input when switching tabs.
|
|
134
|
+ this._keyInput.value = "";
|
|
135
|
+ this._persistCheckbox.checked = false;
|
|
136
|
+
|
|
137
|
+ // Handle replacement of the onion name within the localized
|
|
138
|
+ // string ourselves so we can show the onion name as bold text.
|
|
139
|
+ // We do this by splitting the localized string and creating
|
|
140
|
+ // several HTML <span> elements.
|
|
141
|
+ const fmtString = this.TorStrings.onionServices.authPrompt.description;
|
|
142
|
+ const [prefix, suffix] = fmtString.split("%S");
|
|
143
|
+
|
|
144
|
+ const domainEl = document.createElement("span");
|
|
145
|
+ domainEl.id = "tor-clientauth-notification-onionname";
|
|
146
|
+ domainEl.textContent = TorUIUtils.shortenOnionAddress(
|
|
147
|
+ this._shownDetails?.onionHost ?? ""
|
|
148
|
+ );
|
|
149
|
+
|
|
150
|
+ this._descriptionEl.replaceChildren(prefix, domainEl, suffix);
|
|
151
|
+
|
|
152
|
+ this._showWarning(undefined);
|
|
153
|
+ },
|
|
154
|
+
|
|
155
|
+ /**
|
|
156
|
+ * Callback after the prompt is shown.
|
|
157
|
+ */
|
|
158
|
+ _onPromptShown() {
|
|
159
|
+ this._keyInput.focus();
|
|
160
|
+ },
|
|
161
|
+
|
|
162
|
+ /**
|
|
163
|
+ * Callback when a Notification is removed.
|
|
164
|
+ *
|
|
165
|
+ * @param {PromptDetails} details - The details for the removed notification.
|
|
166
|
+ */
|
|
167
|
+ _onPromptRemoved(details) {
|
|
168
|
+ if (details !== this._shownDetails) {
|
|
169
|
+ // Removing the notification for some other page.
|
|
170
|
+ // For example, closing another tab that also requires authentication.
|
|
171
|
+ this._logger.debug(`Removed not shown: ${this._detailsRepr(details)}`);
|
|
172
|
+ return;
|
|
173
|
+ }
|
|
174
|
+ this._logger.debug(`Removed shown: ${this._detailsRepr(details)}`);
|
|
175
|
+ // Reset the prompt as a precaution.
|
|
176
|
+ // In particular, we want to clear the input so that the entered key does
|
|
177
|
+ // not persist.
|
|
178
|
+ this._onPromptShowing(null);
|
|
179
|
+ },
|
|
180
|
+
|
|
181
|
+ /**
|
|
182
|
+ * Callback when the user submits the key.
|
|
183
|
+ */
|
|
184
|
+ async _onDone() {
|
|
185
|
+ this._logger.debug(
|
|
186
|
+ `Sumbitting key: ${this._detailsRepr(this._shownDetails)}`
|
|
187
|
+ );
|
|
188
|
+
|
|
189
|
+ // Grab the details before they might change as we await.
|
|
190
|
+ const { browser, onionServiceId, notification } = this._shownDetails;
|
|
191
|
+ const isPermanent = this._persistCheckbox.checked;
|
|
192
|
+
|
|
193
|
+ const base64key = this._keyToBase64(this._keyInput.value);
|
|
194
|
+ if (!base64key) {
|
|
195
|
+ this._showWarning(this.TorStrings.onionServices.authPrompt.invalidKey);
|
|
196
|
+ return;
|
|
197
|
+ }
|
|
198
|
+
|
|
199
|
+ try {
|
|
200
|
+ const provider = await this._lazy.TorProviderBuilder.build();
|
|
201
|
+ await provider.onionAuthAdd(onionServiceId, base64key, isPermanent);
|
|
202
|
+ } catch (e) {
|
|
203
|
+ if (e.torMessage) {
|
|
204
|
+ this._showWarning(e.torMessage);
|
|
205
|
+ } else {
|
|
206
|
+ this._logger.error(`Failed to set key for ${onionServiceId}`, e);
|
|
207
|
+ this._showWarning(
|
|
208
|
+ this.TorStrings.onionServices.authPrompt.failedToSetKey
|
|
209
|
+ );
|
153
|
210
|
}
|
154
|
|
- },
|
155
|
|
-
|
156
|
|
- _onKeyFieldKeyPress(aEvent) {
|
157
|
|
- if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
|
158
|
|
- this._onDone();
|
159
|
|
- } else if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
|
160
|
|
- this._prompt.remove();
|
161
|
|
- this._onCancel();
|
162
|
|
- }
|
163
|
|
- },
|
164
|
|
-
|
165
|
|
- _onKeyFieldInput(aEvent) {
|
166
|
|
- this._showWarning(undefined); // Remove the warning.
|
167
|
|
- },
|
168
|
|
-
|
169
|
|
- async _onDone() {
|
170
|
|
- const keyElem = this._getKeyElement();
|
171
|
|
- if (!keyElem) {
|
172
|
|
- return;
|
173
|
|
- }
|
174
|
|
-
|
175
|
|
- const base64key = this._keyToBase64(keyElem.value);
|
176
|
|
- if (!base64key) {
|
177
|
|
- this._showWarning(lazy.TorStrings.onionServices.authPrompt.invalidKey);
|
178
|
|
- return;
|
179
|
|
- }
|
180
|
|
-
|
181
|
|
- this._prompt.remove();
|
182
|
|
-
|
183
|
|
- const controllerFailureMsg =
|
184
|
|
- lazy.TorStrings.onionServices.authPrompt.failedToSetKey;
|
|
211
|
+ return;
|
|
212
|
+ }
|
|
213
|
+
|
|
214
|
+ notification.remove();
|
|
215
|
+ // Success! Reload the page.
|
|
216
|
+ browser.sendMessageToActor("Browser:Reload", {}, "BrowserTab");
|
|
217
|
+ },
|
|
218
|
+
|
|
219
|
+ /**
|
|
220
|
+ * Callback when the user dismisses the prompt.
|
|
221
|
+ */
|
|
222
|
+ _onCancel() {
|
|
223
|
+ // Arrange for an error page to be displayed:
|
|
224
|
+ // we build a short script calling docShell.displayError()
|
|
225
|
+ // and we pass it as a data: URI to loadFrameScript(),
|
|
226
|
+ // which runs it in the content frame which triggered
|
|
227
|
+ // this authentication prompt.
|
|
228
|
+ this._logger.debug(`Cancelling: ${this._detailsRepr(this._shownDetails)}`);
|
|
229
|
+
|
|
230
|
+ const { browser, cause, uri } = this._shownDetails;
|
|
231
|
+ const errorCode =
|
|
232
|
+ cause === this._topics.clientAuthMissing
|
|
233
|
+ ? Cr.NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH
|
|
234
|
+ : Cr.NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH;
|
|
235
|
+ const io =
|
|
236
|
+ 'ChromeUtils.import("resource://gre/modules/Services.jsm").Services.io';
|
|
237
|
+
|
|
238
|
+ browser.messageManager.loadFrameScript(
|
|
239
|
+ `data:application/javascript,${encodeURIComponent(
|
|
240
|
+ `docShell.displayLoadError(${errorCode}, ${io}.newURI(${JSON.stringify(
|
|
241
|
+ uri.spec
|
|
242
|
+ )}), undefined, undefined);`
|
|
243
|
+ )}`,
|
|
244
|
+ false
|
|
245
|
+ );
|
|
246
|
+ },
|
|
247
|
+
|
|
248
|
+ /**
|
|
249
|
+ * Show a warning message to the user or clear the warning.
|
|
250
|
+ *
|
|
251
|
+ * @param {string?} warningMessage - The message to show, or undefined to
|
|
252
|
+ * clear the current message.
|
|
253
|
+ */
|
|
254
|
+ _showWarning(warningMessage) {
|
|
255
|
+ this._logger.debug(`Showing warning: ${warningMessage}`);
|
|
256
|
+ if (warningMessage) {
|
|
257
|
+ this._warningEl.textContent = warningMessage;
|
|
258
|
+ this._warningEl.removeAttribute("hidden");
|
|
259
|
+ this._keyInput.classList.add("invalid");
|
|
260
|
+ } else {
|
|
261
|
+ this._warningEl.setAttribute("hidden", "true");
|
|
262
|
+ this._keyInput.classList.remove("invalid");
|
|
263
|
+ }
|
|
264
|
+ },
|
|
265
|
+
|
|
266
|
+ /**
|
|
267
|
+ * Convert the user-entered key into base64.
|
|
268
|
+ *
|
|
269
|
+ * @param {string} keyString - The key to convert.
|
|
270
|
+ * @returns {string?} - The base64 representation, or undefined if the given
|
|
271
|
+ * key was not the correct format.
|
|
272
|
+ */
|
|
273
|
+ _keyToBase64(keyString) {
|
|
274
|
+ if (!keyString) {
|
|
275
|
+ return undefined;
|
|
276
|
+ }
|
|
277
|
+
|
|
278
|
+ let base64key;
|
|
279
|
+ if (keyString.length === 52) {
|
|
280
|
+ // The key is probably base32-encoded. Attempt to decode.
|
|
281
|
+ // Although base32 specifies uppercase letters, we accept lowercase
|
|
282
|
+ // as well because users may type in lowercase or copy a key out of
|
|
283
|
+ // a tor onion-auth file (which uses lowercase).
|
|
284
|
+ let rawKey;
|
185
|
285
|
try {
|
186
|
|
- // ^(subdomain.)*onionserviceid.onion$ (case-insensitive)
|
187
|
|
- const onionServiceIdRegExp =
|
188
|
|
- /^(.*\.)*(?<onionServiceId>[a-z2-7]{56})\.onion$/i;
|
189
|
|
- // match() will return null on bad match, causing throw
|
190
|
|
- const onionServiceId = this._onionHostname
|
191
|
|
- .match(onionServiceIdRegExp)
|
192
|
|
- .groups.onionServiceId.toLowerCase();
|
193
|
|
-
|
194
|
|
- const checkboxElem = this._getCheckboxElement();
|
195
|
|
- const isPermanent = checkboxElem && checkboxElem.checked;
|
196
|
|
- const provider = await lazy.TorProviderBuilder.build();
|
197
|
|
- await provider.onionAuthAdd(onionServiceId, base64key, isPermanent);
|
198
|
|
- // Success! Reload the page.
|
199
|
|
- this._browser.sendMessageToActor("Browser:Reload", {}, "BrowserTab");
|
200
|
|
- } catch (e) {
|
201
|
|
- if (e.torMessage) {
|
202
|
|
- this.show(e.torMessage);
|
203
|
|
- } else {
|
204
|
|
- console.error(controllerFailureMsg, e);
|
205
|
|
- this.show(controllerFailureMsg);
|
206
|
|
- }
|
207
|
|
- }
|
208
|
|
- },
|
209
|
|
-
|
210
|
|
- _onCancel() {
|
211
|
|
- // Arrange for an error page to be displayed:
|
212
|
|
- // we build a short script calling docShell.displayError()
|
213
|
|
- // and we pass it as a data: URI to loadFrameScript(),
|
214
|
|
- // which runs it in the content frame which triggered
|
215
|
|
- // this authentication prompt.
|
216
|
|
- const failedURI = this._failedURI.spec;
|
217
|
|
- const errorCode =
|
218
|
|
- this._reasonForPrompt === topics.clientAuthMissing
|
219
|
|
- ? Cr.NS_ERROR_TOR_ONION_SVC_MISSING_CLIENT_AUTH
|
220
|
|
- : Cr.NS_ERROR_TOR_ONION_SVC_BAD_CLIENT_AUTH;
|
221
|
|
- const io =
|
222
|
|
- 'ChromeUtils.import("resource://gre/modules/Services.jsm").Services.io';
|
223
|
|
-
|
224
|
|
- this._browser.messageManager.loadFrameScript(
|
225
|
|
- `data:application/javascript,${encodeURIComponent(
|
226
|
|
- `docShell.displayLoadError(${errorCode}, ${io}.newURI(${JSON.stringify(
|
227
|
|
- failedURI
|
228
|
|
- )}), undefined, undefined);`
|
229
|
|
- )}`,
|
230
|
|
- false
|
231
|
|
- );
|
232
|
|
- },
|
233
|
|
-
|
234
|
|
- _getKeyElement() {
|
235
|
|
- let xulDoc = this._browser.ownerDocument;
|
236
|
|
- return xulDoc.getElementById("tor-clientauth-notification-key");
|
237
|
|
- },
|
238
|
|
-
|
239
|
|
- _getCheckboxElement() {
|
240
|
|
- let xulDoc = this._browser.ownerDocument;
|
241
|
|
- return xulDoc.getElementById("tor-clientauth-persistkey-checkbox");
|
242
|
|
- },
|
243
|
|
-
|
244
|
|
- _showWarning(aWarningMessage) {
|
245
|
|
- let xulDoc = this._browser.ownerDocument;
|
246
|
|
- let warningElem = xulDoc.getElementById("tor-clientauth-warning");
|
247
|
|
- let keyElem = this._getKeyElement();
|
248
|
|
- if (warningElem) {
|
249
|
|
- if (aWarningMessage) {
|
250
|
|
- warningElem.textContent = aWarningMessage;
|
251
|
|
- warningElem.removeAttribute("hidden");
|
252
|
|
- if (keyElem) {
|
253
|
|
- keyElem.className = "invalid";
|
254
|
|
- }
|
255
|
|
- } else {
|
256
|
|
- warningElem.setAttribute("hidden", "true");
|
257
|
|
- if (keyElem) {
|
258
|
|
- keyElem.className = "";
|
259
|
|
- }
|
260
|
|
- }
|
261
|
|
- }
|
262
|
|
- },
|
263
|
|
-
|
264
|
|
- // Returns undefined if the key is the wrong length or format.
|
265
|
|
- _keyToBase64(aKeyString) {
|
266
|
|
- if (!aKeyString) {
|
267
|
|
- return undefined;
|
268
|
|
- }
|
|
286
|
+ rawKey = this._lazy.CommonUtils.decodeBase32(keyString.toUpperCase());
|
|
287
|
+ } catch (e) {}
|
269
|
288
|
|
270
|
|
- let base64key;
|
271
|
|
- if (aKeyString.length == 52) {
|
272
|
|
- // The key is probably base32-encoded. Attempt to decode.
|
273
|
|
- // Although base32 specifies uppercase letters, we accept lowercase
|
274
|
|
- // as well because users may type in lowercase or copy a key out of
|
275
|
|
- // a tor onion-auth file (which uses lowercase).
|
276
|
|
- let rawKey;
|
|
289
|
+ if (rawKey) {
|
277
|
290
|
try {
|
278
|
|
- rawKey = lazy.CommonUtils.decodeBase32(aKeyString.toUpperCase());
|
|
291
|
+ base64key = btoa(rawKey);
|
279
|
292
|
} catch (e) {}
|
280
|
|
-
|
281
|
|
- if (rawKey) {
|
282
|
|
- try {
|
283
|
|
- base64key = btoa(rawKey);
|
284
|
|
- } catch (e) {}
|
285
|
|
- }
|
286
|
|
- } else if (
|
287
|
|
- aKeyString.length == 44 &&
|
288
|
|
- /^[a-zA-Z0-9+/]*=*$/.test(aKeyString)
|
289
|
|
- ) {
|
290
|
|
- // The key appears to be a correctly formatted base64 value. If not,
|
291
|
|
- // tor will return an error when we try to add the key via the
|
292
|
|
- // control port.
|
293
|
|
- base64key = aKeyString;
|
294
|
293
|
}
|
295
|
|
-
|
296
|
|
- return base64key;
|
297
|
|
- },
|
298
|
|
- };
|
299
|
|
-
|
300
|
|
- let retval = {
|
301
|
|
- init() {
|
302
|
|
- Services.obs.addObserver(this, topics.clientAuthMissing);
|
303
|
|
- Services.obs.addObserver(this, topics.clientAuthIncorrect);
|
304
|
|
- },
|
305
|
|
-
|
306
|
|
- uninit() {
|
307
|
|
- Services.obs.removeObserver(this, topics.clientAuthMissing);
|
308
|
|
- Services.obs.removeObserver(this, topics.clientAuthIncorrect);
|
309
|
|
- },
|
310
|
|
-
|
311
|
|
- // aSubject is the DOM Window or browser where the prompt should be shown.
|
312
|
|
- // aData contains the .onion name.
|
313
|
|
- observe(aSubject, aTopic, aData) {
|
314
|
|
- if (
|
315
|
|
- aTopic != topics.clientAuthMissing &&
|
316
|
|
- aTopic != topics.clientAuthIncorrect
|
317
|
|
- ) {
|
318
|
|
- return;
|
319
|
|
- }
|
320
|
|
-
|
321
|
|
- let browser;
|
322
|
|
- if (aSubject instanceof Ci.nsIDOMWindow) {
|
323
|
|
- let contentWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
|
324
|
|
- browser = contentWindow.docShell.chromeEventHandler;
|
325
|
|
- } else {
|
326
|
|
- browser = aSubject.QueryInterface(Ci.nsIBrowser);
|
327
|
|
- }
|
328
|
|
-
|
329
|
|
- if (!gBrowser.browsers.some(aBrowser => aBrowser == browser)) {
|
330
|
|
- return; // This window does not contain the subject browser; ignore.
|
|
294
|
+ } else if (
|
|
295
|
+ keyString.length === 44 &&
|
|
296
|
+ /^[a-zA-Z0-9+/]*=*$/.test(keyString)
|
|
297
|
+ ) {
|
|
298
|
+ // The key appears to be a correctly formatted base64 value. If not,
|
|
299
|
+ // tor will return an error when we try to add the key via the
|
|
300
|
+ // control port.
|
|
301
|
+ base64key = keyString;
|
|
302
|
+ }
|
|
303
|
+
|
|
304
|
+ return base64key;
|
|
305
|
+ },
|
|
306
|
+
|
|
307
|
+ /**
|
|
308
|
+ * Initialize the authentication prompt.
|
|
309
|
+ */
|
|
310
|
+ init() {
|
|
311
|
+ this._logger = console.createInstance({
|
|
312
|
+ prefix: "OnionAuthPrompt",
|
|
313
|
+ maxLogLevel: "Warn",
|
|
314
|
+ maxLogLevelPref: "browser.onionAuthPrompt.loglevel",
|
|
315
|
+ });
|
|
316
|
+
|
|
317
|
+ const { TorStrings } = ChromeUtils.importESModule(
|
|
318
|
+ "resource://gre/modules/TorStrings.sys.mjs"
|
|
319
|
+ );
|
|
320
|
+ this.TorStrings = TorStrings;
|
|
321
|
+ ChromeUtils.defineESModuleGetters(this._lazy, {
|
|
322
|
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
|
|
323
|
+ CommonUtils: "resource://services-common/utils.sys.mjs",
|
|
324
|
+ });
|
|
325
|
+
|
|
326
|
+ this._keyInput = document.getElementById("tor-clientauth-notification-key");
|
|
327
|
+ this._persistCheckbox = document.getElementById(
|
|
328
|
+ "tor-clientauth-persistkey-checkbox"
|
|
329
|
+ );
|
|
330
|
+ this._warningEl = document.getElementById("tor-clientauth-warning");
|
|
331
|
+ this._descriptionEl = document.getElementById(
|
|
332
|
+ "tor-clientauth-notification-desc"
|
|
333
|
+ );
|
|
334
|
+
|
|
335
|
+ // Set "Learn More" label and href.
|
|
336
|
+ const learnMoreElem = document.getElementById(
|
|
337
|
+ "tor-clientauth-notification-learnmore"
|
|
338
|
+ );
|
|
339
|
+ learnMoreElem.setAttribute(
|
|
340
|
+ "value",
|
|
341
|
+ this.TorStrings.onionServices.learnMore
|
|
342
|
+ );
|
|
343
|
+
|
|
344
|
+ this._keyInput.setAttribute(
|
|
345
|
+ "placeholder",
|
|
346
|
+ this.TorStrings.onionServices.authPrompt.keyPlaceholder
|
|
347
|
+ );
|
|
348
|
+ this._keyInput.addEventListener("keydown", event => {
|
|
349
|
+ if (event.key === "Enter") {
|
|
350
|
+ event.preventDefault();
|
|
351
|
+ this._onDone();
|
331
|
352
|
}
|
332
|
|
-
|
333
|
|
- let failedURI = browser.currentURI;
|
334
|
|
- let authPrompt = new OnionServicesAuthPrompt(
|
335
|
|
- browser,
|
336
|
|
- failedURI,
|
337
|
|
- aTopic,
|
338
|
|
- aData
|
|
353
|
+ });
|
|
354
|
+ this._keyInput.addEventListener("input", event => {
|
|
355
|
+ // Remove the warning.
|
|
356
|
+ this._showWarning(undefined);
|
|
357
|
+ });
|
|
358
|
+
|
|
359
|
+ Services.obs.addObserver(this, this._topics.clientAuthMissing);
|
|
360
|
+ Services.obs.addObserver(this, this._topics.clientAuthIncorrect);
|
|
361
|
+ },
|
|
362
|
+
|
|
363
|
+ /**
|
|
364
|
+ * Un-initialize the authentication prompt.
|
|
365
|
+ */
|
|
366
|
+ uninit() {
|
|
367
|
+ Services.obs.removeObserver(this, this._topics.clientAuthMissing);
|
|
368
|
+ Services.obs.removeObserver(this, this._topics.clientAuthIncorrect);
|
|
369
|
+ },
|
|
370
|
+
|
|
371
|
+ observe(subject, topic, data) {
|
|
372
|
+ if (
|
|
373
|
+ topic !== this._topics.clientAuthMissing &&
|
|
374
|
+ topic !== this._topics.clientAuthIncorrect
|
|
375
|
+ ) {
|
|
376
|
+ return;
|
|
377
|
+ }
|
|
378
|
+
|
|
379
|
+ // "subject" is the DOM window or browser where the prompt should be shown.
|
|
380
|
+ let browser;
|
|
381
|
+ if (subject instanceof Ci.nsIDOMWindow) {
|
|
382
|
+ let contentWindow = subject.QueryInterface(Ci.nsIDOMWindow);
|
|
383
|
+ browser = contentWindow.docShell.chromeEventHandler;
|
|
384
|
+ } else {
|
|
385
|
+ browser = subject.QueryInterface(Ci.nsIBrowser);
|
|
386
|
+ }
|
|
387
|
+
|
|
388
|
+ if (!gBrowser.browsers.includes(browser)) {
|
|
389
|
+ // This window does not contain the subject browser.
|
|
390
|
+ this._logger.debug(
|
|
391
|
+ `Window ${window.docShell.outerWindowID}: Ignoring ${topic}`
|
339
|
392
|
);
|
340
|
|
- authPrompt.show(undefined);
|
341
|
|
- },
|
342
|
|
- };
|
343
|
|
-
|
344
|
|
- return retval;
|
345
|
|
-})(); /* OnionAuthPrompt */
|
346
|
|
-
|
347
|
|
-Object.defineProperty(this, "OnionAuthPrompt", {
|
348
|
|
- value: OnionAuthPrompt,
|
349
|
|
- enumerable: true,
|
350
|
|
- writable: false,
|
351
|
|
-}); |
|
393
|
+ return;
|
|
394
|
+ }
|
|
395
|
+ this._logger.debug(
|
|
396
|
+ `Window ${window.docShell.outerWindowID}: Handling ${topic}`
|
|
397
|
+ );
|
|
398
|
+
|
|
399
|
+ const onionHost = data;
|
|
400
|
+ // ^(subdomain.)*onionserviceid.onion$ (case-insensitive)
|
|
401
|
+ const onionServiceId = onionHost
|
|
402
|
+ .match(/^(.*\.)?(?<onionServiceId>[a-z2-7]{56})\.onion$/i)
|
|
403
|
+ ?.groups.onionServiceId.toLowerCase();
|
|
404
|
+ if (!onionServiceId) {
|
|
405
|
+ this._logger.error(`Malformed onion address: ${onionHost}`);
|
|
406
|
+ return;
|
|
407
|
+ }
|
|
408
|
+
|
|
409
|
+ const details = {
|
|
410
|
+ browser,
|
|
411
|
+ cause: topic,
|
|
412
|
+ onionHost,
|
|
413
|
+ uri: browser.currentURI,
|
|
414
|
+ onionServiceId,
|
|
415
|
+ };
|
|
416
|
+ this.show(details);
|
|
417
|
+ },
|
|
418
|
+}; |