Bug 1900375 - Add setResponseOverride to nsIHttpChannelInternal.idl r=necko-reviewers,kershaw

Depends on D213399

Differential Revision: https://phabricator.services.mozilla.com/D213400
This commit is contained in:
Julian Descottes 2024-06-26 15:31:07 +00:00
parent c3bdfb1fa4
commit 073990e32f
8 changed files with 297 additions and 7 deletions

View File

@ -105,6 +105,11 @@ class HttpChannelChild final : public PHttpChannelChild,
NS_IMETHOD SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) override;
NS_IMETHOD SetWebTransportSessionEventListener(
WebTransportSessionEventListener* aListener) override;
NS_IMETHOD SetResponseOverride(
nsIReplacedHttpResponse* aReplacedHttpResponse) override {
return NS_OK;
}
// nsISupportsPriority
NS_IMETHOD SetPriority(int32_t value) override;
// nsIClassOfService

View File

@ -294,6 +294,11 @@ class InterceptedHttpChannel final
return NS_OK;
}
NS_IMETHOD SetResponseOverride(
nsIReplacedHttpResponse* aReplacedHttpResponse) override {
return NS_OK;
}
NS_IMETHOD SetLaunchServiceWorkerStart(TimeStamp aTimeStamp) override;
NS_IMETHOD GetLaunchServiceWorkerStart(TimeStamp* aRetVal) override;

View File

