diff options
Diffstat (limited to '')
-rw-r--r-- | netwerk/protocol/res/PageThumbProtocolHandler.cpp | 468 |
1 files changed, 468 insertions, 0 deletions
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 |