mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-30 16:22:00 +00:00
Bug 1765289
- Early Hints: Parse additional header fields for content-policy-security r=necko-reviewers,kershaw,ckerschb,asuth
We capture the CSP header if present and gate the preload on the application of the policy. A temporary loadInfo, csp, and clientInfo are created as no document yet exists. Differential Revision: https://phabricator.services.mozilla.com/D162138
This commit is contained in:
parent
64c5787d70
commit
11e3473f12
@ -2920,11 +2920,12 @@ NS_IMETHODIMP DocumentLoadListener::OnStatus(nsIRequest* aRequest,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP DocumentLoadListener::EarlyHint(
|
||||
const nsACString& aLinkHeader, const nsACString& aReferrerPolicy) {
|
||||
NS_IMETHODIMP DocumentLoadListener::EarlyHint(const nsACString& aLinkHeader,
|
||||
const nsACString& aReferrerPolicy,
|
||||
const nsACString& aCSPHeader) {
|
||||
LOG(("DocumentLoadListener::EarlyHint.\n"));
|
||||
mEarlyHintsService.EarlyHint(aLinkHeader, GetChannelCreationURI(), mChannel,
|
||||
aReferrerPolicy);
|
||||
aReferrerPolicy, aCSPHeader);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -12,8 +12,11 @@
|
||||
#include "NeckoCommon.h"
|
||||
#include "mozilla/CORSMode.h"
|
||||
#include "mozilla/dom/Element.h"
|
||||
#include "mozilla/dom/nsCSPContext.h"
|
||||
#include "mozilla/dom/ReferrerInfo.h"
|
||||
#include "mozilla/glean/GleanMetrics.h"
|
||||
#include "mozilla/ipc/BackgroundUtils.h"
|
||||
#include "mozilla/LoadInfo.h"
|
||||
#include "mozilla/Logging.h"
|
||||
#include "mozilla/net/EarlyHintRegistrar.h"
|
||||
#include "mozilla/net/NeckoChannelParams.h"
|
||||
@ -22,12 +25,14 @@
|
||||
#include "nsAttrValue.h"
|
||||
#include "nsAttrValueInlines.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsContentPolicyUtils.h"
|
||||
#include "nsContentSecurityManager.h"
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsDebug.h"
|
||||
#include "nsHttpChannel.h"
|
||||
#include "nsIAsyncVerifyRedirectCallback.h"
|
||||
#include "nsIChannel.h"
|
||||
#include "nsIContentSecurityPolicy.h"
|
||||
#include "nsIHttpChannel.h"
|
||||
#include "nsIInputStream.h"
|
||||
#include "nsILoadInfo.h"
|
||||
@ -188,7 +193,7 @@ void EarlyHintPreloader::MaybeCreateAndInsertPreload(
|
||||
OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aLinkHeader,
|
||||
nsIURI* aBaseURI, nsIPrincipal* aPrincipal,
|
||||
nsICookieJarSettings* aCookieJarSettings,
|
||||
const nsACString& aResponseReferrerPolicy) {
|
||||
const nsACString& aResponseReferrerPolicy, const nsACString& aCSPHeader) {
|
||||
nsAttrValue as;
|
||||
ParseAsValue(aLinkHeader.mAs, as);
|
||||
|
||||
@ -270,6 +275,73 @@ void EarlyHintPreloader::MaybeCreateAndInsertPreload(
|
||||
corsMode, static_cast<ASDestination>(as.GetEnumValue()),
|
||||
aLinkHeader.mType.LowerCaseEqualsASCII("module"));
|
||||
|
||||
// Verify that the resource should be loaded.
|
||||
// This isn't the ideal way to test the resource against the CSP.
|
||||
// The problem comes from the fact that at the stage of Early Hint
|
||||
// processing we have not yet created a document where we would normally store
|
||||
// the CSP.
|
||||
|
||||
// First we will create a load info,
|
||||
// nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK
|
||||
nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new LoadInfo(
|
||||
aPrincipal, // loading principal
|
||||
aPrincipal, // triggering principal
|
||||
nullptr /* aLoadingContext node */,
|
||||
nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, contentPolicyType);
|
||||
|
||||
if (aCSPHeader.Length() != 0) {
|
||||
// If the CSP header is present then create a new CSP and apply the header
|
||||
// directives to it
|
||||
nsCOMPtr<nsIContentSecurityPolicy> csp = new nsCSPContext();
|
||||
nsresult rv = csp->SetRequestContextWithPrincipal(
|
||||
aPrincipal, aBaseURI, u""_ns, 0 /* aInnerWindowId */);
|
||||
NS_ENSURE_SUCCESS_VOID(rv);
|
||||
rv = CSP_AppendCSPFromHeader(csp, NS_ConvertUTF8toUTF16(aCSPHeader),
|
||||
false /* report only */);
|
||||
NS_ENSURE_SUCCESS_VOID(rv);
|
||||
|
||||
// We create a temporary ClientInfo. This is required on the loadInfo as
|
||||
// that is how the CSP is queried. More specificially, as a hack to be able
|
||||
// to call NS_CheckContentLoadPolicy on nsILoadInfo which exclusively
|
||||
// accesses the CSP from the ClientInfo, we create a synthetic ClientInfo to
|
||||
// hold the CSP we are creating. This is not a safe thing to do in any other
|
||||
// circumstance because ClientInfos are always describing a ClientSource
|
||||
// that corresponds to a global or potential global, so creating an info
|
||||
// without a source is unsound. For the purposes of doing things before a
|
||||
// global exists, fetch has the concept of a
|
||||
// https://fetch.spec.whatwg.org/#concept-request-reserved-client and
|
||||
// nsILoadInfo explicity has methods around GiveReservedClientSource which
|
||||
// are primarily used by ClientChannelHelper. If you are trying to do real
|
||||
// CSP stuff and the ClientInfo is not there yet, please enhance the logic
|
||||
// around ClientChannelHelper.
|
||||
|
||||
mozilla::ipc::PrincipalInfo principalInfo;
|
||||
rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
|
||||
NS_ENSURE_SUCCESS_VOID(rv);
|
||||
dom::ClientInfo clientInfo(nsID::GenerateUUID(), dom::ClientType::Window,
|
||||
principalInfo, TimeStamp::Now());
|
||||
|
||||
// Our newly-created CSP is set on the ClientInfo via the indirect route of
|
||||
// first serializing to CSPInfo
|
||||
ipc::CSPInfo cspInfo;
|
||||
rv = CSPToCSPInfo(csp, &cspInfo);
|
||||
NS_ENSURE_SUCCESS_VOID(rv);
|
||||
clientInfo.SetCspInfo(cspInfo);
|
||||
|
||||
// This ClientInfo is then set on the new loadInfo.
|
||||
// It can now be used to test the resource against the policy
|
||||
secCheckLoadInfo->SetClientInfo(clientInfo);
|
||||
}
|
||||
|
||||
int16_t shouldLoad = nsIContentPolicy::ACCEPT;
|
||||
nsresult rv =
|
||||
NS_CheckContentLoadPolicy(uri, secCheckLoadInfo, ""_ns, &shouldLoad,
|
||||
nsContentUtils::GetContentPolicy());
|
||||
|
||||
if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) {
|
||||
return;
|
||||
}
|
||||
|
||||
NS_ENSURE_SUCCESS_VOID(earlyHintPreloader->OpenChannel(
|
||||
uri, aPrincipal, securityFlags, contentPolicyType, referrerInfo,
|
||||
aCookieJarSettings));
|
||||
@ -291,6 +363,7 @@ nsresult EarlyHintPreloader::OpenChannel(
|
||||
aContentPolicyType == nsContentPolicyType::TYPE_SCRIPT ||
|
||||
aContentPolicyType == nsContentPolicyType::TYPE_STYLESHEET ||
|
||||
aContentPolicyType == nsContentPolicyType::TYPE_FONT);
|
||||
|
||||
nsresult rv =
|
||||
NS_NewChannel(getter_AddRefs(mChannel), aURI, aPrincipal, aSecurityFlags,
|
||||
aContentPolicyType, aCookieJarSettings,
|
||||
|
@ -81,7 +81,7 @@ class EarlyHintPreloader final : public nsIStreamListener,
|
||||
OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aHeader,
|
||||
nsIURI* aBaseURI, nsIPrincipal* aPrincipal,
|
||||
nsICookieJarSettings* aCookieJarSettings,
|
||||
const nsACString& aReferrerPolicy);
|
||||
const nsACString& aReferrerPolicy, const nsACString& aCSPHeader);
|
||||
|
||||
// register Channel to EarlyHintRegistrar returns connect arguments
|
||||
EarlyHintConnectArgs Register();
|
||||
|
@ -31,7 +31,8 @@ EarlyHintsService::~EarlyHintsService() = default;
|
||||
|
||||
void EarlyHintsService::EarlyHint(const nsACString& aLinkHeader,
|
||||
nsIURI* aBaseURI, nsIChannel* aChannel,
|
||||
const nsACString& aReferrerPolicy) {
|
||||
const nsACString& aReferrerPolicy,
|
||||
const nsACString& aCSPHeader) {
|
||||
mEarlyHintsCount++;
|
||||
if (mFirstEarlyHint.isNothing()) {
|
||||
mFirstEarlyHint.emplace(TimeStamp::NowLoRes());
|
||||
@ -91,7 +92,7 @@ void EarlyHintsService::EarlyHint(const nsACString& aLinkHeader,
|
||||
} else if (linkHeader.mRel.LowerCaseEqualsLiteral("preload")) {
|
||||
EarlyHintPreloader::MaybeCreateAndInsertPreload(
|
||||
mOngoingEarlyHints, linkHeader, aBaseURI, principal,
|
||||
cookieJarSettings, aReferrerPolicy);
|
||||
cookieJarSettings, aReferrerPolicy, aCSPHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,8 @@ class EarlyHintsService {
|
||||
EarlyHintsService();
|
||||
~EarlyHintsService();
|
||||
void EarlyHint(const nsACString& aLinkHeader, nsIURI* aBaseURI,
|
||||
nsIChannel* aChannel, const nsACString& aReferrerPolicy);
|
||||
nsIChannel* aChannel, const nsACString& aReferrerPolicy,
|
||||
const nsACString& aCSPHeader);
|
||||
void FinalResponse(uint32_t aResponseStatus);
|
||||
void Cancel(const nsACString& aReason);
|
||||
|
||||
|
@ -629,11 +629,12 @@ HttpTransactionChild::CheckListenerChain() {
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
HttpTransactionChild::EarlyHint(const nsACString& value,
|
||||
const nsACString& referrerPolicy) {
|
||||
HttpTransactionChild::EarlyHint(const nsACString& aValue,
|
||||
const nsACString& aReferrerPolicy,
|
||||
const nsACString& aCSPHeader) {
|
||||
LOG(("HttpTransactionChild::EarlyHint"));
|
||||
if (CanSend()) {
|
||||
Unused << SendEarlyHint(value, referrerPolicy);
|
||||
Unused << SendEarlyHint(aValue, aReferrerPolicy, aCSPHeader);
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -668,13 +668,17 @@ mozilla::ipc::IPCResult HttpTransactionParent::RecvOnH2PushStream(
|
||||
} // namespace net
|
||||
|
||||
mozilla::ipc::IPCResult HttpTransactionParent::RecvEarlyHint(
|
||||
const nsCString& aValue, const nsACString& aReferrerPolicy) {
|
||||
LOG(("HttpTransactionParent::RecvEarlyHint header=%s aReferrerPolicy=%s",
|
||||
const nsCString& aValue, const nsACString& aReferrerPolicy,
|
||||
const nsACString& aCSPHeader) {
|
||||
LOG(
|
||||
("HttpTransactionParent::RecvEarlyHint header=%s aReferrerPolicy=%s "
|
||||
"aCSPHeader=%s",
|
||||
PromiseFlatCString(aValue).get(),
|
||||
PromiseFlatCString(aReferrerPolicy).get()));
|
||||
PromiseFlatCString(aReferrerPolicy).get(),
|
||||
PromiseFlatCString(aCSPHeader).get()));
|
||||
nsCOMPtr<nsIEarlyHintObserver> obs = do_QueryInterface(mChannel);
|
||||
if (obs) {
|
||||
Unused << obs->EarlyHint(aValue, aReferrerPolicy);
|
||||
Unused << obs->EarlyHint(aValue, aReferrerPolicy, aCSPHeader);
|
||||
}
|
||||
|
||||
return IPC_OK();
|
||||
|
@ -75,7 +75,8 @@ class HttpTransactionParent final : public PHttpTransactionParent,
|
||||
const nsCString& aResourceUrl,
|
||||
const nsCString& aRequestString);
|
||||
mozilla::ipc::IPCResult RecvEarlyHint(const nsCString& aValue,
|
||||
const nsACString& aReferrerPolicy);
|
||||
const nsACString& aReferrerPolicy,
|
||||
const nsACString& aCSPHeader);
|
||||
|
||||
virtual mozilla::TimeStamp GetPendingTime() override;
|
||||
|
||||
|
@ -73,7 +73,7 @@ parent:
|
||||
async OnH2PushStream(uint32_t pushedStreamId,
|
||||
nsCString resourceUrl,
|
||||
nsCString requestString);
|
||||
async EarlyHint(nsCString linkHeader, nsCString referrerPolicy);
|
||||
async EarlyHint(nsCString linkHeader, nsCString referrerPolicy, nsCString cspHeader);
|
||||
|
||||
child:
|
||||
async __delete__();
|
||||
|
@ -38,6 +38,7 @@ HTTP_ATOM(Content_Length, "Content-Length")
|
||||
HTTP_ATOM(Content_Location, "Content-Location")
|
||||
HTTP_ATOM(Content_MD5, "Content-MD5")
|
||||
HTTP_ATOM(Content_Range, "Content-Range")
|
||||
HTTP_ATOM(Content_Security_Policy, "Content-Security-Policy")
|
||||
HTTP_ATOM(Content_Type, "Content-Type")
|
||||
HTTP_ATOM(Cookie, "Cookie")
|
||||
HTTP_ATOM(Cross_Origin_Embedder_Policy, "Cross-Origin-Embedder-Policy")
|
||||
|
@ -9865,13 +9865,14 @@ nsHttpChannel::SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) {
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsHttpChannel::EarlyHint(const nsACString& linkHeader,
|
||||
const nsACString& referrerPolicy) {
|
||||
nsHttpChannel::EarlyHint(const nsACString& aLinkHeader,
|
||||
const nsACString& aReferrerPolicy,
|
||||
const nsACString& aCspHeader) {
|
||||
LOG(("nsHttpChannel::EarlyHint.\n"));
|
||||
|
||||
if (mEarlyHintObserver && nsContentUtils::ComputeIsSecureContext(this)) {
|
||||
LOG(("nsHttpChannel::EarlyHint propagated.\n"));
|
||||
mEarlyHintObserver->EarlyHint(linkHeader, referrerPolicy);
|
||||
mEarlyHintObserver->EarlyHint(aLinkHeader, aReferrerPolicy, aCspHeader);
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -1974,6 +1974,10 @@ nsresult nsHttpTransaction::ParseLineSegment(char* segment, uint32_t len) {
|
||||
referrerPolicy);
|
||||
|
||||
if (NS_SUCCEEDED(rv) && !linkHeader.IsEmpty()) {
|
||||
nsCString cspHeader;
|
||||
Unused << mResponseHead->GetHeader(nsHttp::Content_Security_Policy,
|
||||
cspHeader);
|
||||
|
||||
nsCOMPtr<nsIEarlyHintObserver> earlyHint;
|
||||
{
|
||||
MutexAutoLock lock(mLock);
|
||||
@ -1984,8 +1988,9 @@ nsresult nsHttpTransaction::ParseLineSegment(char* segment, uint32_t len) {
|
||||
NS_NewRunnableFunction(
|
||||
"nsIEarlyHintObserver->EarlyHint",
|
||||
[obs{std::move(earlyHint)}, header{std::move(linkHeader)},
|
||||
referrerPolicy{std::move(referrerPolicy)}]() {
|
||||
obs->EarlyHint(header, referrerPolicy);
|
||||
referrerPolicy{std::move(referrerPolicy)},
|
||||
cspHeader{std::move(cspHeader)}]() {
|
||||
obs->EarlyHint(header, referrerPolicy, cspHeader);
|
||||
}),
|
||||
NS_DISPATCH_NORMAL);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
|
@ -5,8 +5,6 @@
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
native ReferrerPolicy(mozilla::dom::ReferrerPolicy);
|
||||
|
||||
[scriptable, uuid(97b6be6f-283c-45dd-81a7-4bb2d87d42f8)]
|
||||
interface nsIEarlyHintObserver : nsISupports
|
||||
{
|
||||
@ -14,5 +12,5 @@ interface nsIEarlyHintObserver : nsISupports
|
||||
* This method is called when the transaction has early hint (i.e. the
|
||||
* '103 Early Hint' informational response) headers.
|
||||
*/
|
||||
void earlyHint(in ACString linkHeader, in ACString referrerPolicy);
|
||||
void earlyHint(in ACString linkHeader, in ACString referrerPolicy, in ACString cspHeader);
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ support-files =
|
||||
early_hint_error.sjs
|
||||
early_hint_asset.sjs
|
||||
early_hint_asset_html.sjs
|
||||
early_hint_csp_options_html.sjs
|
||||
early_hint_preconnect_html.sjs
|
||||
post.html
|
||||
res.css
|
||||
@ -93,10 +94,6 @@ support-files =
|
||||
file_lnk.lnk
|
||||
[browser_post_auth.js]
|
||||
skip-if = socketprocess_networking # Bug 1772209
|
||||
[browser_103_csp.js]
|
||||
support-files =
|
||||
early_hint_preload_test_helper.jsm
|
||||
skip-if = true # Bug 1765289 superseds this test
|
||||
[browser_backgroundtask_purgeHTTPCache.js]
|
||||
skip-if =
|
||||
os == "android" # MultiInstanceLock doesn't work on Android
|
||||
@ -133,4 +130,13 @@ support-files =
|
||||
support-files =
|
||||
early_hint_referrer_policy_html.sjs
|
||||
early_hint_preload_test_helper.jsm
|
||||
[browser_103_csp.js]
|
||||
support-files =
|
||||
early_hint_preload_test_helper.jsm
|
||||
[browser_103_csp_images.js]
|
||||
support-files =
|
||||
early_hint_preload_test_helper.jsm
|
||||
[browser_103_csp_styles.js]
|
||||
support-files =
|
||||
early_hint_preload_test_helper.jsm
|
||||
[browser_103_preconnect.js]
|
||||
|
@ -4,53 +4,83 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const { request_count_checking } = ChromeUtils.import(
|
||||
Services.prefs.setBoolPref("network.early-hints.enabled", true);
|
||||
|
||||
const { test_preload_hint_and_request } = ChromeUtils.import(
|
||||
"resource://testing-common/early_hint_preload_test_helper.jsm"
|
||||
);
|
||||
|
||||
// csp header with "img-src: 'none'" only on main html response, don't show the image on the page
|
||||
add_task(async function test_preload_csp_imgsrc_none() {
|
||||
// reset the count
|
||||
let headers = new Headers();
|
||||
headers.append("X-Early-Hint-Count-Start", "");
|
||||
await fetch(
|
||||
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs",
|
||||
{ headers }
|
||||
);
|
||||
|
||||
let requestUrl =
|
||||
"https://example.com/browser/netwerk/test/browser/103_preload_csp_imgsrc_none.html";
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
add_task(async function test_preload_images_csp_in_early_hints_response() {
|
||||
let tests = [
|
||||
{
|
||||
gBrowser,
|
||||
url: requestUrl,
|
||||
waitForLoad: true,
|
||||
input: {
|
||||
test_name: "image - no csp",
|
||||
resource_type: "image",
|
||||
csp: "",
|
||||
csp_in_early_hint: "",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
},
|
||||
async function(browser) {
|
||||
let noImgLoaded = await SpecialPowers.spawn(browser, [], function() {
|
||||
let loadInfo = content.performance.getEntriesByName(
|
||||
"https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs?1ac2a5e1-90c7-4171-b0f0-676f7d899af3"
|
||||
);
|
||||
return loadInfo.every(entry => entry.decodedBodySize === 0);
|
||||
});
|
||||
await Assert.ok(
|
||||
noImgLoaded,
|
||||
"test_preload_csp_imgsrc_none: Image dislpayed unexpectedly"
|
||||
);
|
||||
}
|
||||
);
|
||||
{
|
||||
input: {
|
||||
test_name: "image img-src 'self';",
|
||||
resource_type: "image",
|
||||
csp: "",
|
||||
csp_in_early_hint: "img-src 'self';",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image img-src 'self'; same host provided",
|
||||
resource_type: "image",
|
||||
csp: "",
|
||||
csp_in_early_hint: "img-src 'self';",
|
||||
host: "https://example.com/browser/netwerk/test/browser/",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image img-src 'self'; other host provided",
|
||||
resource_type: "image",
|
||||
csp: "",
|
||||
csp_in_early_hint: "img-src 'self';",
|
||||
host: "https://example.org/browser/netwerk/test/browser/",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 0, normal: 1 },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image img-src 'none';",
|
||||
resource_type: "image",
|
||||
csp: "",
|
||||
csp_in_early_hint: "img-src 'none';",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 0, normal: 1 },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image img-src 'none'; same host provided",
|
||||
resource_type: "image",
|
||||
csp: "",
|
||||
csp_in_early_hint: "img-src 'none';",
|
||||
host: "https://example.com/browser/netwerk/test/browser/",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 0, normal: 1 },
|
||||
},
|
||||
];
|
||||
|
||||
let gotRequestCount = await fetch(
|
||||
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs"
|
||||
).then(response => response.json());
|
||||
let expectedRequestCount = { hinted: 1, normal: 0 };
|
||||
|
||||
await request_count_checking(
|
||||
"test_preload_csp_imgsrc_none",
|
||||
gotRequestCount,
|
||||
expectedRequestCount
|
||||
);
|
||||
|
||||
Services.cache2.clear();
|
||||
for (let test of tests) {
|
||||
await test_preload_hint_and_request(test.input, test.expected);
|
||||
}
|
||||
});
|
||||
|
170
netwerk/test/browser/browser_103_csp_images.js
Normal file
170
netwerk/test/browser/browser_103_csp_images.js
Normal file
@ -0,0 +1,170 @@
|
||||
/* 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";
|
||||
|
||||
Services.prefs.setBoolPref("network.early-hints.enabled", true);
|
||||
|
||||
// This verifies hints, requests server-side and client-side that the image actually loaded
|
||||
async function test_image_preload_hint_request_loaded(
|
||||
input,
|
||||
expected_results,
|
||||
image_should_load
|
||||
) {
|
||||
// reset the count
|
||||
let headers = new Headers();
|
||||
headers.append("X-Early-Hint-Count-Start", "");
|
||||
await fetch(
|
||||
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs",
|
||||
{ headers }
|
||||
);
|
||||
|
||||
let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_csp_options_html.sjs?as=${
|
||||
input.resource_type
|
||||
}&hinted=${input.hinted ? "1" : "0"}${input.csp ? "&csp=" + input.csp : ""}${
|
||||
input.csp_in_early_hint
|
||||
? "&csp_in_early_hint=" + input.csp_in_early_hint
|
||||
: ""
|
||||
}${input.host ? "&host=" + input.host : ""}`;
|
||||
|
||||
console.log("requestUrl: " + requestUrl);
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: requestUrl,
|
||||
waitForLoad: true,
|
||||
},
|
||||
async function(browser) {
|
||||
let imageLoaded = await ContentTask.spawn(browser, [], function() {
|
||||
let image = content.document.getElementById("test_image");
|
||||
return image && image.complete && image.naturalHeight !== 0;
|
||||
});
|
||||
await Assert.ok(
|
||||
image_should_load == imageLoaded,
|
||||
"test_image_preload_hint_request_loaded: the image can be loaded as expected " +
|
||||
requestUrl
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
let gotRequestCount = await fetch(
|
||||
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs"
|
||||
).then(response => response.json());
|
||||
|
||||
await Assert.deepEqual(gotRequestCount, expected_results, input.test_name);
|
||||
|
||||
Services.cache2.clear();
|
||||
}
|
||||
|
||||
// These tests verify whether or not the image actually loaded in the document
|
||||
add_task(async function test_images_loaded_with_csp() {
|
||||
let tests = [
|
||||
{
|
||||
input: {
|
||||
test_name: "image loaded - no csp",
|
||||
resource_type: "image",
|
||||
csp: "",
|
||||
csp_in_early_hint: "",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
image_should_load: true,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image loaded - img-src none",
|
||||
resource_type: "image",
|
||||
csp: "img-src 'none';",
|
||||
csp_in_early_hint: "",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
image_should_load: false,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image loaded - img-src none in EH response",
|
||||
resource_type: "image",
|
||||
csp: "",
|
||||
csp_in_early_hint: "img-src 'none';",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 0, normal: 1 },
|
||||
image_should_load: true,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image loaded - img-src none in both headers",
|
||||
resource_type: "image",
|
||||
csp: "img-src 'none';",
|
||||
csp_in_early_hint: "img-src 'none';",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 0, normal: 0 },
|
||||
image_should_load: false,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image loaded - img-src self",
|
||||
resource_type: "image",
|
||||
csp: "img-src 'self';",
|
||||
csp_in_early_hint: "",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
image_should_load: true,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image loaded - img-src self in EH response",
|
||||
resource_type: "image",
|
||||
csp: "",
|
||||
csp_in_early_hint: "img-src 'self';",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
image_should_load: true,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "image loaded - conflicting csp, early hint skipped",
|
||||
resource_type: "image",
|
||||
csp: "img-src 'self';",
|
||||
csp_in_early_hint: "img-src 'none';",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 0, normal: 1 },
|
||||
image_should_load: true,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name:
|
||||
"image loaded - conflicting csp, resource not loaded in document",
|
||||
resource_type: "image",
|
||||
csp: "img-src 'none';",
|
||||
csp_in_early_hint: "img-src 'self';",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
image_should_load: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (let test of tests) {
|
||||
await test_image_preload_hint_request_loaded(
|
||||
test.input,
|
||||
test.expected,
|
||||
test.image_should_load
|
||||
);
|
||||
}
|
||||
});
|
86
netwerk/test/browser/browser_103_csp_styles.js
Normal file
86
netwerk/test/browser/browser_103_csp_styles.js
Normal file
@ -0,0 +1,86 @@
|
||||
/* 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";
|
||||
|
||||
Services.prefs.setBoolPref("network.early-hints.enabled", true);
|
||||
|
||||
const { test_preload_hint_and_request } = ChromeUtils.import(
|
||||
"resource://testing-common/early_hint_preload_test_helper.jsm"
|
||||
);
|
||||
|
||||
add_task(async function test_preload_styles_csp_in_response() {
|
||||
let tests = [
|
||||
{
|
||||
input: {
|
||||
test_name: "style - no csp",
|
||||
resource_type: "style",
|
||||
csp: "",
|
||||
csp_in_early_hint: "",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "style style-src 'self';",
|
||||
resource_type: "style",
|
||||
csp: "",
|
||||
csp_in_early_hint: "style-src 'self';",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "style style-src self; same host provided",
|
||||
resource_type: "style",
|
||||
csp: "",
|
||||
csp_in_early_hint: "style-src 'self';",
|
||||
host: "https://example.com/browser/netwerk/test/browser/",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 1, normal: 0 },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "style style-src 'self'; other host provided",
|
||||
resource_type: "style",
|
||||
csp: "",
|
||||
csp_in_early_hint: "style-src 'self';",
|
||||
host: "https://example.org/browser/netwerk/test/browser/",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 0, normal: 1 },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "style style-src 'none';",
|
||||
resource_type: "style",
|
||||
csp: "",
|
||||
csp_in_early_hint: "style-src 'none';",
|
||||
host: "",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 0, normal: 1 },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
test_name: "style style-src 'none'; other host provided",
|
||||
resource_type: "style",
|
||||
csp: "",
|
||||
csp_in_early_hint: "style-src 'none';",
|
||||
host: "https://example.org/browser/netwerk/test/browser/",
|
||||
hinted: true,
|
||||
},
|
||||
expected: { hinted: 0, normal: 1 },
|
||||
},
|
||||
];
|
||||
|
||||
for (let test of tests) {
|
||||
await test_preload_hint_and_request(test.input, test.expected);
|
||||
}
|
||||
});
|
121
netwerk/test/browser/early_hint_csp_options_html.sjs
Normal file
121
netwerk/test/browser/early_hint_csp_options_html.sjs
Normal file
@ -0,0 +1,121 @@
|
||||
"use strict";
|
||||
|
||||
function handleRequest(request, response) {
|
||||
Cu.importGlobalProperties(["URLSearchParams"]);
|
||||
let qs = new URLSearchParams(request.queryString);
|
||||
let asset = qs.get("as");
|
||||
let hinted = qs.get("hinted") !== "0";
|
||||
let httpCode = qs.get("code");
|
||||
let csp = qs.get("csp");
|
||||
let csp_in_early_hint = qs.get("csp_in_early_hint");
|
||||
let host = qs.get("host");
|
||||
|
||||
// eslint-disable-next-line mozilla/use-services
|
||||
let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
|
||||
Ci.nsIUUIDGenerator
|
||||
);
|
||||
let uuid = uuidGenerator.generateUUID().toString();
|
||||
let url = `early_hint_pixel.sjs?as=${asset}&uuid=${uuid}`;
|
||||
if (host) {
|
||||
url = host + url;
|
||||
}
|
||||
|
||||
// write to raw socket
|
||||
response.seizePower();
|
||||
|
||||
if (hinted) {
|
||||
response.write("HTTP/1.1 103 Early Hint\r\n");
|
||||
if (csp_in_early_hint) {
|
||||
response.write(
|
||||
`Content-Security-Policy: ${csp_in_early_hint.replaceAll('"', "")}\r\n`
|
||||
);
|
||||
}
|
||||
response.write(`Link: <${url}>; rel=preload; as=${asset}\r\n`);
|
||||
response.write("\r\n");
|
||||
}
|
||||
|
||||
let body = "";
|
||||
if (asset === "image") {
|
||||
body = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<img id="test_image" src="${url}" width="100px">
|
||||
</body>
|
||||
</html>`;
|
||||
} else if (asset === "style") {
|
||||
body = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="${url}">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test preload css<h1>
|
||||
<div id="square" style="width:100px;height:100px;">
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (asset === "script") {
|
||||
body = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="${url}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test preload javascript<h1>
|
||||
<div id="square" style="width:100px;height:100px;">
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (asset === "fetch") {
|
||||
body = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body onload="onLoad()">
|
||||
<script>
|
||||
function onLoad() {
|
||||
fetch("${url}")
|
||||
.then(r => r.text())
|
||||
.then(r => document.getElementsByTagName("h2")[0].textContent = r);
|
||||
}
|
||||
</script>
|
||||
<h1>Test preload fetch</h1>
|
||||
<h2>Fetching...</h2>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (asset === "font") {
|
||||
body = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "preloadFont";
|
||||
src: url("${url}") format("woff");
|
||||
}
|
||||
body {
|
||||
font-family: "preloadFont";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test preload font<h1>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!httpCode) {
|
||||
response.write(`HTTP/1.1 200 OK\r\n`);
|
||||
} else {
|
||||
response.write(`HTTP/1.1 ${httpCode} Error\r\n`);
|
||||
}
|
||||
response.write("Content-Type: text/html;charset=utf-8\r\n");
|
||||
response.write("Cache-Control: no-cache\r\n");
|
||||
response.write(`Content-Length: ${body.length}\r\n`);
|
||||
if (csp) {
|
||||
response.write(`Content-Security-Policy: ${csp.replaceAll('"', "")}\r\n`);
|
||||
}
|
||||
response.write("\r\n");
|
||||
response.write(body);
|
||||
|
||||
response.finish();
|
||||
}
|
@ -8,6 +8,7 @@ const EXPORTED_SYMBOLS = [
|
||||
"request_count_checking",
|
||||
"test_hint_preload",
|
||||
"test_hint_preload_internal",
|
||||
"test_preload_hint_and_request",
|
||||
];
|
||||
|
||||
const { Assert } = ChromeUtils.importESModule(
|
||||
@ -96,3 +97,47 @@ async function test_hint_preload_internal(
|
||||
|
||||
await request_count_checking(testName, gotRequestCount, expectedRequestCount);
|
||||
}
|
||||
|
||||
// Verify that CSP policies in both the 103 response as well as the main response are respected.
|
||||
// e.g.
|
||||
// 103 Early Hint
|
||||
// Content-Security-Policy: style-src: self;
|
||||
// Link: </style.css>; rel=preload; as=style
|
||||
// 200 OK
|
||||
// Content-Security-Policy: style-src: none;
|
||||
// Link: </font.ttf>; rel=preload; as=font
|
||||
|
||||
// Server-side we verify that:
|
||||
// - the hinted preload request was made as expected
|
||||
// - the load request request was made as expected
|
||||
// Client-side, we verify that the image was loaded or not loaded, depending on the scenario
|
||||
|
||||
// This verifies preload hints and requests
|
||||
async function test_preload_hint_and_request(input, expected_results) {
|
||||
// reset the count
|
||||
let headers = new Headers();
|
||||
headers.append("X-Early-Hint-Count-Start", "");
|
||||
await fetch(
|
||||
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs",
|
||||
{ headers }
|
||||
);
|
||||
|
||||
let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_csp_options_html.sjs?as=${
|
||||
input.resource_type
|
||||
}&hinted=${input.hinted ? "1" : "0"}${input.csp ? "&csp=" + input.csp : ""}${
|
||||
input.csp_in_early_hint
|
||||
? "&csp_in_early_hint=" + input.csp_in_early_hint
|
||||
: ""
|
||||
}${input.host ? "&host=" + input.host : ""}`;
|
||||
|
||||
await BrowserTestUtils.openNewForegroundTab(gBrowser, requestUrl, true);
|
||||
|
||||
let gotRequestCount = await fetch(
|
||||
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs"
|
||||
).then(response => response.json());
|
||||
|
||||
await Assert.deepEqual(gotRequestCount, expected_results, input.test_name);
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
Services.cache2.clear();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user