diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /netwerk/protocol/res/ExtensionProtocolHandler.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/protocol/res/ExtensionProtocolHandler.cpp')
-rw-r--r-- | netwerk/protocol/res/ExtensionProtocolHandler.cpp | 1041 |
1 files changed, 1041 insertions, 0 deletions
diff --git a/netwerk/protocol/res/ExtensionProtocolHandler.cpp b/netwerk/protocol/res/ExtensionProtocolHandler.cpp new file mode 100644 index 0000000000..5f03d344af --- /dev/null +++ b/netwerk/protocol/res/ExtensionProtocolHandler.cpp @@ -0,0 +1,1041 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ExtensionProtocolHandler.h" + +#include "mozilla/BinarySearch.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/FileUtils.h" +#include "mozilla/ipc/FileDescriptor.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/Omnijar.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ResultExtensions.h" + +#include "FileDescriptorFile.h" +#include "LoadInfo.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsICancelable.h" +#include "nsIFile.h" +#include "nsIFileChannel.h" +#include "nsIFileStreams.h" +#include "nsIFileURL.h" +#include "nsIJARChannel.h" +#include "nsIMIMEService.h" +#include "nsIURL.h" +#include "nsIChannel.h" +#include "nsIInputStreamPump.h" +#include "nsIJARURI.h" +#include "nsIStreamListener.h" +#include "nsIInputStream.h" +#include "nsIStreamConverterService.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsURLHelper.h" +#include "prio.h" +#include "SimpleChannel.h" + +#if defined(XP_WIN) +# include "nsILocalFileWin.h" +# include "WinUtils.h" +#endif + +#if defined(XP_MACOSX) +# include "nsMacUtilsImpl.h" +#endif + +#define EXTENSION_SCHEME "moz-extension" +using mozilla::dom::Promise; +using mozilla::ipc::FileDescriptor; + +// A list of file extensions containing purely static data, which can be loaded +// from an extension before the extension is fully ready. The main purpose of +// this is to allow image resources from theme XPIs to load quickly during +// browser startup. +// +// The layout of this array is chosen in order to prevent the need for runtime +// relocation, which an array of char* pointers would require. It also has the +// benefit of being more compact when the difference in length between the +// longest and average string is less than 8 bytes. The length of the +// char[] array must match the size of the longest entry in the list. +// +// This list must be kept sorted. +static const char sStaticFileExtensions[][5] = { + // clang-format off + "bmp", + "gif", + "ico", + "jpeg", + "jpg", + "png", + "svg", + // clang-format on +}; + +namespace mozilla { + +namespace net { + +using extensions::URLInfo; + +LazyLogModule gExtProtocolLog("ExtProtocol"); +#undef LOG +#define LOG(...) MOZ_LOG(gExtProtocolLog, LogLevel::Debug, (__VA_ARGS__)) + +StaticRefPtr<ExtensionProtocolHandler> ExtensionProtocolHandler::sSingleton; + +/** + * Helper class used with SimpleChannel to asynchronously obtain an input + * stream or file descriptor from the parent for a remote moz-extension load + * from the child. + */ +class ExtensionStreamGetter final : public nsICancelable { + NS_DECL_ISUPPORTS + NS_DECL_NSICANCELABLE + + public: + // To use when getting a remote input stream for a resource + // in an unpacked extension. + ExtensionStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo) + : mURI(aURI), mLoadInfo(aLoadInfo), mIsJarChannel(false) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aLoadInfo); + + SetupEventTarget(); + } + + // To use when getting an FD for a packed extension JAR file + // in order to load a resource. + ExtensionStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo, + already_AddRefed<nsIJARChannel>&& aJarChannel, + nsIFile* aJarFile) + : mURI(aURI), + mLoadInfo(aLoadInfo), + mJarChannel(std::move(aJarChannel)), + mJarFile(aJarFile), + mIsJarChannel(true) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aLoadInfo); + MOZ_ASSERT(mJarChannel); + MOZ_ASSERT(aJarFile); + + SetupEventTarget(); + } + + void SetupEventTarget() { + mMainThreadEventTarget = nsContentUtils::GetEventTargetByLoadInfo( + mLoadInfo, TaskCategory::Other); + if (!mMainThreadEventTarget) { + mMainThreadEventTarget = GetMainThreadSerialEventTarget(); + } + } + + // Get an input stream or file descriptor from the parent asynchronously. + RequestOrReason GetAsync(nsIStreamListener* aListener, nsIChannel* aChannel); + + // Handle an input stream being returned from the parent + void OnStream(already_AddRefed<nsIInputStream> aStream); + + // Handle file descriptor being returned from the parent + void OnFD(const FileDescriptor& aFD); + + static void CancelRequest(nsIStreamListener* aListener, nsIChannel* aChannel, + nsresult aResult); + + private: + ~ExtensionStreamGetter() = default; + + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsILoadInfo> mLoadInfo; + nsCOMPtr<nsIJARChannel> mJarChannel; + nsCOMPtr<nsIInputStreamPump> mPump; + nsCOMPtr<nsIFile> mJarFile; + nsCOMPtr<nsIStreamListener> mListener; + nsCOMPtr<nsIChannel> mChannel; + nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget; + bool mIsJarChannel; + bool mCanceled{false}; + nsresult mStatus{NS_OK}; +}; + +NS_IMPL_ISUPPORTS(ExtensionStreamGetter, nsICancelable) + +class ExtensionJARFileOpener final : public nsISupports { + public: + ExtensionJARFileOpener(nsIFile* aFile, + NeckoParent::GetExtensionFDResolver& aResolve) + : mFile(aFile), mResolve(aResolve) { + MOZ_ASSERT(aFile); + MOZ_ASSERT(aResolve); + } + + NS_IMETHOD OpenFile() { + MOZ_ASSERT(!NS_IsMainThread()); + AutoFDClose prFileDesc; + +#if defined(XP_WIN) + nsresult rv; + nsCOMPtr<nsILocalFileWin> winFile = do_QueryInterface(mFile, &rv); + MOZ_ASSERT(winFile); + if (NS_SUCCEEDED(rv)) { + rv = winFile->OpenNSPRFileDescShareDelete(PR_RDONLY, 0, + &prFileDesc.rwget()); + } +#else + nsresult rv = mFile->OpenNSPRFileDesc(PR_RDONLY, 0, &prFileDesc.rwget()); +#endif /* XP_WIN */ + + if (NS_SUCCEEDED(rv)) { + mFD = FileDescriptor(FileDescriptor::PlatformHandleType( + PR_FileDesc2NativeHandle(prFileDesc))); + } + + nsCOMPtr<nsIRunnable> event = + mozilla::NewRunnableMethod("ExtensionJarFileFDResolver", this, + &ExtensionJARFileOpener::SendBackFD); + + rv = NS_DispatchToMainThread(event, nsIEventTarget::DISPATCH_NORMAL); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread"); + return NS_OK; + } + + NS_IMETHOD SendBackFD() { + MOZ_ASSERT(NS_IsMainThread()); + mResolve(mFD); + mResolve = nullptr; + return NS_OK; + } + + NS_DECL_THREADSAFE_ISUPPORTS + + private: + virtual ~ExtensionJARFileOpener() = default; + + nsCOMPtr<nsIFile> mFile; + NeckoParent::GetExtensionFDResolver mResolve; + FileDescriptor mFD; +}; + +NS_IMPL_ISUPPORTS(ExtensionJARFileOpener, nsISupports) + +// The amount of time, in milliseconds, that the file opener thread will remain +// allocated after it is used. This value chosen because to match other uses +// of LazyIdleThread. +#define DEFAULT_THREAD_TIMEOUT_MS 30000 + +// Request an FD or input stream from the parent. +RequestOrReason ExtensionStreamGetter::GetAsync(nsIStreamListener* aListener, + nsIChannel* aChannel) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mMainThreadEventTarget); + + mListener = aListener; + mChannel = aChannel; + + nsCOMPtr<nsICancelable> cancelableRequest(this); + + RefPtr<ExtensionStreamGetter> self = this; + if (mIsJarChannel) { + // Request an FD for this moz-extension URI + gNeckoChild->SendGetExtensionFD(mURI)->Then( + mMainThreadEventTarget, __func__, + [self](const FileDescriptor& fd) { self->OnFD(fd); }, + [self](const mozilla::ipc::ResponseRejectReason) { + self->OnFD(FileDescriptor()); + }); + return RequestOrCancelable(WrapNotNull(cancelableRequest)); + } + + // Request an input stream for this moz-extension URI + gNeckoChild->SendGetExtensionStream(mURI)->Then( + mMainThreadEventTarget, __func__, + [self](const RefPtr<nsIInputStream>& stream) { + self->OnStream(do_AddRef(stream)); + }, + [self](const mozilla::ipc::ResponseRejectReason) { + self->OnStream(nullptr); + }); + return RequestOrCancelable(WrapNotNull(cancelableRequest)); +} + +// Called to cancel the ongoing async request. +NS_IMETHODIMP +ExtensionStreamGetter::Cancel(nsresult aStatus) { + if (mCanceled) { + return NS_OK; + } + + mCanceled = true; + mStatus = aStatus; + + if (mPump) { + mPump->CancelWithReason(aStatus, "ExtensionStreamGetter::Cancel"_ns); + mPump = nullptr; + } + + if (mIsJarChannel && mJarChannel) { + mJarChannel->CancelWithReason(aStatus, "ExtensionStreamGetter::Cancel"_ns); + } + + return NS_OK; +} + +// static +void ExtensionStreamGetter::CancelRequest(nsIStreamListener* aListener, + nsIChannel* aChannel, + nsresult aResult) { + MOZ_ASSERT(aListener); + MOZ_ASSERT(aChannel); + + aListener->OnStartRequest(aChannel); + aListener->OnStopRequest(aChannel, aResult); + aChannel->CancelWithReason(NS_BINDING_ABORTED, + "ExtensionStreamGetter::CancelRequest"_ns); +} + +// Handle an input stream sent from the parent. +void ExtensionStreamGetter::OnStream(already_AddRefed<nsIInputStream> aStream) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mChannel); + MOZ_ASSERT(mListener); + MOZ_ASSERT(mMainThreadEventTarget); + + nsCOMPtr<nsIInputStream> stream = std::move(aStream); + nsCOMPtr<nsIChannel> channel = std::move(mChannel); + + // We must keep an owning reference to the listener + // until we pass it on to AsyncRead. + nsCOMPtr<nsIStreamListener> listener = std::move(mListener); + + if (mCanceled) { + // The channel that has created this stream getter has been canceled. + CancelRequest(listener, channel, mStatus); + return; + } + + if (!stream) { + // The parent didn't send us back a stream. + CancelRequest(listener, channel, NS_ERROR_FILE_ACCESS_DENIED); + return; + } + + nsCOMPtr<nsIInputStreamPump> pump; + nsresult rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget(), 0, + 0, false, mMainThreadEventTarget); + if (NS_FAILED(rv)) { + CancelRequest(listener, channel, rv); + return; + } + + rv = pump->AsyncRead(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, channel, rv); + return; + } + + mPump = pump; +} + +// Handle an FD sent from the parent. +void ExtensionStreamGetter::OnFD(const FileDescriptor& aFD) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mChannel); + MOZ_ASSERT(mListener); + + nsCOMPtr<nsIChannel> channel = std::move(mChannel); + + // We must keep an owning reference to the listener + // until we pass it on to AsyncOpen. + nsCOMPtr<nsIStreamListener> listener = std::move(mListener); + + if (mCanceled) { + // The channel that has created this stream getter has been canceled. + CancelRequest(listener, channel, mStatus); + return; + } + + if (!aFD.IsValid()) { + // The parent didn't send us back a valid file descriptor. + CancelRequest(listener, channel, NS_ERROR_FILE_ACCESS_DENIED); + return; + } + + RefPtr<FileDescriptorFile> fdFile = new FileDescriptorFile(aFD, mJarFile); + mJarChannel->SetJarFile(fdFile); + nsresult rv = mJarChannel->AsyncOpen(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, channel, rv); + } +} + +NS_IMPL_QUERY_INTERFACE(ExtensionProtocolHandler, + nsISubstitutingProtocolHandler, nsIProtocolHandler, + nsIProtocolHandlerWithDynamicFlags, + nsISupportsWeakReference) +NS_IMPL_ADDREF_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler) +NS_IMPL_RELEASE_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler) + +already_AddRefed<ExtensionProtocolHandler> +ExtensionProtocolHandler::GetSingleton() { + if (!sSingleton) { + sSingleton = new ExtensionProtocolHandler(); + ClearOnShutdown(&sSingleton); + } + return do_AddRef(sSingleton); +} + +ExtensionProtocolHandler::ExtensionProtocolHandler() + : SubstitutingProtocolHandler(EXTENSION_SCHEME) { + // Note, extensions.webextensions.protocol.remote=false is for + // debugging purposes only. With process-level sandboxing, child + // processes (specifically content and extension processes), will + // not be able to load most moz-extension URI's when the pref is + // set to false. + mUseRemoteFileChannels = + IsNeckoChild() && + Preferences::GetBool("extensions.webextensions.protocol.remote"); +} + +static inline ExtensionPolicyService& EPS() { + return ExtensionPolicyService::GetSingleton(); +} + +nsresult ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI, + uint32_t* aFlags) { + uint32_t flags = + URI_STD | URI_IS_LOCAL_RESOURCE | URI_IS_POTENTIALLY_TRUSTWORTHY; + + URLInfo url(aURI); + if (auto* policy = EPS().GetByURL(url)) { + // In general a moz-extension URI is only loadable by chrome, but an + // allowlist subset are web-accessible (and cross-origin fetchable). + // The allowlist is checked using EPS.SourceMayLoadExtensionURI in + // BasePrincipal and nsScriptSecurityManager. + if (policy->IsWebAccessiblePath(url.FilePath())) { + if (policy->ManifestVersion() < 3) { + flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE; + } else { + flags |= WEBEXT_URI_WEB_ACCESSIBLE; + } + } else if (policy->Type() == nsGkAtoms::theme) { + // Static themes cannot set web accessible resources, however using this + // flag here triggers SourceMayAccessPath calls necessary to allow another + // extension to access static theme resources in this extension. + flags |= WEBEXT_URI_WEB_ACCESSIBLE; + } else { + flags |= URI_DANGEROUS_TO_LOAD; + } + + // Disallow in private windows if the extension does not have permission. + if (!policy->PrivateBrowsingAllowed()) { + flags |= URI_DISALLOW_IN_PRIVATE_CONTEXT; + } + } else { + // In case there is no policy, then default to treating moz-extension URIs + // as unsafe and generally only allow chrome: to load such. + flags |= URI_DANGEROUS_TO_LOAD; + } + + *aFlags = flags; + return NS_OK; +} + +bool ExtensionProtocolHandler::ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(), + "The ExtensionPolicyService is not thread safe"); + // Create special moz-extension://foo/_generated_background_page.html page + // for all registered extensions. We can't just do this as a substitution + // because substitutions can only match on host. + if (!SubstitutingProtocolHandler::HasSubstitution(aHost)) { + return false; + } + + if (aPathname.EqualsLiteral("/_generated_background_page.html")) { + Unused << EPS().GetGeneratedBackgroundPageUrl(aHost, aResult); + return !aResult.IsEmpty(); + } + + return false; +} + +// For file or JAR URI's, substitute in a remote channel. +Result<Ok, nsresult> ExtensionProtocolHandler::SubstituteRemoteChannel( + nsIURI* aURI, nsILoadInfo* aLoadInfo, nsIChannel** aRetVal) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_TRY(aURI ? NS_OK : NS_ERROR_INVALID_ARG); + MOZ_TRY(aLoadInfo ? NS_OK : NS_ERROR_INVALID_ARG); + + nsAutoCString unResolvedSpec; + MOZ_TRY(aURI->GetSpec(unResolvedSpec)); + + nsAutoCString resolvedSpec; + MOZ_TRY(ResolveURI(aURI, resolvedSpec)); + + // Use the target URI scheme to determine if this is a packed or unpacked + // extension URI. For unpacked extensions, we'll request an input stream + // from the parent. For a packed extension, we'll request a file descriptor + // for the JAR file. + nsAutoCString scheme; + MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme)); + + if (scheme.EqualsLiteral("file")) { + // Unpacked extension + SubstituteRemoteFileChannel(aURI, aLoadInfo, resolvedSpec, aRetVal); + return Ok(); + } + + if (scheme.EqualsLiteral("jar")) { + // Packed extension + return SubstituteRemoteJarChannel(aURI, aLoadInfo, resolvedSpec, aRetVal); + } + + // Only unpacked resource files and JAR files are remoted. + // No other moz-extension loads should be reading from the filesystem. + return Ok(); +} + +void OpenWhenReady( + Promise* aPromise, nsIStreamListener* aListener, nsIChannel* aChannel, + const std::function<nsresult(nsIStreamListener*, nsIChannel*)>& aCallback) { + nsCOMPtr<nsIStreamListener> listener(aListener); + nsCOMPtr<nsIChannel> channel(aChannel); + + Unused << aPromise->ThenWithCycleCollectedArgs( + [channel, aCallback]( + JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + nsIStreamListener* aListener) -> already_AddRefed<Promise> { + nsresult rv = aCallback(aListener, channel); + if (NS_FAILED(rv)) { + ExtensionStreamGetter::CancelRequest(aListener, channel, rv); + } + return nullptr; + }, + listener); +} + +nsresult ExtensionProtocolHandler::SubstituteChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + nsIChannel** result) { + if (mUseRemoteFileChannels) { + MOZ_TRY(SubstituteRemoteChannel(aURI, aLoadInfo, result)); + } + + auto* policy = EPS().GetByURL(aURI); + NS_ENSURE_TRUE(policy, NS_ERROR_UNEXPECTED); + + RefPtr<dom::Promise> readyPromise(policy->ReadyPromise()); + + nsresult rv; + nsCOMPtr<nsIURL> url = do_QueryInterface(aURI, &rv); + MOZ_TRY(rv); + + nsAutoCString ext; + MOZ_TRY(url->GetFileExtension(ext)); + ToLowerCase(ext); + + nsCOMPtr<nsIChannel> channel; + if (ext.EqualsLiteral("css")) { + // Filter CSS files to replace locale message tokens with localized strings. + static const auto convert = [](nsIStreamListener* listener, + nsIChannel* channel, + nsIChannel* origChannel) -> nsresult { + nsresult rv; + nsCOMPtr<nsIStreamConverterService> convService = + do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIURI> uri; + MOZ_TRY(channel->GetURI(getter_AddRefs(uri))); + + const char* kFromType = "application/vnd.mozilla.webext.unlocalized"; + const char* kToType = "text/css"; + + nsCOMPtr<nsIStreamListener> converter; + MOZ_TRY(convService->AsyncConvertData(kFromType, kToType, listener, uri, + getter_AddRefs(converter))); + + return origChannel->AsyncOpen(converter); + }; + + channel = NS_NewSimpleChannel( + aURI, aLoadInfo, *result, + [readyPromise](nsIStreamListener* listener, nsIChannel* channel, + nsIChannel* origChannel) -> RequestOrReason { + if (readyPromise) { + nsCOMPtr<nsIChannel> chan(channel); + OpenWhenReady( + readyPromise, listener, origChannel, + [chan](nsIStreamListener* aListener, nsIChannel* aChannel) { + return convert(aListener, chan, aChannel); + }); + } else { + MOZ_TRY(convert(listener, channel, origChannel)); + } + nsCOMPtr<nsIRequest> request(origChannel); + return RequestOrCancelable(WrapNotNull(request)); + }); + } else if (readyPromise) { + size_t matchIdx; + if (BinarySearchIf( + sStaticFileExtensions, 0, ArrayLength(sStaticFileExtensions), + [&ext](const char* aOther) { + return Compare(ext, nsDependentCString(aOther)); + }, + &matchIdx)) { + // This is a static resource that shouldn't depend on the extension being + // ready. Don't bother waiting for it. + return NS_OK; + } + + channel = NS_NewSimpleChannel( + aURI, aLoadInfo, *result, + [readyPromise](nsIStreamListener* listener, nsIChannel* channel, + nsIChannel* origChannel) -> RequestOrReason { + OpenWhenReady(readyPromise, listener, origChannel, + [](nsIStreamListener* aListener, nsIChannel* aChannel) { + return aChannel->AsyncOpen(aListener); + }); + + nsCOMPtr<nsIRequest> request(origChannel); + return RequestOrCancelable(WrapNotNull(request)); + }); + } else { + return NS_OK; + } + + NS_ENSURE_TRUE(channel, NS_ERROR_OUT_OF_MEMORY); + if (aLoadInfo) { + nsCOMPtr<nsILoadInfo> loadInfo = + static_cast<LoadInfo*>(aLoadInfo)->CloneForNewRequest(); + (*result)->SetLoadInfo(loadInfo); + } + + channel.swap(*result); + return NS_OK; +} + +Result<bool, nsresult> ExtensionProtocolHandler::AllowExternalResource( + nsIFile* aExtensionDir, nsIFile* aRequestedFile) { + MOZ_ASSERT(!IsNeckoChild()); + +#if defined(XP_WIN) + // On Windows, non-package builds don't use symlinks so we never need to + // allow a resource from outside of the extension dir. + return false; +#else + if (mozilla::IsPackagedBuild()) { + return false; + } + + // On Mac and Linux unpackaged dev builds, system extensions use + // symlinks to point to resources in the repo dir which we have to + // allow loading. Before we allow an unpacked extension to load a + // resource outside of the extension dir, we make sure the extension + // dir is within the app directory. + bool result; + MOZ_TRY_VAR(result, AppDirContains(aExtensionDir)); + if (!result) { + return false; + } + +# if defined(XP_MACOSX) + // Additionally, on Mac dev builds, we make sure that the requested + // resource is within the repo dir. We don't perform this check on Linux + // because we don't have a reliable path to the repo dir on Linux. + return DevRepoContains(aRequestedFile); +# else /* XP_MACOSX */ + return true; +# endif +#endif /* defined(XP_WIN) */ +} + +#if defined(XP_MACOSX) +// The |aRequestedFile| argument must already be Normalize()'d +Result<bool, nsresult> ExtensionProtocolHandler::DevRepoContains( + nsIFile* aRequestedFile) { + MOZ_ASSERT(!mozilla::IsPackagedBuild()); + MOZ_ASSERT(!IsNeckoChild()); + + // On the first invocation, set mDevRepo + if (!mAlreadyCheckedDevRepo) { + mAlreadyCheckedDevRepo = true; + MOZ_TRY(nsMacUtilsImpl::GetRepoDir(getter_AddRefs(mDevRepo))); + if (MOZ_LOG_TEST(gExtProtocolLog, LogLevel::Debug)) { + nsAutoCString repoPath; + Unused << mDevRepo->GetNativePath(repoPath); + LOG("Repo path: %s", repoPath.get()); + } + } + + bool result = false; + if (mDevRepo) { + MOZ_TRY(mDevRepo->Contains(aRequestedFile, &result)); + } + return result; +} +#endif /* XP_MACOSX */ + +#if !defined(XP_WIN) +Result<bool, nsresult> ExtensionProtocolHandler::AppDirContains( + nsIFile* aExtensionDir) { + MOZ_ASSERT(!mozilla::IsPackagedBuild()); + MOZ_ASSERT(!IsNeckoChild()); + + // On the first invocation, set mAppDir + if (!mAlreadyCheckedAppDir) { + mAlreadyCheckedAppDir = true; + MOZ_TRY(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(mAppDir))); + if (MOZ_LOG_TEST(gExtProtocolLog, LogLevel::Debug)) { + nsAutoCString appDirPath; + Unused << mAppDir->GetNativePath(appDirPath); + LOG("AppDir path: %s", appDirPath.get()); + } + } + + bool result = false; + if (mAppDir) { + MOZ_TRY(mAppDir->Contains(aExtensionDir, &result)); + } + return result; +} +#endif /* !defined(XP_WIN) */ + +static void LogExternalResourceError(nsIFile* aExtensionDir, + nsIFile* aRequestedFile) { + MOZ_ASSERT(aExtensionDir); + MOZ_ASSERT(aRequestedFile); + + LOG("Rejecting external unpacked extension resource [%s] from " + "extension directory [%s]", + aRequestedFile->HumanReadablePath().get(), + aExtensionDir->HumanReadablePath().get()); +} + +Result<nsCOMPtr<nsIInputStream>, nsresult> ExtensionProtocolHandler::NewStream( + nsIURI* aChildURI, bool* aTerminateSender) { + MOZ_ASSERT(!IsNeckoChild()); + MOZ_TRY(aChildURI ? NS_OK : NS_ERROR_INVALID_ARG); + MOZ_TRY(aTerminateSender ? NS_OK : NS_ERROR_INVALID_ARG); + + *aTerminateSender = true; + nsresult rv; + + // We should never receive a URI that isn't for a moz-extension because + // these requests ordinarily come from the child's ExtensionProtocolHandler. + // Ensure this request is for a moz-extension URI. A rogue child process + // could send us any URI. + if (!aChildURI->SchemeIs(EXTENSION_SCHEME)) { + return Err(NS_ERROR_UNKNOWN_PROTOCOL); + } + + // For errors after this point, we want to propagate the error to + // the child, but we don't force the child to be terminated because + // the error is likely to be due to a bug in the extension. + *aTerminateSender = false; + + /* + * Make sure there is a substitution installed for the host found + * in the child's request URI and make sure the host resolves to + * a directory. + */ + + nsAutoCString host; + MOZ_TRY(aChildURI->GetAsciiHost(host)); + + // Lookup the directory this host string resolves to + nsCOMPtr<nsIURI> baseURI; + MOZ_TRY(GetSubstitution(host, getter_AddRefs(baseURI))); + + // The result should be a file URL for the extension base dir + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(baseURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIFile> extensionDir; + MOZ_TRY(fileURL->GetFile(getter_AddRefs(extensionDir))); + + bool isDirectory = false; + MOZ_TRY(extensionDir->IsDirectory(&isDirectory)); + if (!isDirectory) { + // The host should map to a directory for unpacked extensions + return Err(NS_ERROR_FILE_NOT_DIRECTORY); + } + + // Make sure the child URI resolves to a file URI then get a file + // channel for the request. The resultant channel should be a + // file channel because we only request remote streams for unpacked + // extension resource loads where the URI resolves to a file. + nsAutoCString resolvedSpec; + MOZ_TRY(ResolveURI(aChildURI, resolvedSpec)); + + nsAutoCString resolvedScheme; + MOZ_TRY(net_ExtractURLScheme(resolvedSpec, resolvedScheme)); + if (!resolvedScheme.EqualsLiteral("file")) { + return Err(NS_ERROR_UNEXPECTED); + } + + nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIURI> resolvedURI; + MOZ_TRY(ioService->NewURI(resolvedSpec, nullptr, nullptr, + getter_AddRefs(resolvedURI))); + + // We use the system principal to get a file channel for the request, + // but only after we've checked (above) that the child URI is of + // moz-extension scheme and that the URI host maps to a directory. + nsCOMPtr<nsIChannel> channel; + MOZ_TRY(NS_NewChannel(getter_AddRefs(channel), resolvedURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER)); + + nsCOMPtr<nsIFileChannel> fileChannel = do_QueryInterface(channel, &rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIFile> requestedFile; + MOZ_TRY(fileChannel->GetFile(getter_AddRefs(requestedFile))); + + /* + * Make sure the file we resolved to is within the extension directory. + */ + + // Normalize paths for sane comparisons. nsIFile::Contains depends on + // it for reliable subpath checks. + MOZ_TRY(extensionDir->Normalize()); + MOZ_TRY(requestedFile->Normalize()); +#if defined(XP_WIN) + if (!widget::WinUtils::ResolveJunctionPointsAndSymLinks(extensionDir) || + !widget::WinUtils::ResolveJunctionPointsAndSymLinks(requestedFile)) { + return Err(NS_ERROR_FILE_ACCESS_DENIED); + } +#endif + + bool isResourceFromExtensionDir = false; + MOZ_TRY(extensionDir->Contains(requestedFile, &isResourceFromExtensionDir)); + if (!isResourceFromExtensionDir) { + bool isAllowed; + MOZ_TRY_VAR(isAllowed, AllowExternalResource(extensionDir, requestedFile)); + if (!isAllowed) { + LogExternalResourceError(extensionDir, requestedFile); + return Err(NS_ERROR_FILE_ACCESS_DENIED); + } + } + + nsCOMPtr<nsIInputStream> inputStream; + MOZ_TRY_VAR(inputStream, + NS_NewLocalFileInputStream(requestedFile, PR_RDONLY, -1, + nsIFileInputStream::DEFER_OPEN)); + + return inputStream; +} + +Result<Ok, nsresult> ExtensionProtocolHandler::NewFD( + nsIURI* aChildURI, bool* aTerminateSender, + NeckoParent::GetExtensionFDResolver& aResolve) { + MOZ_ASSERT(!IsNeckoChild()); + MOZ_TRY(aChildURI ? NS_OK : NS_ERROR_INVALID_ARG); + MOZ_TRY(aTerminateSender ? NS_OK : NS_ERROR_INVALID_ARG); + + *aTerminateSender = true; + nsresult rv; + + // Ensure this is a moz-extension URI + if (!aChildURI->SchemeIs(EXTENSION_SCHEME)) { + return Err(NS_ERROR_UNKNOWN_PROTOCOL); + } + + // For errors after this point, we want to propagate the error to + // the child, but we don't force the child to be terminated. + *aTerminateSender = false; + + nsAutoCString host; + MOZ_TRY(aChildURI->GetAsciiHost(host)); + + // We expect the host string to map to a JAR file because the URI + // should refer to a web accessible resource for an enabled extension. + nsCOMPtr<nsIURI> subURI; + MOZ_TRY(GetSubstitution(host, getter_AddRefs(subURI))); + + nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(subURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIURI> innerFileURI; + MOZ_TRY(jarURI->GetJARFile(getter_AddRefs(innerFileURI))); + + nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIFile> jarFile; + MOZ_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile))); + + if (!mFileOpenerThread) { + mFileOpenerThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, + "ExtensionProtocolHandler"); + } + + RefPtr<ExtensionJARFileOpener> fileOpener = + new ExtensionJARFileOpener(jarFile, aResolve); + + nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod( + "ExtensionJarFileOpener", fileOpener, &ExtensionJARFileOpener::OpenFile); + + MOZ_TRY(mFileOpenerThread->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL)); + + return Ok(); +} + +// Set the channel's content type using the provided URI's type + +// static +void ExtensionProtocolHandler::SetContentType(nsIURI* aURI, + nsIChannel* aChannel) { + nsresult rv; + nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1", &rv); + if (NS_SUCCEEDED(rv)) { + nsAutoCString contentType; + rv = mime->GetTypeFromURI(aURI, contentType); + if (NS_SUCCEEDED(rv)) { + Unused << aChannel->SetContentType(contentType); + } + } +} + +// Gets a SimpleChannel that wraps the provided ExtensionStreamGetter + +// static +void ExtensionProtocolHandler::NewSimpleChannel( + nsIURI* aURI, nsILoadInfo* aLoadinfo, ExtensionStreamGetter* aStreamGetter, + nsIChannel** aRetVal) { + nsCOMPtr<nsIChannel> channel = NS_NewSimpleChannel( + aURI, aLoadinfo, aStreamGetter, + [](nsIStreamListener* listener, nsIChannel* simpleChannel, + ExtensionStreamGetter* getter) -> RequestOrReason { + return getter->GetAsync(listener, simpleChannel); + }); + + SetContentType(aURI, channel); + channel.swap(*aRetVal); +} + +// Gets a SimpleChannel that wraps the provided channel + +// static +void ExtensionProtocolHandler::NewSimpleChannel(nsIURI* aURI, + nsILoadInfo* aLoadinfo, + nsIChannel* aChannel, + nsIChannel** aRetVal) { + nsCOMPtr<nsIChannel> channel = NS_NewSimpleChannel( + aURI, aLoadinfo, aChannel, + [](nsIStreamListener* listener, nsIChannel* simpleChannel, + nsIChannel* origChannel) -> RequestOrReason { + nsresult rv = origChannel->AsyncOpen(listener); + if (NS_FAILED(rv)) { + simpleChannel->Cancel(NS_BINDING_ABORTED); + return Err(rv); + } + nsCOMPtr<nsIRequest> request(origChannel); + return RequestOrCancelable(WrapNotNull(request)); + }); + + SetContentType(aURI, channel); + channel.swap(*aRetVal); +} + +void ExtensionProtocolHandler::SubstituteRemoteFileChannel( + nsIURI* aURI, nsILoadInfo* aLoadinfo, nsACString& aResolvedFileSpec, + nsIChannel** aRetVal) { + MOZ_ASSERT(IsNeckoChild()); + + RefPtr<ExtensionStreamGetter> streamGetter = + new ExtensionStreamGetter(aURI, aLoadinfo); + + NewSimpleChannel(aURI, aLoadinfo, streamGetter, aRetVal); +} + +static Result<Ok, nsresult> LogCacheCheck(const nsIJARChannel* aJarChannel, + nsIJARURI* aJarURI, bool aIsCached) { + nsresult rv; + + nsCOMPtr<nsIURI> innerFileURI; + MOZ_TRY(aJarURI->GetJARFile(getter_AddRefs(innerFileURI))); + + nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIFile> jarFile; + MOZ_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile))); + + nsAutoCString uriSpec, jarSpec; + Unused << aJarURI->GetSpec(uriSpec); + Unused << innerFileURI->GetSpec(jarSpec); + LOG("[JARChannel %p] Cache %s: %s (%s)", aJarChannel, + aIsCached ? "hit" : "miss", uriSpec.get(), jarSpec.get()); + + return Ok(); +} + +Result<Ok, nsresult> ExtensionProtocolHandler::SubstituteRemoteJarChannel( + nsIURI* aURI, nsILoadInfo* aLoadinfo, nsACString& aResolvedSpec, + nsIChannel** aRetVal) { + MOZ_ASSERT(IsNeckoChild()); + nsresult rv; + + // Build a JAR URI for this jar:file:// URI and use it to extract the + // inner file URI. + nsCOMPtr<nsIURI> uri; + MOZ_TRY(NS_NewURI(getter_AddRefs(uri), aResolvedSpec)); + + nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri, &rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIJARChannel> jarChannel = do_QueryInterface(*aRetVal, &rv); + MOZ_TRY(rv); + + bool isCached = false; + MOZ_TRY(jarChannel->EnsureCached(&isCached)); + if (MOZ_LOG_TEST(gExtProtocolLog, LogLevel::Debug)) { + Unused << LogCacheCheck(jarChannel, jarURI, isCached); + } + + if (isCached) { + // Using a SimpleChannel with an ExtensionStreamGetter here (like the + // non-cached JAR case) isn't needed to load the extension resource + // because we don't need to ask the parent for an FD for the JAR, but + // wrapping the JARChannel in a SimpleChannel allows HTTP forwarding to + // moz-extension URI's to work because HTTP forwarding requires the + // target channel implement nsIChildChannel. + NewSimpleChannel(aURI, aLoadinfo, jarChannel.get(), aRetVal); + return Ok(); + } + + nsCOMPtr<nsIURI> innerFileURI; + MOZ_TRY(jarURI->GetJARFile(getter_AddRefs(innerFileURI))); + + nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr<nsIFile> jarFile; + MOZ_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile))); + + RefPtr<ExtensionStreamGetter> streamGetter = + new ExtensionStreamGetter(aURI, aLoadinfo, jarChannel.forget(), jarFile); + + NewSimpleChannel(aURI, aLoadinfo, streamGetter, aRetVal); + return Ok(); +} + +} // namespace net +} // namespace mozilla |