Bug 1890689 apply drift correction to input rate instead of output rate r=pehrsons
[gecko.git] / uriloader / preload / FetchPreloader.cpp
blobac580aeabce3d4f14a03b8f7e1c99f119b795b1d
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
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/. */
8 #include "FetchPreloader.h"
10 #include "mozilla/Assertions.h"
11 #include "mozilla/CORSMode.h"
12 #include "mozilla/DebugOnly.h"
13 #include "mozilla/dom/Document.h"
14 #include "mozilla/LoadInfo.h"
15 #include "mozilla/ScopeExit.h"
16 #include "mozilla/Unused.h"
17 #include "mozilla/dom/ReferrerInfo.h"
18 #include "nsContentPolicyUtils.h"
19 #include "nsContentSecurityManager.h"
20 #include "nsContentUtils.h"
21 #include "nsIChannel.h"
22 #include "nsIChildChannel.h"
23 #include "nsIClassOfService.h"
24 #include "nsIHttpChannel.h"
25 #include "nsIHttpChannelInternal.h"
26 #include "nsISupportsPriority.h"
27 #include "nsITimedChannel.h"
28 #include "nsNetUtil.h"
29 #include "nsStringStream.h"
30 #include "nsIDocShell.h"
32 namespace mozilla {
34 NS_IMPL_ISUPPORTS(FetchPreloader, nsIStreamListener, nsIRequestObserver)
36 FetchPreloader::FetchPreloader()
37 : FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD) {}
39 FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType)
40 : mContentPolicyType(aContentPolicyType) {}
42 nsresult FetchPreloader::OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI,
43 const CORSMode aCORSMode,
44 const dom::ReferrerPolicy& aReferrerPolicy,
45 dom::Document* aDocument,
46 uint64_t aEarlyHintPreloaderId,
47 int32_t aSupportsPriorityValue) {
48 nsresult rv;
49 nsCOMPtr<nsIChannel> channel;
51 auto notify = MakeScopeExit([&]() {
52 if (NS_FAILED(rv)) {
53 // Make sure we notify any <link preload> elements when opening fails
54 // because of various technical or security reasons.
55 NotifyStart(channel);
56 // Using the non-channel overload of this method to make it work even
57 // before NotifyOpen has been called on this preload. We are not
58 // switching between channels, so it's safe to do so.
59 NotifyStop(rv);
61 });
63 nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup();
64 nsCOMPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow();
65 nsCOMPtr<nsIInterfaceRequestor> prompter;
66 if (window) {
67 nsIDocShell* docshell = window->GetDocShell();
68 prompter = do_QueryInterface(docshell);
71 rv = CreateChannel(getter_AddRefs(channel), aURI, aCORSMode, aReferrerPolicy,
72 aDocument, loadGroup, prompter, aEarlyHintPreloaderId,
73 aSupportsPriorityValue);
74 NS_ENSURE_SUCCESS(rv, rv);
76 // Doing this now so that we have the channel and tainting set on it properly
77 // to notify the proper event (load or error) on the associated preload tags
78 // when the CSP check fails.
79 rv = CheckContentPolicy(aURI, aDocument);
80 if (NS_FAILED(rv)) {
81 return rv;
84 FetchPreloader::PrioritizeAsPreload(channel);
85 AddLoadBackgroundFlag(channel);
87 NotifyOpen(aKey, channel, aDocument, true);
89 if (aEarlyHintPreloaderId) {
90 nsCOMPtr<nsIHttpChannelInternal> channelInternal =
91 do_QueryInterface(channel);
92 NS_ENSURE_TRUE(channelInternal != nullptr, NS_ERROR_FAILURE);
94 rv = channelInternal->SetEarlyHintPreloaderId(aEarlyHintPreloaderId);
95 NS_ENSURE_SUCCESS(rv, rv);
97 return mAsyncConsumeResult = rv = channel->AsyncOpen(this);
100 nsresult FetchPreloader::CreateChannel(
101 nsIChannel** aChannel, nsIURI* aURI, const CORSMode aCORSMode,
102 const dom::ReferrerPolicy& aReferrerPolicy, dom::Document* aDocument,
103 nsILoadGroup* aLoadGroup, nsIInterfaceRequestor* aCallbacks,
104 uint64_t aEarlyHintPreloaderId, int32_t aSupportsPriorityValue) {
105 nsresult rv;
107 nsSecurityFlags securityFlags =
108 nsContentSecurityManager::ComputeSecurityFlags(
109 aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
110 CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS);
112 nsCOMPtr<nsIChannel> channel;
113 rv = NS_NewChannelWithTriggeringPrincipal(
114 getter_AddRefs(channel), aURI, aDocument, aDocument->NodePrincipal(),
115 securityFlags, nsIContentPolicy::TYPE_FETCH, nullptr, aLoadGroup,
116 aCallbacks);
117 if (NS_FAILED(rv)) {
118 return rv;
121 AdjustPriority(channel, aSupportsPriorityValue);
123 if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) {
124 nsCOMPtr<nsIReferrerInfo> referrerInfo = new dom::ReferrerInfo(
125 aDocument->GetDocumentURIAsReferrer(), aReferrerPolicy);
126 rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo);
127 MOZ_ASSERT(NS_SUCCEEDED(rv));
130 if (nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(channel)) {
131 if (aEarlyHintPreloaderId) {
132 timedChannel->SetInitiatorType(u"early-hints"_ns);
133 } else {
134 timedChannel->SetInitiatorType(u"link"_ns);
138 channel.forget(aChannel);
139 return NS_OK;
142 // static
143 void FetchPreloader::AdjustPriority(nsIChannel* aChannel,
144 int32_t aSupportsPriorityValue) {
145 if (nsCOMPtr<nsISupportsPriority> supportsPriority{
146 do_QueryInterface(aChannel)}) {
147 supportsPriority->SetPriority(aSupportsPriorityValue);
151 nsresult FetchPreloader::CheckContentPolicy(nsIURI* aURI,
152 dom::Document* aDocument) {
153 if (!aDocument) {
154 return NS_OK;
157 nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo(
158 aDocument->NodePrincipal(), aDocument->NodePrincipal(), aDocument,
159 nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, mContentPolicyType);
161 int16_t shouldLoad = nsIContentPolicy::ACCEPT;
162 nsresult rv = NS_CheckContentLoadPolicy(aURI, secCheckLoadInfo, &shouldLoad,
163 nsContentUtils::GetContentPolicy());
164 if (NS_SUCCEEDED(rv) && NS_CP_ACCEPTED(shouldLoad)) {
165 return NS_OK;
168 return NS_ERROR_CONTENT_BLOCKED;
171 // PreloaderBase
173 nsresult FetchPreloader::AsyncConsume(nsIStreamListener* aListener) {
174 if (NS_FAILED(mAsyncConsumeResult)) {
175 // Already being consumed or failed to open.
176 return mAsyncConsumeResult;
179 // Prevent duplicate calls.
180 mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE;
182 if (!mConsumeListener) {
183 // Called before we are getting response from the channel.
184 mConsumeListener = aListener;
185 } else {
186 // Channel already started, push cached calls to this listener.
187 // Can't be anything else than the `Cache`, hence a safe static_cast.
188 Cache* cache = static_cast<Cache*>(mConsumeListener.get());
189 cache->AsyncConsume(aListener);
192 return NS_OK;
195 // static
196 void FetchPreloader::PrioritizeAsPreload(nsIChannel* aChannel) {
197 if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(aChannel)) {
198 cos->AddClassFlags(nsIClassOfService::Unblocked);
202 // nsIRequestObserver + nsIStreamListener
204 NS_IMETHODIMP FetchPreloader::OnStartRequest(nsIRequest* request) {
205 NotifyStart(request);
207 if (!mConsumeListener) {
208 // AsyncConsume not called yet.
209 mConsumeListener = new Cache();
212 return mConsumeListener->OnStartRequest(request);
215 NS_IMETHODIMP FetchPreloader::OnDataAvailable(nsIRequest* request,
216 nsIInputStream* input,
217 uint64_t offset, uint32_t count) {
218 return mConsumeListener->OnDataAvailable(request, input, offset, count);
221 NS_IMETHODIMP FetchPreloader::OnStopRequest(nsIRequest* request,
222 nsresult status) {
223 mConsumeListener->OnStopRequest(request, status);
225 // We want 404 or other types of server responses to be treated as 'error'.
226 if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request)) {
227 uint32_t responseStatus = 0;
228 Unused << httpChannel->GetResponseStatus(&responseStatus);
229 if (responseStatus / 100 != 2) {
230 status = NS_ERROR_FAILURE;
234 // Fetch preloader wants to keep the channel around so that consumers like XHR
235 // can access it even after the preload is done.
236 nsCOMPtr<nsIChannel> channel = mChannel;
237 NotifyStop(request, status);
238 mChannel.swap(channel);
239 return NS_OK;
242 // FetchPreloader::Cache
244 NS_IMPL_ISUPPORTS(FetchPreloader::Cache, nsIStreamListener, nsIRequestObserver)
246 NS_IMETHODIMP FetchPreloader::Cache::OnStartRequest(nsIRequest* request) {
247 mRequest = request;
249 if (mFinalListener) {
250 return mFinalListener->OnStartRequest(mRequest);
253 mCalls.AppendElement(Call{VariantIndex<0>{}, StartRequest{}});
254 return NS_OK;
257 NS_IMETHODIMP FetchPreloader::Cache::OnDataAvailable(nsIRequest* request,
258 nsIInputStream* input,
259 uint64_t offset,
260 uint32_t count) {
261 if (mFinalListener) {
262 return mFinalListener->OnDataAvailable(mRequest, input, offset, count);
265 DataAvailable data;
266 if (!data.mData.SetLength(count, fallible)) {
267 return NS_ERROR_OUT_OF_MEMORY;
270 uint32_t read;
271 nsresult rv = input->Read(data.mData.BeginWriting(), count, &read);
272 if (NS_FAILED(rv)) {
273 return rv;
276 mCalls.AppendElement(Call{VariantIndex<1>{}, std::move(data)});
277 return NS_OK;
280 NS_IMETHODIMP FetchPreloader::Cache::OnStopRequest(nsIRequest* request,
281 nsresult status) {
282 if (mFinalListener) {
283 return mFinalListener->OnStopRequest(mRequest, status);
286 mCalls.AppendElement(Call{VariantIndex<2>{}, StopRequest{status}});
287 return NS_OK;
290 void FetchPreloader::Cache::AsyncConsume(nsIStreamListener* aListener) {
291 // Must dispatch for two reasons:
292 // 1. This is called directly from FetchLoader::AsyncConsume, which must
293 // behave the same way as nsIChannel::AsyncOpen.
294 // 2. In case there are already stream listener events scheduled on the main
295 // thread we preserve the order - those will still end up in Cache.
297 // * The `Cache` object is fully main thread only for now, doesn't support
298 // retargeting, but it can be improved to allow it.
300 nsCOMPtr<nsIStreamListener> listener(aListener);
301 NS_DispatchToMainThread(NewRunnableMethod<nsCOMPtr<nsIStreamListener>>(
302 "FetchPreloader::Cache::Consume", this, &FetchPreloader::Cache::Consume,
303 listener));
306 void FetchPreloader::Cache::Consume(nsCOMPtr<nsIStreamListener> aListener) {
307 MOZ_ASSERT(!mFinalListener, "Duplicate call");
309 mFinalListener = std::move(aListener);
311 // Status of the channel read after each call.
312 nsresult status = NS_OK;
313 nsCOMPtr<nsIChannel> channel(do_QueryInterface(mRequest));
315 RefPtr<Cache> self(this);
316 for (auto& call : mCalls) {
317 nsresult rv = call.match(
318 [&](const StartRequest& startRequest) mutable {
319 return self->mFinalListener->OnStartRequest(self->mRequest);
321 [&](const DataAvailable& dataAvailable) mutable {
322 if (NS_FAILED(status)) {
323 // Channel has been cancelled during this mCalls loop.
324 return NS_OK;
327 nsCOMPtr<nsIInputStream> input;
328 rv = NS_NewCStringInputStream(getter_AddRefs(input),
329 dataAvailable.mData);
330 if (NS_FAILED(rv)) {
331 return rv;
334 return self->mFinalListener->OnDataAvailable(
335 self->mRequest, input, 0, dataAvailable.mData.Length());
337 [&](const StopRequest& stopRequest) {
338 // First cancellation overrides mStatus in nsHttpChannel.
339 nsresult stopStatus =
340 NS_FAILED(status) ? status : stopRequest.mStatus;
341 self->mFinalListener->OnStopRequest(self->mRequest, stopStatus);
342 self->mFinalListener = nullptr;
343 self->mRequest = nullptr;
344 return NS_OK;
347 if (!mRequest) {
348 // We are done!
349 break;
352 bool isCancelled = false;
353 Unused << channel->GetCanceled(&isCancelled);
354 if (isCancelled) {
355 mRequest->GetStatus(&status);
356 } else if (NS_FAILED(rv)) {
357 status = rv;
358 mRequest->Cancel(status);
362 mCalls.Clear();
365 } // namespace mozilla