diff options
Diffstat (limited to '')
-rw-r--r-- | dom/clients/manager/ClientChannelHelper.cpp | 423 |
1 files changed, 423 insertions, 0 deletions
diff --git a/dom/clients/manager/ClientChannelHelper.cpp b/dom/clients/manager/ClientChannelHelper.cpp new file mode 100644 index 0000000000..f36cbb5319 --- /dev/null +++ b/dom/clients/manager/ClientChannelHelper.cpp @@ -0,0 +1,423 @@ +/* -*- 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 "ClientChannelHelper.h" + +#include "ClientManager.h" +#include "ClientSource.h" +#include "MainThreadUtils.h" +#include "mozilla/dom/ClientsBinding.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "nsContentUtils.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIChannel.h" +#include "nsIChannelEventSink.h" +#include "nsIHttpChannelInternal.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" + +namespace mozilla::dom { + +using mozilla::ipc::PrincipalInfoToPrincipal; + +namespace { + +// In the default mode, ClientChannelHelper runs in the content process and +// handles all redirects. When we use DocumentChannel, redirects aren't exposed +// to the content process, so we run an instance of this in both processes, one +// to handle redirects in the parent and one to handle the final channel +// replacement (DocumentChannelChild 'redirects' to the final channel) in the +// child. + +class ClientChannelHelper : public nsIInterfaceRequestor, + public nsIChannelEventSink { + protected: + nsCOMPtr<nsIInterfaceRequestor> mOuter; + nsCOMPtr<nsISerialEventTarget> mEventTarget; + + virtual ~ClientChannelHelper() = default; + + NS_IMETHOD + GetInterface(const nsIID& aIID, void** aResultOut) override { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + *aResultOut = static_cast<nsIChannelEventSink*>(this); + NS_ADDREF_THIS(); + return NS_OK; + } + + if (mOuter) { + return mOuter->GetInterface(aIID, aResultOut); + } + + return NS_ERROR_NO_INTERFACE; + } + + virtual void CreateClient(nsILoadInfo* aLoadInfo, nsIPrincipal* aPrincipal) { + CreateClientForPrincipal(aLoadInfo, aPrincipal, mEventTarget); + } + + NS_IMETHOD + AsyncOnChannelRedirect(nsIChannel* aOldChannel, nsIChannel* aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCallback) override { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = nsContentUtils::CheckSameOrigin(aOldChannel, aNewChannel); + if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_DOM_BAD_URI)) { + return rv; + } + + nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo(); + nsCOMPtr<nsILoadInfo> newLoadInfo = aNewChannel->LoadInfo(); + + UniquePtr<ClientSource> reservedClient = + oldLoadInfo->TakeReservedClientSource(); + + // If its a same-origin redirect we just move our reserved client to the + // new channel. + if (NS_SUCCEEDED(rv)) { + if (reservedClient) { + newLoadInfo->GiveReservedClientSource(std::move(reservedClient)); + } + + // It seems sometimes necko passes two channels with the same LoadInfo. + // We only need to move the reserved/initial ClientInfo over if we + // actually have a different LoadInfo. + else if (oldLoadInfo != newLoadInfo) { + const Maybe<ClientInfo>& reservedClientInfo = + oldLoadInfo->GetReservedClientInfo(); + + const Maybe<ClientInfo>& initialClientInfo = + oldLoadInfo->GetInitialClientInfo(); + + MOZ_DIAGNOSTIC_ASSERT(reservedClientInfo.isNothing() || + initialClientInfo.isNothing()); + + if (reservedClientInfo.isSome()) { + // Create a new client for the case the controller is cleared for the + // new loadInfo. ServiceWorkerManager::DispatchFetchEvent() called + // ServiceWorkerManager::StartControllingClient() making the old + // client to be controlled eventually. However, the controller setting + // propagation to the child process could happen later than + // nsGlobalWindowInner::EnsureClientSource(), such that + // nsGlobalWindowInner will be controlled as unexpected. + if (oldLoadInfo->GetController().isSome() && + newLoadInfo->GetController().isNothing()) { + nsCOMPtr<nsIPrincipal> foreignPartitionedPrincipal; + rv = StoragePrincipalHelper::GetPrincipal( + aNewChannel, + StaticPrefs::privacy_partition_serviceWorkers() + ? StoragePrincipalHelper::eForeignPartitionedPrincipal + : StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(foreignPartitionedPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + reservedClient.reset(); + CreateClient(newLoadInfo, foreignPartitionedPrincipal); + } else { + newLoadInfo->SetReservedClientInfo(reservedClientInfo.ref()); + } + } + + if (initialClientInfo.isSome()) { + newLoadInfo->SetInitialClientInfo(initialClientInfo.ref()); + } + } + } + + // If it's a cross-origin redirect then we discard the old reserved client + // and create a new one. + else { + nsCOMPtr<nsIPrincipal> foreignPartitionedPrincipal; + rv = StoragePrincipalHelper::GetPrincipal( + aNewChannel, + StaticPrefs::privacy_partition_serviceWorkers() + ? StoragePrincipalHelper::eForeignPartitionedPrincipal + : StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(foreignPartitionedPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + reservedClient.reset(); + CreateClient(newLoadInfo, foreignPartitionedPrincipal); + } + + uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL; + nsCOMPtr<nsIHttpChannelInternal> http = do_QueryInterface(aOldChannel); + if (http) { + MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode)); + } + + // Normally we keep the controller across channel redirects, but we must + // clear it when a document load redirects. Only do this for real + // redirects, however. + // + // This is effectively described in step 4.2 of: + // + // https://fetch.spec.whatwg.org/#http-fetch + // + // The spec sets the service-workers mode to none when the request is + // configured to *not* follow redirects. This prevents any further + // service workers from intercepting. The first service worker that + // had a shot at the FetchEvent remains the controller in this case. + if (!(aFlags & nsIChannelEventSink::REDIRECT_INTERNAL) && + redirectMode != nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) { + newLoadInfo->ClearController(); + } + + nsCOMPtr<nsIChannelEventSink> outerSink = do_GetInterface(mOuter); + if (outerSink) { + return outerSink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, + aCallback); + } + + aCallback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; + } + + public: + ClientChannelHelper(nsIInterfaceRequestor* aOuter, + nsISerialEventTarget* aEventTarget) + : mOuter(aOuter), mEventTarget(aEventTarget) {} + + NS_DECL_ISUPPORTS + + virtual void CreateClientForPrincipal(nsILoadInfo* aLoadInfo, + nsIPrincipal* aPrincipal, + nsISerialEventTarget* aEventTarget) { + // Create the new ClientSource. This should only happen for window + // Clients since support cross-origin redirects are blocked by the + // same-origin security policy. + UniquePtr<ClientSource> reservedClient = ClientManager::CreateSource( + ClientType::Window, aEventTarget, aPrincipal); + MOZ_DIAGNOSTIC_ASSERT(reservedClient); + + aLoadInfo->GiveReservedClientSource(std::move(reservedClient)); + } +}; + +NS_IMPL_ISUPPORTS(ClientChannelHelper, nsIInterfaceRequestor, + nsIChannelEventSink); + +class ClientChannelHelperParent final : public ClientChannelHelper { + ~ClientChannelHelperParent() { + // This requires that if a load completes, the associated ClientSource is + // created and registers itself before this ClientChannelHelperParent is + // destroyed. Otherwise, we may incorrectly "forget" a future ClientSource + // which will actually be created. + SetFutureSourceInfo(Nothing()); + } + + void CreateClient(nsILoadInfo* aLoadInfo, nsIPrincipal* aPrincipal) override { + CreateClientForPrincipal(aLoadInfo, aPrincipal, mEventTarget); + } + + void SetFutureSourceInfo(Maybe<ClientInfo>&& aClientInfo) { + if (mRecentFutureSourceInfo) { + // No-op if the corresponding ClientSource has alrady been created, but + // it's not known if that's the case here. + ClientManager::ForgetFutureSource(*mRecentFutureSourceInfo); + } + + if (aClientInfo) { + Unused << NS_WARN_IF(!ClientManager::ExpectFutureSource(*aClientInfo)); + } + + mRecentFutureSourceInfo = std::move(aClientInfo); + } + + // Keep track of the most recent ClientInfo created which isn't backed by a + // ClientSource, which is used to notify ClientManagerService that the + // ClientSource won't ever actually be constructed. + Maybe<ClientInfo> mRecentFutureSourceInfo; + + public: + void CreateClientForPrincipal(nsILoadInfo* aLoadInfo, + nsIPrincipal* aPrincipal, + nsISerialEventTarget* aEventTarget) override { + // If we're managing redirects in the parent, then we don't want + // to create a new ClientSource (since those need to live with + // the global), so just allocate a new ClientInfo/id and we can + // create a ClientSource when the final channel propagates back + // to the child. + Maybe<ClientInfo> reservedInfo = + ClientManager::CreateInfo(ClientType::Window, aPrincipal); + if (reservedInfo) { + aLoadInfo->SetReservedClientInfo(*reservedInfo); + SetFutureSourceInfo(std::move(reservedInfo)); + } + } + ClientChannelHelperParent(nsIInterfaceRequestor* aOuter, + nsISerialEventTarget* aEventTarget) + : ClientChannelHelper(aOuter, nullptr) {} +}; + +class ClientChannelHelperChild final : public ClientChannelHelper { + ~ClientChannelHelperChild() = default; + + NS_IMETHOD + AsyncOnChannelRedirect(nsIChannel* aOldChannel, nsIChannel* aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCallback) override { + MOZ_ASSERT(NS_IsMainThread()); + + // All ClientInfo allocation should have been handled in the parent process + // by ClientChannelHelperParent, so the only remaining thing to do is to + // allocate a ClientSource around the ClientInfo on the channel. + CreateReservedSourceIfNeeded(aNewChannel, mEventTarget); + + nsCOMPtr<nsIChannelEventSink> outerSink = do_GetInterface(mOuter); + if (outerSink) { + return outerSink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, + aCallback); + } + + aCallback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; + } + + public: + ClientChannelHelperChild(nsIInterfaceRequestor* aOuter, + nsISerialEventTarget* aEventTarget) + : ClientChannelHelper(aOuter, aEventTarget) {} +}; + +} // anonymous namespace + +template <typename T> +nsresult AddClientChannelHelperInternal(nsIChannel* aChannel, + Maybe<ClientInfo>&& aReservedClientInfo, + Maybe<ClientInfo>&& aInitialClientInfo, + nsISerialEventTarget* aEventTarget) { + MOZ_ASSERT(NS_IsMainThread()); + + Maybe<ClientInfo> initialClientInfo(std::move(aInitialClientInfo)); + Maybe<ClientInfo> reservedClientInfo(std::move(aReservedClientInfo)); + MOZ_DIAGNOSTIC_ASSERT(reservedClientInfo.isNothing() || + initialClientInfo.isNothing()); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + nsCOMPtr<nsIPrincipal> channelForeignPartitionedPrincipal; + nsresult rv = StoragePrincipalHelper::GetPrincipal( + aChannel, + StaticPrefs::privacy_partition_serviceWorkers() + ? StoragePrincipalHelper::eForeignPartitionedPrincipal + : StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(channelForeignPartitionedPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + // Only allow the initial ClientInfo to be set if the current channel + // principal matches. + if (initialClientInfo.isSome()) { + auto initialPrincipalOrErr = + PrincipalInfoToPrincipal(initialClientInfo.ref().PrincipalInfo()); + + bool equals = false; + rv = initialPrincipalOrErr.isErr() + ? initialPrincipalOrErr.unwrapErr() + : initialPrincipalOrErr.unwrap()->Equals( + channelForeignPartitionedPrincipal, &equals); + if (NS_FAILED(rv) || !equals) { + initialClientInfo.reset(); + } + } + + // Only allow the reserved ClientInfo to be set if the current channel + // principal matches. + if (reservedClientInfo.isSome()) { + auto reservedPrincipalOrErr = + PrincipalInfoToPrincipal(reservedClientInfo.ref().PrincipalInfo()); + + bool equals = false; + rv = reservedPrincipalOrErr.isErr() + ? reservedPrincipalOrErr.unwrapErr() + : reservedPrincipalOrErr.unwrap()->Equals( + channelForeignPartitionedPrincipal, &equals); + if (NS_FAILED(rv) || !equals) { + reservedClientInfo.reset(); + } + } + + nsCOMPtr<nsIInterfaceRequestor> outerCallbacks; + rv = aChannel->GetNotificationCallbacks(getter_AddRefs(outerCallbacks)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<ClientChannelHelper> helper = new T(outerCallbacks, aEventTarget); + + if (initialClientInfo.isNothing() && reservedClientInfo.isNothing()) { + helper->CreateClientForPrincipal( + loadInfo, channelForeignPartitionedPrincipal, aEventTarget); + } + + // Only set the callbacks helper if we are able to reserve the client + // successfully. + rv = aChannel->SetNotificationCallbacks(helper); + NS_ENSURE_SUCCESS(rv, rv); + + if (initialClientInfo.isSome()) { + loadInfo->SetInitialClientInfo(initialClientInfo.ref()); + } + + if (reservedClientInfo.isSome()) { + loadInfo->SetReservedClientInfo(reservedClientInfo.ref()); + } + + return NS_OK; +} + +nsresult AddClientChannelHelper(nsIChannel* aChannel, + Maybe<ClientInfo>&& aReservedClientInfo, + Maybe<ClientInfo>&& aInitialClientInfo, + nsISerialEventTarget* aEventTarget) { + return AddClientChannelHelperInternal<ClientChannelHelper>( + aChannel, std::move(aReservedClientInfo), std::move(aInitialClientInfo), + aEventTarget); +} + +nsresult AddClientChannelHelperInParent( + nsIChannel* aChannel, Maybe<ClientInfo>&& aInitialClientInfo) { + Maybe<ClientInfo> emptyReservedInfo; + return AddClientChannelHelperInternal<ClientChannelHelperParent>( + aChannel, std::move(emptyReservedInfo), std::move(aInitialClientInfo), + nullptr); +} + +nsresult AddClientChannelHelperInChild(nsIChannel* aChannel, + nsISerialEventTarget* aEventTarget) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIInterfaceRequestor> outerCallbacks; + nsresult rv = + aChannel->GetNotificationCallbacks(getter_AddRefs(outerCallbacks)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<ClientChannelHelper> helper = + new ClientChannelHelperChild(outerCallbacks, aEventTarget); + + // Only set the callbacks helper if we are able to reserve the client + // successfully. + rv = aChannel->SetNotificationCallbacks(helper); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void CreateReservedSourceIfNeeded(nsIChannel* aChannel, + nsISerialEventTarget* aEventTarget) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + const Maybe<ClientInfo>& reservedClientInfo = + loadInfo->GetReservedClientInfo(); + + if (reservedClientInfo) { + UniquePtr<ClientSource> reservedClient = + ClientManager::CreateSourceFromInfo(*reservedClientInfo, aEventTarget); + loadInfo->GiveReservedClientSource(std::move(reservedClient)); + } +} + +} // namespace mozilla::dom |