Bug 1665062: HTTPS-Only: Upgraded website creating HTTP auth prompt gets interrupted by error-page r=necko-reviewers,dragana,JulianWels

Differential Revision: https://phabricator.services.mozilla.com/D91908
This commit is contained in:
Christoph Kerschbaumer 2020-10-07 11:47:07 +00:00
parent c5e3e23059
commit d43cc4af7e
14 changed files with 171 additions and 5 deletions

View File

@ -12,7 +12,7 @@
#include "nsHTTPSOnlyUtils.h"
#include "nsIConsoleService.h"
#include "nsIHttpChannel.h"
#include "nsIHttpChannel.h"
#include "nsIHttpChannelInternal.h"
#include "nsIHttpsOnlyModePermission.h"
#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
@ -421,14 +421,21 @@ TestHTTPAnswerRunnable::OnStartRequest(nsIRequest* aRequest) {
}
// Check if the original top-level channel which https-only is trying
// to upgrade is already in progress. If it is, then all good, if not
// to upgrade is already in progress or if the channel is an auth channel.
// If it is in progress or Auth is in progress, then all good, if not
// then let's cancel that channel so we can dispaly the exception page.
nsCOMPtr<nsIChannel> httpsOnlyChannel = mDocumentLoadListener->GetChannel();
if (httpsOnlyChannel) {
nsCOMPtr<nsILoadInfo> loadInfo = httpsOnlyChannel->LoadInfo();
uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
if (!(httpsOnlyStatus &
nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS)) {
uint32_t topLevelLoadInProgress =
loadInfo->GetHttpsOnlyStatus() &
nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS;
nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal =
do_QueryInterface(httpsOnlyChannel);
bool isAuthChannel = false;
Unused << httpChannelInternal->GetIsAuthChannel(&isAuthChannel);
if (!topLevelLoadInProgress && !isAuthChannel) {
// Only really cancel the original top-level channel if it's
// status is still NS_OK, otherwise it might have already
// encountered some other error and was cancelled.

View File

@ -0,0 +1,16 @@
// Custom *.sjs file specifically for the needs of Bug 1665062
function handleRequest(request, response) {
// avoid confusing cache behaviors
response.setHeader("Cache-Control", "no-cache", false);
if (request.scheme === "https") {
response.setHeader("Content-Type", "text/html;charset=utf-8", false);
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
response.setHeader("WWW-Authenticate", "Basic realm=\"bug1665062\"");
return;
}
// we should never get here; just in case, return something unexpected
response.write("do'h");
}

View File

@ -14,3 +14,5 @@ scheme=https
fail-if = xorigin
[test_http_background_request.html]
support-files = file_http_background_request.sjs
[test_http_background_auth_request.html]
support-files = file_http_background_auth_request.sjs

View File

@ -0,0 +1,100 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Bug 1665062 - HTTPS-Only: Do not cancel channel if auth is in progress</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script class="testbody" type="text/javascript">
/*
* Description of the test:
* We send a top-level request which results in a '401 - Unauthorized' and ensure that the
* http background request does not accidentally treat that request as a potential timeout.
* We make sure that ther HTTPS-Only Mode Error Page does *NOT* show up.
*/
SimpleTest.waitForExplicitFinish();
SimpleTest.requestFlakyTimeout("When Auth is in progress, HTTPS-Only page should not show up");
SimpleTest.requestLongerTimeout(8);
const EXPECTED_KICK_OFF_REQUEST =
"http://test1.example.com/tests/dom/security/test/https-only/file_http_background_auth_request.sjs?foo";
const EXPECTED_UPGRADE_REQUEST = EXPECTED_KICK_OFF_REQUEST.replace("http://", "https://");
let EXPECTED_BG_REQUEST = "http://test1.example.com/";
let requestCounter = 0;
function examiner() {
SpecialPowers.addObserver(this, "specialpowers-http-notify-request");
}
examiner.prototype = {
observe(subject, topic, data) {
if (topic !== "specialpowers-http-notify-request") {
return;
}
// On Android we have other requests appear here as well. Let's make
// sure we only evaluate requests triggered by the test.
if (!data.startsWith("http://test1.example.com") &&
!data.startsWith("https://test1.example.com")) {
return;
}
++requestCounter;
if (requestCounter == 1) {
is(data, EXPECTED_KICK_OFF_REQUEST, "kick off request needs to be http");
return;
}
if (requestCounter == 2) {
is(data, EXPECTED_UPGRADE_REQUEST, "upgraded request needs to be https");
return;
}
if (requestCounter == 3) {
is(data, EXPECTED_BG_REQUEST, "background request needs to be http and no sensitive info");
return;
}
ok(false, "we should never get here, but just in case");
},
remove() {
SpecialPowers.removeObserver(this, "specialpowers-http-notify-request");
}
}
window.BackgroundRequestExaminer = new examiner();
// https-only top-level background request occurs after 3 seconds, hence
// we use 5 seconds to make sure the background request did not happen.
function resolveAfter5Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, 5000);
});
}
async function runTests() {
await SpecialPowers.pushPrefEnv({ set: [
["dom.security.https_only_mode", true],
["dom.security.https_only_mode_send_http_background_request", true],
]});
let testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank");
// Give the Auth Process and background request some time before moving on.
await resolveAfter5Seconds();
is(requestCounter, 3, "three requests total (kickoff, upgraded, background)");
let wrappedWin = SpecialPowers.wrap(testWin);
is(wrappedWin.location.href, "about:blank", "displaying authentication happens on about:blank");
is(wrappedWin.document.body.innerHTML, "", "exception page should not be displayed");
testWin.close();
window.BackgroundRequestExaminer.remove();
SimpleTest.finish();
}
runTests();
</script>
</body>
</html>

