diff options
Diffstat (limited to '')
-rw-r--r-- | dom/html/HTMLDNSPrefetch.cpp | 647 |
1 files changed, 647 insertions, 0 deletions
diff --git a/dom/html/HTMLDNSPrefetch.cpp b/dom/html/HTMLDNSPrefetch.cpp new file mode 100644 index 0000000000..a4043195fe --- /dev/null +++ b/dom/html/HTMLDNSPrefetch.cpp @@ -0,0 +1,647 @@ +/* -*- 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<nsITimer> 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<nsICancelable> 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<nsICancelable> 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<nsIWebProgress> 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<nsIObserverService> 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<DeferredDNSPrefetches*>(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 |