forked from mirrors/gecko-dev
Bug 1826198 - [devtools] Add an optional auth prompt listener to the NetworkObserver r=bomsy,devtools-reviewers,valentin,necko-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D189515
This commit is contained in:
parent
1f1c8ec3da
commit
1a70dd3342
9 changed files with 662 additions and 21 deletions
185
devtools/shared/network-observer/NetworkAuthListener.sys.mjs
Normal file
185
devtools/shared/network-observer/NetworkAuthListener.sys.mjs
Normal file
|
|
@ -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",
|
||||
]);
|
||||
|
|
@ -21,6 +21,8 @@ import { DevToolsInfaillibleUtils } from "resource://devtools/shared/DevToolsInf
|
|||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ChannelMap: "resource://devtools/shared/network-observer/ChannelMap.sys.mjs",
|
||||
NetworkAuthListener:
|
||||
"resource://devtools/shared/network-observer/NetworkAuthListener.sys.mjs",
|
||||
NetworkHelper:
|
||||
"resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
|
||||
NetworkOverride:
|
||||
|
|
@ -123,6 +125,12 @@ export class NetworkObserver {
|
|||
* @type {Map}
|
||||
*/
|
||||
#decodedCertificateCache = new Map();
|
||||
/**
|
||||
* Whether the consumer supports listening and handling auth prompts.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
#authPromptListenerEnabled = false;
|
||||
/**
|
||||
* See constructor argument of the same name.
|
||||
*
|
||||
|
|
@ -229,6 +237,10 @@ export class NetworkObserver {
|
|||
);
|
||||
}
|
||||
|
||||
setAuthPromptListenerEnabled(enabled) {
|
||||
this.#authPromptListenerEnabled = enabled;
|
||||
}
|
||||
|
||||
setSaveRequestAndResponseBodies(save) {
|
||||
this.#saveRequestAndResponseBodies = save;
|
||||
}
|
||||
|
|
@ -705,6 +717,10 @@ export class NetworkObserver {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.#authPromptListenerEnabled) {
|
||||
new lazy.NetworkAuthListener(httpActivity.channel, httpActivity.owner);
|
||||
}
|
||||
|
||||
return httpActivity;
|
||||
}
|
||||
|
||||
|
|
@ -1375,6 +1391,10 @@ export class NetworkObserver {
|
|||
* listening.
|
||||
*/
|
||||
destroy() {
|
||||
if (this.#isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
|
||||
gActivityDistributor.removeObserver(this);
|
||||
Services.obs.removeObserver(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
|
|||
|
||||
DevToolsModules(
|
||||
"ChannelMap.sys.mjs",
|
||||
"NetworkAuthListener.sys.mjs",
|
||||
"NetworkHelper.sys.mjs",
|
||||
"NetworkObserver.sys.mjs",
|
||||
"NetworkOverride.sys.mjs",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ support-files = [
|
|||
"gzipped.sjs",
|
||||
"override.html",
|
||||
"override.js",
|
||||
"sjs_network-auth-listener-test-server.sjs",
|
||||
"sjs_network-observer-test-server.sjs",
|
||||
]
|
||||
|
||||
|
|
@ -16,6 +17,8 @@ skip-if = [
|
|||
"http2",
|
||||
]
|
||||
|
||||
["browser_networkobserver_auth_listener.js"]
|
||||
|
||||
["browser_networkobserver_invalid_constructor.js"]
|
||||
|
||||
["browser_networkobserver_override.js"]
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ add_task(async function testSingleRequest() {
|
|||
content.wrappedJSObject.sendRequest(_url);
|
||||
});
|
||||
|
||||
const eventsCount = await onNetworkEvents;
|
||||
is(eventsCount, 1, "Received the expected number of network events");
|
||||
const events = await onNetworkEvents;
|
||||
is(events.length, 1, "Received the expected number of network events");
|
||||
});
|
||||
|
||||
add_task(async function testMultipleRequests() {
|
||||
|
|
@ -39,9 +39,9 @@ add_task(async function testMultipleRequests() {
|
|||
}
|
||||
);
|
||||
|
||||
const eventsCount = await onNetworkEvents;
|
||||
const events = await onNetworkEvents;
|
||||
is(
|
||||
eventsCount,
|
||||
events.length,
|
||||
EXPECTED_REQUESTS_COUNT,
|
||||
"Received the expected number of network events"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,374 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { PromptTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/PromptTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
const TEST_URL = URL_ROOT + "doc_network-observer.html";
|
||||
const AUTH_URL = URL_ROOT + `sjs_network-auth-listener-test-server.sjs`;
|
||||
|
||||
// Correct credentials for sjs_network-auth-listener-test-server.sjs.
|
||||
const USERNAME = "guest";
|
||||
const PASSWORD = "guest";
|
||||
const BAD_PASSWORD = "bad";
|
||||
|
||||
// NetworkEventOwner which will cancel all auth prompt requests.
|
||||
class AuthCancellingOwner extends NetworkEventOwner {
|
||||
hasAuthPrompt = false;
|
||||
|
||||
onAuthPrompt(authDetails, authCallbacks) {
|
||||
this.hasAuthPrompt = true;
|
||||
authCallbacks.cancelAuthPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkEventOwner which will forward all auth prompt requests to the browser.
|
||||
class AuthForwardingOwner extends NetworkEventOwner {
|
||||
hasAuthPrompt = false;
|
||||
|
||||
onAuthPrompt(authDetails, authCallbacks) {
|
||||
this.hasAuthPrompt = true;
|
||||
authCallbacks.forwardAuthPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkEventOwner which will answer provided credentials to auth prompts.
|
||||
class AuthCredentialsProvidingOwner extends NetworkEventOwner {
|
||||
hasAuthPrompt = false;
|
||||
|
||||
constructor(channel, username, password) {
|
||||
super();
|
||||
|
||||
this.channel = channel;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
async onAuthPrompt(authDetails, authCallbacks) {
|
||||
this.hasAuthPrompt = true;
|
||||
|
||||
// Providing credentials immediately can lead to intermittent failures.
|
||||
// TODO: Investigate and remove.
|
||||
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
await authCallbacks.provideAuthCredentials(this.username, this.password);
|
||||
}
|
||||
|
||||
addResponseContent(content) {
|
||||
super.addResponseContent();
|
||||
this.responseContent = content.text;
|
||||
}
|
||||
}
|
||||
|
||||
add_task(async function testAuthRequestWithoutListener() {
|
||||
cleanupAuthManager();
|
||||
const tab = await addTab(TEST_URL);
|
||||
|
||||
const events = [];
|
||||
const networkObserver = new NetworkObserver({
|
||||
ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
|
||||
onNetworkEvent: event => {
|
||||
const owner = new AuthForwardingOwner();
|
||||
events.push(owner);
|
||||
return owner;
|
||||
},
|
||||
});
|
||||
registerCleanupFunction(() => networkObserver.destroy());
|
||||
|
||||
const onAuthPrompt = waitForAuthPrompt(tab);
|
||||
|
||||
await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
|
||||
content.wrappedJSObject.sendRequest(_url);
|
||||
});
|
||||
|
||||
info("Wait for a network event to be created");
|
||||
await BrowserTestUtils.waitForCondition(() => events.length >= 1);
|
||||
is(events.length, 1, "Received the expected number of network events");
|
||||
|
||||
info("Wait for the auth prompt to be displayed");
|
||||
await onAuthPrompt;
|
||||
ok(
|
||||
getTabAuthPrompts(tab).length == 1,
|
||||
"The auth prompt was not blocked by the network observer"
|
||||
);
|
||||
|
||||
// The event owner should have been called for ResponseStart and EventTimings
|
||||
assertEventOwner(events[0], {
|
||||
hasResponseStart: true,
|
||||
hasEventTimings: true,
|
||||
});
|
||||
|
||||
networkObserver.destroy();
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testAuthRequestWithForwardingListener() {
|
||||
cleanupAuthManager();
|
||||
const tab = await addTab(TEST_URL);
|
||||
|
||||
const events = [];
|
||||
const networkObserver = new NetworkObserver({
|
||||
ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
|
||||
onNetworkEvent: event => {
|
||||
info("waitForNetworkEvents received a new event");
|
||||
const owner = new AuthForwardingOwner();
|
||||
events.push(owner);
|
||||
return owner;
|
||||
},
|
||||
});
|
||||
registerCleanupFunction(() => networkObserver.destroy());
|
||||
|
||||
info("Enable the auth prompt listener for this network observer");
|
||||
networkObserver.setAuthPromptListenerEnabled(true);
|
||||
|
||||
const onAuthPrompt = waitForAuthPrompt(tab);
|
||||
|
||||
await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
|
||||
content.wrappedJSObject.sendRequest(_url);
|
||||
});
|
||||
|
||||
info("Wait for a network event to be received");
|
||||
await BrowserTestUtils.waitForCondition(() => events.length >= 1);
|
||||
is(events.length, 1, "Received the expected number of network events");
|
||||
|
||||
// The auth prompt should still be displayed since the network event owner
|
||||
// forwards the auth notification immediately.
|
||||
info("Wait for the auth prompt to be displayed");
|
||||
await onAuthPrompt;
|
||||
ok(
|
||||
getTabAuthPrompts(tab).length == 1,
|
||||
"The auth prompt was not blocked by the network observer"
|
||||
);
|
||||
|
||||
// The event owner should have been called for ResponseStart, EventTimings and
|
||||
// AuthPrompt
|
||||
assertEventOwner(events[0], {
|
||||
hasResponseStart: true,
|
||||
hasEventTimings: true,
|
||||
hasAuthPrompt: true,
|
||||
});
|
||||
|
||||
networkObserver.destroy();
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testAuthRequestWithCancellingListener() {
|
||||
cleanupAuthManager();
|
||||
const tab = await addTab(TEST_URL);
|
||||
|
||||
const events = [];
|
||||
const networkObserver = new NetworkObserver({
|
||||
ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
|
||||
onNetworkEvent: event => {
|
||||
const owner = new AuthCancellingOwner();
|
||||
events.push(owner);
|
||||
return owner;
|
||||
},
|
||||
});
|
||||
registerCleanupFunction(() => networkObserver.destroy());
|
||||
|
||||
info("Enable the auth prompt listener for this network observer");
|
||||
networkObserver.setAuthPromptListenerEnabled(true);
|
||||
|
||||
await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
|
||||
content.wrappedJSObject.sendRequest(_url);
|
||||
});
|
||||
|
||||
info("Wait for a network event to be received");
|
||||
await BrowserTestUtils.waitForCondition(() => events.length >= 1);
|
||||
is(events.length, 1, "Received the expected number of network events");
|
||||
|
||||
await BrowserTestUtils.waitForCondition(() => events[0].hasResponseContent);
|
||||
|
||||
// The auth prompt should not be displayed since the authentication was
|
||||
// cancelled.
|
||||
ok(
|
||||
!getTabAuthPrompts(tab).length,
|
||||
"The auth prompt was cancelled by the network event owner"
|
||||
);
|
||||
|
||||
assertEventOwner(events[0], {
|
||||
hasResponseStart: true,
|
||||
hasResponseContent: true,
|
||||
hasEventTimings: true,
|
||||
hasServerTimings: true,
|
||||
hasAuthPrompt: true,
|
||||
hasSecurityInfo: true,
|
||||
});
|
||||
|
||||
networkObserver.destroy();
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testAuthRequestWithWrongCredentialsListener() {
|
||||
cleanupAuthManager();
|
||||
const tab = await addTab(TEST_URL);
|
||||
|
||||
const events = [];
|
||||
const networkObserver = new NetworkObserver({
|
||||
ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
|
||||
onNetworkEvent: (event, channel) => {
|
||||
const owner = new AuthCredentialsProvidingOwner(
|
||||
channel,
|
||||
USERNAME,
|
||||
BAD_PASSWORD
|
||||
);
|
||||
events.push(owner);
|
||||
return owner;
|
||||
},
|
||||
});
|
||||
registerCleanupFunction(() => networkObserver.destroy());
|
||||
|
||||
info("Enable the auth prompt listener for this network observer");
|
||||
networkObserver.setAuthPromptListenerEnabled(true);
|
||||
|
||||
await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
|
||||
content.wrappedJSObject.sendRequest(_url);
|
||||
});
|
||||
|
||||
info("Wait for all network events to be received");
|
||||
await BrowserTestUtils.waitForCondition(() => events.length >= 1);
|
||||
is(events.length, 1, "Received the expected number of network events");
|
||||
|
||||
// The auth prompt should not be displayed since the authentication was
|
||||
// fulfilled.
|
||||
ok(
|
||||
!getTabAuthPrompts(tab).length,
|
||||
"The auth prompt was handled by the network event owner"
|
||||
);
|
||||
|
||||
assertEventOwner(events[0], {
|
||||
hasAuthPrompt: true,
|
||||
hasResponseStart: true,
|
||||
hasEventTimings: true,
|
||||
});
|
||||
|
||||
networkObserver.destroy();
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testAuthRequestWithCredentialsListener() {
|
||||
cleanupAuthManager();
|
||||
const tab = await addTab(TEST_URL);
|
||||
|
||||
const events = [];
|
||||
const networkObserver = new NetworkObserver({
|
||||
ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL,
|
||||
onNetworkEvent: (event, channel) => {
|
||||
const owner = new AuthCredentialsProvidingOwner(
|
||||
channel,
|
||||
USERNAME,
|
||||
PASSWORD
|
||||
);
|
||||
events.push(owner);
|
||||
return owner;
|
||||
},
|
||||
});
|
||||
registerCleanupFunction(() => networkObserver.destroy());
|
||||
|
||||
info("Enable the auth prompt listener for this network observer");
|
||||
networkObserver.setAuthPromptListenerEnabled(true);
|
||||
|
||||
await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => {
|
||||
content.wrappedJSObject.sendRequest(_url);
|
||||
});
|
||||
|
||||
// TODO: At the moment, providing credentials will result in additional
|
||||
// network events collected by the NetworkObserver, whereas we would expect
|
||||
// to keep the same event.
|
||||
// For successful auth prompts, we receive an additional event.
|
||||
// The last event will contain the responseContent flag.
|
||||
info("Wait for all network events to be received");
|
||||
await BrowserTestUtils.waitForCondition(() => events.length >= 2);
|
||||
is(events.length, 2, "Received the expected number of network events");
|
||||
|
||||
// Since the auth prompt was canceled we should also receive the security
|
||||
// information for the channel, but we still don't get response content.
|
||||
await BrowserTestUtils.waitForCondition(() => events[1].hasResponseContent);
|
||||
|
||||
// The auth prompt should not be displayed since the authentication was
|
||||
// fulfilled.
|
||||
ok(
|
||||
!getTabAuthPrompts(tab).length,
|
||||
"The auth prompt was handled by the network event owner"
|
||||
);
|
||||
|
||||
assertEventOwner(events[1], {
|
||||
hasResponseStart: true,
|
||||
hasEventTimings: true,
|
||||
hasSecurityInfo: true,
|
||||
hasServerTimings: true,
|
||||
hasResponseContent: true,
|
||||
});
|
||||
|
||||
is(events[1].responseContent, "success", "Auth prompt was successful");
|
||||
|
||||
networkObserver.destroy();
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
function assertEventOwner(event, expectedFlags) {
|
||||
is(
|
||||
event.hasResponseStart,
|
||||
!!expectedFlags.hasResponseStart,
|
||||
"network event has the expected ResponseStart flag"
|
||||
);
|
||||
is(
|
||||
event.hasEventTimings,
|
||||
!!expectedFlags.hasEventTimings,
|
||||
"network event has the expected EventTimings flag"
|
||||
);
|
||||
is(
|
||||
event.hasAuthPrompt,
|
||||
!!expectedFlags.hasAuthPrompt,
|
||||
"network event has the expected AuthPrompt flag"
|
||||
);
|
||||
is(
|
||||
event.hasResponseCache,
|
||||
!!expectedFlags.hasResponseCache,
|
||||
"network event has the expected ResponseCache flag"
|
||||
);
|
||||
is(
|
||||
event.hasResponseContent,
|
||||
!!expectedFlags.hasResponseContent,
|
||||
"network event has the expected ResponseContent flag"
|
||||
);
|
||||
is(
|
||||
event.hasSecurityInfo,
|
||||
!!expectedFlags.hasSecurityInfo,
|
||||
"network event has the expected SecurityInfo flag"
|
||||
);
|
||||
is(
|
||||
event.hasServerTimings,
|
||||
!!expectedFlags.hasServerTimings,
|
||||
"network event has the expected ServerTimings flag"
|
||||
);
|
||||
}
|
||||
|
||||
function getTabAuthPrompts(tab) {
|
||||
const tabDialogBox = gBrowser.getTabDialogBox(tab.linkedBrowser);
|
||||
return tabDialogBox
|
||||
.getTabDialogManager()
|
||||
._dialogs.filter(
|
||||
d => d.frameContentWindow?.Dialog.args.promptType == "promptUserAndPass"
|
||||
);
|
||||
}
|
||||
|
||||
function waitForAuthPrompt(tab) {
|
||||
return PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
|
||||
modalType: Services.prompt.MODAL_TYPE_TAB,
|
||||
promptType: "promptUserAndPass",
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup potentially stored credentials before running any test.
|
||||
function cleanupAuthManager() {
|
||||
const authManager = SpecialPowers.Cc[
|
||||
"@mozilla.org/network/http-auth-manager;1"
|
||||
].getService(SpecialPowers.Ci.nsIHttpAuthManager);
|
||||
authManager.clearAll();
|
||||
}
|
||||
|
|
@ -46,18 +46,43 @@ async function addTab(url) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a simple network event owner, with empty implementations of all
|
||||
* Base network event owner class implementing all mandatory callbacks and
|
||||
* keeping track of which callbacks have been called.
|
||||
*/
|
||||
class NetworkEventOwner {
|
||||
hasEventTimings = false;
|
||||
hasResponseCache = false;
|
||||
hasResponseContent = false;
|
||||
hasResponseStart = false;
|
||||
hasSecurityInfo = false;
|
||||
hasServerTimings = false;
|
||||
|
||||
addEventTimings() {
|
||||
this.hasEventTimings = true;
|
||||
}
|
||||
addResponseCache() {
|
||||
this.hasResponseCache = true;
|
||||
}
|
||||
addResponseContent() {
|
||||
this.hasResponseContent = true;
|
||||
}
|
||||
addResponseStart() {
|
||||
this.hasResponseStart = true;
|
||||
}
|
||||
addSecurityInfo() {
|
||||
this.hasSecurityInfo = true;
|
||||
}
|
||||
addServerTimings() {
|
||||
this.hasServerTimings = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple network event owner, with mock implementations of all
|
||||
* the expected APIs for a NetworkEventOwner.
|
||||
*/
|
||||
function createNetworkEventOwner(event) {
|
||||
return {
|
||||
addEventTimings: () => {},
|
||||
addResponseCache: () => {},
|
||||
addResponseContent: () => {},
|
||||
addResponseStart: () => {},
|
||||
addSecurityInfo: () => {},
|
||||
addServerTimings: () => {},
|
||||
};
|
||||
return new NetworkEventOwner();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -69,24 +94,25 @@ function createNetworkEventOwner(event) {
|
|||
* @param {number} expectedRequestsCount
|
||||
* How many different events (requests) are expected.
|
||||
* @returns {Promise}
|
||||
* A promise which will resolve with the current count, when the expected
|
||||
* count is reached.
|
||||
* A promise which will resolve with an array of network event owners, when
|
||||
* the expected event count is reached.
|
||||
*/
|
||||
async function waitForNetworkEvents(expectedUrl, expectedRequestsCount) {
|
||||
let eventsCount = 0;
|
||||
const events = [];
|
||||
const networkObserver = new NetworkObserver({
|
||||
ignoreChannelFunction: channel => channel.URI.spec !== expectedUrl,
|
||||
onNetworkEvent: event => {
|
||||
onNetworkEvent: () => {
|
||||
info("waitForNetworkEvents received a new event");
|
||||
eventsCount++;
|
||||
return createNetworkEventOwner(event);
|
||||
const owner = createNetworkEventOwner();
|
||||
events.push(owner);
|
||||
return owner;
|
||||
},
|
||||
});
|
||||
registerCleanupFunction(() => networkObserver.destroy());
|
||||
|
||||
info("Wait until the events count reaches " + expectedRequestsCount);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => eventsCount >= expectedRequestsCount
|
||||
() => events.length >= expectedRequestsCount
|
||||
);
|
||||
return eventsCount;
|
||||
return events;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
function handleRequest(request, response) {
|
||||
let body;
|
||||
|
||||
// Expect guest/guest as correct credentials, but `btoa` is unavailable in sjs
|
||||
// "Z3Vlc3Q6Z3Vlc3Q=" == btoa("guest:guest")
|
||||
const expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
|
||||
|
||||
// correct login credentials provided
|
||||
if (
|
||||
request.hasHeader("Authorization") &&
|
||||
request.getHeader("Authorization") == expectedHeader
|
||||
) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK, authorized");
|
||||
response.setHeader("Content-Type", "text", false);
|
||||
|
||||
body = "success";
|
||||
} else {
|
||||
// incorrect credentials
|
||||
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
response.setHeader("Content-Type", "text", false);
|
||||
|
||||
body = "failed";
|
||||
}
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
|
@ -5557,6 +5557,7 @@ NS_IMETHODIMP nsHttpChannel::OnAuthAvailable() {
|
|||
|
||||
NS_IMETHODIMP nsHttpChannel::OnAuthCancelled(bool userCancel) {
|
||||
LOG(("nsHttpChannel::OnAuthCancelled [this=%p]", this));
|
||||
MOZ_ASSERT(mAuthRetryPending, "OnAuthCancelled should not be called twice");
|
||||
|
||||
if (mTransactionPump) {
|
||||
// If the channel is trying to authenticate to a proxy and
|
||||
|
|
|
|||
Loading…
Reference in a new issue