/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "HTMLDNSPrefetch.h" #include "base/basictypes.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLLinkElement.h" #include "mozilla/dom/HTMLAnchorElement.h" #include "mozilla/net/NeckoCommon.h" #include "mozilla/net/NeckoChild.h" #include "mozilla/OriginAttributes.h" #include "mozilla/StoragePrincipalHelper.h" #include "nsURLHelper.h" #include "nsCOMPtr.h" #include "nsString.h" #include "nsNetUtil.h" #include "nsNetCID.h" #include "nsIProtocolHandler.h" #include "nsIDNSListener.h" #include "nsIWebProgressListener.h" #include "nsIWebProgress.h" #include "nsIDNSRecord.h" #include "nsIDNSService.h" #include "nsICancelable.h" #include "nsGkAtoms.h" #include "mozilla/dom/Document.h" #include "nsThreadUtils.h" #include "nsITimer.h" #include "nsIObserverService.h" #include "mozilla/Components.h" #include "mozilla/Preferences.h" #include "mozilla/StaticPrefs_network.h" using namespace mozilla::net; namespace mozilla::dom { class NoOpDNSListener final : public nsIDNSListener { // This class exists to give a safe callback no-op DNSListener public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIDNSLISTENER NoOpDNSListener() = default; private: ~NoOpDNSListener() = default; }; NS_IMPL_ISUPPORTS(NoOpDNSListener, nsIDNSListener) NS_IMETHODIMP NoOpDNSListener::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec, nsresult status) { return NS_OK; } // This is just a (size) optimization and could be avoided by storing the // SupportsDNSPrefetch pointer of the element in the prefetch queue, but given // we need this for GetURIForDNSPrefetch... static SupportsDNSPrefetch& ToSupportsDNSPrefetch(Element& aElement) { if (auto* link = HTMLLinkElement::FromNode(aElement)) { return *link; } auto* anchor = HTMLAnchorElement::FromNode(aElement); MOZ_DIAGNOSTIC_ASSERT(anchor); return *anchor; } nsIURI* SupportsDNSPrefetch::GetURIForDNSPrefetch(Element& aElement) { MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == this); if (auto* link = HTMLLinkElement::FromNode(aElement)) { return link->GetURI(); } auto* anchor = HTMLAnchorElement::FromNode(aElement); MOZ_DIAGNOSTIC_ASSERT(anchor); return anchor->GetURI(); } class DeferredDNSPrefetches final : public nsIWebProgressListener, public nsSupportsWeakReference, public nsIObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSIWEBPROGRESSLISTENER NS_DECL_NSIOBSERVER DeferredDNSPrefetches(); void Activate(); nsresult Add(nsIDNSService::DNSFlags flags, SupportsDNSPrefetch&, Element&); void RemoveUnboundLinks(); private: ~DeferredDNSPrefetches(); void Flush(); void SubmitQueue(); void SubmitQueueEntry(Element&, nsIDNSService::DNSFlags aFlags); uint16_t mHead; uint16_t mTail; uint32_t mActiveLoaderCount; nsCOMPtr mTimer; bool mTimerArmed; static void Tick(nsITimer* aTimer, void* aClosure); static const int sMaxDeferred = 512; // keep power of 2 for masking static const int sMaxDeferredMask = (sMaxDeferred - 1); struct deferred_entry { nsIDNSService::DNSFlags mFlags; // SupportsDNSPrefetch clears this raw pointer in Destroyed(). Element* mElement; } mEntries[sMaxDeferred]; }; static NS_DEFINE_CID(kDNSServiceCID, NS_DNSSERVICE_CID); static bool sInitialized = false; static nsIDNSService* sDNSService = nullptr; static DeferredDNSPrefetches* sPrefetches = nullptr; static NoOpDNSListener* sDNSListener = nullptr; nsresult HTMLDNSPrefetch::Initialize() { if (sInitialized) { NS_WARNING("Initialize() called twice"); return NS_OK; } sPrefetches = new DeferredDNSPrefetches(); NS_ADDREF(sPrefetches); sDNSListener = new NoOpDNSListener(); NS_ADDREF(sDNSListener); sPrefetches->Activate(); if (IsNeckoChild()) NeckoChild::InitNeckoChild(); sInitialized = true; return NS_OK; } nsresult HTMLDNSPrefetch::Shutdown() { if (!sInitialized) { NS_WARNING("Not Initialized"); return NS_OK; } sInitialized = false; NS_IF_RELEASE(sDNSService); NS_IF_RELEASE(sPrefetches); NS_IF_RELEASE(sDNSListener); return NS_OK; } static bool EnsureDNSService() { if (sDNSService) { return true; } NS_IF_RELEASE(sDNSService); nsresult rv; rv = CallGetService(kDNSServiceCID, &sDNSService); if (NS_FAILED(rv)) { return false; } return !!sDNSService; } bool HTMLDNSPrefetch::IsAllowed(Document* aDocument) { // There is no need to do prefetch on non UI scenarios such as XMLHttpRequest. return aDocument->IsDNSPrefetchAllowed() && aDocument->GetWindow(); } static nsIDNSService::DNSFlags GetDNSFlagsFromElement(Element& aElement) { nsIChannel* channel = aElement.OwnerDoc()->GetChannel(); if (!channel) { return nsIDNSService::RESOLVE_DEFAULT_FLAGS; } return nsIDNSService::GetFlagsFromTRRMode(channel->GetTRRMode()); } nsIDNSService::DNSFlags HTMLDNSPrefetch::PriorityToDNSServiceFlags( Priority aPriority) { switch (aPriority) { case Priority::Low: return nsIDNSService::RESOLVE_PRIORITY_LOW; case Priority::Medium: return nsIDNSService::RESOLVE_PRIORITY_MEDIUM; case Priority::High: return nsIDNSService::RESOLVE_DEFAULT_FLAGS; } MOZ_ASSERT_UNREACHABLE("Unknown priority"); return nsIDNSService::RESOLVE_DEFAULT_FLAGS; } nsresult HTMLDNSPrefetch::Prefetch(SupportsDNSPrefetch& aSupports, Element& aElement, Priority aPriority) { MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == &aSupports); if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) { return NS_ERROR_NOT_AVAILABLE; } return sPrefetches->Add( GetDNSFlagsFromElement(aElement) | PriorityToDNSServiceFlags(aPriority), aSupports, aElement); } nsresult HTMLDNSPrefetch::Prefetch( const nsAString& hostname, bool isHttps, const OriginAttributes& aPartitionedPrincipalOriginAttributes, nsIDNSService::DNSFlags flags) { if (IsNeckoChild()) { // We need to check IsEmpty() because net_IsValidHostName() // considers empty strings to be valid hostnames if (!hostname.IsEmpty() && net_IsValidHostName(NS_ConvertUTF16toUTF8(hostname))) { // during shutdown gNeckoChild might be null if (gNeckoChild) { gNeckoChild->SendHTMLDNSPrefetch( hostname, isHttps, aPartitionedPrincipalOriginAttributes, flags); } } return NS_OK; } if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) return NS_ERROR_NOT_AVAILABLE; nsCOMPtr tmpOutstanding; nsresult rv = sDNSService->AsyncResolveNative( NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_DEFAULT, flags | nsIDNSService::RESOLVE_SPECULATE, nullptr, sDNSListener, nullptr, aPartitionedPrincipalOriginAttributes, getter_AddRefs(tmpOutstanding)); if (NS_FAILED(rv)) { return rv; } if (StaticPrefs::network_dns_upgrade_with_https_rr() || StaticPrefs::network_dns_use_https_rr_as_altsvc()) { Unused << sDNSService->AsyncResolveNative( NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_HTTPSSVC, flags | nsIDNSService::RESOLVE_SPECULATE, nullptr, sDNSListener, nullptr, aPartitionedPrincipalOriginAttributes, getter_AddRefs(tmpOutstanding)); } return NS_OK; } nsresult HTMLDNSPrefetch::Prefetch( const nsAString& hostname, bool isHttps, const OriginAttributes& aPartitionedPrincipalOriginAttributes, nsIRequest::TRRMode aMode, Priority aPriority) { return Prefetch(hostname, isHttps, aPartitionedPrincipalOriginAttributes, nsIDNSService::GetFlagsFromTRRMode(aMode) | PriorityToDNSServiceFlags(aPriority)); } nsresult HTMLDNSPrefetch::CancelPrefetch(SupportsDNSPrefetch& aSupports, Element& aElement, Priority aPriority, nsresult aReason) { MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == &aSupports); if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) { return NS_ERROR_NOT_AVAILABLE; } nsIDNSService::DNSFlags flags = GetDNSFlagsFromElement(aElement) | PriorityToDNSServiceFlags(aPriority); nsIURI* uri = aSupports.GetURIForDNSPrefetch(aElement); if (!uri) { return NS_OK; } nsAutoCString hostname; uri->GetAsciiHost(hostname); nsAutoString protocol; bool isHttps = uri->SchemeIs("https"); OriginAttributes oa; StoragePrincipalHelper::GetOriginAttributesForNetworkState( aElement.OwnerDoc(), oa); return CancelPrefetch(NS_ConvertUTF8toUTF16(hostname), isHttps, oa, flags, aReason); } nsresult HTMLDNSPrefetch::CancelPrefetch( const nsAString& hostname, bool isHttps, const OriginAttributes& aPartitionedPrincipalOriginAttributes, nsIDNSService::DNSFlags flags, nsresult aReason) { // Forward this request to Necko Parent if we're a child process if (IsNeckoChild()) { // We need to check IsEmpty() because net_IsValidHostName() // considers empty strings to be valid hostnames if (!hostname.IsEmpty() && net_IsValidHostName(NS_ConvertUTF16toUTF8(hostname))) { // during shutdown gNeckoChild might be null if (gNeckoChild) { gNeckoChild->SendCancelHTMLDNSPrefetch( hostname, isHttps, aPartitionedPrincipalOriginAttributes, flags, aReason); } } return NS_OK; } if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) { return NS_ERROR_NOT_AVAILABLE; } // Forward cancellation to DNS service nsresult rv = sDNSService->CancelAsyncResolveNative( NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_DEFAULT, flags | nsIDNSService::RESOLVE_SPECULATE, nullptr, // AdditionalInfo sDNSListener, aReason, aPartitionedPrincipalOriginAttributes); if (StaticPrefs::network_dns_upgrade_with_https_rr() || StaticPrefs::network_dns_use_https_rr_as_altsvc()) { Unused << sDNSService->CancelAsyncResolveNative( NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_HTTPSSVC, flags | nsIDNSService::RESOLVE_SPECULATE, nullptr, // AdditionalInfo sDNSListener, aReason, aPartitionedPrincipalOriginAttributes); } return rv; } nsresult HTMLDNSPrefetch::CancelPrefetch( const nsAString& hostname, bool isHttps, const OriginAttributes& aPartitionedPrincipalOriginAttributes, nsIRequest::TRRMode aTRRMode, Priority aPriority, nsresult aReason) { return CancelPrefetch(hostname, isHttps, aPartitionedPrincipalOriginAttributes, nsIDNSService::GetFlagsFromTRRMode(aTRRMode) | PriorityToDNSServiceFlags(aPriority), aReason); } void HTMLDNSPrefetch::ElementDestroyed(Element& aElement, SupportsDNSPrefetch& aSupports) { MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == &aSupports); MOZ_ASSERT(aSupports.IsInDNSPrefetch()); if (sPrefetches) { // Clean up all the possible links at once. sPrefetches->RemoveUnboundLinks(); } } void SupportsDNSPrefetch::TryDNSPrefetch(Element& aOwner) { MOZ_ASSERT(aOwner.IsInComposedDoc()); if (HTMLDNSPrefetch::IsAllowed(aOwner.OwnerDoc())) { HTMLDNSPrefetch::Prefetch(*this, aOwner, HTMLDNSPrefetch::Priority::Low); } } void SupportsDNSPrefetch::CancelDNSPrefetch(Element& aOwner) { // If prefetch was deferred, clear flag and move on if (mDNSPrefetchDeferred) { mDNSPrefetchDeferred = false; // Else if prefetch was requested, clear flag and send cancellation } else if (mDNSPrefetchRequested) { mDNSPrefetchRequested = false; // Possible that hostname could have changed since binding, but since this // covers common cases, most DNS prefetch requests will be canceled HTMLDNSPrefetch::CancelPrefetch( *this, aOwner, HTMLDNSPrefetch::Priority::Low, NS_ERROR_ABORT); } } DeferredDNSPrefetches::DeferredDNSPrefetches() : mHead(0), mTail(0), mActiveLoaderCount(0), mTimerArmed(false) { mTimer = NS_NewTimer(); } DeferredDNSPrefetches::~DeferredDNSPrefetches() { if (mTimerArmed) { mTimerArmed = false; mTimer->Cancel(); } Flush(); } NS_IMPL_ISUPPORTS(DeferredDNSPrefetches, nsIWebProgressListener, nsISupportsWeakReference, nsIObserver) void DeferredDNSPrefetches::Flush() { for (; mHead != mTail; mTail = (mTail + 1) & sMaxDeferredMask) { Element* element = mEntries[mTail].mElement; if (element) { ToSupportsDNSPrefetch(*element).ClearIsInDNSPrefetch(); } mEntries[mTail].mElement = nullptr; } } nsresult DeferredDNSPrefetches::Add(nsIDNSService::DNSFlags flags, SupportsDNSPrefetch& aSupports, Element& aElement) { // The FIFO has no lock, so it can only be accessed on main thread NS_ASSERTION(NS_IsMainThread(), "DeferredDNSPrefetches::Add must be on main thread"); aSupports.DNSPrefetchRequestDeferred(); if (((mHead + 1) & sMaxDeferredMask) == mTail) { return NS_ERROR_DNS_LOOKUP_QUEUE_FULL; } aSupports.SetIsInDNSPrefetch(); mEntries[mHead].mFlags = flags; mEntries[mHead].mElement = &aElement; mHead = (mHead + 1) & sMaxDeferredMask; if (!mActiveLoaderCount && !mTimerArmed && mTimer) { mTimerArmed = true; mTimer->InitWithNamedFuncCallback( Tick, this, 2000, nsITimer::TYPE_ONE_SHOT, "HTMLDNSPrefetch::DeferredDNSPrefetches::Tick"); } return NS_OK; } void DeferredDNSPrefetches::SubmitQueue() { NS_ASSERTION(NS_IsMainThread(), "DeferredDNSPrefetches::SubmitQueue must be on main thread"); if (!EnsureDNSService()) { return; } for (; mHead != mTail; mTail = (mTail + 1) & sMaxDeferredMask) { Element* element = mEntries[mTail].mElement; if (!element) { continue; } SubmitQueueEntry(*element, mEntries[mTail].mFlags); mEntries[mTail].mElement = nullptr; } if (mTimerArmed) { mTimerArmed = false; mTimer->Cancel(); } } void DeferredDNSPrefetches::SubmitQueueEntry(Element& aElement, nsIDNSService::DNSFlags aFlags) { auto& supports = ToSupportsDNSPrefetch(aElement); supports.ClearIsInDNSPrefetch(); // Only prefetch here if request was deferred and deferral not cancelled if (!supports.IsDNSPrefetchRequestDeferred()) { return; } nsIURI* uri = supports.GetURIForDNSPrefetch(aElement); if (!uri) { return; } nsAutoCString hostName; uri->GetAsciiHost(hostName); if (hostName.IsEmpty()) { return; } bool isLocalResource = false; nsresult rv = NS_URIChainHasFlags( uri, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &isLocalResource); if (NS_FAILED(rv) || isLocalResource) { return; } OriginAttributes oa; StoragePrincipalHelper::GetOriginAttributesForNetworkState( aElement.OwnerDoc(), oa); bool isHttps = uri->SchemeIs("https"); if (IsNeckoChild()) { // during shutdown gNeckoChild might be null if (gNeckoChild) { gNeckoChild->SendHTMLDNSPrefetch(NS_ConvertUTF8toUTF16(hostName), isHttps, oa, mEntries[mTail].mFlags); } } else { nsCOMPtr tmpOutstanding; rv = sDNSService->AsyncResolveNative( hostName, nsIDNSService::RESOLVE_TYPE_DEFAULT, mEntries[mTail].mFlags | nsIDNSService::RESOLVE_SPECULATE, nullptr, sDNSListener, nullptr, oa, getter_AddRefs(tmpOutstanding)); if (NS_FAILED(rv)) { return; } // Fetch HTTPS RR if needed. if (StaticPrefs::network_dns_upgrade_with_https_rr() || StaticPrefs::network_dns_use_https_rr_as_altsvc()) { sDNSService->AsyncResolveNative( hostName, nsIDNSService::RESOLVE_TYPE_HTTPSSVC, mEntries[mTail].mFlags | nsIDNSService::RESOLVE_SPECULATE, nullptr, sDNSListener, nullptr, oa, getter_AddRefs(tmpOutstanding)); } } // Tell element that deferred prefetch was requested. supports.DNSPrefetchRequestStarted(); } void DeferredDNSPrefetches::Activate() { // Register as an observer for the document loader nsCOMPtr progress = components::DocLoader::Service(); if (progress) progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT); // Register as an observer for xpcom shutdown events so we can drop any // element refs nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) observerService->AddObserver(this, "xpcom-shutdown", true); } void DeferredDNSPrefetches::RemoveUnboundLinks() { uint16_t tail = mTail; while (mHead != tail) { Element* element = mEntries[tail].mElement; if (element && !element->IsInComposedDoc()) { ToSupportsDNSPrefetch(*element).ClearIsInDNSPrefetch(); mEntries[tail].mElement = nullptr; } tail = (tail + 1) & sMaxDeferredMask; } } // nsITimer related method void DeferredDNSPrefetches::Tick(nsITimer* aTimer, void* aClosure) { auto* self = static_cast(aClosure); NS_ASSERTION(NS_IsMainThread(), "DeferredDNSPrefetches::Tick must be on main thread"); NS_ASSERTION(self->mTimerArmed, "Timer is not armed"); self->mTimerArmed = false; // If the queue is not submitted here because there are outstanding pages // being loaded, there is no need to rearm the timer as the queue will be // submtited when those loads complete. if (!self->mActiveLoaderCount) { self->SubmitQueue(); } } //////////// nsIWebProgressListener methods NS_IMETHODIMP DeferredDNSPrefetches::OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t progressStateFlags, nsresult aStatus) { // The FIFO has no lock, so it can only be accessed on main thread NS_ASSERTION(NS_IsMainThread(), "DeferredDNSPrefetches::OnStateChange must be on main thread"); if (progressStateFlags & STATE_IS_DOCUMENT) { if (progressStateFlags & STATE_STOP) { // Initialization may have missed a STATE_START notification, so do // not go negative if (mActiveLoaderCount) mActiveLoaderCount--; if (!mActiveLoaderCount) { SubmitQueue(); } } else if (progressStateFlags & STATE_START) mActiveLoaderCount++; } return NS_OK; } NS_IMETHODIMP DeferredDNSPrefetches::OnProgressChange(nsIWebProgress* aProgress, nsIRequest* aRequest, int32_t curSelfProgress, int32_t maxSelfProgress, int32_t curTotalProgress, int32_t maxTotalProgress) { return NS_OK; } NS_IMETHODIMP DeferredDNSPrefetches::OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI* location, uint32_t aFlags) { return NS_OK; } NS_IMETHODIMP DeferredDNSPrefetches::OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsresult aStatus, const char16_t* aMessage) { return NS_OK; } NS_IMETHODIMP DeferredDNSPrefetches::OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aState) { return NS_OK; } NS_IMETHODIMP DeferredDNSPrefetches::OnContentBlockingEvent(nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aEvent) { return NS_OK; } //////////// nsIObserver method NS_IMETHODIMP DeferredDNSPrefetches::Observe(nsISupports* subject, const char* topic, const char16_t* data) { if (!strcmp(topic, "xpcom-shutdown")) Flush(); return NS_OK; } } // namespace mozilla::dom