EarlyHintPreloader.cpp (31436B)
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/. */ 4 5 #include "EarlyHintPreloader.h" 6 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/NetwerkProtocolHttpMetrics.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 "nsAttrValue.h" 27 #include "nsCOMPtr.h" 28 #include "nsContentPolicyUtils.h" 29 #include "nsContentSecurityManager.h" 30 #include "nsContentUtils.h" 31 #include "nsDebug.h" 32 #include "nsHttpChannel.h" 33 #include "nsIAsyncVerifyRedirectCallback.h" 34 #include "nsIChannel.h" 35 #include "nsIContentSecurityPolicy.h" 36 #include "nsIHttpChannel.h" 37 #include "nsIInputStream.h" 38 #include "nsILoadContext.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 "ParentChannelListener.h" 47 #include "nsIChannel.h" 48 #include "nsInterfaceRequestorAgg.h" 49 50 // 51 // To enable logging (see mozilla/Logging.h for full details): 52 // 53 // set MOZ_LOG=EarlyHint:5 54 // set MOZ_LOG_FILE=earlyhint.log 55 // 56 // this enables LogLevel::Debug level information and places all output in 57 // the file earlyhint.log 58 // 59 static mozilla::LazyLogModule gEarlyHintLog("EarlyHint"); 60 61 #undef LOG 62 #define LOG(args) MOZ_LOG(gEarlyHintLog, mozilla::LogLevel::Debug, args) 63 64 #undef LOG_ENABLED 65 #define LOG_ENABLED() MOZ_LOG_TEST(gEarlyHintLog, mozilla::LogLevel::Debug) 66 67 namespace mozilla::net { 68 69 namespace { 70 // This id uniquely identifies each early hint preloader in the 71 // EarlyHintRegistrar. Must only be accessed from main thread. 72 static uint64_t gEarlyHintPreloaderId{0}; 73 } // namespace 74 75 //============================================================================= 76 // OngoingEarlyHints 77 //============================================================================= 78 79 void OngoingEarlyHints::CancelAll(const nsACString& aReason) { 80 for (auto& preloader : mPreloaders) { 81 preloader->CancelChannel(NS_ERROR_ABORT, aReason, /* aDeleteEntry */ true); 82 } 83 mPreloaders.Clear(); 84 mStartedPreloads.Clear(); 85 } 86 87 bool OngoingEarlyHints::Contains(const PreloadHashKey& aKey) { 88 return mStartedPreloads.Contains(aKey); 89 } 90 91 bool OngoingEarlyHints::Add(const PreloadHashKey& aKey, 92 RefPtr<EarlyHintPreloader> aPreloader) { 93 if (!mStartedPreloads.Contains(aKey)) { 94 mStartedPreloads.Insert(aKey); 95 mPreloaders.AppendElement(aPreloader); 96 return true; 97 } 98 return false; 99 } 100 101 void OngoingEarlyHints::RegisterLinksAndGetConnectArgs( 102 dom::ContentParentId aCpId, nsTArray<EarlyHintConnectArgs>& aOutLinks) { 103 // register all channels before returning 104 for (auto& preload : mPreloaders) { 105 EarlyHintConnectArgs args; 106 if (preload->Register(aCpId, args)) { 107 aOutLinks.AppendElement(std::move(args)); 108 } 109 } 110 } 111 112 //============================================================================= 113 // EarlyHintPreloader 114 //============================================================================= 115 116 EarlyHintPreloader::EarlyHintPreloader() { 117 AssertIsOnMainThread(); 118 mConnectArgs.earlyHintPreloaderId() = ++gEarlyHintPreloaderId; 119 }; 120 121 EarlyHintPreloader::~EarlyHintPreloader() { 122 if (mTimer) { 123 mTimer->Cancel(); 124 mTimer = nullptr; 125 } 126 } 127 128 /* static */ 129 Maybe<PreloadHashKey> EarlyHintPreloader::GenerateHashKey( 130 ASDestination aAs, nsIURI* aURI, nsIPrincipal* aPrincipal, 131 CORSMode aCorsMode, bool aIsModulepreload) { 132 if (aIsModulepreload) { 133 return Some(PreloadHashKey::CreateAsScript( 134 aURI, aCorsMode, JS::loader::ScriptKind::eModule)); 135 } 136 if (aAs == ASDestination::DESTINATION_FONT && aCorsMode != CORS_NONE) { 137 return Some(PreloadHashKey::CreateAsFont(aURI, aCorsMode)); 138 } 139 if (aAs == ASDestination::DESTINATION_IMAGE) { 140 return Some(PreloadHashKey::CreateAsImage(aURI, aPrincipal, aCorsMode)); 141 } 142 if (aAs == ASDestination::DESTINATION_SCRIPT) { 143 return Some(PreloadHashKey::CreateAsScript( 144 aURI, aCorsMode, JS::loader::ScriptKind::eClassic)); 145 } 146 if (aAs == ASDestination::DESTINATION_STYLE) { 147 return Some(PreloadHashKey::CreateAsStyle( 148 aURI, aPrincipal, aCorsMode, 149 css::SheetParsingMode::eAuthorSheetFeatures)); 150 } 151 if (aAs == ASDestination::DESTINATION_FETCH && aCorsMode != CORS_NONE) { 152 return Some(PreloadHashKey::CreateAsFetch(aURI, aCorsMode)); 153 } 154 return Nothing(); 155 } 156 157 /* static */ 158 nsSecurityFlags EarlyHintPreloader::ComputeSecurityFlags(CORSMode aCORSMode, 159 ASDestination aAs) { 160 if (aAs == ASDestination::DESTINATION_FONT) { 161 return nsContentSecurityManager::ComputeSecurityFlags( 162 CORSMode::CORS_NONE, 163 nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS); 164 } 165 if (aAs == ASDestination::DESTINATION_IMAGE) { 166 return nsContentSecurityManager::ComputeSecurityFlags( 167 aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: 168 CORS_NONE_MAPS_TO_INHERITED_CONTEXT) | 169 nsILoadInfo::SEC_ALLOW_CHROME; 170 } 171 if (aAs == ASDestination::DESTINATION_SCRIPT) { 172 return nsContentSecurityManager::ComputeSecurityFlags( 173 aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: 174 CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS) | 175 nsILoadInfo::SEC_ALLOW_CHROME; 176 } 177 if (aAs == ASDestination::DESTINATION_STYLE) { 178 return nsContentSecurityManager::ComputeSecurityFlags( 179 aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: 180 CORS_NONE_MAPS_TO_INHERITED_CONTEXT) | 181 nsILoadInfo::SEC_ALLOW_CHROME; 182 ; 183 } 184 if (aAs == ASDestination::DESTINATION_FETCH) { 185 return nsContentSecurityManager::ComputeSecurityFlags( 186 aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: 187 CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS); 188 } 189 MOZ_ASSERT(false, "Unexpected ASDestination"); 190 return nsContentSecurityManager::ComputeSecurityFlags( 191 CORSMode::CORS_NONE, 192 nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS); 193 } 194 195 // static 196 void EarlyHintPreloader::MaybeCreateAndInsertPreload( 197 OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aLinkHeader, 198 nsIURI* aBaseURI, nsIPrincipal* aPrincipal, 199 nsICookieJarSettings* aCookieJarSettings, 200 const nsACString& aResponseReferrerPolicy, const nsACString& aCSPHeader, 201 uint64_t aBrowsingContextID, 202 dom::CanonicalBrowsingContext* aLoadingBrowsingContext, 203 bool aIsModulepreload) { 204 nsAttrValue as; 205 ParseAsValue(aLinkHeader.mAs, as); 206 207 ASDestination destination = static_cast<ASDestination>(as.GetEnumValue()); 208 209 if (!StaticPrefs::network_early_hints_enabled()) { 210 return; 211 } 212 213 if (destination == ASDestination::DESTINATION_INVALID && !aIsModulepreload) { 214 // return early when it's definitly not an asset type we preload 215 // would be caught later as well, e.g. when creating the PreloadHashKey 216 return; 217 } 218 219 if (destination == ASDestination::DESTINATION_FONT && 220 !gfxPlatform::GetPlatform()->DownloadableFontsEnabled()) { 221 return; 222 } 223 224 nsCOMPtr<nsIURI> uri; 225 NS_ENSURE_SUCCESS_VOID( 226 NS_NewURI(getter_AddRefs(uri), aLinkHeader.mHref, nullptr, aBaseURI)); 227 // The link relation may apply to a different resource, specified 228 // in the anchor parameter. For the link relations supported so far, 229 // we simply abort if the link applies to a resource different to the 230 // one we've loaded 231 if (!nsContentUtils::LinkContextIsURI(aLinkHeader.mAnchor, uri)) { 232 return; 233 } 234 235 // only preload secure context urls 236 if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) { 237 return; 238 } 239 240 CORSMode corsMode = dom::Element::StringToCORSMode(aLinkHeader.mCrossOrigin); 241 242 Maybe<PreloadHashKey> hashKey = 243 GenerateHashKey(destination, uri, aPrincipal, corsMode, aIsModulepreload); 244 if (!hashKey) { 245 return; 246 } 247 248 if (aOngoingEarlyHints->Contains(*hashKey)) { 249 return; 250 } 251 252 nsContentPolicyType contentPolicyType = 253 aIsModulepreload ? (IsScriptLikeOrInvalid(aLinkHeader.mAs) 254 ? nsContentPolicyType::TYPE_SCRIPT 255 : nsContentPolicyType::TYPE_INVALID) 256 : AsValueToContentPolicy(as); 257 258 if (contentPolicyType == nsContentPolicyType::TYPE_INVALID) { 259 return; 260 } 261 262 dom::ReferrerPolicy linkReferrerPolicy = 263 dom::ReferrerInfo::ReferrerPolicyAttributeFromString( 264 aLinkHeader.mReferrerPolicy); 265 266 dom::ReferrerPolicy responseReferrerPolicy = 267 dom::ReferrerInfo::ReferrerPolicyAttributeFromString( 268 NS_ConvertUTF8toUTF16(aResponseReferrerPolicy)); 269 270 // The early hint may have two referrer policies, one from the response header 271 // and one from the link element. 272 // 273 // For example, in this server response: 274 // HTTP/1.1 103 Early Hints 275 // Referrer-Policy : origin 276 // Link: </style.css>; rel=preload; as=style referrerpolicy=no-referrer 277 // 278 // The link header referrer policy, if present, will take precedence over 279 // the response referrer policy 280 dom::ReferrerPolicy finalReferrerPolicy = responseReferrerPolicy; 281 if (linkReferrerPolicy != dom::ReferrerPolicy::_empty) { 282 finalReferrerPolicy = linkReferrerPolicy; 283 } 284 nsCOMPtr<nsIReferrerInfo> referrerInfo = 285 new dom::ReferrerInfo(aBaseURI, finalReferrerPolicy); 286 287 RefPtr<EarlyHintPreloader> earlyHintPreloader = new EarlyHintPreloader(); 288 289 earlyHintPreloader->mLoadContext = aLoadingBrowsingContext; 290 291 // Security flags for modulepreload's request mode are computed here directly 292 // until full support for worker destinations can be added. 293 // 294 // Implements "To fetch a single module script," 295 // Step 9. If destination is "worker", "sharedworker", or "serviceworker", 296 // and the top-level module fetch flag is set, then set request's 297 // mode to "same-origin". 298 nsSecurityFlags securityFlags = 299 aIsModulepreload 300 ? ((aLinkHeader.mAs.LowerCaseEqualsASCII("worker") || 301 aLinkHeader.mAs.LowerCaseEqualsASCII("sharedworker") || 302 aLinkHeader.mAs.LowerCaseEqualsASCII("serviceworker")) 303 ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED 304 : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) | 305 (corsMode == CORS_USE_CREDENTIALS 306 ? nsILoadInfo::SEC_COOKIES_INCLUDE 307 : nsILoadInfo::SEC_COOKIES_SAME_ORIGIN) | 308 nsILoadInfo::SEC_ALLOW_CHROME 309 : EarlyHintPreloader::ComputeSecurityFlags(corsMode, destination); 310 311 // Verify that the resource should be loaded. 312 // This isn't the ideal way to test the resource against the CSP. 313 // The problem comes from the fact that at the stage of Early Hint 314 // processing we have not yet created a document where we would normally store 315 // the CSP. 316 317 // First we will create a load info. 318 Result<nsCOMPtr<nsILoadInfo>, nsresult> maybeLoadInfo = LoadInfo::Create( 319 aPrincipal, // loading principal 320 aPrincipal, // triggering principal 321 nullptr /* aLoadingContext node */, 322 nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, contentPolicyType); 323 if (NS_WARN_IF(maybeLoadInfo.isErr())) { 324 return; 325 } 326 nsCOMPtr<nsILoadInfo> secCheckLoadInfo = maybeLoadInfo.unwrap(); 327 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); 338 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. 353 354 mozilla::ipc::PrincipalInfo principalInfo; 355 rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); 356 NS_ENSURE_SUCCESS_VOID(rv); 357 dom::ClientInfo clientInfo(nsID::GenerateUUID(), Nothing(), 358 dom::ClientType::Window, principalInfo, 359 TimeStamp::Now(), ""_ns, dom::FrameType::None); 360 361 // Our newly-created CSP is set on the ClientInfo via the indirect route of 362 // first serializing to CSPInfo 363 ipc::CSPInfo cspInfo; 364 rv = CSPToCSPInfo(csp, &cspInfo); 365 NS_ENSURE_SUCCESS_VOID(rv); 366 367 ipc::PolicyContainerArgs policyContainerArgs; 368 policyContainerArgs.csp() = Some(cspInfo); 369 370 clientInfo.SetPolicyContainerArgs(policyContainerArgs); 371 372 // This ClientInfo is then set on the new loadInfo. 373 // It can now be used to test the resource against the policy 374 secCheckLoadInfo->SetClientInfo(clientInfo); 375 } 376 377 dom::RequestMode requestMode = 378 nsContentSecurityManager::SecurityModeToRequestMode( 379 nsContentSecurityManager::ComputeSecurityMode(securityFlags)); 380 secCheckLoadInfo->SetRequestMode(Some(requestMode)); 381 382 int16_t shouldLoad = nsIContentPolicy::ACCEPT; 383 nsresult rv = NS_CheckContentLoadPolicy(uri, secCheckLoadInfo, &shouldLoad, 384 nsContentUtils::GetContentPolicy()); 385 386 if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) { 387 return; 388 } 389 390 NS_ENSURE_SUCCESS_VOID(earlyHintPreloader->OpenChannel( 391 uri, aPrincipal, securityFlags, contentPolicyType, referrerInfo, 392 aCookieJarSettings, aBrowsingContextID)); 393 394 earlyHintPreloader->SetLinkHeader(aLinkHeader); 395 396 DebugOnly<bool> result = 397 aOngoingEarlyHints->Add(*hashKey, earlyHintPreloader); 398 MOZ_ASSERT(result); 399 } 400 401 nsresult EarlyHintPreloader::OpenChannel( 402 nsIURI* aURI, nsIPrincipal* aPrincipal, nsSecurityFlags aSecurityFlags, 403 nsContentPolicyType aContentPolicyType, nsIReferrerInfo* aReferrerInfo, 404 nsICookieJarSettings* aCookieJarSettings, uint64_t aBrowsingContextID) { 405 MOZ_ASSERT(aContentPolicyType == nsContentPolicyType::TYPE_IMAGE || 406 aContentPolicyType == 407 nsContentPolicyType::TYPE_INTERNAL_FETCH_PRELOAD || 408 aContentPolicyType == nsContentPolicyType::TYPE_SCRIPT || 409 aContentPolicyType == nsContentPolicyType::TYPE_STYLESHEET || 410 aContentPolicyType == nsContentPolicyType::TYPE_FONT); 411 412 nsresult rv = 413 NS_NewChannel(getter_AddRefs(mChannel), aURI, aPrincipal, aSecurityFlags, 414 aContentPolicyType, aCookieJarSettings, 415 /* aPerformanceStorage */ nullptr, 416 /* aLoadGroup */ nullptr, 417 /* aCallbacks */ this, nsIRequest::LOAD_NORMAL); 418 419 NS_ENSURE_SUCCESS(rv, rv); 420 421 RefPtr<nsHttpChannel> httpChannelObject = do_QueryObject(mChannel); 422 if (!httpChannelObject) { 423 mChannel = nullptr; 424 return NS_ERROR_ABORT; 425 } 426 427 // configure HTTP specific stuff 428 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); 429 if (!httpChannel) { 430 mChannel = nullptr; 431 return NS_ERROR_ABORT; 432 } 433 DebugOnly<nsresult> success = httpChannel->SetReferrerInfo(aReferrerInfo); 434 MOZ_ASSERT(NS_SUCCEEDED(success)); 435 success = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false); 436 MOZ_ASSERT(NS_SUCCEEDED(success)); 437 438 mParentListener = new ParentChannelListener(this, nullptr); 439 440 PriorizeAsPreload(); 441 442 if (nsCOMPtr<nsIRaceCacheWithNetwork> rcwn = do_QueryInterface(httpChannel)) { 443 // Since this is an early hint, we should consult the cache first. 444 rcwn->SetAllowRacing(false); 445 } 446 447 rv = mChannel->AsyncOpen(mParentListener); 448 if (NS_FAILED(rv)) { 449 mParentListener = nullptr; 450 return rv; 451 } 452 453 SetState(ePreloaderOpened); 454 455 // Setting the BrowsingContextID here to let Early Hint requests show up in 456 // devtools. Normally that would automatically happen if we would pass the 457 // nsILoadGroup in ns_NewChannel above, but the nsILoadGroup is inaccessible 458 // here in the ParentProcess. The nsILoadGroup only exists in ContentProcess 459 // as part of the document and nsDocShell. It is also not yet determined which 460 // ContentProcess this load belongs to. 461 nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo(); 462 static_cast<LoadInfo*>(loadInfo.get()) 463 ->UpdateBrowsingContextID(aBrowsingContextID); 464 465 return NS_OK; 466 } 467 468 void EarlyHintPreloader::PriorizeAsPreload() { 469 nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL; 470 (void)mChannel->GetLoadFlags(&loadFlags); 471 (void)mChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND); 472 473 if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(mChannel)) { 474 (void)cos->AddClassFlags(nsIClassOfService::Unblocked); 475 } 476 } 477 478 void EarlyHintPreloader::SetLinkHeader(const LinkHeader& aLinkHeader) { 479 mConnectArgs.link() = aLinkHeader; 480 } 481 482 bool EarlyHintPreloader::IsFromContentParent(dom::ContentParentId aCpId) const { 483 return aCpId == mCpId; 484 } 485 486 bool EarlyHintPreloader::Register(dom::ContentParentId aCpId, 487 EarlyHintConnectArgs& aOut) { 488 mCpId = aCpId; 489 490 // Set minimum delay of 1ms to always start the timer after the function call 491 // completed. 492 nsresult rv = NS_NewTimerWithCallback( 493 getter_AddRefs(mTimer), this, 494 std::max(StaticPrefs::network_early_hints_parent_connect_timeout(), 495 (uint32_t)1), 496 nsITimer::TYPE_ONE_SHOT); 497 if (NS_FAILED(rv)) { 498 MOZ_ASSERT(!mTimer); 499 CancelChannel(NS_ERROR_ABORT, "new-timer-failed"_ns, 500 /* aDeleteEntry */ false); 501 return false; 502 } 503 504 // Create an entry in the redirect channel registrar 505 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate(); 506 registrar->RegisterEarlyHint(mConnectArgs.earlyHintPreloaderId(), this); 507 508 aOut = mConnectArgs; 509 return true; 510 } 511 512 nsresult EarlyHintPreloader::CancelChannel(nsresult aStatus, 513 const nsACString& aReason, 514 bool aDeleteEntry) { 515 LOG(("EarlyHintPreloader::CancelChannel [this=%p]\n", this)); 516 517 if (mTimer) { 518 mTimer->Cancel(); 519 mTimer = nullptr; 520 } 521 if (aDeleteEntry) { 522 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate(); 523 registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); 524 } 525 // clear redirect channel in case this channel is cleared between the call of 526 // EarlyHintPreloader::AsyncOnChannelRedirect and 527 // EarlyHintPreloader::OnRedirectResult 528 mRedirectChannel = nullptr; 529 if (mChannel) { 530 if (mSuspended) { 531 mChannel->Resume(); 532 } 533 mChannel->CancelWithReason(aStatus, aReason); 534 // Clearing mChannel is safe, because this EarlyHintPreloader is not in the 535 // EarlyHintRegistrar after this function call and we won't call 536 // SetHttpChannelFromEarlyHintPreloader nor OnStartRequest on mParent. 537 mChannel = nullptr; 538 SetState(ePreloaderCancelled); 539 } 540 return NS_OK; 541 } 542 543 void EarlyHintPreloader::OnParentReady(nsIParentChannel* aParent) { 544 AssertIsOnMainThread(); 545 MOZ_ASSERT(aParent); 546 LOG(("EarlyHintPreloader::OnParentReady [this=%p]\n", this)); 547 548 nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); 549 if (obs) { 550 obs->NotifyObservers(mChannel, "earlyhints-connectback", nullptr); 551 } 552 553 mParent = aParent; 554 555 if (mTimer) { 556 mTimer->Cancel(); 557 mTimer = nullptr; 558 } 559 560 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate(); 561 registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); 562 563 if (mOnStartRequestCalled) { 564 SetParentChannel(); 565 InvokeStreamListenerFunctions(); 566 } 567 } 568 569 void EarlyHintPreloader::SetParentChannel() { 570 RefPtr<HttpBaseChannel> channel = do_QueryObject(mChannel); 571 RefPtr<HttpChannelParent> parent = do_QueryObject(mParent); 572 parent->SetHttpChannelFromEarlyHintPreloader(channel); 573 } 574 575 // Adapted from 576 // https://searchfox.org/mozilla-central/rev/b4150d1c6fae0c51c522df2d2c939cf5ad331d4c/netwerk/ipc/DocumentLoadListener.cpp#1311 577 void EarlyHintPreloader::InvokeStreamListenerFunctions() { 578 AssertIsOnMainThread(); 579 RefPtr<EarlyHintPreloader> self(this); 580 581 LOG(( 582 "EarlyHintPreloader::InvokeStreamListenerFunctions [this=%p parent=%p]\n", 583 this, mParent.get())); 584 585 // If we failed to suspend the channel, then we might have received 586 // some messages while the redirected was being handled. 587 // Manually send them on now. 588 if (!mIsFinished) { 589 // This is safe to do, because OnStartRequest/OnStopRequest/OnDataAvailable 590 // are all called on the main thread. They can't be called until we worked 591 // through all functions in the streamListnerFunctions array. 592 mParentListener->SetListenerAfterRedirect(mParent); 593 } 594 nsTArray<StreamListenerFunction> streamListenerFunctions = 595 std::move(mStreamListenerFunctions); 596 597 ForwardStreamListenerFunctions(std::move(streamListenerFunctions), mParent); 598 599 // We don't expect to get new stream listener functions added 600 // via re-entrancy. If this ever happens, we should understand 601 // exactly why before allowing it. 602 NS_ASSERTION(mStreamListenerFunctions.IsEmpty(), 603 "Should not have added new stream listener function!"); 604 605 if (mChannel && mSuspended) { 606 mChannel->Resume(); 607 } 608 mChannel = nullptr; 609 mParent = nullptr; 610 mParentListener = nullptr; 611 612 SetState(ePreloaderUsed); 613 } 614 615 //----------------------------------------------------------------------------- 616 // EarlyHintPreloader::nsISupports 617 //----------------------------------------------------------------------------- 618 619 NS_IMPL_ISUPPORTS(EarlyHintPreloader, nsIRequestObserver, nsIStreamListener, 620 nsIChannelEventSink, nsIInterfaceRequestor, 621 nsIRedirectResultListener, nsIMultiPartChannelListener, 622 nsINamed, nsITimerCallback); 623 624 //----------------------------------------------------------------------------- 625 // EarlyHintPreloader::nsIStreamListener 626 //----------------------------------------------------------------------------- 627 628 // Implementation copied and adapted from DocumentLoadListener::OnStartRequest 629 // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2317-2508 630 NS_IMETHODIMP 631 EarlyHintPreloader::OnStartRequest(nsIRequest* aRequest) { 632 LOG(("EarlyHintPreloader::OnStartRequest [this=%p]\n", this)); 633 AssertIsOnMainThread(); 634 635 mOnStartRequestCalled = true; 636 637 nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest); 638 if (multiPartChannel) { 639 multiPartChannel->GetBaseChannel(getter_AddRefs(mChannel)); 640 } else { 641 mChannel = do_QueryInterface(aRequest); 642 } 643 MOZ_DIAGNOSTIC_ASSERT(mChannel); 644 645 nsresult status = NS_OK; 646 (void)aRequest->GetStatus(&status); 647 648 if (mParent) { 649 SetParentChannel(); 650 mParent->OnStartRequest(aRequest); 651 InvokeStreamListenerFunctions(); 652 } else { 653 // Don't suspend the chanel when the channel got cancelled with 654 // CancelChannel, because then OnStopRequest wouldn't get called and we 655 // wouldn't clean up the channel. 656 if (NS_SUCCEEDED(status)) { 657 mChannel->Suspend(); 658 mSuspended = true; 659 } 660 mStreamListenerFunctions.AppendElement( 661 AsVariant(OnStartRequestParams{aRequest})); 662 } 663 664 // return error after adding the OnStartRequest forward. The OnStartRequest 665 // failure has to be forwarded to listener, because they called AsyncOpen on 666 // this channel 667 return status; 668 } 669 670 // Implementation copied from DocumentLoadListener::OnStopRequest 671 // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2510-2528 672 NS_IMETHODIMP 673 EarlyHintPreloader::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { 674 AssertIsOnMainThread(); 675 LOG(("EarlyHintPreloader::OnStopRequest [this=%p]\n", this)); 676 mStreamListenerFunctions.AppendElement( 677 AsVariant(OnStopRequestParams{aRequest, aStatusCode})); 678 679 // If we're not a multi-part channel, then we're finished and we don't 680 // expect any further events. If we are, then this might be called again, 681 // so wait for OnAfterLastPart instead. 682 nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest); 683 if (!multiPartChannel) { 684 mIsFinished = true; 685 } 686 687 return NS_OK; 688 } 689 690 //----------------------------------------------------------------------------- 691 // EarlyHintPreloader::nsIStreamListener 692 //----------------------------------------------------------------------------- 693 694 // Implementation copied from DocumentLoadListener::OnDataAvailable 695 // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2530-2549 696 NS_IMETHODIMP 697 EarlyHintPreloader::OnDataAvailable(nsIRequest* aRequest, 698 nsIInputStream* aInputStream, 699 uint64_t aOffset, uint32_t aCount) { 700 AssertIsOnMainThread(); 701 LOG(("EarlyHintPreloader::OnDataAvailable [this=%p]\n", this)); 702 // This isn't supposed to happen, since we suspended the channel, but 703 // sometimes Suspend just doesn't work. This can happen when we're routing 704 // through nsUnknownDecoder to sniff the content type, and it doesn't handle 705 // being suspended. Let's just store the data and manually forward it to our 706 // redirected channel when it's ready. 707 nsCString data; 708 nsresult rv = NS_ReadInputStreamToString(aInputStream, data, aCount); 709 NS_ENSURE_SUCCESS(rv, rv); 710 711 mStreamListenerFunctions.AppendElement(AsVariant( 712 OnDataAvailableParams{aRequest, std::move(data), aOffset, aCount})); 713 714 return NS_OK; 715 } 716 717 //----------------------------------------------------------------------------- 718 // EarlyHintPreloader::nsIMultiPartChannelListener 719 //----------------------------------------------------------------------------- 720 721 NS_IMETHODIMP 722 EarlyHintPreloader::OnAfterLastPart(nsresult aStatus) { 723 LOG(("EarlyHintPreloader::OnAfterLastPart [this=%p]", this)); 724 mStreamListenerFunctions.AppendElement( 725 AsVariant(OnAfterLastPartParams{aStatus})); 726 mIsFinished = true; 727 return NS_OK; 728 } 729 730 //----------------------------------------------------------------------------- 731 // EarlyHintPreloader::nsIChannelEventSink 732 //----------------------------------------------------------------------------- 733 734 NS_IMETHODIMP 735 EarlyHintPreloader::AsyncOnChannelRedirect( 736 nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, 737 nsIAsyncVerifyRedirectCallback* callback) { 738 LOG(("EarlyHintPreloader::AsyncOnChannelRedirect [this=%p]", this)); 739 nsCOMPtr<nsIURI> newURI; 740 nsresult rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI)); 741 NS_ENSURE_SUCCESS(rv, rv); 742 743 rv = aNewChannel->GetURI(getter_AddRefs(newURI)); 744 if (NS_FAILED(rv)) { 745 callback->OnRedirectVerifyCallback(rv); 746 return NS_OK; 747 } 748 749 // HTTP request headers are not automatically forwarded to the new channel. 750 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel); 751 NS_ENSURE_STATE(httpChannel); 752 753 rv = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false); 754 MOZ_ASSERT(NS_SUCCEEDED(rv)); 755 756 // Assign to mChannel after we get notification about success of the 757 // redirect in OnRedirectResult. 758 mRedirectChannel = aNewChannel; 759 760 callback->OnRedirectVerifyCallback(NS_OK); 761 return NS_OK; 762 } 763 764 //----------------------------------------------------------------------------- 765 // EarlyHintPreloader::nsIRedirectResultListener 766 //----------------------------------------------------------------------------- 767 768 NS_IMETHODIMP 769 EarlyHintPreloader::OnRedirectResult(nsresult aStatus) { 770 LOG(("EarlyHintPreloader::OnRedirectResult [this=%p] aProceeding=0x%" PRIx32, 771 this, static_cast<uint32_t>(aStatus))); 772 if (NS_SUCCEEDED(aStatus) && mRedirectChannel) { 773 mChannel = mRedirectChannel; 774 } 775 776 mRedirectChannel = nullptr; 777 778 return NS_OK; 779 } 780 781 //----------------------------------------------------------------------------- 782 // EarlyHintPreloader::nsINamed 783 //----------------------------------------------------------------------------- 784 785 NS_IMETHODIMP 786 EarlyHintPreloader::GetName(nsACString& aName) { 787 aName.AssignLiteral("EarlyHintPreloader"); 788 return NS_OK; 789 } 790 791 //----------------------------------------------------------------------------- 792 // EarlyHintPreloader::nsITimerCallback 793 //----------------------------------------------------------------------------- 794 795 NS_IMETHODIMP 796 EarlyHintPreloader::Notify(nsITimer* timer) { 797 // Death grip, because we will most likely remove the last reference when 798 // deleting us from the EarlyHintRegistrar 799 RefPtr<EarlyHintPreloader> deathGrip(this); 800 801 RefPtr<EarlyHintRegistrar> registrar = EarlyHintRegistrar::GetOrCreate(); 802 registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); 803 804 mTimer = nullptr; 805 mRedirectChannel = nullptr; 806 if (mChannel) { 807 if (mSuspended) { 808 mChannel->Resume(); 809 } 810 mChannel->CancelWithReason(NS_ERROR_ABORT, "parent-connect-timeout"_ns); 811 #ifndef ANDROID 812 glean::netwerk::parent_connect_timeout.Add(1); 813 #endif 814 mChannel = nullptr; 815 } 816 SetState(ePreloaderTimeout); 817 818 return NS_OK; 819 } 820 821 //----------------------------------------------------------------------------- 822 // EarlyHintPreloader::nsIInterfaceRequestor 823 //----------------------------------------------------------------------------- 824 825 NS_IMETHODIMP 826 EarlyHintPreloader::GetInterface(const nsIID& aIID, void** aResult) { 827 if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { 828 NS_ADDREF_THIS(); 829 *aResult = static_cast<nsIChannelEventSink*>(this); 830 return NS_OK; 831 } 832 833 if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { 834 NS_ADDREF_THIS(); 835 *aResult = static_cast<nsIRedirectResultListener*>(this); 836 return NS_OK; 837 } 838 839 if (aIID.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext != nullptr) { 840 nsCOMPtr<nsILoadContext> loadContext = mLoadContext; 841 loadContext.forget(aResult); 842 return NS_OK; 843 } 844 845 return NS_ERROR_NO_INTERFACE; 846 } 847 } // namespace mozilla::net