tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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