1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  *
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file,
6  * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 
8 #include "FetchPreloader.h"
9 
10 #include "mozilla/DebugOnly.h"
11 #include "mozilla/dom/Document.h"
12 #include "mozilla/LoadInfo.h"
13 #include "mozilla/ScopeExit.h"
14 #include "mozilla/Unused.h"
15 #include "nsContentPolicyUtils.h"
16 #include "nsContentUtils.h"
17 #include "nsIChannel.h"
18 #include "nsIClassOfService.h"
19 #include "nsIHttpChannel.h"
20 #include "nsITimedChannel.h"
21 #include "nsNetUtil.h"
22 #include "nsStringStream.h"
23 #include "nsIDocShell.h"
24 
25 namespace mozilla {
26 
NS_IMPL_ISUPPORTS(FetchPreloader,nsIStreamListener,nsIRequestObserver)27 NS_IMPL_ISUPPORTS(FetchPreloader, nsIStreamListener, nsIRequestObserver)
28 
29 FetchPreloader::FetchPreloader()
30     : FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD) {}
31 
FetchPreloader(nsContentPolicyType aContentPolicyType)32 FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType)
33     : mContentPolicyType(aContentPolicyType) {}
34 
OpenChannel(const PreloadHashKey & aKey,nsIURI * aURI,const CORSMode aCORSMode,const dom::ReferrerPolicy & aReferrerPolicy,dom::Document * aDocument)35 nsresult FetchPreloader::OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI,
36                                      const CORSMode aCORSMode,
37                                      const dom::ReferrerPolicy& aReferrerPolicy,
38                                      dom::Document* aDocument) {
39   nsresult rv;
40   nsCOMPtr<nsIChannel> channel;
41 
42   auto notify = MakeScopeExit([&]() {
43     if (NS_FAILED(rv)) {
44       // Make sure we notify any <link preload> elements when opening fails
45       // because of various technical or security reasons.
46       NotifyStart(channel);
47       // Using the non-channel overload of this method to make it work even
48       // before NotifyOpen has been called on this preload.  We are not
49       // switching between channels, so it's safe to do so.
50       NotifyStop(rv);
51     }
52   });
53 
54   nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup();
55   nsCOMPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow();
56   nsCOMPtr<nsIInterfaceRequestor> prompter;
57   if (window) {
58     nsIDocShell* docshell = window->GetDocShell();
59     prompter = do_QueryInterface(docshell);
60   }
61 
62   rv = CreateChannel(getter_AddRefs(channel), aURI, aCORSMode, aReferrerPolicy,
63                      aDocument, loadGroup, prompter);
64   NS_ENSURE_SUCCESS(rv, rv);
65 
66   // Doing this now so that we have the channel and tainting set on it properly
67   // to notify the proper event (load or error) on the associated preload tags
68   // when the CSP check fails.
69   rv = CheckContentPolicy(aURI, aDocument);
70   if (NS_FAILED(rv)) {
71     return rv;
72   }
73 
74   PrioritizeAsPreload(channel);
75   AddLoadBackgroundFlag(channel);
76 
77   NotifyOpen(aKey, channel, aDocument, true);
78 
79   return mAsyncConsumeResult = rv = channel->AsyncOpen(this);
80 }
81 
CreateChannel(nsIChannel ** aChannel,nsIURI * aURI,const CORSMode aCORSMode,const dom::ReferrerPolicy & aReferrerPolicy,dom::Document * aDocument,nsILoadGroup * aLoadGroup,nsIInterfaceRequestor * aCallbacks)82 nsresult FetchPreloader::CreateChannel(
83     nsIChannel** aChannel, nsIURI* aURI, const CORSMode aCORSMode,
84     const dom::ReferrerPolicy& aReferrerPolicy, dom::Document* aDocument,
85     nsILoadGroup* aLoadGroup, nsIInterfaceRequestor* aCallbacks) {
86   nsresult rv;
87 
88   nsSecurityFlags securityFlags =
89       aCORSMode == CORS_NONE
90           ? nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL
91           : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
92   if (aCORSMode == CORS_ANONYMOUS) {
93     securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
94   } else if (aCORSMode == CORS_USE_CREDENTIALS) {
95     securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
96   }
97 
98   nsCOMPtr<nsIChannel> channel;
99   rv = NS_NewChannelWithTriggeringPrincipal(
100       getter_AddRefs(channel), aURI, aDocument, aDocument->NodePrincipal(),
101       securityFlags, nsIContentPolicy::TYPE_FETCH, nullptr, aLoadGroup,
102       aCallbacks);
103   if (NS_FAILED(rv)) {
104     return rv;
105   }
106 
107   if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) {
108     nsCOMPtr<nsIReferrerInfo> referrerInfo = new dom::ReferrerInfo(
109         aDocument->GetDocumentURIAsReferrer(), aReferrerPolicy);
110     rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo);
111     MOZ_ASSERT(NS_SUCCEEDED(rv));
112   }
113 
114   if (nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(channel)) {
115     timedChannel->SetInitiatorType(u"link"_ns);
116   }
117 
118   channel.forget(aChannel);
119   return NS_OK;
120 }
121 
CheckContentPolicy(nsIURI * aURI,dom::Document * aDocument)122 nsresult FetchPreloader::CheckContentPolicy(nsIURI* aURI,
123                                             dom::Document* aDocument) {
124   if (!aDocument) {
125     return NS_OK;
126   }
127 
128   nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo(
129       aDocument->NodePrincipal(), aDocument->NodePrincipal(), aDocument,
130       nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, mContentPolicyType);
131 
132   int16_t shouldLoad = nsIContentPolicy::ACCEPT;
133   nsresult rv =
134       NS_CheckContentLoadPolicy(aURI, secCheckLoadInfo, ""_ns, &shouldLoad,
135                                 nsContentUtils::GetContentPolicy());
136   if (NS_SUCCEEDED(rv) && NS_CP_ACCEPTED(shouldLoad)) {
137     return NS_OK;
138   }
139 
140   return NS_ERROR_CONTENT_BLOCKED;
141 }
142 
143 // PreloaderBase
144 
AsyncConsume(nsIStreamListener * aListener)145 nsresult FetchPreloader::AsyncConsume(nsIStreamListener* aListener) {
146   if (NS_FAILED(mAsyncConsumeResult)) {
147     // Already being consumed or failed to open.
148     return mAsyncConsumeResult;
149   }
150 
151   // Prevent duplicate calls.
152   mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE;
153 
154   if (!mConsumeListener) {
155     // Called before we are getting response from the channel.
156     mConsumeListener = aListener;
157   } else {
158     // Channel already started, push cached calls to this listener.
159     // Can't be anything else than the `Cache`, hence a safe static_cast.
160     Cache* cache = static_cast<Cache*>(mConsumeListener.get());
161     cache->AsyncConsume(aListener);
162   }
163 
164   return NS_OK;
165 }
166 
167 // static
PrioritizeAsPreload(nsIChannel * aChannel)168 void FetchPreloader::PrioritizeAsPreload(nsIChannel* aChannel) {
169   if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(aChannel)) {
170     cos->AddClassFlags(nsIClassOfService::Unblocked);
171   }
172 }
173 
PrioritizeAsPreload()174 void FetchPreloader::PrioritizeAsPreload() { PrioritizeAsPreload(Channel()); }
175 
176 // nsIRequestObserver + nsIStreamListener
177 
OnStartRequest(nsIRequest * request)178 NS_IMETHODIMP FetchPreloader::OnStartRequest(nsIRequest* request) {
179   NotifyStart(request);
180 
181   if (!mConsumeListener) {
182     // AsyncConsume not called yet.
183     mConsumeListener = new Cache();
184   }
185 
186   return mConsumeListener->OnStartRequest(request);
187 }
188 
OnDataAvailable(nsIRequest * request,nsIInputStream * input,uint64_t offset,uint32_t count)189 NS_IMETHODIMP FetchPreloader::OnDataAvailable(nsIRequest* request,
190                                               nsIInputStream* input,
191                                               uint64_t offset, uint32_t count) {
192   return mConsumeListener->OnDataAvailable(request, input, offset, count);
193 }
194 
OnStopRequest(nsIRequest * request,nsresult status)195 NS_IMETHODIMP FetchPreloader::OnStopRequest(nsIRequest* request,
196                                             nsresult status) {
197   mConsumeListener->OnStopRequest(request, status);
198 
199   // We want 404 or other types of server responses to be treated as 'error'.
200   if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request)) {
201     uint32_t responseStatus = 0;
202     Unused << httpChannel->GetResponseStatus(&responseStatus);
203     if (responseStatus / 100 != 2) {
204       status = NS_ERROR_FAILURE;
205     }
206   }
207 
208   // Fetch preloader wants to keep the channel around so that consumers like XHR
209   // can access it even after the preload is done.
210   nsCOMPtr<nsIChannel> channel = mChannel;
211   NotifyStop(request, status);
212   mChannel.swap(channel);
213   return NS_OK;
214 }
215 
216 // FetchPreloader::Cache
217 
NS_IMPL_ISUPPORTS(FetchPreloader::Cache,nsIStreamListener,nsIRequestObserver)218 NS_IMPL_ISUPPORTS(FetchPreloader::Cache, nsIStreamListener, nsIRequestObserver)
219 
220 NS_IMETHODIMP FetchPreloader::Cache::OnStartRequest(nsIRequest* request) {
221   mRequest = request;
222 
223   if (mFinalListener) {
224     return mFinalListener->OnStartRequest(mRequest);
225   }
226 
227   mCalls.AppendElement(Call{VariantIndex<0>{}, StartRequest{}});
228   return NS_OK;
229 }
230 
OnDataAvailable(nsIRequest * request,nsIInputStream * input,uint64_t offset,uint32_t count)231 NS_IMETHODIMP FetchPreloader::Cache::OnDataAvailable(nsIRequest* request,
232                                                      nsIInputStream* input,
233                                                      uint64_t offset,
234                                                      uint32_t count) {
235   if (mFinalListener) {
236     return mFinalListener->OnDataAvailable(mRequest, input, offset, count);
237   }
238 
239   DataAvailable data;
240   if (!data.mData.SetLength(count, fallible)) {
241     return NS_ERROR_OUT_OF_MEMORY;
242   }
243 
244   uint32_t read;
245   nsresult rv = input->Read(data.mData.BeginWriting(), count, &read);
246   if (NS_FAILED(rv)) {
247     return rv;
248   }
249 
250   mCalls.AppendElement(Call{VariantIndex<1>{}, std::move(data)});
251   return NS_OK;
252 }
253 
OnStopRequest(nsIRequest * request,nsresult status)254 NS_IMETHODIMP FetchPreloader::Cache::OnStopRequest(nsIRequest* request,
255                                                    nsresult status) {
256   if (mFinalListener) {
257     return mFinalListener->OnStopRequest(mRequest, status);
258   }
259 
260   mCalls.AppendElement(Call{VariantIndex<2>{}, StopRequest{status}});
261   return NS_OK;
262 }
263 
AsyncConsume(nsIStreamListener * aListener)264 void FetchPreloader::Cache::AsyncConsume(nsIStreamListener* aListener) {
265   // Must dispatch for two reasons:
266   // 1. This is called directly from FetchLoader::AsyncConsume, which must
267   // behave the same way as nsIChannel::AsyncOpen.
268   // 2. In case there are already stream listener events scheduled on the main
269   // thread we preserve the order - those will still end up in Cache.
270 
271   // * The `Cache` object is fully main thread only for now, doesn't support
272   // retargeting, but it can be improved to allow it.
273 
274   nsCOMPtr<nsIStreamListener> listener(aListener);
275   NS_DispatchToMainThread(NewRunnableMethod<nsCOMPtr<nsIStreamListener>>(
276       "FetchPreloader::Cache::Consume", this, &FetchPreloader::Cache::Consume,
277       listener));
278 }
279 
Consume(nsCOMPtr<nsIStreamListener> aListener)280 void FetchPreloader::Cache::Consume(nsCOMPtr<nsIStreamListener> aListener) {
281   MOZ_ASSERT(!mFinalListener, "Duplicate call");
282 
283   mFinalListener = std::move(aListener);
284 
285   // Status of the channel read after each call.
286   nsresult status = NS_OK;
287   nsCOMPtr<nsIChannel> channel(do_QueryInterface(mRequest));
288 
289   RefPtr<Cache> self(this);
290   for (auto& call : mCalls) {
291     nsresult rv = call.match(
292         [&](const StartRequest& startRequest) mutable {
293           return self->mFinalListener->OnStartRequest(self->mRequest);
294         },
295         [&](const DataAvailable& dataAvailable) mutable {
296           if (NS_FAILED(status)) {
297             // Channel has been cancelled during this mCalls loop.
298             return NS_OK;
299           }
300 
301           nsCOMPtr<nsIInputStream> input;
302           rv = NS_NewCStringInputStream(getter_AddRefs(input),
303                                         dataAvailable.mData);
304           if (NS_FAILED(rv)) {
305             return rv;
306           }
307 
308           return self->mFinalListener->OnDataAvailable(
309               self->mRequest, input, 0, dataAvailable.mData.Length());
310         },
311         [&](const StopRequest& stopRequest) {
312           // First cancellation overrides mStatus in nsHttpChannel.
313           nsresult stopStatus =
314               NS_FAILED(status) ? status : stopRequest.mStatus;
315           self->mFinalListener->OnStopRequest(self->mRequest, stopStatus);
316           self->mFinalListener = nullptr;
317           self->mRequest = nullptr;
318           return NS_OK;
319         });
320 
321     if (!mRequest) {
322       // We are done!
323       break;
324     }
325 
326     bool isCancelled = false;
327     Unused << channel->GetCanceled(&isCancelled);
328     if (isCancelled) {
329       mRequest->GetStatus(&status);
330     } else if (NS_FAILED(rv)) {
331       status = rv;
332       mRequest->Cancel(status);
333     }
334   }
335 
336   mCalls.Clear();
337 }
338 
339 }  // namespace mozilla
340