/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 "nsPrefetchService.h" #include "nsICacheEntry.h" #include "nsIServiceManager.h" #include "nsICategoryManager.h" #include "nsIObserverService.h" #include "nsIWebProgress.h" #include "nsCURILoader.h" #include "nsICacheInfoChannel.h" #include "nsIHttpChannel.h" #include "nsIURL.h" #include "nsISimpleEnumerator.h" #include "nsISupportsPriority.h" #include "nsNetUtil.h" #include "nsString.h" #include "nsXPIDLString.h" #include "nsReadableUtils.h" #include "nsStreamUtils.h" #include "nsAutoPtr.h" #include "prtime.h" #include "mozilla/Logging.h" #include "plstr.h" #include "nsIAsyncVerifyRedirectCallback.h" #include "mozilla/Preferences.h" #include "mozilla/Attributes.h" #include "mozilla/CORSMode.h" #include "mozilla/dom/HTMLLinkElement.h" #include "nsIDOMNode.h" #include "nsINode.h" #include "nsIDocument.h" #include "nsContentUtils.h" using namespace mozilla; // // To enable logging (see prlog.h for full details): // // set NSPR_LOG_MODULES=nsPrefetch:5 // set NSPR_LOG_FILE=prefetch.log // // this enables LogLevel::Debug level information and places all output in // the file http.log // static PRLogModuleInfo *gPrefetchLog; #undef LOG #define LOG(args) MOZ_LOG(gPrefetchLog, mozilla::LogLevel::Debug, args) #undef LOG_ENABLED #define LOG_ENABLED() MOZ_LOG_TEST(gPrefetchLog, mozilla::LogLevel::Debug) #define PREFETCH_PREF "network.prefetch-next" #define PARALLELISM_PREF "network.prefetch-next.parallelism" //----------------------------------------------------------------------------- // helpers //----------------------------------------------------------------------------- static inline uint32_t PRTimeToSeconds(PRTime t_usec) { PRTime usec_per_sec = PR_USEC_PER_SEC; return uint32_t(t_usec /= usec_per_sec); } #define NowInSeconds() PRTimeToSeconds(PR_Now()) //----------------------------------------------------------------------------- // nsPrefetchNode //----------------------------------------------------------------------------- nsPrefetchNode::nsPrefetchNode(nsPrefetchService *aService, nsIURI *aURI, nsIURI *aReferrerURI, nsIDOMNode *aSource) : mNext(nullptr) , mURI(aURI) , mReferrerURI(aReferrerURI) , mService(aService) , mChannel(nullptr) , mBytesRead(0) { mSource = do_GetWeakReference(aSource); } nsresult nsPrefetchNode::OpenChannel() { nsCOMPtr source = do_QueryReferent(mSource); if (!source) { // Don't attempt to prefetch if we don't have a source node // (which should never happen). return NS_ERROR_FAILURE; } nsCOMPtr loadGroup = source->OwnerDoc()->GetDocumentLoadGroup(); CORSMode corsMode = CORS_NONE; if (source->IsHTMLElement(nsGkAtoms::link)) { corsMode = static_cast(source.get())->GetCORSMode(); } uint32_t securityFlags; if (corsMode == CORS_NONE) { securityFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS; } else { securityFlags = nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS; if (corsMode == CORS_USE_CREDENTIALS) { securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; } } nsresult rv = NS_NewChannelInternal(getter_AddRefs(mChannel), mURI, source, source->NodePrincipal(), nullptr, //aTriggeringPrincipal securityFlags, nsIContentPolicy::TYPE_OTHER, loadGroup, // aLoadGroup this, // aCallbacks nsIRequest::LOAD_BACKGROUND | nsICachingChannel::LOAD_ONLY_IF_MODIFIED); NS_ENSURE_SUCCESS(rv, rv); // configure HTTP specific stuff nsCOMPtr httpChannel = do_QueryInterface(mChannel); if (httpChannel) { httpChannel->SetReferrer(mReferrerURI); httpChannel->SetRequestHeader( NS_LITERAL_CSTRING("X-Moz"), NS_LITERAL_CSTRING("prefetch"), false); } // Reduce the priority of prefetch network requests. nsCOMPtr priorityChannel = do_QueryInterface(mChannel); if (priorityChannel) { priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_LOWEST); } rv = mChannel->AsyncOpen2(this); if (NS_WARN_IF(NS_FAILED(rv))) { // Drop the ref to the channel, because we don't want to end up with // cycles through it. mChannel = nullptr; } return rv; } nsresult nsPrefetchNode::CancelChannel(nsresult error) { mChannel->Cancel(error); mChannel = nullptr; return NS_OK; } //----------------------------------------------------------------------------- // nsPrefetchNode::nsISupports //----------------------------------------------------------------------------- NS_IMPL_ISUPPORTS(nsPrefetchNode, nsIRequestObserver, nsIStreamListener, nsIInterfaceRequestor, nsIChannelEventSink, nsIRedirectResultListener) //----------------------------------------------------------------------------- // nsPrefetchNode::nsIStreamListener //----------------------------------------------------------------------------- NS_IMETHODIMP nsPrefetchNode::OnStartRequest(nsIRequest *aRequest, nsISupports *aContext) { nsresult rv; nsCOMPtr cacheInfoChannel = do_QueryInterface(aRequest, &rv); if (NS_FAILED(rv)) return rv; // no need to prefetch a document that is already in the cache bool fromCache; if (NS_SUCCEEDED(cacheInfoChannel->IsFromCache(&fromCache)) && fromCache) { LOG(("document is already in the cache; canceling prefetch\n")); return NS_BINDING_ABORTED; } // // no need to prefetch a document that must be requested fresh each // and every time. // uint32_t expTime; if (NS_SUCCEEDED(cacheInfoChannel->GetCacheTokenExpirationTime(&expTime))) { if (NowInSeconds() >= expTime) { LOG(("document cannot be reused from cache; " "canceling prefetch\n")); return NS_BINDING_ABORTED; } } return NS_OK; } NS_IMETHODIMP nsPrefetchNode::OnDataAvailable(nsIRequest *aRequest, nsISupports *aContext, nsIInputStream *aStream, uint64_t aOffset, uint32_t aCount) { uint32_t bytesRead = 0; aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead); mBytesRead += bytesRead; LOG(("prefetched %u bytes [offset=%llu]\n", bytesRead, aOffset)); return NS_OK; } NS_IMETHODIMP nsPrefetchNode::OnStopRequest(nsIRequest *aRequest, nsISupports *aContext, nsresult aStatus) { LOG(("done prefetching [status=%x]\n", aStatus)); if (mBytesRead == 0 && aStatus == NS_OK) { // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was // specified), but the object should report loadedSize as if it // did. mChannel->GetContentLength(&mBytesRead); } mService->NotifyLoadCompleted(this); mService->ProcessNextURI(this); return NS_OK; } //----------------------------------------------------------------------------- // nsPrefetchNode::nsIInterfaceRequestor //----------------------------------------------------------------------------- NS_IMETHODIMP nsPrefetchNode::GetInterface(const nsIID &aIID, void **aResult) { if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { NS_ADDREF_THIS(); *aResult = static_cast(this); return NS_OK; } if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { NS_ADDREF_THIS(); *aResult = static_cast(this); return NS_OK; } return NS_ERROR_NO_INTERFACE; } //----------------------------------------------------------------------------- // nsPrefetchNode::nsIChannelEventSink //----------------------------------------------------------------------------- NS_IMETHODIMP nsPrefetchNode::AsyncOnChannelRedirect(nsIChannel *aOldChannel, nsIChannel *aNewChannel, uint32_t aFlags, nsIAsyncVerifyRedirectCallback *callback) { nsCOMPtr newURI; nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI)); if (NS_FAILED(rv)) return rv; bool match; rv = newURI->SchemeIs("http", &match); if (NS_FAILED(rv) || !match) { rv = newURI->SchemeIs("https", &match); if (NS_FAILED(rv) || !match) { LOG(("rejected: URL is not of type http/https\n")); return NS_ERROR_ABORT; } } // HTTP request headers are not automatically forwarded to the new channel. nsCOMPtr httpChannel = do_QueryInterface(aNewChannel); NS_ENSURE_STATE(httpChannel); httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"), NS_LITERAL_CSTRING("prefetch"), false); // Assign to mChannel after we get notification about success of the // redirect in OnRedirectResult. mRedirectChannel = aNewChannel; callback->OnRedirectVerifyCallback(NS_OK); return NS_OK; } //----------------------------------------------------------------------------- // nsPrefetchNode::nsIRedirectResultListener //----------------------------------------------------------------------------- NS_IMETHODIMP nsPrefetchNode::OnRedirectResult(bool proceeding) { if (proceeding && mRedirectChannel) mChannel = mRedirectChannel; mRedirectChannel = nullptr; return NS_OK; } //----------------------------------------------------------------------------- // nsPrefetchService //----------------------------------------------------------------------------- nsPrefetchService::nsPrefetchService() : mQueueHead(nullptr) , mQueueTail(nullptr) , mMaxParallelism(6) , mStopCount(0) , mHaveProcessed(false) , mDisabled(true) { } nsPrefetchService::~nsPrefetchService() { Preferences::RemoveObserver(this, PREFETCH_PREF); Preferences::RemoveObserver(this, PARALLELISM_PREF); // cannot reach destructor if prefetch in progress (listener owns reference // to this service) EmptyQueue(); } nsresult nsPrefetchService::Init() { if (!gPrefetchLog) gPrefetchLog = PR_NewLogModule("nsPrefetch"); nsresult rv; // read prefs and hook up pref observer mDisabled = !Preferences::GetBool(PREFETCH_PREF, !mDisabled); Preferences::AddWeakObserver(this, PREFETCH_PREF); mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism); if (mMaxParallelism < 1) { mMaxParallelism = 1; } Preferences::AddWeakObserver(this, PARALLELISM_PREF); // Observe xpcom-shutdown event nsCOMPtr observerService = mozilla::services::GetObserverService(); if (!observerService) return NS_ERROR_FAILURE; rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); NS_ENSURE_SUCCESS(rv, rv); if (!mDisabled) AddProgressListener(); return NS_OK; } void nsPrefetchService::ProcessNextURI(nsPrefetchNode *aFinished) { nsresult rv; nsCOMPtr uri, referrer; if (aFinished) { mCurrentNodes.RemoveElement(aFinished); } if (mCurrentNodes.Length() >= static_cast(mMaxParallelism)) { // We already have enough prefetches going on, so hold off // for now. return; } do { RefPtr node; rv = DequeueNode(getter_AddRefs(node)); if (NS_FAILED(rv)) break; if (LOG_ENABLED()) { nsAutoCString spec; node->mURI->GetSpec(spec); LOG(("ProcessNextURI [%s]\n", spec.get())); } // // if opening the channel fails, then just skip to the next uri // rv = node->OpenChannel(); if (NS_SUCCEEDED(rv)) { mCurrentNodes.AppendElement(node); } } while (NS_FAILED(rv)); } void nsPrefetchService::NotifyLoadRequested(nsPrefetchNode *node) { nsCOMPtr observerService = mozilla::services::GetObserverService(); if (!observerService) return; observerService->NotifyObservers(static_cast(node), "prefetch-load-requested", nullptr); } void nsPrefetchService::NotifyLoadCompleted(nsPrefetchNode *node) { nsCOMPtr observerService = mozilla::services::GetObserverService(); if (!observerService) return; observerService->NotifyObservers(static_cast(node), "prefetch-load-completed", nullptr); } //----------------------------------------------------------------------------- // nsPrefetchService //----------------------------------------------------------------------------- void nsPrefetchService::AddProgressListener() { // Register as an observer for the document loader nsCOMPtr progress = do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID); if (progress) progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT); } void nsPrefetchService::RemoveProgressListener() { // Register as an observer for the document loader nsCOMPtr progress = do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID); if (progress) progress->RemoveProgressListener(this); } nsresult nsPrefetchService::EnqueueNode(nsPrefetchNode *aNode) { NS_ADDREF(aNode); if (!mQueueTail) { mQueueHead = aNode; mQueueTail = aNode; } else { mQueueTail->mNext = aNode; mQueueTail = aNode; } return NS_OK; } nsresult nsPrefetchService::EnqueueURI(nsIURI *aURI, nsIURI *aReferrerURI, nsIDOMNode *aSource, nsPrefetchNode **aNode) { nsPrefetchNode *node = new nsPrefetchNode(this, aURI, aReferrerURI, aSource); if (!node) return NS_ERROR_OUT_OF_MEMORY; NS_ADDREF(*aNode = node); return EnqueueNode(node); } nsresult nsPrefetchService::DequeueNode(nsPrefetchNode **node) { if (!mQueueHead) return NS_ERROR_NOT_AVAILABLE; // give the ref to the caller *node = mQueueHead; mQueueHead = mQueueHead->mNext; (*node)->mNext = nullptr; if (!mQueueHead) mQueueTail = nullptr; return NS_OK; } void nsPrefetchService::EmptyQueue() { do { RefPtr node; DequeueNode(getter_AddRefs(node)); } while (mQueueHead); } void nsPrefetchService::StartPrefetching() { // // at initialization time we might miss the first DOCUMENT START // notification, so we have to be careful to avoid letting our // stop count go negative. // if (mStopCount > 0) mStopCount--; LOG(("StartPrefetching [stopcount=%d]\n", mStopCount)); // only start prefetching after we've received enough DOCUMENT // STOP notifications. we do this inorder to defer prefetching // until after all sub-frames have finished loading. if (!mStopCount) { mHaveProcessed = true; while (mQueueHead && mCurrentNodes.Length() < static_cast(mMaxParallelism)) { ProcessNextURI(nullptr); } } } void nsPrefetchService::StopPrefetching() { mStopCount++; LOG(("StopPrefetching [stopcount=%d]\n", mStopCount)); // only kill the prefetch queue if we are actively prefetching right now if (mCurrentNodes.IsEmpty()) { return; } for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); } mCurrentNodes.Clear(); EmptyQueue(); } //----------------------------------------------------------------------------- // nsPrefetchService::nsISupports //----------------------------------------------------------------------------- NS_IMPL_ISUPPORTS(nsPrefetchService, nsIPrefetchService, nsIWebProgressListener, nsIObserver, nsISupportsWeakReference) //----------------------------------------------------------------------------- // nsPrefetchService::nsIPrefetchService //----------------------------------------------------------------------------- nsresult nsPrefetchService::Prefetch(nsIURI *aURI, nsIURI *aReferrerURI, nsIDOMNode *aSource, bool aExplicit) { nsresult rv; NS_ENSURE_ARG_POINTER(aURI); NS_ENSURE_ARG_POINTER(aReferrerURI); if (LOG_ENABLED()) { nsAutoCString spec; aURI->GetSpec(spec); LOG(("PrefetchURI [%s]\n", spec.get())); } if (mDisabled) { LOG(("rejected: prefetch service is disabled\n")); return NS_ERROR_ABORT; } // // XXX we should really be asking the protocol handler if it supports // caching, so we can determine if there is any value to prefetching. // for now, we'll only prefetch http links since we know that's the // most common case. ignore https links since https content only goes // into the memory cache. // // XXX we might want to either leverage nsIProtocolHandler::protocolFlags // or possibly nsIRequest::loadFlags to determine if this URI should be // prefetched. // bool match; rv = aURI->SchemeIs("http", &match); if (NS_FAILED(rv) || !match) { rv = aURI->SchemeIs("https", &match); if (NS_FAILED(rv) || !match) { LOG(("rejected: URL is not of type http/https\n")); return NS_ERROR_ABORT; } } // // the referrer URI must be http: // rv = aReferrerURI->SchemeIs("http", &match); if (NS_FAILED(rv) || !match) { rv = aReferrerURI->SchemeIs("https", &match); if (NS_FAILED(rv) || !match) { LOG(("rejected: referrer URL is neither http nor https\n")); return NS_ERROR_ABORT; } } // skip URLs that contain query strings, except URLs for which prefetching // has been explicitly requested. if (!aExplicit) { nsCOMPtr url(do_QueryInterface(aURI, &rv)); if (NS_FAILED(rv)) return rv; nsAutoCString query; rv = url->GetQuery(query); if (NS_FAILED(rv) || !query.IsEmpty()) { LOG(("rejected: URL has a query string\n")); return NS_ERROR_ABORT; } } // // cancel if being prefetched // for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { bool equals; if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && equals) { LOG(("rejected: URL is already being prefetched\n")); return NS_ERROR_ABORT; } } // // cancel if already on the prefetch queue // nsPrefetchNode *node = mQueueHead; for (; node; node = node->mNext) { bool equals; if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) { LOG(("rejected: URL is already on prefetch queue\n")); return NS_ERROR_ABORT; } } RefPtr enqueuedNode; rv = EnqueueURI(aURI, aReferrerURI, aSource, getter_AddRefs(enqueuedNode)); NS_ENSURE_SUCCESS(rv, rv); NotifyLoadRequested(enqueuedNode); // if there are no pages loading, kick off the request immediately if (mStopCount == 0 && mHaveProcessed) { ProcessNextURI(nullptr); } return NS_OK; } NS_IMETHODIMP nsPrefetchService::PrefetchURI(nsIURI *aURI, nsIURI *aReferrerURI, nsIDOMNode *aSource, bool aExplicit) { return Prefetch(aURI, aReferrerURI, aSource, aExplicit); } NS_IMETHODIMP nsPrefetchService::HasMoreElements(bool *aHasMore) { *aHasMore = (mCurrentNodes.Length() || mQueueHead); return NS_OK; } //----------------------------------------------------------------------------- // nsPrefetchService::nsIWebProgressListener //----------------------------------------------------------------------------- NS_IMETHODIMP nsPrefetchService::OnProgressChange(nsIWebProgress *aProgress, nsIRequest *aRequest, int32_t curSelfProgress, int32_t maxSelfProgress, int32_t curTotalProgress, int32_t maxTotalProgress) { NS_NOTREACHED("notification excluded in AddProgressListener(...)"); return NS_OK; } NS_IMETHODIMP nsPrefetchService::OnStateChange(nsIWebProgress* aWebProgress, nsIRequest *aRequest, uint32_t progressStateFlags, nsresult aStatus) { if (progressStateFlags & STATE_IS_DOCUMENT) { if (progressStateFlags & STATE_STOP) StartPrefetching(); else if (progressStateFlags & STATE_START) StopPrefetching(); } return NS_OK; } NS_IMETHODIMP nsPrefetchService::OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI *location, uint32_t aFlags) { NS_NOTREACHED("notification excluded in AddProgressListener(...)"); return NS_OK; } NS_IMETHODIMP nsPrefetchService::OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsresult aStatus, const char16_t* aMessage) { NS_NOTREACHED("notification excluded in AddProgressListener(...)"); return NS_OK; } NS_IMETHODIMP nsPrefetchService::OnSecurityChange(nsIWebProgress *aWebProgress, nsIRequest *aRequest, uint32_t state) { NS_NOTREACHED("notification excluded in AddProgressListener(...)"); return NS_OK; } //----------------------------------------------------------------------------- // nsPrefetchService::nsIObserver //----------------------------------------------------------------------------- NS_IMETHODIMP nsPrefetchService::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) { LOG(("nsPrefetchService::Observe [topic=%s]\n", aTopic)); if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { StopPrefetching(); EmptyQueue(); mDisabled = true; } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { const char *pref = NS_ConvertUTF16toUTF8(aData).get(); if (!strcmp(pref, PREFETCH_PREF)) { if (Preferences::GetBool(PREFETCH_PREF, false)) { if (mDisabled) { LOG(("enabling prefetching\n")); mDisabled = false; AddProgressListener(); } } else { if (!mDisabled) { LOG(("disabling prefetching\n")); StopPrefetching(); EmptyQueue(); mDisabled = true; RemoveProgressListener(); } } } else if (!strcmp(pref, PARALLELISM_PREF)) { mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism); if (mMaxParallelism < 1) { mMaxParallelism = 1; } // If our parallelism has increased, go ahead and kick off enough // prefetches to fill up our allowance. If we're now over our // allowance, we'll just silently let some of them finish to get // back below our limit. while (mQueueHead && mCurrentNodes.Length() < static_cast(mMaxParallelism)) { ProcessNextURI(nullptr); } } } return NS_OK; } // vim: ts=4 sw=4 expandtab