diff options
Diffstat (limited to '')
-rw-r--r-- | netwerk/protocol/res/ExtensionProtocolHandler.cpp | 987 | ||||
-rw-r--r-- | netwerk/protocol/res/ExtensionProtocolHandler.h | 236 | ||||
-rw-r--r-- | netwerk/protocol/res/PageThumbProtocolHandler.cpp | 468 | ||||
-rw-r--r-- | netwerk/protocol/res/PageThumbProtocolHandler.h | 123 | ||||
-rw-r--r-- | netwerk/protocol/res/SubstitutingJARURI.h | 161 | ||||
-rw-r--r-- | netwerk/protocol/res/SubstitutingProtocolHandler.cpp | 653 | ||||
-rw-r--r-- | netwerk/protocol/res/SubstitutingProtocolHandler.h | 134 | ||||
-rw-r--r-- | netwerk/protocol/res/SubstitutingURL.h | 66 | ||||
-rw-r--r-- | netwerk/protocol/res/moz.build | 40 | ||||
-rw-r--r-- | netwerk/protocol/res/nsIResProtocolHandler.idl | 15 | ||||
-rw-r--r-- | netwerk/protocol/res/nsISubstitutingProtocolHandler.idl | 62 | ||||
-rw-r--r-- | netwerk/protocol/res/nsResProtocolHandler.cpp | 194 | ||||
-rw-r--r-- | netwerk/protocol/res/nsResProtocolHandler.h | 80 |
13 files changed, 3219 insertions, 0 deletions
diff --git a/netwerk/protocol/res/ExtensionProtocolHandler.cpp b/netwerk/protocol/res/ExtensionProtocolHandler.cpp new file mode 100644 index 0000000000..3e4e314577 --- /dev/null +++ b/netwerk/protocol/res/ExtensionProtocolHandler.cpp @@ -0,0 +1,987 @@ +/* -*- 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/ContentChild.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/FileUtils.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ResultExtensions.h" + +#include "FileDescriptor.h" +#include "FileDescriptorFile.h" +#include "LoadInfo.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsDirectoryServiceDefs.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 : public RefCounted<ExtensionStreamGetter> { + 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(); + } + + ~ExtensionStreamGetter() = default; + + void SetupEventTarget() { + mMainThreadEventTarget = nsContentUtils::GetEventTargetByLoadInfo( + mLoadInfo, TaskCategory::Other); + if (!mMainThreadEventTarget) { + mMainThreadEventTarget = GetMainThreadSerialEventTarget(); + } + } + + // Get an input stream or file descriptor from the parent asynchronously. + Result<Ok, nsresult> 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); + + MOZ_DECLARE_REFCOUNTED_TYPENAME(ExtensionStreamGetter) + + private: + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsILoadInfo> mLoadInfo; + nsCOMPtr<nsIJARChannel> mJarChannel; + nsCOMPtr<nsIFile> mJarFile; + nsCOMPtr<nsIStreamListener> mListener; + nsCOMPtr<nsIChannel> mChannel; + nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget; + bool mIsJarChannel; +}; + +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. +Result<Ok, nsresult> ExtensionStreamGetter::GetAsync( + nsIStreamListener* aListener, nsIChannel* aChannel) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mMainThreadEventTarget); + + mListener = aListener; + mChannel = aChannel; + + 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 Ok(); + } + + // 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 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->Cancel(NS_BINDING_ABORTED); +} + +// Handle an input stream sent from the parent. +void ExtensionStreamGetter::OnStream(already_AddRefed<nsIInputStream> aStream) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mListener); + MOZ_ASSERT(mMainThreadEventTarget); + + nsCOMPtr<nsIInputStream> stream = std::move(aStream); + + // We must keep an owning reference to the listener + // until we pass it on to AsyncRead. + nsCOMPtr<nsIStreamListener> listener = std::move(mListener); + + MOZ_ASSERT(mChannel); + + if (!stream) { + // The parent didn't send us back a stream. + CancelRequest(listener, mChannel, 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, mChannel, rv); + return; + } + + rv = pump->AsyncRead(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, mChannel, rv); + } +} + +// Handle an FD sent from the parent. +void ExtensionStreamGetter::OnFD(const FileDescriptor& aFD) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mListener); + MOZ_ASSERT(mChannel); + + if (!aFD.IsValid()) { + OnStream(nullptr); + return; + } + + // We must keep an owning reference to the listener + // until we pass it on to AsyncOpen. + nsCOMPtr<nsIStreamListener> listener = std::move(mListener); + + RefPtr<FileDescriptorFile> fdFile = new FileDescriptorFile(aFD, mJarFile); + mJarChannel->SetJarFile(fdFile); + nsresult rv = mJarChannel->AsyncOpen(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, mChannel, 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) +#if !defined(XP_WIN) +# if defined(XP_MACOSX) + , + mAlreadyCheckedDevRepo(false) +# endif /* XP_MACOSX */ + , + mAlreadyCheckedAppDir(false) +#endif /* ! XP_WIN */ +{ + // 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 a + // whitelisted subset are web-accessible (and cross-origin fetchable). Check + // that whitelist. + if (policy->IsPathWebAccessible(url.FilePath())) { + flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE; + } 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::HandleValue aValue, + 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)); + } + return RequestOrReason(origChannel); + }); + } else if (readyPromise) { + size_t matchIdx; + if (BinarySearchIf( + sStaticFileExtensions, 0, ArrayLength(sStaticFileExtensions), + [&ext](const char* aOther) { return ext.Compare(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); + }); + + return RequestOrReason(origChannel); + }); + } 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, dev builds don't use symlinks so we never need to + // allow a resource from outside of the extension dir. + return false; +#else + if (!mozilla::IsDevelopmentBuild()) { + 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::IsDevelopmentBuild()); + 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::IsDevelopmentBuild()); + 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"_ns); + } + + 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 { + MOZ_TRY(getter->GetAsync(listener, simpleChannel)); + return RequestOrReason(nullptr); + }); + + 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); + } + return RequestOrReason(origChannel); + }); + + 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 diff --git a/netwerk/protocol/res/ExtensionProtocolHandler.h b/netwerk/protocol/res/ExtensionProtocolHandler.h new file mode 100644 index 0000000000..ec253702e9 --- /dev/null +++ b/netwerk/protocol/res/ExtensionProtocolHandler.h @@ -0,0 +1,236 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef ExtensionProtocolHandler_h___ +#define ExtensionProtocolHandler_h___ + +#include "mozilla/net/NeckoParent.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/Result.h" +#include "SubstitutingProtocolHandler.h" + +namespace mozilla { +namespace net { + +class ExtensionStreamGetter; + +class ExtensionProtocolHandler final + : public nsISubstitutingProtocolHandler, + public nsIProtocolHandlerWithDynamicFlags, + public SubstitutingProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPROTOCOLHANDLERWITHDYNAMICFLAGS + NS_FORWARD_NSIPROTOCOLHANDLER(SubstitutingProtocolHandler::) + NS_FORWARD_NSISUBSTITUTINGPROTOCOLHANDLER(SubstitutingProtocolHandler::) + + static already_AddRefed<ExtensionProtocolHandler> GetSingleton(); + + /** + * To be called in the parent process to obtain an input stream for a + * a web accessible resource from an unpacked WebExtension dir. + * + * @param aChildURI a moz-extension URI sent from the child that refers + * to a web accessible resource file in an enabled unpacked extension + * @param aTerminateSender out param set to true when the params are invalid + * and indicate the child should be terminated. If |aChildURI| is + * not a moz-extension URI, the child is in an invalid state and + * should be terminated. + * @return NS_OK with |aTerminateSender| set to false on success. On + * failure, returns an error and sets |aTerminateSender| to indicate + * whether or not the child process should be terminated. + * A moz-extension URI from the child that doesn't resolve to a + * resource file within the extension could be the result of a bug + * in the extension and doesn't result in |aTerminateSender| being + * set to true. + */ + Result<nsCOMPtr<nsIInputStream>, nsresult> NewStream(nsIURI* aChildURI, + bool* aTerminateSender); + + /** + * To be called in the parent process to obtain a file descriptor for an + * enabled WebExtension JAR file. + * + * @param aChildURI a moz-extension URI sent from the child that refers + * to a web accessible resource file in an enabled unpacked extension + * @param aTerminateSender out param set to true when the params are invalid + * and indicate the child should be terminated. If |aChildURI| is + * not a moz-extension URI, the child is in an invalid state and + * should be terminated. + * @param aPromise a promise that will be resolved asynchronously when the + * file descriptor is available. + * @return NS_OK with |aTerminateSender| set to false on success. On + * failure, returns an error and sets |aTerminateSender| to indicate + * whether or not the child process should be terminated. + * A moz-extension URI from the child that doesn't resolve to an + * enabled WebExtension JAR could be the result of a bug in the + * extension and doesn't result in |aTerminateSender| being + * set to true. + */ + Result<Ok, nsresult> NewFD(nsIURI* aChildURI, bool* aTerminateSender, + NeckoParent::GetExtensionFDResolver& aResolve); + + protected: + ~ExtensionProtocolHandler() = default; + + private: + explicit ExtensionProtocolHandler(); + + [[nodiscard]] bool ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) override; + + // |result| is an inout param. On entry to this function, *result + // is expected to be non-null and already addrefed. This function + // may release the object stored in *result on entry and write + // a new pointer to an already addrefed channel to *result. + [[nodiscard]] virtual nsresult SubstituteChannel( + nsIURI* uri, nsILoadInfo* aLoadInfo, nsIChannel** result) override; + + /** + * For moz-extension URI's that resolve to file or JAR URI's, replaces + * the provided channel with a channel that will proxy the load to the + * parent process. For moz-extension URI's that resolve to other types + * of URI's (not file or JAR), the provide channel is not replaced and + * NS_OK is returned. + * + * @param aURI the moz-extension URI + * @param aLoadInfo the loadinfo for the request + * @param aRetVal in/out channel param referring to the channel that + * might need to be substituted with a remote channel. + * @return NS_OK if the channel does not need to be substituted or + * or the replacement channel was created successfully. + * Otherwise returns an error. + */ + Result<Ok, nsresult> SubstituteRemoteChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + nsIChannel** aRetVal); + + /** + * Replaces a file channel with a remote file channel for loading a + * web accessible resource for an unpacked extension from the parent. + * + * @param aURI the moz-extension URI + * @param aLoadInfo the loadinfo for the request + * @param aResolvedFileSpec the resolved URI spec for the file. + * @param aRetVal in/out param referring to the new remote channel. + * The reference to the input param file channel is dropped and + * replaced with a reference to a new channel that remotes + * the file access. The new channel encapsulates a request to + * the parent for an IPCStream for the file. + */ + void SubstituteRemoteFileChannel(nsIURI* aURI, nsILoadInfo* aLoadinfo, + nsACString& aResolvedFileSpec, + nsIChannel** aRetVal); + + /** + * Replaces a JAR channel with a remote JAR channel for loading a + * an extension JAR file from the parent. + * + * @param aURI the moz-extension URI + * @param aLoadInfo the loadinfo for the request + * @param aResolvedFileSpec the resolved URI spec for the file. + * @param aRetVal in/out param referring to the new remote channel. + * The input param JAR channel is replaced with a new channel + * that remotes the JAR file access. The new channel encapsulates + * a request to the parent for the JAR file FD. + */ + Result<Ok, nsresult> SubstituteRemoteJarChannel(nsIURI* aURI, + nsILoadInfo* aLoadinfo, + nsACString& aResolvedSpec, + nsIChannel** aRetVal); + + /** + * Sets the aResult outparam to true if this unpacked extension load of + * a resource that is outside the extension dir should be allowed. This + * is only allowed for system extensions on Mac and Linux dev builds. + * + * @param aExtensionDir the extension directory. Argument must be an + * nsIFile for which Normalize() has already been called. + * @param aRequestedFile the requested web-accessible resource file. Argument + * must be an nsIFile for which Normalize() has already been called. + */ + Result<bool, nsresult> AllowExternalResource(nsIFile* aExtensionDir, + nsIFile* aRequestedFile); + + // Set the channel's content type using the provided URI's type + static void SetContentType(nsIURI* aURI, nsIChannel* aChannel); + + // Gets a SimpleChannel that wraps the provided ExtensionStreamGetter + static void NewSimpleChannel(nsIURI* aURI, nsILoadInfo* aLoadinfo, + ExtensionStreamGetter* aStreamGetter, + nsIChannel** aRetVal); + + // Gets a SimpleChannel that wraps the provided channel + static void NewSimpleChannel(nsIURI* aURI, nsILoadInfo* aLoadinfo, + nsIChannel* aChannel, nsIChannel** aRetVal); + +#if defined(XP_MACOSX) + /** + * Sets the aResult outparam to true if we are a developer build with the + * repo dir environment variable set and the requested file resides in the + * repo dir. Developer builds may load system extensions with web-accessible + * resources that are symlinks to files in the repo dir. This method is for + * checking if an unpacked resource requested by the child is from the repo. + * The requested file must be already Normalized(). Only compile this for + * Mac because the repo dir isn't always available on Linux. + * + * @param aRequestedFile the requested web-accessible resource file. Argument + * must be an nsIFile for which Normalize() has already been called. + */ + Result<bool, nsresult> DevRepoContains(nsIFile* aRequestedFile); + + // On development builds, this points to development repo. Lazily set. + nsCOMPtr<nsIFile> mDevRepo; + + // Set to true once we've already tried to load the dev repo path, + // allowing for lazy initialization of |mDevRepo|. + bool mAlreadyCheckedDevRepo; +#endif /* XP_MACOSX */ + +#if !defined(XP_WIN) + /** + * Sets the aResult outparam to true if we are a developer build and the + * provided directory is within the NS_GRE_DIR directory. Developer builds + * may load system extensions with web-accessible resources that are symlinks + * to files outside of the extension dir to the repo dir. This method is for + * checking if an extension directory is within NS_GRE_DIR. In that case, we + * consider the extension a system extension and allow it to use symlinks to + * resources outside of the extension dir. This exception is only applied + * to loads for unpacked extensions in unpackaged developer builds. + * The requested dir must be already Normalized(). + * + * @param aExtensionDir the extension directory. Argument must be an + * nsIFile for which Normalize() has already been called. + */ + Result<bool, nsresult> AppDirContains(nsIFile* aExtensionDir); + + // On development builds, cache the NS_GRE_DIR repo. Lazily set. + nsCOMPtr<nsIFile> mAppDir; + + // Set to true once we've already read the AppDir, allowing for lazy + // initialization of |mAppDir|. + bool mAlreadyCheckedAppDir; +#endif /* !defined(XP_WIN) */ + + // Used for opening JAR files off the main thread when we just need to + // obtain a file descriptor to send back to the child. + RefPtr<mozilla::LazyIdleThread> mFileOpenerThread; + + // To allow parent IPDL actors to invoke methods on this handler when + // handling moz-extension requests from the child. + static StaticRefPtr<ExtensionProtocolHandler> sSingleton; + + // Set to true when this instance of the handler must proxy loads of + // extension web-accessible resources to the parent process. + bool mUseRemoteFileChannels; +}; + +} // namespace net +} // namespace mozilla + +#endif /* ExtensionProtocolHandler_h___ */ diff --git a/netwerk/protocol/res/PageThumbProtocolHandler.cpp b/netwerk/protocol/res/PageThumbProtocolHandler.cpp new file mode 100644 index 0000000000..02ee4e67cf --- /dev/null +++ b/netwerk/protocol/res/PageThumbProtocolHandler.cpp @@ -0,0 +1,468 @@ +/* -*- 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 "PageThumbProtocolHandler.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ipc/URIParams.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ResultExtensions.h" + +#include "LoadInfo.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIFile.h" +#include "nsIFileChannel.h" +#include "nsIFileStreams.h" +#include "nsIMIMEService.h" +#include "nsIURL.h" +#include "nsIChannel.h" +#include "nsIPageThumbsStorageService.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamListener.h" +#include "nsIInputStream.h" +#include "nsNetUtil.h" +#include "nsURLHelper.h" +#include "prio.h" +#include "SimpleChannel.h" + +#define PAGE_THUMB_HOST "thumbnails" +#define PAGE_THUMB_SCHEME "moz-page-thumb" + +namespace mozilla { +namespace net { + +LazyLogModule gPageThumbProtocolLog("PageThumbProtocol"); + +#undef LOG +#define LOG(level, ...) \ + MOZ_LOG(gPageThumbProtocolLog, LogLevel::level, (__VA_ARGS__)) + +StaticRefPtr<PageThumbProtocolHandler> PageThumbProtocolHandler::sSingleton; + +/** + * Helper class used with SimpleChannel to asynchronously obtain an input + * stream from the parent for a remote moz-page-thumb load from the child. + */ +class PageThumbStreamGetter : public RefCounted<PageThumbStreamGetter> { + public: + PageThumbStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo) + : mURI(aURI), mLoadInfo(aLoadInfo) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aLoadInfo); + + SetupEventTarget(); + } + + ~PageThumbStreamGetter() = default; + + void SetupEventTarget() { + mMainThreadEventTarget = nsContentUtils::GetEventTargetByLoadInfo( + mLoadInfo, TaskCategory::Other); + if (!mMainThreadEventTarget) { + mMainThreadEventTarget = GetMainThreadSerialEventTarget(); + } + } + + // Get an input stream from the parent asynchronously. + Result<Ok, nsresult> GetAsync(nsIStreamListener* aListener, + nsIChannel* aChannel); + + // Handle an input stream being returned from the parent + void OnStream(already_AddRefed<nsIInputStream> aStream); + + static void CancelRequest(nsIStreamListener* aListener, nsIChannel* aChannel, + nsresult aResult); + + MOZ_DECLARE_REFCOUNTED_TYPENAME(PageThumbStreamGetter) + + private: + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsILoadInfo> mLoadInfo; + nsCOMPtr<nsIStreamListener> mListener; + nsCOMPtr<nsIChannel> mChannel; + nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget; +}; + +// Request an input stream from the parent. +Result<Ok, nsresult> PageThumbStreamGetter::GetAsync( + nsIStreamListener* aListener, nsIChannel* aChannel) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mMainThreadEventTarget); + + mListener = aListener; + mChannel = aChannel; + + RefPtr<PageThumbStreamGetter> self = this; + + // Request an input stream for this moz-page-thumb URI. + gNeckoChild->SendGetPageThumbStream(mURI)->Then( + mMainThreadEventTarget, __func__, + [self](const RefPtr<nsIInputStream>& stream) { + self->OnStream(do_AddRef(stream)); + }, + [self](const mozilla::ipc::ResponseRejectReason) { + self->OnStream(nullptr); + }); + return Ok(); +} + +// static +void PageThumbStreamGetter::CancelRequest(nsIStreamListener* aListener, + nsIChannel* aChannel, + nsresult aResult) { + MOZ_ASSERT(aListener); + MOZ_ASSERT(aChannel); + + aListener->OnStartRequest(aChannel); + aListener->OnStopRequest(aChannel, aResult); + aChannel->Cancel(NS_BINDING_ABORTED); +} + +// Handle an input stream sent from the parent. +void PageThumbStreamGetter::OnStream(already_AddRefed<nsIInputStream> aStream) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mListener); + MOZ_ASSERT(mMainThreadEventTarget); + + nsCOMPtr<nsIInputStream> stream = std::move(aStream); + + // We must keep an owning reference to the listener until we pass it on + // to AsyncRead. + nsCOMPtr<nsIStreamListener> listener = mListener.forget(); + + MOZ_ASSERT(mChannel); + + if (!stream) { + // The parent didn't send us back a stream. + CancelRequest(listener, mChannel, 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, mChannel, rv); + return; + } + + rv = pump->AsyncRead(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, mChannel, rv); + } +} + +NS_IMPL_QUERY_INTERFACE(PageThumbProtocolHandler, + nsISubstitutingProtocolHandler, nsIProtocolHandler, + nsIProtocolHandlerWithDynamicFlags, + nsISupportsWeakReference) +NS_IMPL_ADDREF_INHERITED(PageThumbProtocolHandler, SubstitutingProtocolHandler) +NS_IMPL_RELEASE_INHERITED(PageThumbProtocolHandler, SubstitutingProtocolHandler) + +already_AddRefed<PageThumbProtocolHandler> +PageThumbProtocolHandler::GetSingleton() { + if (!sSingleton) { + sSingleton = new PageThumbProtocolHandler(); + ClearOnShutdown(&sSingleton); + } + + return do_AddRef(sSingleton); +} + +PageThumbProtocolHandler::PageThumbProtocolHandler() + : SubstitutingProtocolHandler(PAGE_THUMB_SCHEME) {} + +nsresult PageThumbProtocolHandler::GetFlagsForURI(nsIURI* aURI, + uint32_t* aFlags) { + // A moz-page-thumb URI is only loadable by chrome pages in the parent + // process, or privileged content running in the privileged about content + // process. + *aFlags = URI_STD | URI_IS_UI_RESOURCE | URI_IS_LOCAL_RESOURCE | + URI_NORELATIVE | URI_NOAUTH; + + return NS_OK; +} + +RefPtr<PageThumbStreamPromise> PageThumbProtocolHandler::NewStream( + nsIURI* aChildURI, bool* aTerminateSender) { + MOZ_ASSERT(!IsNeckoChild()); + MOZ_ASSERT(NS_IsMainThread()); + + if (!aChildURI || !aTerminateSender) { + return PageThumbStreamPromise::CreateAndReject(NS_ERROR_INVALID_ARG, + __func__); + } + + *aTerminateSender = true; + nsresult rv; + + // We should never receive a URI that isn't for a moz-page-thumb because + // these requests ordinarily come from the child's PageThumbProtocolHandler. + // Ensure this request is for a moz-page-thumb URI. A compromised child + // process could send us any URI. + bool isPageThumbScheme = false; + if (NS_FAILED(aChildURI->SchemeIs(PAGE_THUMB_SCHEME, &isPageThumbScheme)) || + !isPageThumbScheme) { + return PageThumbStreamPromise::CreateAndReject(NS_ERROR_UNKNOWN_PROTOCOL, + __func__); + } + + // We should never receive a URI that does not have "thumbnails" as the host. + nsAutoCString host; + if (NS_FAILED(aChildURI->GetAsciiHost(host)) || + !host.EqualsLiteral(PAGE_THUMB_HOST)) { + return PageThumbStreamPromise::CreateAndReject(NS_ERROR_UNEXPECTED, + __func__); + } + + // For errors after this point, we want to propagate the error to + // the child, but we don't force the child process to be terminated. + *aTerminateSender = false; + + // Make sure the child URI resolves to a file URI. We will then get a file + // channel for the request. The resultant channel should be a file channel + // because we only request remote streams for resource loads where the URI + // resolves to a file. + nsAutoCString resolvedSpec; + rv = ResolveURI(aChildURI, resolvedSpec); + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + nsAutoCString resolvedScheme; + rv = net_ExtractURLScheme(resolvedSpec, resolvedScheme); + if (NS_FAILED(rv) || !resolvedScheme.EqualsLiteral("file")) { + return PageThumbStreamPromise::CreateAndReject(NS_ERROR_UNEXPECTED, + __func__); + } + + nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv); + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + nsCOMPtr<nsIURI> resolvedURI; + rv = ioService->NewURI(resolvedSpec, nullptr, nullptr, + getter_AddRefs(resolvedURI)); + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + // 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-page-thumb scheme and that the URI host matches PAGE_THUMB_HOST. + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), resolvedURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + auto promiseHolder = MakeUnique<MozPromiseHolder<PageThumbStreamPromise>>(); + RefPtr<PageThumbStreamPromise> promise = promiseHolder->Ensure(__func__); + + rv = NS_DispatchBackgroundTask( + NS_NewRunnableFunction( + "PageThumbProtocolHandler::NewStream", + [channel, holder = std::move(promiseHolder)]() { + nsresult rv; + + nsCOMPtr<nsIFileChannel> fileChannel = + do_QueryInterface(channel, &rv); + if (NS_FAILED(rv)) { + holder->Reject(rv, __func__); + } + + nsCOMPtr<nsIFile> requestedFile; + rv = fileChannel->GetFile(getter_AddRefs(requestedFile)); + if (NS_FAILED(rv)) { + holder->Reject(rv, __func__); + return; + } + + nsCOMPtr<nsIInputStream> inputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), + requestedFile, PR_RDONLY, -1); + if (NS_FAILED(rv)) { + holder->Reject(rv, __func__); + return; + } + + holder->Resolve(inputStream, __func__); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + return promise; +} + +bool PageThumbProtocolHandler::ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) { + // This should match the scheme in PageThumbs.jsm. We will only resolve + // URIs for thumbnails generated by PageThumbs here. + if (!aHost.EqualsLiteral(PAGE_THUMB_HOST)) { + // moz-page-thumb should always have a "thumbnails" host. We do not intend + // to allow substitution rules to be created for moz-page-thumb. + return false; + } + + // Regardless of the outcome, the scheme will be resolved to file://. + aResult.Assign("file://"); + + if (IsNeckoChild()) { + // We will resolve the URI in the parent if load is performed in the child + // because the child does not have access to the profile directory path. + // Technically we could retrieve the path from dom::ContentChild, but I + // would prefer to obtain the path from PageThumbsStorageService (which + // depends on OS.Path). Here, we resolve to the same URI, with the file:// + // scheme. This won't ever be accessed directly by the content process, + // and is mainly used to keep the substitution protocol handler mechanism + // happy. + aResult.Append(aHost); + aResult.Append(aPath); + } else { + // Resolve the URI in the parent to the thumbnail file URI since we will + // attempt to open the channel to load the file after this. + nsAutoString thumbnailUrl; + nsresult rv = GetThumbnailPath(aPath, thumbnailUrl); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + aResult.Append(NS_ConvertUTF16toUTF8(thumbnailUrl)); + } + + return true; +} + +nsresult PageThumbProtocolHandler::SubstituteChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + nsIChannel** aRetVal) { + // Check if URI resolves to a file URI. + nsAutoCString resolvedSpec; + MOZ_TRY(ResolveURI(aURI, resolvedSpec)); + + nsAutoCString scheme; + MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme)); + + if (!scheme.EqualsLiteral("file")) { + NS_WARNING("moz-page-thumb URIs should only resolve to file URIs."); + return NS_ERROR_NO_INTERFACE; + } + + // Load the URI remotely if accessed from a child. + if (IsNeckoChild()) { + MOZ_TRY(SubstituteRemoteChannel(aURI, aLoadInfo, aRetVal)); + } + + return NS_OK; +} + +Result<Ok, nsresult> PageThumbProtocolHandler::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); + +#ifdef DEBUG + nsAutoCString resolvedSpec; + MOZ_TRY(ResolveURI(aURI, resolvedSpec)); + + nsAutoCString scheme; + MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme)); + + MOZ_ASSERT(scheme.EqualsLiteral("file")); +#endif /* DEBUG */ + + RefPtr<PageThumbStreamGetter> streamGetter = + new PageThumbStreamGetter(aURI, aLoadInfo); + + NewSimpleChannel(aURI, aLoadInfo, streamGetter, aRetVal); + return Ok(); +} + +nsresult PageThumbProtocolHandler::GetThumbnailPath(const nsACString& aPath, + nsString& aThumbnailPath) { + MOZ_ASSERT(!IsNeckoChild()); + + // Ensures that the provided path has a query string. We will start parsing + // from there. + int32_t queryIndex = aPath.FindChar('?'); + if (queryIndex <= 0) { + return NS_ERROR_MALFORMED_URI; + } + + nsresult rv; + + nsCOMPtr<nsIPageThumbsStorageService> pageThumbsStorage = + do_GetService("@mozilla.org/thumbnails/pagethumbs-service;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Extract URL from query string. + nsAutoString url; + bool found = + URLParams::Extract(Substring(aPath, queryIndex + 1), u"url"_ns, url); + if (!found || url.IsVoid()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Use PageThumbsStorageService to get the local file path of the screenshot + // for the given URL. + rv = pageThumbsStorage->GetFilePathForURL(url, aThumbnailPath); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// static +void PageThumbProtocolHandler::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); + } + } +} + +// static +void PageThumbProtocolHandler::NewSimpleChannel( + nsIURI* aURI, nsILoadInfo* aLoadinfo, PageThumbStreamGetter* aStreamGetter, + nsIChannel** aRetVal) { + nsCOMPtr<nsIChannel> channel = NS_NewSimpleChannel( + aURI, aLoadinfo, aStreamGetter, + [](nsIStreamListener* listener, nsIChannel* simpleChannel, + PageThumbStreamGetter* getter) -> RequestOrReason { + MOZ_TRY(getter->GetAsync(listener, simpleChannel)); + return RequestOrReason(nullptr); + }); + + SetContentType(aURI, channel); + channel.swap(*aRetVal); +} + +#undef LOG + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/res/PageThumbProtocolHandler.h b/netwerk/protocol/res/PageThumbProtocolHandler.h new file mode 100644 index 0000000000..8c5c90acff --- /dev/null +++ b/netwerk/protocol/res/PageThumbProtocolHandler.h @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef PageThumbProtocolHandler_h___ +#define PageThumbProtocolHandler_h___ + +#include "mozilla/Result.h" +#include "SubstitutingProtocolHandler.h" + +namespace mozilla { +namespace net { + +using PageThumbStreamPromise = + mozilla::MozPromise<nsCOMPtr<nsIInputStream>, nsresult, false>; + +class PageThumbStreamGetter; + +class PageThumbProtocolHandler final + : public nsISubstitutingProtocolHandler, + public nsIProtocolHandlerWithDynamicFlags, + public SubstitutingProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPROTOCOLHANDLERWITHDYNAMICFLAGS + NS_FORWARD_NSIPROTOCOLHANDLER(SubstitutingProtocolHandler::) + NS_FORWARD_NSISUBSTITUTINGPROTOCOLHANDLER(SubstitutingProtocolHandler::) + + static already_AddRefed<PageThumbProtocolHandler> GetSingleton(); + + /** + * To be called in the parent process to obtain an input stream for the + * given thumbnail. + * + * @param aChildURI a moz-page-thumb URI sent from the child. + * @param aTerminateSender out param set to true when the params are invalid + * and indicate the child should be terminated. If |aChildURI| is + * not a moz-page-thumb URI, the child is in an invalid state and + * should be terminated. This outparam will be set synchronously. + * + * @return PageThumbStreamPromise + * The PageThumbStreamPromise will resolve with an nsIInputStream on + * success, and reject with an nsresult on failure. + */ + RefPtr<PageThumbStreamPromise> NewStream(nsIURI* aChildURI, + bool* aTerminateSender); + + protected: + ~PageThumbProtocolHandler() = default; + + private: + explicit PageThumbProtocolHandler(); + + [[nodiscard]] bool ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) override; + + /** + * On entry to this function, *aRetVal is expected to be non-null and already + * addrefed. This function may release the object stored in *aRetVal on entry + * and write a new pointer to an already addrefed channel to *aRetVal. + * + * @param aURI the moz-page-thumb URI. + * @param aLoadInfo the loadinfo for the request. + * @param aRetVal in/out channel param referring to the channel that + * might need to be substituted with a remote channel. + * @return NS_OK if channel has been substituted successfully or no + * substitution at all. Otherwise, returns an error. This function + * will return NS_ERROR_NO_INTERFACE if the URI resolves to a + * non file:// URI. + */ + [[nodiscard]] virtual nsresult SubstituteChannel( + nsIURI* aURI, nsILoadInfo* aLoadInfo, nsIChannel** aRetVal) override; + + /** + * This replaces the provided channel with a channel that will proxy the load + * to the parent process. + * + * @param aURI the moz-page-thumb URI. + * @param aLoadInfo the loadinfo for the request. + * @param aRetVal in/out channel param referring to the channel that + * might need to be substituted with a remote channel. + * @return NS_OK if the replacement channel was created successfully. + * Otherwise, returns an error. + */ + Result<Ok, nsresult> SubstituteRemoteChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + nsIChannel** aRetVal); + + /* + * Extracts the URL from the query string in the given moz-page-thumb URI + * and queries PageThumbsStorageService using the extracted URL to obtain + * the local file path of the screenshot. This should only be called from + * the parent because PageThumbsStorageService relies on the path of the + * profile directory, which is unavailable in the child. + * + * @param aPath the path of the moz-page-thumb URI. + * @param aThumbnailPath in/out string param referring to the thumbnail path. + * @return NS_OK if the thumbnail path was obtained successfully. Otherwise + * returns an error. + */ + nsresult GetThumbnailPath(const nsACString& aPath, nsString& aThumbnailPath); + + // To allow parent IPDL actors to invoke methods on this handler when + // handling moz-page-thumb requests from the child. + static StaticRefPtr<PageThumbProtocolHandler> sSingleton; + + // Set the channel's content type using the provided URI's type. + static void SetContentType(nsIURI* aURI, nsIChannel* aChannel); + + // Gets a SimpleChannel that wraps the provided channel. + static void NewSimpleChannel(nsIURI* aURI, nsILoadInfo* aLoadinfo, + PageThumbStreamGetter* aStreamGetter, + nsIChannel** aRetVal); +}; + +} // namespace net +} // namespace mozilla + +#endif /* PageThumbProtocolHandler_h___ */ diff --git a/netwerk/protocol/res/SubstitutingJARURI.h b/netwerk/protocol/res/SubstitutingJARURI.h new file mode 100644 index 0000000000..fe9208750c --- /dev/null +++ b/netwerk/protocol/res/SubstitutingJARURI.h @@ -0,0 +1,161 @@ +/* -*- 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/. */ + +#ifndef SubstitutingJARURI_h +#define SubstitutingJARURI_h + +#include "nsIURL.h" +#include "nsJARURI.h" +#include "nsISerializable.h" + +namespace mozilla { +namespace net { + +#define NS_SUBSTITUTINGJARURI_IMPL_CID \ + { /* 8f8c54ed-aba7-4ebf-ba6f-e58aec0aba4c */ \ + 0x8f8c54ed, 0xaba7, 0x4ebf, { \ + 0xba, 0x6f, 0xe5, 0x8a, 0xec, 0x0a, 0xba, 0x4c \ + } \ + } + +// Provides a Substituting URI for resource://-like substitutions which +// allows consumers to access the underlying jar resource. +class SubstitutingJARURI : public nsIJARURI, + public nsIStandardURL, + public nsISerializable { + protected: + // Contains the resource://-like URI to be mapped. nsIURI and nsIURL will + // forward to this. + nsCOMPtr<nsIURL> mSource; + // Contains the resolved jar resource, nsIJARURI forwards to this to let + // consumer acccess the underlying jar resource. + nsCOMPtr<nsIJARURI> mResolved; + virtual ~SubstitutingJARURI() = default; + + public: + SubstitutingJARURI(nsIURL* source, nsIJARURI* resolved); + // For deserializing + explicit SubstitutingJARURI() = default; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISERIALIZABLE + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_SUBSTITUTINGJARURI_IMPL_CID) + + NS_FORWARD_SAFE_NSIURL(mSource) + NS_FORWARD_SAFE_NSIJARURI(mResolved) + + // enum used in a few places to specify how .ref attribute should be handled + enum RefHandlingEnum { eIgnoreRef, eHonorRef }; + + // We cannot forward Equals* methods to |mSource| so we override them here + NS_IMETHOD EqualsExceptRef(nsIURI* aOther, bool* aResult) override; + NS_IMETHOD Equals(nsIURI* aOther, bool* aResult) override; + + // Helper to share code between Equals methods. + virtual nsresult EqualsInternal(nsIURI* aOther, + RefHandlingEnum aRefHandlingMode, + bool* aResult); + + // Forward the rest of nsIURI to mSource + NS_IMETHOD GetSpec(nsACString& aSpec) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetSpec(aSpec); + } + NS_IMETHOD GetPrePath(nsACString& aPrePath) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetPrePath(aPrePath); + } + NS_IMETHOD GetScheme(nsACString& aScheme) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetScheme(aScheme); + } + NS_IMETHOD GetUserPass(nsACString& aUserPass) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetUserPass(aUserPass); + } + NS_IMETHOD GetUsername(nsACString& aUsername) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetUsername(aUsername); + } + NS_IMETHOD GetPassword(nsACString& aPassword) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetPassword(aPassword); + } + NS_IMETHOD GetHostPort(nsACString& aHostPort) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetHostPort(aHostPort); + } + NS_IMETHOD GetHost(nsACString& aHost) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetHost(aHost); + } + NS_IMETHOD GetPort(int32_t* aPort) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetPort(aPort); + } + NS_IMETHOD GetPathQueryRef(nsACString& aPathQueryRef) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetPathQueryRef(aPathQueryRef); + } + NS_IMETHOD SchemeIs(const char* scheme, bool* _retval) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->SchemeIs(scheme, _retval); + } + NS_IMETHOD Resolve(const nsACString& relativePath, + nsACString& _retval) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->Resolve(relativePath, _retval); + } + NS_IMETHOD GetAsciiSpec(nsACString& aAsciiSpec) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetAsciiSpec(aAsciiSpec); + } + NS_IMETHOD GetAsciiHostPort(nsACString& aAsciiHostPort) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetAsciiHostPort(aAsciiHostPort); + } + NS_IMETHOD GetAsciiHost(nsACString& aAsciiHost) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetAsciiHost(aAsciiHost); + } + NS_IMETHOD GetRef(nsACString& aRef) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetRef(aRef); + } + NS_IMETHOD GetSpecIgnoringRef(nsACString& aSpecIgnoringRef) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetSpecIgnoringRef(aSpecIgnoringRef); + } + NS_IMETHOD GetHasRef(bool* aHasRef) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetHasRef(aHasRef); + } + NS_IMETHOD GetFilePath(nsACString& aFilePath) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetFilePath(aFilePath); + } + NS_IMETHOD GetQuery(nsACString& aQuery) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetQuery(aQuery); + } + NS_IMETHOD GetDisplayHost(nsACString& aDisplayHost) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetDisplayHost(aDisplayHost); + } + NS_IMETHOD GetDisplayHostPort(nsACString& aDisplayHostPort) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetDisplayHostPort(aDisplayHostPort); + } + NS_IMETHOD GetDisplaySpec(nsACString& aDisplaySpec) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetDisplaySpec(aDisplaySpec); + } + NS_IMETHOD GetDisplayPrePath(nsACString& aDisplayPrePath) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetDisplayPrePath(aDisplayPrePath); + } + NS_IMETHOD Mutate(nsIURIMutator** _retval) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->Mutate(_retval); + } + NS_IMETHOD_(void) Serialize(mozilla::ipc::URIParams& aParams) override { + MOZ_ASSERT(mSource); + mSource->Serialize(aParams); + } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(SubstitutingJARURI, + NS_SUBSTITUTINGJARURI_IMPL_CID) + +} // namespace net +} // namespace mozilla + +#endif /* SubstitutingJARURI_h */ diff --git a/netwerk/protocol/res/SubstitutingProtocolHandler.cpp b/netwerk/protocol/res/SubstitutingProtocolHandler.cpp new file mode 100644 index 0000000000..baf368761b --- /dev/null +++ b/netwerk/protocol/res/SubstitutingProtocolHandler.cpp @@ -0,0 +1,653 @@ +/* -*- 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 "mozilla/ModuleUtils.h" +#include "mozilla/Unused.h" +#include "mozilla/chrome/RegistryMessageUtils.h" +#include "mozilla/dom/ContentParent.h" + +#include "SubstitutingProtocolHandler.h" +#include "SubstitutingURL.h" +#include "SubstitutingJARURI.h" +#include "nsIChannel.h" +#include "nsIIOService.h" +#include "nsIFile.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsURLHelper.h" +#include "nsEscape.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIClassInfoImpl.h" + +using mozilla::dom::ContentParent; + +namespace mozilla { +namespace net { + +// Log module for Substituting Protocol logging. We keep the pre-existing module +// name of "nsResProtocol" to avoid disruption. +static LazyLogModule gResLog("nsResProtocol"); + +static NS_DEFINE_CID(kSubstitutingURLCID, NS_SUBSTITUTINGURL_CID); +static NS_DEFINE_CID(kSubstitutingJARURIImplCID, + NS_SUBSTITUTINGJARURI_IMPL_CID); + +//--------------------------------------------------------------------------------- +// SubstitutingURL : overrides nsStandardURL::GetFile to provide nsIFile +// resolution +//--------------------------------------------------------------------------------- + +// The list of interfaces should be in sync with nsStandardURL +// Queries this list of interfaces. If none match, it queries mURI. +NS_IMPL_NSIURIMUTATOR_ISUPPORTS(SubstitutingURL::Mutator, nsIURISetters, + nsIURIMutator, nsIStandardURLMutator, + nsIURLMutator, nsIFileURLMutator, + nsISerializable) + +NS_IMPL_CLASSINFO(SubstitutingURL, nullptr, nsIClassInfo::THREADSAFE, + NS_SUBSTITUTINGURL_CID) +// Empty CI getter. We only need nsIClassInfo for Serialization +NS_IMPL_CI_INTERFACE_GETTER0(SubstitutingURL) + +NS_IMPL_ADDREF_INHERITED(SubstitutingURL, nsStandardURL) +NS_IMPL_RELEASE_INHERITED(SubstitutingURL, nsStandardURL) +NS_IMPL_QUERY_INTERFACE_CI_INHERITED0(SubstitutingURL, nsStandardURL) + +nsresult SubstitutingURL::EnsureFile() { + nsAutoCString ourScheme; + nsresult rv = GetScheme(ourScheme); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the handler associated with this scheme. It would be nice to just + // pass this in when constructing SubstitutingURLs, but we need a generic + // factory constructor. + nsCOMPtr<nsIIOService> io = do_GetIOService(&rv); + nsCOMPtr<nsIProtocolHandler> handler; + rv = io->GetProtocolHandler(ourScheme.get(), getter_AddRefs(handler)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsISubstitutingProtocolHandler> substHandler = + do_QueryInterface(handler); + MOZ_ASSERT(substHandler); + + nsAutoCString spec; + rv = substHandler->ResolveURI(this, spec); + if (NS_FAILED(rv)) return rv; + + nsAutoCString scheme; + rv = net_ExtractURLScheme(spec, scheme); + if (NS_FAILED(rv)) return rv; + + // Bug 585869: + // In most cases, the scheme is jar if it's not file. + // Regardless, net_GetFileFromURLSpec should be avoided + // when the scheme isn't file. + if (!scheme.EqualsLiteral("file")) return NS_ERROR_NO_INTERFACE; + + return net_GetFileFromURLSpec(spec, getter_AddRefs(mFile)); +} + +/* virtual */ +nsStandardURL* SubstitutingURL::StartClone() { + SubstitutingURL* clone = new SubstitutingURL(); + return clone; +} + +void SubstitutingURL::Serialize(ipc::URIParams& aParams) { + nsStandardURL::Serialize(aParams); + aParams.get_StandardURLParams().isSubstituting() = true; +} + +// SubstitutingJARURI + +SubstitutingJARURI::SubstitutingJARURI(nsIURL* source, nsIJARURI* resolved) + : mSource(source), mResolved(resolved) {} + +// SubstitutingJARURI::nsIURI + +NS_IMETHODIMP +SubstitutingJARURI::Equals(nsIURI* aOther, bool* aResult) { + return EqualsInternal(aOther, eHonorRef, aResult); +} + +NS_IMETHODIMP +SubstitutingJARURI::EqualsExceptRef(nsIURI* aOther, bool* aResult) { + return EqualsInternal(aOther, eIgnoreRef, aResult); +} + +nsresult SubstitutingJARURI::EqualsInternal(nsIURI* aOther, + RefHandlingEnum aRefHandlingMode, + bool* aResult) { + *aResult = false; + if (!aOther) { + return NS_OK; + } + + nsresult rv; + RefPtr<SubstitutingJARURI> other; + rv = + aOther->QueryInterface(kSubstitutingJARURIImplCID, getter_AddRefs(other)); + if (NS_FAILED(rv)) { + return NS_OK; + } + + // We only need to check the source as the resolved URI is the same for a + // given source + return aRefHandlingMode == eHonorRef + ? mSource->Equals(other->mSource, aResult) + : mSource->EqualsExceptRef(other->mSource, aResult); +} + +// SubstitutingJARURI::nsISerializable + +NS_IMETHODIMP +SubstitutingJARURI::Read(nsIObjectInputStream* aStream) { + MOZ_ASSERT(!mSource); + MOZ_ASSERT(!mResolved); + NS_ENSURE_ARG_POINTER(aStream); + + nsresult rv; + nsCOMPtr<nsISupports> source; + rv = aStream->ReadObject(true, getter_AddRefs(source)); + NS_ENSURE_SUCCESS(rv, rv); + + mSource = do_QueryInterface(source, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> resolved; + rv = aStream->ReadObject(true, getter_AddRefs(resolved)); + NS_ENSURE_SUCCESS(rv, rv); + + mResolved = do_QueryInterface(resolved, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +SubstitutingJARURI::Write(nsIObjectOutputStream* aStream) { + NS_ENSURE_ARG_POINTER(aStream); + + nsresult rv; + rv = aStream->WriteCompoundObject(mSource, NS_GET_IID(nsISupports), true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aStream->WriteCompoundObject(mResolved, NS_GET_IID(nsISupports), true); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMPL_CLASSINFO(SubstitutingJARURI, nullptr, nsIClassInfo::MAIN_THREAD_ONLY, + NS_SUBSTITUTINGJARURI_CID) + +NS_IMPL_ADDREF(SubstitutingJARURI) +NS_IMPL_RELEASE(SubstitutingJARURI) + +NS_INTERFACE_MAP_BEGIN(SubstitutingJARURI) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURI) + NS_INTERFACE_MAP_ENTRY(nsIJARURI) + NS_INTERFACE_MAP_ENTRY(nsIURL) + NS_INTERFACE_MAP_ENTRY(nsIStandardURL) + NS_INTERFACE_MAP_ENTRY(nsISerializable) + if (aIID.Equals(kSubstitutingJARURIImplCID)) { + foundInterface = static_cast<nsIURI*>(this); + } else + NS_INTERFACE_MAP_ENTRY(nsIURI) + NS_IMPL_QUERY_CLASSINFO(SubstitutingJARURI) +NS_INTERFACE_MAP_END + +NS_IMPL_CI_INTERFACE_GETTER(SubstitutingJARURI, nsIURI, nsIJARURI, nsIURL, + nsIStandardURL, nsISerializable) + +SubstitutingProtocolHandler::SubstitutingProtocolHandler(const char* aScheme, + uint32_t aFlags, + bool aEnforceFileOrJar) + : mScheme(aScheme), + mSubstitutionsLock("SubstitutingProtocolHandler::mSubstitutions"), + mSubstitutions(16), + mEnforceFileOrJar(aEnforceFileOrJar) { + mFlags.emplace(aFlags); + ConstructInternal(); +} + +SubstitutingProtocolHandler::SubstitutingProtocolHandler(const char* aScheme) + : mScheme(aScheme), + mSubstitutionsLock("SubstitutingProtocolHandler::mSubstitutions"), + mSubstitutions(16), + mEnforceFileOrJar(true) { + ConstructInternal(); +} + +void SubstitutingProtocolHandler::ConstructInternal() { + nsresult rv; + mIOService = do_GetIOService(&rv); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv) && mIOService); +} + +// +// IPC marshalling. +// + +nsresult SubstitutingProtocolHandler::CollectSubstitutions( + nsTArray<SubstitutionMapping>& aMappings) { + AutoReadLock lock(mSubstitutionsLock); + for (auto iter = mSubstitutions.ConstIter(); !iter.Done(); iter.Next()) { + SubstitutionEntry& entry = iter.Data(); + nsCOMPtr<nsIURI> uri = entry.baseURI; + SerializedURI serialized; + if (uri) { + nsresult rv = uri->GetSpec(serialized.spec); + NS_ENSURE_SUCCESS(rv, rv); + } + SubstitutionMapping substitution = {mScheme, nsCString(iter.Key()), + serialized, entry.flags}; + aMappings.AppendElement(substitution); + } + + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::SendSubstitution(const nsACString& aRoot, + nsIURI* aBaseURI, + uint32_t aFlags) { + if (GeckoProcessType_Content == XRE_GetProcessType()) { + return NS_OK; + } + + nsTArray<ContentParent*> parents; + ContentParent::GetAll(parents); + if (!parents.Length()) { + return NS_OK; + } + + SubstitutionMapping mapping; + mapping.scheme = mScheme; + mapping.path = aRoot; + if (aBaseURI) { + nsresult rv = aBaseURI->GetSpec(mapping.resolvedURI.spec); + NS_ENSURE_SUCCESS(rv, rv); + } + mapping.flags = aFlags; + + for (uint32_t i = 0; i < parents.Length(); i++) { + Unused << parents[i]->SendRegisterChromeItem(mapping); + } + + return NS_OK; +} + +//---------------------------------------------------------------------------- +// nsIProtocolHandler +//---------------------------------------------------------------------------- + +nsresult SubstitutingProtocolHandler::GetScheme(nsACString& result) { + result = mScheme; + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::GetDefaultPort(int32_t* result) { + *result = -1; + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::GetProtocolFlags(uint32_t* result) { + if (mFlags.isNothing()) { + NS_WARNING( + "Trying to get protocol flags the wrong way - use " + "nsIProtocolHandlerWithDynamicFlags instead"); + return NS_ERROR_NOT_AVAILABLE; + } + + *result = mFlags.ref(); + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::NewURI(const nsACString& aSpec, + const char* aCharset, + nsIURI* aBaseURI, + nsIURI** aResult) { + // unescape any %2f and %2e to make sure nsStandardURL coalesces them. + // Later net_GetFileFromURLSpec() will do a full unescape and we want to + // treat them the same way the file system will. (bugs 380994, 394075) + nsresult rv; + nsAutoCString spec; + const char* src = aSpec.BeginReading(); + const char* end = aSpec.EndReading(); + const char* last = src; + + spec.SetCapacity(aSpec.Length() + 1); + for (; src < end; ++src) { + if (*src == '%' && (src < end - 2) && *(src + 1) == '2') { + char ch = '\0'; + if (*(src + 2) == 'f' || *(src + 2) == 'F') { + ch = '/'; + } else if (*(src + 2) == 'e' || *(src + 2) == 'E') { + ch = '.'; + } + + if (ch) { + if (last < src) { + spec.Append(last, src - last); + } + spec.Append(ch); + src += 2; + last = src + 1; // src will be incremented by the loop + } + } + if (*src == '?' || *src == '#') { + break; // Don't escape %2f and %2e in the query or ref parts of the URI + } + } + + if (last < end) { + spec.Append(last, end - last); + } + + nsCOMPtr<nsIURI> base(aBaseURI); + nsCOMPtr<nsIURL> uri; + rv = NS_MutateURI(new SubstitutingURL::Mutator()) + .Apply(NS_MutatorMethod(&nsIStandardURLMutator::Init, + nsIStandardURL::URLTYPE_STANDARD, -1, spec, + aCharset, base, nullptr)) + .Finalize(uri); + if (NS_FAILED(rv)) return rv; + + nsAutoCString host; + rv = uri->GetHost(host); + if (NS_FAILED(rv)) return rv; + + // "android" is the only root that would return the RESOLVE_JAR_URI flag + // see nsResProtocolHandler::GetSubstitutionInternal + if (MustResolveJAR(host)) { + return ResolveJARURI(uri, aResult); + } + + uri.forget(aResult); + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::ResolveJARURI(nsIURL* aURL, + nsIURI** aResult) { + nsAutoCString spec; + nsresult rv = ResolveURI(aURL, spec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> resolvedURI; + rv = NS_NewURI(getter_AddRefs(resolvedURI), spec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> innermostURI = NS_GetInnermostURI(resolvedURI); + nsAutoCString scheme; + innermostURI->GetScheme(scheme); + + // We only ever want to resolve to a local jar. + NS_ENSURE_TRUE(scheme.EqualsLiteral("file"), NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIJARURI> jarURI(do_QueryInterface(resolvedURI)); + if (!jarURI) { + // This substitution does not resolve to a jar: URL, so we just + // return the plain SubstitutionURL + nsCOMPtr<nsIURI> url = aURL; + url.forget(aResult); + return NS_OK; + } + + nsCOMPtr<nsIJARURI> result = new SubstitutingJARURI(aURL, jarURI); + result.forget(aResult); + + return rv; +} + +nsresult SubstitutingProtocolHandler::NewChannel(nsIURI* uri, + nsILoadInfo* aLoadInfo, + nsIChannel** result) { + NS_ENSURE_ARG_POINTER(uri); + NS_ENSURE_ARG_POINTER(aLoadInfo); + + nsAutoCString spec; + nsresult rv = ResolveURI(uri, spec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> newURI; + rv = NS_NewURI(getter_AddRefs(newURI), spec); + NS_ENSURE_SUCCESS(rv, rv); + + // We don't want to allow the inner protocol handler to modify the result + // principal URI since we want either |uri| or anything pre-set by upper + // layers to prevail. + nsCOMPtr<nsIURI> savedResultPrincipalURI; + rv = + aLoadInfo->GetResultPrincipalURI(getter_AddRefs(savedResultPrincipalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewChannelInternal(result, newURI, aLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aLoadInfo->SetResultPrincipalURI(savedResultPrincipalURI); + NS_ENSURE_SUCCESS(rv, rv); + rv = (*result)->SetOriginalURI(uri); + NS_ENSURE_SUCCESS(rv, rv); + + return SubstituteChannel(uri, aLoadInfo, result); +} + +nsresult SubstitutingProtocolHandler::AllowPort(int32_t port, + const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +//---------------------------------------------------------------------------- +// nsISubstitutingProtocolHandler +//---------------------------------------------------------------------------- + +nsresult SubstitutingProtocolHandler::SetSubstitution(const nsACString& root, + nsIURI* baseURI) { + // Add-ons use this API but they should not be able to make anything + // content-accessible. + return SetSubstitutionWithFlags(root, baseURI, 0); +} + +nsresult SubstitutingProtocolHandler::SetSubstitutionWithFlags( + const nsACString& origRoot, nsIURI* baseURI, uint32_t flags) { + nsAutoCString root; + ToLowerCase(origRoot, root); + + if (!baseURI) { + { + AutoWriteLock lock(mSubstitutionsLock); + mSubstitutions.Remove(root); + } + + return SendSubstitution(root, baseURI, flags); + } + + // If baseURI isn't a same-scheme URI, we can set the substitution + // immediately. + nsAutoCString scheme; + nsresult rv = baseURI->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, rv); + if (!scheme.Equals(mScheme)) { + if (mEnforceFileOrJar && !scheme.EqualsLiteral("file") && + !scheme.EqualsLiteral("jar") && !scheme.EqualsLiteral("app") && + !scheme.EqualsLiteral("resource")) { + NS_WARNING("Refusing to create substituting URI to non-file:// target"); + return NS_ERROR_INVALID_ARG; + } + + { + AutoWriteLock lock(mSubstitutionsLock); + SubstitutionEntry& entry = mSubstitutions.GetOrInsert(root); + entry.baseURI = baseURI; + entry.flags = flags; + } + + return SendSubstitution(root, baseURI, flags); + } + + // baseURI is a same-type substituting URI, let's resolve it first. + nsAutoCString newBase; + rv = ResolveURI(baseURI, newBase); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIURI> newBaseURI; + rv = + mIOService->NewURI(newBase, nullptr, nullptr, getter_AddRefs(newBaseURI)); + NS_ENSURE_SUCCESS(rv, rv); + + { + AutoWriteLock lock(mSubstitutionsLock); + SubstitutionEntry& entry = mSubstitutions.GetOrInsert(root); + entry.baseURI = newBaseURI; + entry.flags = flags; + } + + return SendSubstitution(root, newBaseURI, flags); +} + +nsresult SubstitutingProtocolHandler::GetSubstitution( + const nsACString& origRoot, nsIURI** result) { + NS_ENSURE_ARG_POINTER(result); + + nsAutoCString root; + ToLowerCase(origRoot, root); + + { + AutoReadLock lock(mSubstitutionsLock); + SubstitutionEntry entry; + if (mSubstitutions.Get(root, &entry)) { + nsCOMPtr<nsIURI> baseURI = entry.baseURI; + baseURI.forget(result); + return NS_OK; + } + } + + uint32_t flags; + return GetSubstitutionInternal(root, result, &flags); +} + +nsresult SubstitutingProtocolHandler::GetSubstitutionFlags( + const nsACString& root, uint32_t* flags) { +#ifdef DEBUG + nsAutoCString lcRoot; + ToLowerCase(root, lcRoot); + MOZ_ASSERT(root.Equals(lcRoot), + "GetSubstitutionFlags should never receive mixed-case root name"); +#endif + + *flags = 0; + + { + AutoReadLock lock(mSubstitutionsLock); + + SubstitutionEntry entry; + if (mSubstitutions.Get(root, &entry)) { + *flags = entry.flags; + return NS_OK; + } + } + + nsCOMPtr<nsIURI> baseURI; + return GetSubstitutionInternal(root, getter_AddRefs(baseURI), flags); +} + +nsresult SubstitutingProtocolHandler::HasSubstitution( + const nsACString& origRoot, bool* result) { + NS_ENSURE_ARG_POINTER(result); + + nsAutoCString root; + ToLowerCase(origRoot, root); + + *result = HasSubstitution(root); + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::ResolveURI(nsIURI* uri, + nsACString& result) { + nsresult rv; + + nsAutoCString host; + nsAutoCString path; + nsAutoCString pathname; + + nsCOMPtr<nsIURL> url = do_QueryInterface(uri); + if (!url) { + return NS_ERROR_MALFORMED_URI; + } + + rv = uri->GetAsciiHost(host); + if (NS_FAILED(rv)) return rv; + + rv = uri->GetPathQueryRef(path); + if (NS_FAILED(rv)) return rv; + + rv = url->GetFilePath(pathname); + if (NS_FAILED(rv)) return rv; + + if (ResolveSpecialCases(host, path, pathname, result)) { + return NS_OK; + } + + nsCOMPtr<nsIURI> baseURI; + rv = GetSubstitution(host, getter_AddRefs(baseURI)); + if (NS_FAILED(rv)) return rv; + + // Unescape the path so we can perform some checks on it. + NS_UnescapeURL(pathname); + if (pathname.FindChar('\\') != -1) { + return NS_ERROR_MALFORMED_URI; + } + + // Some code relies on an empty path resolving to a file rather than a + // directory. + NS_ASSERTION(path.CharAt(0) == '/', "Path must begin with '/'"); + if (path.Length() == 1) { + rv = baseURI->GetSpec(result); + } else { + // Make sure we always resolve the path as file-relative to our target URI. + // When the baseURI is a nsIFileURL, and the directory it points to doesn't + // exist, it doesn't end with a /. In that case, a file-relative resolution + // is going to pick something in the parent directory, so we resolve using + // an absolute path derived from the full path in that case. + nsCOMPtr<nsIFileURL> baseDir = do_QueryInterface(baseURI); + if (baseDir) { + nsAutoCString basePath; + rv = baseURI->GetFilePath(basePath); + if (NS_SUCCEEDED(rv) && !StringEndsWith(basePath, "/"_ns)) { + // Cf. the assertion above, path already starts with a /, so prefixing + // with a string that doesn't end with one will leave us wit the right + // amount of /. + path.Insert(basePath, 0); + } else { + // Allow to fall through below. + baseDir = nullptr; + } + } + if (!baseDir) { + path.Insert('.', 0); + } + rv = baseURI->Resolve(path, result); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (MOZ_LOG_TEST(gResLog, LogLevel::Debug)) { + nsAutoCString spec; + uri->GetAsciiSpec(spec); + MOZ_LOG(gResLog, LogLevel::Debug, + ("%s\n -> %s\n", spec.get(), PromiseFlatCString(result).get())); + } + return rv; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/res/SubstitutingProtocolHandler.h b/netwerk/protocol/res/SubstitutingProtocolHandler.h new file mode 100644 index 0000000000..57bce8c7fe --- /dev/null +++ b/netwerk/protocol/res/SubstitutingProtocolHandler.h @@ -0,0 +1,134 @@ +/* -*- 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/. */ + +#ifndef SubstitutingProtocolHandler_h___ +#define SubstitutingProtocolHandler_h___ + +#include "nsISubstitutingProtocolHandler.h" + +#include "nsDataHashtable.h" +#include "nsStandardURL.h" +#include "nsJARURI.h" +#include "mozilla/chrome/RegistryMessageUtils.h" +#include "mozilla/Maybe.h" +#include "mozilla/RWLock.h" + +class nsIIOService; + +namespace mozilla { +namespace net { + +// +// Base class for resource://-like substitution protocols. +// +// If you add a new protocol, make sure to change nsChromeRegistryChrome +// to properly invoke CollectSubstitutions at the right time. +class SubstitutingProtocolHandler { + public: + SubstitutingProtocolHandler(const char* aScheme, uint32_t aFlags, + bool aEnforceFileOrJar = true); + explicit SubstitutingProtocolHandler(const char* aScheme); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SubstitutingProtocolHandler); + NS_DECL_NON_VIRTUAL_NSIPROTOCOLHANDLER; + NS_DECL_NON_VIRTUAL_NSISUBSTITUTINGPROTOCOLHANDLER; + + bool HasSubstitution(const nsACString& aRoot) const { + AutoReadLock lock(const_cast<RWLock&>(mSubstitutionsLock)); + return mSubstitutions.Get(aRoot, nullptr); + } + + nsresult NewURI(const nsACString& aSpec, const char* aCharset, + nsIURI* aBaseURI, nsIURI** aResult); + + [[nodiscard]] nsresult CollectSubstitutions( + nsTArray<SubstitutionMapping>& aResources); + + protected: + virtual ~SubstitutingProtocolHandler() = default; + void ConstructInternal(); + + [[nodiscard]] nsresult SendSubstitution(const nsACString& aRoot, + nsIURI* aBaseURI, uint32_t aFlags); + + nsresult GetSubstitutionFlags(const nsACString& root, uint32_t* flags); + + // Override this in the subclass to try additional lookups after checking + // mSubstitutions. + [[nodiscard]] virtual nsresult GetSubstitutionInternal( + const nsACString& aRoot, nsIURI** aResult, uint32_t* aFlags) { + *aResult = nullptr; + *aFlags = 0; + return NS_ERROR_NOT_AVAILABLE; + } + + // Override this in the subclass to check for special case when resolving URIs + // _before_ checking substitutions. + [[nodiscard]] virtual bool ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) { + return false; + } + + // This method should only return true if GetSubstitutionInternal would + // return the RESOLVE_JAR_URI flag. + [[nodiscard]] virtual bool MustResolveJAR(const nsACString& aRoot) { + return false; + } + + // Override this in the subclass to check for special case when opening + // channels. + [[nodiscard]] virtual nsresult SubstituteChannel(nsIURI* uri, + nsILoadInfo* aLoadInfo, + nsIChannel** result) { + return NS_OK; + } + + nsIIOService* IOService() { return mIOService; } + + private: + struct SubstitutionEntry { + SubstitutionEntry() : flags(0) {} + + ~SubstitutionEntry() = default; + + nsCOMPtr<nsIURI> baseURI; + uint32_t flags; + }; + + // Notifies all observers that a new substitution from |aRoot| to + // |aBaseURI| has been set/installed for this protocol handler. + void NotifyObservers(const nsACString& aRoot, nsIURI* aBaseURI); + + nsCString mScheme; + Maybe<uint32_t> mFlags; + + RWLock mSubstitutionsLock; + nsDataHashtable<nsCStringHashKey, SubstitutionEntry> mSubstitutions; + nsCOMPtr<nsIIOService> mIOService; + + // Returns a SubstitutingJARURI if |aUrl| maps to a |jar:| URI, + // otherwise will return |aURL| + nsresult ResolveJARURI(nsIURL* aURL, nsIURI** aResult); + + // In general, we expect the principal of a document loaded from a + // substituting URI to be a content principal for that URI (rather than + // a principal for whatever is underneath). However, this only works if + // the protocol handler for the underlying URI doesn't set an explicit + // owner (which chrome:// does, for example). So we want to require that + // substituting URIs only map to other URIs of the same type, or to + // file:// and jar:// URIs. + // + // Enforcing this for ye olde resource:// URIs could carry compat risks, so + // we just try to enforce it on new protocols going forward. + bool mEnforceFileOrJar; +}; + +} // namespace net +} // namespace mozilla + +#endif /* SubstitutingProtocolHandler_h___ */ diff --git a/netwerk/protocol/res/SubstitutingURL.h b/netwerk/protocol/res/SubstitutingURL.h new file mode 100644 index 0000000000..b9012f1665 --- /dev/null +++ b/netwerk/protocol/res/SubstitutingURL.h @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +#ifndef SubstitutingURL_h +#define SubstitutingURL_h + +#include "nsStandardURL.h" + +class nsIIOService; + +namespace mozilla { +namespace net { + +// SubstitutingURL : overrides nsStandardURL::GetFile to provide nsIFile +// resolution +class SubstitutingURL : public nsStandardURL { + public: + NS_DECL_ISUPPORTS_INHERITED + virtual nsStandardURL* StartClone() override; + [[nodiscard]] virtual nsresult EnsureFile() override; + + private: + explicit SubstitutingURL() : nsStandardURL(true) {} + explicit SubstitutingURL(bool aSupportsFileURL) : nsStandardURL(true) { + MOZ_ASSERT(aSupportsFileURL); + } + virtual nsresult Clone(nsIURI** aURI) override { + return nsStandardURL::Clone(aURI); + } + virtual ~SubstitutingURL() = default; + + public: + class Mutator : public TemplatedMutator<SubstitutingURL> { + NS_DECL_ISUPPORTS + public: + explicit Mutator() = default; + + private: + virtual ~Mutator() = default; + + SubstitutingURL* Create() override { return new SubstitutingURL(); } + }; + + NS_IMETHOD Mutate(nsIURIMutator** aMutator) override { + RefPtr<SubstitutingURL::Mutator> mutator = new SubstitutingURL::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; + } + + NS_IMETHOD_(void) Serialize(ipc::URIParams& aParams) override; + + friend BaseURIMutator<SubstitutingURL>; + friend TemplatedMutator<SubstitutingURL>; +}; + +} // namespace net +} // namespace mozilla + +#endif /* SubstitutingURL_h */ diff --git a/netwerk/protocol/res/moz.build b/netwerk/protocol/res/moz.build new file mode 100644 index 0000000000..04bdfeff79 --- /dev/null +++ b/netwerk/protocol/res/moz.build @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_SOURCES += [ + "nsIResProtocolHandler.idl", + "nsISubstitutingProtocolHandler.idl", +] + +XPIDL_MODULE = "necko_res" + +EXPORTS.mozilla.net += [ + "ExtensionProtocolHandler.h", + "PageThumbProtocolHandler.h", + "SubstitutingJARURI.h", + "SubstitutingProtocolHandler.h", + "SubstitutingURL.h", +] + +EXPORTS += [ + "nsResProtocolHandler.h", +] + +UNIFIED_SOURCES += [ + "ExtensionProtocolHandler.cpp", + "nsResProtocolHandler.cpp", + "PageThumbProtocolHandler.cpp", + "SubstitutingProtocolHandler.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/netwerk/base", + "/xpcom/base", +] diff --git a/netwerk/protocol/res/nsIResProtocolHandler.idl b/netwerk/protocol/res/nsIResProtocolHandler.idl new file mode 100644 index 0000000000..7046f2f1d4 --- /dev/null +++ b/netwerk/protocol/res/nsIResProtocolHandler.idl @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISubstitutingProtocolHandler.idl" + +/** + * Protocol handler interface for the resource:// protocol + */ +[scriptable, uuid(241d34ac-9ed5-46d7-910c-7a9d914aa0c5)] +interface nsIResProtocolHandler : nsISubstitutingProtocolHandler +{ + boolean allowContentToAccess(in nsIURI url); +}; diff --git a/netwerk/protocol/res/nsISubstitutingProtocolHandler.idl b/netwerk/protocol/res/nsISubstitutingProtocolHandler.idl new file mode 100644 index 0000000000..cf2e0d42ab --- /dev/null +++ b/netwerk/protocol/res/nsISubstitutingProtocolHandler.idl @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsIProtocolHandler.idl" + + +/** + * Protocol handler superinterface for a protocol which performs substitutions + * from URIs of its scheme to URIs of another scheme. + */ +[scriptable, uuid(154c64fd-a69e-4105-89f8-bd7dfe621372)] +interface nsISubstitutingProtocolHandler : nsIProtocolHandler +{ + /** + * Content script may access files in this package. + */ + const short ALLOW_CONTENT_ACCESS = 1 << 0; + /** + * This substitution exposes nsIJARURI instead of a nsIFileURL. By default + * NewURI will always return a nsIFileURL even when the URL is jar: + */ + const short RESOLVE_JAR_URI = 1 << 1; + + /** + * Sets the substitution for the root key: + * resource://root/path ==> baseURI.resolve(path) + * + * A null baseURI removes the specified substitution. + * + * The root key will be converted to lower-case to conform to + * case-insensitive URI hostname matching behavior. + */ + [must_use] void setSubstitution(in ACString root, in nsIURI baseURI); + + /** + * Same as setSubstitution, but with specific flags. + */ + [must_use] void setSubstitutionWithFlags(in ACString root, in nsIURI baseURI, in uint32_t flags); + + /** + * Gets the substitution for the root key. + * + * @throws NS_ERROR_NOT_AVAILABLE if none exists. + */ + [must_use] nsIURI getSubstitution(in ACString root); + + /** + * Returns TRUE if the substitution exists and FALSE otherwise. + */ + [must_use] boolean hasSubstitution(in ACString root); + + /** + * Utility function to resolve a substituted URI. A resolved URI is not + * guaranteed to reference a resource that exists (ie. opening a channel to + * the resolved URI may fail). + * + * @throws NS_ERROR_NOT_AVAILABLE if resURI.host() is an unknown root key. + */ + [must_use] AUTF8String resolveURI(in nsIURI resURI); +}; diff --git a/netwerk/protocol/res/nsResProtocolHandler.cpp b/netwerk/protocol/res/nsResProtocolHandler.cpp new file mode 100644 index 0000000000..6918799e35 --- /dev/null +++ b/netwerk/protocol/res/nsResProtocolHandler.cpp @@ -0,0 +1,194 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "mozilla/chrome/RegistryMessageUtils.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/Unused.h" + +#include "nsResProtocolHandler.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsURLHelper.h" +#include "nsEscape.h" + +#include "mozilla/Omnijar.h" + +using mozilla::LogLevel; +using mozilla::Unused; +using mozilla::dom::ContentParent; + +#define kAPP "app" +#define kGRE "gre" +#define kAndroid "android" + +mozilla::StaticRefPtr<nsResProtocolHandler> nsResProtocolHandler::sSingleton; + +already_AddRefed<nsResProtocolHandler> nsResProtocolHandler::GetSingleton() { + if (!sSingleton) { + RefPtr<nsResProtocolHandler> handler = new nsResProtocolHandler(); + if (NS_WARN_IF(NS_FAILED(handler->Init()))) { + return nullptr; + } + sSingleton = handler; + ClearOnShutdown(&sSingleton); + } + return do_AddRef(sSingleton); +} + +nsresult nsResProtocolHandler::Init() { + nsresult rv; + rv = mozilla::Omnijar::GetURIString(mozilla::Omnijar::APP, mAppURI); + NS_ENSURE_SUCCESS(rv, rv); + rv = mozilla::Omnijar::GetURIString(mozilla::Omnijar::GRE, mGREURI); + NS_ENSURE_SUCCESS(rv, rv); + + // mozilla::Omnijar::GetURIString always returns a string ending with /, + // and we want to remove it. + mGREURI.Truncate(mGREURI.Length() - 1); + if (mAppURI.Length()) { + mAppURI.Truncate(mAppURI.Length() - 1); + } else { + mAppURI = mGREURI; + } + +#ifdef ANDROID + rv = GetApkURI(mApkURI); +#endif + + // XXXbsmedberg Neil wants a resource://pchrome/ for the profile chrome dir... + // but once I finish multiple chrome registration I'm not sure that it is + // needed + + // XXX dveditz: resource://pchrome/ defeats profile directory salting + // if web content can load it. Tread carefully. + + return rv; +} + +#ifdef ANDROID +nsresult nsResProtocolHandler::GetApkURI(nsACString& aResult) { + nsCString::const_iterator start, iter; + mGREURI.BeginReading(start); + mGREURI.EndReading(iter); + nsCString::const_iterator start_iter = start; + + // This is like jar:jar:file://path/to/apk/base.apk!/path/to/omni.ja!/ + bool found = FindInReadable("!/"_ns, start_iter, iter); + NS_ENSURE_TRUE(found, NS_ERROR_UNEXPECTED); + + // like jar:jar:file://path/to/apk/base.apk!/ + const nsDependentCSubstring& withoutPath = Substring(start, iter); + NS_ENSURE_TRUE(withoutPath.Length() >= 4, NS_ERROR_UNEXPECTED); + + // Let's make sure we're removing what we expect to remove + NS_ENSURE_TRUE(Substring(withoutPath, 0, 4).EqualsLiteral("jar:"), + NS_ERROR_UNEXPECTED); + + // like jar:file://path/to/apk/base.apk!/ + aResult = ToNewCString(Substring(withoutPath, 4)); + + // Remove the trailing / + NS_ENSURE_TRUE(aResult.Length() >= 1, NS_ERROR_UNEXPECTED); + aResult.Truncate(aResult.Length() - 1); + return NS_OK; +} +#endif + +//---------------------------------------------------------------------------- +// nsResProtocolHandler::nsISupports +//---------------------------------------------------------------------------- + +NS_IMPL_QUERY_INTERFACE(nsResProtocolHandler, nsIResProtocolHandler, + nsISubstitutingProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) +NS_IMPL_ADDREF_INHERITED(nsResProtocolHandler, SubstitutingProtocolHandler) +NS_IMPL_RELEASE_INHERITED(nsResProtocolHandler, SubstitutingProtocolHandler) + +NS_IMETHODIMP +nsResProtocolHandler::AllowContentToAccess(nsIURI* aURI, bool* aResult) { + *aResult = false; + + nsAutoCString host; + nsresult rv = aURI->GetAsciiHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t flags; + rv = GetSubstitutionFlags(host, &flags); + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = flags & nsISubstitutingProtocolHandler::ALLOW_CONTENT_ACCESS; + return NS_OK; +} + +nsresult nsResProtocolHandler::GetSubstitutionInternal(const nsACString& aRoot, + nsIURI** aResult, + uint32_t* aFlags) { + nsAutoCString uri; + + if (!ResolveSpecialCases(aRoot, "/"_ns, "/"_ns, uri)) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (aRoot.Equals(kAndroid)) { + *aFlags = nsISubstitutingProtocolHandler::RESOLVE_JAR_URI; + } else { + *aFlags = 0; // No content access. + } + return NS_NewURI(aResult, uri); +} + +bool nsResProtocolHandler::ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) { + if (aHost.EqualsLiteral("") || aHost.EqualsLiteral(kAPP)) { + aResult.Assign(mAppURI); + } else if (aHost.Equals(kGRE)) { + aResult.Assign(mGREURI); +#ifdef ANDROID + } else if (aHost.Equals(kAndroid)) { + aResult.Assign(mApkURI); +#endif + } else { + return false; + } + aResult.Append(aPath); + return true; +} + +nsresult nsResProtocolHandler::SetSubstitution(const nsACString& aRoot, + nsIURI* aBaseURI) { + MOZ_ASSERT(!aRoot.EqualsLiteral("")); + MOZ_ASSERT(!aRoot.EqualsLiteral(kAPP)); + MOZ_ASSERT(!aRoot.EqualsLiteral(kGRE)); + MOZ_ASSERT(!aRoot.EqualsLiteral(kAndroid)); + return SubstitutingProtocolHandler::SetSubstitution(aRoot, aBaseURI); +} + +nsresult nsResProtocolHandler::SetSubstitutionWithFlags(const nsACString& aRoot, + nsIURI* aBaseURI, + uint32_t aFlags) { + MOZ_ASSERT(!aRoot.EqualsLiteral("")); + MOZ_ASSERT(!aRoot.EqualsLiteral(kAPP)); + MOZ_ASSERT(!aRoot.EqualsLiteral(kGRE)); + MOZ_ASSERT(!aRoot.EqualsLiteral(kAndroid)); + return SubstitutingProtocolHandler::SetSubstitutionWithFlags(aRoot, aBaseURI, + aFlags); +} + +nsresult nsResProtocolHandler::HasSubstitution(const nsACString& aRoot, + bool* aResult) { + if (aRoot.EqualsLiteral(kAPP) || aRoot.EqualsLiteral(kGRE) +#ifdef ANDROID + || aRoot.EqualsLiteral(kAndroid) +#endif + ) { + *aResult = true; + return NS_OK; + } + + return mozilla::net::SubstitutingProtocolHandler::HasSubstitution(aRoot, + aResult); +} diff --git a/netwerk/protocol/res/nsResProtocolHandler.h b/netwerk/protocol/res/nsResProtocolHandler.h new file mode 100644 index 0000000000..2d672dfad6 --- /dev/null +++ b/netwerk/protocol/res/nsResProtocolHandler.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsResProtocolHandler_h___ +#define nsResProtocolHandler_h___ + +#include "mozilla/net/SubstitutingProtocolHandler.h" + +#include "nsIResProtocolHandler.h" +#include "nsInterfaceHashtable.h" +#include "nsWeakReference.h" + +struct SubstitutionMapping; +class nsResProtocolHandler final + : public nsIResProtocolHandler, + public mozilla::net::SubstitutingProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRESPROTOCOLHANDLER + + static already_AddRefed<nsResProtocolHandler> GetSingleton(); + + NS_FORWARD_NSIPROTOCOLHANDLER(mozilla::net::SubstitutingProtocolHandler::) + + nsResProtocolHandler() + : mozilla::net::SubstitutingProtocolHandler( + "resource", + URI_STD | URI_IS_UI_RESOURCE | URI_IS_LOCAL_RESOURCE | + URI_IS_POTENTIALLY_TRUSTWORTHY, + /* aEnforceFileOrJar = */ false) {} + + NS_IMETHOD SetSubstitution(const nsACString& aRoot, + nsIURI* aBaseURI) override; + NS_IMETHOD SetSubstitutionWithFlags(const nsACString& aRoot, nsIURI* aBaseURI, + uint32_t aFlags) override; + NS_IMETHOD HasSubstitution(const nsACString& aRoot, bool* aResult) override; + + NS_IMETHOD GetSubstitution(const nsACString& aRoot, + nsIURI** aResult) override { + return mozilla::net::SubstitutingProtocolHandler::GetSubstitution(aRoot, + aResult); + } + + NS_IMETHOD ResolveURI(nsIURI* aResURI, nsACString& aResult) override { + return mozilla::net::SubstitutingProtocolHandler::ResolveURI(aResURI, + aResult); + } + + protected: + [[nodiscard]] nsresult GetSubstitutionInternal(const nsACString& aRoot, + nsIURI** aResult, + uint32_t* aFlags) override; + virtual ~nsResProtocolHandler() = default; + + [[nodiscard]] bool ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) override; + + [[nodiscard]] virtual bool MustResolveJAR(const nsACString& aRoot) override { + return aRoot.EqualsLiteral("android"); + } + + private: + [[nodiscard]] nsresult Init(); + static mozilla::StaticRefPtr<nsResProtocolHandler> sSingleton; + + nsCString mAppURI; + nsCString mGREURI; +#ifdef ANDROID + // Used for resource://android URIs + nsCString mApkURI; + nsresult GetApkURI(nsACString& aResult); +#endif +}; + +#endif /* nsResProtocolHandler_h___ */ |