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 "PreloaderBase.h"
6 
7 #include "mozilla/dom/Document.h"
8 #include "mozilla/Telemetry.h"
9 #include "nsContentUtils.h"
10 #include "nsIAsyncVerifyRedirectCallback.h"
11 #include "nsIChannel.h"
12 #include "nsILoadGroup.h"
13 #include "nsIInterfaceRequestorUtils.h"
14 #include "nsIRedirectResultListener.h"
15 
16 // Change this if we want to cancel and remove the associated preload on removal
17 // of all <link rel=preload> tags from the tree.
18 constexpr static bool kCancelAndRemovePreloadOnZeroReferences = false;
19 
20 namespace mozilla {
21 
UsageTimer(PreloaderBase * aPreload,dom::Document * aDocument)22 PreloaderBase::UsageTimer::UsageTimer(PreloaderBase* aPreload,
23                                       dom::Document* aDocument)
24     : mDocument(aDocument), mPreload(aPreload) {}
25 
26 class PreloaderBase::RedirectSink final : public nsIInterfaceRequestor,
27                                           public nsIChannelEventSink,
28                                           public nsIRedirectResultListener {
29   RedirectSink() = delete;
30   virtual ~RedirectSink();
31 
32  public:
33   NS_DECL_THREADSAFE_ISUPPORTS
34   NS_DECL_NSIINTERFACEREQUESTOR
35   NS_DECL_NSICHANNELEVENTSINK
36   NS_DECL_NSIREDIRECTRESULTLISTENER
37 
38   RedirectSink(PreloaderBase* aPreloader, nsIInterfaceRequestor* aCallbacks);
39 
40  private:
41   MainThreadWeakPtr<PreloaderBase> mPreloader;
42   nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
43   nsCOMPtr<nsIChannel> mRedirectChannel;
44 };
45 
RedirectSink(PreloaderBase * aPreloader,nsIInterfaceRequestor * aCallbacks)46 PreloaderBase::RedirectSink::RedirectSink(PreloaderBase* aPreloader,
47                                           nsIInterfaceRequestor* aCallbacks)
48     : mPreloader(aPreloader), mCallbacks(aCallbacks) {}
49 
50 PreloaderBase::RedirectSink::~RedirectSink() = default;
51 
NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink,nsIInterfaceRequestor,nsIChannelEventSink,nsIRedirectResultListener)52 NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink, nsIInterfaceRequestor,
53                   nsIChannelEventSink, nsIRedirectResultListener)
54 
55 NS_IMETHODIMP PreloaderBase::RedirectSink::AsyncOnChannelRedirect(
56     nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
57     nsIAsyncVerifyRedirectCallback* aCallback) {
58   MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
59 
60   mRedirectChannel = aNewChannel;
61 
62   // Deliberately adding this before confirmation.
63   nsCOMPtr<nsIURI> uri;
64   aNewChannel->GetOriginalURI(getter_AddRefs(uri));
65   if (mPreloader) {
66     mPreloader->mRedirectRecords.AppendElement(
67         RedirectRecord(aFlags, uri.forget()));
68   }
69 
70   if (mCallbacks) {
71     nsCOMPtr<nsIChannelEventSink> sink(do_GetInterface(mCallbacks));
72     if (sink) {
73       return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags,
74                                           aCallback);
75     }
76   }
77 
78   aCallback->OnRedirectVerifyCallback(NS_OK);
79   return NS_OK;
80 }
81 
OnRedirectResult(bool proceeding)82 NS_IMETHODIMP PreloaderBase::RedirectSink::OnRedirectResult(bool proceeding) {
83   if (proceeding && mRedirectChannel) {
84     mPreloader->mChannel = std::move(mRedirectChannel);
85   } else {
86     mRedirectChannel = nullptr;
87   }
88 
89   if (mCallbacks) {
90     nsCOMPtr<nsIRedirectResultListener> sink(do_GetInterface(mCallbacks));
91     if (sink) {
92       return sink->OnRedirectResult(proceeding);
93     }
94   }
95 
96   return NS_OK;
97 }
98 
GetInterface(const nsIID & aIID,void ** aResult)99 NS_IMETHODIMP PreloaderBase::RedirectSink::GetInterface(const nsIID& aIID,
100                                                         void** aResult) {
101   NS_ENSURE_ARG_POINTER(aResult);
102 
103   if (aIID.Equals(NS_GET_IID(nsIChannelEventSink)) ||
104       aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) {
105     return QueryInterface(aIID, aResult);
106   }
107 
108   if (mCallbacks) {
109     return mCallbacks->GetInterface(aIID, aResult);
110   }
111 
112   *aResult = nullptr;
113   return NS_ERROR_NO_INTERFACE;
114 }
115 
~PreloaderBase()116 PreloaderBase::~PreloaderBase() { MOZ_ASSERT(NS_IsMainThread()); }
117 
118 // static
AddLoadBackgroundFlag(nsIChannel * aChannel)119 void PreloaderBase::AddLoadBackgroundFlag(nsIChannel* aChannel) {
120   nsLoadFlags loadFlags;
121   aChannel->GetLoadFlags(&loadFlags);
122   aChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND);
123 }
124 
NotifyOpen(const PreloadHashKey & aKey,dom::Document * aDocument,bool aIsPreload)125 void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey,
126                                dom::Document* aDocument, bool aIsPreload) {
127   if (aDocument) {
128     DebugOnly<bool> alreadyRegistered =
129         aDocument->Preloads().RegisterPreload(aKey, this);
130     // This means there is already a preload registered under this key in this
131     // document.  We only allow replacement when this is a regular load.
132     // Otherwise, this should never happen and is a suspected misuse of the API.
133     MOZ_ASSERT_IF(alreadyRegistered, !aIsPreload);
134   }
135 
136   mKey = aKey;
137   mIsUsed = !aIsPreload;
138 
139   if (!mIsUsed && !mUsageTimer) {
140     auto callback = MakeRefPtr<UsageTimer>(this, aDocument);
141     NS_NewTimerWithCallback(getter_AddRefs(mUsageTimer), callback, 10000,
142                             nsITimer::TYPE_ONE_SHOT);
143   }
144 
145   ReportUsageTelemetry();
146 }
147 
NotifyOpen(const PreloadHashKey & aKey,nsIChannel * aChannel,dom::Document * aDocument,bool aIsPreload)148 void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel,
149                                dom::Document* aDocument, bool aIsPreload) {
150   NotifyOpen(aKey, aDocument, aIsPreload);
151   mChannel = aChannel;
152 
153   nsCOMPtr<nsIInterfaceRequestor> callbacks;
154   mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks));
155   RefPtr<RedirectSink> sink(new RedirectSink(this, callbacks));
156   mChannel->SetNotificationCallbacks(sink);
157 }
158 
NotifyUsage(LoadBackground aLoadBackground)159 void PreloaderBase::NotifyUsage(LoadBackground aLoadBackground) {
160   if (!mIsUsed && mChannel && aLoadBackground == LoadBackground::Drop) {
161     nsLoadFlags loadFlags;
162     mChannel->GetLoadFlags(&loadFlags);
163 
164     // Preloads are initially set the LOAD_BACKGROUND flag.  When becoming
165     // regular loads by hitting its consuming tag, we need to drop that flag,
166     // which also means to re-add the request from/to it's loadgroup to reflect
167     // that flag change.
168     if (loadFlags & nsIRequest::LOAD_BACKGROUND) {
169       nsCOMPtr<nsILoadGroup> loadGroup;
170       mChannel->GetLoadGroup(getter_AddRefs(loadGroup));
171 
172       if (loadGroup) {
173         nsresult status;
174         mChannel->GetStatus(&status);
175 
176         nsresult rv = loadGroup->RemoveRequest(mChannel, nullptr, status);
177         mChannel->SetLoadFlags(loadFlags & ~nsIRequest::LOAD_BACKGROUND);
178         if (NS_SUCCEEDED(rv)) {
179           loadGroup->AddRequest(mChannel, nullptr);
180         }
181       }
182     }
183   }
184 
185   mIsUsed = true;
186   ReportUsageTelemetry();
187   CancelUsageTimer();
188 }
189 
RemoveSelf(dom::Document * aDocument)190 void PreloaderBase::RemoveSelf(dom::Document* aDocument) {
191   if (aDocument) {
192     aDocument->Preloads().DeregisterPreload(mKey);
193   }
194 }
195 
NotifyRestart(dom::Document * aDocument,PreloaderBase * aNewPreloader)196 void PreloaderBase::NotifyRestart(dom::Document* aDocument,
197                                   PreloaderBase* aNewPreloader) {
198   RemoveSelf(aDocument);
199   mKey = PreloadHashKey();
200 
201   CancelUsageTimer();
202 
203   if (aNewPreloader) {
204     aNewPreloader->mNodes = std::move(mNodes);
205   }
206 }
207 
NotifyStart(nsIRequest * aRequest)208 void PreloaderBase::NotifyStart(nsIRequest* aRequest) {
209   // If there is no channel assigned on this preloader, we are not between
210   // channel switching, so we can freely update the mShouldFireLoadEvent using
211   // the given channel.
212   if (mChannel && !SameCOMIdentity(aRequest, mChannel)) {
213     return;
214   }
215 
216   nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
217   if (!httpChannel) {
218     return;
219   }
220 
221   // if the load is cross origin without CORS, or the CORS access is rejected,
222   // always fire load event to avoid leaking site information.
223   nsresult rv;
224   nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo();
225   mShouldFireLoadEvent =
226       loadInfo->GetTainting() == LoadTainting::Opaque ||
227       (loadInfo->GetTainting() == LoadTainting::CORS &&
228        (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv)));
229 }
230 
NotifyStop(nsIRequest * aRequest,nsresult aStatus)231 void PreloaderBase::NotifyStop(nsIRequest* aRequest, nsresult aStatus) {
232   // Filter out notifications that may be arriving from the old channel before
233   // restarting this request.
234   if (!SameCOMIdentity(aRequest, mChannel)) {
235     return;
236   }
237 
238   NotifyStop(aStatus);
239 }
240 
NotifyStop(nsresult aStatus)241 void PreloaderBase::NotifyStop(nsresult aStatus) {
242   mOnStopStatus.emplace(aStatus);
243 
244   nsTArray<nsWeakPtr> nodes = std::move(mNodes);
245 
246   for (nsWeakPtr& weak : nodes) {
247     nsCOMPtr<nsINode> node = do_QueryReferent(weak);
248     if (node) {
249       NotifyNodeEvent(node);
250     }
251   }
252 
253   mChannel = nullptr;
254 }
255 
NotifyValidating()256 void PreloaderBase::NotifyValidating() { mOnStopStatus.reset(); }
257 
NotifyValidated(nsresult aStatus)258 void PreloaderBase::NotifyValidated(nsresult aStatus) {
259   NotifyStop(nullptr, aStatus);
260 }
261 
AddLinkPreloadNode(nsINode * aNode)262 void PreloaderBase::AddLinkPreloadNode(nsINode* aNode) {
263   if (mOnStopStatus) {
264     return NotifyNodeEvent(aNode);
265   }
266 
267   mNodes.AppendElement(do_GetWeakReference(aNode));
268 }
269 
RemoveLinkPreloadNode(nsINode * aNode)270 void PreloaderBase::RemoveLinkPreloadNode(nsINode* aNode) {
271   // Note that do_GetWeakReference returns the internal weak proxy, which is
272   // always the same, so we can use it to search the array using default
273   // comparator.
274   nsWeakPtr node = do_GetWeakReference(aNode);
275   mNodes.RemoveElement(node);
276 
277   if (kCancelAndRemovePreloadOnZeroReferences && mNodes.Length() == 0 &&
278       !mIsUsed) {
279     // Keep a reference, because the following call may release us.  The caller
280     // may use a WeakPtr to access this.
281     RefPtr<PreloaderBase> self(this);
282     RemoveSelf(aNode->OwnerDoc());
283 
284     if (mChannel) {
285       mChannel->Cancel(NS_BINDING_ABORTED);
286     }
287   }
288 }
289 
NotifyNodeEvent(nsINode * aNode)290 void PreloaderBase::NotifyNodeEvent(nsINode* aNode) {
291   PreloadService::NotifyNodeEvent(
292       aNode, mShouldFireLoadEvent || NS_SUCCEEDED(*mOnStopStatus));
293 }
294 
CancelUsageTimer()295 void PreloaderBase::CancelUsageTimer() {
296   if (mUsageTimer) {
297     mUsageTimer->Cancel();
298     mUsageTimer = nullptr;
299   }
300 }
301 
ReportUsageTelemetry()302 void PreloaderBase::ReportUsageTelemetry() {
303   if (mUsageTelementryReported) {
304     return;
305   }
306   mUsageTelementryReported = true;
307 
308   if (mKey.As() == PreloadHashKey::ResourceType::NONE) {
309     return;
310   }
311 
312   // The labels are structured as type1-used, type1-unused, type2-used, ...
313   // The first "as" resource type is NONE with value 0.
314   auto index = (static_cast<uint32_t>(mKey.As()) - 1) * 2;
315   if (!mIsUsed) {
316     ++index;
317   }
318 
319   auto label = static_cast<Telemetry::LABELS_REL_PRELOAD_MISS_RATIO>(index);
320   Telemetry::AccumulateCategorical(label);
321 }
322 
AsyncConsume(nsIStreamListener * aListener)323 nsresult PreloaderBase::AsyncConsume(nsIStreamListener* aListener) {
324   // We want to return an error so that consumers can't ever use a preload to
325   // consume data unless it's properly implemented.
326   return NS_ERROR_NOT_IMPLEMENTED;
327 }
328 
329 // PreloaderBase::RedirectRecord
330 
Spec() const331 nsCString PreloaderBase::RedirectRecord::Spec() const {
332   nsCOMPtr<nsIURI> noFragment;
333   NS_GetURIWithoutRef(mURI, getter_AddRefs(noFragment));
334   MOZ_ASSERT(noFragment);
335   return noFragment->GetSpecOrDefault();
336 }
337 
Fragment() const338 nsCString PreloaderBase::RedirectRecord::Fragment() const {
339   nsCString fragment;
340   mURI->GetRef(fragment);
341   return fragment;
342 }
343 
344 // PreloaderBase::UsageTimer
345 
NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer,nsITimerCallback,nsINamed)346 NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer, nsITimerCallback, nsINamed)
347 
348 NS_IMETHODIMP PreloaderBase::UsageTimer::Notify(nsITimer* aTimer) {
349   if (!mPreload || !mDocument) {
350     return NS_OK;
351   }
352 
353   MOZ_ASSERT(aTimer == mPreload->mUsageTimer);
354   mPreload->mUsageTimer = nullptr;
355 
356   if (mPreload->IsUsed()) {
357     // Left in the hashtable, but marked as used.  This is a valid case, and we
358     // don't want to emit a warning for this preload then.
359     return NS_OK;
360   }
361 
362   mPreload->ReportUsageTelemetry();
363 
364   // PreloadHashKey overrides GetKey, we need to use the nsURIHashKey one to get
365   // the URI.
366   nsIURI* uri = static_cast<nsURIHashKey*>(&mPreload->mKey)->GetKey();
367   if (!uri) {
368     return NS_OK;
369   }
370 
371   nsString spec;
372   NS_GetSanitizedURIStringFromURI(uri, spec);
373   nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns,
374                                   mDocument, nsContentUtils::eDOM_PROPERTIES,
375                                   "UnusedLinkPreloadPending",
376                                   nsTArray<nsString>({std::move(spec)}));
377   return NS_OK;
378 }
379 
380 NS_IMETHODIMP
GetName(nsACString & aName)381 PreloaderBase::UsageTimer::GetName(nsACString& aName) {
382   aName.AssignLiteral("PreloaderBase::UsageTimer");
383   return NS_OK;
384 }
385 
386 }  // namespace mozilla
387