diff -r 000000000000 -r 6474c204b198 dom/workers/ScriptLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/workers/ScriptLoader.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,956 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* 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 "ScriptLoader.h" + +#include "nsIChannel.h" +#include "nsIChannelPolicy.h" +#include "nsIContentPolicy.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIHttpChannel.h" +#include "nsIIOService.h" +#include "nsIProtocolHandler.h" +#include "nsIScriptSecurityManager.h" +#include "nsIStreamLoader.h" +#include "nsIURI.h" + +#include "jsapi.h" +#include "nsChannelPolicy.h" +#include "nsError.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsDocShellCID.h" +#include "nsISupportsPrimitives.h" +#include "nsNetUtil.h" +#include "nsScriptLoader.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsXPCOM.h" +#include "xpcpublic.h" + +#include "mozilla/dom/Exceptions.h" +#include "Principal.h" +#include "WorkerFeature.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" + +#define MAX_CONCURRENT_SCRIPTS 1000 + +USING_WORKERS_NAMESPACE + +using mozilla::dom::workers::exceptions::ThrowDOMExceptionForNSResult; + +namespace { + +nsresult +ChannelFromScriptURL(nsIPrincipal* principal, + nsIURI* baseURI, + nsIDocument* parentDoc, + nsILoadGroup* loadGroup, + nsIIOService* ios, + nsIScriptSecurityManager* secMan, + const nsAString& aScriptURL, + bool aIsWorkerScript, + nsIChannel** aChannel) +{ + AssertIsOnMainThread(); + + nsresult rv; + nsCOMPtr uri; + rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), + aScriptURL, parentDoc, + baseURI); + if (NS_FAILED(rv)) { + return NS_ERROR_DOM_SYNTAX_ERR; + } + + // If we're part of a document then check the content load policy. + if (parentDoc) { + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_SCRIPT, uri, + principal, parentDoc, + NS_LITERAL_CSTRING("text/javascript"), + nullptr, &shouldLoad, + nsContentUtils::GetContentPolicy(), + secMan); + if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) { + if (NS_FAILED(rv) || shouldLoad != nsIContentPolicy::REJECT_TYPE) { + return rv = NS_ERROR_CONTENT_BLOCKED; + } + return rv = NS_ERROR_CONTENT_BLOCKED_SHOW_ALT; + } + } + + // If this script loader is being used to make a new worker then we need + // to do a same-origin check. Otherwise we need to clear the load with the + // security manager. + if (aIsWorkerScript) { + nsCString scheme; + rv = uri->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, rv); + + // We pass true as the 3rd argument to checkMayLoad here. + // This allows workers in sandboxed documents to load data URLs + // (and other URLs that inherit their principal from their + // creator.) + rv = principal->CheckMayLoad(uri, false, true); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SECURITY_ERR); + } + else { + rv = secMan->CheckLoadURIWithPrincipal(principal, uri, 0); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SECURITY_ERR); + } + + // Get Content Security Policy from parent document to pass into channel. + nsCOMPtr csp; + rv = principal->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr channelPolicy; + if (csp) { + channelPolicy = do_CreateInstance(NSCHANNELPOLICY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = channelPolicy->SetContentSecurityPolicy(csp); + NS_ENSURE_SUCCESS(rv, rv); + + rv = channelPolicy->SetLoadType(nsIContentPolicy::TYPE_SCRIPT); + NS_ENSURE_SUCCESS(rv, rv); + } + + uint32_t flags = nsIRequest::LOAD_NORMAL | nsIChannel::LOAD_CLASSIFY_URI; + + nsCOMPtr channel; + rv = NS_NewChannel(getter_AddRefs(channel), uri, ios, loadGroup, nullptr, + flags, channelPolicy); + NS_ENSURE_SUCCESS(rv, rv); + + channel.forget(aChannel); + return rv; +} + +struct ScriptLoadInfo +{ + ScriptLoadInfo() + : mScriptTextBuf(nullptr) + , mScriptTextLength(0) + , mLoadResult(NS_ERROR_NOT_INITIALIZED), mExecutionScheduled(false) + , mExecutionResult(false) + { } + + ~ScriptLoadInfo() + { + if (mScriptTextBuf) { + js_free(mScriptTextBuf); + } + } + + bool + ReadyToExecute() + { + return !mChannel && NS_SUCCEEDED(mLoadResult) && !mExecutionScheduled; + } + + nsString mURL; + nsCOMPtr mChannel; + jschar* mScriptTextBuf; + size_t mScriptTextLength; + + nsresult mLoadResult; + bool mExecutionScheduled; + bool mExecutionResult; +}; + +class ScriptLoaderRunnable; + +class ScriptExecutorRunnable MOZ_FINAL : public MainThreadWorkerSyncRunnable +{ + ScriptLoaderRunnable& mScriptLoader; + uint32_t mFirstIndex; + uint32_t mLastIndex; + +public: + ScriptExecutorRunnable(ScriptLoaderRunnable& aScriptLoader, + nsIEventTarget* aSyncLoopTarget, uint32_t aFirstIndex, + uint32_t aLastIndex); + +private: + ~ScriptExecutorRunnable() + { } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) MOZ_OVERRIDE; + + virtual void + PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult) + MOZ_OVERRIDE; + + NS_DECL_NSICANCELABLERUNNABLE + + void + ShutdownScriptLoader(JSContext* aCx, + WorkerPrivate* aWorkerPrivate, + bool aResult); +}; + +class ScriptLoaderRunnable MOZ_FINAL : public WorkerFeature, + public nsIRunnable, + public nsIStreamLoaderObserver +{ + friend class ScriptExecutorRunnable; + + WorkerPrivate* mWorkerPrivate; + nsCOMPtr mSyncLoopTarget; + nsTArray mLoadInfos; + bool mIsWorkerScript; + bool mCanceled; + bool mCanceledMainThread; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + + ScriptLoaderRunnable(WorkerPrivate* aWorkerPrivate, + nsIEventTarget* aSyncLoopTarget, + nsTArray& aLoadInfos, + bool aIsWorkerScript) + : mWorkerPrivate(aWorkerPrivate), mSyncLoopTarget(aSyncLoopTarget), + mIsWorkerScript(aIsWorkerScript), mCanceled(false), + mCanceledMainThread(false) + { + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aSyncLoopTarget); + MOZ_ASSERT_IF(aIsWorkerScript, aLoadInfos.Length() == 1); + + mLoadInfos.SwapElements(aLoadInfos); + } + +private: + ~ScriptLoaderRunnable() + { } + + NS_IMETHOD + Run() MOZ_OVERRIDE + { + AssertIsOnMainThread(); + + if (NS_FAILED(RunInternal())) { + CancelMainThread(); + } + + return NS_OK; + } + + NS_IMETHOD + OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString) MOZ_OVERRIDE + { + AssertIsOnMainThread(); + + nsCOMPtr indexSupports(do_QueryInterface(aContext)); + NS_ASSERTION(indexSupports, "This should never fail!"); + + uint32_t index = UINT32_MAX; + if (NS_FAILED(indexSupports->GetData(&index)) || + index >= mLoadInfos.Length()) { + NS_ERROR("Bad index!"); + } + + ScriptLoadInfo& loadInfo = mLoadInfos[index]; + + loadInfo.mLoadResult = OnStreamCompleteInternal(aLoader, aContext, aStatus, + aStringLen, aString, + loadInfo); + + ExecuteFinishedScripts(); + + return NS_OK; + } + + virtual bool + Notify(JSContext* aCx, Status aStatus) MOZ_OVERRIDE + { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (aStatus >= Terminating && !mCanceled) { + mCanceled = true; + + nsCOMPtr runnable = + NS_NewRunnableMethod(this, &ScriptLoaderRunnable::CancelMainThread); + NS_ASSERTION(runnable, "This should never fail!"); + + if (NS_FAILED(NS_DispatchToMainThread(runnable, NS_DISPATCH_NORMAL))) { + JS_ReportError(aCx, "Failed to cancel script loader!"); + return false; + } + } + + return true; + } + + void + CancelMainThread() + { + AssertIsOnMainThread(); + + if (mCanceledMainThread) { + return; + } + + mCanceledMainThread = true; + + // Cancel all the channels that were already opened. + for (uint32_t index = 0; index < mLoadInfos.Length(); index++) { + ScriptLoadInfo& loadInfo = mLoadInfos[index]; + + if (loadInfo.mChannel && + NS_FAILED(loadInfo.mChannel->Cancel(NS_BINDING_ABORTED))) { + NS_WARNING("Failed to cancel channel!"); + loadInfo.mChannel = nullptr; + loadInfo.mLoadResult = NS_BINDING_ABORTED; + } + } + + ExecuteFinishedScripts(); + } + + nsresult + RunInternal() + { + AssertIsOnMainThread(); + + WorkerPrivate* parentWorker = mWorkerPrivate->GetParent(); + + // Figure out which principal to use. + nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); + if (!principal) { + NS_ASSERTION(parentWorker, "Must have a principal!"); + NS_ASSERTION(mIsWorkerScript, "Must have a principal for importScripts!"); + + principal = parentWorker->GetPrincipal(); + } + NS_ASSERTION(principal, "This should never be null here!"); + + // Figure out our base URI. + nsCOMPtr baseURI; + if (mIsWorkerScript) { + if (parentWorker) { + baseURI = parentWorker->GetBaseURI(); + NS_ASSERTION(baseURI, "Should have been set already!"); + } + else { + // May be null. + baseURI = mWorkerPrivate->GetBaseURI(); + } + } + else { + baseURI = mWorkerPrivate->GetBaseURI(); + NS_ASSERTION(baseURI, "Should have been set already!"); + } + + // May be null. + nsCOMPtr parentDoc = mWorkerPrivate->GetDocument(); + + nsCOMPtr channel; + if (mIsWorkerScript) { + // May be null. + channel = mWorkerPrivate->ForgetWorkerChannel(); + } + + // All of these can potentially be null, but that should be ok. We'll either + // succeed without them or fail below. + nsCOMPtr loadGroup; + if (parentDoc) { + loadGroup = parentDoc->GetDocumentLoadGroup(); + } + + nsCOMPtr ios(do_GetIOService()); + + nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(secMan, "This should never be null!"); + + for (uint32_t index = 0; index < mLoadInfos.Length(); index++) { + ScriptLoadInfo& loadInfo = mLoadInfos[index]; + nsresult& rv = loadInfo.mLoadResult; + + if (!channel) { + rv = ChannelFromScriptURL(principal, baseURI, parentDoc, loadGroup, ios, + secMan, loadInfo.mURL, mIsWorkerScript, + getter_AddRefs(channel)); + if (NS_FAILED(rv)) { + return rv; + } + } + + // We need to know which index we're on in OnStreamComplete so we know + // where to put the result. + nsCOMPtr indexSupports = + do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = indexSupports->SetData(index); + NS_ENSURE_SUCCESS(rv, rv); + + // We don't care about progress so just use the simple stream loader for + // OnStreamComplete notification only. + nsCOMPtr loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + NS_ENSURE_SUCCESS(rv, rv); + + rv = channel->AsyncOpen(loader, indexSupports); + NS_ENSURE_SUCCESS(rv, rv); + + loadInfo.mChannel.swap(channel); + } + + return NS_OK; + } + + nsresult + OnStreamCompleteInternal(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString, ScriptLoadInfo& aLoadInfo) + { + AssertIsOnMainThread(); + + if (!aLoadInfo.mChannel) { + return NS_BINDING_ABORTED; + } + + aLoadInfo.mChannel = nullptr; + + if (NS_FAILED(aStatus)) { + return aStatus; + } + + if (!aStringLen) { + return NS_OK; + } + + NS_ASSERTION(aString, "This should never be null!"); + + // Make sure we're not seeing the result of a 404 or something by checking + // the 'requestSucceeded' attribute on the http channel. + nsCOMPtr request; + nsresult rv = aLoader->GetRequest(getter_AddRefs(request)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr httpChannel = do_QueryInterface(request); + if (httpChannel) { + bool requestSucceeded; + rv = httpChannel->GetRequestSucceeded(&requestSucceeded); + NS_ENSURE_SUCCESS(rv, rv); + + if (!requestSucceeded) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + // May be null. + nsIDocument* parentDoc = mWorkerPrivate->GetDocument(); + + // Use the regular nsScriptLoader for this grunt work! Should be just fine + // because we're running on the main thread. + // Unlike