/* 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 "ExtensionAPIRequestForwarder.h" #include "ExtensionEventListener.h" #include "js/Promise.h" #include "js/PropertyAndElement.h" // JS_GetElement #include "mozilla/dom/Client.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/dom/ClonedErrorHolder.h" #include "mozilla/dom/ClonedErrorHolderBinding.h" #include "mozilla/dom/ExtensionBrowserBinding.h" #include "mozilla/dom/FunctionBinding.h" #include "mozilla/dom/WorkerScope.h" #include "mozilla/dom/SerializedStackHolder.h" #include "mozilla/dom/ServiceWorkerInfo.h" #include "mozilla/dom/ServiceWorkerManager.h" #include "mozilla/dom/ServiceWorkerRegistrationInfo.h" #include "mozilla/dom/StructuredCloneTags.h" #include "mozilla/ExtensionPolicyService.h" #include "nsIGlobalObject.h" #include "nsImportModule.h" #include "nsIXPConnect.h" namespace mozilla { namespace extensions { // ExtensionAPIRequestForwarder // static void ExtensionAPIRequestForwarder::ThrowUnexpectedError(JSContext* aCx, ErrorResult& aRv) { aRv.MightThrowJSException(); JS_ReportErrorASCII(aCx, "An unexpected error occurred"); aRv.StealExceptionFromJSContext(aCx); } ExtensionAPIRequestForwarder::ExtensionAPIRequestForwarder( const mozIExtensionAPIRequest::RequestType aRequestType, const nsAString& aApiNamespace, const nsAString& aApiMethod, const nsAString& aApiObjectType, const nsAString& aApiObjectId) { mRequestType = aRequestType; mRequestTarget.mNamespace = aApiNamespace; mRequestTarget.mMethod = aApiMethod; mRequestTarget.mObjectType = aApiObjectType; mRequestTarget.mObjectId = aApiObjectId; } // static nsresult ExtensionAPIRequestForwarder::JSArrayToSequence( JSContext* aCx, JS::Handle aJSValue, dom::Sequence& aResult) { bool isArray; JS::Rooted obj(aCx, aJSValue.toObjectOrNull()); if (NS_WARN_IF(!obj || !JS::IsArrayObject(aCx, obj, &isArray))) { return NS_ERROR_UNEXPECTED; } if (isArray) { uint32_t len; if (NS_WARN_IF(!JS::GetArrayLength(aCx, obj, &len))) { return NS_ERROR_UNEXPECTED; } for (uint32_t i = 0; i < len; i++) { JS::Rooted v(aCx); JS_GetElement(aCx, obj, i, &v); if (NS_WARN_IF(!aResult.AppendElement(v, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } } } else if (NS_WARN_IF(!aResult.AppendElement(aJSValue, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } return NS_OK; } /* static */ mozIExtensionAPIRequestHandler& ExtensionAPIRequestForwarder::APIRequestHandler() { static nsCOMPtr sAPIRequestHandler; MOZ_ASSERT(NS_IsMainThread()); if (MOZ_UNLIKELY(!sAPIRequestHandler)) { sAPIRequestHandler = do_ImportModule("resource://gre/modules/ExtensionProcessScript.jsm", "ExtensionAPIRequestHandler"); MOZ_RELEASE_ASSERT(sAPIRequestHandler); ClearOnShutdown(&sAPIRequestHandler); } return *sAPIRequestHandler; } void ExtensionAPIRequestForwarder::SetSerializedCallerStack( UniquePtr aCallerStack) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); MOZ_ASSERT(mStackHolder.isNothing()); mStackHolder = Some(std::move(aCallerStack)); } void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx, const dom::Sequence& aArgs, ExtensionEventListener* aListener, JS::MutableHandle aRetVal, ErrorResult& aRv) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); dom::WorkerPrivate* workerPrivate = dom::GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); RefPtr runnable = new RequestWorkerRunnable(workerPrivate, this); if (mStackHolder.isSome()) { runnable->SetSerializedCallerStack(mStackHolder.extract()); } RefPtr domPromise; IgnoredErrorResult rv; switch (mRequestType) { case APIRequestType::CALL_FUNCTION_ASYNC: domPromise = dom::Promise::Create(aGlobal, rv); if (NS_WARN_IF(rv.Failed())) { ThrowUnexpectedError(aCx, aRv); return; } runnable->Init(aGlobal, aCx, aArgs, domPromise, rv); break; case APIRequestType::ADD_LISTENER: [[fallthrough]]; case APIRequestType::REMOVE_LISTENER: runnable->Init(aGlobal, aCx, aArgs, aListener, aRv); break; default: runnable->Init(aGlobal, aCx, aArgs, rv); } if (NS_WARN_IF(rv.Failed())) { ThrowUnexpectedError(aCx, aRv); return; } runnable->Dispatch(dom::WorkerStatus::Canceling, rv); if (NS_WARN_IF(rv.Failed())) { ThrowUnexpectedError(aCx, aRv); return; } auto resultType = runnable->GetResultType(); if (resultType.isNothing()) { if (NS_WARN_IF(ExtensionAPIRequest::ShouldHaveResult(mRequestType))) { ThrowUnexpectedError(aCx, aRv); } return; } // Read and throw the extension error if needed. if (resultType.isSome() && *resultType == APIResultType::EXTENSION_ERROR) { JS::Rooted ignoredResultValue(aCx); runnable->ReadResult(aCx, &ignoredResultValue, aRv); // When the result type is an error aRv is expected to be // failed, if it is not throw the generic // "An unexpected error occurred". if (NS_WARN_IF(!aRv.Failed())) { ThrowUnexpectedError(aCx, aRv); } return; } if (mRequestType == APIRequestType::CALL_FUNCTION_ASYNC) { MOZ_ASSERT(domPromise); if (NS_WARN_IF(!ToJSValue(aCx, domPromise, aRetVal))) { ThrowUnexpectedError(aCx, aRv); } return; } JS::Rooted resultValue(aCx); runnable->ReadResult(aCx, &resultValue, rv); if (NS_WARN_IF(rv.Failed())) { ThrowUnexpectedError(aCx, aRv); return; } aRetVal.set(resultValue); } void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx, const dom::Sequence& aArgs, JS::MutableHandle aRetVal, ErrorResult& aRv) { Run(aGlobal, aCx, aArgs, nullptr, aRetVal, aRv); } void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx, const dom::Sequence& aArgs, ErrorResult& aRv) { JS::Rooted ignoredRetval(aCx); Run(aGlobal, aCx, aArgs, nullptr, &ignoredRetval, aRv); } void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx, const dom::Sequence& aArgs, ExtensionEventListener* aListener, ErrorResult& aRv) { MOZ_ASSERT(aListener); JS::Rooted ignoredRetval(aCx); Run(aGlobal, aCx, aArgs, aListener, &ignoredRetval, aRv); } void ExtensionAPIRequestForwarder::Run( nsIGlobalObject* aGlobal, JSContext* aCx, const dom::Sequence& aArgs, const RefPtr& aPromiseRetval, ErrorResult& aRv) { MOZ_ASSERT(aPromiseRetval); JS::Rooted promisedRetval(aCx); Run(aGlobal, aCx, aArgs, &promisedRetval, aRv); if (aRv.Failed()) { return; } aPromiseRetval->MaybeResolve(promisedRetval); } void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx, JS::MutableHandle aRetVal, ErrorResult& aRv) { Run(aGlobal, aCx, {}, aRetVal, aRv); } namespace { // Custom PromiseWorkerProxy callback to deserialize error objects // from ClonedErrorHolder structured clone data. JSObject* ExtensionAPIRequestStructuredCloneRead( JSContext* aCx, JSStructuredCloneReader* aReader, const dom::PromiseWorkerProxy* aProxy, uint32_t aTag, uint32_t aData) { // Deserialize ClonedErrorHolder that may have been structured cloned // as a result of a resolved/rejected promise. if (aTag == dom::SCTAG_DOM_CLONED_ERROR_OBJECT) { return dom::ClonedErrorHolder::ReadStructuredClone(aCx, aReader, nullptr); } return nullptr; } // Custom PromiseWorkerProxy callback to serialize error objects into // ClonedErrorHolder structured clone data. bool ExtensionAPIRequestStructuredCloneWrite(JSContext* aCx, JSStructuredCloneWriter* aWriter, dom::PromiseWorkerProxy* aProxy, JS::Handle aObj) { // Try to serialize the object as a CloneErrorHolder, if it fails then // the object wasn't an error. IgnoredErrorResult rv; RefPtr ceh = dom::ClonedErrorHolder::Create(aCx, aObj, rv); if (NS_WARN_IF(rv.Failed()) || !ceh) { return false; } return ceh->WriteStructuredClone(aCx, aWriter, nullptr); } } // namespace RequestWorkerRunnable::RequestWorkerRunnable( dom::WorkerPrivate* aWorkerPrivate, ExtensionAPIRequestForwarder* aOuterAPIRequest) : WorkerMainThreadRunnable(aWorkerPrivate, "ExtensionAPIRequest :: WorkerRunnable"_ns) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); MOZ_ASSERT(aOuterAPIRequest); mOuterRequest = aOuterAPIRequest; } void RequestWorkerRunnable::Init(nsIGlobalObject* aGlobal, JSContext* aCx, const dom::Sequence& aArgs, ExtensionEventListener* aListener, ErrorResult& aRv) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); mSWDescriptorId = mWorkerPrivate->ServiceWorkerID(); auto* workerScope = mWorkerPrivate->GlobalScope(); if (NS_WARN_IF(!workerScope)) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } mClientInfo = workerScope->GetClientInfo(); if (mClientInfo.isNothing()) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } IgnoredErrorResult rv; SerializeArgs(aCx, aArgs, rv); if (NS_WARN_IF(rv.Failed())) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } if (!mStackHolder.isSome()) { SerializeCallerStack(aCx); } mEventListener = aListener; } void RequestWorkerRunnable::Init(nsIGlobalObject* aGlobal, JSContext* aCx, const dom::Sequence& aArgs, const RefPtr& aPromiseRetval, ErrorResult& aRv) { // Custom callbacks needed to make the PromiseWorkerProxy instance to // be able to write and read errors using CloneErrorHolder. static const dom::PromiseWorkerProxy:: PromiseWorkerProxyStructuredCloneCallbacks kExtensionAPIRequestStructuredCloneCallbacks = { ExtensionAPIRequestStructuredCloneRead, ExtensionAPIRequestStructuredCloneWrite, }; Init(aGlobal, aCx, aArgs, /* aListener */ nullptr, aRv); if (aRv.Failed()) { return; } RefPtr promiseProxy = dom::PromiseWorkerProxy::Create( mWorkerPrivate, aPromiseRetval, &kExtensionAPIRequestStructuredCloneCallbacks); if (!promiseProxy) { aRv.Throw(NS_ERROR_DOM_ABORT_ERR); return; } mPromiseProxy = promiseProxy.forget(); } void RequestWorkerRunnable::SetSerializedCallerStack( UniquePtr aCallerStack) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); MOZ_ASSERT(mStackHolder.isNothing()); mStackHolder = Some(std::move(aCallerStack)); } void RequestWorkerRunnable::SerializeCallerStack(JSContext* aCx) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); MOZ_ASSERT(mStackHolder.isNothing()); mStackHolder = Some(dom::GetCurrentStack(aCx)); } void RequestWorkerRunnable::DeserializeCallerStack( JSContext* aCx, JS::MutableHandle aRetval) { MOZ_ASSERT(NS_IsMainThread()); if (mStackHolder.isSome()) { JS::Rooted savedFrame(aCx, mStackHolder->get()->ReadStack(aCx)); MOZ_ASSERT(savedFrame); aRetval.set(JS::ObjectValue(*savedFrame)); mStackHolder = Nothing(); } } void RequestWorkerRunnable::SerializeArgs(JSContext* aCx, const dom::Sequence& aArgs, ErrorResult& aRv) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); MOZ_ASSERT(!mArgsHolder); JS::Rooted jsval(aCx); if (NS_WARN_IF(!ToJSValue(aCx, aArgs, &jsval))) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } mArgsHolder = Some(MakeUnique( dom::StructuredCloneHolder::CloningSupported, dom::StructuredCloneHolder::TransferringNotSupported, JS::StructuredCloneScope::SameProcess)); mArgsHolder->get()->Write(aCx, jsval, aRv); } nsresult RequestWorkerRunnable::DeserializeArgs( JSContext* aCx, JS::MutableHandle aArgs) { MOZ_ASSERT(NS_IsMainThread()); if (mArgsHolder.isSome() && mArgsHolder->get()->HasData()) { IgnoredErrorResult rv; JS::Rooted jsvalue(aCx); mArgsHolder->get()->Read(xpc::CurrentNativeGlobal(aCx), aCx, &jsvalue, rv); if (NS_WARN_IF(rv.Failed())) { return NS_ERROR_UNEXPECTED; } aArgs.set(jsvalue); } return NS_OK; } bool RequestWorkerRunnable::MainThreadRun() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr handler = &ExtensionAPIRequestForwarder::APIRequestHandler(); nsCOMPtr wrapped = do_QueryInterface(handler); dom::AutoJSAPI jsapi; if (!jsapi.Init(wrapped->GetJSObjectGlobal())) { return false; } auto* cx = jsapi.cx(); JS::Rooted retval(cx); return HandleAPIRequest(cx, &retval); } already_AddRefed RequestWorkerRunnable::CreateAPIRequest( JSContext* aCx) { JS::Rooted callArgs(aCx); JS::Rooted callerStackValue(aCx); DeserializeArgs(aCx, &callArgs); DeserializeCallerStack(aCx, &callerStackValue); RefPtr request = new ExtensionAPIRequest( mOuterRequest->GetRequestType(), *mOuterRequest->GetRequestTarget()); request->Init(mClientInfo, mSWDescriptorId, callArgs, callerStackValue); if (mEventListener) { request->SetEventListener(mEventListener.forget()); } return request.forget(); } already_AddRefed RequestWorkerRunnable::GetWebExtensionPolicy() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mWorkerPrivate); auto* baseURI = mWorkerPrivate->GetBaseURI(); RefPtr policy = ExtensionPolicyService::GetSingleton().GetByURL(baseURI); return policy.forget(); } bool RequestWorkerRunnable::HandleAPIRequest( JSContext* aCx, JS::MutableHandle aRetval) { MOZ_ASSERT(NS_IsMainThread()); RefPtr policy = GetWebExtensionPolicy(); if (NS_WARN_IF(!policy || !policy->Active())) { // Fails if no extension policy object has been found, or if the // extension is not active. return false; } nsresult rv; RefPtr request = CreateAPIRequest(aCx); nsCOMPtr handler = &ExtensionAPIRequestForwarder::APIRequestHandler(); RefPtr apiResult; rv = handler->HandleAPIRequest(policy, request, getter_AddRefs(apiResult)); if (NS_FAILED(rv)) { return false; } // A missing apiResult is expected for some request types // (e.g. CALL_FUNCTION_NO_RETURN/ADD_LISTENER/REMOVE_LISTENER). // If the apiResult is missing for a request type that expects // to have one, consider the request as failed with an unknown error. if (!apiResult) { return !request->ShouldHaveResult(); } mozIExtensionAPIRequestResult::ResultType resultType; apiResult->GetType(&resultType); apiResult->GetValue(aRetval); mResultType = Some(resultType); bool isExtensionError = resultType == mozIExtensionAPIRequestResult::ResultType::EXTENSION_ERROR; bool okSerializedError = false; if (aRetval.isObject()) { // Try to serialize the result as an ClonedErrorHolder // (because all API requests could receive one for EXTENSION_ERROR // result types, and some also as a RETURN_VALUE result, e.g. // runtime.lastError). JS::Rooted errObj(aCx, &aRetval.toObject()); IgnoredErrorResult rv; RefPtr ceh = dom::ClonedErrorHolder::Create(aCx, errObj, rv); if (!rv.Failed() && ceh) { JS::Rooted obj(aCx); // Note: `ToJSValue` cannot be used because ClonedErrorHolder isn't // wrapper cached. okSerializedError = ceh->WrapObject(aCx, nullptr, &obj); aRetval.setObject(*obj); } else { okSerializedError = false; } } if (isExtensionError && !okSerializedError) { NS_WARNING("Failed to wrap ClonedErrorHolder"); MOZ_DIAGNOSTIC_ASSERT(false, "Failed to wrap ClonedErrorHolder"); return false; } if (isExtensionError && !aRetval.isObject()) { NS_WARNING("Unexpected non-object error"); return false; } switch (resultType) { case mozIExtensionAPIRequestResult::ResultType::RETURN_VALUE: return ProcessHandlerResult(aCx, aRetval); case mozIExtensionAPIRequestResult::ResultType::EXTENSION_ERROR: if (!aRetval.isObject()) { return false; } return ProcessHandlerResult(aCx, aRetval); } MOZ_DIAGNOSTIC_ASSERT(false, "Unexpected API request ResultType"); return false; } bool RequestWorkerRunnable::ProcessHandlerResult( JSContext* aCx, JS::MutableHandle aRetval) { MOZ_ASSERT(NS_IsMainThread()); if (mOuterRequest->GetRequestType() == APIRequestType::CALL_FUNCTION_ASYNC) { if (NS_WARN_IF(mResultType.isNothing())) { return false; } if (*mResultType == APIResultType::RETURN_VALUE) { // For an Async API method we expect a promise object to be set // as the value to return, if it is not we return earlier here // (and then throw a generic unexpected error to the caller). if (NS_WARN_IF(!aRetval.isObject())) { return false; } JS::Rooted obj(aCx, &aRetval.toObject()); if (NS_WARN_IF(!JS::IsPromiseObject(obj))) { return false; } ErrorResult rv; nsIGlobalObject* glob = xpc::CurrentNativeGlobal(aCx); RefPtr retPromise = dom::Promise::Resolve(glob, aCx, aRetval, rv); if (rv.Failed()) { return false; } retPromise->AppendNativeHandler(mPromiseProxy); return true; } } switch (*mResultType) { case APIResultType::RETURN_VALUE: [[fallthrough]]; case APIResultType::EXTENSION_ERROR: { // In all other case we expect the result to be: // - a structured clonable result // - an extension error (e.g. due to the API call params validation // errors), // previously converted into a CloneErrorHolder IgnoredErrorResult rv; mResultHolder = Some(MakeUnique( dom::StructuredCloneHolder::CloningSupported, dom::StructuredCloneHolder::TransferringNotSupported, JS::StructuredCloneScope::SameProcess)); mResultHolder->get()->Write(aCx, aRetval, rv); return !rv.Failed(); } } MOZ_DIAGNOSTIC_ASSERT(false, "Unexpected API request ResultType"); return false; } void RequestWorkerRunnable::ReadResult(JSContext* aCx, JS::MutableHandle aResult, ErrorResult& aRv) { MOZ_ASSERT(mWorkerPrivate->IsOnCurrentThread()); if (mResultHolder.isNothing() || !mResultHolder->get()->HasData()) { return; } if (NS_WARN_IF(mResultType.isNothing())) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } switch (*mResultType) { case mozIExtensionAPIRequestResult::ResultType::RETURN_VALUE: mResultHolder->get()->Read(xpc::CurrentNativeGlobal(aCx), aCx, aResult, aRv); return; case mozIExtensionAPIRequestResult::ResultType::EXTENSION_ERROR: JS::Rooted exn(aCx); IgnoredErrorResult rv; mResultHolder->get()->Read(xpc::CurrentNativeGlobal(aCx), aCx, &exn, rv); if (rv.Failed()) { NS_WARNING("Failed to deserialize extension error"); ExtensionAPIBase::ThrowUnexpectedError(aCx, aRv); return; } aRv.MightThrowJSException(); aRv.ThrowJSException(aCx, exn); return; } MOZ_DIAGNOSTIC_ASSERT(false, "Unexpected API request ResultType"); aRv.Throw(NS_ERROR_UNEXPECTED); } // RequestInitWorkerContextRunnable RequestInitWorkerRunnable::RequestInitWorkerRunnable( dom::WorkerPrivate* aWorkerPrivate, Maybe& aSWClientInfo) : WorkerMainThreadRunnable(aWorkerPrivate, "extensions::RequestInitWorkerRunnable"_ns) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); MOZ_ASSERT(aSWClientInfo.isSome()); mClientInfo = aSWClientInfo; } bool RequestInitWorkerRunnable::MainThreadRun() { MOZ_ASSERT(NS_IsMainThread()); auto* baseURI = mWorkerPrivate->GetBaseURI(); RefPtr policy = ExtensionPolicyService::GetSingleton().GetByURL(baseURI); RefPtr swInfo = new ExtensionServiceWorkerInfo( *mClientInfo, mWorkerPrivate->ServiceWorkerID()); nsCOMPtr handler = &ExtensionAPIRequestForwarder::APIRequestHandler(); MOZ_ASSERT(handler); if (NS_FAILED(handler->InitExtensionWorker(policy, swInfo))) { NS_WARNING("nsIExtensionAPIRequestHandler.initExtensionWorker call failed"); } return true; } // NotifyWorkerLoadedRunnable nsresult NotifyWorkerLoadedRunnable::Run() { MOZ_ASSERT(NS_IsMainThread()); RefPtr policy = ExtensionPolicyService::GetSingleton().GetByURL(mSWBaseURI.get()); nsCOMPtr handler = &ExtensionAPIRequestForwarder::APIRequestHandler(); MOZ_ASSERT(handler); if (NS_FAILED(handler->OnExtensionWorkerLoaded(policy, mSWDescriptorId))) { NS_WARNING( "nsIExtensionAPIRequestHandler.onExtensionWorkerLoaded call failed"); } return NS_OK; } // NotifyWorkerDestroyedRunnable nsresult NotifyWorkerDestroyedRunnable::Run() { MOZ_ASSERT(NS_IsMainThread()); RefPtr policy = ExtensionPolicyService::GetSingleton().GetByURL(mSWBaseURI.get()); nsCOMPtr handler = &ExtensionAPIRequestForwarder::APIRequestHandler(); MOZ_ASSERT(handler); if (NS_FAILED(handler->OnExtensionWorkerDestroyed(policy, mSWDescriptorId))) { NS_WARNING( "nsIExtensionAPIRequestHandler.onExtensionWorkerDestroyed call failed"); } return NS_OK; } } // namespace extensions } // namespace mozilla