/* -*- 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 "ClientSource.h" #include "ClientManager.h" #include "ClientManagerChild.h" #include "ClientSourceChild.h" #include "ClientState.h" #include "ClientValidation.h" #include "mozilla/dom/ClientIPCTypes.h" #include "mozilla/dom/ipc/StructuredCloneData.h" #include "mozilla/dom/MessageEvent.h" #include "mozilla/dom/MessageEventBinding.h" #include "mozilla/dom/Navigator.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WorkerScope.h" #include "mozilla/dom/ServiceWorkerContainer.h" #include "mozilla/dom/workers/ServiceWorkerManager.h" #include "mozilla/dom/workers/bindings/ServiceWorker.h" #include "nsContentUtils.h" #include "nsIDocShell.h" #include "nsPIDOMWindow.h" namespace mozilla { namespace dom { using mozilla::dom::ipc::StructuredCloneData; using mozilla::dom::workers::ServiceWorkerInfo; using mozilla::dom::workers::ServiceWorkerManager; using mozilla::dom::workers::ServiceWorkerRegistrationInfo; using mozilla::dom::workers::WorkerPrivate; using mozilla::ipc::PrincipalInfo; using mozilla::ipc::PrincipalInfoToPrincipal; void ClientSource::Shutdown() { NS_ASSERT_OWNINGTHREAD(ClientSource); if (IsShutdown()) { return; } ShutdownThing(); mManager = nullptr; } void ClientSource::ExecutionReady(const ClientSourceExecutionReadyArgs& aArgs) { // Fast fail if we don't understand this particular principal/URL combination. // This can happen since we use MozURL for validation which does not handle // some of the more obscure internal principal/url combinations. Normal // content pages will pass this check. if (NS_WARN_IF(!ClientIsValidCreationURL(mClientInfo.PrincipalInfo(), aArgs.url()))) { Shutdown(); return; } mClientInfo.SetURL(aArgs.url()); mClientInfo.SetFrameType(aArgs.frameType()); MaybeExecute([aArgs](PClientSourceChild* aActor) { aActor->SendExecutionReady(aArgs); }); } nsresult ClientSource::SnapshotWindowState(ClientState* aStateOut) { MOZ_ASSERT(NS_IsMainThread()); nsPIDOMWindowInner* window = GetInnerWindow(); if (!window || !window->IsCurrentInnerWindow() || !window->HasActiveDocument()) { *aStateOut = ClientState(ClientWindowState(VisibilityState::Hidden, TimeStamp(), nsContentUtils::StorageAccess::eDeny, false)); return NS_OK; } nsIDocument* doc = window->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return NS_ERROR_UNEXPECTED; } ErrorResult rv; bool focused = doc->HasFocus(rv); if (NS_WARN_IF(rv.Failed())) { rv.SuppressException(); return rv.StealNSResult(); } nsContentUtils::StorageAccess storage = nsContentUtils::StorageAllowedForDocument(doc); *aStateOut = ClientState(ClientWindowState(doc->VisibilityState(), doc->LastFocusTime(), storage, focused)); return NS_OK; } WorkerPrivate* ClientSource::GetWorkerPrivate() const { NS_ASSERT_OWNINGTHREAD(ClientSource); if (!mOwner.is()) { return nullptr; } return mOwner.as(); } nsIDocShell* ClientSource::GetDocShell() const { NS_ASSERT_OWNINGTHREAD(ClientSource); if (!mOwner.is>()) { return nullptr; } return mOwner.as>(); } void ClientSource::MaybeCreateInitialDocument() { nsIDocShell* docshell = GetDocShell(); if (docshell) { // Force the create of the initial document if it does not exist yet. Unused << docshell->GetDocument(); MOZ_DIAGNOSTIC_ASSERT(GetInnerWindow()); } } ClientSource::ClientSource(ClientManager* aManager, nsISerialEventTarget* aEventTarget, const ClientSourceConstructorArgs& aArgs) : mManager(aManager) , mEventTarget(aEventTarget) , mOwner(AsVariant(Nothing())) , mClientInfo(aArgs.id(), aArgs.type(), aArgs.principalInfo(), aArgs.creationTime()) { MOZ_ASSERT(mManager); MOZ_ASSERT(mEventTarget); } void ClientSource::Activate(PClientManagerChild* aActor) { NS_ASSERT_OWNINGTHREAD(ClientSource); MOZ_ASSERT(!GetActor()); if (IsShutdown()) { return; } // Fast fail if we don't understand this particular kind of PrincipalInfo. // This can happen since we use MozURL for validation which does not handle // some of the more obscure internal principal/url combinations. Normal // content pages will pass this check. if (NS_WARN_IF(!ClientIsValidPrincipalInfo(mClientInfo.PrincipalInfo()))) { Shutdown(); return; } ClientSourceConstructorArgs args(mClientInfo.Id(), mClientInfo.Type(), mClientInfo.PrincipalInfo(), mClientInfo.CreationTime()); PClientSourceChild* actor = aActor->SendPClientSourceConstructor(args); if (!actor) { Shutdown(); return; } ActivateThing(static_cast(actor)); } ClientSource::~ClientSource() { Shutdown(); } nsPIDOMWindowInner* ClientSource::GetInnerWindow() const { NS_ASSERT_OWNINGTHREAD(ClientSource); if (!mOwner.is>()) { return nullptr; } return mOwner.as>(); } void ClientSource::WorkerExecutionReady(WorkerPrivate* aWorkerPrivate) { MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); if (IsShutdown()) { return; } // A client without access to storage should never be controlled by // a service worker. Check this here in case we were controlled before // execution ready. We can't reliably determine what our storage policy // is before execution ready, unfortunately. if (mController.isSome()) { MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate->IsStorageAllowed()); } // Its safe to store the WorkerPrivate* here because the ClientSource // is explicitly destroyed by WorkerPrivate before exiting its run loop. MOZ_DIAGNOSTIC_ASSERT(mOwner.is()); mOwner = AsVariant(aWorkerPrivate); ClientSourceExecutionReadyArgs args( aWorkerPrivate->GetLocationInfo().mHref, FrameType::None); ExecutionReady(args); } nsresult ClientSource::WindowExecutionReady(nsPIDOMWindowInner* aInnerWindow) { MOZ_ASSERT(NS_IsMainThread()); MOZ_DIAGNOSTIC_ASSERT(aInnerWindow); MOZ_DIAGNOSTIC_ASSERT(aInnerWindow->IsCurrentInnerWindow()); MOZ_DIAGNOSTIC_ASSERT(aInnerWindow->HasActiveDocument()); if (IsShutdown()) { return NS_OK; } nsIDocument* doc = aInnerWindow->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return NS_ERROR_UNEXPECTED; } // A client without access to storage should never be controlled by // a service worker. Check this here in case we were controlled before // execution ready. We can't reliably determine what our storage policy // is before execution ready, unfortunately. if (mController.isSome()) { MOZ_DIAGNOSTIC_ASSERT(nsContentUtils::StorageAllowedForWindow(aInnerWindow) == nsContentUtils::StorageAccess::eAllow); } // Don't use nsAutoCString here since IPC requires a full nsCString anyway. nsCString spec; nsIURI* uri = doc->GetOriginalURI(); if (uri) { nsresult rv = uri->GetSpec(spec); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } nsPIDOMWindowOuter* outer = aInnerWindow->GetOuterWindow(); if (NS_WARN_IF(!outer)) { return NS_ERROR_UNEXPECTED; } FrameType frameType = FrameType::Top_level; if (!outer->IsTopLevelWindow()) { frameType = FrameType::Nested; } else if(outer->HadOriginalOpener()) { frameType = FrameType::Auxiliary; } // We should either be setting a window execution ready for the // first time or setting the same window execution ready again. // The secondary calls are due to initial about:blank replacement. MOZ_DIAGNOSTIC_ASSERT(mOwner.is() || mOwner.is>() || GetInnerWindow() == aInnerWindow); // This creates a cycle with the window. It is broken when // nsGlobalWindow::FreeInnerObjects() deletes the ClientSource. mOwner = AsVariant(RefPtr(aInnerWindow)); ClientSourceExecutionReadyArgs args(spec, frameType); ExecutionReady(args); return NS_OK; } nsresult ClientSource::DocShellExecutionReady(nsIDocShell* aDocShell) { MOZ_ASSERT(NS_IsMainThread()); MOZ_DIAGNOSTIC_ASSERT(aDocShell); if (IsShutdown()) { return NS_OK; } nsPIDOMWindowOuter* outer = aDocShell->GetWindow(); if (NS_WARN_IF(!outer)) { return NS_ERROR_UNEXPECTED; } // Note: We don't assert storage access for a controlled client. If // the about:blank actually gets used then WindowExecutionReady() will // get called which asserts storage access. // TODO: dedupe this with WindowExecutionReady FrameType frameType = FrameType::Top_level; if (!outer->IsTopLevelWindow()) { frameType = FrameType::Nested; } else if(outer->HadOriginalOpener()) { frameType = FrameType::Auxiliary; } MOZ_DIAGNOSTIC_ASSERT(mOwner.is()); // This creates a cycle with the docshell. It is broken when // nsDocShell::Destroy() deletes the ClientSource. mOwner = AsVariant(nsCOMPtr(aDocShell)); ClientSourceExecutionReadyArgs args(NS_LITERAL_CSTRING("about:blank"), frameType); ExecutionReady(args); return NS_OK; } void ClientSource::Freeze() { MaybeExecute([](PClientSourceChild* aActor) { aActor->SendFreeze(); }); } void ClientSource::Thaw() { MaybeExecute([](PClientSourceChild* aActor) { aActor->SendThaw(); }); } const ClientInfo& ClientSource::Info() const { return mClientInfo; } void ClientSource::WorkerSyncPing(WorkerPrivate* aWorkerPrivate) { NS_ASSERT_OWNINGTHREAD(ClientSource); MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate); if (IsShutdown()) { return; } MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate == mManager->GetWorkerPrivate()); aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_DIAGNOSTIC_ASSERT(GetActor()); GetActor()->SendWorkerSyncPing(); } void ClientSource::SetController(const ServiceWorkerDescriptor& aServiceWorker) { NS_ASSERT_OWNINGTHREAD(ClientSource); // A client in private browsing mode should never be controlled by // a service worker. The principal origin attributes should guarantee // this invariant. MOZ_DIAGNOSTIC_ASSERT(!mClientInfo.IsPrivateBrowsing()); // A client without access to storage should never be controlled a // a service worker. If we are already execution ready with a real // window or worker, then verify assert the storage policy is correct. if (GetInnerWindow()) { MOZ_DIAGNOSTIC_ASSERT(nsContentUtils::StorageAllowedForWindow(GetInnerWindow()) == nsContentUtils::StorageAccess::eAllow); } else if (GetWorkerPrivate()) { MOZ_DIAGNOSTIC_ASSERT(GetWorkerPrivate()->IsStorageAllowed()); } if (mController.isSome() && mController.ref() == aServiceWorker) { return; } mController.reset(); mController.emplace(aServiceWorker); RefPtr swc; nsPIDOMWindowInner* window = GetInnerWindow(); if (window) { RefPtr navigator = static_cast(window->GetNavigator()); if (navigator) { swc = navigator->ServiceWorker(); } } // TODO: Also self.navigator.serviceWorker on workers when its exposed there if (swc && nsContentUtils::IsSafeToRunScript()) { IgnoredErrorResult ignored; swc->ControllerChanged(ignored); } } RefPtr ClientSource::Control(const ClientControlledArgs& aArgs) { NS_ASSERT_OWNINGTHREAD(ClientSource); SetController(ServiceWorkerDescriptor(aArgs.serviceWorker())); RefPtr ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__); return ref.forget(); } const Maybe& ClientSource::GetController() const { return mController; } RefPtr ClientSource::Focus(const ClientFocusArgs& aArgs) { NS_ASSERT_OWNINGTHREAD(ClientSource); RefPtr ref; if (mClientInfo.Type() != ClientType::Window) { ref = ClientOpPromise::CreateAndReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, __func__); return ref.forget(); } nsPIDOMWindowOuter* outer = nullptr; nsPIDOMWindowInner* inner = GetInnerWindow(); if (inner) { outer = inner->GetOuterWindow(); } else { nsIDocShell* docshell = GetDocShell(); if (docshell) { outer = docshell->GetWindow(); } } if (!outer) { ref = ClientOpPromise::CreateAndReject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); return ref.forget(); } MOZ_ASSERT(NS_IsMainThread()); nsresult rv = nsContentUtils::DispatchFocusChromeEvent(outer); if (NS_FAILED(rv)) { ref = ClientOpPromise::CreateAndReject(rv, __func__); return ref.forget(); } ClientState state; rv = SnapshotState(&state); if (NS_FAILED(rv)) { ref = ClientOpPromise::CreateAndReject(rv, __func__); return ref.forget(); } ref = ClientOpPromise::CreateAndResolve(state.ToIPC(), __func__); return ref.forget(); } RefPtr ClientSource::PostMessage(const ClientPostMessageArgs& aArgs) { NS_ASSERT_OWNINGTHREAD(ClientSource); RefPtr ref; ServiceWorkerDescriptor source(aArgs.serviceWorker()); const PrincipalInfo& principalInfo = source.PrincipalInfo(); StructuredCloneData clonedData; clonedData.BorrowFromClonedMessageDataForBackgroundChild(aArgs.clonedData()); // Currently we only support firing these messages on window Clients. // Once we expose ServiceWorkerContainer and the ServiceWorker on Worker // threads then this will need to change. See bug 1113522. if (mClientInfo.Type() != ClientType::Window) { ref = ClientOpPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__); return ref.forget(); } MOZ_ASSERT(NS_IsMainThread()); RefPtr target; nsCOMPtr globalObject; // We don't need to force the creation of the about:blank document // here because there is no postMessage listener. If a listener // was registered then the document will already be created. nsPIDOMWindowInner* window = GetInnerWindow(); if (window) { globalObject = do_QueryInterface(window); RefPtr navigator = static_cast(window->GetNavigator()); if (navigator) { target = navigator->ServiceWorker(); } } if (NS_WARN_IF(!target)) { ref = ClientOpPromise::CreateAndReject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); return ref.forget(); } // If AutoJSAPI::Init() fails then either global is nullptr or not // in a usable state. AutoJSAPI jsapi; if (!jsapi.Init(globalObject)) { ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__); return ref.forget(); } JSContext* cx = jsapi.cx(); ErrorResult result; JS::Rooted messageData(cx); clonedData.Read(cx, &messageData, result); if (result.MaybeSetPendingException(cx)) { // We reported the error in the current window context. Resolve // promise instead of rejecting. ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__); return ref.forget(); } RootedDictionary init(cx); init.mData = messageData; if (!clonedData.TakeTransferredPortsAsSequence(init.mPorts)) { // Report the error in the current window context and resolve the // promise instead of rejecting. xpc::Throw(cx, NS_ERROR_OUT_OF_MEMORY); ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__); return ref.forget(); } nsresult rv = NS_OK; nsCOMPtr principal = PrincipalInfoToPrincipal(principalInfo, &rv); if (NS_FAILED(rv) || !principal) { ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); return ref.forget(); } nsAutoCString origin; rv = principal->GetOriginNoSuffix(origin); if (NS_SUCCEEDED(rv)) { CopyUTF8toUTF16(origin, init.mOrigin); } RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { // Shutting down. Just don't deliver this message. ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); return ref.forget(); } RefPtr reg = swm->GetRegistration(principal, source.Scope()); if (reg) { RefPtr serviceWorker = reg->GetByID(source.Id()); if (serviceWorker) { init.mSource.SetValue().SetAsServiceWorker() = serviceWorker->GetOrCreateInstance(GetInnerWindow()); } } RefPtr event = MessageEvent::Constructor(target, NS_LITERAL_STRING("message"), init); event->SetTrusted(true); bool preventDefaultCalled = false; rv = target->DispatchEvent(static_cast(event.get()), &preventDefaultCalled); if (NS_FAILED(rv)) { ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); return ref.forget(); } ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__); return ref.forget(); } RefPtr ClientSource::Claim(const ClientClaimArgs& aArgs) { RefPtr ref; ServiceWorkerDescriptor swd(aArgs.serviceWorker()); // Today the ServiceWorkerManager maintains its own list of // nsIDocument objects controlled by each service worker. We // need to try to update that data structure for now. If we // can't, however, then simply mark the Client as controlled. // In the future this will be enough for the SWM as well since // it will eventually hold ClientHandle objects instead of // nsIDocuments. nsPIDOMWindowInner* innerWindow = GetInnerWindow(); nsIDocument* doc = innerWindow ? innerWindow->GetExtantDoc() : nullptr; RefPtr swm = doc ? ServiceWorkerManager::GetInstance() : nullptr; if (!swm || !doc) { SetController(swd); ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__); return ref.forget(); } RefPtr outerPromise = new ClientOpPromise::Private(__func__); RefPtr p = swm->MaybeClaimClient(doc, swd); p->Then(mEventTarget, __func__, [outerPromise] (bool aResult) { outerPromise->Resolve(NS_OK, __func__); }, [outerPromise] (nsresult aResult) { outerPromise->Reject(aResult, __func__); }); ref = outerPromise; return ref.forget(); } RefPtr ClientSource::GetInfoAndState(const ClientGetInfoAndStateArgs& aArgs) { RefPtr ref; ClientState state; nsresult rv = SnapshotState(&state); if (NS_FAILED(rv)) { ref = ClientOpPromise::CreateAndReject(rv, __func__); return ref.forget(); } ref = ClientOpPromise::CreateAndResolve(ClientInfoAndState(mClientInfo.ToIPC(), state.ToIPC()), __func__); return ref.forget(); } nsresult ClientSource::SnapshotState(ClientState* aStateOut) { NS_ASSERT_OWNINGTHREAD(ClientSource); MOZ_DIAGNOSTIC_ASSERT(aStateOut); if (mClientInfo.Type() == ClientType::Window) { MaybeCreateInitialDocument(); nsresult rv = SnapshotWindowState(aStateOut); if (NS_FAILED(rv)) { return rv; } return NS_OK; } WorkerPrivate* workerPrivate = GetWorkerPrivate(); if (!workerPrivate) { return NS_ERROR_DOM_INVALID_STATE_ERR; } // Workers only keep a boolean for storage access at the moment. // Map this back to eAllow or eDeny for now. nsContentUtils::StorageAccess storage = workerPrivate->IsStorageAllowed() ? nsContentUtils::StorageAccess::eAllow : nsContentUtils::StorageAccess::eDeny; *aStateOut = ClientState(ClientWorkerState(storage)); return NS_OK; } nsISerialEventTarget* ClientSource::EventTarget() const { return mEventTarget; } void ClientSource::Traverse(nsCycleCollectionTraversalCallback& aCallback, const char* aName, uint32_t aFlags) { if (mOwner.is>()) { ImplCycleCollectionTraverse(aCallback, mOwner.as>(), aName, aFlags); } else if (mOwner.is>()) { ImplCycleCollectionTraverse(aCallback, mOwner.as>(), aName, aFlags); } } void ClientSource::NoteCalledRegisterForServiceWorkerScope(const nsACString& aScope) { if (mRegisteringScopeList.Contains(aScope)) { return; } mRegisteringScopeList.AppendElement(aScope); } bool ClientSource::CalledRegisterForServiceWorkerScope(const nsACString& aScope) { return mRegisteringScopeList.Contains(aScope); } } // namespace dom } // namespace mozilla