diff options
Diffstat (limited to 'devtools/shared/network-observer/NetworkAuthListener.sys.mjs')
-rw-r--r-- | devtools/shared/network-observer/NetworkAuthListener.sys.mjs | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/devtools/shared/network-observer/NetworkAuthListener.sys.mjs b/devtools/shared/network-observer/NetworkAuthListener.sys.mjs new file mode 100644 index 0000000000..2ab5517aa1 --- /dev/null +++ b/devtools/shared/network-observer/NetworkAuthListener.sys.mjs @@ -0,0 +1,185 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +/** + * This class is a simplified version from the AuthRequestor used by the + * WebExtensions codebase at: + * https://searchfox.org/mozilla-central/rev/fd2325f5b2a5be8f8f2acf9307285f2b7de06582/toolkit/components/extensions/webrequest/WebRequest.sys.mjs#434-579 + * + * The NetworkAuthListener will monitor the provided channel and will invoke the + * owner's `onAuthPrompt` end point whenever an auth challenge is requested. + * + * The owner will receive several callbacks to proceed with the prompt: + * - cancelAuthPrompt(): cancel the authentication attempt + * - forwardAuthPrompt(): forward the auth prompt request to the next + * notification callback. If no other custom callback is set, this will + * typically lead to show the auth prompt dialog in the browser UI. + * - provideAuthCredentials(username, password): attempt to authenticate with + * the provided username and password. + * + * Please note that the request will be blocked until the consumer calls one of + * the callbacks listed above. Make sure to eventually unblock the request if + * you implement `onAuthPrompt`. + * + * @param {nsIChannel} channel + * The channel to monitor. + * @param {object} owner + * The owner object, expected to implement `onAuthPrompt`. + */ +export class NetworkAuthListener { + constructor(channel, owner) { + this.notificationCallbacks = channel.notificationCallbacks; + this.loadGroupCallbacks = + channel.loadGroup && channel.loadGroup.notificationCallbacks; + this.owner = owner; + + // Setup the channel's notificationCallbacks to be handled by this instance. + channel.notificationCallbacks = this; + } + + // See https://searchfox.org/mozilla-central/source/netwerk/base/nsIAuthPrompt2.idl + asyncPromptAuth(channel, callback, context, level, authInfo) { + const isProxy = !!(authInfo.flags & authInfo.AUTH_PROXY); + const cancelAuthPrompt = () => { + if (channel.canceled) { + return; + } + + try { + callback.onAuthCancelled(context, false); + } catch (e) { + console.error(`NetworkAuthListener failed to cancel auth prompt ${e}`); + } + }; + + const forwardAuthPrompt = () => { + if (channel.canceled) { + return; + } + + const prompt = this.#getForwardPrompt(isProxy); + prompt.asyncPromptAuth(channel, callback, context, level, authInfo); + }; + + const provideAuthCredentials = (username, password) => { + if (channel.canceled) { + return; + } + + authInfo.username = username; + authInfo.password = password; + try { + callback.onAuthAvailable(context, authInfo); + } catch (e) { + console.error( + `NetworkAuthListener failed to provide auth credentials ${e}` + ); + } + }; + + const authDetails = { + isProxy, + realm: authInfo.realm, + scheme: authInfo.authenticationScheme, + }; + const authCallbacks = { + cancelAuthPrompt, + forwardAuthPrompt, + provideAuthCredentials, + }; + + // The auth callbacks may only be called asynchronously after this method + // successfully returned. + lazy.setTimeout(() => this.#notifyOwner(authDetails, authCallbacks), 1); + + return { + QueryInterface: ChromeUtils.generateQI(["nsICancelable"]), + cancel: cancelAuthPrompt, + }; + } + + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsIAuthPrompt2)) { + return this; + } + try { + return this.notificationCallbacks.getInterface(iid); + } catch (e) {} + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + + // See https://searchfox.org/mozilla-central/source/netwerk/base/nsIAuthPromptProvider.idl + getAuthPrompt(reason, iid) { + // This should never get called without getInterface having been called first. + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this; + } + return this.#getForwardedInterface(Ci.nsIAuthPromptProvider).getAuthPrompt( + reason, + iid + ); + } + + // See https://searchfox.org/mozilla-central/source/netwerk/base/nsIAuthPrompt2.idl + promptAuth(channel, level, authInfo) { + this.#getForwardedInterface(Ci.nsIAuthPrompt2).promptAuth( + channel, + level, + authInfo + ); + } + + #getForwardedInterface(iid) { + try { + return this.notificationCallbacks.getInterface(iid); + } catch (e) { + return this.loadGroupCallbacks.getInterface(iid); + } + } + + #getForwardPrompt(isProxy) { + const reason = isProxy + ? Ci.nsIAuthPromptProvider.PROMPT_PROXY + : Ci.nsIAuthPromptProvider.PROMPT_NORMAL; + for (const callbacks of [ + this.notificationCallbacks, + this.loadGroupCallbacks, + ]) { + try { + return callbacks + .getInterface(Ci.nsIAuthPromptProvider) + .getAuthPrompt(reason, Ci.nsIAuthPrompt2); + } catch (e) {} + try { + return callbacks.getInterface(Ci.nsIAuthPrompt2); + } catch (e) {} + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + + #notifyOwner(authDetails, authCallbacks) { + if (typeof this.owner.onAuthPrompt == "function") { + this.owner.onAuthPrompt(authDetails, authCallbacks); + } else { + console.error( + "NetworkObserver owner enabled the auth prompt listener " + + "but does not implement 'onAuthPrompt'. " + + "Forwarding the auth prompt to the next notification callback." + ); + authCallbacks.forwardAuthPrompt(); + } + } +} + +NetworkAuthListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIAuthPromptProvider", + "nsIAuthPrompt2", +]); |