diff options
Diffstat (limited to 'dom/base/WebSocket.cpp')
-rw-r--r-- | dom/base/WebSocket.cpp | 2908 |
1 files changed, 2908 insertions, 0 deletions
diff --git a/dom/base/WebSocket.cpp b/dom/base/WebSocket.cpp new file mode 100644 index 0000000000..d85bae82be --- /dev/null +++ b/dom/base/WebSocket.cpp @@ -0,0 +1,2908 @@ +/* -*- 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 "WebSocket.h" +#include "mozilla/dom/WebSocketBinding.h" +#include "mozilla/net/WebSocketChannel.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/net/WebSocketChannel.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" +#include "nsAutoPtr.h" +#include "nsGlobalWindow.h" +#include "nsIScriptGlobalObject.h" +#include "nsIDOMWindow.h" +#include "nsIDocument.h" +#include "nsXPCOM.h" +#include "nsIXPConnect.h" +#include "nsContentUtils.h" +#include "nsError.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsIURL.h" +#include "nsIUnicodeEncoder.h" +#include "nsThreadUtils.h" +#include "nsIPromptFactory.h" +#include "nsIWindowWatcher.h" +#include "nsIPrompt.h" +#include "nsIStringBundle.h" +#include "nsIConsoleService.h" +#include "mozilla/dom/CloseEvent.h" +#include "mozilla/net/WebSocketEventService.h" +#include "nsICryptoHash.h" +#include "nsJSUtils.h" +#include "nsIScriptError.h" +#include "nsNetUtil.h" +#include "nsIAuthPrompt.h" +#include "nsIAuthPrompt2.h" +#include "nsILoadGroup.h" +#include "mozilla/Preferences.h" +#include "xpcpublic.h" +#include "nsContentPolicyUtils.h" +#include "nsWrapperCacheInlines.h" +#include "nsIObserverService.h" +#include "nsIEventTarget.h" +#include "nsIInterfaceRequestor.h" +#include "nsIObserver.h" +#include "nsIRequest.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIWebSocketChannel.h" +#include "nsIWebSocketListener.h" +#include "nsProxyRelease.h" +#include "nsWeakReference.h" + +using namespace mozilla::net; +using namespace mozilla::dom::workers; + +namespace mozilla { +namespace dom { + +class WebSocketImpl final : public nsIInterfaceRequestor + , public nsIWebSocketListener + , public nsIObserver + , public nsSupportsWeakReference + , public nsIRequest + , public nsIEventTarget +{ +public: + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSIWEBSOCKETLISTENER + NS_DECL_NSIOBSERVER + NS_DECL_NSIREQUEST + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET + using nsIEventTarget::Dispatch; + + explicit WebSocketImpl(WebSocket* aWebSocket) + : mWebSocket(aWebSocket) + , mIsServerSide(false) + , mSecure(false) + , mOnCloseScheduled(false) + , mFailed(false) + , mDisconnectingOrDisconnected(false) + , mCloseEventWasClean(false) + , mCloseEventCode(nsIWebSocketChannel::CLOSE_ABNORMAL) + , mScriptLine(0) + , mScriptColumn(0) + , mInnerWindowID(0) + , mWorkerPrivate(nullptr) +#ifdef DEBUG + , mHasWorkerHolderRegistered(false) +#endif + , mIsMainThread(true) + , mMutex("WebSocketImpl::mMutex") + , mWorkerShuttingDown(false) + { + if (!NS_IsMainThread()) { + mWorkerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(mWorkerPrivate); + mIsMainThread = false; + } + } + + void AssertIsOnTargetThread() const + { + MOZ_ASSERT(IsTargetThread()); + } + + bool IsTargetThread() const; + + void Init(JSContext* aCx, + nsIPrincipal* aPrincipal, + bool aIsServerSide, + const nsAString& aURL, + nsTArray<nsString>& aProtocolArray, + const nsACString& aScriptFile, + uint32_t aScriptLine, + uint32_t aScriptColumn, + ErrorResult& aRv, + bool* aConnectionFailed); + + void AsyncOpen(nsIPrincipal* aPrincipal, uint64_t aInnerWindowID, + nsITransportProvider* aTransportProvider, + const nsACString& aNegotiatedExtensions, + ErrorResult& aRv); + + nsresult ParseURL(const nsAString& aURL); + nsresult InitializeConnection(nsIPrincipal* aPrincipal); + + // These methods when called can release the WebSocket object + void FailConnection(uint16_t reasonCode, + const nsACString& aReasonString = EmptyCString()); + nsresult CloseConnection(uint16_t reasonCode, + const nsACString& aReasonString = EmptyCString()); + void Disconnect(); + void DisconnectInternal(); + + nsresult ConsoleError(); + void PrintErrorOnConsole(const char* aBundleURI, + const char16_t* aError, + const char16_t** aFormatStrings, + uint32_t aFormatStringsLen); + + nsresult DoOnMessageAvailable(const nsACString& aMsg, + bool isBinary); + + // ConnectionCloseEvents: 'error' event if needed, then 'close' event. + nsresult ScheduleConnectionCloseEvents(nsISupports* aContext, + nsresult aStatusCode); + // 2nd half of ScheduleConnectionCloseEvents, run in its own event. + void DispatchConnectionCloseEvents(); + + nsresult UpdateURI(); + + void AddRefObject(); + void ReleaseObject(); + + bool RegisterWorkerHolder(); + void UnregisterWorkerHolder(); + + nsresult CancelInternal(); + + RefPtr<WebSocket> mWebSocket; + + nsCOMPtr<nsIWebSocketChannel> mChannel; + + bool mIsServerSide; // True if we're implementing the server side of a + // websocket connection + + bool mSecure; // if true it is using SSL and the wss scheme, + // otherwise it is using the ws scheme with no SSL + + bool mOnCloseScheduled; + bool mFailed; + bool mDisconnectingOrDisconnected; + + // Set attributes of DOM 'onclose' message + bool mCloseEventWasClean; + nsString mCloseEventReason; + uint16_t mCloseEventCode; + + nsCString mAsciiHost; // hostname + uint32_t mPort; + nsCString mResource; // [filepath[?query]] + nsString mUTF16Origin; + + nsCString mURI; + nsCString mRequestedProtocolList; + + nsWeakPtr mOriginDocument; + + // Web Socket owner information: + // - the script file name, UTF8 encoded. + // - source code line number and column number where the Web Socket object + // was constructed. + // - the ID of the inner window where the script lives. Note that this may not + // be the same as the Web Socket owner window. + // These attributes are used for error reporting. + nsCString mScriptFile; + uint32_t mScriptLine; + uint32_t mScriptColumn; + uint64_t mInnerWindowID; + + WorkerPrivate* mWorkerPrivate; + nsAutoPtr<WorkerHolder> mWorkerHolder; + +#ifdef DEBUG + // This is protected by mutex. + bool mHasWorkerHolderRegistered; + + bool HasWorkerHolderRegistered() + { + MOZ_ASSERT(mWebSocket); + MutexAutoLock lock(mWebSocket->mMutex); + return mHasWorkerHolderRegistered; + } + + void SetHasWorkerHolderRegistered(bool aValue) + { + MOZ_ASSERT(mWebSocket); + MutexAutoLock lock(mWebSocket->mMutex); + mHasWorkerHolderRegistered = aValue; + } +#endif + + nsWeakPtr mWeakLoadGroup; + + bool mIsMainThread; + + // This mutex protects mWorkerShuttingDown. + mozilla::Mutex mMutex; + bool mWorkerShuttingDown; + + RefPtr<WebSocketEventService> mService; + +private: + ~WebSocketImpl() + { + // If we threw during Init we never called disconnect + if (!mDisconnectingOrDisconnected) { + Disconnect(); + } + } +}; + +NS_IMPL_ISUPPORTS(WebSocketImpl, + nsIInterfaceRequestor, + nsIWebSocketListener, + nsIObserver, + nsISupportsWeakReference, + nsIRequest, + nsIEventTarget) + +class CallDispatchConnectionCloseEvents final : public CancelableRunnable +{ +public: + explicit CallDispatchConnectionCloseEvents(WebSocketImpl* aWebSocketImpl) + : mWebSocketImpl(aWebSocketImpl) + { + aWebSocketImpl->AssertIsOnTargetThread(); + } + + NS_IMETHOD Run() override + { + mWebSocketImpl->AssertIsOnTargetThread(); + mWebSocketImpl->DispatchConnectionCloseEvents(); + return NS_OK; + } + +private: + RefPtr<WebSocketImpl> mWebSocketImpl; +}; + +//----------------------------------------------------------------------------- +// WebSocketImpl +//----------------------------------------------------------------------------- + +namespace { + +class PrintErrorOnConsoleRunnable final : public WorkerMainThreadRunnable +{ +public: + PrintErrorOnConsoleRunnable(WebSocketImpl* aImpl, + const char* aBundleURI, + const char16_t* aError, + const char16_t** aFormatStrings, + uint32_t aFormatStringsLen) + : WorkerMainThreadRunnable(aImpl->mWorkerPrivate, + NS_LITERAL_CSTRING("WebSocket :: print error on console")) + , mImpl(aImpl) + , mBundleURI(aBundleURI) + , mError(aError) + , mFormatStrings(aFormatStrings) + , mFormatStringsLen(aFormatStringsLen) + { } + + bool MainThreadRun() override + { + mImpl->PrintErrorOnConsole(mBundleURI, mError, mFormatStrings, + mFormatStringsLen); + return true; + } + +private: + // Raw pointer because this runnable is sync. + WebSocketImpl* mImpl; + + const char* mBundleURI; + const char16_t* mError; + const char16_t** mFormatStrings; + uint32_t mFormatStringsLen; +}; + +} // namespace + +void +WebSocketImpl::PrintErrorOnConsole(const char *aBundleURI, + const char16_t *aError, + const char16_t **aFormatStrings, + uint32_t aFormatStringsLen) +{ + // This method must run on the main thread. + + if (!NS_IsMainThread()) { + MOZ_ASSERT(mWorkerPrivate); + + RefPtr<PrintErrorOnConsoleRunnable> runnable = + new PrintErrorOnConsoleRunnable(this, aBundleURI, aError, aFormatStrings, + aFormatStringsLen); + ErrorResult rv; + runnable->Dispatch(rv); + // XXXbz this seems totally broken. We should be propagating this out, but + // none of our callers really propagate anything usefully. Come to think of + // it, why is this a syncrunnable anyway? Can't this be a fire-and-forget + // runnable?? + rv.SuppressException(); + return; + } + + nsresult rv; + nsCOMPtr<nsIStringBundleService> bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIStringBundle> strBundle; + rv = bundleService->CreateBundle(aBundleURI, getter_AddRefs(strBundle)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIConsoleService> console( + do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIScriptError> errorObject( + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS_VOID(rv); + + // Localize the error message + nsXPIDLString message; + if (aFormatStrings) { + rv = strBundle->FormatStringFromName(aError, aFormatStrings, + aFormatStringsLen, + getter_Copies(message)); + } else { + rv = strBundle->GetStringFromName(aError, getter_Copies(message)); + } + NS_ENSURE_SUCCESS_VOID(rv); + + if (mInnerWindowID) { + rv = errorObject->InitWithWindowID(message, + NS_ConvertUTF8toUTF16(mScriptFile), + EmptyString(), mScriptLine, + mScriptColumn, + nsIScriptError::errorFlag, "Web Socket", + mInnerWindowID); + } else { + rv = errorObject->Init(message, + NS_ConvertUTF8toUTF16(mScriptFile), + EmptyString(), mScriptLine, mScriptColumn, + nsIScriptError::errorFlag, "Web Socket"); + } + + NS_ENSURE_SUCCESS_VOID(rv); + + // print the error message directly to the JS console + rv = console->LogMessage(errorObject); + NS_ENSURE_SUCCESS_VOID(rv); +} + +namespace { + +class CancelWebSocketRunnable final : public Runnable +{ +public: + CancelWebSocketRunnable(nsIWebSocketChannel* aChannel, uint16_t aReasonCode, + const nsACString& aReasonString) + : mChannel(aChannel) + , mReasonCode(aReasonCode) + , mReasonString(aReasonString) + {} + + NS_IMETHOD Run() override + { + mChannel->Close(mReasonCode, mReasonString); + return NS_OK; + } + +private: + nsCOMPtr<nsIWebSocketChannel> mChannel; + uint16_t mReasonCode; + nsCString mReasonString; +}; + +class MOZ_STACK_CLASS MaybeDisconnect +{ +public: + explicit MaybeDisconnect(WebSocketImpl* aImpl) + : mImpl(aImpl) + { + } + + ~MaybeDisconnect() + { + bool toDisconnect = false; + + { + MutexAutoLock lock(mImpl->mMutex); + toDisconnect = mImpl->mWorkerShuttingDown; + } + + if (toDisconnect) { + mImpl->Disconnect(); + } + } + +private: + WebSocketImpl* mImpl; +}; + +class CloseConnectionRunnable final : public Runnable +{ +public: + CloseConnectionRunnable(WebSocketImpl* aImpl, + uint16_t aReasonCode, + const nsACString& aReasonString) + : mImpl(aImpl) + , mReasonCode(aReasonCode) + , mReasonString(aReasonString) + {} + + NS_IMETHOD Run() override + { + return mImpl->CloseConnection(mReasonCode, mReasonString); + } + +private: + RefPtr<WebSocketImpl> mImpl; + uint16_t mReasonCode; + const nsCString mReasonString; +}; + +} // namespace + +nsresult +WebSocketImpl::CloseConnection(uint16_t aReasonCode, + const nsACString& aReasonString) +{ + if (!IsTargetThread()) { + nsCOMPtr<nsIRunnable> runnable = + new CloseConnectionRunnable(this, aReasonCode, aReasonString); + return Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + } + + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return NS_OK; + } + + // If this method is called because the worker is going away, we will not + // receive the OnStop() method and we have to disconnect the WebSocket and + // release the WorkerHolder. + MaybeDisconnect md(this); + + uint16_t readyState = mWebSocket->ReadyState(); + if (readyState == WebSocket::CLOSING || + readyState == WebSocket::CLOSED) { + return NS_OK; + } + + // The common case... + if (mChannel) { + mWebSocket->SetReadyState(WebSocket::CLOSING); + + // The channel has to be closed on the main-thread. + + if (NS_IsMainThread()) { + return mChannel->Close(aReasonCode, aReasonString); + } + + RefPtr<CancelWebSocketRunnable> runnable = + new CancelWebSocketRunnable(mChannel, aReasonCode, aReasonString); + return NS_DispatchToMainThread(runnable); + } + + // No channel, but not disconnected: canceled or failed early + MOZ_ASSERT(readyState == WebSocket::CONNECTING, + "Should only get here for early websocket cancel/error"); + + // Server won't be sending us a close code, so use what's passed in here. + mCloseEventCode = aReasonCode; + CopyUTF8toUTF16(aReasonString, mCloseEventReason); + + mWebSocket->SetReadyState(WebSocket::CLOSING); + + ScheduleConnectionCloseEvents( + nullptr, + (aReasonCode == nsIWebSocketChannel::CLOSE_NORMAL || + aReasonCode == nsIWebSocketChannel::CLOSE_GOING_AWAY) ? + NS_OK : NS_ERROR_FAILURE); + + return NS_OK; +} + +nsresult +WebSocketImpl::ConsoleError() +{ + AssertIsOnTargetThread(); + + { + MutexAutoLock lock(mMutex); + if (mWorkerShuttingDown) { + // Too late to report anything, bail out. + return NS_OK; + } + } + + NS_ConvertUTF8toUTF16 specUTF16(mURI); + const char16_t* formatStrings[] = { specUTF16.get() }; + + if (mWebSocket->ReadyState() < WebSocket::OPEN) { + PrintErrorOnConsole("chrome://global/locale/appstrings.properties", + u"connectionFailure", + formatStrings, ArrayLength(formatStrings)); + } else { + PrintErrorOnConsole("chrome://global/locale/appstrings.properties", + u"netInterrupt", + formatStrings, ArrayLength(formatStrings)); + } + /// todo some specific errors - like for message too large + return NS_OK; +} + +void +WebSocketImpl::FailConnection(uint16_t aReasonCode, + const nsACString& aReasonString) +{ + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return; + } + + ConsoleError(); + mFailed = true; + CloseConnection(aReasonCode, aReasonString); +} + +namespace { + +class DisconnectInternalRunnable final : public WorkerMainThreadRunnable +{ +public: + explicit DisconnectInternalRunnable(WebSocketImpl* aImpl) + : WorkerMainThreadRunnable(aImpl->mWorkerPrivate, + NS_LITERAL_CSTRING("WebSocket :: disconnect")) + , mImpl(aImpl) + { } + + bool MainThreadRun() override + { + mImpl->DisconnectInternal(); + return true; + } + +private: + // A raw pointer because this runnable is sync. + WebSocketImpl* mImpl; +}; + +} // namespace + +void +WebSocketImpl::Disconnect() +{ + if (mDisconnectingOrDisconnected) { + return; + } + + AssertIsOnTargetThread(); + + // DontKeepAliveAnyMore() and DisconnectInternal() can release the object. So + // hold a reference to this until the end of the method. + RefPtr<WebSocketImpl> kungfuDeathGrip = this; + + // Disconnect can be called from some control event (such as Notify() of + // WorkerHolder). This will be schedulated before any other sync/async + // runnable. In order to prevent some double Disconnect() calls, we use this + // boolean. + mDisconnectingOrDisconnected = true; + + // DisconnectInternal touches observers and nsILoadGroup and it must run on + // the main thread. + + if (NS_IsMainThread()) { + DisconnectInternal(); + } else { + RefPtr<DisconnectInternalRunnable> runnable = + new DisconnectInternalRunnable(this); + ErrorResult rv; + runnable->Dispatch(rv); + // XXXbz this seems totally broken. We should be propagating this out, but + // where to, exactly? + rv.SuppressException(); + } + + NS_ReleaseOnMainThread(mChannel.forget()); + NS_ReleaseOnMainThread(mService.forget()); + + mWebSocket->DontKeepAliveAnyMore(); + mWebSocket->mImpl = nullptr; + + if (mWorkerPrivate && mWorkerHolder) { + UnregisterWorkerHolder(); + } + + // We want to release the WebSocket in the correct thread. + mWebSocket = nullptr; +} + +void +WebSocketImpl::DisconnectInternal() +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsILoadGroup> loadGroup = do_QueryReferent(mWeakLoadGroup); + if (loadGroup) { + loadGroup->RemoveRequest(this, nullptr, NS_OK); + // mWeakLoadGroup has to be release on main-thread because WeakReferences + // are not thread-safe. + mWeakLoadGroup = nullptr; + } + + if (!mWorkerPrivate) { + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->RemoveObserver(this, DOM_WINDOW_DESTROYED_TOPIC); + os->RemoveObserver(this, DOM_WINDOW_FROZEN_TOPIC); + } + } +} + +//----------------------------------------------------------------------------- +// WebSocketImpl::nsIWebSocketListener methods: +//----------------------------------------------------------------------------- + +nsresult +WebSocketImpl::DoOnMessageAvailable(const nsACString& aMsg, bool isBinary) +{ + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return NS_OK; + } + + int16_t readyState = mWebSocket->ReadyState(); + if (readyState == WebSocket::CLOSED) { + NS_ERROR("Received message after CLOSED"); + return NS_ERROR_UNEXPECTED; + } + + if (readyState == WebSocket::OPEN) { + // Dispatch New Message + nsresult rv = mWebSocket->CreateAndDispatchMessageEvent(aMsg, isBinary); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the message event"); + } + + return NS_OK; + } + + // CLOSING should be the only other state where it's possible to get msgs + // from channel: Spec says to drop them. + MOZ_ASSERT(readyState == WebSocket::CLOSING, + "Received message while CONNECTING or CLOSED"); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::OnMessageAvailable(nsISupports* aContext, + const nsACString& aMsg) +{ + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return NS_OK; + } + + return DoOnMessageAvailable(aMsg, false); +} + +NS_IMETHODIMP +WebSocketImpl::OnBinaryMessageAvailable(nsISupports* aContext, + const nsACString& aMsg) +{ + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return NS_OK; + } + + return DoOnMessageAvailable(aMsg, true); +} + +NS_IMETHODIMP +WebSocketImpl::OnStart(nsISupports* aContext) +{ + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return NS_OK; + } + + int16_t readyState = mWebSocket->ReadyState(); + + // This is the only function that sets OPEN, and should be called only once + MOZ_ASSERT(readyState != WebSocket::OPEN, + "readyState already OPEN! OnStart called twice?"); + + // Nothing to do if we've already closed/closing + if (readyState != WebSocket::CONNECTING) { + return NS_OK; + } + + // Attempt to kill "ghost" websocket: but usually too early for check to fail + nsresult rv = mWebSocket->CheckInnerWindowCorrectness(); + if (NS_FAILED(rv)) { + CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY); + return rv; + } + + if (!mRequestedProtocolList.IsEmpty()) { + mChannel->GetProtocol(mWebSocket->mEstablishedProtocol); + } + + mChannel->GetExtensions(mWebSocket->mEstablishedExtensions); + UpdateURI(); + + mWebSocket->SetReadyState(WebSocket::OPEN); + + mService->WebSocketOpened(mChannel->Serial(),mInnerWindowID, + mWebSocket->mEffectiveURL, + mWebSocket->mEstablishedProtocol, + mWebSocket->mEstablishedExtensions); + + // Let's keep the object alive because the webSocket can be CCed in the + // onopen callback. + RefPtr<WebSocket> webSocket = mWebSocket; + + // Call 'onopen' + rv = webSocket->CreateAndDispatchSimpleEvent(NS_LITERAL_STRING("open")); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the open event"); + } + + webSocket->UpdateMustKeepAlive(); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::OnStop(nsISupports* aContext, nsresult aStatusCode) +{ + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return NS_OK; + } + + // We can be CONNECTING here if connection failed. + // We can be OPEN if we have encountered a fatal protocol error + // We can be CLOSING if close() was called and/or server initiated close. + MOZ_ASSERT(mWebSocket->ReadyState() != WebSocket::CLOSED, + "Shouldn't already be CLOSED when OnStop called"); + + return ScheduleConnectionCloseEvents(aContext, aStatusCode); +} + +nsresult +WebSocketImpl::ScheduleConnectionCloseEvents(nsISupports* aContext, + nsresult aStatusCode) +{ + AssertIsOnTargetThread(); + + // no-op if some other code has already initiated close event + if (!mOnCloseScheduled) { + mCloseEventWasClean = NS_SUCCEEDED(aStatusCode); + + if (aStatusCode == NS_BASE_STREAM_CLOSED) { + // don't generate an error event just because of an unclean close + aStatusCode = NS_OK; + } + + if (NS_FAILED(aStatusCode)) { + ConsoleError(); + mFailed = true; + } + + mOnCloseScheduled = true; + + NS_DispatchToCurrentThread(new CallDispatchConnectionCloseEvents(this)); + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::OnAcknowledge(nsISupports *aContext, uint32_t aSize) +{ + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return NS_OK; + } + + if (aSize > mWebSocket->mOutgoingBufferedAmount) { + return NS_ERROR_UNEXPECTED; + } + + mWebSocket->mOutgoingBufferedAmount -= aSize; + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::OnServerClose(nsISupports *aContext, uint16_t aCode, + const nsACString &aReason) +{ + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return NS_OK; + } + + int16_t readyState = mWebSocket->ReadyState(); + + MOZ_ASSERT(readyState != WebSocket::CONNECTING, + "Received server close before connected?"); + MOZ_ASSERT(readyState != WebSocket::CLOSED, + "Received server close after already closed!"); + + // store code/string for onclose DOM event + mCloseEventCode = aCode; + CopyUTF8toUTF16(aReason, mCloseEventReason); + + if (readyState == WebSocket::OPEN) { + // Server initiating close. + // RFC 6455, 5.5.1: "When sending a Close frame in response, the endpoint + // typically echos the status code it received". + // But never send certain codes, per section 7.4.1 + if (aCode == 1005 || aCode == 1006 || aCode == 1015) { + CloseConnection(0, EmptyCString()); + } else { + CloseConnection(aCode, aReason); + } + } else { + // We initiated close, and server has replied: OnStop does rest of the work. + MOZ_ASSERT(readyState == WebSocket::CLOSING, "unknown state"); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// WebSocketImpl::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebSocketImpl::GetInterface(const nsIID& aIID, void** aResult) +{ + AssertIsOnMainThread(); + + if (!mWebSocket || mWebSocket->ReadyState() == WebSocket::CLOSED) { + return NS_ERROR_FAILURE; + } + + if (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) || + aIID.Equals(NS_GET_IID(nsIAuthPrompt2))) { + nsCOMPtr<nsPIDOMWindowInner> win = mWebSocket->GetWindowIfCurrent(); + if (!win) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv; + nsCOMPtr<nsIPromptFactory> wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = win->GetOuterWindow(); + return wwatch->GetPrompt(outerWindow, aIID, aResult); + } + + return QueryInterface(aIID, aResult); +} + +//////////////////////////////////////////////////////////////////////////////// +// WebSocket +//////////////////////////////////////////////////////////////////////////////// + +WebSocket::WebSocket(nsPIDOMWindowInner* aOwnerWindow) + : DOMEventTargetHelper(aOwnerWindow) + , mIsMainThread(true) + , mKeepingAlive(false) + , mCheckMustKeepAlive(true) + , mOutgoingBufferedAmount(0) + , mBinaryType(dom::BinaryType::Blob) + , mMutex("WebSocket::mMutex") + , mReadyState(CONNECTING) +{ + mImpl = new WebSocketImpl(this); + mIsMainThread = mImpl->mIsMainThread; +} + +WebSocket::~WebSocket() +{ +} + +JSObject* +WebSocket::WrapObject(JSContext* cx, JS::Handle<JSObject*> aGivenProto) +{ + return WebSocketBinding::Wrap(cx, this, aGivenProto); +} + +//--------------------------------------------------------------------------- +// WebIDL +//--------------------------------------------------------------------------- + +// Constructor: +already_AddRefed<WebSocket> +WebSocket::Constructor(const GlobalObject& aGlobal, + const nsAString& aUrl, + ErrorResult& aRv) +{ + Sequence<nsString> protocols; + return WebSocket::ConstructorCommon(aGlobal, aUrl, protocols, nullptr, + EmptyCString(), aRv); +} + +already_AddRefed<WebSocket> +WebSocket::Constructor(const GlobalObject& aGlobal, + const nsAString& aUrl, + const nsAString& aProtocol, + ErrorResult& aRv) +{ + Sequence<nsString> protocols; + if (!protocols.AppendElement(aProtocol, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + return WebSocket::ConstructorCommon(aGlobal, aUrl, protocols, nullptr, + EmptyCString(), aRv); +} + +already_AddRefed<WebSocket> +WebSocket::Constructor(const GlobalObject& aGlobal, + const nsAString& aUrl, + const Sequence<nsString>& aProtocols, + ErrorResult& aRv) +{ + return WebSocket::ConstructorCommon(aGlobal, aUrl, aProtocols, nullptr, + EmptyCString(), aRv); +} + +already_AddRefed<WebSocket> +WebSocket::CreateServerWebSocket(const GlobalObject& aGlobal, + const nsAString& aUrl, + const Sequence<nsString>& aProtocols, + nsITransportProvider* aTransportProvider, + const nsAString& aNegotiatedExtensions, + ErrorResult& aRv) +{ + return WebSocket::ConstructorCommon(aGlobal, aUrl, aProtocols, aTransportProvider, + NS_ConvertUTF16toUTF8(aNegotiatedExtensions), aRv); +} + +namespace { + +// This class is used to clear any exception. +class MOZ_STACK_CLASS ClearException +{ +public: + explicit ClearException(JSContext* aCx) + : mCx(aCx) + { + } + + ~ClearException() + { + JS_ClearPendingException(mCx); + } + +private: + JSContext* mCx; +}; + +class WebSocketMainThreadRunnable : public WorkerMainThreadRunnable +{ +public: + WebSocketMainThreadRunnable(WorkerPrivate* aWorkerPrivate, + const nsACString& aTelemetryKey) + : WorkerMainThreadRunnable(aWorkerPrivate, aTelemetryKey) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + bool MainThreadRun() override + { + AssertIsOnMainThread(); + + // Walk up to our containing page + WorkerPrivate* wp = mWorkerPrivate; + while (wp->GetParent()) { + wp = wp->GetParent(); + } + + nsPIDOMWindowInner* window = wp->GetWindow(); + if (window) { + return InitWithWindow(window); + } + + return InitWindowless(wp); + } + +protected: + virtual bool InitWithWindow(nsPIDOMWindowInner* aWindow) = 0; + + virtual bool InitWindowless(WorkerPrivate* aTopLevelWorkerPrivate) = 0; +}; + +class InitRunnable final : public WebSocketMainThreadRunnable +{ +public: + InitRunnable(WebSocketImpl* aImpl, bool aIsServerSide, + const nsAString& aURL, + nsTArray<nsString>& aProtocolArray, + const nsACString& aScriptFile, uint32_t aScriptLine, + uint32_t aScriptColumn, + ErrorResult& aRv, bool* aConnectionFailed) + : WebSocketMainThreadRunnable(aImpl->mWorkerPrivate, + NS_LITERAL_CSTRING("WebSocket :: init")) + , mImpl(aImpl) + , mIsServerSide(aIsServerSide) + , mURL(aURL) + , mProtocolArray(aProtocolArray) + , mScriptFile(aScriptFile) + , mScriptLine(aScriptLine) + , mScriptColumn(aScriptColumn) + , mRv(aRv) + , mConnectionFailed(aConnectionFailed) + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + } + +protected: + virtual bool InitWithWindow(nsPIDOMWindowInner* aWindow) override + { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(aWindow))) { + mRv.Throw(NS_ERROR_FAILURE); + return true; + } + + ClearException ce(jsapi.cx()); + + nsIDocument* doc = aWindow->GetExtantDoc(); + if (!doc) { + mRv.Throw(NS_ERROR_FAILURE); + return true; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + if (!principal) { + mRv.Throw(NS_ERROR_FAILURE); + return true; + } + + mImpl->Init(jsapi.cx(), principal, mIsServerSide, mURL, mProtocolArray, + mScriptFile, mScriptLine, mScriptColumn, mRv, + mConnectionFailed); + return true; + } + + virtual bool InitWindowless(WorkerPrivate* aTopLevelWorkerPrivate) override + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTopLevelWorkerPrivate && !aTopLevelWorkerPrivate->GetWindow()); + + mImpl->Init(nullptr, aTopLevelWorkerPrivate->GetPrincipal(), mIsServerSide, + mURL, mProtocolArray, mScriptFile, mScriptLine, mScriptColumn, + mRv, mConnectionFailed); + return true; + } + + // Raw pointer. This worker runs synchronously. + WebSocketImpl* mImpl; + + bool mIsServerSide; + const nsAString& mURL; + nsTArray<nsString>& mProtocolArray; + nsCString mScriptFile; + uint32_t mScriptLine; + uint32_t mScriptColumn; + ErrorResult& mRv; + bool* mConnectionFailed; +}; + +class AsyncOpenRunnable final : public WebSocketMainThreadRunnable +{ +public: + AsyncOpenRunnable(WebSocketImpl* aImpl, ErrorResult& aRv) + : WebSocketMainThreadRunnable(aImpl->mWorkerPrivate, + NS_LITERAL_CSTRING("WebSocket :: AsyncOpen")) + , mImpl(aImpl) + , mRv(aRv) + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + } + +protected: + virtual bool InitWithWindow(nsPIDOMWindowInner* aWindow) override + { + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + nsIDocument* doc = aWindow->GetExtantDoc(); + if (!doc) { + mRv.Throw(NS_ERROR_FAILURE); + return true; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + if (!principal) { + mRv.Throw(NS_ERROR_FAILURE); + return true; + } + + uint64_t windowID = 0; + nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop(); + nsCOMPtr<nsPIDOMWindowInner> topInner; + if (topWindow) { + topInner = topWindow->GetCurrentInnerWindow(); + } + + if (topInner) { + windowID = topInner->WindowID(); + } + + mImpl->AsyncOpen(principal, windowID, nullptr, EmptyCString(), mRv); + return true; + } + + virtual bool InitWindowless(WorkerPrivate* aTopLevelWorkerPrivate) override + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTopLevelWorkerPrivate && !aTopLevelWorkerPrivate->GetWindow()); + + mImpl->AsyncOpen(aTopLevelWorkerPrivate->GetPrincipal(), 0, nullptr, + EmptyCString(), mRv); + return true; + } + +private: + // Raw pointer. This worker runs synchronously. + WebSocketImpl* mImpl; + + ErrorResult& mRv; +}; + +} // namespace + +already_AddRefed<WebSocket> +WebSocket::ConstructorCommon(const GlobalObject& aGlobal, + const nsAString& aUrl, + const Sequence<nsString>& aProtocols, + nsITransportProvider* aTransportProvider, + const nsACString& aNegotiatedExtensions, + ErrorResult& aRv) +{ + MOZ_ASSERT_IF(!aTransportProvider, aNegotiatedExtensions.IsEmpty()); + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsPIDOMWindowInner> ownerWindow; + + if (NS_IsMainThread()) { + nsCOMPtr<nsIScriptObjectPrincipal> scriptPrincipal = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!scriptPrincipal) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + principal = scriptPrincipal->GetPrincipal(); + if (!principal) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsIScriptGlobalObject> sgo = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!sgo) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + ownerWindow = do_QueryInterface(aGlobal.GetAsSupports()); + if (!ownerWindow) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + } + + MOZ_ASSERT_IF(ownerWindow, ownerWindow->IsInnerWindow()); + + nsTArray<nsString> protocolArray; + + for (uint32_t index = 0, len = aProtocols.Length(); index < len; ++index) { + + const nsString& protocolElement = aProtocols[index]; + + if (protocolElement.IsEmpty()) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + if (protocolArray.Contains(protocolElement)) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + if (protocolElement.FindChar(',') != -1) /* interferes w/list */ { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + + protocolArray.AppendElement(protocolElement); + } + + RefPtr<WebSocket> webSocket = new WebSocket(ownerWindow); + RefPtr<WebSocketImpl> webSocketImpl = webSocket->mImpl; + + bool connectionFailed = true; + + if (NS_IsMainThread()) { + webSocketImpl->Init(aGlobal.Context(), principal, !!aTransportProvider, + aUrl, protocolArray, EmptyCString(), + 0, 0, aRv, &connectionFailed); + } else { + // In workers we have to keep the worker alive using a workerHolder in order + // to dispatch messages correctly. + if (!webSocketImpl->RegisterWorkerHolder()) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + unsigned lineno, column; + JS::AutoFilename file; + if (!JS::DescribeScriptedCaller(aGlobal.Context(), &file, &lineno, + &column)) { + NS_WARNING("Failed to get line number and filename in workers."); + } + + RefPtr<InitRunnable> runnable = + new InitRunnable(webSocketImpl, !!aTransportProvider, aUrl, + protocolArray, nsDependentCString(file.get()), lineno, + column, aRv, &connectionFailed); + runnable->Dispatch(aRv); + } + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // It can be that we have been already disconnected because the WebSocket is + // gone away while we where initializing the webSocket. + if (!webSocket->mImpl) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // We don't return an error if the connection just failed. Instead we dispatch + // an event. + if (connectionFailed) { + webSocket->mImpl->FailConnection(nsIWebSocketChannel::CLOSE_ABNORMAL); + } + + // If we don't have a channel, the connection is failed and onerror() will be + // called asynchrounsly. + if (!webSocket->mImpl->mChannel) { + return webSocket.forget(); + } + + class MOZ_STACK_CLASS ClearWebSocket + { + public: + explicit ClearWebSocket(WebSocketImpl* aWebSocketImpl) + : mWebSocketImpl(aWebSocketImpl) + , mDone(false) + { + } + + void Done() + { + mDone = true; + } + + ~ClearWebSocket() + { + if (!mDone) { + mWebSocketImpl->mChannel = nullptr; + mWebSocketImpl->FailConnection(nsIWebSocketChannel::CLOSE_ABNORMAL); + } + } + + WebSocketImpl* mWebSocketImpl; + bool mDone; + }; + + ClearWebSocket cws(webSocket->mImpl); + + // This operation must be done on the correct thread. The rest must run on the + // main-thread. + aRv = webSocket->mImpl->mChannel->SetNotificationCallbacks(webSocket->mImpl); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (NS_IsMainThread()) { + MOZ_ASSERT(principal); + + nsPIDOMWindowOuter* outerWindow = ownerWindow->GetOuterWindow(); + + uint64_t windowID = 0; + nsCOMPtr<nsPIDOMWindowOuter> topWindow = outerWindow->GetScriptableTop(); + nsCOMPtr<nsPIDOMWindowInner> topInner; + if (topWindow) { + topInner = topWindow->GetCurrentInnerWindow(); + } + + if (topInner) { + windowID = topInner->WindowID(); + } + + webSocket->mImpl->AsyncOpen(principal, windowID, aTransportProvider, + aNegotiatedExtensions, aRv); + } else { + MOZ_ASSERT(!aTransportProvider && aNegotiatedExtensions.IsEmpty(), + "not yet implemented"); + RefPtr<AsyncOpenRunnable> runnable = + new AsyncOpenRunnable(webSocket->mImpl, aRv); + runnable->Dispatch(aRv); + } + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // It can be that we have been already disconnected because the WebSocket is + // gone away while we where initializing the webSocket. + if (!webSocket->mImpl) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // Let's inform devtools about this new active WebSocket. + webSocket->mImpl->mService->WebSocketCreated(webSocket->mImpl->mChannel->Serial(), + webSocket->mImpl->mInnerWindowID, + webSocket->mURI, + webSocket->mImpl->mRequestedProtocolList); + + cws.Done(); + return webSocket.forget(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(WebSocket) + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(WebSocket) + bool isBlack = tmp->IsBlack(); + if (isBlack || tmp->mKeepingAlive) { + if (tmp->mListenerManager) { + tmp->mListenerManager->MarkForCC(); + } + if (!isBlack && tmp->PreservingWrapper()) { + // This marks the wrapper black. + tmp->GetWrapper(); + } + return true; + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(WebSocket) + return tmp->IsBlack(); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(WebSocket) + return tmp->IsBlack(); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(WebSocket, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebSocket, + DOMEventTargetHelper) + if (tmp->mImpl) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImpl->mChannel) + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WebSocket, + DOMEventTargetHelper) + if (tmp->mImpl) { + NS_IMPL_CYCLE_COLLECTION_UNLINK(mImpl->mChannel) + tmp->mImpl->Disconnect(); + MOZ_ASSERT(!tmp->mImpl); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(WebSocket) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(WebSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(WebSocket, DOMEventTargetHelper) + +void +WebSocket::DisconnectFromOwner() +{ + AssertIsOnMainThread(); + DOMEventTargetHelper::DisconnectFromOwner(); + + if (mImpl) { + mImpl->CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY); + } + + DontKeepAliveAnyMore(); +} + +//----------------------------------------------------------------------------- +// WebSocketImpl:: initialization +//----------------------------------------------------------------------------- + +void +WebSocketImpl::Init(JSContext* aCx, + nsIPrincipal* aPrincipal, + bool aIsServerSide, + const nsAString& aURL, + nsTArray<nsString>& aProtocolArray, + const nsACString& aScriptFile, + uint32_t aScriptLine, + uint32_t aScriptColumn, + ErrorResult& aRv, + bool* aConnectionFailed) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + mService = WebSocketEventService::GetOrCreate(); + + // We need to keep the implementation alive in case the init disconnects it + // because of some error. + RefPtr<WebSocketImpl> kungfuDeathGrip = this; + + // Attempt to kill "ghost" websocket: but usually too early for check to fail + aRv = mWebSocket->CheckInnerWindowCorrectness(); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // Shut down websocket if window is frozen or destroyed (only needed for + // "ghost" websockets--see bug 696085) + if (!mWorkerPrivate) { + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!os)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + aRv = os->AddObserver(this, DOM_WINDOW_DESTROYED_TOPIC, true); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aRv = os->AddObserver(this, DOM_WINDOW_FROZEN_TOPIC, true); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + if (mWorkerPrivate) { + mScriptFile = aScriptFile; + mScriptLine = aScriptLine; + mScriptColumn = aScriptColumn; + } else { + MOZ_ASSERT(aCx); + + unsigned lineno, column; + JS::AutoFilename file; + if (JS::DescribeScriptedCaller(aCx, &file, &lineno, &column)) { + mScriptFile = file.get(); + mScriptLine = lineno; + mScriptColumn = column; + } + } + + mIsServerSide = aIsServerSide; + + // If we don't have aCx, we are window-less, so we don't have a + // inner-windowID. This can happen in sharedWorkers and ServiceWorkers or in + // DedicateWorkers created by JSM. + if (aCx) { + mInnerWindowID = nsJSUtils::GetCurrentlyRunningCodeInnerWindowID(aCx); + } + + // parses the url + aRv = ParseURL(PromiseFlatString(aURL)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsIDocument> originDoc = mWebSocket->GetDocumentIfCurrent(); + if (!originDoc) { + nsresult rv = mWebSocket->CheckInnerWindowCorrectness(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + } + mOriginDocument = do_GetWeakReference(originDoc); + + if (!mIsServerSide) { + nsCOMPtr<nsIURI> uri; + { + nsresult rv = NS_NewURI(getter_AddRefs(uri), mURI); + + // We crash here because we are sure that mURI is a valid URI, so either we + // are OOM'ing or something else bad is happening. + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_CRASH(); + } + } + + // The 'real' nsHttpChannel of the websocket gets opened in the parent. + // Since we don't serialize the CSP within child and parent and also not + // the context, we have to perform content policy checks here instead of + // AsyncOpen2(). + // Please note that websockets can't follow redirects, hence there is no + // need to perform a CSP check after redirects. + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + aRv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_WEBSOCKET, + uri, + aPrincipal, + originDoc, + EmptyCString(), + nullptr, + &shouldLoad, + nsContentUtils::GetContentPolicy(), + nsContentUtils::GetSecurityManager()); + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (NS_CP_REJECTED(shouldLoad)) { + // Disallowed by content policy + aRv.Throw(NS_ERROR_CONTENT_BLOCKED); + return; + } + } + + // Potentially the page uses the CSP directive 'upgrade-insecure-requests'. + // In such a case we have to upgrade ws: to wss: and also update mSecure + // to reflect that upgrade. Please note that we can not upgrade from ws: + // to wss: before performing content policy checks because CSP needs to + // send reports in case the scheme is about to be upgraded. + if (!mIsServerSide && !mSecure && originDoc && + originDoc->GetUpgradeInsecureRequests(false)) { + // let's use the old specification before the upgrade for logging + NS_ConvertUTF8toUTF16 reportSpec(mURI); + + // upgrade the request from ws:// to wss:// and mark as secure + mURI.ReplaceSubstring("ws://", "wss://"); + if (NS_WARN_IF(mURI.Find("wss://") != 0)) { + return; + } + mSecure = true; + + const char16_t* params[] = { reportSpec.get(), u"wss" }; + CSP_LogLocalizedStr(u"upgradeInsecureRequest", + params, ArrayLength(params), + EmptyString(), // aSourceFile + EmptyString(), // aScriptSample + 0, // aLineNumber + 0, // aColumnNumber + nsIScriptError::warningFlag, "CSP", + mInnerWindowID); + } + + // Don't allow https:// to open ws:// + if (!mIsServerSide && !mSecure && + !Preferences::GetBool("network.websocket.allowInsecureFromHTTPS", + false)) { + // Confirmed we are opening plain ws:// and want to prevent this from a + // secure context (e.g. https). + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIURI> originURI; + if (mWorkerPrivate) { + // For workers, retrieve the URI from the WorkerPrivate + principal = mWorkerPrivate->GetPrincipal(); + } else { + // Check the principal's uri to determine if we were loaded from https. + nsCOMPtr<nsIGlobalObject> globalObject(GetEntryGlobal()); + if (globalObject) { + principal = globalObject->PrincipalOrNull(); + } + + nsCOMPtr<nsPIDOMWindowInner> innerWindow; + + while (true) { + if (principal && !principal->GetIsNullPrincipal()) { + break; + } + + if (!innerWindow) { + innerWindow = do_QueryInterface(globalObject); + if (!innerWindow) { + // If we are in a XPConnect sandbox or in a JS component, + // innerWindow will be null. There is nothing on top of this to be + // considered. + break; + } + } + + nsCOMPtr<nsPIDOMWindowOuter> parentWindow = + innerWindow->GetScriptableParent(); + if (NS_WARN_IF(!parentWindow)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsCOMPtr<nsPIDOMWindowInner> currentInnerWindow = + parentWindow->GetCurrentInnerWindow(); + if (NS_WARN_IF(!currentInnerWindow)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + // We are at the top. Let's see if we have an opener window. + if (innerWindow == currentInnerWindow) { + ErrorResult error; + parentWindow = + nsGlobalWindow::Cast(innerWindow)->GetOpenerWindow(error); + if (NS_WARN_IF(error.Failed())) { + error.SuppressException(); + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + if (!parentWindow) { + break; + } + + currentInnerWindow = parentWindow->GetCurrentInnerWindow(); + if (NS_WARN_IF(!currentInnerWindow)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + MOZ_ASSERT(currentInnerWindow != innerWindow); + } + + innerWindow = currentInnerWindow; + + nsCOMPtr<nsIDocument> document = innerWindow->GetExtantDoc(); + if (NS_WARN_IF(!document)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + principal = document->NodePrincipal(); + } + } + + if (principal) { + principal->GetURI(getter_AddRefs(originURI)); + } + + if (originURI) { + bool originIsHttps = false; + aRv = originURI->SchemeIs("https", &originIsHttps); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + if (originIsHttps) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + } + } + + // Assign the sub protocol list and scan it for illegal values + for (uint32_t index = 0; index < aProtocolArray.Length(); ++index) { + for (uint32_t i = 0; i < aProtocolArray[index].Length(); ++i) { + if (aProtocolArray[index][i] < static_cast<char16_t>(0x0021) || + aProtocolArray[index][i] > static_cast<char16_t>(0x007E)) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return; + } + } + + if (!mRequestedProtocolList.IsEmpty()) { + mRequestedProtocolList.AppendLiteral(", "); + } + + AppendUTF16toUTF8(aProtocolArray[index], mRequestedProtocolList); + } + + // the constructor should throw a SYNTAX_ERROR only if it fails to parse the + // url parameter, so don't throw if InitializeConnection fails, and call + // onerror/onclose asynchronously + if (NS_FAILED(InitializeConnection(aPrincipal))) { + *aConnectionFailed = true; + } else { + *aConnectionFailed = false; + } +} + +void +WebSocketImpl::AsyncOpen(nsIPrincipal* aPrincipal, uint64_t aInnerWindowID, + nsITransportProvider* aTransportProvider, + const nsACString& aNegotiatedExtensions, + ErrorResult& aRv) +{ + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + MOZ_ASSERT_IF(!aTransportProvider, aNegotiatedExtensions.IsEmpty()); + + nsCString asciiOrigin; + aRv = nsContentUtils::GetASCIIOrigin(aPrincipal, asciiOrigin); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (aTransportProvider) { + aRv = mChannel->SetServerParameters(aTransportProvider, aNegotiatedExtensions); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + ToLowerCase(asciiOrigin); + + nsCOMPtr<nsIURI> uri; + if (!aTransportProvider) { + aRv = NS_NewURI(getter_AddRefs(uri), mURI); + MOZ_ASSERT(!aRv.Failed()); + } + + aRv = mChannel->AsyncOpen(uri, asciiOrigin, aInnerWindowID, this, nullptr); + if (NS_WARN_IF(aRv.Failed())) { + aRv.Throw(NS_ERROR_CONTENT_BLOCKED); + return; + } + + mInnerWindowID = aInnerWindowID; +} + +//----------------------------------------------------------------------------- +// WebSocketImpl methods: +//----------------------------------------------------------------------------- + +class nsAutoCloseWS final +{ +public: + explicit nsAutoCloseWS(WebSocketImpl* aWebSocketImpl) + : mWebSocketImpl(aWebSocketImpl) + {} + + ~nsAutoCloseWS() + { + if (!mWebSocketImpl->mChannel) { + mWebSocketImpl->CloseConnection(nsIWebSocketChannel::CLOSE_INTERNAL_ERROR); + } + } +private: + RefPtr<WebSocketImpl> mWebSocketImpl; +}; + +nsresult +WebSocketImpl::InitializeConnection(nsIPrincipal* aPrincipal) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!mChannel, "mChannel should be null"); + + nsCOMPtr<nsIWebSocketChannel> wsChannel; + nsAutoCloseWS autoClose(this); + nsresult rv; + + if (mSecure) { + wsChannel = + do_CreateInstance("@mozilla.org/network/protocol;1?name=wss", &rv); + } else { + wsChannel = + do_CreateInstance("@mozilla.org/network/protocol;1?name=ws", &rv); + } + NS_ENSURE_SUCCESS(rv, rv); + + // add ourselves to the document's load group and + // provide the http stack the loadgroup info too + nsCOMPtr<nsILoadGroup> loadGroup; + rv = GetLoadGroup(getter_AddRefs(loadGroup)); + if (loadGroup) { + rv = wsChannel->SetLoadGroup(loadGroup); + NS_ENSURE_SUCCESS(rv, rv); + rv = loadGroup->AddRequest(this, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + mWeakLoadGroup = do_GetWeakReference(loadGroup); + } + + // manually adding loadinfo to the channel since it + // was not set during channel creation. + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mOriginDocument); + + // mOriginDocument has to be release on main-thread because WeakReferences + // are not thread-safe. + mOriginDocument = nullptr; + + + // The TriggeringPrincipal for websockets must always be a script. + // Let's make sure that the doc's principal (if a doc exists) + // and aPrincipal are same origin. + MOZ_ASSERT(!doc || doc->NodePrincipal()->Equals(aPrincipal)); + + wsChannel->InitLoadInfo(doc ? doc->AsDOMNode() : nullptr, + doc ? doc->NodePrincipal() : aPrincipal, + aPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_WEBSOCKET); + + if (!mRequestedProtocolList.IsEmpty()) { + rv = wsChannel->SetProtocol(mRequestedProtocolList); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(wsChannel); + NS_ENSURE_TRUE(rr, NS_ERROR_FAILURE); + + rv = rr->RetargetDeliveryTo(this); + NS_ENSURE_SUCCESS(rv, rv); + + mChannel = wsChannel; + + return NS_OK; +} + +void +WebSocketImpl::DispatchConnectionCloseEvents() +{ + AssertIsOnTargetThread(); + + if (mDisconnectingOrDisconnected) { + return; + } + + mWebSocket->SetReadyState(WebSocket::CLOSED); + + // Let's keep the object alive because the webSocket can be CCed in the + // onerror or in the onclose callback. + RefPtr<WebSocket> webSocket = mWebSocket; + + // Call 'onerror' if needed + if (mFailed) { + nsresult rv = + webSocket->CreateAndDispatchSimpleEvent(NS_LITERAL_STRING("error")); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the error event"); + } + } + + nsresult rv = webSocket->CreateAndDispatchCloseEvent(mCloseEventWasClean, + mCloseEventCode, + mCloseEventReason); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the close event"); + } + + webSocket->UpdateMustKeepAlive(); + Disconnect(); +} + +nsresult +WebSocket::CreateAndDispatchSimpleEvent(const nsAString& aName) +{ + MOZ_ASSERT(mImpl); + AssertIsOnTargetThread(); + + nsresult rv = CheckInnerWindowCorrectness(); + if (NS_FAILED(rv)) { + return NS_OK; + } + + RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr); + + // it doesn't bubble, and it isn't cancelable + event->InitEvent(aName, false, false); + event->SetTrusted(true); + + return DispatchDOMEvent(nullptr, event, nullptr, nullptr); +} + +nsresult +WebSocket::CreateAndDispatchMessageEvent(const nsACString& aData, + bool aIsBinary) +{ + MOZ_ASSERT(mImpl); + AssertIsOnTargetThread(); + + AutoJSAPI jsapi; + + if (NS_IsMainThread()) { + if (NS_WARN_IF(!jsapi.Init(GetOwner()))) { + return NS_ERROR_FAILURE; + } + } else { + MOZ_ASSERT(!mIsMainThread); + MOZ_ASSERT(mImpl->mWorkerPrivate); + if (NS_WARN_IF(!jsapi.Init(mImpl->mWorkerPrivate->GlobalScope()))) { + return NS_ERROR_FAILURE; + } + } + + JSContext* cx = jsapi.cx(); + + nsresult rv = CheckInnerWindowCorrectness(); + if (NS_FAILED(rv)) { + return NS_OK; + } + + uint16_t messageType = nsIWebSocketEventListener::TYPE_STRING; + + // Create appropriate JS object for message + JS::Rooted<JS::Value> jsData(cx); + if (aIsBinary) { + if (mBinaryType == dom::BinaryType::Blob) { + messageType = nsIWebSocketEventListener::TYPE_BLOB; + + RefPtr<Blob> blob = + Blob::CreateStringBlob(GetOwner(), aData, EmptyString()); + MOZ_ASSERT(blob); + + if (!ToJSValue(cx, blob, &jsData)) { + return NS_ERROR_FAILURE; + } + + } else if (mBinaryType == dom::BinaryType::Arraybuffer) { + messageType = nsIWebSocketEventListener::TYPE_ARRAYBUFFER; + + JS::Rooted<JSObject*> arrayBuf(cx); + nsresult rv = nsContentUtils::CreateArrayBuffer(cx, aData, + arrayBuf.address()); + NS_ENSURE_SUCCESS(rv, rv); + jsData.setObject(*arrayBuf); + } else { + NS_RUNTIMEABORT("Unknown binary type!"); + return NS_ERROR_UNEXPECTED; + } + } else { + // JS string + NS_ConvertUTF8toUTF16 utf16Data(aData); + JSString* jsString; + jsString = JS_NewUCStringCopyN(cx, utf16Data.get(), utf16Data.Length()); + NS_ENSURE_TRUE(jsString, NS_ERROR_FAILURE); + + jsData.setString(jsString); + } + + mImpl->mService->WebSocketMessageAvailable(mImpl->mChannel->Serial(), + mImpl->mInnerWindowID, + aData, messageType); + + // create an event that uses the MessageEvent interface, + // which does not bubble, is not cancelable, and has no default action + + RefPtr<MessageEvent> event = new MessageEvent(this, nullptr, nullptr); + + event->InitMessageEvent(nullptr, NS_LITERAL_STRING("message"), false, false, + jsData, mImpl->mUTF16Origin, EmptyString(), nullptr, + Sequence<OwningNonNull<MessagePort>>()); + event->SetTrusted(true); + + return DispatchDOMEvent(nullptr, static_cast<Event*>(event), nullptr, + nullptr); +} + +nsresult +WebSocket::CreateAndDispatchCloseEvent(bool aWasClean, + uint16_t aCode, + const nsAString& aReason) +{ + AssertIsOnTargetThread(); + + // This method is called by a runnable and it can happen that, in the + // meantime, GC unlinked this object, so mImpl could be null. + if (mImpl && mImpl->mChannel) { + mImpl->mService->WebSocketClosed(mImpl->mChannel->Serial(), + mImpl->mInnerWindowID, + aWasClean, aCode, aReason); + } + + nsresult rv = CheckInnerWindowCorrectness(); + if (NS_FAILED(rv)) { + return NS_OK; + } + + CloseEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mWasClean = aWasClean; + init.mCode = aCode; + init.mReason = aReason; + + RefPtr<CloseEvent> event = + CloseEvent::Constructor(this, NS_LITERAL_STRING("close"), init); + event->SetTrusted(true); + + return DispatchDOMEvent(nullptr, event, nullptr, nullptr); +} + +nsresult +WebSocketImpl::ParseURL(const nsAString& aURL) +{ + AssertIsOnMainThread(); + NS_ENSURE_TRUE(!aURL.IsEmpty(), NS_ERROR_DOM_SYNTAX_ERR); + + if (mIsServerSide) { + mWebSocket->mURI = aURL; + CopyUTF16toUTF8(mWebSocket->mURI, mURI); + + return NS_OK; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); + + nsCOMPtr<nsIURL> parsedURL = do_QueryInterface(uri, &rv); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); + + bool hasRef; + rv = parsedURL->GetHasRef(&hasRef); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && !hasRef, + NS_ERROR_DOM_SYNTAX_ERR); + + nsAutoCString scheme; + rv = parsedURL->GetScheme(scheme); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && !scheme.IsEmpty(), + NS_ERROR_DOM_SYNTAX_ERR); + + nsAutoCString host; + rv = parsedURL->GetAsciiHost(host); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && !host.IsEmpty(), NS_ERROR_DOM_SYNTAX_ERR); + + int32_t port; + rv = parsedURL->GetPort(&port); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); + + rv = NS_CheckPortSafety(port, scheme.get()); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SECURITY_ERR); + + nsAutoCString filePath; + rv = parsedURL->GetFilePath(filePath); + if (filePath.IsEmpty()) { + filePath.Assign('/'); + } + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); + + nsAutoCString query; + rv = parsedURL->GetQuery(query); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); + + if (scheme.LowerCaseEqualsLiteral("ws")) { + mSecure = false; + mPort = (port == -1) ? DEFAULT_WS_SCHEME_PORT : port; + } else if (scheme.LowerCaseEqualsLiteral("wss")) { + mSecure = true; + mPort = (port == -1) ? DEFAULT_WSS_SCHEME_PORT : port; + } else { + return NS_ERROR_DOM_SYNTAX_ERR; + } + + rv = nsContentUtils::GetUTFOrigin(parsedURL, mUTF16Origin); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); + + mAsciiHost = host; + ToLowerCase(mAsciiHost); + + mResource = filePath; + if (!query.IsEmpty()) { + mResource.Append('?'); + mResource.Append(query); + } + uint32_t length = mResource.Length(); + uint32_t i; + for (i = 0; i < length; ++i) { + if (mResource[i] < static_cast<char16_t>(0x0021) || + mResource[i] > static_cast<char16_t>(0x007E)) { + return NS_ERROR_DOM_SYNTAX_ERR; + } + } + + rv = parsedURL->GetSpec(mURI); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + CopyUTF8toUTF16(mURI, mWebSocket->mURI); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// Methods that keep alive the WebSocket object when: +// 1. the object has registered event listeners that can be triggered +// ("strong event listeners"); +// 2. there are outgoing not sent messages. +//----------------------------------------------------------------------------- + +void +WebSocket::UpdateMustKeepAlive() +{ + // Here we could not have mImpl. + MOZ_ASSERT(NS_IsMainThread() == mIsMainThread); + + if (!mCheckMustKeepAlive || !mImpl) { + return; + } + + bool shouldKeepAlive = false; + uint16_t readyState = ReadyState(); + + if (mListenerManager) { + switch (readyState) + { + case CONNECTING: + { + if (mListenerManager->HasListenersFor(nsGkAtoms::onopen) || + mListenerManager->HasListenersFor(nsGkAtoms::onmessage) || + mListenerManager->HasListenersFor(nsGkAtoms::onerror) || + mListenerManager->HasListenersFor(nsGkAtoms::onclose)) { + shouldKeepAlive = true; + } + } + break; + + case OPEN: + case CLOSING: + { + if (mListenerManager->HasListenersFor(nsGkAtoms::onmessage) || + mListenerManager->HasListenersFor(nsGkAtoms::onerror) || + mListenerManager->HasListenersFor(nsGkAtoms::onclose) || + mOutgoingBufferedAmount != 0) { + shouldKeepAlive = true; + } + } + break; + + case CLOSED: + { + shouldKeepAlive = false; + } + } + } + + if (mKeepingAlive && !shouldKeepAlive) { + mKeepingAlive = false; + mImpl->ReleaseObject(); + } else if (!mKeepingAlive && shouldKeepAlive) { + mKeepingAlive = true; + mImpl->AddRefObject(); + } +} + +void +WebSocket::DontKeepAliveAnyMore() +{ + // Here we could not have mImpl. + MOZ_ASSERT(NS_IsMainThread() == mIsMainThread); + + if (mKeepingAlive) { + MOZ_ASSERT(mImpl); + + mKeepingAlive = false; + mImpl->ReleaseObject(); + } + + mCheckMustKeepAlive = false; +} + +namespace { + +class WebSocketWorkerHolder final : public WorkerHolder +{ +public: + explicit WebSocketWorkerHolder(WebSocketImpl* aWebSocketImpl) + : mWebSocketImpl(aWebSocketImpl) + { + } + + bool Notify(Status aStatus) override + { + MOZ_ASSERT(aStatus > workers::Running); + + if (aStatus >= Canceling) { + { + MutexAutoLock lock(mWebSocketImpl->mMutex); + mWebSocketImpl->mWorkerShuttingDown = true; + } + + mWebSocketImpl->CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY, + EmptyCString()); + } + + return true; + } + +private: + WebSocketImpl* mWebSocketImpl; +}; + +} // namespace + +void +WebSocketImpl::AddRefObject() +{ + AssertIsOnTargetThread(); + AddRef(); +} + +void +WebSocketImpl::ReleaseObject() +{ + AssertIsOnTargetThread(); + Release(); +} + +bool +WebSocketImpl::RegisterWorkerHolder() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!mWorkerHolder); + mWorkerHolder = new WebSocketWorkerHolder(this); + + if (NS_WARN_IF(!mWorkerHolder->HoldWorker(mWorkerPrivate, Canceling))) { + mWorkerHolder = nullptr; + return false; + } + +#ifdef DEBUG + SetHasWorkerHolderRegistered(true); +#endif + + return true; +} + +void +WebSocketImpl::UnregisterWorkerHolder() +{ + MOZ_ASSERT(mDisconnectingOrDisconnected); + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerHolder); + + { + MutexAutoLock lock(mMutex); + mWorkerShuttingDown = true; + } + + // The DTOR of this WorkerHolder will release the worker for us. + mWorkerHolder = nullptr; + + mWorkerPrivate = nullptr; + +#ifdef DEBUG + SetHasWorkerHolderRegistered(false); +#endif +} + +nsresult +WebSocketImpl::UpdateURI() +{ + AssertIsOnTargetThread(); + + // Check for Redirections + RefPtr<BaseWebSocketChannel> channel; + channel = static_cast<BaseWebSocketChannel*>(mChannel.get()); + MOZ_ASSERT(channel); + + channel->GetEffectiveURL(mWebSocket->mEffectiveURL); + mSecure = channel->IsEncrypted(); + + return NS_OK; +} + +void +WebSocket::EventListenerAdded(nsIAtom* aType) +{ + AssertIsOnMainThread(); + UpdateMustKeepAlive(); +} + +void +WebSocket::EventListenerRemoved(nsIAtom* aType) +{ + AssertIsOnMainThread(); + UpdateMustKeepAlive(); +} + +//----------------------------------------------------------------------------- +// WebSocket - methods +//----------------------------------------------------------------------------- + +// webIDL: readonly attribute unsigned short readyState; +uint16_t +WebSocket::ReadyState() +{ + MutexAutoLock lock(mMutex); + return mReadyState; +} + +void +WebSocket::SetReadyState(uint16_t aReadyState) +{ + MutexAutoLock lock(mMutex); + mReadyState = aReadyState; +} + +// webIDL: readonly attribute unsigned long bufferedAmount; +uint32_t +WebSocket::BufferedAmount() const +{ + AssertIsOnTargetThread(); + return mOutgoingBufferedAmount; +} + +// webIDL: attribute BinaryType binaryType; +dom::BinaryType +WebSocket::BinaryType() const +{ + AssertIsOnTargetThread(); + return mBinaryType; +} + +// webIDL: attribute BinaryType binaryType; +void +WebSocket::SetBinaryType(dom::BinaryType aData) +{ + AssertIsOnTargetThread(); + mBinaryType = aData; +} + +// webIDL: readonly attribute DOMString url +void +WebSocket::GetUrl(nsAString& aURL) +{ + AssertIsOnTargetThread(); + + if (mEffectiveURL.IsEmpty()) { + aURL = mURI; + } else { + aURL = mEffectiveURL; + } +} + +// webIDL: readonly attribute DOMString extensions; +void +WebSocket::GetExtensions(nsAString& aExtensions) +{ + AssertIsOnTargetThread(); + CopyUTF8toUTF16(mEstablishedExtensions, aExtensions); +} + +// webIDL: readonly attribute DOMString protocol; +void +WebSocket::GetProtocol(nsAString& aProtocol) +{ + AssertIsOnTargetThread(); + CopyUTF8toUTF16(mEstablishedProtocol, aProtocol); +} + +// webIDL: void send(DOMString data); +void +WebSocket::Send(const nsAString& aData, + ErrorResult& aRv) +{ + AssertIsOnTargetThread(); + + NS_ConvertUTF16toUTF8 msgString(aData); + Send(nullptr, msgString, msgString.Length(), false, aRv); +} + +void +WebSocket::Send(Blob& aData, ErrorResult& aRv) +{ + AssertIsOnTargetThread(); + + nsCOMPtr<nsIInputStream> msgStream; + aData.GetInternalStream(getter_AddRefs(msgStream), aRv); + if (NS_WARN_IF(aRv.Failed())){ + return; + } + + uint64_t msgLength = aData.GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())){ + return; + } + + if (msgLength > UINT32_MAX) { + aRv.Throw(NS_ERROR_FILE_TOO_BIG); + return; + } + + Send(msgStream, EmptyCString(), msgLength, true, aRv); +} + +void +WebSocket::Send(const ArrayBuffer& aData, + ErrorResult& aRv) +{ + AssertIsOnTargetThread(); + + aData.ComputeLengthAndData(); + + static_assert(sizeof(*aData.Data()) == 1, "byte-sized data required"); + + uint32_t len = aData.Length(); + char* data = reinterpret_cast<char*>(aData.Data()); + + nsDependentCSubstring msgString(data, len); + Send(nullptr, msgString, len, true, aRv); +} + +void +WebSocket::Send(const ArrayBufferView& aData, + ErrorResult& aRv) +{ + AssertIsOnTargetThread(); + + aData.ComputeLengthAndData(); + + static_assert(sizeof(*aData.Data()) == 1, "byte-sized data required"); + + uint32_t len = aData.Length(); + char* data = reinterpret_cast<char*>(aData.Data()); + + nsDependentCSubstring msgString(data, len); + Send(nullptr, msgString, len, true, aRv); +} + +void +WebSocket::Send(nsIInputStream* aMsgStream, + const nsACString& aMsgString, + uint32_t aMsgLength, + bool aIsBinary, + ErrorResult& aRv) +{ + AssertIsOnTargetThread(); + + int64_t readyState = ReadyState(); + if (readyState == CONNECTING) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // Always increment outgoing buffer len, even if closed + CheckedUint32 size = mOutgoingBufferedAmount; + size += aMsgLength; + if (!size.isValid()) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + mOutgoingBufferedAmount = size.value(); + + if (readyState == CLOSING || + readyState == CLOSED) { + return; + } + + // We must have mImpl when connected. + MOZ_ASSERT(mImpl); + MOZ_ASSERT(readyState == OPEN, "Unknown state in WebSocket::Send"); + + nsresult rv; + if (aMsgStream) { + rv = mImpl->mChannel->SendBinaryStream(aMsgStream, aMsgLength); + } else { + if (aIsBinary) { + rv = mImpl->mChannel->SendBinaryMsg(aMsgString); + } else { + rv = mImpl->mChannel->SendMsg(aMsgString); + } + } + + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + UpdateMustKeepAlive(); +} + +// webIDL: void close(optional unsigned short code, optional DOMString reason): +void +WebSocket::Close(const Optional<uint16_t>& aCode, + const Optional<nsAString>& aReason, + ErrorResult& aRv) +{ + AssertIsOnTargetThread(); + + // the reason code is optional, but if provided it must be in a specific range + uint16_t closeCode = 0; + if (aCode.WasPassed()) { + if (aCode.Value() != 1000 && (aCode.Value() < 3000 || aCode.Value() > 4999)) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return; + } + closeCode = aCode.Value(); + } + + nsCString closeReason; + if (aReason.WasPassed()) { + CopyUTF16toUTF8(aReason.Value(), closeReason); + + // The API requires the UTF-8 string to be 123 or less bytes + if (closeReason.Length() > 123) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return; + } + } + + int64_t readyState = ReadyState(); + if (readyState == CLOSING || + readyState == CLOSED) { + return; + } + + // If the webSocket is not closed we MUST have a mImpl. + MOZ_ASSERT(mImpl); + + if (readyState == CONNECTING) { + mImpl->FailConnection(closeCode, closeReason); + return; + } + + MOZ_ASSERT(readyState == OPEN); + mImpl->CloseConnection(closeCode, closeReason); +} + +//----------------------------------------------------------------------------- +// WebSocketImpl::nsIObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebSocketImpl::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + AssertIsOnMainThread(); + + int64_t readyState = mWebSocket->ReadyState(); + if ((readyState == WebSocket::CLOSING) || + (readyState == WebSocket::CLOSED)) { + return NS_OK; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aSubject); + if (!mWebSocket->GetOwner() || window != mWebSocket->GetOwner()) { + return NS_OK; + } + + if ((strcmp(aTopic, DOM_WINDOW_FROZEN_TOPIC) == 0) || + (strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) == 0)) + { + CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// WebSocketImpl::nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebSocketImpl::GetName(nsACString& aName) +{ + AssertIsOnMainThread(); + + CopyUTF16toUTF8(mWebSocket->mURI, aName); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::IsPending(bool* aValue) +{ + AssertIsOnTargetThread(); + + int64_t readyState = mWebSocket->ReadyState(); + *aValue = (readyState != WebSocket::CLOSED); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::GetStatus(nsresult* aStatus) +{ + AssertIsOnTargetThread(); + + *aStatus = NS_OK; + return NS_OK; +} + +namespace { + +class CancelRunnable final : public MainThreadWorkerRunnable +{ +public: + CancelRunnable(WorkerPrivate* aWorkerPrivate, WebSocketImpl* aImpl) + : MainThreadWorkerRunnable(aWorkerPrivate) + , mImpl(aImpl) + { + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->AssertIsOnWorkerThread(); + return !NS_FAILED(mImpl->CancelInternal()); + } + +private: + RefPtr<WebSocketImpl> mImpl; +}; + +} // namespace + +// Window closed, stop/reload button pressed, user navigated away from page, etc. +NS_IMETHODIMP +WebSocketImpl::Cancel(nsresult aStatus) +{ + AssertIsOnMainThread(); + + if (!mIsMainThread) { + MOZ_ASSERT(mWorkerPrivate); + RefPtr<CancelRunnable> runnable = + new CancelRunnable(mWorkerPrivate, this); + if (!runnable->Dispatch()) { + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + return CancelInternal(); +} + +nsresult +WebSocketImpl::CancelInternal() +{ + AssertIsOnTargetThread(); + + // If CancelInternal is called by a runnable, we may already be disconnected + // by the time it runs. + if (mDisconnectingOrDisconnected) { + return NS_OK; + } + + int64_t readyState = mWebSocket->ReadyState(); + if (readyState == WebSocket::CLOSING || readyState == WebSocket::CLOSED) { + return NS_OK; + } + + return CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY); +} + +NS_IMETHODIMP +WebSocketImpl::Suspend() +{ + AssertIsOnMainThread(); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WebSocketImpl::Resume() +{ + AssertIsOnMainThread(); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WebSocketImpl::GetLoadGroup(nsILoadGroup** aLoadGroup) +{ + AssertIsOnMainThread(); + + *aLoadGroup = nullptr; + + if (mIsMainThread) { + nsCOMPtr<nsIDocument> doc = mWebSocket->GetDocumentIfCurrent(); + if (doc) { + *aLoadGroup = doc->GetDocumentLoadGroup().take(); + } + + return NS_OK; + } + + MOZ_ASSERT(mWorkerPrivate); + + // Walk up to our containing page + WorkerPrivate* wp = mWorkerPrivate; + while (wp->GetParent()) { + wp = wp->GetParent(); + } + + nsPIDOMWindowInner* window = wp->GetWindow(); + if (!window) { + return NS_OK; + } + + nsIDocument* doc = window->GetExtantDoc(); + if (doc) { + *aLoadGroup = doc->GetDocumentLoadGroup().take(); + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::SetLoadGroup(nsILoadGroup* aLoadGroup) +{ + AssertIsOnMainThread(); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +WebSocketImpl::GetLoadFlags(nsLoadFlags* aLoadFlags) +{ + AssertIsOnMainThread(); + + *aLoadFlags = nsIRequest::LOAD_BACKGROUND; + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::SetLoadFlags(nsLoadFlags aLoadFlags) +{ + AssertIsOnMainThread(); + + // we won't change the load flags at all. + return NS_OK; +} + +namespace { + +class WorkerRunnableDispatcher final : public WorkerRunnable +{ + RefPtr<WebSocketImpl> mWebSocketImpl; + +public: + WorkerRunnableDispatcher(WebSocketImpl* aImpl, WorkerPrivate* aWorkerPrivate, + already_AddRefed<nsIRunnable> aEvent) + : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + , mWebSocketImpl(aImpl) + , mEvent(Move(aEvent)) + { + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->AssertIsOnWorkerThread(); + + // No messages when disconnected. + if (mWebSocketImpl->mDisconnectingOrDisconnected) { + NS_WARNING("Dispatching a WebSocket event after the disconnection!"); + return true; + } + + return !NS_FAILED(mEvent->Run()); + } + + void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + bool aRunResult) override + { + } + + bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + // We don't call WorkerRunnable::PreDispatch because it would assert the + // wrong thing about which thread we're on. We're on whichever thread the + // channel implementation is running on (probably the main thread or socket + // transport thread). + return true; + } + + void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + // We don't call WorkerRunnable::PreDispatch because it would assert the + // wrong thing about which thread we're on. We're on whichever thread the + // channel implementation is running on (probably the main thread or socket + // transport thread). + } + +private: + nsCOMPtr<nsIRunnable> mEvent; +}; + +} // namespace + +NS_IMETHODIMP +WebSocketImpl::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) +{ + nsCOMPtr<nsIRunnable> event(aEvent); + return Dispatch(event.forget(), aFlags); +} + +NS_IMETHODIMP +WebSocketImpl::Dispatch(already_AddRefed<nsIRunnable> aEvent, uint32_t aFlags) +{ + nsCOMPtr<nsIRunnable> event_ref(aEvent); + // If the target is the main-thread we can just dispatch the runnable. + if (mIsMainThread) { + return NS_DispatchToMainThread(event_ref.forget()); + } + + MutexAutoLock lock(mMutex); + if (mWorkerShuttingDown) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(mWorkerPrivate); + +#ifdef DEBUG + MOZ_ASSERT(HasWorkerHolderRegistered()); +#endif + + // If the target is a worker, we have to use a custom WorkerRunnableDispatcher + // runnable. + RefPtr<WorkerRunnableDispatcher> event = + new WorkerRunnableDispatcher(this, mWorkerPrivate, event_ref.forget()); + + if (!event->Dispatch()) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WebSocketImpl::IsOnCurrentThread(bool* aResult) +{ + *aResult = IsTargetThread(); + return NS_OK; +} + +bool +WebSocketImpl::IsTargetThread() const +{ + return NS_IsMainThread() == mIsMainThread; +} + +void +WebSocket::AssertIsOnTargetThread() const +{ + MOZ_ASSERT(NS_IsMainThread() == mIsMainThread); +} + +} // namespace dom +} // namespace mozilla |