From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- dom/websocket/WebSocket.cpp | 2891 ++++++++++++++++++++ dom/websocket/WebSocket.h | 192 ++ dom/websocket/moz.build | 33 + dom/websocket/tests/chrome.ini | 4 + dom/websocket/tests/file_bug1384658.html | 18 + dom/websocket/tests/file_websocket_basic_wsh.py | 31 + dom/websocket/tests/file_websocket_bigBlob_wsh.py | 11 + dom/websocket/tests/file_websocket_hello_wsh.py | 12 + .../tests/file_websocket_http_resource.txt | 1 + ...le_websocket_permessage_deflate_disabled_wsh.py | 18 + ...file_websocket_permessage_deflate_params_wsh.py | 24 + ...le_websocket_permessage_deflate_rejected_wsh.py | 24 + .../tests/file_websocket_permessage_deflate_wsh.py | 23 + dom/websocket/tests/file_websocket_wsh.py | 168 ++ dom/websocket/tests/frame_bug1384658.html | 13 + dom/websocket/tests/iframe_websocket_sandbox.html | 65 + dom/websocket/tests/iframe_websocket_wss.html | 35 + dom/websocket/tests/mochitest.ini | 64 + dom/websocket/tests/test_bug1081686.html | 71 + dom/websocket/tests/test_bug1384658.html | 54 + dom/websocket/tests/test_event_listener_leaks.html | 57 + dom/websocket/tests/test_websocket1.html | 42 + dom/websocket/tests/test_websocket2.html | 44 + dom/websocket/tests/test_websocket3.html | 44 + dom/websocket/tests/test_websocket4.html | 42 + dom/websocket/tests/test_websocket5.html | 41 + dom/websocket/tests/test_websocket_basic.html | 289 ++ dom/websocket/tests/test_websocket_bigBlob.html | 55 + dom/websocket/tests/test_websocket_frame.html | 161 ++ dom/websocket/tests/test_websocket_hello.html | 49 + dom/websocket/tests/test_websocket_longString.html | 48 + .../tests/test_websocket_mixed_content.html | 96 + .../tests/test_websocket_mixed_content_opener.html | 139 + .../tests/test_websocket_no_duplicate_packet.html | 106 + .../tests/test_websocket_permessage_deflate.html | 110 + dom/websocket/tests/test_websocket_sandbox.html | 34 + .../tests/test_websocket_sharedWorker.html | 30 + dom/websocket/tests/test_worker_websocket1.html | 46 + dom/websocket/tests/test_worker_websocket2.html | 46 + dom/websocket/tests/test_worker_websocket3.html | 46 + dom/websocket/tests/test_worker_websocket4.html | 46 + dom/websocket/tests/test_worker_websocket5.html | 46 + .../tests/test_worker_websocket_basic.html | 57 + .../tests/test_worker_websocket_https.html | 30 + .../tests/test_worker_websocket_loadgroup.html | 61 + dom/websocket/tests/websocket_basic_worker.js | 48 + dom/websocket/tests/websocket_helpers.js | 69 + dom/websocket/tests/websocket_https_worker.js | 11 + .../tests/websocket_hybi/file_binary-frames_wsh.py | 19 + .../file_check-binary-messages_wsh.py | 27 + dom/websocket/tests/websocket_hybi/mochitest.ini | 15 + .../websocket_hybi/test_receive-arraybuffer.html | 97 + .../tests/websocket_hybi/test_receive-blob.html | 110 + .../websocket_hybi/test_send-arraybuffer.html | 82 + .../tests/websocket_hybi/test_send-blob.html | 72 + dom/websocket/tests/websocket_loadgroup_worker.js | 26 + dom/websocket/tests/websocket_sharedWorker.js | 34 + dom/websocket/tests/websocket_tests.js | 1483 ++++++++++ dom/websocket/tests/websocket_worker1.js | 19 + dom/websocket/tests/websocket_worker2.js | 19 + dom/websocket/tests/websocket_worker3.js | 17 + dom/websocket/tests/websocket_worker4.js | 19 + dom/websocket/tests/websocket_worker5.js | 14 + dom/websocket/tests/websocket_worker_helpers.js | 27 + dom/websocket/tests/websocket_worker_https.html | 14 + dom/websocket/tests/window_bug1384658.html | 19 + dom/websocket/tests/window_websocket_wss.html | 65 + 67 files changed, 7823 insertions(+) create mode 100644 dom/websocket/WebSocket.cpp create mode 100644 dom/websocket/WebSocket.h create mode 100644 dom/websocket/moz.build create mode 100644 dom/websocket/tests/chrome.ini create mode 100644 dom/websocket/tests/file_bug1384658.html create mode 100644 dom/websocket/tests/file_websocket_basic_wsh.py create mode 100644 dom/websocket/tests/file_websocket_bigBlob_wsh.py create mode 100644 dom/websocket/tests/file_websocket_hello_wsh.py create mode 100644 dom/websocket/tests/file_websocket_http_resource.txt create mode 100644 dom/websocket/tests/file_websocket_permessage_deflate_disabled_wsh.py create mode 100644 dom/websocket/tests/file_websocket_permessage_deflate_params_wsh.py create mode 100644 dom/websocket/tests/file_websocket_permessage_deflate_rejected_wsh.py create mode 100644 dom/websocket/tests/file_websocket_permessage_deflate_wsh.py create mode 100644 dom/websocket/tests/file_websocket_wsh.py create mode 100644 dom/websocket/tests/frame_bug1384658.html create mode 100644 dom/websocket/tests/iframe_websocket_sandbox.html create mode 100644 dom/websocket/tests/iframe_websocket_wss.html create mode 100644 dom/websocket/tests/mochitest.ini create mode 100644 dom/websocket/tests/test_bug1081686.html create mode 100644 dom/websocket/tests/test_bug1384658.html create mode 100644 dom/websocket/tests/test_event_listener_leaks.html create mode 100644 dom/websocket/tests/test_websocket1.html create mode 100644 dom/websocket/tests/test_websocket2.html create mode 100644 dom/websocket/tests/test_websocket3.html create mode 100644 dom/websocket/tests/test_websocket4.html create mode 100644 dom/websocket/tests/test_websocket5.html create mode 100644 dom/websocket/tests/test_websocket_basic.html create mode 100644 dom/websocket/tests/test_websocket_bigBlob.html create mode 100644 dom/websocket/tests/test_websocket_frame.html create mode 100644 dom/websocket/tests/test_websocket_hello.html create mode 100644 dom/websocket/tests/test_websocket_longString.html create mode 100644 dom/websocket/tests/test_websocket_mixed_content.html create mode 100644 dom/websocket/tests/test_websocket_mixed_content_opener.html create mode 100644 dom/websocket/tests/test_websocket_no_duplicate_packet.html create mode 100644 dom/websocket/tests/test_websocket_permessage_deflate.html create mode 100644 dom/websocket/tests/test_websocket_sandbox.html create mode 100644 dom/websocket/tests/test_websocket_sharedWorker.html create mode 100644 dom/websocket/tests/test_worker_websocket1.html create mode 100644 dom/websocket/tests/test_worker_websocket2.html create mode 100644 dom/websocket/tests/test_worker_websocket3.html create mode 100644 dom/websocket/tests/test_worker_websocket4.html create mode 100644 dom/websocket/tests/test_worker_websocket5.html create mode 100644 dom/websocket/tests/test_worker_websocket_basic.html create mode 100644 dom/websocket/tests/test_worker_websocket_https.html create mode 100644 dom/websocket/tests/test_worker_websocket_loadgroup.html create mode 100644 dom/websocket/tests/websocket_basic_worker.js create mode 100644 dom/websocket/tests/websocket_helpers.js create mode 100644 dom/websocket/tests/websocket_https_worker.js create mode 100644 dom/websocket/tests/websocket_hybi/file_binary-frames_wsh.py create mode 100644 dom/websocket/tests/websocket_hybi/file_check-binary-messages_wsh.py create mode 100644 dom/websocket/tests/websocket_hybi/mochitest.ini create mode 100644 dom/websocket/tests/websocket_hybi/test_receive-arraybuffer.html create mode 100644 dom/websocket/tests/websocket_hybi/test_receive-blob.html create mode 100644 dom/websocket/tests/websocket_hybi/test_send-arraybuffer.html create mode 100644 dom/websocket/tests/websocket_hybi/test_send-blob.html create mode 100644 dom/websocket/tests/websocket_loadgroup_worker.js create mode 100644 dom/websocket/tests/websocket_sharedWorker.js create mode 100644 dom/websocket/tests/websocket_tests.js create mode 100644 dom/websocket/tests/websocket_worker1.js create mode 100644 dom/websocket/tests/websocket_worker2.js create mode 100644 dom/websocket/tests/websocket_worker3.js create mode 100644 dom/websocket/tests/websocket_worker4.js create mode 100644 dom/websocket/tests/websocket_worker5.js create mode 100644 dom/websocket/tests/websocket_worker_helpers.js create mode 100644 dom/websocket/tests/websocket_worker_https.html create mode 100644 dom/websocket/tests/window_bug1384658.html create mode 100644 dom/websocket/tests/window_websocket_wss.html (limited to 'dom/websocket') diff --git a/dom/websocket/WebSocket.cpp b/dom/websocket/WebSocket.cpp new file mode 100644 index 0000000000..39b3501ea2 --- /dev/null +++ b/dom/websocket/WebSocket.cpp @@ -0,0 +1,2891 @@ +/* -*- 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/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::dom { + +class WebSocketImpl; + +// This class is responsible for proxying nsIObserver and nsIWebSocketImpl +// interfaces to WebSocketImpl. WebSocketImplProxy should be only accessed on +// main thread, so we can let it support weak reference. +class WebSocketImplProxy final : public nsIObserver, + public nsSupportsWeakReference, + public nsIWebSocketImpl { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIWEBSOCKETIMPL + + explicit WebSocketImplProxy(WebSocketImpl* aOwner) : mOwner(aOwner) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void Disconnect() { + MOZ_ASSERT(NS_IsMainThread()); + + mOwner = nullptr; + } + + private: + ~WebSocketImplProxy() = default; + + RefPtr mOwner; +}; + +class WebSocketImpl final : public nsIInterfaceRequestor, + public nsIWebSocketListener, + public nsIObserver, + public nsIRequest, + public nsISerialEventTarget, + 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, bool aIsSecure, nsIPrincipal* aPrincipal, + const Maybe& aClientInfo, + nsICSPEventListener* aCSPEventListener, bool aIsServerSide, + const nsAString& aURL, nsTArray& aProtocolArray, + const nsACString& aScriptFile, uint32_t aScriptLine, + uint32_t aScriptColumn); + + nsresult AsyncOpen(nsIPrincipal* aPrincipal, uint64_t aInnerWindowID, + nsITransportProvider* aTransportProvider, + const nsACString& aNegotiatedExtensions, + UniquePtr 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&& aFormatStrings); + + nsresult DoOnMessageAvailable(const nsACString& aMsg, bool isBinary) const; + + // 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 IsSecure(bool* aValue); + + RefPtr mWebSocket; + + nsCOMPtr 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 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 Web Socket owner window. Note that this may not + // be the same as the inner window where the script lives. + // e.g within iframes + // These attributes are used for error reporting. + nsCString mScriptFile; + uint32_t mScriptLine; + uint32_t mScriptColumn; + uint64_t mInnerWindowID; + bool mPrivateBrowsing; + bool mIsChromeContext; + + RefPtr mWorkerRef; + + nsWeakPtr mWeakLoadGroup; + + bool mIsMainThread; + + // This mutex protects mWorkerShuttingDown. + mozilla::Mutex mMutex; + bool mWorkerShuttingDown MOZ_GUARDED_BY(mMutex); + + RefPtr mService; + nsCOMPtr mLoadingPrincipal; + + // For dispatching runnables to main thread. + nsCOMPtr mMainThreadEventTarget; + + RefPtr mImplProxy; + + private: + ~WebSocketImpl() { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread() == mIsMainThread || + mDisconnectingOrDisconnected); + + // If we threw during Init we never called disconnect + if (!mDisconnectingOrDisconnected) { + Disconnect(); + } + } +}; + +NS_IMPL_ISUPPORTS(WebSocketImplProxy, nsIObserver, nsISupportsWeakReference, + nsIWebSocketImpl) + +NS_IMETHODIMP +WebSocketImplProxy::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!mOwner) { + return NS_OK; + } + + return mOwner->Observe(aSubject, aTopic, aData); +} + +NS_IMETHODIMP +WebSocketImplProxy::SendMessage(const nsAString& aMessage) { + if (!mOwner) { + return NS_OK; + } + + return mOwner->SendMessage(aMessage); +} + +NS_IMPL_ISUPPORTS(WebSocketImpl, nsIInterfaceRequestor, nsIWebSocketListener, + nsIObserver, nsIRequest, nsIEventTarget, nsISerialEventTarget, + 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 mWebSocketImpl; +}; + +//----------------------------------------------------------------------------- +// WebSocketImpl +//----------------------------------------------------------------------------- + +namespace { + +class PrintErrorOnConsoleRunnable final : public WorkerMainThreadRunnable { + public: + PrintErrorOnConsoleRunnable(WebSocketImpl* aImpl, const char* aBundleURI, + const char* aError, + nsTArray&& 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 mFormatStrings; +}; + +} // namespace + +void WebSocketImpl::PrintErrorOnConsole(const char* aBundleURI, + const char* aError, + nsTArray&& aFormatStrings) { + // This method must run on the main thread. + + if (!NS_IsMainThread()) { + MOZ_ASSERT(mWorkerRef); + + RefPtr 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 bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr strBundle; + rv = bundleService->CreateBundle(aBundleURI, getter_AddRefs(strBundle)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr console( + do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr 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"_ns, + mInnerWindowID); + } else { + rv = + errorObject->Init(message, NS_ConvertUTF8toUTF16(mScriptFile), u""_ns, + mScriptLine, mScriptColumn, nsIScriptError::errorFlag, + "Web Socket"_ns, 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 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 mImpl; + uint16_t mReasonCode; + const nsCString mReasonString; +}; + +} // namespace + +nsresult WebSocketImpl::CloseConnection(uint16_t aReasonCode, + const nsACString& aReasonString) { + if (!IsTargetThread()) { + nsCOMPtr 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 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 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); + + if (NS_IsMainThread() && mImplProxy) { + mImplProxy->Disconnect(); + mImplProxy = nullptr; + } +} + +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 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 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 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 os = mozilla::services::GetObserverService(); + if (os) { + os->RemoveObserver(mImplProxy, DOM_WINDOW_DESTROYED_TOPIC); + os->RemoveObserver(mImplProxy, DOM_WINDOW_FROZEN_TOPIC); + } + } + + if (mImplProxy) { + mImplProxy->Disconnect(); + mImplProxy = nullptr; + } +} + +//----------------------------------------------------------------------------- +// WebSocketImpl::nsIWebSocketImpl +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebSocketImpl::SendMessage(const nsAString& aMessage) { + nsString message(aMessage); + nsCOMPtr runnable = NS_NewRunnableFunction( + "WebSocketImpl::SendMessage", + [self = RefPtr(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) const { + 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 = 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 win = mWebSocket->GetWindowIfCurrent(); + if (!win) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv; + nsCOMPtr wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 +WebSocket::GetDebuggerNotificationType() const { + return mozilla::Some(EventCallbackDebuggerNotificationType::Websocket); +} + +JSObject* WebSocket::WrapObject(JSContext* cx, + JS::Handle aGivenProto) { + return WebSocket_Binding::Wrap(cx, this, aGivenProto); +} + +//--------------------------------------------------------------------------- +// WebIDL +//--------------------------------------------------------------------------- + +// Constructor: +already_AddRefed 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 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::CreateServerWebSocket( + const GlobalObject& aGlobal, const nsAString& aUrl, + const Sequence& 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, + const Maybe& aClientInfo, + bool aIsServerSide, const nsAString& aURL, + nsTArray& aProtocolArray, + const nsACString& aScriptFile, uint32_t aScriptLine, + uint32_t aScriptColumn) + : WebSocketMainThreadRunnable(aWorkerPrivate, "WebSocket :: init"_ns), + mImpl(aImpl), + mClientInfo(aClientInfo), + 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()->SchemeIs("https"), + doc->NodePrincipal(), mClientInfo, mWorkerPrivate->CSPEventListener(), + 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()->SchemeIs("https"), + aTopLevelWorkerPrivate->GetPrincipal(), mClientInfo, + mWorkerPrivate->CSPEventListener(), mIsServerSide, mURL, + mProtocolArray, mScriptFile, mScriptLine, mScriptColumn); + return true; + } + + // Raw pointer. This worker runnable runs synchronously. + WebSocketImpl* mImpl; + + Maybe mClientInfo; + bool mIsServerSide; + const nsAString& mURL; + nsTArray& 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 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 principal = doc->PartitionedPrincipal(); + if (!principal) { + mErrorCode = NS_ERROR_FAILURE; + return true; + } + + uint64_t windowID = 0; + if (WindowContext* wc = aWindow->GetWindowContext()) { + windowID = wc->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->GetPartitionedPrincipal(), 0, + nullptr, ""_ns, nullptr); + return true; + } + + private: + // Raw pointer. This worker runs synchronously. + WebSocketImpl* mImpl; + + UniquePtr mOriginStack; + + nsresult mErrorCode; +}; + +} // namespace + +// Check a protocol entry contains only valid characters +bool WebSocket::IsValidProtocolString(const nsString& aValue) { + // RFC 6455 (4.1): "not including separator characters as defined in RFC 2616" + const char16_t illegalCharacters[] = {0x28, 0x29, 0x3C, 0x3E, 0x40, 0x2C, + 0x3B, 0x3A, 0x5C, 0x22, 0x2F, 0x5B, + 0x5D, 0x3F, 0x3D, 0x7B, 0x7D}; + + // Cannot be empty string + if (aValue.IsEmpty()) { + return false; + } + + const auto* start = aValue.BeginReading(); + const auto* end = aValue.EndReading(); + + auto charFilter = [&](char16_t c) { + // RFC 6455 (4.1 P18): "in the range U+0021 to U+007E" + if (c < 0x21 || c > 0x7E) { + return true; + } + + return std::find(std::begin(illegalCharacters), std::end(illegalCharacters), + c) != std::end(illegalCharacters); + }; + + return std::find_if(start, end, charFilter) == end; +} + +already_AddRefed WebSocket::ConstructorCommon( + const GlobalObject& aGlobal, const nsAString& aUrl, + const Sequence& aProtocols, + nsITransportProvider* aTransportProvider, + const nsACString& aNegotiatedExtensions, ErrorResult& aRv) { + MOZ_ASSERT_IF(!aTransportProvider, aNegotiatedExtensions.IsEmpty()); + nsCOMPtr principal; + nsCOMPtr partitionedPrincipal; + + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + if (NS_IsMainThread()) { + nsCOMPtr scriptPrincipal = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!scriptPrincipal) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + principal = scriptPrincipal->GetPrincipal(); + partitionedPrincipal = scriptPrincipal->PartitionedPrincipal(); + if (!principal || !partitionedPrincipal) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + } + + nsTArray protocolArray; + + for (uint32_t index = 0, len = aProtocols.Length(); index < len; ++index) { + const nsString& protocolElement = aProtocols[index]; + + // Repeated protocols are not allowed + if (protocolArray.Contains(protocolElement)) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + + // Protocol string value must match constraints + if (!IsValidProtocolString(protocolElement)) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + + protocolArray.AppendElement(protocolElement); + } + + RefPtr webSocket = new WebSocket(global); + RefPtr 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. + if (webSocket->GetOwner()) { + webSocket->GetOwner()->UpdateWebSocketCount(1); + } + + bool isSecure = principal->SchemeIs("https"); + aRv = webSocketImpl->IsSecure(&isSecure); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + aRv = webSocketImpl->Init(aGlobal.Context(), isSecure, principal, Nothing(), + nullptr, !!aTransportProvider, aUrl, + protocolArray, ""_ns, 0, 0); + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr 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 runnable = new InitRunnable( + workerPrivate, webSocketImpl, + workerPrivate->GlobalScope()->GetClientInfo(), !!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 = + 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); + MOZ_ASSERT(partitionedPrincipal); + + nsCOMPtr ownerWindow = do_QueryInterface(global); + + UniquePtr stack; + uint64_t windowID = 0; + + if (ownerWindow) { + BrowsingContext* browsingContext = ownerWindow->GetBrowsingContext(); + if (browsingContext && browsingContext->WatchedByDevTools()) { + stack = GetCurrentStackForNetMonitor(aGlobal.Context()); + } + + if (WindowContext* wc = ownerWindow->GetWindowContext()) { + windowID = wc->InnerWindowId(); + } + } + + aRv = webSocket->mImpl->AsyncOpen(partitionedPrincipal, windowID, + aTransportProvider, aNegotiatedExtensions, + std::move(stack)); + } else { + MOZ_ASSERT(!aTransportProvider && aNegotiatedExtensions.IsEmpty(), + "not yet implemented"); + + UniquePtr stack; + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + if (workerPrivate->IsWatchedByDevTools()) { + stack = GetCurrentStackForNetMonitor(aGlobal.Context()); + } + + RefPtr 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, bool aIsSecure, + nsIPrincipal* aPrincipal, + const Maybe& aClientInfo, + nsICSPEventListener* aCSPEventListener, + bool aIsServerSide, const nsAString& aURL, + nsTArray& 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 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) + RefPtr proxy; + if (mIsMainThread) { + nsCOMPtr os = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!os)) { + return NS_ERROR_FAILURE; + } + + proxy = new WebSocketImplProxy(this); + rv = os->AddObserver(proxy, DOM_WINDOW_DESTROYED_TOPIC, true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = os->AddObserver(proxy, 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) { + if (nsPIDOMWindowInner* ownerWindow = mWebSocket->GetOwner()) { + mInnerWindowID = ownerWindow->WindowID(); + } + } + + mPrivateBrowsing = !!aPrincipal->OriginAttributesRef().mPrivateBrowsingId; + mIsChromeContext = aPrincipal->IsSystemPrincipal(); + + // parses the url + rv = ParseURL(aURL); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr originDoc = mWebSocket->GetDocumentIfCurrent(); + if (!originDoc) { + rv = mWebSocket->CheckCurrentGlobalCorrectness(); + NS_ENSURE_SUCCESS(rv, rv); + } + mOriginDocument = do_GetWeakReference(originDoc); + + if (!mIsServerSide) { + nsCOMPtr 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 secCheckLoadInfo = new net::LoadInfo( + aPrincipal, // loading principal + aPrincipal, // triggering principal + originDoc, nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + nsIContentPolicy::TYPE_WEBSOCKET, aClientInfo); + + if (aCSPEventListener) { + secCheckLoadInfo->SetCspEventListener(aCSPEventListener); + } + + 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 (!mSecure && originDoc && + !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL( + originDoc->GetDocumentURI())) { + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mURI); + NS_ENSURE_SUCCESS(rv, rv); + + // secCheckLoadInfo is only used for the triggering principal, so this + // is okay. + if (nsHTTPSOnlyUtils::ShouldUpgradeWebSocket(uri, secCheckLoadInfo)) { + 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) && + !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL( + originDoc->GetDocumentURI())) { + // let's use the old specification before the upgrade for logging + AutoTArray 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 (aIsSecure) { + 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) { + if (!WebSocket::IsValidProtocolString(aProtocolArray[index])) { + return NS_ERROR_DOM_SYNTAX_ERR; + } + + if (!mRequestedProtocolList.IsEmpty()) { + mRequestedProtocolList.AppendLiteral(", "); + } + + AppendUTF16toUTF8(aProtocolArray[index], mRequestedProtocolList); + } + + if (mIsMainThread) { + mImplProxy = std::move(proxy); + } + return NS_OK; +} + +nsresult WebSocketImpl::AsyncOpen( + nsIPrincipal* aPrincipal, uint64_t aInnerWindowID, + nsITransportProvider* aTransportProvider, + const nsACString& aNegotiatedExtensions, + UniquePtr 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 uri; + if (!aTransportProvider) { + rv = NS_NewURI(getter_AddRefs(uri), mURI); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + rv = mChannel->AsyncOpenNative(uri, asciiOrigin, + aPrincipal->OriginAttributesRef(), + 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 mWebSocketImpl; +}; + +nsresult WebSocketImpl::InitializeConnection( + nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mChannel, "mChannel should be null"); + + nsCOMPtr 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 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 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 rr = do_QueryInterface(wsChannel); + NS_ENSURE_TRUE(rr, NS_ERROR_FAILURE); + + rv = rr->RetargetDeliveryTo(this); + NS_ENSURE_SUCCESS(rv, rv); + + mChannel = wsChannel; + + if (mIsMainThread) { + MOZ_ASSERT(mImplProxy); + mService->AssociateWebSocketImplWithSerialID(mImplProxy, + 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 = 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 = 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 jsData(cx); + if (aIsBinary) { + if (mBinaryType == dom::BinaryType::Blob) { + messageType = nsIWebSocketEventListener::TYPE_BLOB; + + RefPtr 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 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 event = new MessageEvent(this, nullptr, nullptr); + + event->InitMessageEvent(nullptr, MESSAGE_EVENT_STRING, CanBubble::eNo, + Cancelable::eNo, jsData, mImpl->mUTF16Origin, u""_ns, + nullptr, Sequence>()); + 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 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 uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); + + nsCOMPtr 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(0x0021) || + mResource[i] > static_cast(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 self = this; + + // In workers we have to keep the worker alive using a strong reference in + // order to dispatch messages correctly. + RefPtr 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 channel; + channel = static_cast(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 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(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(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& aCode, + const Optional& 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; + } + + RefPtr impl = mImpl; + if (readyState == CONNECTING) { + impl->FailConnection(closeCode, closeReason); + return; + } + + MOZ_ASSERT(readyState == OPEN); + impl->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 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 mImpl; +}; + +} // namespace + +NS_IMETHODIMP WebSocketImpl::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP WebSocketImpl::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP WebSocketImpl::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +// 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 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 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 mWebSocketImpl; + + public: + WorkerRunnableDispatcher(WebSocketImpl* aImpl, + ThreadSafeWorkerRef* aWorkerRef, + already_AddRefed 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 mEvent; +}; + +} // namespace + +NS_IMETHODIMP +WebSocketImpl::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) { + nsCOMPtr event(aEvent); + return Dispatch(event.forget(), aFlags); +} + +NS_IMETHODIMP +WebSocketImpl::Dispatch(already_AddRefed aEvent, uint32_t aFlags) { + nsCOMPtr 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()) + : GetMainThreadSerialEventTarget()->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 event = + new WorkerRunnableDispatcher(this, mWorkerRef, event_ref.forget()); + + if (!event->Dispatch()) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketImpl::DelayedDispatch(already_AddRefed, uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WebSocketImpl::RegisterShutdownTask(nsITargetShutdownTask*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WebSocketImpl::UnregisterShutdownTask(nsITargetShutdownTask*) { + 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 { + // FIXME: This should also check if we're on the worker thread. Code using + // `IsOnCurrentThread` could easily misbehave here! + return NS_IsMainThread() == mIsMainThread; +} + +void WebSocket::AssertIsOnTargetThread() const { + MOZ_ASSERT(NS_IsMainThread() == mIsMainThread); +} + +nsresult WebSocketImpl::IsSecure(bool* aValue) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mIsMainThread); + + // Check the principal's uri to determine if we were loaded from https. + nsCOMPtr globalObject(GetEntryGlobal()); + nsCOMPtr principal; + + if (globalObject) { + principal = globalObject->PrincipalOrNull(); + } + + nsCOMPtr 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. + if (NS_WARN_IF(!principal)) { + return NS_OK; + } + *aValue = principal->SchemeIs("https"); + return NS_OK; + } + + RefPtr windowContext = innerWindow->GetWindowContext(); + if (NS_WARN_IF(!windowContext)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + while (true) { + if (windowContext->GetIsSecure()) { + *aValue = true; + return NS_OK; + } + + if (windowContext->IsTop()) { + break; + } 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; + } + } + + *aValue = windowContext->GetIsSecure(); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/websocket/WebSocket.h b/dom/websocket/WebSocket.h new file mode 100644 index 0000000000..a290750cf0 --- /dev/null +++ b/dom/websocket/WebSocket.h @@ -0,0 +1,192 @@ +/* -*- 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 + GetDebuggerNotificationType() const override; + + // nsWrapperCache + virtual JSObject* WrapObject(JSContext* cx, + JS::Handle 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 Constructor( + const GlobalObject& aGlobal, const nsAString& aUrl, + const StringOrStringSequence& aProtocols, ErrorResult& rv); + + static already_AddRefed CreateServerWebSocket( + const GlobalObject& aGlobal, const nsAString& aUrl, + const Sequence& aProtocols, + nsITransportProvider* aTransportProvider, + const nsAString& aNegotiatedExtensions, ErrorResult& rv); + + static already_AddRefed ConstructorCommon( + const GlobalObject& aGlobal, const nsAString& aUrl, + const Sequence& 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& aCode, + const Optional& 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); + + static bool IsValidProtocolString(const nsString& aValue); + + // 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 MOZ_GUARDED_BY(mMutex); +}; + +} // 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/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 @@ + + + + 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..5853f8acd7 --- /dev/null +++ b/dom/websocket/tests/file_websocket_permessage_deflate_disabled_wsh.py @@ -0,0 +1,18 @@ +from mod_pywebsocket import common, msgutil + + +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..e6ea12232f --- /dev/null +++ b/dom/websocket/tests/file_websocket_permessage_deflate_params_wsh.py @@ -0,0 +1,24 @@ +from mod_pywebsocket import common, msgutil + + +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..b67627d67c --- /dev/null +++ b/dom/websocket/tests/file_websocket_permessage_deflate_rejected_wsh.py @@ -0,0 +1,24 @@ +from mod_pywebsocket import common, msgutil + + +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..c6436c8499 --- /dev/null +++ b/dom/websocket/tests/file_websocket_permessage_deflate_wsh.py @@ -0,0 +1,23 @@ +from mod_pywebsocket import common, msgutil + + +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..de69f747e8 --- /dev/null +++ b/dom/websocket/tests/file_websocket_wsh.py @@ -0,0 +1,168 @@ +import time + +import six +from mod_pywebsocket import msgutil + +# 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, six.ensure_text(resp)) + resp = "wrong message" + if msgutil.receive_message(request) == "3": + resp = "4" + msgutil.send_message(request, six.ensure_text(resp)) + resp = "wrong message" + if msgutil.receive_message(request) == "5": + resp = ( + b"\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86\xe3\x81\x88\xe3\x81\x8a".decode( + "utf-8" + ) + ) + msgutil.send_message(request, six.ensure_text(resp)) + 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, six.ensure_text(resp)) + 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(b"\xff\x01\x61") + # after a bad utf8 message + request.connection.write(b"\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, six.ensure_text(resp)) + 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 = six.ensure_text(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, b"\x00\x04", 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 six.ensure_text(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 @@ + + +
+ + +
+ + + 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 @@ + + + + diff --git a/dom/websocket/tests/iframe_websocket_wss.html b/dom/websocket/tests/iframe_websocket_wss.html new file mode 100644 index 0000000000..71945a30d9 --- /dev/null +++ b/dom/websocket/tests/iframe_websocket_wss.html @@ -0,0 +1,35 @@ + +Creating WebSocket + + diff --git a/dom/websocket/tests/mochitest.ini b/dom/websocket/tests/mochitest.ini new file mode 100644 index 0000000000..50968d7626 --- /dev/null +++ b/dom/websocket/tests/mochitest.ini @@ -0,0 +1,64 @@ +[DEFAULT] +skip-if = + toolkit == 'android' # bug 982828 + http3 +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_websocket_mixed_content.html] +scheme=https +support-files = iframe_websocket_wss.html +[test_websocket_mixed_content_opener.html] +scheme=https +support-files = window_websocket_wss.html +[test_worker_websocket1.html] +support-files = websocket_worker1.js +[test_worker_websocket2.html] +support-files = websocket_worker2.js +skip-if = socketprocess_networking # bug 1787044 +[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 @@ + + + + + bug 1081686 + + + + +

+ +
+
+
+
+ + +
+ + + + diff --git a/dom/websocket/tests/test_bug1384658.html b/dom/websocket/tests/test_bug1384658.html new file mode 100644 index 0000000000..dd90a0d5d3 --- /dev/null +++ b/dom/websocket/tests/test_bug1384658.html @@ -0,0 +1,54 @@ + + + + + + Test for Bug 1384658 + + + + + + + + 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 @@ + + + + + Bug 1450358 - Test WebSocket event listener leak conditions + + + + + + + + + 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 @@ + + + + + WebSocket test + + + + + + + + +
+
+ + + 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 @@ + + + + + WebSocket test + + + + + + + + +
+
+ + + 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 @@ + + + + + WebSocket test + + + + + + + + +
+
+ + + 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 @@ + + + + + WebSocket test + + + + + + + + +
+
+ + + 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 @@ + + + + + WebSocket test + + + + + + + + +
+
+ + + 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 @@ + + + + Basic WebSocket test + + + + + +Mozilla Bug 472529 +

+ +
+
+
+ + 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 @@ + + + + + WebSocket test - big blob on content side + + + + + + + + diff --git a/dom/websocket/tests/test_websocket_frame.html b/dom/websocket/tests/test_websocket_frame.html new file mode 100644 index 0000000000..6c95419bc2 --- /dev/null +++ b/dom/websocket/tests/test_websocket_frame.html @@ -0,0 +1,161 @@ + + + + + Basic websocket frame interception test + + + + + + + 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 @@ + + + + + Basic websocket test + + + + +Mozilla Bug +

+ +
+
+
+
+ + +
+ + + + 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 @@ + + + + + WebSocket test - big blob on content side + + + + + + + + diff --git a/dom/websocket/tests/test_websocket_mixed_content.html b/dom/websocket/tests/test_websocket_mixed_content.html new file mode 100644 index 0000000000..bc2ac40188 --- /dev/null +++ b/dom/websocket/tests/test_websocket_mixed_content.html @@ -0,0 +1,96 @@ + + + + + WebSocket mixed content tests - load secure and insecure websockets + + + + + + +
+ + + + diff --git a/dom/websocket/tests/test_websocket_mixed_content_opener.html b/dom/websocket/tests/test_websocket_mixed_content_opener.html new file mode 100644 index 0000000000..7db0325825 --- /dev/null +++ b/dom/websocket/tests/test_websocket_mixed_content_opener.html @@ -0,0 +1,139 @@ + + + + + WebSocket mixed content opener tests - load secure and insecure websockets in secure and insecure iframes through secure and insecure opened windows + + + + + + +
+ + + 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 @@ + + + + + WebSocket test - big blob on content side + + + + + + + + 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 @@ + + + + Basic test of permessage compression websocket extension + + + + +Mozilla Bug +

+ +
+
+
+ + diff --git a/dom/websocket/tests/test_websocket_sandbox.html b/dom/websocket/tests/test_websocket_sandbox.html new file mode 100644 index 0000000000..2803186ff0 --- /dev/null +++ b/dom/websocket/tests/test_websocket_sandbox.html @@ -0,0 +1,34 @@ + + + + Bug 1252751 + + + + +
+ + + + diff --git a/dom/websocket/tests/test_websocket_sharedWorker.html b/dom/websocket/tests/test_websocket_sharedWorker.html new file mode 100644 index 0000000000..b79b0a6ced --- /dev/null +++ b/dom/websocket/tests/test_websocket_sharedWorker.html @@ -0,0 +1,30 @@ + + + + + Test for bug 1090183 + + + + + + + + + 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 @@ + + + + + Test for WebSocket object in workers + + + + +

+

+
+
+
+
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 @@
+
+
+
+
+  Test for WebSocket object in workers
+  
+  
+
+
+

+

+
+
+
+
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 @@
+
+
+
+
+  Test for WebSocket object in workers
+  
+  
+
+
+

+

+
+
+
+
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 @@
+
+
+
+
+  Test for WebSocket object in workers
+  
+  
+
+
+

+

+
+
+
+
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 @@
+
+
+
+
+  Test for WebSocket object in workers
+  
+  
+
+
+

+

+
+
+
+
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 @@
+
+
+
+
+  Test for WebSocket object in workers
+  
+  
+
+
+

+ +

+
+
+
+
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 @@
+
+
+
+
+  Test that creating insecure websockets from https workers is not possible
+  
+  
+
+
+

+ +
+
+
+ + + 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 @@ + + + + + Test for WebSocket object in workers + + + + +

+ +

+
+
+
+
diff --git a/dom/websocket/tests/websocket_basic_worker.js b/dom/websocket/tests/websocket_basic_worker.js
new file mode 100644
index 0000000000..089fe3edc4
--- /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..6b988b2fa4
--- /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..e07b0dbec5
--- /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..16ace4d3a6
--- /dev/null
+++ b/dom/websocket/tests/websocket_hybi/file_binary-frames_wsh.py
@@ -0,0 +1,19 @@
+import six
+from mod_pywebsocket import common, 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:
+        message = six.b(message)
+        # 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..d72ec1f462
--- /dev/null
+++ b/dom/websocket/tests/websocket_hybi/file_check-binary-messages_wsh.py
@@ -0,0 +1,27 @@
+import six
+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):
+        expected_message = six.b(expected_message)
+        message = msgutil.receive_message(request)
+        if 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..dbba7ba175
--- /dev/null
+++ b/dom/websocket/tests/websocket_hybi/mochitest.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+skip-if =
+  http3
+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 @@
+
+
+
+  
+  
+
+
+
+

+ +
+
+
+
+
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 @@
+
+
+
+  
+  
+
+
+
+

+ +
+
+
+
+
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 @@
+
+
+
+  
+  
+
+
+
+

+ +
+
+
+
+
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 @@
+
+
+
+  
+  
+
+
+
+

+

+ +
+
+
+
+
diff --git a/dom/websocket/tests/websocket_loadgroup_worker.js b/dom/websocket/tests/websocket_loadgroup_worker.js
new file mode 100644
index 0000000000..6de82d1900
--- /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_sharedWorker.js b/dom/websocket/tests/websocket_sharedWorker.js
new file mode 100644
index 0000000000..7c832f5c30
--- /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_tests.js b/dom/websocket/tests/websocket_tests.js
new file mode 100644
index 0000000000..df6e12b45f
--- /dev/null
+++ b/dom/websocket/tests/websocket_tests.js
@@ -0,0 +1,1483 @@
+// 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");
+
+      // Since third-party loads can't set HSTS state, this will not set
+      // HSTS 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 ws:// again
+        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,
+            "ws://example.com/tests/dom/websocket/tests/file_websocket",
+            "test 41c ws should not be redirected by hsts to wss"
+          );
+          wsc.close();
+        };
+
+        wsc.onclose = function () {
+          ok(true, "test 41c close");
+          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 @@
+
+
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 @@
+
+
+
+
+
+
+
+
diff --git a/dom/websocket/tests/window_websocket_wss.html b/dom/websocket/tests/window_websocket_wss.html
new file mode 100644
index 0000000000..391ab74e40
--- /dev/null
+++ b/dom/websocket/tests/window_websocket_wss.html
@@ -0,0 +1,65 @@
+
+Creating WebSocket
+
+
+
-- 
cgit v1.2.3