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