@ -98,6 +98,11 @@ class TRRServiceChannel : public HttpBaseChannel,
return NS_OK;
}
NS_IMETHOD SetResponseOverride(
nsIReplacedHttpResponse* aReplacedHttpResponse) override {
return NS_OK;
}
[[nodiscard]] nsresult OnPush(uint32_t aPushedStreamId,
const nsACString& aUrl,
const nsACString& aRequestString,

View File

@ -89,6 +89,7 @@
#include "nsIStreamConverterService.h"
#include "nsISiteSecurityService.h"
#include "nsString.h"
#include "nsStringStream.h"
#include "mozilla/dom/PerformanceStorage.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/Telemetry.h"
@ -880,9 +881,123 @@ nsresult nsHttpChannel::ContinueOnBeforeConnect(bool aShouldUpgrade,
return CallOrWaitForResume([](auto* self) { return self->Connect(); });
}
class MOZ_STACK_CLASS AddResponseHeadersToResponseHead final
: public nsIHttpHeaderVisitor {
public:
explicit AddResponseHeadersToResponseHead(nsHttpResponseHead* aResponseHead)
: mResponseHead(aResponseHead) {}
NS_IMETHOD VisitHeader(const nsACString& aHeader,
const nsACString& aValue) override {
nsAutoCString headerLine = aHeader + ": "_ns + aValue;
DebugOnly<nsresult> rv = mResponseHead->ParseHeaderLine(headerLine);
MOZ_ASSERT(NS_SUCCEEDED(rv));
return NS_OK;
}
NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
// Stub AddRef/Release since this is a stack class.
NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override {
return ++mRefCnt;
}
NS_IMETHOD_(MozExternalRefCountType) Release(void) override {
return --mRefCnt;
}
virtual ~AddResponseHeadersToResponseHead() {
MOZ_DIAGNOSTIC_ASSERT(mRefCnt == 0);
}
private:
nsHttpResponseHead* mResponseHead;
nsrefcnt mRefCnt = 0;
};
NS_IMPL_QUERY_INTERFACE(AddResponseHeadersToResponseHead, nsIHttpHeaderVisitor)
nsresult nsHttpChannel::HandleOverrideResponse() {
// Start building a response with the data from mOverrideResponse.
mResponseHead = MakeUnique<nsHttpResponseHead>();
// Apply override response status code and status text.
uint32_t statusCode;
nsresult rv = mOverrideResponse->GetResponseStatus(&statusCode);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString statusText;
rv = mOverrideResponse->GetResponseStatusText(statusText);
NS_ENSURE_SUCCESS(rv, rv);
// Hardcoding protocol HTTP/1.1
nsPrintfCString line("HTTP/1.1 %u %s", statusCode, statusText.get());
rv = mResponseHead->ParseStatusLine(line);
NS_ENSURE_SUCCESS(rv, rv);
// Apply override response headers.
AddResponseHeadersToResponseHead visitor(mResponseHead.get());
rv = mOverrideResponse->VisitResponseHeaders(&visitor);
NS_ENSURE_SUCCESS(rv, rv);
if (WillRedirect(*mResponseHead)) {
// TODO: Bug 759040 - We should call HandleAsyncRedirect directly here,
// to avoid event dispatching latency.
LOG(("Skipping read of overridden response redirect entity\n"));
return AsyncCall(&nsHttpChannel::HandleAsyncRedirect);
}
// Handle Set-Cookie headers as if the response was from networking.
if (nsAutoCString cookie;
NS_SUCCEEDED(mResponseHead->GetHeader(nsHttp::Set_Cookie, cookie))) {
SetCookie(cookie);
nsCOMPtr<nsIParentChannel> parentChannel;
NS_QueryNotificationCallbacks(this, parentChannel);
if (RefPtr<HttpChannelParent> httpParent = do_QueryObject(parentChannel)) {
httpParent->SetCookie(std::move(cookie));
}
}
rv = ProcessSecurityHeaders();
if (NS_FAILED(rv)) {
NS_WARNING("ProcessSecurityHeaders failed, continuing load.");
}
if ((statusCode < 500) && (statusCode != 421)) {
ProcessAltService();
}
nsAutoCString body;
rv = mOverrideResponse->GetResponseBody(body);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIInputStream> stringStream;
rv = NS_NewCStringInputStream(getter_AddRefs(stringStream), body);
NS_ENSURE_SUCCESS(rv, rv);
rv = nsInputStreamPump::Create(getter_AddRefs(mCachePump), stringStream, 0, 0,
true);
if (NS_FAILED(rv)) {
stringStream->Close();
return rv;
}
rv = mCachePump->AsyncRead(this);
if (NS_FAILED(rv)) return rv;
return NS_OK;
}
nsresult nsHttpChannel::Connect() {
LOG(("nsHttpChannel::Connect [this=%p]\n", this));
// If mOverrideResponse is set, bypass the rest of the connection and reply
// immediately with a response built using the data from mOverrideResponse.
if (mOverrideResponse) {
return HandleOverrideResponse();
}
// Don't allow resuming when cache must be used
if (LoadResuming() && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) {
LOG(("Resuming from cache is not supported yet"));
@ -10734,6 +10849,13 @@ nsHttpChannel::EarlyHint(const nsACString& aLinkHeader,
return NS_OK;
}
NS_IMETHODIMP nsHttpChannel::SetResponseOverride(
nsIReplacedHttpResponse* aReplacedHttpResponse) {
mOverrideResponse = new nsMainThreadPtrHolder<nsIReplacedHttpResponse>(
"nsIReplacedHttpResponse", aReplacedHttpResponse);
return NS_OK;
}
NS_IMETHODIMP nsHttpChannel::SetWebTransportSessionEventListener(
WebTransportSessionEventListener* aListener) {
mWebTransportSessionEventListener = aListener;

View File

@ -10,6 +10,7 @@
#include "AlternateServices.h"
#include "AutoClose.h"
#include "HttpBaseChannel.h"
#include "nsIReplacedHttpResponse.h"
#include "TimingStruct.h"
#include "mozilla/AtomicBitfields.h"
#include "mozilla/Atomics.h"
@ -206,6 +207,8 @@ class nsHttpChannel final : public HttpBaseChannel,
NS_IMETHOD SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) override;
NS_IMETHOD SetWebTransportSessionEventListener(
WebTransportSessionEventListener* aListener) override;
NS_IMETHOD SetResponseOverride(
nsIReplacedHttpResponse* aReplacedHttpResponse) override;
void SetWarningReporter(HttpChannelSecurityWarningReporter* aReporter);
HttpChannelSecurityWarningReporter* GetWarningReporter();
@ -344,6 +347,7 @@ class nsHttpChannel final : public HttpBaseChannel,
void AsyncContinueProcessResponse();
[[nodiscard]] nsresult ContinueProcessResponse1();
[[nodiscard]] nsresult ContinueProcessResponse2(nsresult);
nsresult HandleOverrideResponse();
public:
void UpdateCacheDisposition(bool aSuccessfulReval, bool aPartialContentUsed);
@ -861,6 +865,7 @@ class nsHttpChannel final : public HttpBaseChannel,
RefPtr<nsIEarlyHintObserver> mEarlyHintObserver;
Maybe<nsCString> mOpenerCallingScriptLocation;
RefPtr<WebTransportSessionEventListener> mWebTransportSessionEventListener;
nsMainThreadPtrHandle<nsIReplacedHttpResponse> mOverrideResponse;
};
NS_DEFINE_STATIC_IID_ACCESSOR(nsHttpChannel, NS_HTTPCHANNEL_IID)

View File

@ -5,6 +5,7 @@
#include "nsISupports.idl"
#include "nsILoadInfo.idl"
#include "nsIReplacedHttpResponse.idl"
#include "nsIRequest.idl"
#include "nsITRRSkipReason.idl"
@ -84,7 +85,7 @@ interface nsIHttpChannelInternal : nsISupports
[must_use]
void getResponseVersion(out unsigned long major, out unsigned long minor);
/*
/**
* Retrieves all security messages from the security message queue
* and empties the queue after retrieval
*/
@ -425,7 +426,7 @@ interface nsIHttpChannelInternal : nsISupports
[noscript, infallible]
attribute unsigned long lastRedirectFlags;
// This is use to determine the duration since navigation started.
// This is used to determine the duration since navigation started.
[noscript] attribute TimeStamp navigationStartTimeStamp;
/**
@ -485,7 +486,7 @@ interface nsIHttpChannelInternal : nsISupports
*/
[must_use] void setEarlyHintObserver(in nsIEarlyHintObserver aObserver);
/*
/**
* id of the EarlyHintPreloader to connect back from PreloadService to
* EarlyHintPreloader.
*/
@ -493,12 +494,12 @@ interface nsIHttpChannelInternal : nsISupports
[notxpcom, nostdcall] void setConnectionInfo(in nsHttpConnectionInfo aInfo);
/*
* This attribute indicates if the channel was loaded via Proxy.
*/
/**
* This attribute indicates if the channel was loaded via Proxy.
*/
[must_use] readonly attribute boolean isProxyUsed;
/**
/**
* Set mWebTransportSessionEventListener.
*/
[must_use] void setWebTransportSessionEventListener(
@ -518,4 +519,14 @@ interface nsIHttpChannelInternal : nsISupports
* overridden header value.
*/
[must_use] attribute boolean isUserAgentHeaderModified;
/**
* The nsIReplacedHttpResponse will be used to override the response of the
* channel. Should be called before connect (http-on-before-connect).
* When setResponseOverride is called, the request will bypass the network
* and a mocked response based on the nsIReplacedHttpResponse content will
* be used instead.
*/
[must_use] void setResponseOverride(
in nsIReplacedHttpResponse aReplacedHttpResponse);
};

View File

@ -192,6 +192,8 @@ skip-if = ["socketprocess_networking"] # Bug 1772209
["browser_resource_navigation.js"]
["browser_set_response_override.js"]
["browser_speculative_connection_link_header.js"]
["browser_test_data_channel_observer.js"]

View File

@ -0,0 +1,135 @@
/* 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/. */
"use strict";
// Test for the nsIHttpChannelInternal helper setResponseOverride.
// which allows to bypass the network for a request before connect, and
// reply with a mocked response instead.
add_task(async function test_set_response_override() {
let observer = {
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
observe(aSubject, aTopic) {
aSubject = aSubject.QueryInterface(Ci.nsIHttpChannelInternal);
if (
aTopic == "http-on-before-connect" &&
aSubject.URI.spec ==
"https://example.com/browser/netwerk/test/browser/dummy.html"
) {
const replacedHttpResponse = Cc[
"@mozilla.org/network/replaced-http-response;1"
].createInstance(Ci.nsIReplacedHttpResponse);
replacedHttpResponse.responseStatus = 200;
replacedHttpResponse.responseStatusText = "Och Aye";
replacedHttpResponse.responseBody =
"<div id=from-response-override>From setResponseOverride";
replacedHttpResponse.setResponseHeader(
"some-header",
"some-value",
false
);
replacedHttpResponse.setResponseHeader(
"Set-Cookie",
"foo=bar;Path=/",
false
);
aSubject.setResponseOverride(replacedHttpResponse);
}
},
};
Services.obs.addObserver(observer, "http-on-before-connect");
const onTabLoaded = BrowserTestUtils.withNewTab(
{
gBrowser,
url: "https://example.com/browser/netwerk/test/browser/dummy.html",
waitForLoad: true,
},
async function (browser) {
await ContentTask.spawn(browser, [], async function () {
Assert.ok(
!!content.document.getElementById("from-response-override"),
"Page was loaded using the response override"
);
Assert.equal(
content.document.cookie,
"foo=bar",
"Cookie was set from the response override headers"
);
// Perform another request to the same URL to check status and headers override.
const response = await content.fetch(
"https://example.com/browser/netwerk/test/browser/dummy.html"
);
Assert.equal(
response.status,
200,
"Status was set from the response override"
);
Assert.equal(
response.statusText,
"Och Aye",
"Status text was set from the response override"
);
Assert.equal(
response.headers.get("some-header"),
"some-value",
"same-header header was set from the response override"
);
});
}
);
await onTabLoaded;
Services.obs.removeObserver(observer, "http-on-before-connect");
});
// Test that a response override with 302 Found status + Location header
// redirects to the URL specified in the Location header.
add_task(async function test_set_response_override_redirects() {
let observer = {
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
observe(aSubject, aTopic) {
aSubject = aSubject.QueryInterface(Ci.nsIHttpChannelInternal);
if (
aTopic == "http-on-before-connect" &&
aSubject.URI.spec ==
"https://example.com/browser/netwerk/test/browser/dummy.html"
) {
const replacedHttpResponse = Cc[
"@mozilla.org/network/replaced-http-response;1"
].createInstance(Ci.nsIReplacedHttpResponse);
replacedHttpResponse.responseStatus = 302;
replacedHttpResponse.responseStatusText = "Found";
replacedHttpResponse.setResponseHeader(
"Location",
"https://example.com/browser/netwerk/test/browser/dummy.html?redirected=true",
false
);
aSubject.setResponseOverride(replacedHttpResponse);
}
},
};
Services.obs.addObserver(observer, "http-on-before-connect");
const onTabLoaded = BrowserTestUtils.withNewTab(
{
gBrowser,
url: "https://example.com/browser/netwerk/test/browser/dummy.html",
waitForLoad: true,
},
async function (browser) {
await ContentTask.spawn(browser, [], async function () {
Assert.equal(
content.location.href,
"https://example.com/browser/netwerk/test/browser/dummy.html?redirected=true",
"Navigation was redirected based on the overridden response"
);
});
}
);
await onTabLoaded;
Services.obs.removeObserver(observer, "http-on-before-connect");
});