1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* 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 file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "PageThumbProtocolHandler.h"
8
9 #include "mozilla/ClearOnShutdown.h"
10 #include "mozilla/ipc/URIParams.h"
11 #include "mozilla/ipc/URIUtils.h"
12 #include "mozilla/net/NeckoChild.h"
13 #include "mozilla/RefPtr.h"
14 #include "mozilla/ResultExtensions.h"
15
16 #include "LoadInfo.h"
17 #include "nsContentUtils.h"
18 #include "nsServiceManagerUtils.h"
19 #include "nsIFile.h"
20 #include "nsIFileChannel.h"
21 #include "nsIFileStreams.h"
22 #include "nsIMIMEService.h"
23 #include "nsIURL.h"
24 #include "nsIChannel.h"
25 #include "nsIPageThumbsStorageService.h"
26 #include "nsIInputStreamPump.h"
27 #include "nsIStreamListener.h"
28 #include "nsIInputStream.h"
29 #include "nsNetUtil.h"
30 #include "nsURLHelper.h"
31 #include "prio.h"
32 #include "SimpleChannel.h"
33
34 #define PAGE_THUMB_HOST "thumbnails"
35 #define PAGE_THUMB_SCHEME "moz-page-thumb"
36
37 namespace mozilla {
38 namespace net {
39
40 LazyLogModule gPageThumbProtocolLog("PageThumbProtocol");
41
42 #undef LOG
43 #define LOG(level, ...) \
44 MOZ_LOG(gPageThumbProtocolLog, LogLevel::level, (__VA_ARGS__))
45
46 StaticRefPtr<PageThumbProtocolHandler> PageThumbProtocolHandler::sSingleton;
47
48 /**
49 * Helper class used with SimpleChannel to asynchronously obtain an input
50 * stream from the parent for a remote moz-page-thumb load from the child.
51 */
52 class PageThumbStreamGetter final : public nsICancelable {
53 NS_DECL_ISUPPORTS
54 NS_DECL_NSICANCELABLE
55
56 public:
PageThumbStreamGetter(nsIURI * aURI,nsILoadInfo * aLoadInfo)57 PageThumbStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo)
58 : mURI(aURI), mLoadInfo(aLoadInfo) {
59 MOZ_ASSERT(aURI);
60 MOZ_ASSERT(aLoadInfo);
61
62 SetupEventTarget();
63 }
64
SetupEventTarget()65 void SetupEventTarget() {
66 mMainThreadEventTarget = nsContentUtils::GetEventTargetByLoadInfo(
67 mLoadInfo, TaskCategory::Other);
68 if (!mMainThreadEventTarget) {
69 mMainThreadEventTarget = GetMainThreadSerialEventTarget();
70 }
71 }
72
73 // Get an input stream from the parent asynchronously.
74 RequestOrReason GetAsync(nsIStreamListener* aListener, nsIChannel* aChannel);
75
76 // Handle an input stream being returned from the parent
77 void OnStream(already_AddRefed<nsIInputStream> aStream);
78
79 static void CancelRequest(nsIStreamListener* aListener, nsIChannel* aChannel,
80 nsresult aResult);
81
82 private:
83 ~PageThumbStreamGetter() = default;
84
85 nsCOMPtr<nsIURI> mURI;
86 nsCOMPtr<nsILoadInfo> mLoadInfo;
87 nsCOMPtr<nsIStreamListener> mListener;
88 nsCOMPtr<nsIChannel> mChannel;
89 nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget;
90 nsCOMPtr<nsIInputStreamPump> mPump;
91 bool mCanceled{false};
92 nsresult mStatus{NS_OK};
93 };
94
NS_IMPL_ISUPPORTS(PageThumbStreamGetter,nsICancelable)95 NS_IMPL_ISUPPORTS(PageThumbStreamGetter, nsICancelable)
96
97 // Request an input stream from the parent.
98 RequestOrReason PageThumbStreamGetter::GetAsync(nsIStreamListener* aListener,
99 nsIChannel* aChannel) {
100 MOZ_ASSERT(IsNeckoChild());
101 MOZ_ASSERT(mMainThreadEventTarget);
102
103 mListener = aListener;
104 mChannel = aChannel;
105
106 nsCOMPtr<nsICancelable> cancelableRequest(this);
107
108 RefPtr<PageThumbStreamGetter> self = this;
109
110 // Request an input stream for this moz-page-thumb URI.
111 gNeckoChild->SendGetPageThumbStream(mURI)->Then(
112 mMainThreadEventTarget, __func__,
113 [self](const RefPtr<nsIInputStream>& stream) {
114 self->OnStream(do_AddRef(stream));
115 },
116 [self](const mozilla::ipc::ResponseRejectReason) {
117 self->OnStream(nullptr);
118 });
119 return RequestOrCancelable(WrapNotNull(cancelableRequest));
120 }
121
122 // Called to cancel the ongoing async request.
123 NS_IMETHODIMP
Cancel(nsresult aStatus)124 PageThumbStreamGetter::Cancel(nsresult aStatus) {
125 if (mCanceled) {
126 return NS_OK;
127 }
128
129 mCanceled = true;
130 mStatus = aStatus;
131
132 if (mPump) {
133 mPump->Cancel(aStatus);
134 mPump = nullptr;
135 }
136
137 return NS_OK;
138 }
139
140 // static
CancelRequest(nsIStreamListener * aListener,nsIChannel * aChannel,nsresult aResult)141 void PageThumbStreamGetter::CancelRequest(nsIStreamListener* aListener,
142 nsIChannel* aChannel,
143 nsresult aResult) {
144 MOZ_ASSERT(aListener);
145 MOZ_ASSERT(aChannel);
146
147 aListener->OnStartRequest(aChannel);
148 aListener->OnStopRequest(aChannel, aResult);
149 aChannel->Cancel(NS_BINDING_ABORTED);
150 }
151
152 // Handle an input stream sent from the parent.
OnStream(already_AddRefed<nsIInputStream> aStream)153 void PageThumbStreamGetter::OnStream(already_AddRefed<nsIInputStream> aStream) {
154 MOZ_ASSERT(IsNeckoChild());
155 MOZ_ASSERT(mChannel);
156 MOZ_ASSERT(mListener);
157 MOZ_ASSERT(mMainThreadEventTarget);
158
159 nsCOMPtr<nsIInputStream> stream = std::move(aStream);
160 nsCOMPtr<nsIChannel> channel = std::move(mChannel);
161
162 // We must keep an owning reference to the listener until we pass it on
163 // to AsyncRead.
164 nsCOMPtr<nsIStreamListener> listener = mListener.forget();
165
166 if (mCanceled) {
167 // The channel that has created this stream getter has been canceled.
168 CancelRequest(listener, channel, mStatus);
169 return;
170 }
171
172 if (!stream) {
173 // The parent didn't send us back a stream.
174 CancelRequest(listener, channel, NS_ERROR_FILE_ACCESS_DENIED);
175 return;
176 }
177
178 nsCOMPtr<nsIInputStreamPump> pump;
179 nsresult rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget(), 0,
180 0, false, mMainThreadEventTarget);
181 if (NS_FAILED(rv)) {
182 CancelRequest(listener, channel, rv);
183 return;
184 }
185
186 rv = pump->AsyncRead(listener);
187 if (NS_FAILED(rv)) {
188 CancelRequest(listener, channel, rv);
189 return;
190 }
191
192 mPump = pump;
193 }
194
NS_IMPL_QUERY_INTERFACE(PageThumbProtocolHandler,nsISubstitutingProtocolHandler,nsIProtocolHandler,nsIProtocolHandlerWithDynamicFlags,nsISupportsWeakReference)195 NS_IMPL_QUERY_INTERFACE(PageThumbProtocolHandler,
196 nsISubstitutingProtocolHandler, nsIProtocolHandler,
197 nsIProtocolHandlerWithDynamicFlags,
198 nsISupportsWeakReference)
199 NS_IMPL_ADDREF_INHERITED(PageThumbProtocolHandler, SubstitutingProtocolHandler)
200 NS_IMPL_RELEASE_INHERITED(PageThumbProtocolHandler, SubstitutingProtocolHandler)
201
202 already_AddRefed<PageThumbProtocolHandler>
203 PageThumbProtocolHandler::GetSingleton() {
204 if (!sSingleton) {
205 sSingleton = new PageThumbProtocolHandler();
206 ClearOnShutdown(&sSingleton);
207 }
208
209 return do_AddRef(sSingleton);
210 }
211
PageThumbProtocolHandler()212 PageThumbProtocolHandler::PageThumbProtocolHandler()
213 : SubstitutingProtocolHandler(PAGE_THUMB_SCHEME) {}
214
GetFlagsForURI(nsIURI * aURI,uint32_t * aFlags)215 nsresult PageThumbProtocolHandler::GetFlagsForURI(nsIURI* aURI,
216 uint32_t* aFlags) {
217 // A moz-page-thumb URI is only loadable by chrome pages in the parent
218 // process, or privileged content running in the privileged about content
219 // process.
220 *aFlags = URI_STD | URI_IS_UI_RESOURCE | URI_IS_LOCAL_RESOURCE |
221 URI_NORELATIVE | URI_NOAUTH;
222
223 return NS_OK;
224 }
225
NewStream(nsIURI * aChildURI,bool * aTerminateSender)226 RefPtr<PageThumbStreamPromise> PageThumbProtocolHandler::NewStream(
227 nsIURI* aChildURI, bool* aTerminateSender) {
228 MOZ_ASSERT(!IsNeckoChild());
229 MOZ_ASSERT(NS_IsMainThread());
230
231 if (!aChildURI || !aTerminateSender) {
232 return PageThumbStreamPromise::CreateAndReject(NS_ERROR_INVALID_ARG,
233 __func__);
234 }
235
236 *aTerminateSender = true;
237 nsresult rv;
238
239 // We should never receive a URI that isn't for a moz-page-thumb because
240 // these requests ordinarily come from the child's PageThumbProtocolHandler.
241 // Ensure this request is for a moz-page-thumb URI. A compromised child
242 // process could send us any URI.
243 bool isPageThumbScheme = false;
244 if (NS_FAILED(aChildURI->SchemeIs(PAGE_THUMB_SCHEME, &isPageThumbScheme)) ||
245 !isPageThumbScheme) {
246 return PageThumbStreamPromise::CreateAndReject(NS_ERROR_UNKNOWN_PROTOCOL,
247 __func__);
248 }
249
250 // We should never receive a URI that does not have "thumbnails" as the host.
251 nsAutoCString host;
252 if (NS_FAILED(aChildURI->GetAsciiHost(host)) ||
253 !host.EqualsLiteral(PAGE_THUMB_HOST)) {
254 return PageThumbStreamPromise::CreateAndReject(NS_ERROR_UNEXPECTED,
255 __func__);
256 }
257
258 // For errors after this point, we want to propagate the error to
259 // the child, but we don't force the child process to be terminated.
260 *aTerminateSender = false;
261
262 // Make sure the child URI resolves to a file URI. We will then get a file
263 // channel for the request. The resultant channel should be a file channel
264 // because we only request remote streams for resource loads where the URI
265 // resolves to a file.
266 nsAutoCString resolvedSpec;
267 rv = ResolveURI(aChildURI, resolvedSpec);
268 if (NS_FAILED(rv)) {
269 return PageThumbStreamPromise::CreateAndReject(rv, __func__);
270 }
271
272 nsAutoCString resolvedScheme;
273 rv = net_ExtractURLScheme(resolvedSpec, resolvedScheme);
274 if (NS_FAILED(rv) || !resolvedScheme.EqualsLiteral("file")) {
275 return PageThumbStreamPromise::CreateAndReject(NS_ERROR_UNEXPECTED,
276 __func__);
277 }
278
279 nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
280 if (NS_FAILED(rv)) {
281 return PageThumbStreamPromise::CreateAndReject(rv, __func__);
282 }
283
284 nsCOMPtr<nsIURI> resolvedURI;
285 rv = ioService->NewURI(resolvedSpec, nullptr, nullptr,
286 getter_AddRefs(resolvedURI));
287 if (NS_FAILED(rv)) {
288 return PageThumbStreamPromise::CreateAndReject(rv, __func__);
289 }
290
291 // We use the system principal to get a file channel for the request,
292 // but only after we've checked (above) that the child URI is of
293 // moz-page-thumb scheme and that the URI host matches PAGE_THUMB_HOST.
294 nsCOMPtr<nsIChannel> channel;
295 rv = NS_NewChannel(getter_AddRefs(channel), resolvedURI,
296 nsContentUtils::GetSystemPrincipal(),
297 nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
298 nsIContentPolicy::TYPE_OTHER);
299 if (NS_FAILED(rv)) {
300 return PageThumbStreamPromise::CreateAndReject(rv, __func__);
301 }
302
303 auto promiseHolder = MakeUnique<MozPromiseHolder<PageThumbStreamPromise>>();
304 RefPtr<PageThumbStreamPromise> promise = promiseHolder->Ensure(__func__);
305
306 rv = NS_DispatchBackgroundTask(
307 NS_NewRunnableFunction(
308 "PageThumbProtocolHandler::NewStream",
309 [channel, holder = std::move(promiseHolder)]() {
310 nsresult rv;
311
312 nsCOMPtr<nsIFileChannel> fileChannel =
313 do_QueryInterface(channel, &rv);
314 if (NS_FAILED(rv)) {
315 holder->Reject(rv, __func__);
316 }
317
318 nsCOMPtr<nsIFile> requestedFile;
319 rv = fileChannel->GetFile(getter_AddRefs(requestedFile));
320 if (NS_FAILED(rv)) {
321 holder->Reject(rv, __func__);
322 return;
323 }
324
325 nsCOMPtr<nsIInputStream> inputStream;
326 rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream),
327 requestedFile, PR_RDONLY, -1);
328 if (NS_FAILED(rv)) {
329 holder->Reject(rv, __func__);
330 return;
331 }
332
333 holder->Resolve(inputStream, __func__);
334 }),
335 NS_DISPATCH_EVENT_MAY_BLOCK);
336
337 if (NS_FAILED(rv)) {
338 return PageThumbStreamPromise::CreateAndReject(rv, __func__);
339 }
340
341 return promise;
342 }
343
ResolveSpecialCases(const nsACString & aHost,const nsACString & aPath,const nsACString & aPathname,nsACString & aResult)344 bool PageThumbProtocolHandler::ResolveSpecialCases(const nsACString& aHost,
345 const nsACString& aPath,
346 const nsACString& aPathname,
347 nsACString& aResult) {
348 // This should match the scheme in PageThumbs.jsm. We will only resolve
349 // URIs for thumbnails generated by PageThumbs here.
350 if (!aHost.EqualsLiteral(PAGE_THUMB_HOST)) {
351 // moz-page-thumb should always have a "thumbnails" host. We do not intend
352 // to allow substitution rules to be created for moz-page-thumb.
353 return false;
354 }
355
356 // Regardless of the outcome, the scheme will be resolved to file://.
357 aResult.Assign("file://");
358
359 if (IsNeckoChild()) {
360 // We will resolve the URI in the parent if load is performed in the child
361 // because the child does not have access to the profile directory path.
362 // Technically we could retrieve the path from dom::ContentChild, but I
363 // would prefer to obtain the path from PageThumbsStorageService (which
364 // depends on OS.Path). Here, we resolve to the same URI, with the file://
365 // scheme. This won't ever be accessed directly by the content process,
366 // and is mainly used to keep the substitution protocol handler mechanism
367 // happy.
368 aResult.Append(aHost);
369 aResult.Append(aPath);
370 } else {
371 // Resolve the URI in the parent to the thumbnail file URI since we will
372 // attempt to open the channel to load the file after this.
373 nsAutoString thumbnailUrl;
374 nsresult rv = GetThumbnailPath(aPath, thumbnailUrl);
375 if (NS_WARN_IF(NS_FAILED(rv))) {
376 return false;
377 }
378
379 aResult.Append(NS_ConvertUTF16toUTF8(thumbnailUrl));
380 }
381
382 return true;
383 }
384
SubstituteChannel(nsIURI * aURI,nsILoadInfo * aLoadInfo,nsIChannel ** aRetVal)385 nsresult PageThumbProtocolHandler::SubstituteChannel(nsIURI* aURI,
386 nsILoadInfo* aLoadInfo,
387 nsIChannel** aRetVal) {
388 // Check if URI resolves to a file URI.
389 nsAutoCString resolvedSpec;
390 MOZ_TRY(ResolveURI(aURI, resolvedSpec));
391
392 nsAutoCString scheme;
393 MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme));
394
395 if (!scheme.EqualsLiteral("file")) {
396 NS_WARNING("moz-page-thumb URIs should only resolve to file URIs.");
397 return NS_ERROR_NO_INTERFACE;
398 }
399
400 // Load the URI remotely if accessed from a child.
401 if (IsNeckoChild()) {
402 MOZ_TRY(SubstituteRemoteChannel(aURI, aLoadInfo, aRetVal));
403 }
404
405 return NS_OK;
406 }
407
SubstituteRemoteChannel(nsIURI * aURI,nsILoadInfo * aLoadInfo,nsIChannel ** aRetVal)408 Result<Ok, nsresult> PageThumbProtocolHandler::SubstituteRemoteChannel(
409 nsIURI* aURI, nsILoadInfo* aLoadInfo, nsIChannel** aRetVal) {
410 MOZ_ASSERT(IsNeckoChild());
411 MOZ_TRY(aURI ? NS_OK : NS_ERROR_INVALID_ARG);
412 MOZ_TRY(aLoadInfo ? NS_OK : NS_ERROR_INVALID_ARG);
413
414 #ifdef DEBUG
415 nsAutoCString resolvedSpec;
416 MOZ_TRY(ResolveURI(aURI, resolvedSpec));
417
418 nsAutoCString scheme;
419 MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme));
420
421 MOZ_ASSERT(scheme.EqualsLiteral("file"));
422 #endif /* DEBUG */
423
424 RefPtr<PageThumbStreamGetter> streamGetter =
425 new PageThumbStreamGetter(aURI, aLoadInfo);
426
427 NewSimpleChannel(aURI, aLoadInfo, streamGetter, aRetVal);
428 return Ok();
429 }
430
GetThumbnailPath(const nsACString & aPath,nsString & aThumbnailPath)431 nsresult PageThumbProtocolHandler::GetThumbnailPath(const nsACString& aPath,
432 nsString& aThumbnailPath) {
433 MOZ_ASSERT(!IsNeckoChild());
434
435 // Ensures that the provided path has a query string. We will start parsing
436 // from there.
437 int32_t queryIndex = aPath.FindChar('?');
438 if (queryIndex <= 0) {
439 return NS_ERROR_MALFORMED_URI;
440 }
441
442 nsresult rv;
443
444 nsCOMPtr<nsIPageThumbsStorageService> pageThumbsStorage =
445 do_GetService("@mozilla.org/thumbnails/pagethumbs-service;1", &rv);
446 if (NS_WARN_IF(NS_FAILED(rv))) {
447 return rv;
448 }
449
450 // Extract URL from query string.
451 nsAutoString url;
452 bool found =
453 URLParams::Extract(Substring(aPath, queryIndex + 1), u"url"_ns, url);
454 if (!found || url.IsVoid()) {
455 return NS_ERROR_NOT_AVAILABLE;
456 }
457
458 // Use PageThumbsStorageService to get the local file path of the screenshot
459 // for the given URL.
460 rv = pageThumbsStorage->GetFilePathForURL(url, aThumbnailPath);
461
462 if (NS_WARN_IF(NS_FAILED(rv))) {
463 return rv;
464 }
465
466 return NS_OK;
467 }
468
469 // static
SetContentType(nsIURI * aURI,nsIChannel * aChannel)470 void PageThumbProtocolHandler::SetContentType(nsIURI* aURI,
471 nsIChannel* aChannel) {
472 nsresult rv;
473 nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1", &rv);
474 if (NS_SUCCEEDED(rv)) {
475 nsAutoCString contentType;
476 rv = mime->GetTypeFromURI(aURI, contentType);
477 if (NS_SUCCEEDED(rv)) {
478 Unused << aChannel->SetContentType(contentType);
479 }
480 }
481 }
482
483 // static
NewSimpleChannel(nsIURI * aURI,nsILoadInfo * aLoadinfo,PageThumbStreamGetter * aStreamGetter,nsIChannel ** aRetVal)484 void PageThumbProtocolHandler::NewSimpleChannel(
485 nsIURI* aURI, nsILoadInfo* aLoadinfo, PageThumbStreamGetter* aStreamGetter,
486 nsIChannel** aRetVal) {
487 nsCOMPtr<nsIChannel> channel = NS_NewSimpleChannel(
488 aURI, aLoadinfo, aStreamGetter,
489 [](nsIStreamListener* listener, nsIChannel* simpleChannel,
490 PageThumbStreamGetter* getter) -> RequestOrReason {
491 return getter->GetAsync(listener, simpleChannel);
492 });
493
494 SetContentType(aURI, channel);
495 channel.swap(*aRetVal);
496 }
497
498 #undef LOG
499
500 } // namespace net
501 } // namespace mozilla
502