/* -*- 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; // If we have the document, use it if (parentDoc) { rv = NS_NewChannel(getter_AddRefs(channel), uri, parentDoc, nsILoadInfo::SEC_NORMAL, nsIContentPolicy::TYPE_SCRIPT, channelPolicy, loadGroup, nullptr, // aCallbacks flags, ios); } else { // we should use 'principal' here; needs to be fixed before // we move security checks to AsyncOpen. We use nullPrincipal // for now, because the loadGroup is null and hence causes // GetChannelUriPrincipal to return the wrong principal. nsCOMPtr nullPrincipal = do_CreateInstance("@mozilla.org/nullprincipal;1", &rv); rv = NS_NewChannel(getter_AddRefs(channel), uri, nullPrincipal, nsILoadInfo::SEC_NORMAL, nsIContentPolicy::TYPE_SCRIPT, channelPolicy, loadGroup, nullptr, // aCallbacks flags, ios); } 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; char16_t* 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))) { 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; } 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