View File

@ -353,6 +353,11 @@ ClassifierDummyChannel::SetupFallbackChannel(const char* aFallbackKey) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
ClassifierDummyChannel::GetIsAuthChannel(bool* aIsAuthChannel) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
ClassifierDummyChannel::GetThirdPartyFlags(uint32_t* aThirdPartyFlags) {
return NS_ERROR_NOT_IMPLEMENTED;

View File

@ -2340,6 +2340,9 @@ HttpChannelChild::SetupFallbackChannel(const char* aFallbackKey) {
DROP_DEAD();
}
NS_IMETHODIMP
HttpChannelChild::GetIsAuthChannel(bool* aIsAuthChannel) { DROP_DEAD(); }
//-----------------------------------------------------------------------------
// HttpChannelChild::nsICacheInfoChannel
//-----------------------------------------------------------------------------

View File

@ -101,6 +101,7 @@ class HttpChannelChild final : public PHttpChannelChild,
void DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() override;
// nsIHttpChannelInternal
NS_IMETHOD SetupFallbackChannel(const char* aFallbackKey) override;
NS_IMETHOD GetIsAuthChannel(bool* aIsAuthChannel) override;
// nsISupportsPriority
NS_IMETHOD SetPriority(int32_t value) override;
// nsIClassOfService

View File

@ -552,6 +552,11 @@ InterceptedHttpChannel::SetupFallbackChannel(const char* aFallbackKey) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
InterceptedHttpChannel::GetIsAuthChannel(bool* aIsAuthChannel) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
InterceptedHttpChannel::SetPriority(int32_t aPriority) {
mPriority = clamped<int32_t>(aPriority, INT16_MIN, INT16_MAX);

View File

@ -159,6 +159,9 @@ class InterceptedHttpChannel final
NS_IMETHOD
SetupFallbackChannel(const char* aFallbackKey) override;
NS_IMETHOD
GetIsAuthChannel(bool* aIsAuthChannel) override;
NS_IMETHOD
SetPriority(int32_t aPriority) override;

View File

@ -1275,6 +1275,11 @@ TRRServiceChannel::SetupFallbackChannel(const char* aFallbackKey) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
TRRServiceChannel::GetIsAuthChannel(bool* aIsAuthChannel) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
TRRServiceChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) {
mCallbacks = aCallbacks;

View File

@ -73,6 +73,7 @@ class TRRServiceChannel : public HttpBaseChannel,
const nsAString& aURL,
const nsAString& aContentType) override;
NS_IMETHOD SetupFallbackChannel(const char* aFallbackKey) override;
NS_IMETHOD GetIsAuthChannel(bool* aIsAuthChannel) override;
NS_IMETHOD SetNotificationCallbacks(
nsIInterfaceRequestor* aCallbacks) override;

View File

@ -356,6 +356,7 @@ nsHttpChannel::nsHttpChannel()
mCacheOpenWithPriority(false),
mCacheQueueSizeWhenOpen(0),
mCachedContentIsValid(false),
mIsAuthChannel(false),
mAuthRetryPending(false),
mCachedContentIsPartial(false),
mCacheOnlyMetadata(false),
@ -2874,6 +2875,7 @@ nsresult nsHttpChannel::ContinueProcessResponse3(nsresult rv) {
if (rv == NS_ERROR_IN_PROGRESS) {
// authentication prompt has been invoked and result
// is expected asynchronously
mIsAuthChannel = true;
mAuthRetryPending = true;
if (httpStatus == 407 ||
(mTransaction && mTransaction->ProxyConnectFailed()))
@ -2903,6 +2905,7 @@ nsresult nsHttpChannel::ContinueProcessResponse3(nsresult rv) {
}
rv = ProcessNormal();
} else {
mIsAuthChannel = true;
mAuthRetryPending = true; // see DoAuthRetry
}
break;
@ -6221,6 +6224,7 @@ NS_IMETHODIMP nsHttpChannel::OnAuthAvailable() {
// setting mAuthRetryPending flag and resuming the transaction
// triggers process of throwing away the unauthenticated data already
// coming from the network
mIsAuthChannel = true;
mAuthRetryPending = true;
mProxyAuthPending = false;
LOG(("Resuming the transaction, we got credentials from user"));
@ -7248,6 +7252,12 @@ nsHttpChannel::SetupFallbackChannel(const char* aFallbackKey) {
return NS_OK;
}
NS_IMETHODIMP
nsHttpChannel::GetIsAuthChannel(bool* aIsAuthChannel) {
*aIsAuthChannel = mIsAuthChannel;
return NS_OK;
}
NS_IMETHODIMP
nsHttpChannel::SetChannelIsForDownload(bool aChannelIsForDownload) {
if (aChannelIsForDownload) {

View File

@ -152,6 +152,7 @@ class nsHttpChannel final : public HttpBaseChannel,
NS_IMETHOD GetEncodedBodySize(uint64_t* aEncodedBodySize) override;
// nsIHttpChannelInternal
NS_IMETHOD SetupFallbackChannel(const char* aFallbackKey) override;
NS_IMETHOD GetIsAuthChannel(bool* aIsAuthChannel) override;
NS_IMETHOD SetChannelIsForDownload(bool aChannelIsForDownload) override;
NS_IMETHOD GetNavigationStartTimeStamp(TimeStamp* aTimeStamp) override;
NS_IMETHOD SetNavigationStartTimeStamp(TimeStamp aTimeStamp) override;
@ -652,6 +653,7 @@ class nsHttpChannel final : public HttpBaseChannel,
uint32_t mCacheQueueSizeWhenOpen;
Atomic<bool, Relaxed> mCachedContentIsValid;
Atomic<bool> mIsAuthChannel;
Atomic<bool> mAuthRetryPending;
// state flags

View File

@ -92,6 +92,12 @@ interface nsIHttpChannelInternal : nsISupports
*/
[must_use] void setupFallbackChannel(in string aFallbackKey);
/**
* Returns true in case this channel is used for auth;
* (the response header includes 'www-authenticate').
*/
[noscript, must_use] readonly attribute bool isAuthChannel;
/**
* This flag is set to force relevant cookies to be sent with this load
* even if normally they wouldn't be.