Bug 1814798 - pt 2. Add a PHCManager component to control PHC r=glandium,emilio
[gecko.git] / netwerk / protocol / http / EarlyHintPreloader.cpp
bloba5f363091bb4ca81acedce9a218fb11c7d1e2e80
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 "mozilla/CORSMode.h"
14 #include "mozilla/dom/Element.h"
15 #include "mozilla/dom/nsCSPContext.h"
16 #include "mozilla/dom/nsMixedContentBlocker.h"
17 #include "mozilla/dom/ReferrerInfo.h"
18 #include "mozilla/glean/GleanMetrics.h"
19 #include "mozilla/ipc/BackgroundUtils.h"
20 #include "mozilla/LoadInfo.h"
21 #include "mozilla/Logging.h"
22 #include "mozilla/net/EarlyHintRegistrar.h"
23 #include "mozilla/net/NeckoChannelParams.h"
24 #include "mozilla/StaticPrefs_network.h"
25 #include "mozilla/Telemetry.h"
26 #include "nsAttrValue.h"
27 #include "nsAttrValueInlines.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 "nsILoadInfo.h"
40 #include "nsIParentChannel.h"
41 #include "nsIReferrerInfo.h"
42 #include "nsITimer.h"
43 #include "nsIURI.h"
44 #include "nsNetUtil.h"
45 #include "nsQueryObject.h"
46 #include "nsStreamUtils.h"
47 #include "nsStringStream.h"
48 #include "ParentChannelListener.h"
49 #include "nsIChannel.h"
50 #include "nsInterfaceRequestorAgg.h"
53 // To enable logging (see mozilla/Logging.h for full details):
55 // set MOZ_LOG=EarlyHint:5
56 // set MOZ_LOG_FILE=earlyhint.log
58 // this enables LogLevel::Debug level information and places all output in
59 // the file earlyhint.log
61 static mozilla::LazyLogModule gEarlyHintLog("EarlyHint");
63 #undef LOG
64 #define LOG(args) MOZ_LOG(gEarlyHintLog, mozilla::LogLevel::Debug, args)
66 #undef LOG_ENABLED
67 #define LOG_ENABLED() MOZ_LOG_TEST(gEarlyHintLog, mozilla::LogLevel::Debug)
69 namespace mozilla::net {
71 namespace {
72 // This id uniquely identifies each early hint preloader in the
73 // EarlyHintRegistrar. Must only be accessed from main thread.
74 static uint64_t gEarlyHintPreloaderId{0};
75 } // namespace
77 //=============================================================================
78 // OngoingEarlyHints
79 //=============================================================================
81 void OngoingEarlyHints::CancelAll(const nsACString& aReason) {
82 for (auto& preloader : mPreloaders) {
83 preloader->CancelChannel(NS_ERROR_ABORT, aReason, /* aDeleteEntry */ true);
85 mPreloaders.Clear();
86 mStartedPreloads.Clear();
89 bool OngoingEarlyHints::Contains(const PreloadHashKey& aKey) {
90 return mStartedPreloads.Contains(aKey);
93 bool OngoingEarlyHints::Add(const PreloadHashKey& aKey,
94 RefPtr<EarlyHintPreloader> aPreloader) {
95 if (!mStartedPreloads.Contains(aKey)) {
96 mStartedPreloads.Insert(aKey);
97 mPreloaders.AppendElement(aPreloader);
98 return true;
100 return false;
103 void OngoingEarlyHints::RegisterLinksAndGetConnectArgs(
104 dom::ContentParentId aCpId, nsTArray<EarlyHintConnectArgs>& aOutLinks) {
105 // register all channels before returning
106 for (auto& preload : mPreloaders) {
107 EarlyHintConnectArgs args;
108 if (preload->Register(aCpId, args)) {
109 aOutLinks.AppendElement(std::move(args));
114 //=============================================================================
115 // EarlyHintPreloader
116 //=============================================================================
118 EarlyHintPreloader::EarlyHintPreloader() {
119 AssertIsOnMainThread();
120 mConnectArgs.earlyHintPreloaderId() = ++gEarlyHintPreloaderId;
123 EarlyHintPreloader::~EarlyHintPreloader() {
124 if (mTimer) {
125 mTimer->Cancel();
126 mTimer = nullptr;
128 Telemetry::Accumulate(Telemetry::EH_STATE_OF_PRELOAD_REQUEST, mState);
131 /* static */
132 Maybe<PreloadHashKey> EarlyHintPreloader::GenerateHashKey(
133 ASDestination aAs, nsIURI* aURI, nsIPrincipal* aPrincipal,
134 CORSMode aCorsMode, bool aIsModulepreload) {
135 if (aIsModulepreload) {
136 return Some(PreloadHashKey::CreateAsScript(
137 aURI, aCorsMode, JS::loader::ScriptKind::eModule));
139 if (aAs == ASDestination::DESTINATION_FONT && aCorsMode != CORS_NONE) {
140 return Some(PreloadHashKey::CreateAsFont(aURI, aCorsMode));
142 if (aAs == ASDestination::DESTINATION_IMAGE) {
143 return Some(PreloadHashKey::CreateAsImage(aURI, aPrincipal, aCorsMode));
145 if (aAs == ASDestination::DESTINATION_SCRIPT) {
146 return Some(PreloadHashKey::CreateAsScript(
147 aURI, aCorsMode, JS::loader::ScriptKind::eClassic));
149 if (aAs == ASDestination::DESTINATION_STYLE) {
150 return Some(PreloadHashKey::CreateAsStyle(
151 aURI, aPrincipal, aCorsMode,
152 css::SheetParsingMode::eAuthorSheetFeatures));
154 if (aAs == ASDestination::DESTINATION_FETCH && aCorsMode != CORS_NONE) {
155 return Some(PreloadHashKey::CreateAsFetch(aURI, aCorsMode));
157 return Nothing();
160 /* static */
161 nsSecurityFlags EarlyHintPreloader::ComputeSecurityFlags(CORSMode aCORSMode,
162 ASDestination aAs) {
163 if (aAs == ASDestination::DESTINATION_FONT) {
164 return nsContentSecurityManager::ComputeSecurityFlags(
165 CORSMode::CORS_NONE,
166 nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS);
168 if (aAs == ASDestination::DESTINATION_IMAGE) {
169 return nsContentSecurityManager::ComputeSecurityFlags(
170 aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
171 CORS_NONE_MAPS_TO_INHERITED_CONTEXT) |
172 nsILoadInfo::SEC_ALLOW_CHROME;
174 if (aAs == ASDestination::DESTINATION_SCRIPT) {
175 return nsContentSecurityManager::ComputeSecurityFlags(
176 aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
177 CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS) |
178 nsILoadInfo::SEC_ALLOW_CHROME;
180 if (aAs == ASDestination::DESTINATION_STYLE) {
181 return nsContentSecurityManager::ComputeSecurityFlags(
182 aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
183 CORS_NONE_MAPS_TO_INHERITED_CONTEXT) |
184 nsILoadInfo::SEC_ALLOW_CHROME;
187 if (aAs == ASDestination::DESTINATION_FETCH) {
188 return nsContentSecurityManager::ComputeSecurityFlags(
189 aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
190 CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS);
192 MOZ_ASSERT(false, "Unexpected ASDestination");
193 return nsContentSecurityManager::ComputeSecurityFlags(
194 CORSMode::CORS_NONE,
195 nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS);
198 // static
199 void EarlyHintPreloader::MaybeCreateAndInsertPreload(
200 OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aLinkHeader,
201 nsIURI* aBaseURI, nsIPrincipal* aPrincipal,
202 nsICookieJarSettings* aCookieJarSettings,
203 const nsACString& aResponseReferrerPolicy, const nsACString& aCSPHeader,
204 uint64_t aBrowsingContextID, nsIInterfaceRequestor* aCallbacks,
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 nsCOMPtr<nsIURI> uri;
223 NS_ENSURE_SUCCESS_VOID(
224 NS_NewURI(getter_AddRefs(uri), aLinkHeader.mHref, nullptr, aBaseURI));
225 // The link relation may apply to a different resource, specified
226 // in the anchor parameter. For the link relations supported so far,
227 // we simply abort if the link applies to a resource different to the
228 // one we've loaded
229 if (!nsContentUtils::LinkContextIsURI(aLinkHeader.mAnchor, uri)) {
230 return;
233 // only preload secure context urls
234 if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) {
235 return;
238 CORSMode corsMode = dom::Element::StringToCORSMode(aLinkHeader.mCrossOrigin);
240 Maybe<PreloadHashKey> hashKey =
241 GenerateHashKey(destination, uri, aPrincipal, corsMode, aIsModulepreload);
242 if (!hashKey) {
243 return;
246 if (aOngoingEarlyHints->Contains(*hashKey)) {
247 return;
250 nsContentPolicyType contentPolicyType =
251 aIsModulepreload ? (IsScriptLikeOrInvalid(aLinkHeader.mAs)
252 ? nsContentPolicyType::TYPE_SCRIPT
253 : nsContentPolicyType::TYPE_INVALID)
254 : AsValueToContentPolicy(as);
256 if (contentPolicyType == nsContentPolicyType::TYPE_INVALID) {
257 return;
260 dom::ReferrerPolicy linkReferrerPolicy =
261 dom::ReferrerInfo::ReferrerPolicyAttributeFromString(
262 aLinkHeader.mReferrerPolicy);
264 dom::ReferrerPolicy responseReferrerPolicy =
265 dom::ReferrerInfo::ReferrerPolicyAttributeFromString(
266 NS_ConvertUTF8toUTF16(aResponseReferrerPolicy));
268 // The early hint may have two referrer policies, one from the response header
269 // and one from the link element.
271 // For example, in this server response:
272 // HTTP/1.1 103 Early Hints
273 // Referrer-Policy : origin
274 // Link: </style.css>; rel=preload; as=style referrerpolicy=no-referrer
276 // The link header referrer policy, if present, will take precedence over
277 // the response referrer policy
278 dom::ReferrerPolicy finalReferrerPolicy = responseReferrerPolicy;
279 if (linkReferrerPolicy != dom::ReferrerPolicy::_empty) {
280 finalReferrerPolicy = linkReferrerPolicy;
282 nsCOMPtr<nsIReferrerInfo> referrerInfo =
283 new dom::ReferrerInfo(aBaseURI, finalReferrerPolicy);
285 RefPtr<EarlyHintPreloader> earlyHintPreloader = new EarlyHintPreloader();
287 // Security flags for modulepreload's request mode are computed here directly
288 // until full support for worker destinations can be added.
290 // Implements "To fetch a single module script,"
291 // Step 9. If destination is "worker", "sharedworker", or "serviceworker",
292 // and the top-level module fetch flag is set, then set request's
293 // mode to "same-origin".
294 nsSecurityFlags securityFlags =
295 aIsModulepreload
296 ? ((aLinkHeader.mAs.LowerCaseEqualsASCII("worker") ||
297 aLinkHeader.mAs.LowerCaseEqualsASCII("sharedworker") ||
298 aLinkHeader.mAs.LowerCaseEqualsASCII("serviceworker"))
299 ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED
300 : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) |
301 (corsMode == CORS_USE_CREDENTIALS
302 ? nsILoadInfo::SEC_COOKIES_INCLUDE
303 : nsILoadInfo::SEC_COOKIES_SAME_ORIGIN) |
304 nsILoadInfo::SEC_ALLOW_CHROME
305 : EarlyHintPreloader::ComputeSecurityFlags(corsMode, destination);
307 // Verify that the resource should be loaded.
308 // This isn't the ideal way to test the resource against the CSP.
309 // The problem comes from the fact that at the stage of Early Hint
310 // processing we have not yet created a document where we would normally store
311 // the CSP.
313 // First we will create a load info,
314 // nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK
315 nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new LoadInfo(
316 aPrincipal, // loading principal
317 aPrincipal, // triggering principal
318 nullptr /* aLoadingContext node */,
319 nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, contentPolicyType);
321 if (aCSPHeader.Length() != 0) {
322 // If the CSP header is present then create a new CSP and apply the header
323 // directives to it
324 nsCOMPtr<nsIContentSecurityPolicy> csp = new nsCSPContext();
325 nsresult rv = csp->SetRequestContextWithPrincipal(
326 aPrincipal, aBaseURI, u""_ns, 0 /* aInnerWindowId */);
327 NS_ENSURE_SUCCESS_VOID(rv);
328 rv = CSP_AppendCSPFromHeader(csp, NS_ConvertUTF8toUTF16(aCSPHeader),
329 false /* report only */);
330 NS_ENSURE_SUCCESS_VOID(rv);
332 // We create a temporary ClientInfo. This is required on the loadInfo as
333 // that is how the CSP is queried. More specificially, as a hack to be able
334 // to call NS_CheckContentLoadPolicy on nsILoadInfo which exclusively
335 // accesses the CSP from the ClientInfo, we create a synthetic ClientInfo to
336 // hold the CSP we are creating. This is not a safe thing to do in any other
337 // circumstance because ClientInfos are always describing a ClientSource
338 // that corresponds to a global or potential global, so creating an info
339 // without a source is unsound. For the purposes of doing things before a
340 // global exists, fetch has the concept of a
341 // https://fetch.spec.whatwg.org/#concept-request-reserved-client and
342 // nsILoadInfo explicity has methods around GiveReservedClientSource which
343 // are primarily used by ClientChannelHelper. If you are trying to do real
344 // CSP stuff and the ClientInfo is not there yet, please enhance the logic
345 // around ClientChannelHelper.
347 mozilla::ipc::PrincipalInfo principalInfo;
348 rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
349 NS_ENSURE_SUCCESS_VOID(rv);
350 dom::ClientInfo clientInfo(nsID::GenerateUUID(), dom::ClientType::Window,
351 principalInfo, TimeStamp::Now());
353 // Our newly-created CSP is set on the ClientInfo via the indirect route of
354 // first serializing to CSPInfo
355 ipc::CSPInfo cspInfo;
356 rv = CSPToCSPInfo(csp, &cspInfo);
357 NS_ENSURE_SUCCESS_VOID(rv);
358 clientInfo.SetCspInfo(cspInfo);
360 // This ClientInfo is then set on the new loadInfo.
361 // It can now be used to test the resource against the policy
362 secCheckLoadInfo->SetClientInfo(clientInfo);
365 int16_t shouldLoad = nsIContentPolicy::ACCEPT;
366 nsresult rv =
367 NS_CheckContentLoadPolicy(uri, secCheckLoadInfo, ""_ns, &shouldLoad,
368 nsContentUtils::GetContentPolicy());
370 if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) {
371 return;
374 NS_ENSURE_SUCCESS_VOID(earlyHintPreloader->OpenChannel(
375 uri, aPrincipal, securityFlags, contentPolicyType, referrerInfo,
376 aCookieJarSettings, aBrowsingContextID, aCallbacks));
378 earlyHintPreloader->SetLinkHeader(aLinkHeader);
380 DebugOnly<bool> result =
381 aOngoingEarlyHints->Add(*hashKey, earlyHintPreloader);
382 MOZ_ASSERT(result);
385 nsresult EarlyHintPreloader::OpenChannel(
386 nsIURI* aURI, nsIPrincipal* aPrincipal, nsSecurityFlags aSecurityFlags,
387 nsContentPolicyType aContentPolicyType, nsIReferrerInfo* aReferrerInfo,
388 nsICookieJarSettings* aCookieJarSettings, uint64_t aBrowsingContextID,
389 nsIInterfaceRequestor* aCallbacks) {
390 MOZ_ASSERT(aContentPolicyType == nsContentPolicyType::TYPE_IMAGE ||
391 aContentPolicyType ==
392 nsContentPolicyType::TYPE_INTERNAL_FETCH_PRELOAD ||
393 aContentPolicyType == nsContentPolicyType::TYPE_SCRIPT ||
394 aContentPolicyType == nsContentPolicyType::TYPE_STYLESHEET ||
395 aContentPolicyType == nsContentPolicyType::TYPE_FONT);
397 nsCOMPtr<nsIInterfaceRequestor> wrappedCallbacks;
398 NS_NewInterfaceRequestorAggregation(this, aCallbacks,
399 getter_AddRefs(wrappedCallbacks));
400 nsresult rv =
401 NS_NewChannel(getter_AddRefs(mChannel), aURI, aPrincipal, aSecurityFlags,
402 aContentPolicyType, aCookieJarSettings,
403 /* aPerformanceStorage */ nullptr,
404 /* aLoadGroup */ nullptr,
405 /* aCallbacks */ wrappedCallbacks, nsIRequest::LOAD_NORMAL);
407 NS_ENSURE_SUCCESS(rv, rv);
409 RefPtr<nsHttpChannel> httpChannelObject = do_QueryObject(mChannel);
410 if (!httpChannelObject) {
411 mChannel = nullptr;
412 return NS_ERROR_ABORT;
415 // configure HTTP specific stuff
416 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
417 if (!httpChannel) {
418 mChannel = nullptr;
419 return NS_ERROR_ABORT;
421 DebugOnly<nsresult> success = httpChannel->SetReferrerInfo(aReferrerInfo);
422 MOZ_ASSERT(NS_SUCCEEDED(success));
423 success = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false);
424 MOZ_ASSERT(NS_SUCCEEDED(success));
426 mParentListener = new ParentChannelListener(this, nullptr);
428 PriorizeAsPreload();
430 rv = mChannel->AsyncOpen(mParentListener);
431 if (NS_FAILED(rv)) {
432 mParentListener = nullptr;
433 return rv;
436 SetState(ePreloaderOpened);
438 // Setting the BrowsingContextID here to let Early Hint requests show up in
439 // devtools. Normally that would automatically happen if we would pass the
440 // nsILoadGroup in ns_NewChannel above, but the nsILoadGroup is inaccessible
441 // here in the ParentProcess. The nsILoadGroup only exists in ContentProcess
442 // as part of the document and nsDocShell. It is also not yet determined which
443 // ContentProcess this load belongs to.
444 nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo();
445 static_cast<LoadInfo*>(loadInfo.get())
446 ->UpdateBrowsingContextID(aBrowsingContextID);
448 return NS_OK;
451 void EarlyHintPreloader::PriorizeAsPreload() {
452 nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL;
453 Unused << mChannel->GetLoadFlags(&loadFlags);
454 Unused << mChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND);
456 if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(mChannel)) {
457 Unused << cos->AddClassFlags(nsIClassOfService::Unblocked);
461 void EarlyHintPreloader::SetLinkHeader(const LinkHeader& aLinkHeader) {
462 mConnectArgs.link() = aLinkHeader;
465 bool EarlyHintPreloader::IsFromContentParent(dom::ContentParentId aCpId) const {
466 return aCpId == mCpId;
469 bool EarlyHintPreloader::Register(dom::ContentParentId aCpId,
470 EarlyHintConnectArgs& aOut) {
471 mCpId = aCpId;
473 // Set minimum delay of 1ms to always start the timer after the function call
474 // completed.
475 nsresult rv = NS_NewTimerWithCallback(
476 getter_AddRefs(mTimer), this,
477 std::max(StaticPrefs::network_early_hints_parent_connect_timeout(),
478 (uint32_t)1),
479 nsITimer::TYPE_ONE_SHOT);
480 if (NS_FAILED(rv)) {
481 MOZ_ASSERT(!mTimer);
482 CancelChannel(NS_ERROR_ABORT, "new-timer-failed"_ns,
483 /* aDeleteEntry */ false);
484 return false;
487 // Create an entry in the redirect channel registrar
488 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate();
489 registrar->RegisterEarlyHint(mConnectArgs.earlyHintPreloaderId(), this);
491 aOut = mConnectArgs;
492 return true;
495 nsresult EarlyHintPreloader::CancelChannel(nsresult aStatus,
496 const nsACString& aReason,
497 bool aDeleteEntry) {
498 LOG(("EarlyHintPreloader::CancelChannel [this=%p]\n", this));
500 if (mTimer) {
501 mTimer->Cancel();
502 mTimer = nullptr;
504 if (aDeleteEntry) {
505 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate();
506 registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId());
508 // clear redirect channel in case this channel is cleared between the call of
509 // EarlyHintPreloader::AsyncOnChannelRedirect and
510 // EarlyHintPreloader::OnRedirectResult
511 mRedirectChannel = nullptr;
512 if (mChannel) {
513 if (mSuspended) {
514 mChannel->Resume();
516 mChannel->CancelWithReason(aStatus, aReason);
517 // Clearing mChannel is safe, because this EarlyHintPreloader is not in the
518 // EarlyHintRegistrar after this function call and we won't call
519 // SetHttpChannelFromEarlyHintPreloader nor OnStartRequest on mParent.
520 mChannel = nullptr;
521 SetState(ePreloaderCancelled);
523 return NS_OK;
526 void EarlyHintPreloader::OnParentReady(nsIParentChannel* aParent) {
527 AssertIsOnMainThread();
528 MOZ_ASSERT(aParent);
529 LOG(("EarlyHintPreloader::OnParentReady [this=%p]\n", this));
531 nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
532 if (obs) {
533 obs->NotifyObservers(mChannel, "earlyhints-connectback", nullptr);
536 mParent = aParent;
538 if (mTimer) {
539 mTimer->Cancel();
540 mTimer = nullptr;
543 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate();
544 registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId());
546 if (mOnStartRequestCalled) {
547 SetParentChannel();
548 InvokeStreamListenerFunctions();
552 void EarlyHintPreloader::SetParentChannel() {
553 RefPtr<HttpBaseChannel> channel = do_QueryObject(mChannel);
554 RefPtr<HttpChannelParent> parent = do_QueryObject(mParent);
555 parent->SetHttpChannelFromEarlyHintPreloader(channel);
558 // Adapted from
559 // https://searchfox.org/mozilla-central/rev/b4150d1c6fae0c51c522df2d2c939cf5ad331d4c/netwerk/ipc/DocumentLoadListener.cpp#1311
560 void EarlyHintPreloader::InvokeStreamListenerFunctions() {
561 AssertIsOnMainThread();
562 RefPtr<EarlyHintPreloader> self(this);
564 LOG((
565 "EarlyHintPreloader::InvokeStreamListenerFunctions [this=%p parent=%p]\n",
566 this, mParent.get()));
568 // If we failed to suspend the channel, then we might have received
569 // some messages while the redirected was being handled.
570 // Manually send them on now.
571 if (!mIsFinished) {
572 // This is safe to do, because OnStartRequest/OnStopRequest/OnDataAvailable
573 // are all called on the main thread. They can't be called until we worked
574 // through all functions in the streamListnerFunctions array.
575 mParentListener->SetListenerAfterRedirect(mParent);
577 nsTArray<StreamListenerFunction> streamListenerFunctions =
578 std::move(mStreamListenerFunctions);
580 ForwardStreamListenerFunctions(streamListenerFunctions, mParent);
582 // We don't expect to get new stream listener functions added
583 // via re-entrancy. If this ever happens, we should understand
584 // exactly why before allowing it.
585 NS_ASSERTION(mStreamListenerFunctions.IsEmpty(),
586 "Should not have added new stream listener function!");
588 if (mChannel && mSuspended) {
589 mChannel->Resume();
591 mChannel = nullptr;
592 mParent = nullptr;
593 mParentListener = nullptr;
595 SetState(ePreloaderUsed);
598 //-----------------------------------------------------------------------------
599 // EarlyHintPreloader::nsISupports
600 //-----------------------------------------------------------------------------
602 NS_IMPL_ISUPPORTS(EarlyHintPreloader, nsIRequestObserver, nsIStreamListener,
603 nsIChannelEventSink, nsIInterfaceRequestor,
604 nsIRedirectResultListener, nsIMultiPartChannelListener,
605 nsINamed, nsITimerCallback);
607 //-----------------------------------------------------------------------------
608 // EarlyHintPreloader::nsIStreamListener
609 //-----------------------------------------------------------------------------
611 // Implementation copied and adapted from DocumentLoadListener::OnStartRequest
612 // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2317-2508
613 NS_IMETHODIMP
614 EarlyHintPreloader::OnStartRequest(nsIRequest* aRequest) {
615 LOG(("EarlyHintPreloader::OnStartRequest [this=%p]\n", this));
616 AssertIsOnMainThread();
618 mOnStartRequestCalled = true;
620 nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest);
621 if (multiPartChannel) {
622 multiPartChannel->GetBaseChannel(getter_AddRefs(mChannel));
623 } else {
624 mChannel = do_QueryInterface(aRequest);
626 MOZ_DIAGNOSTIC_ASSERT(mChannel);
628 nsresult status = NS_OK;
629 Unused << aRequest->GetStatus(&status);
631 if (mParent) {
632 SetParentChannel();
633 mParent->OnStartRequest(aRequest);
634 InvokeStreamListenerFunctions();
635 } else {
636 // Don't suspend the chanel when the channel got cancelled with
637 // CancelChannel, because then OnStopRequest wouldn't get called and we
638 // wouldn't clean up the channel.
639 if (NS_SUCCEEDED(status)) {
640 mChannel->Suspend();
641 mSuspended = true;
643 mStreamListenerFunctions.AppendElement(
644 AsVariant(OnStartRequestParams{aRequest}));
647 // return error after adding the OnStartRequest forward. The OnStartRequest
648 // failure has to be forwarded to listener, because they called AsyncOpen on
649 // this channel
650 return status;
653 // Implementation copied from DocumentLoadListener::OnStopRequest
654 // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2510-2528
655 NS_IMETHODIMP
656 EarlyHintPreloader::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
657 AssertIsOnMainThread();
658 LOG(("EarlyHintPreloader::OnStopRequest [this=%p]\n", this));
659 mStreamListenerFunctions.AppendElement(
660 AsVariant(OnStopRequestParams{aRequest, aStatusCode}));
662 // If we're not a multi-part channel, then we're finished and we don't
663 // expect any further events. If we are, then this might be called again,
664 // so wait for OnAfterLastPart instead.
665 nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest);
666 if (!multiPartChannel) {
667 mIsFinished = true;
670 return NS_OK;
673 //-----------------------------------------------------------------------------
674 // EarlyHintPreloader::nsIStreamListener
675 //-----------------------------------------------------------------------------
677 // Implementation copied from DocumentLoadListener::OnDataAvailable
678 // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2530-2549
679 NS_IMETHODIMP
680 EarlyHintPreloader::OnDataAvailable(nsIRequest* aRequest,
681 nsIInputStream* aInputStream,
682 uint64_t aOffset, uint32_t aCount) {
683 AssertIsOnMainThread();
684 LOG(("EarlyHintPreloader::OnDataAvailable [this=%p]\n", this));
685 // This isn't supposed to happen, since we suspended the channel, but
686 // sometimes Suspend just doesn't work. This can happen when we're routing
687 // through nsUnknownDecoder to sniff the content type, and it doesn't handle
688 // being suspended. Let's just store the data and manually forward it to our
689 // redirected channel when it's ready.
690 nsCString data;
691 nsresult rv = NS_ReadInputStreamToString(aInputStream, data, aCount);
692 NS_ENSURE_SUCCESS(rv, rv);
694 mStreamListenerFunctions.AppendElement(
695 AsVariant(OnDataAvailableParams{aRequest, data, aOffset, aCount}));
697 return NS_OK;
700 //-----------------------------------------------------------------------------
701 // EarlyHintPreloader::nsIMultiPartChannelListener
702 //-----------------------------------------------------------------------------
704 NS_IMETHODIMP
705 EarlyHintPreloader::OnAfterLastPart(nsresult aStatus) {
706 LOG(("EarlyHintPreloader::OnAfterLastPart [this=%p]", this));
707 mStreamListenerFunctions.AppendElement(
708 AsVariant(OnAfterLastPartParams{aStatus}));
709 mIsFinished = true;
710 return NS_OK;
713 //-----------------------------------------------------------------------------
714 // EarlyHintPreloader::nsIChannelEventSink
715 //-----------------------------------------------------------------------------
717 NS_IMETHODIMP
718 EarlyHintPreloader::AsyncOnChannelRedirect(
719 nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
720 nsIAsyncVerifyRedirectCallback* callback) {
721 LOG(("EarlyHintPreloader::AsyncOnChannelRedirect [this=%p]", this));
722 nsCOMPtr<nsIURI> newURI;
723 nsresult rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI));
724 NS_ENSURE_SUCCESS(rv, rv);
726 rv = aNewChannel->GetURI(getter_AddRefs(newURI));
727 if (NS_FAILED(rv)) {
728 callback->OnRedirectVerifyCallback(rv);
729 return NS_OK;
732 // HTTP request headers are not automatically forwarded to the new channel.
733 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel);
734 NS_ENSURE_STATE(httpChannel);
736 rv = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false);
737 MOZ_ASSERT(NS_SUCCEEDED(rv));
739 // Assign to mChannel after we get notification about success of the
740 // redirect in OnRedirectResult.
741 mRedirectChannel = aNewChannel;
743 callback->OnRedirectVerifyCallback(NS_OK);
744 return NS_OK;
747 //-----------------------------------------------------------------------------
748 // EarlyHintPreloader::nsIRedirectResultListener
749 //-----------------------------------------------------------------------------
751 NS_IMETHODIMP
752 EarlyHintPreloader::OnRedirectResult(nsresult aStatus) {
753 LOG(("EarlyHintPreloader::OnRedirectResult [this=%p] aProceeding=0x%" PRIx32,
754 this, static_cast<uint32_t>(aStatus)));
755 if (NS_SUCCEEDED(aStatus) && mRedirectChannel) {
756 mChannel = mRedirectChannel;
759 mRedirectChannel = nullptr;
761 return NS_OK;
764 //-----------------------------------------------------------------------------
765 // EarlyHintPreloader::nsINamed
766 //-----------------------------------------------------------------------------
768 NS_IMETHODIMP
769 EarlyHintPreloader::GetName(nsACString& aName) {
770 aName.AssignLiteral("EarlyHintPreloader");
771 return NS_OK;
774 //-----------------------------------------------------------------------------
775 // EarlyHintPreloader::nsITimerCallback
776 //-----------------------------------------------------------------------------
778 NS_IMETHODIMP
779 EarlyHintPreloader::Notify(nsITimer* timer) {
780 // Death grip, because we will most likely remove the last reference when
781 // deleting us from the EarlyHintRegistrar
782 RefPtr<EarlyHintPreloader> deathGrip(this);
784 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate();
785 registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId());
787 mTimer = nullptr;
788 mRedirectChannel = nullptr;
789 if (mChannel) {
790 if (mSuspended) {
791 mChannel->Resume();
793 mChannel->CancelWithReason(NS_ERROR_ABORT, "parent-connect-timeout"_ns);
794 mChannel = nullptr;
796 SetState(ePreloaderTimeout);
798 return NS_OK;
801 //-----------------------------------------------------------------------------
802 // EarlyHintPreloader::nsIInterfaceRequestor
803 //-----------------------------------------------------------------------------
805 NS_IMETHODIMP
806 EarlyHintPreloader::GetInterface(const nsIID& aIID, void** aResult) {
807 if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
808 NS_ADDREF_THIS();
809 *aResult = static_cast<nsIChannelEventSink*>(this);
810 return NS_OK;
813 if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) {
814 NS_ADDREF_THIS();
815 *aResult = static_cast<nsIRedirectResultListener*>(this);
816 return NS_OK;
819 return NS_ERROR_NO_INTERFACE;
822 void EarlyHintPreloader::CollectResourcesTypeTelemetry(
823 ASDestination aASDestination) {
824 if (aASDestination == ASDestination::DESTINATION_FONT) {
825 glean::netwerk::early_hints.Get("font"_ns).Add(1);
826 } else if (aASDestination == ASDestination::DESTINATION_SCRIPT) {
827 glean::netwerk::early_hints.Get("script"_ns).Add(1);
828 } else if (aASDestination == ASDestination::DESTINATION_STYLE) {
829 glean::netwerk::early_hints.Get("stylesheet"_ns).Add(1);
830 } else if (aASDestination == ASDestination::DESTINATION_IMAGE) {
831 glean::netwerk::early_hints.Get("image"_ns).Add(1);
832 } else if (aASDestination == ASDestination::DESTINATION_FETCH) {
833 glean::netwerk::early_hints.Get("fetch"_ns).Add(1);
834 } else {
835 glean::netwerk::early_hints.Get("other"_ns).Add(1);
838 } // namespace mozilla::net