Add a synced ParentInitiatedNavigationEpoch field to browsing context, which only gets incremented when we start navigations in the parent process. When a child process initiates a navigation, it sends the current value of the field that it sees via DocumentChannelCreationArgs. In the parent process, we can compare the value of that field with the latest one for the same browsing context. If the latest value is higher than the one provided by the content process, it means that in the meantime parent process has started a navigation so the earlier navigation originating in the content process will be cancelled. Differential Revision: https://phabricator.services.mozilla.com/D126842
476 lines
16 KiB
C++
476 lines
16 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set sw=2 ts=8 et 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 "DocumentChannelChild.h"
|
|
|
|
#include "mozilla/dom/Document.h"
|
|
#include "mozilla/extensions/StreamFilterParent.h"
|
|
#include "mozilla/ipc/Endpoint.h"
|
|
#include "mozilla/net/HttpBaseChannel.h"
|
|
#include "mozilla/net/NeckoChild.h"
|
|
#include "mozilla/ScopeExit.h"
|
|
#include "mozilla/StaticPrefs_fission.h"
|
|
#include "nsHashPropertyBag.h"
|
|
#include "nsIHttpChannelInternal.h"
|
|
#include "nsIObjectLoadingContent.h"
|
|
#include "nsIXULRuntime.h"
|
|
#include "nsIWritablePropertyBag.h"
|
|
#include "nsFrameLoader.h"
|
|
#include "nsFrameLoaderOwner.h"
|
|
#include "nsQueryObject.h"
|
|
#include "nsDocShellLoadState.h"
|
|
|
|
using namespace mozilla::dom;
|
|
using namespace mozilla::ipc;
|
|
|
|
extern mozilla::LazyLogModule gDocumentChannelLog;
|
|
#define LOG(fmt) MOZ_LOG(gDocumentChannelLog, mozilla::LogLevel::Verbose, fmt)
|
|
|
|
namespace mozilla {
|
|
namespace net {
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// DocumentChannelChild::nsISupports
|
|
|
|
NS_INTERFACE_MAP_BEGIN(DocumentChannelChild)
|
|
NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectCallback)
|
|
NS_INTERFACE_MAP_END_INHERITING(DocumentChannel)
|
|
|
|
NS_IMPL_ADDREF_INHERITED(DocumentChannelChild, DocumentChannel)
|
|
NS_IMPL_RELEASE_INHERITED(DocumentChannelChild, DocumentChannel)
|
|
|
|
DocumentChannelChild::DocumentChannelChild(nsDocShellLoadState* aLoadState,
|
|
net::LoadInfo* aLoadInfo,
|
|
nsLoadFlags aLoadFlags,
|
|
uint32_t aCacheKey,
|
|
bool aUriModified, bool aIsXFOError)
|
|
: DocumentChannel(aLoadState, aLoadInfo, aLoadFlags, aCacheKey,
|
|
aUriModified, aIsXFOError) {
|
|
mLoadingContext = nullptr;
|
|
LOG(("DocumentChannelChild ctor [this=%p, uri=%s]", this,
|
|
aLoadState->URI()->GetSpecOrDefault().get()));
|
|
}
|
|
|
|
DocumentChannelChild::~DocumentChannelChild() {
|
|
LOG(("DocumentChannelChild dtor [this=%p]", this));
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
DocumentChannelChild::AsyncOpen(nsIStreamListener* aListener) {
|
|
nsresult rv = NS_OK;
|
|
|
|
nsCOMPtr<nsIStreamListener> listener = aListener;
|
|
|
|
NS_ENSURE_TRUE(gNeckoChild, NS_ERROR_FAILURE);
|
|
NS_ENSURE_ARG_POINTER(listener);
|
|
NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS);
|
|
NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED);
|
|
|
|
// Port checked in parent, but duplicate here so we can return with error
|
|
// immediately, as we've done since before e10s.
|
|
rv = NS_CheckPortSafety(mURI);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
bool isNotDownload = mLoadState->FileName().IsVoid();
|
|
|
|
// If not a download, add ourselves to the load group
|
|
if (isNotDownload && mLoadGroup) {
|
|
// During this call, we can re-enter back into the DocumentChannelChild to
|
|
// call SetNavigationTiming.
|
|
mLoadGroup->AddRequest(this, nullptr);
|
|
}
|
|
|
|
if (mCanceled) {
|
|
// We may have been canceled already, either by on-modify-request
|
|
// listeners or by load group observers; in that case, don't create IPDL
|
|
// connection. See nsHttpChannel::AsyncOpen().
|
|
return mStatus;
|
|
}
|
|
|
|
gHttpHandler->OnOpeningDocumentRequest(this);
|
|
|
|
RefPtr<nsDocShell> docShell = GetDocShell();
|
|
if (!docShell) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// `loadingContext` is the BC that is initiating the resource load.
|
|
// For normal subdocument loads, the BC is the one that the subdoc will load
|
|
// into. For <object>/<embed> it's the embedder doc's BC.
|
|
RefPtr<BrowsingContext> loadingContext = docShell->GetBrowsingContext();
|
|
if (!loadingContext || loadingContext->IsDiscarded()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
mLoadingContext = loadingContext;
|
|
|
|
DocumentChannelCreationArgs args;
|
|
|
|
args.loadState() = mLoadState->Serialize();
|
|
args.cacheKey() = mCacheKey;
|
|
args.channelId() = mChannelId;
|
|
args.asyncOpenTime() = TimeStamp::Now();
|
|
args.parentInitiatedNavigationEpoch() =
|
|
loadingContext->GetParentInitiatedNavigationEpoch();
|
|
|
|
Maybe<IPCClientInfo> ipcClientInfo;
|
|
if (mInitialClientInfo.isSome()) {
|
|
ipcClientInfo.emplace(mInitialClientInfo.ref().ToIPC());
|
|
}
|
|
args.initialClientInfo() = ipcClientInfo;
|
|
|
|
if (mTiming) {
|
|
args.timing() = Some(mTiming);
|
|
}
|
|
|
|
switch (mLoadInfo->GetExternalContentPolicyType()) {
|
|
case ExtContentPolicy::TYPE_DOCUMENT:
|
|
case ExtContentPolicy::TYPE_SUBDOCUMENT: {
|
|
DocumentCreationArgs docArgs;
|
|
docArgs.uriModified() = mUriModified;
|
|
docArgs.isXFOError() = mIsXFOError;
|
|
|
|
args.elementCreationArgs() = docArgs;
|
|
break;
|
|
}
|
|
|
|
case ExtContentPolicy::TYPE_OBJECT: {
|
|
ObjectCreationArgs objectArgs;
|
|
objectArgs.embedderInnerWindowId() = InnerWindowIDForExtantDoc(docShell);
|
|
objectArgs.loadFlags() = mLoadFlags;
|
|
objectArgs.contentPolicyType() = mLoadInfo->InternalContentPolicyType();
|
|
objectArgs.isUrgentStart() = UserActivation::IsHandlingUserInput();
|
|
|
|
args.elementCreationArgs() = objectArgs;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
MOZ_ASSERT_UNREACHABLE("unsupported content policy type");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
switch (mLoadInfo->GetExternalContentPolicyType()) {
|
|
case ExtContentPolicy::TYPE_DOCUMENT:
|
|
case ExtContentPolicy::TYPE_SUBDOCUMENT:
|
|
MOZ_ALWAYS_SUCCEEDS(loadingContext->SetCurrentLoadIdentifier(
|
|
Some(mLoadState->GetLoadIdentifier())));
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
gNeckoChild->SendPDocumentChannelConstructor(this, loadingContext, args);
|
|
|
|
mIsPending = true;
|
|
mWasOpened = true;
|
|
mListener = listener;
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
IPCResult DocumentChannelChild::RecvFailedAsyncOpen(
|
|
const nsresult& aStatusCode) {
|
|
if (aStatusCode == NS_ERROR_RECURSIVE_DOCUMENT_LOAD) {
|
|
// This exists so that we are able to fire an error event
|
|
// for when there are too many recursive iframe or object loads.
|
|
// This is an incomplete solution, because right now we don't have a unified
|
|
// way of firing error events due to errors in document channel.
|
|
// This should be fixed in bug 1629201.
|
|
MOZ_DIAGNOSTIC_ASSERT(mLoadingContext);
|
|
if (RefPtr<Element> embedder = mLoadingContext->GetEmbedderElement()) {
|
|
if (RefPtr<nsFrameLoaderOwner> flo = do_QueryObject(embedder)) {
|
|
if (RefPtr<nsFrameLoader> fl = flo->GetFrameLoader()) {
|
|
fl->FireErrorEvent();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ShutdownListeners(aStatusCode);
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult DocumentChannelChild::RecvDisconnectChildListeners(
|
|
const nsresult& aStatus, const nsresult& aLoadGroupStatus,
|
|
bool aSwitchedProcess) {
|
|
// If this disconnect is not due to a process switch, perform the disconnect
|
|
// immediately.
|
|
if (!aSwitchedProcess) {
|
|
DisconnectChildListeners(aStatus, aLoadGroupStatus);
|
|
return IPC_OK();
|
|
}
|
|
|
|
// Otherwise, the disconnect will occur later using some other mechanism,
|
|
// depending on what's happening to the loading DocShell. If this is a
|
|
// toplevel navigation, and this BrowsingContext enters the BFCache, we will
|
|
// cancel this channel when the PageHide event is firing, whereas if it does
|
|
// not enter BFCache (e.g. due to being an object, subframe or non-bfcached
|
|
// toplevel navigation), we will cancel this channel when the DocShell is
|
|
// destroyed.
|
|
nsDocShell* shell = GetDocShell();
|
|
if (mLoadInfo->GetExternalContentPolicyType() ==
|
|
ExtContentPolicy::TYPE_DOCUMENT &&
|
|
shell) {
|
|
MOZ_ASSERT(shell->GetBrowsingContext()->IsTop());
|
|
if (mozilla::SessionHistoryInParent() &&
|
|
shell->GetBrowsingContext()->IsInBFCache()) {
|
|
DisconnectChildListeners(aStatus, aLoadGroupStatus);
|
|
} else {
|
|
// Tell the DocShell which channel to cancel if it enters the BFCache.
|
|
shell->SetChannelToDisconnectOnPageHide(mChannelId);
|
|
}
|
|
}
|
|
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult DocumentChannelChild::RecvRedirectToRealChannel(
|
|
RedirectToRealChannelArgs&& aArgs,
|
|
nsTArray<Endpoint<extensions::PStreamFilterParent>>&& aEndpoints,
|
|
RedirectToRealChannelResolver&& aResolve) {
|
|
LOG(("DocumentChannelChild RecvRedirectToRealChannel [this=%p, uri=%s]", this,
|
|
aArgs.uri()->GetSpecOrDefault().get()));
|
|
|
|
// The document that created the cspToInherit.
|
|
// This is used when deserializing LoadInfo from the parent
|
|
// process, since we can't serialize Documents directly.
|
|
// TODO: For a fission OOP iframe this will be unavailable,
|
|
// as will the loadingContext computed in LoadInfoArgsToLoadInfo.
|
|
// Figure out if we need these for cross-origin subdocs.
|
|
RefPtr<dom::Document> cspToInheritLoadingDocument;
|
|
nsCOMPtr<nsIContentSecurityPolicy> policy = mLoadState->Csp();
|
|
if (policy) {
|
|
nsWeakPtr ctx =
|
|
static_cast<nsCSPContext*>(policy.get())->GetLoadingContext();
|
|
cspToInheritLoadingDocument = do_QueryReferent(ctx);
|
|
}
|
|
nsCOMPtr<nsILoadInfo> loadInfo;
|
|
MOZ_ALWAYS_SUCCEEDS(LoadInfoArgsToLoadInfo(
|
|
aArgs.loadInfo(), cspToInheritLoadingDocument, getter_AddRefs(loadInfo)));
|
|
|
|
mRedirectResolver = std::move(aResolve);
|
|
|
|
nsCOMPtr<nsIChannel> newChannel;
|
|
MOZ_ASSERT((aArgs.loadStateInternalLoadFlags() &
|
|
nsDocShell::InternalLoad::INTERNAL_LOAD_FLAGS_IS_SRCDOC) ||
|
|
aArgs.srcdocData().IsVoid());
|
|
nsresult rv = nsDocShell::CreateRealChannelForDocument(
|
|
getter_AddRefs(newChannel), aArgs.uri(), loadInfo, nullptr,
|
|
aArgs.newLoadFlags(), aArgs.srcdocData(), aArgs.baseUri());
|
|
if (newChannel) {
|
|
newChannel->SetLoadGroup(mLoadGroup);
|
|
}
|
|
|
|
// This is used to report any errors back to the parent by calling
|
|
// CrossProcessRedirectFinished.
|
|
auto scopeExit = MakeScopeExit([&]() {
|
|
mRedirectResolver(rv);
|
|
mRedirectResolver = nullptr;
|
|
});
|
|
|
|
if (NS_FAILED(rv)) {
|
|
return IPC_OK();
|
|
}
|
|
|
|
if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(newChannel)) {
|
|
rv = httpChannel->SetChannelId(aArgs.channelId());
|
|
}
|
|
if (NS_FAILED(rv)) {
|
|
return IPC_OK();
|
|
}
|
|
|
|
rv = newChannel->SetOriginalURI(aArgs.originalURI());
|
|
if (NS_FAILED(rv)) {
|
|
return IPC_OK();
|
|
}
|
|
|
|
if (nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal =
|
|
do_QueryInterface(newChannel)) {
|
|
rv = httpChannelInternal->SetRedirectMode(aArgs.redirectMode());
|
|
}
|
|
if (NS_FAILED(rv)) {
|
|
return IPC_OK();
|
|
}
|
|
|
|
newChannel->SetNotificationCallbacks(mCallbacks);
|
|
|
|
if (aArgs.init()) {
|
|
HttpBaseChannel::ReplacementChannelConfig config(*aArgs.init());
|
|
HttpBaseChannel::ConfigureReplacementChannel(
|
|
newChannel, config,
|
|
HttpBaseChannel::ReplacementReason::DocumentChannel);
|
|
}
|
|
|
|
if (aArgs.contentDisposition()) {
|
|
newChannel->SetContentDisposition(*aArgs.contentDisposition());
|
|
}
|
|
|
|
if (aArgs.contentDispositionFilename()) {
|
|
newChannel->SetContentDispositionFilename(
|
|
*aArgs.contentDispositionFilename());
|
|
}
|
|
|
|
nsDocShell* docShell = GetDocShell();
|
|
if (docShell && aArgs.loadingSessionHistoryInfo().isSome()) {
|
|
docShell->SetLoadingSessionHistoryInfo(
|
|
aArgs.loadingSessionHistoryInfo().ref());
|
|
}
|
|
|
|
// transfer any properties. This appears to be entirely a content-side
|
|
// interface and isn't copied across to the parent. Copying the values
|
|
// for this from this into the new actor will work, since the parent
|
|
// won't have the right details anyway.
|
|
// TODO: What about the process switch equivalent
|
|
// (ContentChild::RecvCrossProcessRedirect)? In that case there is no local
|
|
// existing actor in the destination process... We really need all information
|
|
// to go up to the parent, and then come down to the new child actor.
|
|
if (nsCOMPtr<nsIWritablePropertyBag> bag = do_QueryInterface(newChannel)) {
|
|
nsHashPropertyBag::CopyFrom(bag, aArgs.properties());
|
|
}
|
|
|
|
// connect parent.
|
|
nsCOMPtr<nsIChildChannel> childChannel = do_QueryInterface(newChannel);
|
|
if (childChannel) {
|
|
rv = childChannel->ConnectParent(
|
|
aArgs.registrarId()); // creates parent channel
|
|
if (NS_FAILED(rv)) {
|
|
return IPC_OK();
|
|
}
|
|
}
|
|
mRedirectChannel = newChannel;
|
|
mStreamFilterEndpoints = std::move(aEndpoints);
|
|
|
|
rv = gHttpHandler->AsyncOnChannelRedirect(
|
|
this, newChannel, aArgs.redirectFlags(), GetMainThreadEventTarget());
|
|
|
|
if (NS_SUCCEEDED(rv)) {
|
|
scopeExit.release();
|
|
}
|
|
|
|
// scopeExit will call CrossProcessRedirectFinished(rv) here
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult DocumentChannelChild::RecvUpgradeObjectLoad(
|
|
UpgradeObjectLoadResolver&& aResolve) {
|
|
// We're doing a load for an <object> or <embed> element if we got here.
|
|
MOZ_ASSERT(mLoadFlags & nsIRequest::LOAD_HTML_OBJECT_DATA,
|
|
"Should have LOAD_HTML_OBJECT_DATA set");
|
|
MOZ_ASSERT(!(mLoadFlags & nsIChannel::LOAD_DOCUMENT_URI),
|
|
"Shouldn't be a LOAD_DOCUMENT_URI load yet");
|
|
MOZ_ASSERT(mLoadInfo->GetExternalContentPolicyType() ==
|
|
ExtContentPolicy::TYPE_OBJECT,
|
|
"Should have the TYPE_OBJECT content policy type");
|
|
|
|
// If our load has already failed, or been cancelled, abort this attempt to
|
|
// upgade the load.
|
|
if (NS_FAILED(mStatus)) {
|
|
aResolve(nullptr);
|
|
return IPC_OK();
|
|
}
|
|
|
|
nsCOMPtr<nsIObjectLoadingContent> loadingContent;
|
|
NS_QueryNotificationCallbacks(this, loadingContent);
|
|
if (!loadingContent) {
|
|
return IPC_FAIL(this, "Channel is not for ObjectLoadingContent!");
|
|
}
|
|
|
|
// We're upgrading to a document channel now. Add the LOAD_DOCUMENT_URI flag
|
|
// after-the-fact.
|
|
mLoadFlags |= nsIChannel::LOAD_DOCUMENT_URI;
|
|
|
|
RefPtr<BrowsingContext> browsingContext;
|
|
nsresult rv = loadingContent->UpgradeLoadToDocument(
|
|
this, getter_AddRefs(browsingContext));
|
|
if (NS_FAILED(rv) || !browsingContext) {
|
|
// Oops! Looks like something went wrong, so let's bail out.
|
|
mLoadFlags &= ~nsIChannel::LOAD_DOCUMENT_URI;
|
|
aResolve(nullptr);
|
|
return IPC_OK();
|
|
}
|
|
|
|
aResolve(browsingContext);
|
|
return IPC_OK();
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
DocumentChannelChild::OnRedirectVerifyCallback(nsresult aStatusCode) {
|
|
LOG(
|
|
("DocumentChannelChild OnRedirectVerifyCallback [this=%p, "
|
|
"aRv=0x%08" PRIx32 " ]",
|
|
this, static_cast<uint32_t>(aStatusCode)));
|
|
nsCOMPtr<nsIChannel> redirectChannel = std::move(mRedirectChannel);
|
|
RedirectToRealChannelResolver redirectResolver = std::move(mRedirectResolver);
|
|
|
|
// If we've already shut down, then just notify the parent that
|
|
// we're done.
|
|
if (NS_FAILED(mStatus)) {
|
|
redirectChannel->SetNotificationCallbacks(nullptr);
|
|
redirectResolver(aStatusCode);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult rv = aStatusCode;
|
|
if (NS_SUCCEEDED(rv)) {
|
|
if (nsCOMPtr<nsIChildChannel> childChannel =
|
|
do_QueryInterface(redirectChannel)) {
|
|
rv = childChannel->CompleteRedirectSetup(mListener);
|
|
} else {
|
|
rv = redirectChannel->AsyncOpen(mListener);
|
|
}
|
|
} else {
|
|
redirectChannel->SetNotificationCallbacks(nullptr);
|
|
}
|
|
|
|
for (auto& endpoint : mStreamFilterEndpoints) {
|
|
extensions::StreamFilterParent::Attach(redirectChannel,
|
|
std::move(endpoint));
|
|
}
|
|
|
|
redirectResolver(rv);
|
|
|
|
if (NS_FAILED(rv)) {
|
|
ShutdownListeners(rv);
|
|
return NS_OK;
|
|
}
|
|
|
|
if (mLoadGroup) {
|
|
mLoadGroup->RemoveRequest(this, nullptr, NS_BINDING_REDIRECTED);
|
|
}
|
|
mCallbacks = nullptr;
|
|
mListener = nullptr;
|
|
|
|
// This calls NeckoChild::DeallocPDocumentChannel(), which deletes |this| if
|
|
// IPDL holds the last reference. Don't rely on |this| existing after here!
|
|
if (CanSend()) {
|
|
Send__delete__(this);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
DocumentChannelChild::Cancel(nsresult aStatusCode) {
|
|
if (mCanceled) {
|
|
return NS_OK;
|
|
}
|
|
|
|
mCanceled = true;
|
|
if (CanSend()) {
|
|
SendCancel(aStatusCode);
|
|
}
|
|
|
|
ShutdownListeners(aStatusCode);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
} // namespace net
|
|
} // namespace mozilla
|
|
|
|
#undef LOG
|