diff options
Diffstat (limited to '')
64 files changed, 7358 insertions, 0 deletions
diff --git a/dom/websocket/WebSocket.cpp b/dom/websocket/WebSocket.cpp new file mode 100644 index 0000000000..1df3136e0d --- /dev/null +++ b/dom/websocket/WebSocket.cpp @@ -0,0 +1,2765 @@ +/* -*- 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/Atomics.h" +#include "mozilla/BasePrincipal.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/nsHTTPSOnlyUtils.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SerializedStackHolder.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/LoadInfo.h" +#include "nsGlobalWindow.h" +#include "nsIScriptGlobalObject.h" +#include "mozilla/dom/Document.h" +#include "nsXPCOM.h" +#include "nsContentUtils.h" +#include "nsError.h" +#include "nsICookieJarSettings.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsIURL.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 "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" +#include "nsIWebSocketImpl.h" + +#define OPEN_EVENT_STRING u"open"_ns +#define MESSAGE_EVENT_STRING u"message"_ns +#define ERROR_EVENT_STRING u"error"_ns +#define CLOSE_EVENT_STRING u"close"_ns + +using namespace mozilla::net; + +namespace mozilla { +namespace dom { + +class WebSocketImpl final : public nsIInterfaceRequestor, + public nsIWebSocketListener, + public nsIObserver, + public nsSupportsWeakReference, + public nsIRequest, + public nsIEventTarget, + public nsIWebSocketImpl { + public: + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSIWEBSOCKETLISTENER + NS_DECL_NSIOBSERVER + NS_DECL_NSIREQUEST + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET_FULL + NS_DECL_NSIWEBSOCKETIMPL + + explicit WebSocketImpl(WebSocket* aWebSocket) + : mWebSocket(aWebSocket), + mIsServerSide(false), + mSecure(false), + mOnCloseScheduled(false), + mFailed(false), + mDisconnectingOrDisconnected(false), + mCloseEventWasClean(false), + mCloseEventCode(nsIWebSocketChannel::CLOSE_ABNORMAL), + mPort(0), + mScriptLine(0), + mScriptColumn(0), + mInnerWindowID(0), + mPrivateBrowsing(false), + mIsChromeContext(false), + mIsMainThread(true), + mMutex("WebSocketImpl::mMutex"), + mWorkerShuttingDown(false) { + if (!NS_IsMainThread()) { + mIsMainThread = false; + } + } + + void AssertIsOnTargetThread() const { MOZ_ASSERT(IsTargetThread()); } + + bool IsTargetThread() const; + + nsresult Init(JSContext* aCx, nsIPrincipal* aLoadingPrincipal, + nsIPrincipal* aPrincipal, bool aIsServerSide, + const nsAString& aURL, nsTArray<nsString>& aProtocolArray, + const nsACString& aScriptFile, uint32_t aScriptLine, + uint32_t aScriptColumn); + + nsresult AsyncOpen(nsIPrincipal* aPrincipal, uint64_t aInnerWindowID, + nsITransportProvider* aTransportProvider, + const nsACString& aNegotiatedExtensions, + UniquePtr<SerializedStackHolder> aOriginStack); + + nsresult ParseURL(const nsAString& aURL); + nsresult InitializeConnection(nsIPrincipal* aPrincipal, + nsICookieJarSettings* aCookieJarSettings); + + // These methods when called can release the WebSocket object + void FailConnection(uint16_t reasonCode, + const nsACString& aReasonString = ""_ns); + nsresult CloseConnection(uint16_t reasonCode, + const nsACString& aReasonString = ""_ns); + void Disconnect(); + void DisconnectInternal(); + + nsresult ConsoleError(); + void PrintErrorOnConsole(const char* aBundleURI, const char* aError, + nsTArray<nsString>&& aFormatStrings); + + 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 RegisterWorkerRef(WorkerPrivate* aWorkerPrivate); + void UnregisterWorkerRef(); + + nsresult CancelInternal(); + + nsresult GetLoadingPrincipal(nsIPrincipal** aPrincipal); + + 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; + Atomic<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; + bool mPrivateBrowsing; + bool mIsChromeContext; + + RefPtr<ThreadSafeWorkerRef> mWorkerRef; + + nsWeakPtr mWeakLoadGroup; + + bool mIsMainThread; + + // This mutex protects mWorkerShuttingDown. + mozilla::Mutex mMutex; + bool mWorkerShuttingDown; + + RefPtr<WebSocketEventService> mService; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + + // For dispatching runnables to main thread. + nsCOMPtr<nsIEventTarget> mMainThreadEventTarget; + + private: + ~WebSocketImpl() { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread() == mIsMainThread || + mDisconnectingOrDisconnected); + + // If we threw during Init we never called disconnect + if (!mDisconnectingOrDisconnected) { + Disconnect(); + } + } +}; + +NS_IMPL_ISUPPORTS(WebSocketImpl, nsIInterfaceRequestor, nsIWebSocketListener, + nsIObserver, nsISupportsWeakReference, nsIRequest, + nsIEventTarget, nsIWebSocketImpl) + +class CallDispatchConnectionCloseEvents final : public DiscardableRunnable { + public: + explicit CallDispatchConnectionCloseEvents(WebSocketImpl* aWebSocketImpl) + : DiscardableRunnable("dom::CallDispatchConnectionCloseEvents"), + 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 char* aError, + nsTArray<nsString>&& aFormatStrings) + : WorkerMainThreadRunnable(aImpl->mWorkerRef->Private(), + "WebSocket :: print error on console"_ns), + mImpl(aImpl), + mBundleURI(aBundleURI), + mError(aError), + mFormatStrings(std::move(aFormatStrings)) {} + + bool MainThreadRun() override { + mImpl->PrintErrorOnConsole(mBundleURI, mError, std::move(mFormatStrings)); + return true; + } + + private: + // Raw pointer because this runnable is sync. + WebSocketImpl* mImpl; + + const char* mBundleURI; + const char* mError; + nsTArray<nsString> mFormatStrings; +}; + +} // namespace + +void WebSocketImpl::PrintErrorOnConsole(const char* aBundleURI, + const char* aError, + nsTArray<nsString>&& aFormatStrings) { + // This method must run on the main thread. + + if (!NS_IsMainThread()) { + MOZ_ASSERT(mWorkerRef); + + RefPtr<PrintErrorOnConsoleRunnable> runnable = + new PrintErrorOnConsoleRunnable(this, aBundleURI, aError, + std::move(aFormatStrings)); + ErrorResult rv; + runnable->Dispatch(Killing, 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 + nsAutoString message; + if (!aFormatStrings.IsEmpty()) { + rv = strBundle->FormatStringFromName(aError, aFormatStrings, message); + } else { + rv = strBundle->GetStringFromName(aError, message); + } + NS_ENSURE_SUCCESS_VOID(rv); + + if (mInnerWindowID) { + rv = errorObject->InitWithWindowID( + message, NS_ConvertUTF8toUTF16(mScriptFile), u""_ns, mScriptLine, + mScriptColumn, nsIScriptError::errorFlag, "Web Socket", mInnerWindowID); + } else { + rv = + errorObject->Init(message, NS_ConvertUTF8toUTF16(mScriptFile), u""_ns, + mScriptLine, mScriptColumn, nsIScriptError::errorFlag, + "Web Socket", mPrivateBrowsing, mIsChromeContext); + } + + 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) + : Runnable("dom::CancelWebSocketRunnable"), + mChannel(aChannel), + mReasonCode(aReasonCode), + mReasonString(aReasonString) {} + + NS_IMETHOD Run() override { + nsresult rv = mChannel->Close(mReasonCode, mReasonString); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the close message"); + } + 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) + : Runnable("dom::CloseConnectionRunnable"), + 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 ThreadSafeWorkerRef. + 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; + } + } + + nsTArray<nsString> formatStrings; + CopyUTF8toUTF16(mURI, *formatStrings.AppendElement()); + + if (mWebSocket->ReadyState() < WebSocket::OPEN) { + PrintErrorOnConsole("chrome://global/locale/appstrings.properties", + "connectionFailure", std::move(formatStrings)); + } else { + PrintErrorOnConsole("chrome://global/locale/appstrings.properties", + "netInterrupt", std::move(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(GetCurrentThreadWorkerPrivate(), + "WebSocket :: disconnect"_ns), + mImpl(aImpl) {} + + bool MainThreadRun() override { + mImpl->DisconnectInternal(); + return true; + } + + private: + // A raw pointer because this runnable is sync. + WebSocketImpl* mImpl; +}; + +} // namespace + +void WebSocketImpl::Disconnect() { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread() == mIsMainThread); + + if (mDisconnectingOrDisconnected) { + return; + } + + // 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 a callback from + // StrongWorkerRef). 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(); + + // If we haven't called WebSocket::DisconnectFromOwner yet, update + // web socket count here. + if (mWebSocket->GetOwner()) { + mWebSocket->GetOwner()->UpdateWebSocketCount(-1); + } + } else { + RefPtr<DisconnectInternalRunnable> runnable = + new DisconnectInternalRunnable(this); + ErrorResult rv; + runnable->Dispatch(Killing, rv); + // XXXbz this seems totally broken. We should be propagating this out, but + // where to, exactly? + rv.SuppressException(); + } + + NS_ReleaseOnMainThread("WebSocketImpl::mChannel", mChannel.forget()); + NS_ReleaseOnMainThread("WebSocketImpl::mService", mService.forget()); + + mWebSocket->DontKeepAliveAnyMore(); + mWebSocket->mImpl = nullptr; + + if (mWorkerRef) { + UnregisterWorkerRef(); + } + + // 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 (!mWorkerRef) { + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->RemoveObserver(this, DOM_WINDOW_DESTROYED_TOPIC); + os->RemoveObserver(this, DOM_WINDOW_FROZEN_TOPIC); + } + } +} + +//----------------------------------------------------------------------------- +// WebSocketImpl::nsIWebSocketImpl +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebSocketImpl::SendMessage(const nsAString& aMessage) { + nsString message(aMessage); + nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( + "WebSocketImpl::SendMessage", + [self = RefPtr<WebSocketImpl>(this), message = std::move(message)]() { + ErrorResult IgnoredErrorResult; + self->mWebSocket->Send(message, IgnoredErrorResult); + }); + return Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); +} + +//----------------------------------------------------------------------------- +// 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->CheckCurrentGlobalCorrectness(); + if (NS_FAILED(rv)) { + CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY); + return rv; + } + + if (!mRequestedProtocolList.IsEmpty()) { + rv = mChannel->GetProtocol(mWebSocket->mEstablishedProtocol); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + rv = mChannel->GetExtensions(mWebSocket->mEstablishedExtensions); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + UpdateURI(); + + mWebSocket->SetReadyState(WebSocket::OPEN); + + mService->WebSocketOpened( + mChannel->Serial(), mInnerWindowID, mWebSocket->mEffectiveURL, + mWebSocket->mEstablishedProtocol, mWebSocket->mEstablishedExtensions, + mChannel->HttpChannelId()); + + // 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(OPEN_EVENT_STRING); + 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 (aStatusCode == NS_ERROR_NET_INADEQUATE_SECURITY) { + // TLS negotiation failed so we need to set status code to 1015. + mCloseEventCode = 1015; + } + + 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; + } + + MOZ_RELEASE_ASSERT(mWebSocket->mOutgoingBufferedAmount.isValid()); + if (aSize > mWebSocket->mOutgoingBufferedAmount.value()) { + return NS_ERROR_UNEXPECTED; + } + + CheckedUint64 outgoingBufferedAmount = mWebSocket->mOutgoingBufferedAmount; + outgoingBufferedAmount -= aSize; + if (!outgoingBufferedAmount.isValid()) { + return NS_ERROR_UNEXPECTED; + } + + mWebSocket->mOutgoingBufferedAmount = outgoingBufferedAmount; + MOZ_RELEASE_ASSERT(mWebSocket->mOutgoingBufferedAmount.isValid()); + + 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, ""_ns); + } 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; +} + +NS_IMETHODIMP +WebSocketImpl::OnError() { + if (!IsTargetThread()) { + return Dispatch( + NS_NewRunnableFunction("dom::FailConnectionRunnable", + [self = RefPtr{this}]() { + self->FailConnection( + nsIWebSocketChannel::CLOSE_ABNORMAL); + }), + NS_DISPATCH_NORMAL); + } + + AssertIsOnTargetThread(); + FailConnection(nsIWebSocketChannel::CLOSE_ABNORMAL); + 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(nsIGlobalObject* aGlobal) + : DOMEventTargetHelper(aGlobal), + mIsMainThread(true), + mKeepingAlive(false), + mCheckMustKeepAlive(true), + mOutgoingBufferedAmount(0), + mBinaryType(dom::BinaryType::Blob), + mMutex("WebSocket::mMutex"), + mReadyState(CONNECTING) { + MOZ_ASSERT(aGlobal); + + mImpl = new WebSocketImpl(this); + mIsMainThread = mImpl->mIsMainThread; +} + +WebSocket::~WebSocket() = default; + +mozilla::Maybe<EventCallbackDebuggerNotificationType> +WebSocket::GetDebuggerNotificationType() const { + return mozilla::Some(EventCallbackDebuggerNotificationType::Websocket); +} + +JSObject* WebSocket::WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) { + return WebSocket_Binding::Wrap(cx, this, aGivenProto); +} + +//--------------------------------------------------------------------------- +// WebIDL +//--------------------------------------------------------------------------- + +// Constructor: +already_AddRefed<WebSocket> WebSocket::Constructor( + const GlobalObject& aGlobal, const nsAString& aUrl, + const StringOrStringSequence& aProtocols, ErrorResult& aRv) { + if (aProtocols.IsStringSequence()) { + return WebSocket::ConstructorCommon( + aGlobal, aUrl, aProtocols.GetAsStringSequence(), nullptr, ""_ns, aRv); + } + + Sequence<nsString> protocols; + if (!protocols.AppendElement(aProtocols.GetAsString(), fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + return WebSocket::ConstructorCommon(aGlobal, aUrl, protocols, nullptr, ""_ns, + 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(WorkerPrivate* aWorkerPrivate, WebSocketImpl* aImpl, + bool aIsServerSide, const nsAString& aURL, + nsTArray<nsString>& aProtocolArray, + const nsACString& aScriptFile, uint32_t aScriptLine, + uint32_t aScriptColumn) + : WebSocketMainThreadRunnable(aWorkerPrivate, "WebSocket :: init"_ns), + mImpl(aImpl), + mIsServerSide(aIsServerSide), + mURL(aURL), + mProtocolArray(aProtocolArray), + mScriptFile(aScriptFile), + mScriptLine(aScriptLine), + mScriptColumn(aScriptColumn), + mErrorCode(NS_OK) { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + } + + nsresult ErrorCode() const { return mErrorCode; } + + protected: + virtual bool InitWithWindow(nsPIDOMWindowInner* aWindow) override { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(aWindow))) { + mErrorCode = NS_ERROR_FAILURE; + return true; + } + + ClearException ce(jsapi.cx()); + + Document* doc = aWindow->GetExtantDoc(); + if (!doc) { + mErrorCode = NS_ERROR_FAILURE; + return true; + } + + mErrorCode = + mImpl->Init(jsapi.cx(), mWorkerPrivate->GetPrincipal(), + doc->NodePrincipal(), mIsServerSide, mURL, mProtocolArray, + mScriptFile, mScriptLine, mScriptColumn); + return true; + } + + virtual bool InitWindowless(WorkerPrivate* aTopLevelWorkerPrivate) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTopLevelWorkerPrivate && !aTopLevelWorkerPrivate->GetWindow()); + + mErrorCode = + mImpl->Init(nullptr, mWorkerPrivate->GetPrincipal(), + aTopLevelWorkerPrivate->GetPrincipal(), mIsServerSide, mURL, + mProtocolArray, mScriptFile, mScriptLine, mScriptColumn); + return true; + } + + // Raw pointer. This worker runnable runs synchronously. + WebSocketImpl* mImpl; + + bool mIsServerSide; + const nsAString& mURL; + nsTArray<nsString>& mProtocolArray; + nsCString mScriptFile; + uint32_t mScriptLine; + uint32_t mScriptColumn; + nsresult mErrorCode; +}; + +class ConnectRunnable final : public WebSocketMainThreadRunnable { + public: + ConnectRunnable(WorkerPrivate* aWorkerPrivate, WebSocketImpl* aImpl) + : WebSocketMainThreadRunnable(aWorkerPrivate, "WebSocket :: init"_ns), + mImpl(aImpl), + mConnectionFailed(true) { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + } + + bool ConnectionFailed() const { return mConnectionFailed; } + + protected: + virtual bool InitWithWindow(nsPIDOMWindowInner* aWindow) override { + Document* doc = aWindow->GetExtantDoc(); + if (!doc) { + return true; + } + + mConnectionFailed = NS_FAILED(mImpl->InitializeConnection( + doc->NodePrincipal(), mWorkerPrivate->CookieJarSettings())); + return true; + } + + virtual bool InitWindowless(WorkerPrivate* aTopLevelWorkerPrivate) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTopLevelWorkerPrivate && !aTopLevelWorkerPrivate->GetWindow()); + + mConnectionFailed = NS_FAILED( + mImpl->InitializeConnection(aTopLevelWorkerPrivate->GetPrincipal(), + mWorkerPrivate->CookieJarSettings())); + return true; + } + + // Raw pointer. This worker runnable runs synchronously. + WebSocketImpl* mImpl; + + bool mConnectionFailed; +}; + +class AsyncOpenRunnable final : public WebSocketMainThreadRunnable { + public: + explicit AsyncOpenRunnable(WebSocketImpl* aImpl, + UniquePtr<SerializedStackHolder> aOriginStack) + : WebSocketMainThreadRunnable(aImpl->mWorkerRef->Private(), + "WebSocket :: AsyncOpen"_ns), + mImpl(aImpl), + mOriginStack(std::move(aOriginStack)), + mErrorCode(NS_OK) { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + } + + nsresult ErrorCode() const { return mErrorCode; } + + protected: + virtual bool InitWithWindow(nsPIDOMWindowInner* aWindow) override { + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + Document* doc = aWindow->GetExtantDoc(); + if (!doc) { + mErrorCode = NS_ERROR_FAILURE; + return true; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + if (!principal) { + mErrorCode = NS_ERROR_FAILURE; + return true; + } + + uint64_t windowID = 0; + if (WindowContext* wc = aWindow->GetWindowContext()) { + windowID = wc->TopWindowContext()->InnerWindowId(); + } + + mErrorCode = mImpl->AsyncOpen(principal, windowID, nullptr, ""_ns, + std::move(mOriginStack)); + return true; + } + + virtual bool InitWindowless(WorkerPrivate* aTopLevelWorkerPrivate) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTopLevelWorkerPrivate && !aTopLevelWorkerPrivate->GetWindow()); + + mErrorCode = mImpl->AsyncOpen(aTopLevelWorkerPrivate->GetPrincipal(), 0, + nullptr, ""_ns, nullptr); + return true; + } + + private: + // Raw pointer. This worker runs synchronously. + WebSocketImpl* mImpl; + + UniquePtr<SerializedStackHolder> mOriginStack; + + nsresult mErrorCode; +}; + +} // 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<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + 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; + } + } + + 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(global); + RefPtr<WebSocketImpl> webSocketImpl = webSocket->mImpl; + + bool connectionFailed = true; + + if (NS_IsMainThread()) { + // We're keeping track of all main thread web sockets to be able to + // avoid throttling timeouts when we have active web sockets. + webSocket->GetOwner()->UpdateWebSocketCount(1); + + nsCOMPtr<nsIPrincipal> loadingPrincipal; + aRv = webSocketImpl->GetLoadingPrincipal(getter_AddRefs(loadingPrincipal)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + aRv = webSocketImpl->Init(aGlobal.Context(), loadingPrincipal, principal, + !!aTransportProvider, aUrl, protocolArray, ""_ns, + 0, 0); + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<Document> doc = webSocket->GetDocumentIfCurrent(); + + // 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 + connectionFailed = NS_FAILED(webSocketImpl->InitializeConnection( + principal, doc ? doc->CookieJarSettings() : nullptr)); + } else { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + 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( + workerPrivate, webSocketImpl, !!aTransportProvider, aUrl, protocolArray, + nsDependentCString(file.get()), lineno, column); + runnable->Dispatch(Canceling, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + aRv = runnable->ErrorCode(); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (NS_WARN_IF(!webSocketImpl->RegisterWorkerRef(workerPrivate))) { + // The worker is shutting down. + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<ConnectRunnable> connectRunnable = + new ConnectRunnable(workerPrivate, webSocketImpl); + connectRunnable->Dispatch(Canceling, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + connectionFailed = connectRunnable->ConnectionFailed(); + } + + // 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); + + nsCOMPtr<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(global); + + UniquePtr<SerializedStackHolder> stack; + BrowsingContext* browsingContext = ownerWindow->GetBrowsingContext(); + if (browsingContext && browsingContext->WatchedByDevTools()) { + stack = GetCurrentStackForNetMonitor(aGlobal.Context()); + } + + uint64_t windowID = 0; + if (WindowContext* wc = ownerWindow->GetWindowContext()) { + windowID = wc->TopWindowContext()->InnerWindowId(); + } + + aRv = webSocket->mImpl->AsyncOpen(principal, windowID, aTransportProvider, + aNegotiatedExtensions, std::move(stack)); + } else { + MOZ_ASSERT(!aTransportProvider && aNegotiatedExtensions.IsEmpty(), + "not yet implemented"); + + UniquePtr<SerializedStackHolder> stack; + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + if (workerPrivate->IsWatchedByDevTools()) { + stack = GetCurrentStackForNetMonitor(aGlobal.Context()); + } + + RefPtr<AsyncOpenRunnable> runnable = + new AsyncOpenRunnable(webSocket->mImpl, std::move(stack)); + runnable->Dispatch(Canceling, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + aRv = runnable->ErrorCode(); + } + + 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_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 + +bool WebSocket::IsCertainlyAliveForCC() const { return mKeepingAlive; } + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebSocket) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(WebSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(WebSocket, DOMEventTargetHelper) + +void WebSocket::DisconnectFromOwner() { + // If we haven't called WebSocketImpl::Disconnect yet, update web + // socket count here. + if (NS_IsMainThread() && mImpl && !mImpl->mDisconnectingOrDisconnected && + GetOwner()) { + GetOwner()->UpdateWebSocketCount(-1); + } + + DOMEventTargetHelper::DisconnectFromOwner(); + + if (mImpl) { + mImpl->CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY); + } + + DontKeepAliveAnyMore(); +} + +//----------------------------------------------------------------------------- +// WebSocketImpl:: initialization +//----------------------------------------------------------------------------- + +nsresult WebSocketImpl::Init(JSContext* aCx, nsIPrincipal* aLoadingPrincipal, + nsIPrincipal* aPrincipal, bool aIsServerSide, + const nsAString& aURL, + nsTArray<nsString>& aProtocolArray, + const nsACString& aScriptFile, + uint32_t aScriptLine, uint32_t aScriptColumn) { + 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 + nsresult rv = mWebSocket->CheckCurrentGlobalCorrectness(); + NS_ENSURE_SUCCESS(rv, rv); + + // Shut down websocket if window is frozen or destroyed (only needed for + // "ghost" websockets--see bug 696085) + if (mIsMainThread) { + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!os)) { + return NS_ERROR_FAILURE; + } + + rv = os->AddObserver(this, DOM_WINDOW_DESTROYED_TOPIC, true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = os->AddObserver(this, DOM_WINDOW_FROZEN_TOPIC, true); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!mIsMainThread) { + 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); + } + + mPrivateBrowsing = !!aPrincipal->OriginAttributesRef().mPrivateBrowsingId; + mIsChromeContext = aPrincipal->IsSystemPrincipal(); + + // parses the url + rv = ParseURL(aURL); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<Document> originDoc = mWebSocket->GetDocumentIfCurrent(); + if (!originDoc) { + rv = mWebSocket->CheckCurrentGlobalCorrectness(); + NS_ENSURE_SUCCESS(rv, rv); + } + 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 + // AsyncOpen(). + // Please note that websockets can't follow redirects, hence there is no + // need to perform a CSP check after redirects. + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo( + aPrincipal, // loading principal + aPrincipal, // triggering principal + originDoc, nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + nsIContentPolicy::TYPE_WEBSOCKET); + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(uri, secCheckLoadInfo, ""_ns, &shouldLoad, + nsContentUtils::GetContentPolicy()); + NS_ENSURE_SUCCESS(rv, rv); + + if (NS_CP_REJECTED(shouldLoad)) { + // Disallowed by content policy + return NS_ERROR_CONTENT_BLOCKED; + } + } + + // If the HTTPS-Only mode is enabled, we need to upgrade the websocket + // connection from ws:// to wss:// and mark it as secure. + if (!mIsServerSide && !mSecure && originDoc) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mURI); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel = originDoc->GetChannel(); + if (channel) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + + if (nsHTTPSOnlyUtils::ShouldUpgradeWebSocket(uri, loadInfo)) { + mURI.ReplaceSubstring("ws://", "wss://"); + if (NS_WARN_IF(mURI.Find("wss://") != 0)) { + return NS_OK; + } + mSecure = true; + } + } + } + + // 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 + AutoTArray<nsString, 2> params; + CopyUTF8toUTF16(mURI, *params.AppendElement()); + + // upgrade the request from ws:// to wss:// and mark as secure + mURI.ReplaceSubstring("ws://", "wss://"); + if (NS_WARN_IF(mURI.Find("wss://") != 0)) { + return NS_OK; + } + mSecure = true; + + params.AppendElement(u"wss"_ns); + CSP_LogLocalizedStr("upgradeInsecureRequest", params, + u""_ns, // aSourceFile + u""_ns, // aScriptSample + 0, // aLineNumber + 0, // aColumnNumber + nsIScriptError::warningFlag, + "upgradeInsecureRequest"_ns, mInnerWindowID, + mPrivateBrowsing); + } + + // Don't allow https:// to open ws:// + if (!mIsServerSide && !mSecure && + !Preferences::GetBool("network.websocket.allowInsecureFromHTTPS", + false) && + !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost( + mAsciiHost)) { + if (aLoadingPrincipal->SchemeIs("https")) { + return NS_ERROR_DOM_SECURITY_ERR; + } + } + + // 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)) { + return NS_ERROR_DOM_SYNTAX_ERR; + } + } + + if (!mRequestedProtocolList.IsEmpty()) { + mRequestedProtocolList.AppendLiteral(", "); + } + + AppendUTF16toUTF8(aProtocolArray[index], mRequestedProtocolList); + } + + return NS_OK; +} + +nsresult WebSocketImpl::AsyncOpen( + nsIPrincipal* aPrincipal, uint64_t aInnerWindowID, + nsITransportProvider* aTransportProvider, + const nsACString& aNegotiatedExtensions, + UniquePtr<SerializedStackHolder> aOriginStack) { + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + MOZ_ASSERT_IF(!aTransportProvider, aNegotiatedExtensions.IsEmpty()); + + nsCString asciiOrigin; + nsresult rv = aPrincipal->GetAsciiOrigin(asciiOrigin); + if (NS_FAILED(rv)) { + asciiOrigin.AssignLiteral("null"); + } + + if (aTransportProvider) { + rv = mChannel->SetServerParameters(aTransportProvider, + aNegotiatedExtensions); + NS_ENSURE_SUCCESS(rv, rv); + } + + ToLowerCase(asciiOrigin); + + nsCOMPtr<nsIURI> uri; + if (!aTransportProvider) { + rv = NS_NewURI(getter_AddRefs(uri), mURI); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + rv = mChannel->AsyncOpen(uri, asciiOrigin, aInnerWindowID, this, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_CONTENT_BLOCKED; + } + + NotifyNetworkMonitorAlternateStack(mChannel, std::move(aOriginStack)); + + mInnerWindowID = aInnerWindowID; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// 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, nsICookieJarSettings* aCookieJarSettings) { + 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<Document> 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)); + + rv = wsChannel->InitLoadInfoNative( + doc, doc ? doc->NodePrincipal() : aPrincipal, aPrincipal, + aCookieJarSettings, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_WEBSOCKET, 0); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + 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; + + if (mIsMainThread) { + mService->AssociateWebSocketImplWithSerialID(this, mChannel->Serial()); + } + + if (mIsMainThread && doc) { + mMainThreadEventTarget = doc->EventTargetFor(TaskCategory::Other); + } + + 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(ERROR_EVENT_STRING); + 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 = CheckCurrentGlobalCorrectness(); + 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); + + ErrorResult err; + DispatchEvent(*event, err); + return err.StealNSResult(); +} + +nsresult WebSocket::CreateAndDispatchMessageEvent(const nsACString& aData, + bool aIsBinary) { + MOZ_ASSERT(mImpl); + AssertIsOnTargetThread(); + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + + nsresult rv = CheckCurrentGlobalCorrectness(); + 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(GetOwnerGlobal(), aData, u""_ns); + if (NS_WARN_IF(!blob)) { + return NS_ERROR_FAILURE; + } + + 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 { + MOZ_CRASH("Unknown binary type!"); + return NS_ERROR_UNEXPECTED; + } + } else { + // JS string + nsAutoString utf16Data; + if (!AppendUTF8toUTF16(aData, utf16Data, mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + 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, MESSAGE_EVENT_STRING, CanBubble::eNo, + Cancelable::eNo, jsData, mImpl->mUTF16Origin, u""_ns, + nullptr, Sequence<OwningNonNull<MessagePort>>()); + event->SetTrusted(true); + + ErrorResult err; + DispatchEvent(*event, err); + return err.StealNSResult(); +} + +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 = CheckCurrentGlobalCorrectness(); + 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, CLOSE_EVENT_STRING, init); + event->SetTrusted(true); + + ErrorResult err; + DispatchEvent(*event, err); + return err.StealNSResult(); +} + +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); + + 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(OPEN_EVENT_STRING) || + mListenerManager->HasListenersFor(MESSAGE_EVENT_STRING) || + mListenerManager->HasListenersFor(ERROR_EVENT_STRING) || + mListenerManager->HasListenersFor(CLOSE_EVENT_STRING)) { + shouldKeepAlive = true; + } + } break; + + case OPEN: + case CLOSING: { + if (mListenerManager->HasListenersFor(MESSAGE_EVENT_STRING) || + mListenerManager->HasListenersFor(ERROR_EVENT_STRING) || + mListenerManager->HasListenersFor(CLOSE_EVENT_STRING) || + mOutgoingBufferedAmount.value() != 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; +} + +void WebSocketImpl::AddRefObject() { + AssertIsOnTargetThread(); + AddRef(); +} + +void WebSocketImpl::ReleaseObject() { + AssertIsOnTargetThread(); + Release(); +} + +bool WebSocketImpl::RegisterWorkerRef(WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + + RefPtr<WebSocketImpl> self = this; + + // In workers we have to keep the worker alive using a strong reference in + // order to dispatch messages correctly. + RefPtr<StrongWorkerRef> workerRef = + StrongWorkerRef::Create(aWorkerPrivate, "WebSocketImpl", [self]() { + { + MutexAutoLock lock(self->mMutex); + self->mWorkerShuttingDown = true; + } + + self->CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY, ""_ns); + }); + if (NS_WARN_IF(!workerRef)) { + return false; + } + + mWorkerRef = new ThreadSafeWorkerRef(workerRef); + MOZ_ASSERT(mWorkerRef); + + return true; +} + +void WebSocketImpl::UnregisterWorkerRef() { + MOZ_ASSERT(mDisconnectingOrDisconnected); + MOZ_ASSERT(mWorkerRef); + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + { + MutexAutoLock lock(mMutex); + mWorkerShuttingDown = true; + } + + // The DTOR of this StrongWorkerRef will release the worker for us. + mWorkerRef = nullptr; +} + +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(nsAtom* aType) { + AssertIsOnTargetThread(); + UpdateMustKeepAlive(); +} + +void WebSocket::EventListenerRemoved(nsAtom* aType) { + AssertIsOnTargetThread(); + 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 long bufferedAmount; +uint64_t WebSocket::BufferedAmount() const { + AssertIsOnTargetThread(); + MOZ_RELEASE_ASSERT(mOutgoingBufferedAmount.isValid()); + return mOutgoingBufferedAmount.value(); +} + +// 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(); + + nsAutoCString msgString; + if (!AppendUTF16toUTF8(aData, msgString, mozilla::fallible_t())) { + aRv.Throw(NS_ERROR_FILE_TOO_BIG); + return; + } + Send(nullptr, msgString, msgString.Length(), false, aRv); +} + +void WebSocket::Send(Blob& aData, ErrorResult& aRv) { + AssertIsOnTargetThread(); + + nsCOMPtr<nsIInputStream> msgStream; + aData.CreateInputStream(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, ""_ns, msgLength, true, aRv); +} + +void WebSocket::Send(const ArrayBuffer& aData, ErrorResult& aRv) { + AssertIsOnTargetThread(); + + aData.ComputeState(); + + static_assert(sizeof(*aData.Data()) == 1, "byte-sized data required"); + + uint32_t len = aData.Length(); + char* data = reinterpret_cast<char*>(aData.Data()); + + nsDependentCSubstring msgString; + if (!msgString.Assign(data, len, mozilla::fallible_t())) { + aRv.Throw(NS_ERROR_FILE_TOO_BIG); + return; + } + Send(nullptr, msgString, len, true, aRv); +} + +void WebSocket::Send(const ArrayBufferView& aData, ErrorResult& aRv) { + AssertIsOnTargetThread(); + + aData.ComputeState(); + + static_assert(sizeof(*aData.Data()) == 1, "byte-sized data required"); + + uint32_t len = aData.Length(); + char* data = reinterpret_cast<char*>(aData.Data()); + + nsDependentCSubstring msgString; + if (!msgString.Assign(data, len, mozilla::fallible_t())) { + aRv.Throw(NS_ERROR_FILE_TOO_BIG); + return; + } + 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; + } + + CheckedUint64 outgoingBufferedAmount = mOutgoingBufferedAmount; + outgoingBufferedAmount += aMsgLength; + if (!outgoingBufferedAmount.isValid()) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + // Always increment outgoing buffer len, even if closed + mOutgoingBufferedAmount = outgoingBufferedAmount; + MOZ_RELEASE_ASSERT(mOutgoingBufferedAmount.isValid()); + + 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 we don't have mImpl, we are in a shutting down worker where we are still + // in CONNECTING state, but already disconnected internally. + if (!mImpl) { + MOZ_ASSERT(readyState == CONNECTING); + SetReadyState(CLOSING); + return; + } + + 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(ThreadSafeWorkerRef* aWorkerRef, WebSocketImpl* aImpl) + : MainThreadWorkerRunnable(aWorkerRef->Private()), 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(mWorkerRef); + RefPtr<CancelRunnable> runnable = new CancelRunnable(mWorkerRef, 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<Document> doc = mWebSocket->GetDocumentIfCurrent(); + if (doc) { + *aLoadGroup = doc->GetDocumentLoadGroup().take(); + } + + return NS_OK; + } + + MOZ_ASSERT(mWorkerRef); + + // Walk up to our containing page + WorkerPrivate* wp = mWorkerRef->Private(); + while (wp->GetParent()) { + wp = wp->GetParent(); + } + + nsPIDOMWindowInner* window = wp->GetWindow(); + if (!window) { + return NS_OK; + } + + Document* 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; +} + +NS_IMETHODIMP +WebSocketImpl::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +WebSocketImpl::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +namespace { + +class WorkerRunnableDispatcher final : public WorkerRunnable { + RefPtr<WebSocketImpl> mWebSocketImpl; + + public: + WorkerRunnableDispatcher(WebSocketImpl* aImpl, + ThreadSafeWorkerRef* aWorkerRef, + already_AddRefed<nsIRunnable> aEvent) + : WorkerRunnable(aWorkerRef->Private(), WorkerThreadUnchangedBusyCount), + mWebSocketImpl(aImpl), + mEvent(std::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 should try to dispatch the runnable + // to a labeled event target. + if (mIsMainThread) { + return mMainThreadEventTarget + ? mMainThreadEventTarget->Dispatch(event_ref.forget()) + : GetMainThreadEventTarget()->Dispatch(event_ref.forget()); + } + + MutexAutoLock lock(mMutex); + if (mWorkerShuttingDown) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(mWorkerRef); + + // If the target is a worker, we have to use a custom WorkerRunnableDispatcher + // runnable. + RefPtr<WorkerRunnableDispatcher> event = + new WorkerRunnableDispatcher(this, mWorkerRef, 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; +} + +NS_IMETHODIMP_(bool) +WebSocketImpl::IsOnCurrentThreadInfallible() { return IsTargetThread(); } + +bool WebSocketImpl::IsTargetThread() const { + return NS_IsMainThread() == mIsMainThread; +} + +void WebSocket::AssertIsOnTargetThread() const { + MOZ_ASSERT(NS_IsMainThread() == mIsMainThread); +} + +nsresult WebSocketImpl::GetLoadingPrincipal(nsIPrincipal** aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mIsMainThread); + + // Check the principal's uri to determine if we were loaded from https. + nsCOMPtr<nsIGlobalObject> globalObject(GetEntryGlobal()); + nsCOMPtr<nsIPrincipal> principal; + + if (globalObject) { + principal = globalObject->PrincipalOrNull(); + } + + nsCOMPtr<nsPIDOMWindowInner> 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. + principal.forget(aPrincipal); + return NS_OK; + } + + RefPtr<WindowContext> windowContext = innerWindow->GetWindowContext(); + + while (true) { + if (principal && !principal->GetIsNullPrincipal()) { + break; + } + + if (NS_WARN_IF(!windowContext)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + if (windowContext->IsTop()) { + if (!windowContext->GetBrowsingContext()->HadOriginalOpener()) { + break; + } + // We are at the top. Let's see if we have an opener window. + RefPtr<BrowsingContext> opener = + windowContext->GetBrowsingContext()->GetOpener(); + if (!opener) { + break; + } + windowContext = opener->GetCurrentWindowContext(); + } else { + // If we're not a top window get the parent window context instead. + windowContext = windowContext->GetParentWindowContext(); + } + + if (NS_WARN_IF(!windowContext)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<Document> document = windowContext->GetExtantDoc(); + if (NS_WARN_IF(!document)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + principal = document->NodePrincipal(); + } + + principal.forget(aPrincipal); + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/websocket/WebSocket.h b/dom/websocket/WebSocket.h new file mode 100644 index 0000000000..e73c5fd705 --- /dev/null +++ b/dom/websocket/WebSocket.h @@ -0,0 +1,190 @@ +/* -*- 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/. */ + +#ifndef WebSocket_h__ +#define WebSocket_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/WebSocketBinding.h" // for BinaryType +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Mutex.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsISupportsUtils.h" +#include "nsString.h" +#include "nsWrapperCache.h" + +#define DEFAULT_WS_SCHEME_PORT 80 +#define DEFAULT_WSS_SCHEME_PORT 443 + +class nsIInputStream; +class nsITransportProvider; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class Blob; +class StringOrStringSequence; +class WebSocketImpl; + +class WebSocket final : public DOMEventTargetHelper { + friend class WebSocketImpl; + + public: + enum { CONNECTING = 0, OPEN = 1, CLOSING = 2, CLOSED = 3 }; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(WebSocket, DOMEventTargetHelper) + virtual bool IsCertainlyAliveForCC() const override; + + // EventTarget + using EventTarget::EventListenerAdded; + virtual void EventListenerAdded(nsAtom* aType) override; + + using EventTarget::EventListenerRemoved; + virtual void EventListenerRemoved(nsAtom* aType) override; + + virtual void DisconnectFromOwner() override; + + mozilla::Maybe<EventCallbackDebuggerNotificationType> + GetDebuggerNotificationType() const override; + + // nsWrapperCache + virtual JSObject* WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) override; + + public: // static helpers: + // Determine if preferences allow WebSocket + static bool PrefEnabled(JSContext* aCx = nullptr, + JSObject* aGlobal = nullptr); + + public: // WebIDL interface: + // Constructor: + static already_AddRefed<WebSocket> Constructor( + const GlobalObject& aGlobal, const nsAString& aUrl, + const StringOrStringSequence& aProtocols, ErrorResult& rv); + + static already_AddRefed<WebSocket> CreateServerWebSocket( + const GlobalObject& aGlobal, const nsAString& aUrl, + const Sequence<nsString>& aProtocols, + nsITransportProvider* aTransportProvider, + const nsAString& aNegotiatedExtensions, ErrorResult& rv); + + static already_AddRefed<WebSocket> ConstructorCommon( + const GlobalObject& aGlobal, const nsAString& aUrl, + const Sequence<nsString>& aProtocols, + nsITransportProvider* aTransportProvider, + const nsACString& aNegotiatedExtensions, ErrorResult& rv); + + // webIDL: readonly attribute DOMString url + void GetUrl(nsAString& aResult); + + // webIDL: readonly attribute unsigned short readyState; + uint16_t ReadyState(); + + // webIDL: readonly attribute unsigned long long bufferedAmount; + uint64_t BufferedAmount() const; + + // webIDL: attribute Function? onopen; + IMPL_EVENT_HANDLER(open) + + // webIDL: attribute Function? onerror; + IMPL_EVENT_HANDLER(error) + + // webIDL: attribute Function? onclose; + IMPL_EVENT_HANDLER(close) + + // webIDL: readonly attribute DOMString extensions; + void GetExtensions(nsAString& aResult); + + // webIDL: readonly attribute DOMString protocol; + void GetProtocol(nsAString& aResult); + + // webIDL: void close(optional unsigned short code, + // optional DOMString reason): + void Close(const Optional<uint16_t>& aCode, + const Optional<nsAString>& aReason, ErrorResult& aRv); + + // webIDL: attribute Function? onmessage; + IMPL_EVENT_HANDLER(message) + + // webIDL: attribute DOMString binaryType; + dom::BinaryType BinaryType() const; + void SetBinaryType(dom::BinaryType aData); + + // webIDL: void send(DOMString|Blob|ArrayBufferView data); + void Send(const nsAString& aData, ErrorResult& aRv); + void Send(Blob& aData, ErrorResult& aRv); + void Send(const ArrayBuffer& aData, ErrorResult& aRv); + void Send(const ArrayBufferView& aData, ErrorResult& aRv); + + private: // constructor && destructor + explicit WebSocket(nsIGlobalObject* aGlobal); + virtual ~WebSocket(); + + void SetReadyState(uint16_t aReadyState); + + // These methods actually do the dispatch for various events. + nsresult CreateAndDispatchSimpleEvent(const nsAString& aName); + nsresult CreateAndDispatchMessageEvent(const nsACString& aData, + bool aIsBinary); + nsresult CreateAndDispatchCloseEvent(bool aWasClean, uint16_t aCode, + const nsAString& aReason); + + // if there are "strong event listeners" (see comment in WebSocket.cpp) or + // outgoing not sent messages then this method keeps the object alive + // when js doesn't have strong references to it. + void UpdateMustKeepAlive(); + // ATTENTION, when calling this method the object can be released + // (and possibly collected). + void DontKeepAliveAnyMore(); + + private: + WebSocket(const WebSocket& x) = delete; // prevent bad usage + WebSocket& operator=(const WebSocket& x) = delete; + + void Send(nsIInputStream* aMsgStream, const nsACString& aMsgString, + uint32_t aMsgLength, bool aIsBinary, ErrorResult& aRv); + + void AssertIsOnTargetThread() const; + + // Raw pointer because this WebSocketImpl is created, managed and destroyed by + // WebSocket. + WebSocketImpl* mImpl; + + bool mIsMainThread; + + bool mKeepingAlive; + bool mCheckMustKeepAlive; + + CheckedUint64 mOutgoingBufferedAmount; + + // related to the WebSocket constructor steps + nsString mURI; + nsString mEffectiveURL; // after redirects + nsCString mEstablishedExtensions; + nsCString mEstablishedProtocol; + + dom::BinaryType mBinaryType; + + // This mutex protects mReadyState that is the only variable that is used in + // different threads. + mozilla::Mutex mMutex; + + // This value should not be used directly but use ReadyState() instead. + uint16_t mReadyState; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/websocket/moz.build b/dom/websocket/moz.build new file mode 100644 index 0000000000..fbd2399e42 --- /dev/null +++ b/dom/websocket/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Networking") + +EXPORTS.mozilla.dom += [ + "WebSocket.h", +] + +UNIFIED_SOURCES += [ + "WebSocket.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "tests/chrome.ini", +] + +MOCHITEST_MANIFESTS += [ + "tests/mochitest.ini", + "tests/websocket_hybi/mochitest.ini", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/websocket/tests/.eslintrc.js b/dom/websocket/tests/.eslintrc.js new file mode 100644 index 0000000000..721e0938af --- /dev/null +++ b/dom/websocket/tests/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/mochitest-test", "plugin:mozilla/chrome-test"], +}; diff --git a/dom/websocket/tests/chrome.ini b/dom/websocket/tests/chrome.ini new file mode 100644 index 0000000000..b781f62e6e --- /dev/null +++ b/dom/websocket/tests/chrome.ini @@ -0,0 +1,4 @@ +[DEFAULT] +skip-if = os == 'android' + +[test_websocket_frame.html] diff --git a/dom/websocket/tests/file_bug1384658.html b/dom/websocket/tests/file_bug1384658.html new file mode 100644 index 0000000000..9db632831e --- /dev/null +++ b/dom/websocket/tests/file_bug1384658.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<script> + onload = function() { + function done(success) { + var bc = new BroadcastChannel("test_channel"); + bc.postMessage({success}); + bc.close(); + } + try { + new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic"); + done(true); // no hang! + } catch (e) { + done(false); + } + } +</script> +</html> diff --git a/dom/websocket/tests/file_websocket_basic_wsh.py b/dom/websocket/tests/file_websocket_basic_wsh.py new file mode 100644 index 0000000000..050e7ccc10 --- /dev/null +++ b/dom/websocket/tests/file_websocket_basic_wsh.py @@ -0,0 +1,31 @@ +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + # must set request.ws_protocol to the selected version from ws_requested_protocols + request.ws_protocol = request.ws_requested_protocols[0] + + if request.ws_protocol == "error": + raise ValueError("Error") + pass + + +def web_socket_transfer_data(request): + while True: + line = msgutil.receive_message(request) + if line == "protocol": + msgutil.send_message(request, request.ws_protocol) + continue + + if line == "resource": + msgutil.send_message(request, request.ws_resource) + continue + + if line == "origin": + msgutil.send_message(request, request.ws_origin) + continue + + msgutil.send_message(request, line) + + if line == "end": + return diff --git a/dom/websocket/tests/file_websocket_bigBlob_wsh.py b/dom/websocket/tests/file_websocket_bigBlob_wsh.py new file mode 100644 index 0000000000..5ccc85f3a0 --- /dev/null +++ b/dom/websocket/tests/file_websocket_bigBlob_wsh.py @@ -0,0 +1,11 @@ +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + while True: + line = msgutil.receive_message(request) + msgutil.send_message(request, line, True, True) diff --git a/dom/websocket/tests/file_websocket_hello_wsh.py b/dom/websocket/tests/file_websocket_hello_wsh.py new file mode 100644 index 0000000000..5711d2283c --- /dev/null +++ b/dom/websocket/tests/file_websocket_hello_wsh.py @@ -0,0 +1,12 @@ +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + resp = "Test" + if msgutil.receive_message(request) == "data": + resp = "Hello world!" + msgutil.send_message(request, resp) diff --git a/dom/websocket/tests/file_websocket_http_resource.txt b/dom/websocket/tests/file_websocket_http_resource.txt new file mode 100644 index 0000000000..35dc67f08d --- /dev/null +++ b/dom/websocket/tests/file_websocket_http_resource.txt @@ -0,0 +1 @@ +server data diff --git a/dom/websocket/tests/file_websocket_permessage_deflate_disabled_wsh.py b/dom/websocket/tests/file_websocket_permessage_deflate_disabled_wsh.py new file mode 100644 index 0000000000..c588ebc77c --- /dev/null +++ b/dom/websocket/tests/file_websocket_permessage_deflate_disabled_wsh.py @@ -0,0 +1,19 @@ +from mod_pywebsocket import msgutil +from mod_pywebsocket import common + + +def web_socket_do_extra_handshake(request): + if request.ws_requested_extensions is not None: + for extension_request in request.ws_requested_extensions: + if extension_request.name() == "permessage-deflate": + raise ValueError("permessage-deflate should not be offered") + + +def web_socket_transfer_data(request): + while True: + rcvd = msgutil.receive_message(request) + opcode = request.ws_stream.get_last_received_opcode() + if opcode == common.OPCODE_BINARY: + msgutil.send_message(request, rcvd, binary=True) + elif opcode == common.OPCODE_TEXT: + msgutil.send_message(request, rcvd) diff --git a/dom/websocket/tests/file_websocket_permessage_deflate_params_wsh.py b/dom/websocket/tests/file_websocket_permessage_deflate_params_wsh.py new file mode 100644 index 0000000000..8387569645 --- /dev/null +++ b/dom/websocket/tests/file_websocket_permessage_deflate_params_wsh.py @@ -0,0 +1,25 @@ +from mod_pywebsocket import msgutil +from mod_pywebsocket import common + + +def web_socket_do_extra_handshake(request): + deflate_found = False + + if request.ws_extension_processors is not None: + for extension_processor in request.ws_extension_processors: + if extension_processor.name() == "deflate": + extension_processor.set_client_no_context_takeover(True) + deflate_found = True + + if deflate_found is False: + raise ValueError("deflate extension processor not found") + + +def web_socket_transfer_data(request): + while True: + rcvd = msgutil.receive_message(request) + opcode = request.ws_stream.get_last_received_opcode() + if opcode == common.OPCODE_BINARY: + msgutil.send_message(request, rcvd, binary=True) + elif opcode == common.OPCODE_TEXT: + msgutil.send_message(request, rcvd) diff --git a/dom/websocket/tests/file_websocket_permessage_deflate_rejected_wsh.py b/dom/websocket/tests/file_websocket_permessage_deflate_rejected_wsh.py new file mode 100644 index 0000000000..e0a99467f3 --- /dev/null +++ b/dom/websocket/tests/file_websocket_permessage_deflate_rejected_wsh.py @@ -0,0 +1,25 @@ +from mod_pywebsocket import msgutil +from mod_pywebsocket import common + + +def web_socket_do_extra_handshake(request): + deflate_removed = False + + if request.ws_extension_processors is not None: + for extension_processor in request.ws_extension_processors: + if extension_processor.name() == "deflate": + request.ws_extension_processors.remove(extension_processor) + deflate_removed = True + + if deflate_removed is False: + raise ValueError("deflate extension processor not found") + + +def web_socket_transfer_data(request): + while True: + rcvd = msgutil.receive_message(request) + opcode = request.ws_stream.get_last_received_opcode() + if opcode == common.OPCODE_BINARY: + msgutil.send_message(request, rcvd, binary=True) + elif opcode == common.OPCODE_TEXT: + msgutil.send_message(request, rcvd) diff --git a/dom/websocket/tests/file_websocket_permessage_deflate_wsh.py b/dom/websocket/tests/file_websocket_permessage_deflate_wsh.py new file mode 100644 index 0000000000..8075829481 --- /dev/null +++ b/dom/websocket/tests/file_websocket_permessage_deflate_wsh.py @@ -0,0 +1,24 @@ +from mod_pywebsocket import msgutil +from mod_pywebsocket import common + + +def web_socket_do_extra_handshake(request): + pmce_offered = False + + if request.ws_requested_extensions is not None: + for extension_request in request.ws_requested_extensions: + if extension_request.name() == "permessage-deflate": + pmce_offered = True + + if pmce_offered is False: + raise ValueError("permessage-deflate not offered") + + +def web_socket_transfer_data(request): + while True: + rcvd = msgutil.receive_message(request) + opcode = request.ws_stream.get_last_received_opcode() + if opcode == common.OPCODE_BINARY: + msgutil.send_message(request, rcvd, binary=True) + elif opcode == common.OPCODE_TEXT: + msgutil.send_message(request, rcvd) diff --git a/dom/websocket/tests/file_websocket_wsh.py b/dom/websocket/tests/file_websocket_wsh.py new file mode 100644 index 0000000000..b9301276c0 --- /dev/null +++ b/dom/websocket/tests/file_websocket_wsh.py @@ -0,0 +1,164 @@ +from mod_pywebsocket import msgutil + +import time +import struct + +# see the list of tests in test_websocket.html + + +def web_socket_do_extra_handshake(request): + # must set request.ws_protocol to the selected version from ws_requested_protocols + for x in request.ws_requested_protocols: + if x != "test-does-not-exist": + request.ws_protocol = x + break + + if request.ws_protocol == "test-2.1": + time.sleep(3) + elif request.ws_protocol == "test-9": + time.sleep(3) + elif request.ws_protocol == "test-10": + time.sleep(3) + elif request.ws_protocol == "test-19": + raise ValueError("Aborting (test-19)") + elif request.ws_protocol == "test-20" or request.ws_protocol == "test-17": + time.sleep(3) + elif request.ws_protocol == "test-22": + # The timeout is 5 seconds + time.sleep(13) + elif request.ws_protocol == "test-41b": + request.sts = "max-age=100" + elif request.ws_protocol == "test-49": + # subprotocols are compared case-sensitively, so this should fail + request.ws_protocol = "teST-49" + else: + pass + + +# Behave according to recommendation of RFC 6455, section # 5.5.1: +# "When sending a Close frame in response, the endpoint typically echos the +# status code it received." +# - Without this, pywebsocket replies with 1000 to any close code. +# +# Note that this function is only called when the client initiates the close + + +def web_socket_passive_closing_handshake(request): + if request.ws_close_code == 1005: + return None, None + return request.ws_close_code, request.ws_close_reason + + +def web_socket_transfer_data(request): + if request.ws_protocol == "test-2.1" or request.ws_protocol == "test-2.2": + msgutil.close_connection(request) + elif request.ws_protocol == "test-6": + resp = "wrong message" + if msgutil.receive_message(request) == "1": + resp = "2" + msgutil.send_message(request, resp.decode("utf-8")) + resp = "wrong message" + if msgutil.receive_message(request) == "3": + resp = "4" + msgutil.send_message(request, resp.decode("utf-8")) + resp = "wrong message" + if msgutil.receive_message(request) == "5": + resp = "あいうえお" + msgutil.send_message(request, resp.decode("utf-8")) + msgutil.close_connection(request) + elif request.ws_protocol == "test-7": + msgutil.send_message(request, "test-7 data") + elif request.ws_protocol == "test-10": + msgutil.close_connection(request) + elif request.ws_protocol == "test-11": + resp = "wrong message" + if msgutil.receive_message(request) == "client data": + resp = "server data" + msgutil.send_message(request, resp.decode("utf-8")) + elif request.ws_protocol == "test-12": + msg = msgutil.receive_message(request) + if msg == u"a\ufffdb": + # converted unpaired surrogate in UTF-16 to UTF-8 OK + msgutil.send_message(request, "SUCCESS") + else: + msgutil.send_message( + request, + "FAIL got '" + msg + "' instead of string with replacement char'", + ) + elif request.ws_protocol == "test-13": + # first one binary message containing the byte 0x61 ('a') + request.connection.write("\xff\x01\x61") + # after a bad utf8 message + request.connection.write("\x01\x61\xff") + msgutil.close_connection(request) + elif request.ws_protocol == "test-14": + msgutil.close_connection(request) + msgutil.send_message(request, "server data") + elif request.ws_protocol == "test-15": + # DISABLED: close_connection hasn't supported 2nd 'abort' argument for a + # long time. Passing extra arg was causing exception, which conveniently + # caused abort :) but as of pywebsocket v606 raising an exception here no + # longer aborts, and there's no obvious way to close TCP connection w/o + # sending websocket CLOSE. + raise RuntimeError("test-15 should be disabled for now") + # msgutil.close_connection(request, True) # OBSOLETE 2nd arg + # return + elif request.ws_protocol == "test-17" or request.ws_protocol == "test-21": + time.sleep(2) + resp = "wrong message" + if msgutil.receive_message(request) == "client data": + resp = "server data" + msgutil.send_message(request, resp.decode("utf-8")) + time.sleep(2) + msgutil.close_connection(request) + elif request.ws_protocol == "test-20": + msgutil.send_message(request, "server data") + msgutil.close_connection(request) + elif request.ws_protocol == "test-34": + request.ws_stream.close_connection(1001, "going away now") + elif request.ws_protocol == "test-35a": + while not request.client_terminated: + msgutil.receive_message(request) + global test35code + test35code = request.ws_close_code + global test35reason + test35reason = request.ws_close_reason + elif request.ws_protocol == "test-35b": + request.ws_stream.close_connection(test35code + 1, test35reason) + elif request.ws_protocol == "test-37b": + while not request.client_terminated: + msgutil.receive_message(request) + global test37code + test37code = request.ws_close_code + global test37reason + test37reason = request.ws_close_reason + elif request.ws_protocol == "test-37c": + request.ws_stream.close_connection(test37code, test37reason) + elif request.ws_protocol == "test-42": + # Echo back 3 messages + msgutil.send_message(request, msgutil.receive_message(request)) + msgutil.send_message(request, msgutil.receive_message(request)) + msgutil.send_message(request, msgutil.receive_message(request)) + elif request.ws_protocol == "test-44": + rcv = msgutil.receive_message(request) + # check we received correct binary msg + if len(rcv) == 3 and ord(rcv[0]) == 5 and ord(rcv[1]) == 0 and ord(rcv[2]) == 7: + # reply with binary msg 0x04 + msgutil.send_message(request, struct.pack("cc", chr(0), chr(4)), True, True) + else: + msgutil.send_message(request, "incorrect binary msg received!") + elif request.ws_protocol == "test-45": + rcv = msgutil.receive_message(request) + # check we received correct binary msg + if rcv == "flob": + # send back same blob as binary msg + msgutil.send_message(request, rcv, True, True) + else: + msgutil.send_message( + request, "incorrect binary msg received: '" + rcv + "'" + ) + elif request.ws_protocol == "test-46": + msgutil.send_message(request, "client must drop this if close was called") + + while not request.client_terminated: + msgutil.receive_message(request) diff --git a/dom/websocket/tests/frame_bug1384658.html b/dom/websocket/tests/frame_bug1384658.html new file mode 100644 index 0000000000..c13cb17f3a --- /dev/null +++ b/dom/websocket/tests/frame_bug1384658.html @@ -0,0 +1,13 @@ +<html> +<body> + <form action="file_bug1384658.html" method="GET" id="form"> + <input type='submit' name='y'> + <input type='hidden' name='x'> + </form> + <script> +onload = function() { + document.getElementById("form").submit(); +}; + </script> +</body> +</html> diff --git a/dom/websocket/tests/iframe_webSocket_sandbox.html b/dom/websocket/tests/iframe_webSocket_sandbox.html new file mode 100644 index 0000000000..0e6d1d97bf --- /dev/null +++ b/dom/websocket/tests/iframe_webSocket_sandbox.html @@ -0,0 +1,65 @@ +<html><body> +<iframe id="frame" sandbox="allow-scripts allow-popups"></iframe> +<script type="application/javascript"> +onmessage = function(e) { + parent.postMessage(e.data, '*'); +} + +var ifr = document.getElementById('frame'); + +if (location.search == '?nested') { + var url = new URL(location); + url.search = ""; + ifr.src = url.href; +} else if (location.search == '?popup') { + var url = new URL(location); + url.search = "?opener"; + + ifr.srcdoc = "<html><script>" + + "window.open('" + url.href + "', 'foobar');" + + "onmessage = function(e) { " + + " parent.postMessage(e.data, '*'); " + + "}" + + "</scr" + "ipt></html>"; +} else if (location.search == '?opener') { + try{ + var socket = new WebSocket('ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic'); + socket.onerror = function(e) { + opener.postMessage('WS onerror', '*'); + close(); + }; + socket.onopen = function(event) { + opener.postMessage('WS onopen', '*'); + close(); + }; + } catch(e) { + if (e.name == 'SecurityError') { + opener.postMessage('WS Throws!', '*'); + } else { + opener.postMessage('WS Throws something else!', '*'); + } + close(); + } +} else { + ifr.srcdoc = ` + <html><script> + try{ + var socket = new WebSocket('ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic'); + socket.onerror = function(e) { + parent.postMessage('WS onerror', '*'); + }; + socket.onopen = function(event) { + parent.postMessage('WS onopen', '*'); + }; + } catch(e) { + if (e.name == 'SecurityError') { + parent.postMessage('WS Throws!', '*'); + } else { + parent.postMessage('WS Throws something else!', '*'); + } + } + </scr`+`ipt> + </html>`; +} +</script> +</body></html> diff --git a/dom/websocket/tests/mochitest.ini b/dom/websocket/tests/mochitest.ini new file mode 100644 index 0000000000..7e66cbefeb --- /dev/null +++ b/dom/websocket/tests/mochitest.ini @@ -0,0 +1,55 @@ +[DEFAULT] +skip-if = toolkit == 'android' # bug 982828 +support-files = + !/dom/events/test/event_leak_utils.js + file_websocket_basic_wsh.py + file_websocket_hello_wsh.py + file_websocket_http_resource.txt + file_websocket_permessage_deflate_wsh.py + file_websocket_permessage_deflate_disabled_wsh.py + file_websocket_permessage_deflate_rejected_wsh.py + file_websocket_permessage_deflate_params_wsh.py + file_websocket_wsh.py + websocket_helpers.js + websocket_tests.js + websocket_worker_helpers.js + +[test_bug1081686.html] +[test_bug1384658.html] +support-files = window_bug1384658.html frame_bug1384658.html file_bug1384658.html +[test_event_listener_leaks.html] +support-files = file_websocket_bigBlob_wsh.py +skip-if = (os == "win" && processor == "aarch64") #bug 1535784 +[test_websocket1.html] +[test_websocket2.html] +[test_websocket3.html] +[test_websocket4.html] +[test_websocket5.html] +[test_websocket_basic.html] +[test_websocket_hello.html] +[test_websocket_permessage_deflate.html] +[test_webSocket_sandbox.html] +support-files = iframe_webSocket_sandbox.html +[test_worker_websocket1.html] +support-files = websocket_worker1.js +[test_worker_websocket2.html] +support-files = websocket_worker2.js +[test_worker_websocket3.html] +support-files = websocket_worker3.js +[test_worker_websocket4.html] +support-files = websocket_worker4.js +[test_worker_websocket5.html] +support-files = websocket_worker5.js +[test_worker_websocket_basic.html] +support-files = websocket_basic_worker.js +[test_worker_websocket_https.html] +support-files = websocket_worker_https.html websocket_https_worker.js +[test_worker_websocket_loadgroup.html] +support-files = websocket_loadgroup_worker.js +[test_webSocket_sharedWorker.html] +support-files = webSocket_sharedWorker.js +[test_websocket_bigBlob.html] +support-files = file_websocket_bigBlob_wsh.py +[test_webSocket_longString.html] +[test_webSocket_no_duplicate_packet.html] +scheme = https diff --git a/dom/websocket/tests/test_bug1081686.html b/dom/websocket/tests/test_bug1081686.html new file mode 100644 index 0000000000..debcd97184 --- /dev/null +++ b/dom/websocket/tests/test_bug1081686.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>bug 1081686</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="testWebSocket()"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var ws; + +function forcegc() +{ + SpecialPowers.forceGC(); + SpecialPowers.gc(); + setTimeout(function() + { + SpecialPowers.gc(); + }, 0); +} + +function testWebSocket () { + ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello"); + ws.onopen = function(e) { + ws.send("data"); + } + ws.onclose = function(e) { + forcegc(); + setTimeout(function() { + is(ws.readyState, 3, 'WebSocket is closed'); + is(ws.bufferedAmount, 0, 'WebSocket.bufferedAmount should be empty.'); + is(ws.binaryType, 'blob', 'WebSocket.binaryType is blob'); + ws.binaryType = 'arraybuffer'; + is(ws.binaryType, 'arraybuffer', 'WebSocket.binaryType is arraybuffer'); + is(ws.url, 'ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello', 'WebSocket.url is correct'); + ws.close(); + ws.send('foobar'); + SimpleTest.finish(); + }, 1000); + } + + ws.onerror = function(e) { + ok(false, "onerror called!"); + SimpleTest.finish(); + } + ws.onmessage = function(e) { + is(e.data, "Hello world!", "Wrong data"); + ws.close(); + } +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +</script> +</pre> +<div> + + +</div> + + +</body> +</html> diff --git a/dom/websocket/tests/test_bug1384658.html b/dom/websocket/tests/test_bug1384658.html new file mode 100644 index 0000000000..c90e4c8bdf --- /dev/null +++ b/dom/websocket/tests/test_bug1384658.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1384658 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1384658</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1384658 **/ + +function test_frameset() { + var bc = new BroadcastChannel("test_channel"); + bc.postMessage("go"); + bc.onmessage = ev => { + ok(ev.data.success, "We didn't hang"); + bc.close(); + test_window(); + }; +} + +function test_window() { + var win = window.open("http://example.com/tests/dom/websocket/tests/window_bug1384658.html", + "_blank", "width=100,height=100"); + var bc = new BroadcastChannel("test_channel"); + bc.onmessage = ev => { + ok(ev.data.success, "We didn't hang"); + bc.close(); + win.close(); + SimpleTest.finish(); + }; +} + +SimpleTest.waitForExplicitFinish(); +// Use nsICookieService.BEHAVIOR_REJECT_TRACKER to not partition BroadcastChannel +// by extra first-party domain information. +SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior", 4]], +}, test_frameset); + + </script> +</head> +<frameset id="frame_set" cols="25%,75%" frameborder="yes" border="5" bordercolor="#008800"> + <frame id="test_frame" name="test_frame" src="frame_bug1384658.html" marginwidth="20" marginheight="20"> +</frameset> +</html> diff --git a/dom/websocket/tests/test_event_listener_leaks.html b/dom/websocket/tests/test_event_listener_leaks.html new file mode 100644 index 0000000000..9a79fa1354 --- /dev/null +++ b/dom/websocket/tests/test_event_listener_leaks.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1450358 - Test WebSocket event listener leak conditions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +// Manipulate WebSocket objects in the frame's context. +// Its important here that we create a listener callback from +// the DOM objects back to the frame's global in order to +// exercise the leak condition. +async function useWebSocket(contentWindow) { + const url = "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_bigBlob"; + let ws = new contentWindow.WebSocket(url); + + ws.onmessage = _ => { + contentWindow.messageCount += 1; + }; + + contentWindow.openCount = 0; + await new Promise((resolve, reject) => { + ws.onopen = _ => { + contentWindow.openCount += 1; + resolve(); + }; + ws.onerror = e => { + contentWindow.errorCount += 1; + reject("websocket error"); + }; + }); + + is(contentWindow.openCount, 1, "open should be received"); +} + +async function runTest() { + try { + await checkForEventListenerLeaks("WebSocket", useWebSocket); + } catch (e) { + ok(false, e); + } finally { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +addEventListener("load", runTest, { once: true }); +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_webSocket_longString.html b/dom/websocket/tests/test_webSocket_longString.html new file mode 100644 index 0000000000..78c8e471c7 --- /dev/null +++ b/dom/websocket/tests/test_webSocket_longString.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta> + <title>WebSocket test - big blob on content side</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="websocket_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic", "test"); +is(ws.readyState, 0, "Initial readyState is 0"); + +const longString = new Array(1024*1024).join('123456789ABCDEF'); + +ws.onopen = function(e) { + is(this, ws, "[onopen()] 'this' should point to the WebSocket."); + ws.send(longString); +}; + +ws.onclose = function(e) { + is(this, ws, "[onclose()] 'this' should point to the WebSocket."); + ok(e.wasClean, "Connection closed cleanly"); + + SimpleTest.executeSoon(SimpleTest.finish); +}; + +ws.onerror = function(e) { + is(this, ws, "[onerror()] 'this' should point to the WebSocket."); + ok(false, "onerror()] should not have been called!"); + SimpleTest.executeSoon(SimpleTest.finish); +}; + +ws.onmessage = function(e) { + is(this, ws, "[onmessage()] 'this' should point to the WebSocket."); + // Do not use |is(e.data, longString, "...");| that results in a _very_ long line. + is(e.data.length, longString.length, "Length of received message"); + ok(e.data === longString, "Content of received message"); + this.close(); +}; + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/websocket/tests/test_webSocket_no_duplicate_packet.html b/dom/websocket/tests/test_webSocket_no_duplicate_packet.html new file mode 100644 index 0000000000..7b2b0fc690 --- /dev/null +++ b/dom/websocket/tests/test_webSocket_no_duplicate_packet.html @@ -0,0 +1,106 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta> + <title>WebSocket test - big blob on content side</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="websocket_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +// Test steps: +// 1. Create a websocket and send 8 chunks of 1MB random data. +// 2. Store the hash of each chunk (1MB of random data). +// 3. Websocket server returns the same data back. +// 4. Calculate the hash again and check the hash is the same as the stored one. + +function genRandomPayload() { + const count = 128; + const chunkSize = 64 * 1024; + let buffer = new Uint8Array(chunkSize * count); + let offset = 0; + for (let i = 0; i < count; i++) { + let data = new Uint8Array(chunkSize); + crypto.getRandomValues(data); + buffer.set(data, offset); + offset += chunkSize; + } + + return buffer; +} + +function genRandomFile() { + return new File([genRandomPayload()], "payload.bin", { + type: 'application/octet-stream' + }); +} + +async function toHexString(buffer) { + let hashBuffer = await crypto.subtle.digest("SHA-256", buffer); + let hashBytes = new Uint8Array(hashBuffer); + let toHex = b => b.toString(16).padStart(2, "0"); + return Array.from(hashBytes, toHex).join(""); +} + +let message_count = 0; +let sentHashArray = []; +async function sendFile(file, ws) { + const oneMiB = 1 * 1024 * 1024; + + let offset = 0; + while (offset < file.size) { + let blob = file.slice(offset, offset + oneMiB); + let buffer = await blob.arrayBuffer(); + let hash = await toHexString(buffer); + sentHashArray.push(hash); + ws.send(buffer); + offset += blob.size; + message_count++; + } +} + +var ws = CreateTestWS("wss://example.com/tests/dom/websocket/tests/file_websocket_bigBlob"); +is(ws.readyState, 0, "Initial readyState is 0"); +ws.binaryType = "blob"; + +ws.onopen = function() { + is(ws.readyState, 1, "Open readyState is 1"); + let file = genRandomFile(); + sendFile(file, ws); +} + +let receivedBlobs = []; +ws.onmessage = function(e) { + ok(e.data instanceof Blob, "We should be receiving a Blob"); + receivedBlobs.push(e.data); + message_count--; + if (message_count == 0) { + ws.close(); + } +} + +async function checkContent() { + is(receivedBlobs.length, sentHashArray.length, "length should be the same"); + for (let index = 0; index < receivedBlobs.length; index++) { + let buffer = await receivedBlobs[index].arrayBuffer(); + let hash = await toHexString(buffer); + is(hash, sentHashArray[index], "hash should be equal"); + } +} + +ws.onclose = function(e) { + is(ws.readyState, 3, "Close readyState is 3"); + checkContent().then(() => { + SimpleTest.finish(); + }); +} + +SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " + + "Expect all sorts of flakiness in this test..."); +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/websocket/tests/test_webSocket_sandbox.html b/dom/websocket/tests/test_webSocket_sandbox.html new file mode 100644 index 0000000000..363afcfe21 --- /dev/null +++ b/dom/websocket/tests/test_webSocket_sandbox.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1252751</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="container"></div> +<iframe id="frame"></iframe> +<script type="application/javascript"> +var urls = [ "https://example.com/tests/dom/websocket/tests/iframe_webSocket_sandbox.html", + "https://example.com/tests/dom/websocket/tests/iframe_webSocket_sandbox.html?nested", + "https://example.com/tests/dom/websocket/tests/iframe_webSocket_sandbox.html?popup" ]; + +onmessage = function(e) { + is(e.data, "WS Throws!", "ws://URI cannot be used by a https iframe"); + runTest(); +} + +function runTest() { + if (!urls.length) { + SimpleTest.finish(); + return; + } + + document.getElementById("frame").src = urls.shift(); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); +</script> +</body> +</html> diff --git a/dom/websocket/tests/test_webSocket_sharedWorker.html b/dom/websocket/tests/test_webSocket_sharedWorker.html new file mode 100644 index 0000000000..9e5c5bbafd --- /dev/null +++ b/dom/websocket/tests/test_webSocket_sharedWorker.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 1090183</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> + +var sw = new SharedWorker('webSocket_sharedWorker.js'); +sw.port.onmessage = function(event) { + if (event.data.type == 'finish') { + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_websocket1.html b/dom/websocket/tests/test_websocket1.html new file mode 100644 index 0000000000..1844a67f58 --- /dev/null +++ b/dom/websocket/tests/test_websocket1.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta> + <title>WebSocket test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="websocket_helpers.js"></script> + <script type="text/javascript" src="websocket_tests.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="testWebSocket()"> +<script class="testbody" type="text/javascript"> + +var tests = [ + test1, // client tries to connect to a http scheme location; + test2, // assure serialization of the connections; + test3, // client tries to connect to an non-existent ws server; + test4, // client tries to connect using a relative url; + test5, // client uses an invalid protocol value; + test6, // counter and encoding check; + test7, // onmessage event origin property check + test8, // client calls close() and the server sends the close frame (with no + // code or reason) in acknowledgement; + test9, // client closes the connection before the ws connection is established; + test10, // client sends a message before the ws connection is established; +]; + +function testWebSocket() { + doTest(); +} + +SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " + + "Expect all sorts of flakiness in this test..."); +SimpleTest.waitForExplicitFinish(); + +</script> + +<div id="feedback"> +</div> + +</body> +</html> diff --git a/dom/websocket/tests/test_websocket2.html b/dom/websocket/tests/test_websocket2.html new file mode 100644 index 0000000000..afbbaba62f --- /dev/null +++ b/dom/websocket/tests/test_websocket2.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta> + <title>WebSocket test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="websocket_helpers.js"></script> + <script type="text/javascript" src="websocket_tests.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="testWebSocket()"> +<script class="testbody" type="text/javascript"> + +var tests = [ + test11, // a simple hello echo; + test12, // client sends a message containing unpaired surrogates + test13, //server sends an invalid message; + test14, // server sends the close frame, it doesn't close the tcp connection + // and it keeps sending normal ws messages; + test15, // server closes the tcp connection, but it doesn't send the close + // frame; + test16, // client calls close() and tries to send a message; + test17, // see bug 572975 - all event listeners set + test18, // client tries to connect to an http resource; + test19, // server closes the tcp connection before establishing the ws + // connection; + test20, // see bug 572975 - only on error and onclose event listeners set +]; + +function testWebSocket() { + doTest(); +} + +SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " + + "Expect all sorts of flakiness in this test..."); +SimpleTest.waitForExplicitFinish(); + +</script> + +<div id="feedback"> +</div> + +</body> +</html> diff --git a/dom/websocket/tests/test_websocket3.html b/dom/websocket/tests/test_websocket3.html new file mode 100644 index 0000000000..cc2e091fda --- /dev/null +++ b/dom/websocket/tests/test_websocket3.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta> + <title>WebSocket test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="websocket_helpers.js"></script> + <script type="text/javascript" src="websocket_tests.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="testWebSocket()"> +<script class="testbody" type="text/javascript"> + +var tests = [ + test21, // see bug 572975 - same as test 17, but delete strong event listeners + // when receiving the message event; + test22, // server takes too long to establish the ws connection; + test23, // should detect WebSocket on window object; + test24, // server rejects sub-protocol string + test25, // ctor with valid empty sub-protocol array + test26, // ctor with invalid sub-protocol array containing 1 empty element + test27, // ctor with invalid sub-protocol array containing an empty element in + // list + test28, // ctor using valid 1 element sub-protocol array + test29, // ctor using all valid 5 element sub-protocol array + test30, // ctor using valid 1 element sub-protocol array with element server + // will reject +]; + +function testWebSocket() { + doTest(); +} + +SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " + + "Expect all sorts of flakiness in this test..."); +SimpleTest.waitForExplicitFinish(); + +</script> + +<div id="feedback"> +</div> + +</body> +</html> diff --git a/dom/websocket/tests/test_websocket4.html b/dom/websocket/tests/test_websocket4.html new file mode 100644 index 0000000000..186a434ab3 --- /dev/null +++ b/dom/websocket/tests/test_websocket4.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta> + <title>WebSocket test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="websocket_helpers.js"></script> + <script type="text/javascript" src="websocket_tests.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="testWebSocket()"> +<script class="testbody" type="text/javascript"> + +var tests = [ + test31, // ctor using valid 2 element sub-protocol array with 1 element server + // will reject and one server will accept + test32, // ctor using invalid sub-protocol array that contains duplicate items + test33, // test for sending/receiving custom close code (but no close reason) + test34, // test for receiving custom close code and reason + test35, // test for sending custom close code and reason + test36, // negative test for sending out of range close code + test37, // negative test for too long of a close reason + test38, // ensure extensions attribute is defined + test39, // a basic wss:// connectivity test + test40, // negative test for wss:// with no cert +]; + +function testWebSocket() { + doTest(); +} + +SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " + + "Expect all sorts of flakiness in this test..."); +SimpleTest.waitForExplicitFinish(); + +</script> + +<div id="feedback"> +</div> + +</body> +</html> diff --git a/dom/websocket/tests/test_websocket5.html b/dom/websocket/tests/test_websocket5.html new file mode 100644 index 0000000000..d86752ed5a --- /dev/null +++ b/dom/websocket/tests/test_websocket5.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta> + <title>WebSocket test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="websocket_helpers.js"></script> + <script type="text/javascript" src="websocket_tests.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="testWebSocket()"> +<script class="testbody" type="text/javascript"> + +var tests = [ + test41, // HSTS + test42, // non-char utf-8 sequences + test43, // Test setting binaryType attribute + test44, // Test sending/receving binary ArrayBuffer + test45, // Test sending/receving binary Blob + test46, // Test that we don't dispatch incoming msgs once in CLOSING state + test47, // Make sure onerror/onclose aren't called during close() + test48, // see bug 1227136 - client calls close() from onopen() and waits + // until WebSocketChannel::mSocketIn is nulled out on socket thread + test49, // Test that we fail if subprotocol returned from server doesn't match +]; + +function testWebSocket() { + doTest(); +} + +SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " + + "Expect all sorts of flakiness in this test..."); +SimpleTest.waitForExplicitFinish(); + +</script> + +<div id="feedback"> +</div> + +</body> +</html> diff --git a/dom/websocket/tests/test_websocket_basic.html b/dom/websocket/tests/test_websocket_basic.html new file mode 100644 index 0000000000..2ae2e690ac --- /dev/null +++ b/dom/websocket/tests/test_websocket_basic.html @@ -0,0 +1,289 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Basic WebSocket test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body onload="testWebSocket()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=472529">Mozilla Bug 472529</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const kUrl = "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic"; + +var gTestElement; +var ws; + +function forcegc() { + SpecialPowers.forceGC(); + SpecialPowers.gc(); +} + +function testWebSocket() { + gTestElement = document.getElementById("test"); + + SimpleTest.executeSoon(testWebSocket1); +} + +/** + * Sends message keywords, then receives their values. + */ +function testWebSocket1() { + gTestElement.textContent = "Running testWebSocket1()"; + + var results = ["test", + "/tests/dom/websocket/tests/file_websocket_basic", + "http://mochi.test:8888", + "end"]; + + ws = new WebSocket(kUrl, "test"); + is(ws.url, kUrl, "[1] WebSocket.url"); + ws.onopen = function(e) { + const params = ["protocol", "resource", "origin", "end"]; + + gTestElement.textContent += "\nSending :"; + for (var i = 0; i < params.length; ++i) { + gTestElement.textContent += " " + params[i]; + ws.send(params[i]); + } + + // Set this before onmessage() is called, so it is displayed once only. + gTestElement.textContent += "\nReceived:"; + }; + ws.onclose = function(e) { + is(results.length, 0, "[1] Number of unreceived messages"); + ok(e.wasClean, "[1] Connection closed cleanly"); + + SimpleTest.executeSoon(testWebSocket2); + }; + ws.onerror = function(e) { + ok(false, "[1] onerror() should not have been called!"); + gTestElement.textContent += "\nonerror() should not have been called!"; + SimpleTest.executeSoon(SimpleTest.finish); + }; + ws.onmessage = function(e) { + is(e.data, results[0], "[1] Received message"); + gTestElement.textContent += " " + e.data; + results.shift(); + }; +} + +/** + * Sends 1000+1 test messages, then receives them. + */ +function testWebSocket2() { + gTestElement.textContent = "Running testWebSocket2()"; + + const displayInterval = 100; + const testCount = 1000; + const testMessage = "test message 2."; + + var messageCount = 0; + + ws = new WebSocket(kUrl, "test"); + ws.onopen = function(e) { + gTestElement.textContent += "\nSending :"; + for (var i = 1; i <= testCount; ++i) { + if (i % displayInterval == 1) { + gTestElement.textContent += " " + i; + } + ws.send(testMessage + i); + } + gTestElement.textContent += " end"; + ws.send("end"); + + // Set this before onmessage() is called, so it is displayed once only. + gTestElement.textContent += "\nReceived:"; + }; + ws.onclose = function(e) { + is(messageCount, testCount + 1, "[2] Number of received messages"); + ok(e.wasClean, "[2] Connection closed cleanly"); + + SimpleTest.executeSoon(testWebSocket3); + }; + ws.onerror = function(e) { + ok(false, "[2] onerror() should not have been called!"); + gTestElement.textContent += "\nonerror() should not have been called!"; + SimpleTest.executeSoon(SimpleTest.finish); + }; + ws.onmessage = function(e) { + ++messageCount; + if (messageCount > testCount) + is(e.data, "end", "[2] Received message"); + else + is(e.data, testMessage + messageCount, "[2] Received message"); + if (messageCount % displayInterval == 1) { + gTestElement.textContent += " " + messageCount; + } + }; +} + +/** + * Sends testcount+1 test messages, then receives them, calling forcegc() at each step. + */ +function testWebSocket3() { + gTestElement.textContent = "Running testWebSocket3() [can take a little while]"; + + const displayInterval = 10; + const testCount = 10; + const testMessage = "test message 3."; + + var messageCount = 0; + + ws = new WebSocket(kUrl, "test"); + // Set this before onopen() is called, + // otherwise its display would be delayed by forcegc() calls... + gTestElement.textContent += "\nSending :"; + ws.onopen = function(e) { + for (var i = 1; i <= testCount; ++i) { + forcegc(); + if (i % displayInterval == 1) { + // Actual display is delayed by forcegc() calls... + gTestElement.textContent += " " + i; + } + ws.send(testMessage + i); + } + forcegc(); + gTestElement.textContent += " end"; + ws.send("end"); + + // Set this before onmessage() is called, so it is displayed once only. + gTestElement.textContent += "\nReceived:"; + }; + ws.onclose = function(e) { + is(messageCount, testCount + 1, "[3] Number of received messages"); + ok(e.wasClean, "[3] Connection closed cleanly"); + + SimpleTest.executeSoon(testWebSocket4); + }; + ws.onerror = function(e) { + ok(false, "[3] onerror() should not have been called!"); + gTestElement.textContent += "\nonerror() should not have been called!"; + SimpleTest.executeSoon(SimpleTest.finish); + }; + ws.onmessage = function(e) { + forcegc(); + ++messageCount; + if (messageCount > testCount) + is(e.data, "end", "[3] Received message"); + else + is(e.data, testMessage + messageCount, "[3] Received message"); + if (messageCount % displayInterval == 1) { + // Actual display is delayed by forcegc() call(s)... + gTestElement.textContent += " " + messageCount; + } + }; +} + +/** + * Sends a huge test message, then receives it, then closes the WebSocket from client-side. + */ +function testWebSocket4() { + gTestElement.textContent = "Running testWebSocket4()"; + + // String length = 13 + ((10,000 - 1) * 26) + 11 = 259,998 = almost 254 KiB. + const longString = "messageStart " + new Array(10000).join(" -huge WebSocket message- ") + " messageEnd"; + + ws = new WebSocket(kUrl, "test"); + ws.onopen = function(e) { + is(this, ws, "[4, onopen()] 'this' should point to the WebSocket."); + gTestElement.textContent += "\nSending the huge message"; + ws.send(longString); + }; + ws.onclose = function(e) { + is(this, ws, "[4, onclose()] 'this' should point to the WebSocket."); + ok(e.wasClean, "[4] Connection closed cleanly"); + + SimpleTest.executeSoon(testWebSocket5); + }; + ws.onerror = function(e) { + is(this, ws, "[4, onerror()] 'this' should point to the WebSocket."); + ok(false, "[4, onerror()] should not have been called!"); + gTestElement.textContent += "\nonerror() should not have been called!"; + SimpleTest.executeSoon(SimpleTest.finish); + }; + ws.onmessage = function(e) { + is(this, ws, "[4, onmessage()] 'this' should point to the WebSocket."); + // Do not use |is(e.data, longString, "...");| that results in a _very_ long line. + is(e.data.length, longString.length, "[4] Length of received message"); + ok(e.data == longString, "[4] Content of received message"); + gTestElement.textContent += "\nReceived the huge message"; + this.close(); + }; +} + +/** + * Closes the WebSocket from client-side, then sends a test message that should be buffered. + */ +function testWebSocket5() { + gTestElement.textContent = "Running testWebSocket5()"; + + ws = new WebSocket(kUrl, "test"); + ws.onopen = function(e) { + is(this.bufferedAmount, 0, "[5] Length of empty buffer before closing"); + this.close(); + }; + ws.onclose = function(e) { + ok(e.wasClean, "[5] Connection closed cleanly"); + is(this.bufferedAmount, 0, "[5] Length of empty buffer after closing"); + + var msg = "test message to be buffered"; + this.send(msg); + is(this.bufferedAmount, msg.length, "[5] Length of buffered message sent after closing"); + + gTestElement.textContent += "\ntestWebSocket5() completed"; + + SimpleTest.executeSoon(testWebSocket6); + }; + ws.onerror = function(e) { + ok(false, "[5] onerror() should not have been called!"); + gTestElement.textContent += "\nonerror() should not have been called!"; + SimpleTest.executeSoon(SimpleTest.finish); + }; +} + +function testWebSocket6() { + gTestElement.textContent = "Running testWebSocket6()"; + + var msgReceived = false; + ws = new WebSocket(kUrl, "test"); + ws.onopen = function(e) { + gTestElement.textContent += "\nSending ©"; + ws.send("©"); + gTestElement.textContent += " end"; + ws.send("end"); + }; + ws.onclose = function(e) { + ok(msgReceived, "[6] Number of received messages"); + ok(e.wasClean, "[6] Connection closed cleanly"); + + SimpleTest.executeSoon(SimpleTest.finish); + }; + ws.onerror = function(e) { + ok(false, "[6] onerror() should not have been called!"); + gTestElement.textContent += "\nonerror() should not have been called!"; + SimpleTest.executeSoon(SimpleTest.finish); + }; + + ws.onmessage = function(e) { + if (msgReceived) { + is(e.data, "end", "[6] Received message"); + } else { + gTestElement.textContent += "\nReceived: " + e.data; + is(e.data, "©", "[6] Received message"); + msgReceived = true; + } + }; +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_websocket_bigBlob.html b/dom/websocket/tests/test_websocket_bigBlob.html new file mode 100644 index 0000000000..9db01d6a7f --- /dev/null +++ b/dom/websocket/tests/test_websocket_bigBlob.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta> + <title>WebSocket test - big blob on content side</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="websocket_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +var ws = CreateTestWS("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_bigBlob"); +is(ws.readyState, 0, "Initial readyState is 0"); +ws.binaryType = "blob"; + +ws.onopen = function() { + is(ws.readyState, 1, "Open readyState is 1"); + ws.send(new Blob([new Array(1024*1024).join('123456789ABCDEF')])); +} + +let receivedBlob; +ws.onmessage = function(e) { + ok(e.data instanceof Blob, "We should be receiving a Blob"); + receivedBlob = e.data; + ws.close(); +} + +ws.onclose = function(e) { + is(ws.readyState, 3, "Close readyState is 3"); + + // check blob contents + var reader = new FileReader(); + reader.onload = function(event) { + is(reader.result, new Array(1024*1024).join('123456789ABCDEF'), "All data matches"); + } + + reader.onerror = function(event) { + ok(false, "Something bad happen."); + } + + reader.onloadend = function(event) { + SimpleTest.finish(); + } + + reader.readAsBinaryString(receivedBlob); +} + +SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " + + "Expect all sorts of flakiness in this test..."); +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/websocket/tests/test_websocket_frame.html b/dom/websocket/tests/test_websocket_frame.html new file mode 100644 index 0000000000..26910246e4 --- /dev/null +++ b/dom/websocket/tests/test_websocket_frame.html @@ -0,0 +1,163 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Basic websocket frame interception test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +</head> +<body> +<script class="testbody" type="text/javascript"> + +const URI = "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic"; + +var frameReceivedCounter = 0; +var frameSentCounter = 0; +var webSocketCreatedCounter = 0; +var webSocketOpenedCounter = 0; +var webSocketMessageAvailableCounter = 0; +var webSocketClosedCounter = 0; + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +var tests = [ + { payload: "Hello world!" }, + { payload: (function() { var buffer = ""; for (var i = 0; i < 120; ++i) buffer += i; return buffer; }()) }, +] + +var innerId = window.top.windowGlobalChild.innerWindowId; +ok(innerId, "We have a valid innerWindowID: " + innerId); + +var service = Cc["@mozilla.org/websocketevent/service;1"] + .getService(Ci.nsIWebSocketEventService); +ok(!!service, "We have the nsIWebSocketEventService"); + +var listener = { + QueryInterface: ChromeUtils.generateQI(["nsIWebSocketEventListener"]), + + webSocketCreated(aWebSocketSerialID, aURI, aProtocols) { + info("WebSocketCreated"); + + is(aURI, URI, "URI matches"); + is(aProtocols, "frame", "Protocol matches"); + + webSocketCreatedCounter++; + }, + + webSocketOpened(aWebSocketSerialID, aEffectiveURI, aProtocols, aExtensions, httpChannelId) { + info("WebSocketOpened"); + + is(aEffectiveURI, URI, "EffectiveURI matches"); + is(aProtocols, "frame", "Protocol matches"); + is(aExtensions, "permessage-deflate", "No extensions"); + ok(httpChannelId > 0, "Channel ID received"); + + webSocketOpenedCounter++; + }, + + webSocketMessageAvailable(aWebSocketSerialID, aData, aMessageType) { + info("WebSocketMessageAvailable"); + + if (tests.length) { + is(aData, tests[0].payload, "Message matches!"); + is(aMessageType, Ci.nsIWebSocketEventListener.TYPE_STRING, "The type is 'string'"); + + webSocketMessageAvailableCounter++; + + tests.shift(); + if (tests.length) { + ws.send(tests[0].payload); + } else { + ws.send("end"); + } + } + }, + + webSocketClosed(aWebSocketSerialID, aWasClean, + aCode, aReason) { + info("WebSocketClosed"); + + ok(aWasClean, "The socket is closed in a clean state"); + is(aCode, 1000, "Exit code 1000"); + ok(!aReason.length, "No reason"); + + webSocketClosedCounter++; + checkListener(); + }, + + frameReceived(aWebSocketSerialID, aFrame) { + ok(!!aFrame, "We have received a frame"); + + if (tests.length) { + ok(aFrame.timeStamp, "Checking timeStamp: " + aFrame.timeStamp); + is(aFrame.finBit, true, "Checking finBit"); + is(aFrame.rsvBit1, true, "Checking rsvBit1"); + is(aFrame.rsvBit2, false, "Checking rsvBit2"); + is(aFrame.rsvBit3, false, "Checking rsvBit3"); + is(aFrame.opCode, aFrame.OPCODE_TEXT, "Checking opCode"); + is(aFrame.maskBit, false, "Checking maskBit"); + is(aFrame.mask, 0, "Checking mask"); + is(aFrame.payload, tests[0].payload, "Checking payload: " + aFrame.payload); + } + + frameReceivedCounter++; + }, + + frameSent(aWebSocketSerialID, aFrame) { + ok(!!aFrame, "We have sent a frame"); + + if (tests.length) { + ok(aFrame.timeStamp, "Checking timeStamp: " + aFrame.timeStamp); + is(aFrame.finBit, true, "Checking finBit"); + is(aFrame.rsvBit1, true, "Checking rsvBit1"); + is(aFrame.rsvBit2, false, "Checking rsvBit2"); + is(aFrame.rsvBit3, false, "Checking rsvBit3"); + is(aFrame.opCode, aFrame.OPCODE_TEXT, "Checking opCode"); + is(aFrame.maskBit, true, "Checking maskBit"); + ok(!!aFrame.mask, "Checking mask: " + aFrame.mask); + is(aFrame.payload, tests[0].payload, "Checking payload: " + aFrame.payload); + } + + frameSentCounter++; + } +}; + +service.addListener(innerId, listener); +ok(true, "Listener added"); + +function checkListener() { + service.removeListener(innerId, listener); + + ok(frameReceivedCounter, "We received some frames!"); + ok(frameSentCounter, "We sent some frames!"); + ok(webSocketCreatedCounter, "We have a create notification"); + ok(webSocketOpenedCounter, "We have a open notification"); + ok(webSocketMessageAvailableCounter, "We have a messageAvailable notification"); + ok(webSocketClosedCounter, "We have a close notification"); + SimpleTest.finish(); +} + +var ws = new WebSocket(URI, "frame"); +ws.onopen = function(e) { + info("onopen"); + + ws.send(tests[0].payload); +} + +ws.onclose = function(e) { + info("onclose"); +} + +ws.onmessage = function(e) { + info("onmessage"); + if (tests.length) { + is(e.data, tests[0].payload, "Wrong data"); + } +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/websocket/tests/test_websocket_hello.html b/dom/websocket/tests/test_websocket_hello.html new file mode 100644 index 0000000000..8508fba97b --- /dev/null +++ b/dom/websocket/tests/test_websocket_hello.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Basic websocket test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="testWebSocket()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=472529">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var ws; + +function testWebSocket () { + ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello"); + ws.onopen = function(e) { + ws.send("data"); + } + ws.onclose = function(e) { + } + ws.onerror = function(e) { + ok(false, "onerror called!"); + SimpleTest.finish(); + } + ws.onmessage = function(e) { + is(e.data, "Hello world!", "Wrong data"); + ws.close(); + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<div> + + +</div> + + +</body> +</html> diff --git a/dom/websocket/tests/test_websocket_permessage_deflate.html b/dom/websocket/tests/test_websocket_permessage_deflate.html new file mode 100644 index 0000000000..ecf66419f2 --- /dev/null +++ b/dom/websocket/tests/test_websocket_permessage_deflate.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Basic test of permessage compression websocket extension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="loadDeflate()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=792831">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var ws; +var textMessage = "This is a text message"; +var binaryMessage = "This is a binary message"; +var testIdx = 0; +var sendText = true; + +tests = [ + // enable PMCE + [ true, true, "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_permessage_deflate" ], + // disable PMCE + [ false, false, "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_permessage_deflate_disabled" ], + // server rejects offered PMCE + [ true, false, "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_permessage_deflate_rejected" ], + // server returns parameters in the handshake + [ true, true, "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_permessage_deflate_params" ] +] + +function ab2str(buf) { + return String.fromCharCode.apply(null, new Uint16Array(buf)); +} + +function str2ab(str) { + var buf = new ArrayBuffer(str.length*2); + var bufView = new Uint16Array(buf); + for (var i=0, strLen=str.length; i<strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} + +function sendMessage() { + if (sendText) { + ws.send(textMessage); + } else { + ws.binaryType = "arraybuffer"; + ws.send(str2ab(binaryMessage)); + } +} + +function testDeflate() { + ws = new WebSocket(tests[testIdx][2]); + + ws.onopen = function(e) { + if (tests[testIdx][1]) { + is(ws.extensions, "permessage-deflate", "permessage-deflate not negotiated!"); + } else { + is(ws.extensions, "", "permessage-deflate should not be negotiated!"); + } + + sendMessage(); + } + + ws.onclose = function(e) { + if (!e.wasClean) { + ok(false, "Connection should be closed cleanly!"); + SimpleTest.finish(); + } + } + + ws.onerror = function(e) { + ok(false, "onerror called!"); + SimpleTest.finish(); + } + + ws.onmessage = function(e) { + if (sendText) { + is(e.data, textMessage, "Text message not received successfully!"); + sendText = false; + sendMessage(); + } else { + ok(e.data instanceof ArrayBuffer, "Should receive an arraybuffer!"); + is(ab2str(e.data), binaryMessage, "Binary message not received successfully!"); + ws.close(); + + sendText = true; + testIdx++; + if (testIdx < tests.length) { + loadDeflate(); + } else { + SimpleTest.finish(); + } + } + } +} + +function loadDeflate() { + SpecialPowers.pushPrefEnv({"set":[['network.websocket.extensions.permessage-deflate', tests[testIdx][0]]]}, testDeflate); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_worker_websocket1.html b/dom/websocket/tests/test_worker_websocket1.html new file mode 100644 index 0000000000..71414fbe42 --- /dev/null +++ b/dom/websocket/tests/test_worker_websocket1.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker1.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_worker_websocket2.html b/dom/websocket/tests/test_worker_websocket2.html new file mode 100644 index 0000000000..d9e16281b9 --- /dev/null +++ b/dom/websocket/tests/test_worker_websocket2.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker2.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_worker_websocket3.html b/dom/websocket/tests/test_worker_websocket3.html new file mode 100644 index 0000000000..4387192c42 --- /dev/null +++ b/dom/websocket/tests/test_worker_websocket3.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker3.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_worker_websocket4.html b/dom/websocket/tests/test_worker_websocket4.html new file mode 100644 index 0000000000..5f4c390574 --- /dev/null +++ b/dom/websocket/tests/test_worker_websocket4.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker4.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_worker_websocket5.html b/dom/websocket/tests/test_worker_websocket5.html new file mode 100644 index 0000000000..0b3450b7b6 --- /dev/null +++ b/dom/websocket/tests/test_worker_websocket5.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker5.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_worker_websocket_basic.html b/dom/websocket/tests/test_worker_websocket_basic.html new file mode 100644 index 0000000000..39ff1647e7 --- /dev/null +++ b/dom/websocket/tests/test_worker_websocket_basic.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_basic_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker); + + if (event.data.type == 'finish') { + runTest(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + var tests = [ + function() { worker.postMessage(0); }, + function() { worker.postMessage(1); } + ]; + + function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + runTest(); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/test_worker_websocket_https.html b/dom/websocket/tests/test_worker_websocket_https.html new file mode 100644 index 0000000000..9283b9bd09 --- /dev/null +++ b/dom/websocket/tests/test_worker_websocket_https.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that creating insecure websockets from https workers is not possible</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + onmessage = function(event) { + is(event.data, "not created", "WebSocket object must not be created"); + SimpleTest.finish(); + }; + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<iframe src="https://example.com/tests/dom/websocket/tests/websocket_worker_https.html"></iframe> +</body> +</html> diff --git a/dom/websocket/tests/test_worker_websocket_loadgroup.html b/dom/websocket/tests/test_worker_websocket_loadgroup.html new file mode 100644 index 0000000000..d02b8f149c --- /dev/null +++ b/dom/websocket/tests/test_worker_websocket_loadgroup.html @@ -0,0 +1,61 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_loadgroup_worker.js"); + + var stopped = false; + worker.onmessage = function(e) { + if (e.data == 'opened') { + stopped = true; + window.stop(); + } else if (e.data == 'closed') { + ok(stopped, "Good!"); + stopped = false; + runTest(); + } else { + ok(false, "An error has been received"); + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + var tests = [ + function() { worker.postMessage(0); }, + function() { worker.postMessage(1); } + ]; + + function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + runTest(); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/websocket/tests/webSocket_sharedWorker.js b/dom/websocket/tests/webSocket_sharedWorker.js new file mode 100644 index 0000000000..8943ce2ac3 --- /dev/null +++ b/dom/websocket/tests/webSocket_sharedWorker.js @@ -0,0 +1,34 @@ +onconnect = function(evt) { + var ws = new WebSocket( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello" + ); + + ws.onopen = function(e) { + evt.ports[0].postMessage({ + type: "status", + status: true, + msg: "OnOpen called", + }); + ws.send("data"); + }; + + ws.onclose = function(e) {}; + + ws.onerror = function(e) { + evt.ports[0].postMessage({ + type: "status", + status: false, + msg: "onerror called!", + }); + }; + + ws.onmessage = function(e) { + evt.ports[0].postMessage({ + type: "status", + status: e.data == "Hello world!", + msg: "Wrong data", + }); + ws.close(); + evt.ports[0].postMessage({ type: "finish" }); + }; +}; diff --git a/dom/websocket/tests/websocket_basic_worker.js b/dom/websocket/tests/websocket_basic_worker.js new file mode 100644 index 0000000000..d1d263afcc --- /dev/null +++ b/dom/websocket/tests/websocket_basic_worker.js @@ -0,0 +1,48 @@ +onmessage = function(event) { + if (event.data != 0) { + var worker = new Worker("websocket_basic_worker.js"); + worker.onmessage = function(e) { + postMessage(e.data); + }; + + worker.postMessage(event.data - 1); + return; + } + + status = false; + try { + if (WebSocket instanceof Object) { + status = true; + } + } catch (e) {} + + postMessage({ + type: "status", + status, + msg: "WebSocket object:" + WebSocket, + }); + + var ws = new WebSocket( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello" + ); + ws.onopen = function(e) { + postMessage({ type: "status", status: true, msg: "OnOpen called" }); + ws.send("data"); + }; + + ws.onclose = function(e) {}; + + ws.onerror = function(e) { + postMessage({ type: "status", status: false, msg: "onerror called!" }); + }; + + ws.onmessage = function(e) { + postMessage({ + type: "status", + status: e.data == "Hello world!", + msg: "Wrong data", + }); + ws.close(); + postMessage({ type: "finish" }); + }; +}; diff --git a/dom/websocket/tests/websocket_helpers.js b/dom/websocket/tests/websocket_helpers.js new file mode 100644 index 0000000000..18f9f5a835 --- /dev/null +++ b/dom/websocket/tests/websocket_helpers.js @@ -0,0 +1,69 @@ +var current_test = 0; + +function shouldNotOpen(e) { + var ws = e.target; + ok(false, "onopen shouldn't be called on test " + ws._testNumber + "!"); +} + +function shouldCloseCleanly(e) { + var ws = e.target; + ok( + e.wasClean, + "the ws connection in test " + ws._testNumber + " should be closed cleanly" + ); +} + +function shouldCloseNotCleanly(e) { + var ws = e.target; + ok( + !e.wasClean, + "the ws connection in test " + + ws._testNumber + + " shouldn't be closed cleanly" + ); +} + +function ignoreError(e) {} + +function CreateTestWS(ws_location, ws_protocol) { + var ws; + + if (ws_protocol == undefined) { + ws = new WebSocket(ws_location); + } else { + ws = new WebSocket(ws_location, ws_protocol); + } + + ws._testNumber = current_test; + ok(true, "Created websocket for test " + ws._testNumber + "\n"); + + ws.onerror = function(e) { + ok(false, "onerror called on test " + e.target._testNumber + "!"); + }; + + return ws; +} + +function forcegc() { + SpecialPowers.forceGC(); + SpecialPowers.gc(); +} + +function feedback() { + $("feedback").innerHTML = + "executing test: " + (current_test + 1) + " of " + tests.length + " tests."; +} + +function finish() { + SimpleTest.finish(); +} + +function doTest() { + if (current_test >= tests.length) { + finish(); + return; + } + + feedback(); + tests[current_test++]().then(doTest); +} diff --git a/dom/websocket/tests/websocket_https_worker.js b/dom/websocket/tests/websocket_https_worker.js new file mode 100644 index 0000000000..6d7fd8376e --- /dev/null +++ b/dom/websocket/tests/websocket_https_worker.js @@ -0,0 +1,11 @@ +onmessage = function() { + var wsCreated = true; + try { + new WebSocket( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello" + ); + } catch (e) { + wsCreated = false; + } + postMessage(wsCreated ? "created" : "not created"); +}; diff --git a/dom/websocket/tests/websocket_hybi/file_binary-frames_wsh.py b/dom/websocket/tests/websocket_hybi/file_binary-frames_wsh.py new file mode 100644 index 0000000000..830367e846 --- /dev/null +++ b/dom/websocket/tests/websocket_hybi/file_binary-frames_wsh.py @@ -0,0 +1,18 @@ +from mod_pywebsocket import common +from mod_pywebsocket import stream + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + messages_to_send = ["Hello, world!", "", all_distinct_bytes()] + for message in messages_to_send: + # FIXME: Should use better API to send binary messages when pywebsocket supports it. + header = stream.create_header(common.OPCODE_BINARY, len(message), 1, 0, 0, 0, 0) + request.connection.write(header + message) + + +def all_distinct_bytes(): + return "".join([chr(i) for i in range(256)]) diff --git a/dom/websocket/tests/websocket_hybi/file_check-binary-messages_wsh.py b/dom/websocket/tests/websocket_hybi/file_check-binary-messages_wsh.py new file mode 100644 index 0000000000..1d5bc49dfa --- /dev/null +++ b/dom/websocket/tests/websocket_hybi/file_check-binary-messages_wsh.py @@ -0,0 +1,25 @@ +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + pass # Always accept. + + +def web_socket_transfer_data(request): + expected_messages = ["Hello, world!", "", all_distinct_bytes()] + + for test_number, expected_message in enumerate(expected_messages): + message = msgutil.receive_message(request) + if type(message) == str and message == expected_message: + msgutil.send_message(request, "PASS: Message #{:d}.".format(test_number)) + else: + msgutil.send_message( + request, + "FAIL: Message #{:d}: Received unexpected message: {!r}".format( + test_number, message + ), + ) + + +def all_distinct_bytes(): + return "".join([chr(i) for i in range(256)]) diff --git a/dom/websocket/tests/websocket_hybi/mochitest.ini b/dom/websocket/tests/websocket_hybi/mochitest.ini new file mode 100644 index 0000000000..21d61c8c86 --- /dev/null +++ b/dom/websocket/tests/websocket_hybi/mochitest.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + file_binary-frames_wsh.py + file_check-binary-messages_wsh.py + +[test_receive-arraybuffer.html] +skip-if = toolkit == 'android' +[test_receive-blob.html] +skip-if = toolkit == 'android' +[test_send-arraybuffer.html] +skip-if = toolkit == 'android' +[test_send-blob.html] +skip-if = toolkit == 'android' diff --git a/dom/websocket/tests/websocket_hybi/test_receive-arraybuffer.html b/dom/websocket/tests/websocket_hybi/test_receive-arraybuffer.html new file mode 100644 index 0000000000..6d465b7677 --- /dev/null +++ b/dom/websocket/tests/websocket_hybi/test_receive-arraybuffer.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> + +<script class="testbody" type="text/javascript"> + +function debug(msg) { + ok(1, msg); +} + +function createArrayBufferContainingHelloWorld() +{ + var hello = "Hello, world!"; + var array = new Uint8Array(hello.length); + for (var i = 0; i < hello.length; ++i) + array[i] = hello.charCodeAt(i); + return array.buffer; +} + +function createEmptyArrayBuffer() +{ + return new ArrayBuffer(0); +} + +function createArrayBufferContainingAllDistinctBytes() +{ + var array = new Uint8Array(256); + for (var i = 0; i < 256; ++i) + array[i] = i; + return array.buffer; +} + +var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/websocket_hybi/file_binary-frames"); +ws.binaryType = "arraybuffer"; +is(ws.binaryType, "arraybuffer", "should be equal to 'arraybuffer'"); + +var closeEvent; +var receivedMessages = []; +var expectedValues = [createArrayBufferContainingHelloWorld(), createEmptyArrayBuffer(), createArrayBufferContainingAllDistinctBytes()]; + +ws.onmessage = function(event) +{ + receivedMessages.push(event.data); +}; + +ws.onclose = function(event) +{ + closeEvent = event; + + is(receivedMessages.length, expectedValues.length, "lengths not equal"); + for (var i = 0; i < expectedValues.length; ++i) + check(i); + SimpleTest.finish(); +}; + +var responseType; + +function check(index) +{ + debug("Checking message #" + index + "."); + ok(receivedMessages[index] instanceof ArrayBuffer, + "Should receive an arraybuffer!"); + checkArrayBuffer(index, receivedMessages[index], expectedValues[index]); +} + +var actualArray; +var expectedArray; + +function checkArrayBuffer(testIndex, actual, expected) +{ + actualArray = new Uint8Array(actual); + expectedArray = new Uint8Array(expected); + is(actualArray.length, expectedArray.length, "lengths not equal"); + // Print only the first mismatched byte in order not to flood console. + for (var i = 0; i < expectedArray.length; ++i) { + if (actualArray[i] != expectedArray[i]) { + ok(false, "Value mismatch: actualArray[" + i + "] = " + actualArray[i] + ", expectedArray[" + i + "] = " + expectedArray[i]); + return; + } + } + ok(true, "Passed: Message #" + testIndex + "."); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/websocket/tests/websocket_hybi/test_receive-blob.html b/dom/websocket/tests/websocket_hybi/test_receive-blob.html new file mode 100644 index 0000000000..5589633a7c --- /dev/null +++ b/dom/websocket/tests/websocket_hybi/test_receive-blob.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> + +<script class="testbody" type="text/javascript"> + +function debug(msg) { + ok(true, msg); +} + +function createArrayBufferContainingHelloWorld() +{ + var hello = "Hello, world!"; + var array = new Uint8Array(hello.length); + for (var i = 0; i < hello.length; ++i) + array[i] = hello.charCodeAt(i); + return array.buffer; +} + +function createEmptyArrayBuffer() +{ + return new ArrayBuffer(0); +} + +function createArrayBufferContainingAllDistinctBytes() +{ + var array = new Uint8Array(256); + for (var i = 0; i < 256; ++i) + array[i] = i; + return array.buffer; +} + +var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/websocket_hybi/file_binary-frames"); +is(ws.binaryType, "blob", "should be 'blob'"); + +var closeEvent; +var receivedMessages = []; +var expectedValues = [createArrayBufferContainingHelloWorld(), createEmptyArrayBuffer(), createArrayBufferContainingAllDistinctBytes()]; + +ws.onmessage = function(event) +{ + receivedMessages.push(event.data); +}; + +ws.onclose = function(event) +{ + closeEvent = event; + + is(receivedMessages.length, expectedValues.length, "lengths not same"); + check(0); +}; + +var responseType; + +function check(index) +{ + if (index == expectedValues.length) { + SimpleTest.finish(); + return; + } + + debug("Checking message #" + index + "."); + ok(receivedMessages[index] instanceof Blob, + "We should be receiving a Blob"); + var reader = new FileReader(); + reader.readAsArrayBuffer(receivedMessages[index]); + reader.onload = function(event) + { + checkArrayBuffer(index, reader.result, expectedValues[index]); + check(index + 1); + }; + reader.onerror = function(event) + { + ok(false, "Failed to read blob: error code = " + reader.error.code); + check(index + 1); + }; +} + +var actualArray; +var expectedArray; + +function checkArrayBuffer(testIndex, actual, expected) +{ + actualArray = new Uint8Array(actual); + expectedArray = new Uint8Array(expected); + is(actualArray.length, expectedArray.length, "lengths not same"); + // Print only the first mismatched byte in order not to flood console. + for (var i = 0; i < expectedArray.length; ++i) { + if (actualArray[i] != expectedArray[i]) { + ok(false, "Value mismatch: actualArray[" + i + "] = " + actualArray[i] + ", expectedArray[" + i + "] = " + expectedArray[i]); + return; + } + } + ok(true, "Passed: Message #" + testIndex + "."); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/websocket/tests/websocket_hybi/test_send-arraybuffer.html b/dom/websocket/tests/websocket_hybi/test_send-arraybuffer.html new file mode 100644 index 0000000000..6c71ca5415 --- /dev/null +++ b/dom/websocket/tests/websocket_hybi/test_send-arraybuffer.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> + +<script class="testbody" type="text/javascript"> + +function debug(msg) { + ok(1, msg); +} + +function startsWith(target, prefix) +{ + return target.indexOf(prefix) === 0; +} + +function createArrayBufferContainingHelloWorld() +{ + var hello = "Hello, world!"; + var array = new Uint8Array(hello.length); + for (var i = 0; i < hello.length; ++i) + array[i] = hello.charCodeAt(i); + return array.buffer; +} + +function createEmptyArrayBuffer() +{ + return new ArrayBuffer(0); +} + +function createArrayBufferContainingAllDistinctBytes() +{ + var array = new Uint8Array(256); + for (var i = 0; i < 256; ++i) + array[i] = i; + return array.buffer; +} + +var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/websocket_hybi/file_check-binary-messages"); +var closeEvent; + +ws.onopen = function() +{ + ok(true, "onopen reached"); + ws.send(createArrayBufferContainingHelloWorld()); + ws.send(createEmptyArrayBuffer()); + ws.send(createArrayBufferContainingAllDistinctBytes()); +}; + +ws.onmessage = function(event) +{ + var message = event.data; + if (startsWith(message, "PASS")) + ok(true, message); + else + ok(false, message); +}; + +ws.onclose = function(event) +{ + ok(event.wasClean, "should have closed cleanly"); + SimpleTest.finish(); +}; + +ws.onerror = function(event) +{ + ok(false, "onerror should not have been called"); +}; + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/websocket/tests/websocket_hybi/test_send-blob.html b/dom/websocket/tests/websocket_hybi/test_send-blob.html new file mode 100644 index 0000000000..6af1e2df03 --- /dev/null +++ b/dom/websocket/tests/websocket_hybi/test_send-blob.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> + +<script class="testbody" type="text/javascript"> + +function startsWith(target, prefix) +{ + return target.indexOf(prefix) === 0; +} + +function distinctBytes() +{ + var array = new Array(); + for (var i = 0; i < 256; ++i) + array[i] = i; + // Concatenates chars into a single binary string + return String.fromCharCode.apply(null, array); +} + +var filesToCreate = [ + {name: "hellofile", data: "Hello, world!"}, + {name: "emptyfile"}, + {name: "allchars", data: distinctBytes()}, +]; + +SpecialPowers.createFiles(filesToCreate, function (files) { + var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/websocket_hybi/file_check-binary-messages"); + var closeEvent; + + ws.onopen = function() + { + ws.send(files[0]); + ws.send(files[1]); + ws.send(files[2]); + }; + + ws.onmessage = function(event) + { + var message = event.data; + if (startsWith(message, "PASS")) + ok(true, message); + else + ok(false, message); + }; + + ws.onclose = function(event) + { + ok(event.wasClean, "should have closed cleanly"); + SimpleTest.finish(); + }; +}, +function (msg) { + ok(false, "Failed to create files: " + msg); + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/websocket/tests/websocket_loadgroup_worker.js b/dom/websocket/tests/websocket_loadgroup_worker.js new file mode 100644 index 0000000000..f97cd4c5f2 --- /dev/null +++ b/dom/websocket/tests/websocket_loadgroup_worker.js @@ -0,0 +1,26 @@ +onmessage = function(event) { + if (event.data != 0) { + var worker = new Worker("websocket_loadgroup_worker.js"); + worker.onmessage = function(e) { + postMessage(e.data); + }; + + worker.postMessage(event.data - 1); + return; + } + + var ws = new WebSocket( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello" + ); + ws.onopen = function(e) { + postMessage("opened"); + }; + + ws.onclose = function(e) { + postMessage("closed"); + }; + + ws.onerror = function(e) { + postMessage("error"); + }; +}; diff --git a/dom/websocket/tests/websocket_tests.js b/dom/websocket/tests/websocket_tests.js new file mode 100644 index 0000000000..53561785b6 --- /dev/null +++ b/dom/websocket/tests/websocket_tests.js @@ -0,0 +1,1493 @@ +// test1: client tries to connect to a http scheme location; +function test1() { + return new Promise(function(resolve, reject) { + try { + var ws = CreateTestWS( + "http://mochi.test:8888/tests/dom/websocket/tests/file_websocket" + ); + ok(false, "test1 failed"); + } catch (e) { + ok(true, "test1 failed"); + } + + resolve(); + }); +} + +// test2: assure serialization of the connections; +// this test expects that the serialization list to connect to the proxy +// is empty. +function test2() { + return new Promise(function(resolve, reject) { + var waitTest2Part1 = true; + var waitTest2Part2 = true; + + var ws1 = CreateTestWS( + "ws://sub2.test2.example.com/tests/dom/websocket/tests/file_websocket", + "test-2.1" + ); + var ws2 = CreateTestWS( + "ws://sub2.test2.example.com/tests/dom/websocket/tests/file_websocket", + "test-2.2" + ); + + var ws2CanConnect = false; + + function maybeFinished() { + if (!waitTest2Part1 && !waitTest2Part2) { + resolve(); + } + } + + ws1.onopen = function() { + ok(true, "ws1 open in test 2"); + ws2CanConnect = true; + ws1.close(); + }; + + ws1.onclose = function(e) { + waitTest2Part1 = false; + maybeFinished(); + }; + + ws2.onopen = function() { + ok(ws2CanConnect, "shouldn't connect yet in test-2!"); + ws2.close(); + }; + + ws2.onclose = function(e) { + waitTest2Part2 = false; + maybeFinished(); + }; + }); +} + +// test3: client tries to connect to an non-existent ws server; +function test3() { + return new Promise(function(resolve, reject) { + var hasError = false; + var ws = CreateTestWS("ws://this.websocket.server.probably.does.not.exist"); + + ws.onopen = shouldNotOpen; + + ws.onerror = function(e) { + hasError = true; + }; + + ws.onclose = function(e) { + shouldCloseNotCleanly(e); + ok(hasError, "rcvd onerror event"); + is(e.code, 1006, "test-3 close code should be 1006 but is:" + e.code); + resolve(); + }; + }); +} + +// test4: client tries to connect using a relative url; +function test4() { + return new Promise(function(resolve, reject) { + try { + var ws = CreateTestWS("file_websocket"); + ok(false, "test-4 failed"); + } catch (e) { + ok(true, "test-4 failed"); + } + + resolve(); + }); +} + +// test5: client uses an invalid protocol value; +function test5() { + return new Promise(function(resolve, reject) { + try { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "" + ); + ok(false, "couldn't accept an empty string in the protocol parameter"); + } catch (e) { + ok(true, "couldn't accept an empty string in the protocol parameter"); + } + + try { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "\n" + ); + ok( + false, + "couldn't accept any not printable ASCII character in the protocol parameter" + ); + } catch (e) { + ok( + true, + "couldn't accept any not printable ASCII character in the protocol parameter" + ); + } + + try { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test 5" + ); + ok(false, "U+0020 not acceptable in protocol parameter"); + } catch (e) { + ok(true, "U+0020 not acceptable in protocol parameter"); + } + + resolve(); + }); +} + +// test6: counter and encoding check; +function test6() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-6" + ); + var counter = 1; + + ws.onopen = function() { + ws.send(counter); + }; + + ws.onmessage = function(e) { + if (counter == 5) { + is(e.data, "あいうえお", "test-6 counter 5 data ok"); + ws.close(); + } else { + is(parseInt(e.data), counter + 1, "bad counter"); + counter += 2; + ws.send(counter); + } + }; + + ws.onclose = function(e) { + shouldCloseCleanly(e); + resolve(); + }; + }); +} + +// test7: onmessage event origin property check +function test7() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://sub2.test2.example.org/tests/dom/websocket/tests/file_websocket", + "test-7" + ); + var gotmsg = false; + + ws.onopen = function() { + ok(true, "test 7 open"); + }; + + ws.onmessage = function(e) { + ok(true, "test 7 message"); + is( + e.origin, + "ws://sub2.test2.example.org", + "onmessage origin set to ws:// host" + ); + gotmsg = true; + ws.close(); + }; + + ws.onclose = function(e) { + ok(gotmsg, "recvd message in test 7 before close"); + shouldCloseCleanly(e); + resolve(); + }; + }); +} + +// test8: client calls close() and the server sends the close frame (with no +// code or reason) in acknowledgement; +function test8() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-8" + ); + + ws.onopen = function() { + is(ws.protocol, "test-8", "test-8 subprotocol selection"); + ws.close(); + }; + + ws.onclose = function(e) { + shouldCloseCleanly(e); + // We called close() with no close code: so pywebsocket will also send no + // close code, which translates to code 1005 + is(e.code, 1005, "test-8 close code has wrong value:" + e.code); + is(e.reason, "", "test-8 close reason has wrong value:" + e.reason); + resolve(); + }; + }); +} + +// test9: client closes the connection before the ws connection is established; +function test9() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://test2.example.org/tests/dom/websocket/tests/file_websocket", + "test-9" + ); + + ws._receivedErrorEvent = false; + + ws.onopen = shouldNotOpen; + + ws.onerror = function(e) { + ws._receivedErrorEvent = true; + }; + + ws.onclose = function(e) { + ok(ws._receivedErrorEvent, "Didn't received the error event in test 9."); + shouldCloseNotCleanly(e); + resolve(); + }; + + ws.close(); + }); +} + +// test10: client sends a message before the ws connection is established; +function test10() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://sub1.test1.example.com/tests/dom/websocket/tests/file_websocket", + "test-10" + ); + + ws.onclose = function(e) { + shouldCloseCleanly(e); + resolve(); + }; + + try { + ws.send("client data"); + ok(false, "Couldn't send data before connecting!"); + } catch (e) { + ok(true, "Couldn't send data before connecting!"); + } + + ws.onopen = function() { + ok(true, "test 10 opened"); + ws.close(); + }; + }); +} + +// test11: a simple hello echo; +function test11() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-11" + ); + is(ws.readyState, 0, "create bad readyState in test-11!"); + + ws.onopen = function() { + is(ws.readyState, 1, "open bad readyState in test-11!"); + ws.send("client data"); + }; + + ws.onmessage = function(e) { + is(e.data, "server data", "bad received message in test-11!"); + ws.close(1000, "Have a nice day"); + + // this ok() is disabled due to a race condition - it state may have + // advanced through 2 (closing) and into 3 (closed) before it is evald + // ok(ws.readyState == 2, "onmessage bad readyState in test-11!"); + }; + + ws.onclose = function(e) { + is(ws.readyState, 3, "onclose bad readyState in test-11!"); + shouldCloseCleanly(e); + is(e.code, 1000, "test 11 got wrong close code: " + e.code); + is( + e.reason, + "Have a nice day", + "test 11 got wrong close reason: " + e.reason + ); + resolve(); + }; + }); +} + +// test12: client sends a message containing unpaired surrogates +function test12() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-12" + ); + + ws.onopen = function() { + try { + // send an unpaired surrogate + ws._gotMessage = false; + ws.send("a\ud800b"); + ok(true, "ok to send an unpaired surrogate"); + } catch (e) { + ok( + false, + "shouldn't fail any more when sending an unpaired surrogate!" + ); + } + }; + + ws.onmessage = function(msg) { + is( + msg.data, + "SUCCESS", + "Unpaired surrogate in UTF-16 not converted in test-12" + ); + ws._gotMessage = true; + // Must support unpaired surrogates in close reason, too + ws.close(1000, "a\ud800b"); + }; + + ws.onclose = function(e) { + is(ws.readyState, 3, "onclose bad readyState in test-12!"); + ok(ws._gotMessage, "didn't receive message!"); + shouldCloseCleanly(e); + is(e.code, 1000, "test 12 got wrong close code: " + e.code); + is( + e.reason, + "a\ufffdb", + "test 11 didn't get replacement char in close reason: " + e.reason + ); + resolve(); + }; + }); +} + +// test13: server sends an invalid message; +function test13() { + return new Promise(function(resolve, reject) { + // previous versions of this test counted the number of protocol errors + // returned, but the protocol stack typically closes down after reporting a + // protocol level error - trying to resync is too dangerous + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-13" + ); + ws._timesCalledOnError = 0; + + ws.onerror = function() { + ws._timesCalledOnError++; + }; + + ws.onclose = function(e) { + ok(ws._timesCalledOnError > 0, "no error events"); + resolve(); + }; + }); +} + +// test14: server sends the close frame, it doesn't close the tcp connection +// and it keeps sending normal ws messages; +function test14() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-14" + ); + + ws.onmessage = function() { + ok( + false, + "shouldn't received message after the server sent the close frame" + ); + }; + + ws.onclose = function(e) { + shouldCloseCleanly(e); + resolve(); + }; + }); +} + +// test15: server closes the tcp connection, but it doesn't send the close +// frame; +function test15() { + return new Promise(function(resolve, reject) { + /* + * DISABLED: see comments for test-15 case in file_websocket_wsh.py + */ + resolve(); + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-15" + ); + ws.onclose = function(e) { + shouldCloseNotCleanly(e); + resolve(); + }; + + // termination of the connection might cause an error event if it happens in OPEN + ws.onerror = function() {}; + }); +} + +// test16: client calls close() and tries to send a message; +function test16() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-16" + ); + + ws.onopen = function() { + ws.close(); + ok( + !ws.send("client data"), + "shouldn't send message after calling close()" + ); + }; + + ws.onmessage = function() { + ok(false, "shouldn't send message after calling close()"); + }; + + ws.onerror = function() {}; + + ws.onclose = function() { + resolve(); + }; + }); +} + +// test17: see bug 572975 - all event listeners set +function test17() { + return new Promise(function(resolve, reject) { + var status_test17 = "not started"; + + var test17func = function() { + var local_ws = new WebSocket( + "ws://sub1.test2.example.org/tests/dom/websocket/tests/file_websocket", + "test-17" + ); + status_test17 = "started"; + + local_ws.onopen = function(e) { + status_test17 = "opened"; + e.target.send("client data"); + forcegc(); + }; + + local_ws.onerror = function() { + ok(false, "onerror called on test " + current_test + "!"); + }; + + local_ws.onmessage = function(e) { + ok(e.data == "server data", "Bad message in test-17"); + status_test17 = "got message"; + forcegc(); + }; + + local_ws.onclose = function(e) { + ok(status_test17 == "got message", "Didn't got message in test-17!"); + shouldCloseCleanly(e); + status_test17 = "closed"; + forcegc(); + resolve(); + }; + + window._test17 = null; + forcegc(); + }; + + window._test17 = test17func; + window._test17(); + }); +} + +// The tests that expects that their websockets neither open nor close MUST +// be in the end of the tests, i.e. HERE, in order to prevent blocking the other +// tests. + +// test18: client tries to connect to an http resource; +function test18() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_http_resource.txt" + ); + ws.onopen = shouldNotOpen; + ws.onerror = ignoreError; + ws.onclose = function(e) { + shouldCloseNotCleanly(e); + resolve(); + }; + }); +} + +// test19: server closes the tcp connection before establishing the ws +// connection; +function test19() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-19" + ); + ws.onopen = shouldNotOpen; + ws.onerror = ignoreError; + ws.onclose = function(e) { + shouldCloseNotCleanly(e); + resolve(); + }; + }); +} + +// test20: see bug 572975 - only on error and onclose event listeners set +function test20() { + return new Promise(function(resolve, reject) { + var test20func = function() { + var local_ws = new WebSocket( + "ws://sub1.test1.example.org/tests/dom/websocket/tests/file_websocket", + "test-20" + ); + + local_ws.onerror = function() { + ok(false, "onerror called on test " + current_test + "!"); + }; + + local_ws.onclose = function(e) { + ok(true, "test 20 closed despite gc"); + resolve(); + }; + + local_ws = null; + window._test20 = null; + forcegc(); + }; + + window._test20 = test20func; + window._test20(); + }); +} + +// test21: see bug 572975 - same as test 17, but delete strong event listeners +// when receiving the message event; +function test21() { + return new Promise(function(resolve, reject) { + var test21func = function() { + var local_ws = new WebSocket( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-21" + ); + var received_message = false; + + local_ws.onopen = function(e) { + e.target.send("client data"); + forcegc(); + e.target.onopen = null; + forcegc(); + }; + + local_ws.onerror = function() { + ok(false, "onerror called on test " + current_test + "!"); + }; + + local_ws.onmessage = function(e) { + is(e.data, "server data", "Bad message in test-21"); + received_message = true; + forcegc(); + e.target.onmessage = null; + forcegc(); + }; + + local_ws.onclose = function(e) { + shouldCloseCleanly(e); + ok(received_message, "close transitioned through onmessage"); + resolve(); + }; + + local_ws = null; + window._test21 = null; + forcegc(); + }; + + window._test21 = test21func; + window._test21(); + }); +} + +// test22: server takes too long to establish the ws connection; +function test22() { + return new Promise(function(resolve, reject) { + const pref_open = "network.websocket.timeout.open"; + SpecialPowers.setIntPref(pref_open, 5); + + var ws = CreateTestWS( + "ws://sub2.test2.example.org/tests/dom/websocket/tests/file_websocket", + "test-22" + ); + + ws.onopen = shouldNotOpen; + ws.onerror = ignoreError; + + ws.onclose = function(e) { + shouldCloseNotCleanly(e); + resolve(); + }; + + SpecialPowers.clearUserPref(pref_open); + }); +} + +// test23: should detect WebSocket on window object; +function test23() { + return new Promise(function(resolve, reject) { + ok("WebSocket" in window, "WebSocket should be available on window object"); + resolve(); + }); +} + +// test24: server rejects sub-protocol string +function test24() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-does-not-exist" + ); + + ws.onopen = shouldNotOpen; + ws.onclose = function(e) { + shouldCloseNotCleanly(e); + resolve(); + }; + + ws.onerror = function() {}; + }); +} + +// test25: ctor with valid empty sub-protocol array +function test25() { + return new Promise(function(resolve, reject) { + var prots = []; + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + // This test errors because the server requires a sub-protocol, but + // the test just wants to ensure that the ctor doesn't generate an + // exception + ws.onerror = ignoreError; + ws.onopen = shouldNotOpen; + + ws.onclose = function(e) { + is(ws.protocol, "", "test25 subprotocol selection"); + ok(true, "test 25 protocol array close"); + resolve(); + }; + }); +} + +// test26: ctor with invalid sub-protocol array containing 1 empty element +function test26() { + return new Promise(function(resolve, reject) { + var prots = [""]; + + try { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + ok(false, "testing empty element sub protocol array"); + } catch (e) { + ok(true, "testing empty sub element protocol array"); + } + + resolve(); + }); +} + +// test27: ctor with invalid sub-protocol array containing an empty element in +// list +function test27() { + return new Promise(function(resolve, reject) { + var prots = ["test27", ""]; + + try { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + ok(false, "testing empty element mixed sub protocol array"); + } catch (e) { + ok(true, "testing empty element mixed sub protocol array"); + } + + resolve(); + }); +} + +// test28: ctor using valid 1 element sub-protocol array +function test28() { + return new Promise(function(resolve, reject) { + var prots = ["test28"]; + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = function(e) { + ok(true, "test 28 protocol array open"); + ws.close(); + }; + + ws.onclose = function(e) { + is(ws.protocol, "test28", "test28 subprotocol selection"); + ok(true, "test 28 protocol array close"); + resolve(); + }; + }); +} + +// test29: ctor using all valid 5 element sub-protocol array +function test29() { + return new Promise(function(resolve, reject) { + var prots = ["test29a", "test29b"]; + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = function(e) { + ok(true, "test 29 protocol array open"); + ws.close(); + }; + + ws.onclose = function(e) { + ok(true, "test 29 protocol array close"); + resolve(); + }; + }); +} + +// test30: ctor using valid 1 element sub-protocol array with element server +// will reject +function test30() { + return new Promise(function(resolve, reject) { + var prots = ["test-does-not-exist"]; + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = shouldNotOpen; + + ws.onclose = function(e) { + shouldCloseNotCleanly(e); + resolve(); + }; + + ws.onerror = function() {}; + }); +} + +// test31: ctor using valid 2 element sub-protocol array with 1 element server +// will reject and one server will accept +function test31() { + return new Promise(function(resolve, reject) { + var prots = ["test-does-not-exist", "test31"]; + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = function(e) { + ok(true, "test 31 protocol array open"); + ws.close(); + }; + + ws.onclose = function(e) { + is(ws.protocol, "test31", "test31 subprotocol selection"); + ok(true, "test 31 protocol array close"); + resolve(); + }; + }); +} + +// test32: ctor using invalid sub-protocol array that contains duplicate items +function test32() { + return new Promise(function(resolve, reject) { + var prots = ["test32", "test32"]; + + try { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + ok(false, "testing duplicated element sub protocol array"); + } catch (e) { + ok(true, "testing duplicated sub element protocol array"); + } + + resolve(); + }); +} + +// test33: test for sending/receiving custom close code (but no close reason) +function test33() { + return new Promise(function(resolve, reject) { + var prots = ["test33"]; + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = function(e) { + ok(true, "test 33 open"); + ws.close(3131); // pass code but not reason + }; + + ws.onclose = function(e) { + ok(true, "test 33 close"); + shouldCloseCleanly(e); + is(e.code, 3131, "test 33 got wrong close code: " + e.code); + is(e.reason, "", "test 33 got wrong close reason: " + e.reason); + resolve(); + }; + }); +} + +// test34: test for receiving custom close code and reason +function test34() { + return new Promise(function(resolve, reject) { + var prots = ["test-34"]; + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = function(e) { + ok(true, "test 34 open"); + ws.close(); + }; + + ws.onclose = function(e) { + ok(true, "test 34 close"); + ok(e.wasClean, "test 34 closed cleanly"); + is(e.code, 1001, "test 34 custom server code"); + is(e.reason, "going away now", "test 34 custom server reason"); + resolve(); + }; + }); +} + +// test35: test for sending custom close code and reason +function test35() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-35a" + ); + + ws.onopen = function(e) { + ok(true, "test 35a open"); + ws.close(3500, "my code"); + }; + + ws.onclose = function(e) { + ok(true, "test 35a close"); + ok(e.wasClean, "test 35a closed cleanly"); + var wsb = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-35b" + ); + + wsb.onopen = function(event) { + ok(true, "test 35b open"); + wsb.close(); + }; + + wsb.onclose = function(event) { + ok(true, "test 35b close"); + ok(event.wasClean, "test 35b closed cleanly"); + is(event.code, 3501, "test 35 custom server code"); + is(event.reason, "my code", "test 35 custom server reason"); + resolve(); + }; + }; + }); +} + +// test36: negative test for sending out of range close code +function test36() { + return new Promise(function(resolve, reject) { + var prots = ["test-36"]; + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = function(e) { + ok(true, "test 36 open"); + + try { + ws.close(13200); + ok(false, "testing custom close code out of range"); + } catch (ex) { + ok(true, "testing custom close code out of range"); + ws.close(3200); + } + }; + + ws.onclose = function(e) { + ok(true, "test 36 close"); + ok(e.wasClean, "test 36 closed cleanly"); + resolve(); + }; + }); +} + +// test37: negative test for too long of a close reason +function test37() { + return new Promise(function(resolve, reject) { + var prots = ["test-37"]; + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = function(e) { + ok(true, "test 37 open"); + + try { + ws.close( + 3100, + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123" + ); + ok(false, "testing custom close reason out of range"); + } catch (ex) { + ok(true, "testing custom close reason out of range"); + ws.close( + 3100, + "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012" + ); + } + }; + + ws.onclose = function(e) { + ok(true, "test 37 close"); + ok(e.wasClean, "test 37 closed cleanly"); + + var wsb = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-37b" + ); + + wsb.onopen = function(event) { + // now test that a rejected close code and reason dont persist + ok(true, "test 37b open"); + try { + wsb.close( + 3101, + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123" + ); + ok(false, "testing custom close reason out of range 37b"); + } catch (ex) { + ok(true, "testing custom close reason out of range 37b"); + wsb.close(); + } + }; + + wsb.onclose = function(event) { + ok(true, "test 37b close"); + ok(event.wasClean, "test 37b closed cleanly"); + + var wsc = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-37c" + ); + + wsc.onopen = function(eventInner) { + ok(true, "test 37c open"); + wsc.close(); + }; + + wsc.onclose = function(eventInner) { + isnot( + eventInner.code, + 3101, + "test 37c custom server code not present" + ); + is( + eventInner.reason, + "", + "test 37c custom server reason not present" + ); + resolve(); + }; + }; + }; + }); +} + +// test38: ensure extensions attribute is defined +function test38() { + return new Promise(function(resolve, reject) { + var prots = ["test-38"]; + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = function(e) { + ok(true, "test 38 open"); + isnot(ws.extensions, undefined, "extensions attribute defined"); + // is(ws.extensions, "deflate-stream", "extensions attribute deflate-stream"); + ws.close(); + }; + + ws.onclose = function(e) { + ok(true, "test 38 close"); + resolve(); + }; + }); +} + +// test39: a basic wss:// connectivity test +function test39() { + return new Promise(function(resolve, reject) { + var prots = ["test-39"]; + + var ws = CreateTestWS( + "wss://example.com/tests/dom/websocket/tests/file_websocket", + prots + ); + status_test39 = "started"; + + ws.onopen = function(e) { + status_test39 = "opened"; + ok(true, "test 39 open"); + ws.close(); + }; + + ws.onclose = function(e) { + ok(true, "test 39 close"); + is(status_test39, "opened", "test 39 did open"); + resolve(); + }; + }); +} + +// test40: negative test for wss:// with no cert +function test40() { + return new Promise(function(resolve, reject) { + var prots = ["test-40"]; + + var ws = CreateTestWS( + "wss://nocert.example.com/tests/dom/websocket/tests/file_websocket", + prots + ); + + status_test40 = "started"; + ws.onerror = ignoreError; + + ws.onopen = function(e) { + status_test40 = "opened"; + ok(false, "test 40 open"); + ws.close(); + }; + + ws.onclose = function(e) { + ok(true, "test 40 close"); + is(status_test40, "started", "test 40 did not open"); + resolve(); + }; + }); +} + +// test41: HSTS +function test41() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://example.com/tests/dom/websocket/tests/file_websocket", + "test-41a", + 1 + ); + + ws.onopen = function(e) { + ok(true, "test 41a open"); + is( + ws.url, + "ws://example.com/tests/dom/websocket/tests/file_websocket", + "test 41a initial ws should not be redirected" + ); + ws.close(); + }; + + ws.onclose = function(e) { + ok(true, "test 41a close"); + + // establish a hsts policy for example.com + var wsb = CreateTestWS( + "wss://example.com/tests/dom/websocket/tests/file_websocket", + "test-41b", + 1 + ); + + wsb.onopen = function(event) { + ok(true, "test 41b open"); + wsb.close(); + }; + + wsb.onclose = function(event) { + ok(true, "test 41b close"); + + // try ws:// again, it should be done over wss:// now due to hsts + var wsc = CreateTestWS( + "ws://example.com/tests/dom/websocket/tests/file_websocket", + "test-41c" + ); + + wsc.onopen = function() { + ok(true, "test 41c open"); + is( + wsc.url, + "wss://example.com/tests/dom/websocket/tests/file_websocket", + "test 41c ws should be redirected by hsts to wss" + ); + wsc.close(); + }; + + wsc.onclose = function() { + ok(true, "test 41c close"); + + // clean up the STS state + const Ci = SpecialPowers.Ci; + var loadContext = SpecialPowers.wrap(window).docShell.QueryInterface( + Ci.nsILoadContext + ); + var flags = 0; + if (loadContext.usePrivateBrowsing) { + flags |= Ci.nsISocketProvider.NO_PERMANENT_STORAGE; + } + SpecialPowers.cleanUpSTSData("http://example.com", flags); + resolve(); + }; + }; + }; + }); +} + +// test42: non-char utf-8 sequences +function test42() { + return new Promise(function(resolve, reject) { + // test some utf-8 non-characters. They should be allowed in the + // websockets context. Test via round trip echo. + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-42" + ); + var data = ["U+FFFE \ufffe", "U+FFFF \uffff", "U+10FFFF \udbff\udfff"]; + var index = 0; + + ws.onopen = function() { + ws.send(data[0]); + ws.send(data[1]); + ws.send(data[2]); + }; + + ws.onmessage = function(e) { + is( + e.data, + data[index], + "bad received message in test-42! index=" + index + ); + index++; + if (index == 3) { + ws.close(); + } + }; + + ws.onclose = function(e) { + resolve(); + }; + }); +} + +// test43: Test setting binaryType attribute +function test43() { + return new Promise(function(resolve, reject) { + var prots = ["test-43"]; + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + prots + ); + + ws.onopen = function(e) { + ok(true, "test 43 open"); + // Test binaryType setting + ws.binaryType = "arraybuffer"; + ws.binaryType = "blob"; + ws.binaryType = ""; // illegal + is(ws.binaryType, "blob"); + ws.binaryType = "ArrayBuffer"; // illegal + is(ws.binaryType, "blob"); + ws.binaryType = "Blob"; // illegal + is(ws.binaryType, "blob"); + ws.binaryType = "mcfoofluu"; // illegal + is(ws.binaryType, "blob"); + ws.close(); + }; + + ws.onclose = function(e) { + ok(true, "test 43 close"); + resolve(); + }; + }); +} + +// test44: Test sending/receving binary ArrayBuffer +function test44() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-44" + ); + is(ws.readyState, 0, "bad readyState in test-44!"); + ws.binaryType = "arraybuffer"; + + ws.onopen = function() { + is(ws.readyState, 1, "open bad readyState in test-44!"); + var buf = new ArrayBuffer(3); + // create byte view + var view = new Uint8Array(buf); + view[0] = 5; + view[1] = 0; // null byte + view[2] = 7; + ws.send(buf); + }; + + ws.onmessage = function(e) { + ok(e.data instanceof ArrayBuffer, "Should receive an arraybuffer!"); + var view = new Uint8Array(e.data); + ok( + view.length == 2 && view[0] == 0 && view[1] == 4, + "testing Reply arraybuffer" + ); + ws.close(); + }; + + ws.onclose = function(e) { + is(ws.readyState, 3, "onclose bad readyState in test-44!"); + shouldCloseCleanly(e); + resolve(); + }; + }); +} + +// test45: Test sending/receving binary Blob +function test45() { + return new Promise(function(resolve, reject) { + function test45Real(blobFile) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-45" + ); + is(ws.readyState, 0, "bad readyState in test-45!"); + // ws.binaryType = "blob"; // Don't need to specify: blob is the default + + ws.onopen = function() { + is(ws.readyState, 1, "open bad readyState in test-45!"); + ws.send(blobFile); + }; + + var test45blob; + + ws.onmessage = function(e) { + test45blob = e.data; + ok(test45blob instanceof Blob, "We should be receiving a Blob"); + + ws.close(); + }; + + ws.onclose = function(e) { + is(ws.readyState, 3, "onclose bad readyState in test-45!"); + shouldCloseCleanly(e); + + // check blob contents + var reader = new FileReader(); + reader.onload = function(event) { + is( + reader.result, + "flob", + "response should be 'flob': got '" + reader.result + "'" + ); + }; + + reader.onerror = function(event) { + testFailed("Failed to read blob: error code = " + reader.error.code); + }; + + reader.onloadend = function(event) { + resolve(); + }; + + reader.readAsBinaryString(test45blob); + }; + } + + SpecialPowers.createFiles( + [{ name: "testBlobFile", data: "flob" }], + function(files) { + test45Real(files[0]); + }, + function(msg) { + testFailed("Failed to create file for test45: " + msg); + resolve(); + } + ); + }); +} + +// test46: Test that we don't dispatch incoming msgs once in CLOSING state +function test46() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-46" + ); + is(ws.readyState, 0, "create bad readyState in test-46!"); + + ws.onopen = function() { + is(ws.readyState, 1, "open bad readyState in test-46!"); + ws.close(); + is(ws.readyState, 2, "close must set readyState to 2 in test-46!"); + }; + + ws.onmessage = function(e) { + ok(false, "received message after calling close in test-46!"); + }; + + ws.onclose = function(e) { + is(ws.readyState, 3, "onclose bad readyState in test-46!"); + shouldCloseCleanly(e); + resolve(); + }; + }); +} + +// test47: Make sure onerror/onclose aren't called during close() +function test47() { + return new Promise(function(resolve, reject) { + var hasError = false; + var ws = CreateTestWS( + "ws://another.websocket.server.that.probably.does.not.exist" + ); + + ws.onopen = shouldNotOpen; + + ws.onerror = function(e) { + is( + ws.readyState, + 3, + "test-47: readyState should be CLOSED(3) in onerror: got " + + ws.readyState + ); + ok(!ws._withinClose, "onerror() called during close()!"); + hasError = true; + }; + + ws.onclose = function(e) { + shouldCloseNotCleanly(e); + ok(hasError, "test-47: should have called onerror before onclose"); + is( + ws.readyState, + 3, + "test-47: readyState should be CLOSED(3) in onclose: got " + + ws.readyState + ); + ok(!ws._withinClose, "onclose() called during close()!"); + is(e.code, 1006, "test-47 close code should be 1006 but is:" + e.code); + resolve(); + }; + + // Call close before we're connected: throws error + // Make sure we call onerror/onclose asynchronously + ws._withinClose = 1; + ws.close(3333, "Closed before we were open: error"); + ws._withinClose = 0; + is( + ws.readyState, + 2, + "test-47: readyState should be CLOSING(2) after close(): got " + + ws.readyState + ); + }); +} + +// test48: see bug 1227136 - client calls close() from onopen() and waits until +// WebSocketChannel::mSocketIn is nulled out on socket thread. +function test48() { + return new Promise(function(resolve, reject) { + const pref_close = "network.websocket.timeout.close"; + SpecialPowers.setIntPref(pref_close, 1); + + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-48" + ); + + ws.onopen = function() { + ws.close(); + + var date = new Date(); + var curDate = null; + do { + curDate = new Date(); + } while (curDate - date < 1500); + }; + + ws.onclose = function(e) { + ok(true, "ws close in test 48"); + resolve(); + }; + + SpecialPowers.clearUserPref(pref_close); + }); +} + +function test49() { + return new Promise(function(resolve, reject) { + var ws = CreateTestWS( + "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket", + "test-49" + ); + var gotError = 0; + ok(ws.readyState == 0, "create bad readyState in test-49!"); + + ws.onopen = function() { + ok(false, "Connection must fail in test-49"); + }; + + ws.onerror = function(e) { + gotError = 1; + }; + + ws.onclose = function(e) { + ok(gotError, "Should get error in test-49!"); + resolve(); + }; + }); +} diff --git a/dom/websocket/tests/websocket_worker1.js b/dom/websocket/tests/websocket_worker1.js new file mode 100644 index 0000000000..26b37b8e48 --- /dev/null +++ b/dom/websocket/tests/websocket_worker1.js @@ -0,0 +1,19 @@ +importScripts("websocket_helpers.js"); +importScripts("websocket_tests.js"); +importScripts("websocket_worker_helpers.js"); + +var tests = [ + test1, // client tries to connect to a http scheme location; + test2, // assure serialization of the connections; + test3, // client tries to connect to an non-existent ws server; + test4, // client tries to connect using a relative url; + test5, // client uses an invalid protocol value; + test6, // counter and encoding check; + test7, // onmessage event origin property check + test8, // client calls close() and the server sends the close frame (with no + // code or reason) in acknowledgement; + test9, // client closes the connection before the ws connection is established; + test10, // client sends a message before the ws connection is established; +]; + +doTest(); diff --git a/dom/websocket/tests/websocket_worker2.js b/dom/websocket/tests/websocket_worker2.js new file mode 100644 index 0000000000..fbc4591bdb --- /dev/null +++ b/dom/websocket/tests/websocket_worker2.js @@ -0,0 +1,19 @@ +importScripts("websocket_helpers.js"); +importScripts("websocket_tests.js"); +importScripts("websocket_worker_helpers.js"); + +var tests = [ + test11, // a simple hello echo; + test12, // client sends a message containing unpaired surrogates + test13, //server sends an invalid message; + test14, // server sends the close frame, it doesn't close the tcp connection + // and it keeps sending normal ws messages; + test15, // server closes the tcp connection, but it doesn't send the close + // frame; + test16, // client calls close() and tries to send a message; + test18, // client tries to connect to an http resource; + test19, // server closes the tcp connection before establishing the ws + // connection; +]; + +doTest(); diff --git a/dom/websocket/tests/websocket_worker3.js b/dom/websocket/tests/websocket_worker3.js new file mode 100644 index 0000000000..8fee7f4ca5 --- /dev/null +++ b/dom/websocket/tests/websocket_worker3.js @@ -0,0 +1,17 @@ +importScripts("websocket_helpers.js"); +importScripts("websocket_tests.js"); +importScripts("websocket_worker_helpers.js"); + +var tests = [ + test24, // server rejects sub-protocol string + test25, // ctor with valid empty sub-protocol array + test26, // ctor with invalid sub-protocol array containing 1 empty element + test27, // ctor with invalid sub-protocol array containing an empty element in + // list + test28, // ctor using valid 1 element sub-protocol array + test29, // ctor using all valid 5 element sub-protocol array + test30, // ctor using valid 1 element sub-protocol array with element server + // will reject +]; + +doTest(); diff --git a/dom/websocket/tests/websocket_worker4.js b/dom/websocket/tests/websocket_worker4.js new file mode 100644 index 0000000000..3e3408b019 --- /dev/null +++ b/dom/websocket/tests/websocket_worker4.js @@ -0,0 +1,19 @@ +importScripts("websocket_helpers.js"); +importScripts("websocket_tests.js"); +importScripts("websocket_worker_helpers.js"); + +var tests = [ + test31, // ctor using valid 2 element sub-protocol array with 1 element server + // will reject and one server will accept + test32, // ctor using invalid sub-protocol array that contains duplicate items + test33, // test for sending/receiving custom close code (but no close reason) + test34, // test for receiving custom close code and reason + test35, // test for sending custom close code and reason + test36, // negative test for sending out of range close code + test37, // negative test for too long of a close reason + test38, // ensure extensions attribute is defined + test39, // a basic wss:// connectivity test + test40, // negative test for wss:// with no cert +]; + +doTest(); diff --git a/dom/websocket/tests/websocket_worker5.js b/dom/websocket/tests/websocket_worker5.js new file mode 100644 index 0000000000..0ed1bbd486 --- /dev/null +++ b/dom/websocket/tests/websocket_worker5.js @@ -0,0 +1,14 @@ +importScripts("websocket_helpers.js"); +importScripts("websocket_tests.js"); +importScripts("websocket_worker_helpers.js"); + +var tests = [ + test42, // non-char utf-8 sequences + test43, // Test setting binaryType attribute + test44, // Test sending/receving binary ArrayBuffer + test46, // Test that we don't dispatch incoming msgs once in CLOSING state + test47, // Make sure onerror/onclose aren't called during close() + test49, // Test that we fail if subprotocol returned from server doesn't match +]; + +doTest(); diff --git a/dom/websocket/tests/websocket_worker_helpers.js b/dom/websocket/tests/websocket_worker_helpers.js new file mode 100644 index 0000000000..e9c55b1009 --- /dev/null +++ b/dom/websocket/tests/websocket_worker_helpers.js @@ -0,0 +1,27 @@ +function feedback() { + postMessage({ + type: "feedback", + msg: + "executing test: " + + (current_test + 1) + + " of " + + tests.length + + " tests.", + }); +} + +function ok(status, msg) { + postMessage({ type: "status", status: !!status, msg }); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function isnot(a, b, msg) { + ok(a != b, msg); +} + +function finish() { + postMessage({ type: "finish" }); +} diff --git a/dom/websocket/tests/websocket_worker_https.html b/dom/websocket/tests/websocket_worker_https.html new file mode 100644 index 0000000000..0eb17af65b --- /dev/null +++ b/dom/websocket/tests/websocket_worker_https.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + var worker = new Worker("https://example.com/tests/dom/websocket/tests/websocket_https_worker.js"); + + worker.onmessage = function(event) { + parent.postMessage(event.data, "*"); + }; + + worker.onerror = function(event) { + parent.postMessage("error", "*"); + }; + + worker.postMessage("start"); +</script> diff --git a/dom/websocket/tests/window_bug1384658.html b/dom/websocket/tests/window_bug1384658.html new file mode 100644 index 0000000000..c2d6e16c8d --- /dev/null +++ b/dom/websocket/tests/window_bug1384658.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<script> + onload = function() { + if (window.location.search == "") { + window.open("window_bug1384658.html?opened", "_top", ""); + } else { + var iframeURL = "http://mochi.test:8888/tests/dom/websocket/tests/file_bug1384658.html"; + var iframe = document.createElement("iframe"); + iframe.src = iframeURL; + document.body.appendChild(iframe); + } + }; +</script> +</head> +<body> +</body> +</html> |