Bug 1913305 - Add test. r=mtigley
[gecko.git] / netwerk / protocol / http / EarlyHintPreloader.cpp
blobe4dbbc7e335116324783868d7c3d6edcc8c862b4
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 #include "EarlyHintPreloader.h"
7 #include "EarlyHintRegistrar.h"
8 #include "EarlyHintsService.h"
9 #include "ErrorList.h"
10 #include "HttpChannelParent.h"
11 #include "MainThreadUtils.h"
12 #include "NeckoCommon.h"
13 #include "gfxPlatform.h"
14 #include "mozilla/CORSMode.h"
15 #include "mozilla/dom/Element.h"
16 #include "mozilla/dom/nsCSPContext.h"
17 #include "mozilla/dom/nsMixedContentBlocker.h"
18 #include "mozilla/dom/ReferrerInfo.h"
19 #include "mozilla/glean/GleanMetrics.h"
20 #include "mozilla/ipc/BackgroundUtils.h"
21 #include "mozilla/LoadInfo.h"
22 #include "mozilla/Logging.h"
23 #include "mozilla/net/EarlyHintRegistrar.h"
24 #include "mozilla/net/NeckoChannelParams.h"
25 #include "mozilla/StaticPrefs_network.h"
26 #include "mozilla/Telemetry.h"
27 #include "nsAttrValue.h"
28 #include "nsCOMPtr.h"
29 #include "nsContentPolicyUtils.h"
30 #include "nsContentSecurityManager.h"
31 #include "nsContentUtils.h"
32 #include "nsDebug.h"
33 #include "nsHttpChannel.h"
34 #include "nsIAsyncVerifyRedirectCallback.h"
35 #include "nsIChannel.h"
36 #include "nsIContentSecurityPolicy.h"
37 #include "nsIHttpChannel.h"
38 #include "nsIInputStream.h"
39 #include "nsILoadContext.h"
40 #include "nsILoadInfo.h"
41 #include "nsIParentChannel.h"
42 #include "nsIReferrerInfo.h"
43 #include "nsITimer.h"
44 #include "nsIURI.h"
45 #include "nsNetUtil.h"
46 #include "nsQueryObject.h"
47 #include "ParentChannelListener.h"
48 #include "nsIChannel.h"
49 #include "nsInterfaceRequestorAgg.h"
52 // To enable logging (see mozilla/Logging.h for full details):
54 // set MOZ_LOG=EarlyHint:5
55 // set MOZ_LOG_FILE=earlyhint.log
57 // this enables LogLevel::Debug level information and places all output in
58 // the file earlyhint.log
60 static mozilla::LazyLogModule gEarlyHintLog("EarlyHint");
62 #undef LOG
63 #define LOG(args) MOZ_LOG(gEarlyHintLog, mozilla::LogLevel::Debug, args)
65 #undef LOG_ENABLED
66 #define LOG_ENABLED() MOZ_LOG_TEST(gEarlyHintLog, mozilla::LogLevel::Debug)
68 namespace mozilla::net {
70 namespace {
71 // This id uniquely identifies each early hint preloader in the
72 // EarlyHintRegistrar. Must only be accessed from main thread.
73 static uint64_t gEarlyHintPreloaderId{0};
74 } // namespace
76 //=============================================================================
77 // OngoingEarlyHints
78 //=============================================================================
80 void OngoingEarlyHints::CancelAll(const nsACString& aReason) {
81 for (auto& preloader : mPreloaders) {
82 preloader->CancelChannel(NS_ERROR_ABORT, aReason, /* aDeleteEntry */ true);
84 mPreloaders.Clear();
85 mStartedPreloads.Clear();
88 bool OngoingEarlyHints::Contains(const PreloadHashKey& aKey) {
89 return mStartedPreloads.Contains(aKey);
92 bool OngoingEarlyHints::Add(const PreloadHashKey& aKey,
93 RefPtr<EarlyHintPreloader> aPreloader) {
94 if (!mStartedPreloads.Contains(aKey)) {
95 mStartedPreloads.Insert(aKey);
96 mPreloaders.AppendElement(aPreloader);
97 return true;
99 return false;
102 void OngoingEarlyHints::RegisterLinksAndGetConnectArgs(
103 dom::ContentParentId aCpId, nsTArray<EarlyHintConnectArgs>& aOutLinks) {
104 // register all channels before returning
105 for (auto& preload : mPreloaders) {
106 EarlyHintConnectArgs args;
107 if (preload->Register(aCpId, args)) {
108 aOutLinks.AppendElement(std::move(args));
113 //=============================================================================
114 // EarlyHintPreloader
115 //=============================================================================
117 EarlyHintPreloader::EarlyHintPreloader() {
118 AssertIsOnMainThread();
119 mConnectArgs.earlyHintPreloaderId() = ++gEarlyHintPreloaderId;
122 EarlyHintPreloader::~EarlyHintPreloader() {
123 if (mTimer) {
124 mTimer->Cancel();
125 mTimer = nullptr;
127 Telemetry::Accumulate(Telemetry::EH_STATE_OF_PRELOAD_REQUEST, mState);
130 /* static */
131 Maybe<PreloadHashKey> EarlyHintPreloader::GenerateHashKey(
132 ASDestination aAs, nsIURI* aURI, nsIPrincipal* aPrincipal,
133 CORSMode aCorsMode, bool aIsModulepreload) {
134 if (aIsModulepreload) {
135 return Some(PreloadHashKey::CreateAsScript(
136 aURI, aCorsMode, JS::loader::ScriptKind::eModule));
138 if (aAs == ASDestination::DESTINATION_FONT && aCorsMode != CORS_NONE) {
139 return Some(PreloadHashKey::CreateAsFont(aURI, aCorsMode));
141 if (aAs == ASDestination::DESTINATION_IMAGE) {
142 return Some(PreloadHashKey::CreateAsImage(aURI, aPrincipal, aCorsMode));
144 if (aAs == ASDestination::DESTINATION_SCRIPT) {
145 return Some(PreloadHashKey::CreateAsScript(
146 aURI, aCorsMode, JS::loader::ScriptKind::eClassic));
148 if (aAs == ASDestination::DESTINATION_STYLE) {
149 return Some(PreloadHashKey::CreateAsStyle(
150 aURI, aPrincipal, aCorsMode,
151 css::SheetParsingMode::eAuthorSheetFeatures));
153 if (aAs == ASDestination::DESTINATION_FETCH && aCorsMode != CORS_NONE) {
154 return Some(PreloadHashKey::CreateAsFetch(aURI, aCorsMode));
156 return Nothing();
159 /* static */
160 nsSecurityFlags EarlyHintPreloader::ComputeSecurityFlags(CORSMode aCORSMode,
161 ASDestination aAs) {
162 if (aAs == ASDestination::DESTINATION_FONT) {
163 return nsContentSecurityManager::ComputeSecurityFlags(
164 CORSMode::CORS_NONE,
165 nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS);
167 if (aAs == ASDestination::DESTINATION_IMAGE) {
168 return nsContentSecurityManager::ComputeSecurityFlags(
169 aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
170 CORS_NONE_MAPS_TO_INHERITED_CONTEXT) |
171 nsILoadInfo::SEC_ALLOW_CHROME;
173 if (aAs == ASDestination::DESTINATION_SCRIPT) {
174 return nsContentSecurityManager::ComputeSecurityFlags(
175 aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
176 CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS) |
177 nsILoadInfo::SEC_ALLOW_CHROME;
179 if (aAs == ASDestination::DESTINATION_STYLE) {
180 return nsContentSecurityManager::ComputeSecurityFlags(
181 aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
182 CORS_NONE_MAPS_TO_INHERITED_CONTEXT) |
183 nsILoadInfo::SEC_ALLOW_CHROME;
186 if (aAs == ASDestination::DESTINATION_FETCH) {
187 return nsContentSecurityManager::ComputeSecurityFlags(
188 aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
189 CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS);
191 MOZ_ASSERT(false, "Unexpected ASDestination");
192 return nsContentSecurityManager::ComputeSecurityFlags(
193 CORSMode::CORS_NONE,
194 nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS);
197 // static
198 void EarlyHintPreloader::MaybeCreateAndInsertPreload(
199 OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aLinkHeader,
200 nsIURI* aBaseURI, nsIPrincipal* aPrincipal,
201 nsICookieJarSettings* aCookieJarSettings,
202 const nsACString& aResponseReferrerPolicy, const nsACString& aCSPHeader,
203 uint64_t aBrowsingContextID,
204 dom::CanonicalBrowsingContext* aLoadingBrowsingContext,
205 bool aIsModulepreload) {
206 nsAttrValue as;
207 ParseAsValue(aLinkHeader.mAs, as);
209 ASDestination destination = static_cast<ASDestination>(as.GetEnumValue());
210 CollectResourcesTypeTelemetry(destination);
212 if (!StaticPrefs::network_early_hints_enabled()) {
213 return;
216 if (destination == ASDestination::DESTINATION_INVALID && !aIsModulepreload) {
217 // return early when it's definitly not an asset type we preload
218 // would be caught later as well, e.g. when creating the PreloadHashKey
219 return;
222 if (destination == ASDestination::DESTINATION_FONT &&
223 !gfxPlatform::GetPlatform()->DownloadableFontsEnabled()) {
224 return;
227 nsCOMPtr<nsIURI> uri;
228 NS_ENSURE_SUCCESS_VOID(
229 NS_NewURI(getter_AddRefs(uri), aLinkHeader.mHref, nullptr, aBaseURI));
230 // The link relation may apply to a different resource, specified
231 // in the anchor parameter. For the link relations supported so far,
232 // we simply abort if the link applies to a resource different to the
233 // one we've loaded
234 if (!nsContentUtils::LinkContextIsURI(aLinkHeader.mAnchor, uri)) {
235 return;
238 // only preload secure context urls
239 if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) {
240 return;
243 CORSMode corsMode = dom::Element::StringToCORSMode(aLinkHeader.mCrossOrigin);
245 Maybe<PreloadHashKey> hashKey =
246 GenerateHashKey(destination, uri, aPrincipal, corsMode, aIsModulepreload);
247 if (!hashKey) {
248 return;
251 if (aOngoingEarlyHints->Contains(*hashKey)) {
252 return;
255 nsContentPolicyType contentPolicyType =
256 aIsModulepreload ? (IsScriptLikeOrInvalid(aLinkHeader.mAs)
257 ? nsContentPolicyType::TYPE_SCRIPT
258 : nsContentPolicyType::TYPE_INVALID)
259 : AsValueToContentPolicy(as);
261 if (contentPolicyType == nsContentPolicyType::TYPE_INVALID) {
262 return;
265 dom::ReferrerPolicy linkReferrerPolicy =
266 dom::ReferrerInfo::ReferrerPolicyAttributeFromString(
267 aLinkHeader.mReferrerPolicy);
269 dom::ReferrerPolicy responseReferrerPolicy =
270 dom::ReferrerInfo::ReferrerPolicyAttributeFromString(
271 NS_ConvertUTF8toUTF16(aResponseReferrerPolicy));
273 // The early hint may have two referrer policies, one from the response header
274 // and one from the link element.
276 // For example, in this server response:
277 // HTTP/1.1 103 Early Hints
278 // Referrer-Policy : origin
279 // Link: </style.css>; rel=preload; as=style referrerpolicy=no-referrer
281 // The link header referrer policy, if present, will take precedence over
282 // the response referrer policy
283 dom::ReferrerPolicy finalReferrerPolicy = responseReferrerPolicy;
284 if (linkReferrerPolicy != dom::ReferrerPolicy::_empty) {
285 finalReferrerPolicy = linkReferrerPolicy;
287 nsCOMPtr<nsIReferrerInfo> referrerInfo =
288 new dom::ReferrerInfo(aBaseURI, finalReferrerPolicy);
290 RefPtr<EarlyHintPreloader> earlyHintPreloader = new EarlyHintPreloader();
292 earlyHintPreloader->mLoadContext = aLoadingBrowsingContext;
294 // Security flags for modulepreload's request mode are computed here directly
295 // until full support for worker destinations can be added.
297 // Implements "To fetch a single module script,"
298 // Step 9. If destination is "worker", "sharedworker", or "serviceworker",
299 // and the top-level module fetch flag is set, then set request's
300 // mode to "same-origin".
301 nsSecurityFlags securityFlags =
302 aIsModulepreload
303 ? ((aLinkHeader.mAs.LowerCaseEqualsASCII("worker") ||
304 aLinkHeader.mAs.LowerCaseEqualsASCII("sharedworker") ||
305 aLinkHeader.mAs.LowerCaseEqualsASCII("serviceworker"))
306 ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED
307 : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) |
308 (corsMode == CORS_USE_CREDENTIALS
309 ? nsILoadInfo::SEC_COOKIES_INCLUDE
310 : nsILoadInfo::SEC_COOKIES_SAME_ORIGIN) |
311 nsILoadInfo::SEC_ALLOW_CHROME
312 : EarlyHintPreloader::ComputeSecurityFlags(corsMode, destination);
314 // Verify that the resource should be loaded.
315 // This isn't the ideal way to test the resource against the CSP.
316 // The problem comes from the fact that at the stage of Early Hint
317 // processing we have not yet created a document where we would normally store
318 // the CSP.
320 // First we will create a load info,
321 // nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK
322 nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new LoadInfo(
323 aPrincipal, // loading principal
324 aPrincipal, // triggering principal
325 nullptr /* aLoadingContext node */,
326 nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, contentPolicyType);
328 if (aCSPHeader.Length() != 0) {
329 // If the CSP header is present then create a new CSP and apply the header
330 // directives to it
331 nsCOMPtr<nsIContentSecurityPolicy> csp = new nsCSPContext();
332 nsresult rv = csp->SetRequestContextWithPrincipal(
333 aPrincipal, aBaseURI, ""_ns, 0 /* aInnerWindowId */);
334 NS_ENSURE_SUCCESS_VOID(rv);
335 rv = CSP_AppendCSPFromHeader(csp, NS_ConvertUTF8toUTF16(aCSPHeader),
336 false /* report only */);
337 NS_ENSURE_SUCCESS_VOID(rv);
339 // We create a temporary ClientInfo. This is required on the loadInfo as
340 // that is how the CSP is queried. More specificially, as a hack to be able
341 // to call NS_CheckContentLoadPolicy on nsILoadInfo which exclusively
342 // accesses the CSP from the ClientInfo, we create a synthetic ClientInfo to
343 // hold the CSP we are creating. This is not a safe thing to do in any other
344 // circumstance because ClientInfos are always describing a ClientSource
345 // that corresponds to a global or potential global, so creating an info
346 // without a source is unsound. For the purposes of doing things before a
347 // global exists, fetch has the concept of a
348 // https://fetch.spec.whatwg.org/#concept-request-reserved-client and
349 // nsILoadInfo explicity has methods around GiveReservedClientSource which
350 // are primarily used by ClientChannelHelper. If you are trying to do real
351 // CSP stuff and the ClientInfo is not there yet, please enhance the logic
352 // around ClientChannelHelper.
354 mozilla::ipc::PrincipalInfo principalInfo;
355 rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
356 NS_ENSURE_SUCCESS_VOID(rv);
357 dom::ClientInfo clientInfo(nsID::GenerateUUID(), dom::ClientType::Window,
358 principalInfo, TimeStamp::Now());
360 // Our newly-created CSP is set on the ClientInfo via the indirect route of
361 // first serializing to CSPInfo
362 ipc::CSPInfo cspInfo;
363 rv = CSPToCSPInfo(csp, &cspInfo);
364 NS_ENSURE_SUCCESS_VOID(rv);
365 clientInfo.SetCspInfo(cspInfo);
367 // This ClientInfo is then set on the new loadInfo.
368 // It can now be used to test the resource against the policy
369 secCheckLoadInfo->SetClientInfo(clientInfo);
372 int16_t shouldLoad = nsIContentPolicy::ACCEPT;
373 nsresult rv = NS_CheckContentLoadPolicy(uri, secCheckLoadInfo, &shouldLoad,
374 nsContentUtils::GetContentPolicy());
376 if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) {
377 return;
380 NS_ENSURE_SUCCESS_VOID(earlyHintPreloader->OpenChannel(
381 uri, aPrincipal, securityFlags, contentPolicyType, referrerInfo,
382 aCookieJarSettings, aBrowsingContextID));
384 earlyHintPreloader->SetLinkHeader(aLinkHeader);
386 DebugOnly<bool> result =
387 aOngoingEarlyHints->Add(*hashKey, earlyHintPreloader);
388 MOZ_ASSERT(result);
391 nsresult EarlyHintPreloader::OpenChannel(
392 nsIURI* aURI, nsIPrincipal* aPrincipal, nsSecurityFlags aSecurityFlags,
393 nsContentPolicyType aContentPolicyType, nsIReferrerInfo* aReferrerInfo,
394 nsICookieJarSettings* aCookieJarSettings, uint64_t aBrowsingContextID) {
395 MOZ_ASSERT(aContentPolicyType == nsContentPolicyType::TYPE_IMAGE ||
396 aContentPolicyType ==
397 nsContentPolicyType::TYPE_INTERNAL_FETCH_PRELOAD ||
398 aContentPolicyType == nsContentPolicyType::TYPE_SCRIPT ||
399 aContentPolicyType == nsContentPolicyType::TYPE_STYLESHEET ||
400 aContentPolicyType == nsContentPolicyType::TYPE_FONT);
402 nsresult rv =
403 NS_NewChannel(getter_AddRefs(mChannel), aURI, aPrincipal, aSecurityFlags,
404 aContentPolicyType, aCookieJarSettings,
405 /* aPerformanceStorage */ nullptr,
406 /* aLoadGroup */ nullptr,
407 /* aCallbacks */ this, nsIRequest::LOAD_NORMAL);
409 NS_ENSURE_SUCCESS(rv, rv);
411 RefPtr<nsHttpChannel> httpChannelObject = do_QueryObject(mChannel);
412 if (!httpChannelObject) {
413 mChannel = nullptr;
414 return NS_ERROR_ABORT;
417 // configure HTTP specific stuff
418 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
419 if (!httpChannel) {
420 mChannel = nullptr;
421 return NS_ERROR_ABORT;
423 DebugOnly<nsresult> success = httpChannel->SetReferrerInfo(aReferrerInfo);
424 MOZ_ASSERT(NS_SUCCEEDED(success));
425 success = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false);
426 MOZ_ASSERT(NS_SUCCEEDED(success));
428 mParentListener = new ParentChannelListener(this, nullptr);
430 PriorizeAsPreload();
432 rv = mChannel->AsyncOpen(mParentListener);
433 if (NS_FAILED(rv)) {
434 mParentListener = nullptr;
435 return rv;
438 SetState(ePreloaderOpened);
440 // Setting the BrowsingContextID here to let Early Hint requests show up in
441 // devtools. Normally that would automatically happen if we would pass the
442 // nsILoadGroup in ns_NewChannel above, but the nsILoadGroup is inaccessible
443 // here in the ParentProcess. The nsILoadGroup only exists in ContentProcess
444 // as part of the document and nsDocShell. It is also not yet determined which
445 // ContentProcess this load belongs to.
446 nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo();
447 static_cast<LoadInfo*>(loadInfo.get())
448 ->UpdateBrowsingContextID(aBrowsingContextID);
450 return NS_OK;
453 void EarlyHintPreloader::PriorizeAsPreload() {
454 nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL;
455 Unused << mChannel->GetLoadFlags(&loadFlags);
456 Unused << mChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND);
458 if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(mChannel)) {
459 Unused << cos->AddClassFlags(nsIClassOfService::Unblocked);
463 void EarlyHintPreloader::SetLinkHeader(const LinkHeader& aLinkHeader) {
464 mConnectArgs.link() = aLinkHeader;
467 bool EarlyHintPreloader::IsFromContentParent(dom::ContentParentId aCpId) const {
468 return aCpId == mCpId;
471 bool EarlyHintPreloader::Register(dom::ContentParentId aCpId,
472 EarlyHintConnectArgs& aOut) {
473 mCpId = aCpId;
475 // Set minimum delay of 1ms to always start the timer after the function call
476 // completed.
477 nsresult rv = NS_NewTimerWithCallback(
478 getter_AddRefs(mTimer), this,
479 std::max(StaticPrefs::network_early_hints_parent_connect_timeout(),
480 (uint32_t)1),
481 nsITimer::TYPE_ONE_SHOT);
482 if (NS_FAILED(rv)) {
483 MOZ_ASSERT(!mTimer);
484 CancelChannel(NS_ERROR_ABORT, "new-timer-failed"_ns,
485 /* aDeleteEntry */ false);
486 return false;
489 // Create an entry in the redirect channel registrar
490 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate();
491 registrar->RegisterEarlyHint(mConnectArgs.earlyHintPreloaderId(), this);
493 aOut = mConnectArgs;
494 return true;
497 nsresult EarlyHintPreloader::CancelChannel(nsresult aStatus,
498 const nsACString& aReason,
499 bool aDeleteEntry) {
500 LOG(("EarlyHintPreloader::CancelChannel [this=%p]\n", this));
502 if (mTimer) {
503 mTimer->Cancel();
504 mTimer = nullptr;
506 if (aDeleteEntry) {
507 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate();
508 registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId());
510 // clear redirect channel in case this channel is cleared between the call of
511 // EarlyHintPreloader::AsyncOnChannelRedirect and
512 // EarlyHintPreloader::OnRedirectResult
513 mRedirectChannel = nullptr;
514 if (mChannel) {
515 if (mSuspended) {
516 mChannel->Resume();
518 mChannel->CancelWithReason(aStatus, aReason);
519 // Clearing mChannel is safe, because this EarlyHintPreloader is not in the
520 // EarlyHintRegistrar after this function call and we won't call
521 // SetHttpChannelFromEarlyHintPreloader nor OnStartRequest on mParent.
522 mChannel = nullptr;
523 SetState(ePreloaderCancelled);
525 return NS_OK;
528 void EarlyHintPreloader::OnParentReady(nsIParentChannel* aParent) {
529 AssertIsOnMainThread();
530 MOZ_ASSERT(aParent);
531 LOG(("EarlyHintPreloader::OnParentReady [this=%p]\n", this));
533 nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
534 if (obs) {
535 obs->NotifyObservers(mChannel, "earlyhints-connectback", nullptr);
538 mParent = aParent;
540 if (mTimer) {
541 mTimer->Cancel();
542 mTimer = nullptr;
545 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate();
546 registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId());
548 if (mOnStartRequestCalled) {
549 SetParentChannel();
550 InvokeStreamListenerFunctions();
554 void EarlyHintPreloader::SetParentChannel() {
555 RefPtr<HttpBaseChannel> channel = do_QueryObject(mChannel);
556 RefPtr<HttpChannelParent> parent = do_QueryObject(mParent);
557 parent->SetHttpChannelFromEarlyHintPreloader(channel);
560 // Adapted from
561 // https://searchfox.org/mozilla-central/rev/b4150d1c6fae0c51c522df2d2c939cf5ad331d4c/netwerk/ipc/DocumentLoadListener.cpp#1311
562 void EarlyHintPreloader::InvokeStreamListenerFunctions() {
563 AssertIsOnMainThread();
564 RefPtr<EarlyHintPreloader> self(this);
566 LOG((
567 "EarlyHintPreloader::InvokeStreamListenerFunctions [this=%p parent=%p]\n",
568 this, mParent.get()));
570 // If we failed to suspend the channel, then we might have received
571 // some messages while the redirected was being handled.
572 // Manually send them on now.
573 if (!mIsFinished) {
574 // This is safe to do, because OnStartRequest/OnStopRequest/OnDataAvailable
575 // are all called on the main thread. They can't be called until we worked
576 // through all functions in the streamListnerFunctions array.
577 mParentListener->SetListenerAfterRedirect(mParent);
579 nsTArray<StreamListenerFunction> streamListenerFunctions =
580 std::move(mStreamListenerFunctions);
582 ForwardStreamListenerFunctions(std::move(streamListenerFunctions), mParent);
584 // We don't expect to get new stream listener functions added
585 // via re-entrancy. If this ever happens, we should understand
586 // exactly why before allowing it.
587 NS_ASSERTION(mStreamListenerFunctions.IsEmpty(),
588 "Should not have added new stream listener function!");
590 if (mChannel && mSuspended) {
591 mChannel->Resume();
593 mChannel = nullptr;
594 mParent = nullptr;
595 mParentListener = nullptr;
597 SetState(ePreloaderUsed);
600 //-----------------------------------------------------------------------------
601 // EarlyHintPreloader::nsISupports
602 //-----------------------------------------------------------------------------
604 NS_IMPL_ISUPPORTS(EarlyHintPreloader, nsIRequestObserver, nsIStreamListener,
605 nsIChannelEventSink, nsIInterfaceRequestor,
606 nsIRedirectResultListener, nsIMultiPartChannelListener,
607 nsINamed, nsITimerCallback);
609 //-----------------------------------------------------------------------------
610 // EarlyHintPreloader::nsIStreamListener
611 //-----------------------------------------------------------------------------
613 // Implementation copied and adapted from DocumentLoadListener::OnStartRequest
614 // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2317-2508
615 NS_IMETHODIMP
616 EarlyHintPreloader::OnStartRequest(nsIRequest* aRequest) {
617 LOG(("EarlyHintPreloader::OnStartRequest [this=%p]\n", this));
618 AssertIsOnMainThread();
620 mOnStartRequestCalled = true;
622 nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest);
623 if (multiPartChannel) {
624 multiPartChannel->GetBaseChannel(getter_AddRefs(mChannel));
625 } else {
626 mChannel = do_QueryInterface(aRequest);
628 MOZ_DIAGNOSTIC_ASSERT(mChannel);
630 nsresult status = NS_OK;
631 Unused << aRequest->GetStatus(&status);
633 if (mParent) {
634 SetParentChannel();
635 mParent->OnStartRequest(aRequest);
636 InvokeStreamListenerFunctions();
637 } else {
638 // Don't suspend the chanel when the channel got cancelled with
639 // CancelChannel, because then OnStopRequest wouldn't get called and we
640 // wouldn't clean up the channel.
641 if (NS_SUCCEEDED(status)) {
642 mChannel->Suspend();
643 mSuspended = true;
645 mStreamListenerFunctions.AppendElement(
646 AsVariant(OnStartRequestParams{aRequest}));
649 // return error after adding the OnStartRequest forward. The OnStartRequest
650 // failure has to be forwarded to listener, because they called AsyncOpen on
651 // this channel
652 return status;
655 // Implementation copied from DocumentLoadListener::OnStopRequest
656 // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2510-2528
657 NS_IMETHODIMP
658 EarlyHintPreloader::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
659 AssertIsOnMainThread();
660 LOG(("EarlyHintPreloader::OnStopRequest [this=%p]\n", this));
661 mStreamListenerFunctions.AppendElement(
662 AsVariant(OnStopRequestParams{aRequest, aStatusCode}));
664 // If we're not a multi-part channel, then we're finished and we don't
665 // expect any further events. If we are, then this might be called again,
666 // so wait for OnAfterLastPart instead.
667 nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest);
668 if (!multiPartChannel) {
669 mIsFinished = true;
672 return NS_OK;
675 //-----------------------------------------------------------------------------
676 // EarlyHintPreloader::nsIStreamListener
677 //-----------------------------------------------------------------------------
679 // Implementation copied from DocumentLoadListener::OnDataAvailable
680 // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2530-2549
681 NS_IMETHODIMP
682 EarlyHintPreloader::OnDataAvailable(nsIRequest* aRequest,
683 nsIInputStream* aInputStream,
684 uint64_t aOffset, uint32_t aCount) {
685 AssertIsOnMainThread();
686 LOG(("EarlyHintPreloader::OnDataAvailable [this=%p]\n", this));
687 // This isn't supposed to happen, since we suspended the channel, but
688 // sometimes Suspend just doesn't work. This can happen when we're routing
689 // through nsUnknownDecoder to sniff the content type, and it doesn't handle
690 // being suspended. Let's just store the data and manually forward it to our
691 // redirected channel when it's ready.
692 nsCString data;
693 nsresult rv = NS_ReadInputStreamToString(aInputStream, data, aCount);
694 NS_ENSURE_SUCCESS(rv, rv);
696 mStreamListenerFunctions.AppendElement(AsVariant(
697 OnDataAvailableParams{aRequest, std::move(data), aOffset, aCount}));
699 return NS_OK;
702 //-----------------------------------------------------------------------------
703 // EarlyHintPreloader::nsIMultiPartChannelListener
704 //-----------------------------------------------------------------------------
706 NS_IMETHODIMP
707 EarlyHintPreloader::OnAfterLastPart(nsresult aStatus) {
708 LOG(("EarlyHintPreloader::OnAfterLastPart [this=%p]", this));
709 mStreamListenerFunctions.AppendElement(
710 AsVariant(OnAfterLastPartParams{aStatus}));
711 mIsFinished = true;
712 return NS_OK;
715 //-----------------------------------------------------------------------------
716 // EarlyHintPreloader::nsIChannelEventSink
717 //-----------------------------------------------------------------------------
719 NS_IMETHODIMP
720 EarlyHintPreloader::AsyncOnChannelRedirect(
721 nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
722 nsIAsyncVerifyRedirectCallback* callback) {
723 LOG(("EarlyHintPreloader::AsyncOnChannelRedirect [this=%p]", this));
724 nsCOMPtr<nsIURI> newURI;
725 nsresult rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI));
726 NS_ENSURE_SUCCESS(rv, rv);
728 rv = aNewChannel->GetURI(getter_AddRefs(newURI));
729 if (NS_FAILED(rv)) {
730 callback->OnRedirectVerifyCallback(rv);
731 return NS_OK;
734 // HTTP request headers are not automatically forwarded to the new channel.
735 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel);
736 NS_ENSURE_STATE(httpChannel);
738 rv = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false);
739 MOZ_ASSERT(NS_SUCCEEDED(rv));
741 // Assign to mChannel after we get notification about success of the
742 // redirect in OnRedirectResult.
743 mRedirectChannel = aNewChannel;
745 callback->OnRedirectVerifyCallback(NS_OK);
746 return NS_OK;
749 //-----------------------------------------------------------------------------
750 // EarlyHintPreloader::nsIRedirectResultListener
751 //-----------------------------------------------------------------------------
753 NS_IMETHODIMP
754 EarlyHintPreloader::OnRedirectResult(nsresult aStatus) {
755 LOG(("EarlyHintPreloader::OnRedirectResult [this=%p] aProceeding=0x%" PRIx32,
756 this, static_cast<uint32_t>(aStatus)));
757 if (NS_SUCCEEDED(aStatus) && mRedirectChannel) {
758 mChannel = mRedirectChannel;
761 mRedirectChannel = nullptr;
763 return NS_OK;
766 //-----------------------------------------------------------------------------
767 // EarlyHintPreloader::nsINamed
768 //-----------------------------------------------------------------------------
770 NS_IMETHODIMP
771 EarlyHintPreloader::GetName(nsACString& aName) {
772 aName.AssignLiteral("EarlyHintPreloader");
773 return NS_OK;
776 //-----------------------------------------------------------------------------
777 // EarlyHintPreloader::nsITimerCallback
778 //-----------------------------------------------------------------------------
780 NS_IMETHODIMP
781 EarlyHintPreloader::Notify(nsITimer* timer) {
782 // Death grip, because we will most likely remove the last reference when
783 // deleting us from the EarlyHintRegistrar
784 RefPtr<EarlyHintPreloader> deathGrip(this);
786 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate();
787 registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId());
789 mTimer = nullptr;
790 mRedirectChannel = nullptr;
791 if (mChannel) {
792 if (mSuspended) {
793 mChannel->Resume();
795 mChannel->CancelWithReason(NS_ERROR_ABORT, "parent-connect-timeout"_ns);
796 #ifndef ANDROID
797 glean::netwerk::parent_connect_timeout.Add(1);
798 #endif
799 mChannel = nullptr;
801 SetState(ePreloaderTimeout);
803 return NS_OK;
806 //-----------------------------------------------------------------------------
807 // EarlyHintPreloader::nsIInterfaceRequestor
808 //-----------------------------------------------------------------------------
810 NS_IMETHODIMP
811 EarlyHintPreloader::GetInterface(const nsIID& aIID, void** aResult) {
812 if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
813 NS_ADDREF_THIS();
814 *aResult = static_cast<nsIChannelEventSink*>(this);
815 return NS_OK;
818 if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) {
819 NS_ADDREF_THIS();
820 *aResult = static_cast<nsIRedirectResultListener*>(this);
821 return NS_OK;
824 if (aIID.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext != nullptr) {
825 nsCOMPtr<nsILoadContext> loadContext = mLoadContext;
826 loadContext.forget(aResult);
827 return NS_OK;
830 return NS_ERROR_NO_INTERFACE;
833 void EarlyHintPreloader::CollectResourcesTypeTelemetry(
834 ASDestination aASDestination) {
835 if (aASDestination == ASDestination::DESTINATION_FONT) {
836 glean::netwerk::early_hints.Get("font"_ns).Add(1);
837 } else if (aASDestination == ASDestination::DESTINATION_SCRIPT) {
838 glean::netwerk::early_hints.Get("script"_ns).Add(1);
839 } else if (aASDestination == ASDestination::DESTINATION_STYLE) {
840 glean::netwerk::early_hints.Get("stylesheet"_ns).Add(1);
841 } else if (aASDestination == ASDestination::DESTINATION_IMAGE) {
842 glean::netwerk::early_hints.Get("image"_ns).Add(1);
843 } else if (aASDestination == ASDestination::DESTINATION_FETCH) {
844 glean::netwerk::early_hints.Get("fetch"_ns).Add(1);
845 } else {
846 glean::netwerk::early_hints.Get("other"_ns).Add(1);
849 } // namespace mozilla::net