/* -*- 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 "BroadcastChannel.h" #include "BroadcastChannelChild.h" #include "mozilla/dom/BroadcastChannelBinding.h" #include "mozilla/dom/Navigator.h" #include "mozilla/dom/StructuredCloneUtils.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/Preferences.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "nsIAppsService.h" #include "nsIDocument.h" #include "nsIScriptSecurityManager.h" #include "nsServiceManagerUtils.h" #include "nsISupportsPrimitives.h" #ifdef XP_WIN #undef PostMessage #endif namespace mozilla { using namespace ipc; namespace dom { using namespace workers; class BroadcastChannelMessage final { public: NS_INLINE_DECL_REFCOUNTING(BroadcastChannelMessage) JSAutoStructuredCloneBuffer mBuffer; StructuredCloneClosure mClosure; BroadcastChannelMessage() { } private: ~BroadcastChannelMessage() { } }; namespace { void GetOrigin(nsIPrincipal* aPrincipal, nsAString& aOrigin, ErrorResult& aRv) { MOZ_ASSERT(aPrincipal); bool unknownAppId; aRv = aPrincipal->GetUnknownAppId(&unknownAppId); if (NS_WARN_IF(aRv.Failed())) { return; } if (!unknownAppId) { uint32_t appId; aRv = aPrincipal->GetAppId(&appId); if (NS_WARN_IF(aRv.Failed())) { return; } if (appId != nsIScriptSecurityManager::NO_APP_ID) { // If we are in "app code", use manifest URL as unique origin since // multiple apps can share the same origin but not same broadcast // messages. nsresult rv; nsCOMPtr appsService = do_GetService("@mozilla.org/AppsService;1", &rv); if (NS_WARN_IF(NS_FAILED(rv))) { aRv.Throw(rv); return; } appsService->GetManifestURLByLocalId(appId, aOrigin); return; } } nsAutoString tmp; aRv = nsContentUtils::GetUTFOrigin(aPrincipal, tmp); if (NS_WARN_IF(aRv.Failed())) { return; } // 'null' means an unknown origin (it can be chrome code or it can be some // about: page). aOrigin = tmp; if (!aOrigin.EqualsASCII("null")) { return; } nsCOMPtr uri; aRv = aPrincipal->GetURI(getter_AddRefs(uri)); if (NS_WARN_IF(aRv.Failed())) { return; } if (NS_WARN_IF(!uri)) { aRv.Throw(NS_ERROR_FAILURE); return; } nsAutoCString spec; aRv = uri->GetSpec(spec); if (NS_WARN_IF(aRv.Failed())) { return; } aOrigin = NS_ConvertUTF8toUTF16(spec); } nsIPrincipal* GetPrincipalFromWorkerPrivate(WorkerPrivate* aWorkerPrivate) { nsIPrincipal* principal = aWorkerPrivate->GetPrincipal(); if (principal) { return principal; } // Walk up to our containing page WorkerPrivate* wp = aWorkerPrivate; while (wp->GetParent()) { wp = wp->GetParent(); } return wp->GetPrincipal(); } class InitializeRunnable final : public WorkerMainThreadRunnable { public: InitializeRunnable(WorkerPrivate* aWorkerPrivate, nsAString& aOrigin, PrincipalInfo& aPrincipalInfo, bool& aPrivateBrowsing, ErrorResult& aRv) : WorkerMainThreadRunnable(aWorkerPrivate) , mWorkerPrivate(GetCurrentThreadWorkerPrivate()) , mOrigin(aOrigin) , mPrincipalInfo(aPrincipalInfo) , mPrivateBrowsing(aPrivateBrowsing) , mRv(aRv) { MOZ_ASSERT(mWorkerPrivate); } bool MainThreadRun() override { MOZ_ASSERT(NS_IsMainThread()); nsIPrincipal* principal = GetPrincipalFromWorkerPrivate(mWorkerPrivate); if (!principal) { mRv.Throw(NS_ERROR_FAILURE); return true; } bool isNullPrincipal; mRv = principal->GetIsNullPrincipal(&isNullPrincipal); if (NS_WARN_IF(mRv.Failed())) { return true; } if (NS_WARN_IF(isNullPrincipal)) { mRv.Throw(NS_ERROR_FAILURE); return true; } mRv = PrincipalToPrincipalInfo(principal, &mPrincipalInfo); if (NS_WARN_IF(mRv.Failed())) { return true; } GetOrigin(principal, mOrigin, mRv); if (NS_WARN_IF(mRv.Failed())) { return true; } // Walk up to our containing page WorkerPrivate* wp = mWorkerPrivate; while (wp->GetParent()) { wp = wp->GetParent(); } // Window doesn't exist for some kind of workers (eg: SharedWorkers) nsPIDOMWindow* window = wp->GetWindow(); if (!window) { return true; } nsIDocument* doc = window->GetExtantDoc(); if (doc) { mPrivateBrowsing = nsContentUtils::IsInPrivateBrowsing(doc); // No bfcache when BroadcastChannel is used. doc->DisallowBFCaching(); } return true; } private: WorkerPrivate* mWorkerPrivate; nsAString& mOrigin; PrincipalInfo& mPrincipalInfo; bool& mPrivateBrowsing; ErrorResult& mRv; }; class BCPostMessageRunnable final : public nsICancelableRunnable { public: NS_DECL_ISUPPORTS BCPostMessageRunnable(BroadcastChannelChild* aActor, BroadcastChannelMessage* aData) : mActor(aActor) , mData(aData) { MOZ_ASSERT(mActor); } NS_IMETHODIMP Run() override { MOZ_ASSERT(mActor); if (mActor->IsActorDestroyed()) { return NS_OK; } ClonedMessageData message; SerializedStructuredCloneBuffer& buffer = message.data(); buffer.data = mData->mBuffer.data(); buffer.dataLength = mData->mBuffer.nbytes(); PBackgroundChild* backgroundManager = mActor->Manager(); MOZ_ASSERT(backgroundManager); const nsTArray>& blobs = mData->mClosure.mBlobs; if (!blobs.IsEmpty()) { message.blobsChild().SetCapacity(blobs.Length()); for (uint32_t i = 0, len = blobs.Length(); i < len; ++i) { PBlobChild* blobChild = BackgroundChild::GetOrCreateActorForBlob(backgroundManager, blobs[i]); MOZ_ASSERT(blobChild); message.blobsChild().AppendElement(blobChild); } } mActor->SendPostMessage(message); return NS_OK; } NS_IMETHODIMP Cancel() override { mActor = nullptr; return NS_OK; } private: ~BCPostMessageRunnable() {} nsRefPtr mActor; nsRefPtr mData; }; NS_IMPL_ISUPPORTS(BCPostMessageRunnable, nsICancelableRunnable, nsIRunnable) class CloseRunnable final : public nsICancelableRunnable { public: NS_DECL_ISUPPORTS explicit CloseRunnable(BroadcastChannel* aBC) : mBC(aBC) { MOZ_ASSERT(mBC); } NS_IMETHODIMP Run() override { mBC->Shutdown(); return NS_OK; } NS_IMETHODIMP Cancel() override { mBC = nullptr; return NS_OK; } private: ~CloseRunnable() {} nsRefPtr mBC; }; NS_IMPL_ISUPPORTS(CloseRunnable, nsICancelableRunnable, nsIRunnable) class TeardownRunnable final : public nsICancelableRunnable { public: NS_DECL_ISUPPORTS explicit TeardownRunnable(BroadcastChannelChild* aActor) : mActor(aActor) { MOZ_ASSERT(mActor); } NS_IMETHODIMP Run() override { MOZ_ASSERT(mActor); if (!mActor->IsActorDestroyed()) { mActor->SendClose(); } return NS_OK; } NS_IMETHODIMP Cancel() override { mActor = nullptr; return NS_OK; } private: ~TeardownRunnable() {} nsRefPtr mActor; }; NS_IMPL_ISUPPORTS(TeardownRunnable, nsICancelableRunnable, nsIRunnable) class BroadcastChannelFeature final : public workers::WorkerFeature { BroadcastChannel* mChannel; public: explicit BroadcastChannelFeature(BroadcastChannel* aChannel) : mChannel(aChannel) { MOZ_COUNT_CTOR(BroadcastChannelFeature); } virtual bool Notify(JSContext* aCx, workers::Status aStatus) override { if (aStatus >= Closing) { mChannel->Shutdown(); } return true; } private: ~BroadcastChannelFeature() { MOZ_COUNT_DTOR(BroadcastChannelFeature); } }; class PrefEnabledRunnable final : public WorkerMainThreadRunnable { public: explicit PrefEnabledRunnable(WorkerPrivate* aWorkerPrivate) : WorkerMainThreadRunnable(aWorkerPrivate) , mEnabled(false) { } bool MainThreadRun() override { AssertIsOnMainThread(); mEnabled = Preferences::GetBool("dom.broadcastChannel.enabled", false); return true; } bool IsEnabled() const { return mEnabled; } private: bool mEnabled; }; } // anonymous namespace /* static */ bool BroadcastChannel::IsEnabled(JSContext* aCx, JSObject* aGlobal) { if (NS_IsMainThread()) { return Preferences::GetBool("dom.broadcastChannel.enabled", false); } WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); nsRefPtr runnable = new PrefEnabledRunnable(workerPrivate); runnable->Dispatch(workerPrivate->GetJSContext()); return runnable->IsEnabled(); } BroadcastChannel::BroadcastChannel(nsPIDOMWindow* aWindow, const PrincipalInfo& aPrincipalInfo, const nsAString& aOrigin, const nsAString& aChannel, bool aPrivateBrowsing) : DOMEventTargetHelper(aWindow) , mWorkerFeature(nullptr) , mPrincipalInfo(new PrincipalInfo(aPrincipalInfo)) , mOrigin(aOrigin) , mChannel(aChannel) , mPrivateBrowsing(aPrivateBrowsing) , mIsKeptAlive(false) , mInnerID(0) , mState(StateActive) { // Window can be null in workers } BroadcastChannel::~BroadcastChannel() { Shutdown(); MOZ_ASSERT(!mWorkerFeature); } JSObject* BroadcastChannel::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return BroadcastChannelBinding::Wrap(aCx, this, aGivenProto); } /* static */ already_AddRefed BroadcastChannel::Constructor(const GlobalObject& aGlobal, const nsAString& aChannel, ErrorResult& aRv) { nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); // Window is null in workers. nsAutoString origin; PrincipalInfo principalInfo; bool privateBrowsing = false; WorkerPrivate* workerPrivate = nullptr; if (NS_IsMainThread()) { nsCOMPtr incumbent = mozilla::dom::GetIncumbentGlobal(); if (!incumbent) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } nsIPrincipal* principal = incumbent->PrincipalOrNull(); if (!principal) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } bool isNullPrincipal; aRv = principal->GetIsNullPrincipal(&isNullPrincipal); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } if (NS_WARN_IF(isNullPrincipal)) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } GetOrigin(principal, origin, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } aRv = PrincipalToPrincipalInfo(principal, &principalInfo); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } nsIDocument* doc = window->GetExtantDoc(); if (doc) { privateBrowsing = nsContentUtils::IsInPrivateBrowsing(doc); // No bfcache when BroadcastChannel is used. doc->DisallowBFCaching(); } } else { JSContext* cx = aGlobal.Context(); workerPrivate = GetWorkerPrivateFromContext(cx); MOZ_ASSERT(workerPrivate); nsRefPtr runnable = new InitializeRunnable(workerPrivate, origin, principalInfo, privateBrowsing, aRv); runnable->Dispatch(cx); } if (aRv.Failed()) { return nullptr; } nsRefPtr bc = new BroadcastChannel(window, principalInfo, origin, aChannel, privateBrowsing); // Register this component to PBackground. PBackgroundChild* actor = BackgroundChild::GetForCurrentThread(); if (actor) { bc->ActorCreated(actor); } else { BackgroundChild::GetOrCreateForCurrentThread(bc); } if (!workerPrivate) { MOZ_ASSERT(window); MOZ_ASSERT(window->IsInnerWindow()); bc->mInnerID = window->WindowID(); // Register as observer for inner-window-destroyed. nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(bc, "inner-window-destroyed", false); } } else { bc->mWorkerFeature = new BroadcastChannelFeature(bc); JSContext* cx = workerPrivate->GetJSContext(); if (NS_WARN_IF(!workerPrivate->AddFeature(cx, bc->mWorkerFeature))) { NS_WARNING("Failed to register the BroadcastChannel worker feature."); bc->mWorkerFeature = nullptr; aRv.Throw(NS_ERROR_FAILURE); return nullptr; } } return bc.forget(); } void BroadcastChannel::PostMessage(JSContext* aCx, JS::Handle aMessage, ErrorResult& aRv) { if (mState != StateActive) { aRv.Throw(NS_ERROR_FAILURE); return; } PostMessageInternal(aCx, aMessage, aRv); } void BroadcastChannel::PostMessageInternal(JSContext* aCx, JS::Handle aMessage, ErrorResult& aRv) { nsRefPtr data = new BroadcastChannelMessage(); if (!WriteStructuredClone(aCx, aMessage, data->mBuffer, data->mClosure)) { aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); return; } const nsTArray>& blobs = data->mClosure.mBlobs; for (uint32_t i = 0, len = blobs.Length(); i < len; ++i) { if (!blobs[i]->Impl()->MayBeClonedToOtherThreads()) { aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); return; } } PostMessageData(data); } void BroadcastChannel::PostMessageData(BroadcastChannelMessage* aData) { if (mActor) { nsRefPtr runnable = new BCPostMessageRunnable(mActor, aData); if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) { NS_WARNING("Failed to dispatch to the current thread!"); } return; } mPendingMessages.AppendElement(aData); } void BroadcastChannel::Close() { if (mState != StateActive) { return; } if (mPendingMessages.IsEmpty()) { // We cannot call Shutdown() immediatelly because we could have some // postMessage runnable already dispatched. Instead, we change the state to // StateClosed and we shutdown the actor asynchrounsly. mState = StateClosed; nsRefPtr runnable = new CloseRunnable(this); if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) { NS_WARNING("Failed to dispatch to the current thread!"); } } else { MOZ_ASSERT(!mActor); mState = StateClosing; } } void BroadcastChannel::ActorFailed() { MOZ_CRASH("Failed to create a PBackgroundChild actor!"); } void BroadcastChannel::ActorCreated(PBackgroundChild* aActor) { MOZ_ASSERT(aActor); if (mState == StateClosed) { return; } PBroadcastChannelChild* actor = aActor->SendPBroadcastChannelConstructor(*mPrincipalInfo, mOrigin, mChannel, mPrivateBrowsing); mActor = static_cast(actor); MOZ_ASSERT(mActor); mActor->SetParent(this); // Flush pending messages. for (uint32_t i = 0; i < mPendingMessages.Length(); ++i) { PostMessageData(mPendingMessages[i]); } mPendingMessages.Clear(); if (mState == StateClosing) { Shutdown(); } } void BroadcastChannel::Shutdown() { mState = StateClosed; if (mWorkerFeature) { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); workerPrivate->RemoveFeature(workerPrivate->GetJSContext(), mWorkerFeature); mWorkerFeature = nullptr; } if (mActor) { mActor->SetParent(nullptr); nsRefPtr runnable = new TeardownRunnable(mActor); NS_DispatchToCurrentThread(runnable); mActor = nullptr; } // If shutdown() is called we have to release the reference if we still keep // it. if (mIsKeptAlive) { mIsKeptAlive = false; Release(); } } EventHandlerNonNull* BroadcastChannel::GetOnmessage() { if (NS_IsMainThread()) { return GetEventHandler(nsGkAtoms::onmessage, EmptyString()); } return GetEventHandler(nullptr, NS_LITERAL_STRING("message")); } void BroadcastChannel::SetOnmessage(EventHandlerNonNull* aCallback) { if (NS_IsMainThread()) { SetEventHandler(nsGkAtoms::onmessage, EmptyString(), aCallback); } else { SetEventHandler(nullptr, NS_LITERAL_STRING("message"), aCallback); } UpdateMustKeepAlive(); } void BroadcastChannel::AddEventListener(const nsAString& aType, EventListener* aCallback, bool aCapture, const dom::Nullable& aWantsUntrusted, ErrorResult& aRv) { DOMEventTargetHelper::AddEventListener(aType, aCallback, aCapture, aWantsUntrusted, aRv); if (aRv.Failed()) { return; } UpdateMustKeepAlive(); } void BroadcastChannel::RemoveEventListener(const nsAString& aType, EventListener* aCallback, bool aCapture, ErrorResult& aRv) { DOMEventTargetHelper::RemoveEventListener(aType, aCallback, aCapture, aRv); if (aRv.Failed()) { return; } UpdateMustKeepAlive(); } void BroadcastChannel::UpdateMustKeepAlive() { bool toKeepAlive = HasListenersFor(NS_LITERAL_STRING("message")); if (toKeepAlive == mIsKeptAlive) { return; } mIsKeptAlive = toKeepAlive; if (toKeepAlive) { AddRef(); } else { Release(); } } NS_IMETHODIMP BroadcastChannel::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!strcmp(aTopic, "inner-window-destroyed")); // If the window is destroyed we have to release the reference that we are // keeping. if (!mIsKeptAlive) { return NS_OK; } nsCOMPtr wrapper = do_QueryInterface(aSubject); NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); uint64_t innerID; nsresult rv = wrapper->GetData(&innerID); NS_ENSURE_SUCCESS(rv, rv); if (innerID == mInnerID) { nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, "inner-window-destroyed"); } Shutdown(); } return NS_OK; } NS_IMPL_CYCLE_COLLECTION_CLASS(BroadcastChannel) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(BroadcastChannel, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(BroadcastChannel, DOMEventTargetHelper) tmp->Shutdown(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(BroadcastChannel) NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) NS_IMPL_ADDREF_INHERITED(BroadcastChannel, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(BroadcastChannel, DOMEventTargetHelper) } // dom namespace } // mozilla namespace