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 "ExtensionProtocolHandler.h"
8 
9 #include "mozilla/BinarySearch.h"
10 #include "mozilla/ClearOnShutdown.h"
11 #include "mozilla/dom/ContentChild.h"
12 #include "mozilla/dom/Promise.h"
13 #include "mozilla/dom/Promise-inl.h"
14 #include "mozilla/ExtensionPolicyService.h"
15 #include "mozilla/FileUtils.h"
16 #include "mozilla/ipc/FileDescriptor.h"
17 #include "mozilla/ipc/IPCStreamUtils.h"
18 #include "mozilla/ipc/URIUtils.h"
19 #include "mozilla/net/NeckoChild.h"
20 #include "mozilla/RefPtr.h"
21 #include "mozilla/ResultExtensions.h"
22 
23 #include "FileDescriptorFile.h"
24 #include "LoadInfo.h"
25 #include "nsContentUtils.h"
26 #include "nsServiceManagerUtils.h"
27 #include "nsDirectoryServiceDefs.h"
28 #include "nsICancelable.h"
29 #include "nsIFile.h"
30 #include "nsIFileChannel.h"
31 #include "nsIFileStreams.h"
32 #include "nsIFileURL.h"
33 #include "nsIJARChannel.h"
34 #include "nsIMIMEService.h"
35 #include "nsIURL.h"
36 #include "nsIChannel.h"
37 #include "nsIInputStreamPump.h"
38 #include "nsIJARURI.h"
39 #include "nsIStreamListener.h"
40 #include "nsIInputStream.h"
41 #include "nsIStreamConverterService.h"
42 #include "nsNetUtil.h"
43 #include "nsReadableUtils.h"
44 #include "nsURLHelper.h"
45 #include "prio.h"
46 #include "SimpleChannel.h"
47 
48 #if defined(XP_WIN)
49 #  include "nsILocalFileWin.h"
50 #  include "WinUtils.h"
51 #endif
52 
53 #if defined(XP_MACOSX)
54 #  include "nsMacUtilsImpl.h"
55 #endif
56 
57 #define EXTENSION_SCHEME "moz-extension"
58 using mozilla::dom::Promise;
59 using mozilla::ipc::FileDescriptor;
60 
61 // A list of file extensions containing purely static data, which can be loaded
62 // from an extension before the extension is fully ready. The main purpose of
63 // this is to allow image resources from theme XPIs to load quickly during
64 // browser startup.
65 //
66 // The layout of this array is chosen in order to prevent the need for runtime
67 // relocation, which an array of char* pointers would require. It also has the
68 // benefit of being more compact when the difference in length between the
69 // longest and average string is less than 8 bytes. The length of the
70 // char[] array must match the size of the longest entry in the list.
71 //
72 // This list must be kept sorted.
73 static const char sStaticFileExtensions[][5] = {
74     // clang-format off
75   "bmp",
76   "gif",
77   "ico",
78   "jpeg",
79   "jpg",
80   "png",
81   "svg",
82     // clang-format on
83 };
84 
85 namespace mozilla {
86 
87 namespace net {
88 
89 using extensions::URLInfo;
90 
91 LazyLogModule gExtProtocolLog("ExtProtocol");
92 #undef LOG
93 #define LOG(...) MOZ_LOG(gExtProtocolLog, LogLevel::Debug, (__VA_ARGS__))
94 
95 StaticRefPtr<ExtensionProtocolHandler> ExtensionProtocolHandler::sSingleton;
96 
97 /**
98  * Helper class used with SimpleChannel to asynchronously obtain an input
99  * stream or file descriptor from the parent for a remote moz-extension load
100  * from the child.
101  */
102 class ExtensionStreamGetter final : public nsICancelable {
103   NS_DECL_ISUPPORTS
104   NS_DECL_NSICANCELABLE
105 
106  public:
107   // To use when getting a remote input stream for a resource
108   // in an unpacked extension.
ExtensionStreamGetter(nsIURI * aURI,nsILoadInfo * aLoadInfo)109   ExtensionStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo)
110       : mURI(aURI), mLoadInfo(aLoadInfo), mIsJarChannel(false) {
111     MOZ_ASSERT(aURI);
112     MOZ_ASSERT(aLoadInfo);
113 
114     SetupEventTarget();
115   }
116 
117   // To use when getting an FD for a packed extension JAR file
118   // in order to load a resource.
ExtensionStreamGetter(nsIURI * aURI,nsILoadInfo * aLoadInfo,already_AddRefed<nsIJARChannel> && aJarChannel,nsIFile * aJarFile)119   ExtensionStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo,
120                         already_AddRefed<nsIJARChannel>&& aJarChannel,
121                         nsIFile* aJarFile)
122       : mURI(aURI),
123         mLoadInfo(aLoadInfo),
124         mJarChannel(std::move(aJarChannel)),
125         mJarFile(aJarFile),
126         mIsJarChannel(true) {
127     MOZ_ASSERT(aURI);
128     MOZ_ASSERT(aLoadInfo);
129     MOZ_ASSERT(mJarChannel);
130     MOZ_ASSERT(aJarFile);
131 
132     SetupEventTarget();
133   }
134 
SetupEventTarget()135   void SetupEventTarget() {
136     mMainThreadEventTarget = nsContentUtils::GetEventTargetByLoadInfo(
137         mLoadInfo, TaskCategory::Other);
138     if (!mMainThreadEventTarget) {
139       mMainThreadEventTarget = GetMainThreadSerialEventTarget();
140     }
141   }
142 
143   // Get an input stream or file descriptor from the parent asynchronously.
144   RequestOrReason GetAsync(nsIStreamListener* aListener, nsIChannel* aChannel);
145 
146   // Handle an input stream being returned from the parent
147   void OnStream(already_AddRefed<nsIInputStream> aStream);
148 
149   // Handle file descriptor being returned from the parent
150   void OnFD(const FileDescriptor& aFD);
151 
152   static void CancelRequest(nsIStreamListener* aListener, nsIChannel* aChannel,
153                             nsresult aResult);
154 
155  private:
156   ~ExtensionStreamGetter() = default;
157 
158   nsCOMPtr<nsIURI> mURI;
159   nsCOMPtr<nsILoadInfo> mLoadInfo;
160   nsCOMPtr<nsIJARChannel> mJarChannel;
161   nsCOMPtr<nsIInputStreamPump> mPump;
162   nsCOMPtr<nsIFile> mJarFile;
163   nsCOMPtr<nsIStreamListener> mListener;
164   nsCOMPtr<nsIChannel> mChannel;
165   nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget;
166   bool mIsJarChannel;
167   bool mCanceled{false};
168   nsresult mStatus{NS_OK};
169 };
170 
171 NS_IMPL_ISUPPORTS(ExtensionStreamGetter, nsICancelable)
172 
173 class ExtensionJARFileOpener final : public nsISupports {
174  public:
ExtensionJARFileOpener(nsIFile * aFile,NeckoParent::GetExtensionFDResolver & aResolve)175   ExtensionJARFileOpener(nsIFile* aFile,
176                          NeckoParent::GetExtensionFDResolver& aResolve)
177       : mFile(aFile), mResolve(aResolve) {
178     MOZ_ASSERT(aFile);
179     MOZ_ASSERT(aResolve);
180   }
181 
OpenFile()182   NS_IMETHOD OpenFile() {
183     MOZ_ASSERT(!NS_IsMainThread());
184     AutoFDClose prFileDesc;
185 
186 #if defined(XP_WIN)
187     nsresult rv;
188     nsCOMPtr<nsILocalFileWin> winFile = do_QueryInterface(mFile, &rv);
189     MOZ_ASSERT(winFile);
190     if (NS_SUCCEEDED(rv)) {
191       rv = winFile->OpenNSPRFileDescShareDelete(PR_RDONLY, 0,
192                                                 &prFileDesc.rwget());
193     }
194 #else
195     nsresult rv = mFile->OpenNSPRFileDesc(PR_RDONLY, 0, &prFileDesc.rwget());
196 #endif /* XP_WIN */
197 
198     if (NS_SUCCEEDED(rv)) {
199       mFD = FileDescriptor(FileDescriptor::PlatformHandleType(
200           PR_FileDesc2NativeHandle(prFileDesc)));
201     }
202 
203     nsCOMPtr<nsIRunnable> event =
204         mozilla::NewRunnableMethod("ExtensionJarFileFDResolver", this,
205                                    &ExtensionJARFileOpener::SendBackFD);
206 
207     rv = NS_DispatchToMainThread(event, nsIEventTarget::DISPATCH_NORMAL);
208     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread");
209     return NS_OK;
210   }
211 
SendBackFD()212   NS_IMETHOD SendBackFD() {
213     MOZ_ASSERT(NS_IsMainThread());
214     mResolve(mFD);
215     mResolve = nullptr;
216     return NS_OK;
217   }
218 
219   NS_DECL_THREADSAFE_ISUPPORTS
220 
221  private:
222   virtual ~ExtensionJARFileOpener() = default;
223 
224   nsCOMPtr<nsIFile> mFile;
225   NeckoParent::GetExtensionFDResolver mResolve;
226   FileDescriptor mFD;
227 };
228 
NS_IMPL_ISUPPORTS(ExtensionJARFileOpener,nsISupports)229 NS_IMPL_ISUPPORTS(ExtensionJARFileOpener, nsISupports)
230 
231 // The amount of time, in milliseconds, that the file opener thread will remain
232 // allocated after it is used. This value chosen because to match other uses
233 // of LazyIdleThread.
234 #define DEFAULT_THREAD_TIMEOUT_MS 30000
235 
236 // Request an FD or input stream from the parent.
237 RequestOrReason ExtensionStreamGetter::GetAsync(nsIStreamListener* aListener,
238                                                 nsIChannel* aChannel) {
239   MOZ_ASSERT(IsNeckoChild());
240   MOZ_ASSERT(mMainThreadEventTarget);
241 
242   mListener = aListener;
243   mChannel = aChannel;
244 
245   nsCOMPtr<nsICancelable> cancelableRequest(this);
246 
247   RefPtr<ExtensionStreamGetter> self = this;
248   if (mIsJarChannel) {
249     // Request an FD for this moz-extension URI
250     gNeckoChild->SendGetExtensionFD(mURI)->Then(
251         mMainThreadEventTarget, __func__,
252         [self](const FileDescriptor& fd) { self->OnFD(fd); },
253         [self](const mozilla::ipc::ResponseRejectReason) {
254           self->OnFD(FileDescriptor());
255         });
256     return RequestOrCancelable(WrapNotNull(cancelableRequest));
257   }
258 
259   // Request an input stream for this moz-extension URI
260   gNeckoChild->SendGetExtensionStream(mURI)->Then(
261       mMainThreadEventTarget, __func__,
262       [self](const RefPtr<nsIInputStream>& stream) {
263         self->OnStream(do_AddRef(stream));
264       },
265       [self](const mozilla::ipc::ResponseRejectReason) {
266         self->OnStream(nullptr);
267       });
268   return RequestOrCancelable(WrapNotNull(cancelableRequest));
269 }
270 
271 // Called to cancel the ongoing async request.
272 NS_IMETHODIMP
Cancel(nsresult aStatus)273 ExtensionStreamGetter::Cancel(nsresult aStatus) {
274   if (mCanceled) {
275     return NS_OK;
276   }
277 
278   mCanceled = true;
279   mStatus = aStatus;
280 
281   if (mPump) {
282     mPump->Cancel(aStatus);
283     mPump = nullptr;
284   }
285 
286   if (mIsJarChannel && mJarChannel) {
287     mJarChannel->Cancel(aStatus);
288   }
289 
290   return NS_OK;
291 }
292 
293 // static
CancelRequest(nsIStreamListener * aListener,nsIChannel * aChannel,nsresult aResult)294 void ExtensionStreamGetter::CancelRequest(nsIStreamListener* aListener,
295                                           nsIChannel* aChannel,
296                                           nsresult aResult) {
297   MOZ_ASSERT(aListener);
298   MOZ_ASSERT(aChannel);
299 
300   aListener->OnStartRequest(aChannel);
301   aListener->OnStopRequest(aChannel, aResult);
302   aChannel->Cancel(NS_BINDING_ABORTED);
303 }
304 
305 // Handle an input stream sent from the parent.
OnStream(already_AddRefed<nsIInputStream> aStream)306 void ExtensionStreamGetter::OnStream(already_AddRefed<nsIInputStream> aStream) {
307   MOZ_ASSERT(IsNeckoChild());
308   MOZ_ASSERT(mChannel);
309   MOZ_ASSERT(mListener);
310   MOZ_ASSERT(mMainThreadEventTarget);
311 
312   nsCOMPtr<nsIInputStream> stream = std::move(aStream);
313   nsCOMPtr<nsIChannel> channel = std::move(mChannel);
314 
315   // We must keep an owning reference to the listener
316   // until we pass it on to AsyncRead.
317   nsCOMPtr<nsIStreamListener> listener = std::move(mListener);
318 
319   if (mCanceled) {
320     // The channel that has created this stream getter has been canceled.
321     CancelRequest(listener, channel, mStatus);
322     return;
323   }
324 
325   if (!stream) {
326     // The parent didn't send us back a stream.
327     CancelRequest(listener, channel, NS_ERROR_FILE_ACCESS_DENIED);
328     return;
329   }
330 
331   nsCOMPtr<nsIInputStreamPump> pump;
332   nsresult rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget(), 0,
333                                       0, false, mMainThreadEventTarget);
334   if (NS_FAILED(rv)) {
335     CancelRequest(listener, channel, rv);
336     return;
337   }
338 
339   rv = pump->AsyncRead(listener);
340   if (NS_FAILED(rv)) {
341     CancelRequest(listener, channel, rv);
342     return;
343   }
344 
345   mPump = pump;
346 }
347 
348 // Handle an FD sent from the parent.
OnFD(const FileDescriptor & aFD)349 void ExtensionStreamGetter::OnFD(const FileDescriptor& aFD) {
350   MOZ_ASSERT(IsNeckoChild());
351   MOZ_ASSERT(mChannel);
352   MOZ_ASSERT(mListener);
353 
354   nsCOMPtr<nsIChannel> channel = std::move(mChannel);
355 
356   // We must keep an owning reference to the listener
357   // until we pass it on to AsyncOpen.
358   nsCOMPtr<nsIStreamListener> listener = std::move(mListener);
359 
360   if (mCanceled) {
361     // The channel that has created this stream getter has been canceled.
362     CancelRequest(listener, channel, mStatus);
363     return;
364   }
365 
366   if (!aFD.IsValid()) {
367     // The parent didn't send us back a valid file descriptor.
368     CancelRequest(listener, channel, NS_ERROR_FILE_ACCESS_DENIED);
369     return;
370   }
371 
372   RefPtr<FileDescriptorFile> fdFile = new FileDescriptorFile(aFD, mJarFile);
373   mJarChannel->SetJarFile(fdFile);
374   nsresult rv = mJarChannel->AsyncOpen(listener);
375   if (NS_FAILED(rv)) {
376     CancelRequest(listener, channel, rv);
377   }
378 }
379 
NS_IMPL_QUERY_INTERFACE(ExtensionProtocolHandler,nsISubstitutingProtocolHandler,nsIProtocolHandler,nsIProtocolHandlerWithDynamicFlags,nsISupportsWeakReference)380 NS_IMPL_QUERY_INTERFACE(ExtensionProtocolHandler,
381                         nsISubstitutingProtocolHandler, nsIProtocolHandler,
382                         nsIProtocolHandlerWithDynamicFlags,
383                         nsISupportsWeakReference)
384 NS_IMPL_ADDREF_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler)
385 NS_IMPL_RELEASE_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler)
386 
387 already_AddRefed<ExtensionProtocolHandler>
388 ExtensionProtocolHandler::GetSingleton() {
389   if (!sSingleton) {
390     sSingleton = new ExtensionProtocolHandler();
391     ClearOnShutdown(&sSingleton);
392   }
393   return do_AddRef(sSingleton);
394 }
395 
ExtensionProtocolHandler()396 ExtensionProtocolHandler::ExtensionProtocolHandler()
397     : SubstitutingProtocolHandler(EXTENSION_SCHEME) {
398   // Note, extensions.webextensions.protocol.remote=false is for
399   // debugging purposes only. With process-level sandboxing, child
400   // processes (specifically content and extension processes), will
401   // not be able to load most moz-extension URI's when the pref is
402   // set to false.
403   mUseRemoteFileChannels =
404       IsNeckoChild() &&
405       Preferences::GetBool("extensions.webextensions.protocol.remote");
406 }
407 
EPS()408 static inline ExtensionPolicyService& EPS() {
409   return ExtensionPolicyService::GetSingleton();
410 }
411 
GetFlagsForURI(nsIURI * aURI,uint32_t * aFlags)412 nsresult ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI,
413                                                   uint32_t* aFlags) {
414   uint32_t flags =
415       URI_STD | URI_IS_LOCAL_RESOURCE | URI_IS_POTENTIALLY_TRUSTWORTHY;
416 
417   URLInfo url(aURI);
418   if (auto* policy = EPS().GetByURL(url)) {
419     // In general a moz-extension URI is only loadable by chrome, but a
420     // whitelisted subset are web-accessible (and cross-origin fetchable). Check
421     // that whitelist.  For Manifest V3 extensions, an additional whitelist
422     // for the source loading the url must be checked so we add the flag
423     // WEBEXT_URI_WEB_ACCESSIBLE, which is then checked in
424     // nsScriptSecurityManager.
425     if (policy->IsWebAccessiblePath(url.FilePath())) {
426       flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE |
427                WEBEXT_URI_WEB_ACCESSIBLE;
428     } else {
429       flags |= URI_DANGEROUS_TO_LOAD;
430     }
431 
432     // Disallow in private windows if the extension does not have permission.
433     if (!policy->PrivateBrowsingAllowed()) {
434       flags |= URI_DISALLOW_IN_PRIVATE_CONTEXT;
435     }
436   } else {
437     // In case there is no policy, then default to treating moz-extension URIs
438     // as unsafe and generally only allow chrome: to load such.
439     flags |= URI_DANGEROUS_TO_LOAD;
440   }
441 
442   *aFlags = flags;
443   return NS_OK;
444 }
445 
ResolveSpecialCases(const nsACString & aHost,const nsACString & aPath,const nsACString & aPathname,nsACString & aResult)446 bool ExtensionProtocolHandler::ResolveSpecialCases(const nsACString& aHost,
447                                                    const nsACString& aPath,
448                                                    const nsACString& aPathname,
449                                                    nsACString& aResult) {
450   MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(),
451                         "The ExtensionPolicyService is not thread safe");
452   // Create special moz-extension://foo/_generated_background_page.html page
453   // for all registered extensions. We can't just do this as a substitution
454   // because substitutions can only match on host.
455   if (!SubstitutingProtocolHandler::HasSubstitution(aHost)) {
456     return false;
457   }
458 
459   if (aPathname.EqualsLiteral("/_generated_background_page.html")) {
460     Unused << EPS().GetGeneratedBackgroundPageUrl(aHost, aResult);
461     return !aResult.IsEmpty();
462   }
463 
464   return false;
465 }
466 
467 // For file or JAR URI's, substitute in a remote channel.
SubstituteRemoteChannel(nsIURI * aURI,nsILoadInfo * aLoadInfo,nsIChannel ** aRetVal)468 Result<Ok, nsresult> ExtensionProtocolHandler::SubstituteRemoteChannel(
469     nsIURI* aURI, nsILoadInfo* aLoadInfo, nsIChannel** aRetVal) {
470   MOZ_ASSERT(IsNeckoChild());
471   MOZ_TRY(aURI ? NS_OK : NS_ERROR_INVALID_ARG);
472   MOZ_TRY(aLoadInfo ? NS_OK : NS_ERROR_INVALID_ARG);
473 
474   nsAutoCString unResolvedSpec;
475   MOZ_TRY(aURI->GetSpec(unResolvedSpec));
476 
477   nsAutoCString resolvedSpec;
478   MOZ_TRY(ResolveURI(aURI, resolvedSpec));
479 
480   // Use the target URI scheme to determine if this is a packed or unpacked
481   // extension URI. For unpacked extensions, we'll request an input stream
482   // from the parent. For a packed extension, we'll request a file descriptor
483   // for the JAR file.
484   nsAutoCString scheme;
485   MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme));
486 
487   if (scheme.EqualsLiteral("file")) {
488     // Unpacked extension
489     SubstituteRemoteFileChannel(aURI, aLoadInfo, resolvedSpec, aRetVal);
490     return Ok();
491   }
492 
493   if (scheme.EqualsLiteral("jar")) {
494     // Packed extension
495     return SubstituteRemoteJarChannel(aURI, aLoadInfo, resolvedSpec, aRetVal);
496   }
497 
498   // Only unpacked resource files and JAR files are remoted.
499   // No other moz-extension loads should be reading from the filesystem.
500   return Ok();
501 }
502 
OpenWhenReady(Promise * aPromise,nsIStreamListener * aListener,nsIChannel * aChannel,const std::function<nsresult (nsIStreamListener *,nsIChannel *)> & aCallback)503 void OpenWhenReady(
504     Promise* aPromise, nsIStreamListener* aListener, nsIChannel* aChannel,
505     const std::function<nsresult(nsIStreamListener*, nsIChannel*)>& aCallback) {
506   nsCOMPtr<nsIStreamListener> listener(aListener);
507   nsCOMPtr<nsIChannel> channel(aChannel);
508 
509   Unused << aPromise->ThenWithCycleCollectedArgs(
510       [channel, aCallback](
511           JSContext* aCx, JS::HandleValue aValue,
512           nsIStreamListener* aListener) -> already_AddRefed<Promise> {
513         nsresult rv = aCallback(aListener, channel);
514         if (NS_FAILED(rv)) {
515           ExtensionStreamGetter::CancelRequest(aListener, channel, rv);
516         }
517         return nullptr;
518       },
519       listener);
520 }
521 
SubstituteChannel(nsIURI * aURI,nsILoadInfo * aLoadInfo,nsIChannel ** result)522 nsresult ExtensionProtocolHandler::SubstituteChannel(nsIURI* aURI,
523                                                      nsILoadInfo* aLoadInfo,
524                                                      nsIChannel** result) {
525   if (mUseRemoteFileChannels) {
526     MOZ_TRY(SubstituteRemoteChannel(aURI, aLoadInfo, result));
527   }
528 
529   auto* policy = EPS().GetByURL(aURI);
530   NS_ENSURE_TRUE(policy, NS_ERROR_UNEXPECTED);
531 
532   RefPtr<dom::Promise> readyPromise(policy->ReadyPromise());
533 
534   nsresult rv;
535   nsCOMPtr<nsIURL> url = do_QueryInterface(aURI, &rv);
536   MOZ_TRY(rv);
537 
538   nsAutoCString ext;
539   MOZ_TRY(url->GetFileExtension(ext));
540   ToLowerCase(ext);
541 
542   nsCOMPtr<nsIChannel> channel;
543   if (ext.EqualsLiteral("css")) {
544     // Filter CSS files to replace locale message tokens with localized strings.
545     static const auto convert = [](nsIStreamListener* listener,
546                                    nsIChannel* channel,
547                                    nsIChannel* origChannel) -> nsresult {
548       nsresult rv;
549       nsCOMPtr<nsIStreamConverterService> convService =
550           do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv);
551       MOZ_TRY(rv);
552 
553       nsCOMPtr<nsIURI> uri;
554       MOZ_TRY(channel->GetURI(getter_AddRefs(uri)));
555 
556       const char* kFromType = "application/vnd.mozilla.webext.unlocalized";
557       const char* kToType = "text/css";
558 
559       nsCOMPtr<nsIStreamListener> converter;
560       MOZ_TRY(convService->AsyncConvertData(kFromType, kToType, listener, uri,
561                                             getter_AddRefs(converter)));
562 
563       return origChannel->AsyncOpen(converter);
564     };
565 
566     channel = NS_NewSimpleChannel(
567         aURI, aLoadInfo, *result,
568         [readyPromise](nsIStreamListener* listener, nsIChannel* channel,
569                        nsIChannel* origChannel) -> RequestOrReason {
570           if (readyPromise) {
571             nsCOMPtr<nsIChannel> chan(channel);
572             OpenWhenReady(
573                 readyPromise, listener, origChannel,
574                 [chan](nsIStreamListener* aListener, nsIChannel* aChannel) {
575                   return convert(aListener, chan, aChannel);
576                 });
577           } else {
578             MOZ_TRY(convert(listener, channel, origChannel));
579           }
580           nsCOMPtr<nsIRequest> request(origChannel);
581           return RequestOrCancelable(WrapNotNull(request));
582         });
583   } else if (readyPromise) {
584     size_t matchIdx;
585     if (BinarySearchIf(
586             sStaticFileExtensions, 0, ArrayLength(sStaticFileExtensions),
587             [&ext](const char* aOther) { return ext.Compare(aOther); },
588             &matchIdx)) {
589       // This is a static resource that shouldn't depend on the extension being
590       // ready. Don't bother waiting for it.
591       return NS_OK;
592     }
593 
594     channel = NS_NewSimpleChannel(
595         aURI, aLoadInfo, *result,
596         [readyPromise](nsIStreamListener* listener, nsIChannel* channel,
597                        nsIChannel* origChannel) -> RequestOrReason {
598           OpenWhenReady(readyPromise, listener, origChannel,
599                         [](nsIStreamListener* aListener, nsIChannel* aChannel) {
600                           return aChannel->AsyncOpen(aListener);
601                         });
602 
603           nsCOMPtr<nsIRequest> request(origChannel);
604           return RequestOrCancelable(WrapNotNull(request));
605         });
606   } else {
607     return NS_OK;
608   }
609 
610   NS_ENSURE_TRUE(channel, NS_ERROR_OUT_OF_MEMORY);
611   if (aLoadInfo) {
612     nsCOMPtr<nsILoadInfo> loadInfo =
613         static_cast<LoadInfo*>(aLoadInfo)->CloneForNewRequest();
614     (*result)->SetLoadInfo(loadInfo);
615   }
616 
617   channel.swap(*result);
618   return NS_OK;
619 }
620 
AllowExternalResource(nsIFile * aExtensionDir,nsIFile * aRequestedFile)621 Result<bool, nsresult> ExtensionProtocolHandler::AllowExternalResource(
622     nsIFile* aExtensionDir, nsIFile* aRequestedFile) {
623   MOZ_ASSERT(!IsNeckoChild());
624 
625 #if defined(XP_WIN)
626   // On Windows, dev builds don't use symlinks so we never need to
627   // allow a resource from outside of the extension dir.
628   return false;
629 #else
630   if (!mozilla::IsDevelopmentBuild()) {
631     return false;
632   }
633 
634   // On Mac and Linux unpackaged dev builds, system extensions use
635   // symlinks to point to resources in the repo dir which we have to
636   // allow loading. Before we allow an unpacked extension to load a
637   // resource outside of the extension dir, we make sure the extension
638   // dir is within the app directory.
639   bool result;
640   MOZ_TRY_VAR(result, AppDirContains(aExtensionDir));
641   if (!result) {
642     return false;
643   }
644 
645 #  if defined(XP_MACOSX)
646   // Additionally, on Mac dev builds, we make sure that the requested
647   // resource is within the repo dir. We don't perform this check on Linux
648   // because we don't have a reliable path to the repo dir on Linux.
649   return DevRepoContains(aRequestedFile);
650 #  else /* XP_MACOSX */
651   return true;
652 #  endif
653 #endif /* defined(XP_WIN) */
654 }
655 
656 #if defined(XP_MACOSX)
657 // The |aRequestedFile| argument must already be Normalize()'d
DevRepoContains(nsIFile * aRequestedFile)658 Result<bool, nsresult> ExtensionProtocolHandler::DevRepoContains(
659     nsIFile* aRequestedFile) {
660   MOZ_ASSERT(mozilla::IsDevelopmentBuild());
661   MOZ_ASSERT(!IsNeckoChild());
662 
663   // On the first invocation, set mDevRepo
664   if (!mAlreadyCheckedDevRepo) {
665     mAlreadyCheckedDevRepo = true;
666     MOZ_TRY(nsMacUtilsImpl::GetRepoDir(getter_AddRefs(mDevRepo)));
667     if (MOZ_LOG_TEST(gExtProtocolLog, LogLevel::Debug)) {
668       nsAutoCString repoPath;
669       Unused << mDevRepo->GetNativePath(repoPath);
670       LOG("Repo path: %s", repoPath.get());
671     }
672   }
673 
674   bool result = false;
675   if (mDevRepo) {
676     MOZ_TRY(mDevRepo->Contains(aRequestedFile, &result));
677   }
678   return result;
679 }
680 #endif /* XP_MACOSX */
681 
682 #if !defined(XP_WIN)
AppDirContains(nsIFile * aExtensionDir)683 Result<bool, nsresult> ExtensionProtocolHandler::AppDirContains(
684     nsIFile* aExtensionDir) {
685   MOZ_ASSERT(mozilla::IsDevelopmentBuild());
686   MOZ_ASSERT(!IsNeckoChild());
687 
688   // On the first invocation, set mAppDir
689   if (!mAlreadyCheckedAppDir) {
690     mAlreadyCheckedAppDir = true;
691     MOZ_TRY(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(mAppDir)));
692     if (MOZ_LOG_TEST(gExtProtocolLog, LogLevel::Debug)) {
693       nsAutoCString appDirPath;
694       Unused << mAppDir->GetNativePath(appDirPath);
695       LOG("AppDir path: %s", appDirPath.get());
696     }
697   }
698 
699   bool result = false;
700   if (mAppDir) {
701     MOZ_TRY(mAppDir->Contains(aExtensionDir, &result));
702   }
703   return result;
704 }
705 #endif /* !defined(XP_WIN) */
706 
LogExternalResourceError(nsIFile * aExtensionDir,nsIFile * aRequestedFile)707 static void LogExternalResourceError(nsIFile* aExtensionDir,
708                                      nsIFile* aRequestedFile) {
709   MOZ_ASSERT(aExtensionDir);
710   MOZ_ASSERT(aRequestedFile);
711 
712   LOG("Rejecting external unpacked extension resource [%s] from "
713       "extension directory [%s]",
714       aRequestedFile->HumanReadablePath().get(),
715       aExtensionDir->HumanReadablePath().get());
716 }
717 
NewStream(nsIURI * aChildURI,bool * aTerminateSender)718 Result<nsCOMPtr<nsIInputStream>, nsresult> ExtensionProtocolHandler::NewStream(
719     nsIURI* aChildURI, bool* aTerminateSender) {
720   MOZ_ASSERT(!IsNeckoChild());
721   MOZ_TRY(aChildURI ? NS_OK : NS_ERROR_INVALID_ARG);
722   MOZ_TRY(aTerminateSender ? NS_OK : NS_ERROR_INVALID_ARG);
723 
724   *aTerminateSender = true;
725   nsresult rv;
726 
727   // We should never receive a URI that isn't for a moz-extension because
728   // these requests ordinarily come from the child's ExtensionProtocolHandler.
729   // Ensure this request is for a moz-extension URI. A rogue child process
730   // could send us any URI.
731   if (!aChildURI->SchemeIs(EXTENSION_SCHEME)) {
732     return Err(NS_ERROR_UNKNOWN_PROTOCOL);
733   }
734 
735   // For errors after this point, we want to propagate the error to
736   // the child, but we don't force the child to be terminated because
737   // the error is likely to be due to a bug in the extension.
738   *aTerminateSender = false;
739 
740   /*
741    * Make sure there is a substitution installed for the host found
742    * in the child's request URI and make sure the host resolves to
743    * a directory.
744    */
745 
746   nsAutoCString host;
747   MOZ_TRY(aChildURI->GetAsciiHost(host));
748 
749   // Lookup the directory this host string resolves to
750   nsCOMPtr<nsIURI> baseURI;
751   MOZ_TRY(GetSubstitution(host, getter_AddRefs(baseURI)));
752 
753   // The result should be a file URL for the extension base dir
754   nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(baseURI, &rv);
755   MOZ_TRY(rv);
756 
757   nsCOMPtr<nsIFile> extensionDir;
758   MOZ_TRY(fileURL->GetFile(getter_AddRefs(extensionDir)));
759 
760   bool isDirectory = false;
761   MOZ_TRY(extensionDir->IsDirectory(&isDirectory));
762   if (!isDirectory) {
763     // The host should map to a directory for unpacked extensions
764     return Err(NS_ERROR_FILE_NOT_DIRECTORY);
765   }
766 
767   // Make sure the child URI resolves to a file URI then get a file
768   // channel for the request. The resultant channel should be a
769   // file channel because we only request remote streams for unpacked
770   // extension resource loads where the URI resolves to a file.
771   nsAutoCString resolvedSpec;
772   MOZ_TRY(ResolveURI(aChildURI, resolvedSpec));
773 
774   nsAutoCString resolvedScheme;
775   MOZ_TRY(net_ExtractURLScheme(resolvedSpec, resolvedScheme));
776   if (!resolvedScheme.EqualsLiteral("file")) {
777     return Err(NS_ERROR_UNEXPECTED);
778   }
779 
780   nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
781   MOZ_TRY(rv);
782 
783   nsCOMPtr<nsIURI> resolvedURI;
784   MOZ_TRY(ioService->NewURI(resolvedSpec, nullptr, nullptr,
785                             getter_AddRefs(resolvedURI)));
786 
787   // We use the system principal to get a file channel for the request,
788   // but only after we've checked (above) that the child URI is of
789   // moz-extension scheme and that the URI host maps to a directory.
790   nsCOMPtr<nsIChannel> channel;
791   MOZ_TRY(NS_NewChannel(getter_AddRefs(channel), resolvedURI,
792                         nsContentUtils::GetSystemPrincipal(),
793                         nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
794                         nsIContentPolicy::TYPE_OTHER));
795 
796   nsCOMPtr<nsIFileChannel> fileChannel = do_QueryInterface(channel, &rv);
797   MOZ_TRY(rv);
798 
799   nsCOMPtr<nsIFile> requestedFile;
800   MOZ_TRY(fileChannel->GetFile(getter_AddRefs(requestedFile)));
801 
802   /*
803    * Make sure the file we resolved to is within the extension directory.
804    */
805 
806   // Normalize paths for sane comparisons. nsIFile::Contains depends on
807   // it for reliable subpath checks.
808   MOZ_TRY(extensionDir->Normalize());
809   MOZ_TRY(requestedFile->Normalize());
810 #if defined(XP_WIN)
811   if (!widget::WinUtils::ResolveJunctionPointsAndSymLinks(extensionDir) ||
812       !widget::WinUtils::ResolveJunctionPointsAndSymLinks(requestedFile)) {
813     return Err(NS_ERROR_FILE_ACCESS_DENIED);
814   }
815 #endif
816 
817   bool isResourceFromExtensionDir = false;
818   MOZ_TRY(extensionDir->Contains(requestedFile, &isResourceFromExtensionDir));
819   if (!isResourceFromExtensionDir) {
820     bool isAllowed;
821     MOZ_TRY_VAR(isAllowed, AllowExternalResource(extensionDir, requestedFile));
822     if (!isAllowed) {
823       LogExternalResourceError(extensionDir, requestedFile);
824       return Err(NS_ERROR_FILE_ACCESS_DENIED);
825     }
826   }
827 
828   nsCOMPtr<nsIInputStream> inputStream;
829   MOZ_TRY_VAR(inputStream,
830               NS_NewLocalFileInputStream(requestedFile, PR_RDONLY, -1,
831                                          nsIFileInputStream::DEFER_OPEN));
832 
833   return inputStream;
834 }
835 
NewFD(nsIURI * aChildURI,bool * aTerminateSender,NeckoParent::GetExtensionFDResolver & aResolve)836 Result<Ok, nsresult> ExtensionProtocolHandler::NewFD(
837     nsIURI* aChildURI, bool* aTerminateSender,
838     NeckoParent::GetExtensionFDResolver& aResolve) {
839   MOZ_ASSERT(!IsNeckoChild());
840   MOZ_TRY(aChildURI ? NS_OK : NS_ERROR_INVALID_ARG);
841   MOZ_TRY(aTerminateSender ? NS_OK : NS_ERROR_INVALID_ARG);
842 
843   *aTerminateSender = true;
844   nsresult rv;
845 
846   // Ensure this is a moz-extension URI
847   if (!aChildURI->SchemeIs(EXTENSION_SCHEME)) {
848     return Err(NS_ERROR_UNKNOWN_PROTOCOL);
849   }
850 
851   // For errors after this point, we want to propagate the error to
852   // the child, but we don't force the child to be terminated.
853   *aTerminateSender = false;
854 
855   nsAutoCString host;
856   MOZ_TRY(aChildURI->GetAsciiHost(host));
857 
858   // We expect the host string to map to a JAR file because the URI
859   // should refer to a web accessible resource for an enabled extension.
860   nsCOMPtr<nsIURI> subURI;
861   MOZ_TRY(GetSubstitution(host, getter_AddRefs(subURI)));
862 
863   nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(subURI, &rv);
864   MOZ_TRY(rv);
865 
866   nsCOMPtr<nsIURI> innerFileURI;
867   MOZ_TRY(jarURI->GetJARFile(getter_AddRefs(innerFileURI)));
868 
869   nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv);
870   MOZ_TRY(rv);
871 
872   nsCOMPtr<nsIFile> jarFile;
873   MOZ_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile)));
874 
875   if (!mFileOpenerThread) {
876     mFileOpenerThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS,
877                                            "ExtensionProtocolHandler"_ns);
878   }
879 
880   RefPtr<ExtensionJARFileOpener> fileOpener =
881       new ExtensionJARFileOpener(jarFile, aResolve);
882 
883   nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod(
884       "ExtensionJarFileOpener", fileOpener, &ExtensionJARFileOpener::OpenFile);
885 
886   MOZ_TRY(mFileOpenerThread->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL));
887 
888   return Ok();
889 }
890 
891 // Set the channel's content type using the provided URI's type
892 
893 // static
SetContentType(nsIURI * aURI,nsIChannel * aChannel)894 void ExtensionProtocolHandler::SetContentType(nsIURI* aURI,
895                                               nsIChannel* aChannel) {
896   nsresult rv;
897   nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1", &rv);
898   if (NS_SUCCEEDED(rv)) {
899     nsAutoCString contentType;
900     rv = mime->GetTypeFromURI(aURI, contentType);
901     if (NS_SUCCEEDED(rv)) {
902       Unused << aChannel->SetContentType(contentType);
903     }
904   }
905 }
906 
907 // Gets a SimpleChannel that wraps the provided ExtensionStreamGetter
908 
909 // static
NewSimpleChannel(nsIURI * aURI,nsILoadInfo * aLoadinfo,ExtensionStreamGetter * aStreamGetter,nsIChannel ** aRetVal)910 void ExtensionProtocolHandler::NewSimpleChannel(
911     nsIURI* aURI, nsILoadInfo* aLoadinfo, ExtensionStreamGetter* aStreamGetter,
912     nsIChannel** aRetVal) {
913   nsCOMPtr<nsIChannel> channel = NS_NewSimpleChannel(
914       aURI, aLoadinfo, aStreamGetter,
915       [](nsIStreamListener* listener, nsIChannel* simpleChannel,
916          ExtensionStreamGetter* getter) -> RequestOrReason {
917         return getter->GetAsync(listener, simpleChannel);
918       });
919 
920   SetContentType(aURI, channel);
921   channel.swap(*aRetVal);
922 }
923 
924 // Gets a SimpleChannel that wraps the provided channel
925 
926 // static
NewSimpleChannel(nsIURI * aURI,nsILoadInfo * aLoadinfo,nsIChannel * aChannel,nsIChannel ** aRetVal)927 void ExtensionProtocolHandler::NewSimpleChannel(nsIURI* aURI,
928                                                 nsILoadInfo* aLoadinfo,
929                                                 nsIChannel* aChannel,
930                                                 nsIChannel** aRetVal) {
931   nsCOMPtr<nsIChannel> channel = NS_NewSimpleChannel(
932       aURI, aLoadinfo, aChannel,
933       [](nsIStreamListener* listener, nsIChannel* simpleChannel,
934          nsIChannel* origChannel) -> RequestOrReason {
935         nsresult rv = origChannel->AsyncOpen(listener);
936         if (NS_FAILED(rv)) {
937           simpleChannel->Cancel(NS_BINDING_ABORTED);
938           return Err(rv);
939         }
940         nsCOMPtr<nsIRequest> request(origChannel);
941         return RequestOrCancelable(WrapNotNull(request));
942       });
943 
944   SetContentType(aURI, channel);
945   channel.swap(*aRetVal);
946 }
947 
SubstituteRemoteFileChannel(nsIURI * aURI,nsILoadInfo * aLoadinfo,nsACString & aResolvedFileSpec,nsIChannel ** aRetVal)948 void ExtensionProtocolHandler::SubstituteRemoteFileChannel(
949     nsIURI* aURI, nsILoadInfo* aLoadinfo, nsACString& aResolvedFileSpec,
950     nsIChannel** aRetVal) {
951   MOZ_ASSERT(IsNeckoChild());
952 
953   RefPtr<ExtensionStreamGetter> streamGetter =
954       new ExtensionStreamGetter(aURI, aLoadinfo);
955 
956   NewSimpleChannel(aURI, aLoadinfo, streamGetter, aRetVal);
957 }
958 
LogCacheCheck(const nsIJARChannel * aJarChannel,nsIJARURI * aJarURI,bool aIsCached)959 static Result<Ok, nsresult> LogCacheCheck(const nsIJARChannel* aJarChannel,
960                                           nsIJARURI* aJarURI, bool aIsCached) {
961   nsresult rv;
962 
963   nsCOMPtr<nsIURI> innerFileURI;
964   MOZ_TRY(aJarURI->GetJARFile(getter_AddRefs(innerFileURI)));
965 
966   nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv);
967   MOZ_TRY(rv);
968 
969   nsCOMPtr<nsIFile> jarFile;
970   MOZ_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile)));
971 
972   nsAutoCString uriSpec, jarSpec;
973   Unused << aJarURI->GetSpec(uriSpec);
974   Unused << innerFileURI->GetSpec(jarSpec);
975   LOG("[JARChannel %p] Cache %s: %s (%s)", aJarChannel,
976       aIsCached ? "hit" : "miss", uriSpec.get(), jarSpec.get());
977 
978   return Ok();
979 }
980 
SubstituteRemoteJarChannel(nsIURI * aURI,nsILoadInfo * aLoadinfo,nsACString & aResolvedSpec,nsIChannel ** aRetVal)981 Result<Ok, nsresult> ExtensionProtocolHandler::SubstituteRemoteJarChannel(
982     nsIURI* aURI, nsILoadInfo* aLoadinfo, nsACString& aResolvedSpec,
983     nsIChannel** aRetVal) {
984   MOZ_ASSERT(IsNeckoChild());
985   nsresult rv;
986 
987   // Build a JAR URI for this jar:file:// URI and use it to extract the
988   // inner file URI.
989   nsCOMPtr<nsIURI> uri;
990   MOZ_TRY(NS_NewURI(getter_AddRefs(uri), aResolvedSpec));
991 
992   nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri, &rv);
993   MOZ_TRY(rv);
994 
995   nsCOMPtr<nsIJARChannel> jarChannel = do_QueryInterface(*aRetVal, &rv);
996   MOZ_TRY(rv);
997 
998   bool isCached = false;
999   MOZ_TRY(jarChannel->EnsureCached(&isCached));
1000   if (MOZ_LOG_TEST(gExtProtocolLog, LogLevel::Debug)) {
1001     Unused << LogCacheCheck(jarChannel, jarURI, isCached);
1002   }
1003 
1004   if (isCached) {
1005     // Using a SimpleChannel with an ExtensionStreamGetter here (like the
1006     // non-cached JAR case) isn't needed to load the extension resource
1007     // because we don't need to ask the parent for an FD for the JAR, but
1008     // wrapping the JARChannel in a SimpleChannel allows HTTP forwarding to
1009     // moz-extension URI's to work because HTTP forwarding requires the
1010     // target channel implement nsIChildChannel.
1011     NewSimpleChannel(aURI, aLoadinfo, jarChannel.get(), aRetVal);
1012     return Ok();
1013   }
1014 
1015   nsCOMPtr<nsIURI> innerFileURI;
1016   MOZ_TRY(jarURI->GetJARFile(getter_AddRefs(innerFileURI)));
1017 
1018   nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv);
1019   MOZ_TRY(rv);
1020 
1021   nsCOMPtr<nsIFile> jarFile;
1022   MOZ_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile)));
1023 
1024   RefPtr<ExtensionStreamGetter> streamGetter =
1025       new ExtensionStreamGetter(aURI, aLoadinfo, jarChannel.forget(), jarFile);
1026 
1027   NewSimpleChannel(aURI, aLoadinfo, streamGetter, aRetVal);
1028   return Ok();
1029 }
1030 
1031 }  // namespace net
1032 }  // namespace mozilla
1033