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