summaryrefslogtreecommitdiffstats
path: root/dom/websocket
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/websocket
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/websocket')
-rw-r--r--dom/websocket/WebSocket.cpp2891
-rw-r--r--dom/websocket/WebSocket.h192
-rw-r--r--dom/websocket/moz.build33
-rw-r--r--dom/websocket/tests/chrome.ini4
-rw-r--r--dom/websocket/tests/file_bug1384658.html18
-rw-r--r--dom/websocket/tests/file_websocket_basic_wsh.py31
-rw-r--r--dom/websocket/tests/file_websocket_bigBlob_wsh.py11
-rw-r--r--dom/websocket/tests/file_websocket_hello_wsh.py12
-rw-r--r--dom/websocket/tests/file_websocket_http_resource.txt1
-rw-r--r--dom/websocket/tests/file_websocket_permessage_deflate_disabled_wsh.py18
-rw-r--r--dom/websocket/tests/file_websocket_permessage_deflate_params_wsh.py24
-rw-r--r--dom/websocket/tests/file_websocket_permessage_deflate_rejected_wsh.py24
-rw-r--r--dom/websocket/tests/file_websocket_permessage_deflate_wsh.py23
-rw-r--r--dom/websocket/tests/file_websocket_wsh.py168
-rw-r--r--dom/websocket/tests/frame_bug1384658.html13
-rw-r--r--dom/websocket/tests/iframe_websocket_sandbox.html65
-rw-r--r--dom/websocket/tests/iframe_websocket_wss.html35
-rw-r--r--dom/websocket/tests/mochitest.ini64
-rw-r--r--dom/websocket/tests/test_bug1081686.html71
-rw-r--r--dom/websocket/tests/test_bug1384658.html54
-rw-r--r--dom/websocket/tests/test_event_listener_leaks.html57
-rw-r--r--dom/websocket/tests/test_websocket1.html42
-rw-r--r--dom/websocket/tests/test_websocket2.html44
-rw-r--r--dom/websocket/tests/test_websocket3.html44
-rw-r--r--dom/websocket/tests/test_websocket4.html42
-rw-r--r--dom/websocket/tests/test_websocket5.html41
-rw-r--r--dom/websocket/tests/test_websocket_basic.html289
-rw-r--r--dom/websocket/tests/test_websocket_bigBlob.html55
-rw-r--r--dom/websocket/tests/test_websocket_frame.html161
-rw-r--r--dom/websocket/tests/test_websocket_hello.html49
-rw-r--r--dom/websocket/tests/test_websocket_longString.html48
-rw-r--r--dom/websocket/tests/test_websocket_mixed_content.html96
-rw-r--r--dom/websocket/tests/test_websocket_mixed_content_opener.html139
-rw-r--r--dom/websocket/tests/test_websocket_no_duplicate_packet.html106
-rw-r--r--dom/websocket/tests/test_websocket_permessage_deflate.html110
-rw-r--r--dom/websocket/tests/test_websocket_sandbox.html34
-rw-r--r--dom/websocket/tests/test_websocket_sharedWorker.html30
-rw-r--r--dom/websocket/tests/test_worker_websocket1.html46
-rw-r--r--dom/websocket/tests/test_worker_websocket2.html46
-rw-r--r--dom/websocket/tests/test_worker_websocket3.html46
-rw-r--r--dom/websocket/tests/test_worker_websocket4.html46
-rw-r--r--dom/websocket/tests/test_worker_websocket5.html46
-rw-r--r--dom/websocket/tests/test_worker_websocket_basic.html57
-rw-r--r--dom/websocket/tests/test_worker_websocket_https.html30
-rw-r--r--dom/websocket/tests/test_worker_websocket_loadgroup.html61
-rw-r--r--dom/websocket/tests/websocket_basic_worker.js48
-rw-r--r--dom/websocket/tests/websocket_helpers.js69
-rw-r--r--dom/websocket/tests/websocket_https_worker.js11
-rw-r--r--dom/websocket/tests/websocket_hybi/file_binary-frames_wsh.py19
-rw-r--r--dom/websocket/tests/websocket_hybi/file_check-binary-messages_wsh.py27
-rw-r--r--dom/websocket/tests/websocket_hybi/mochitest.ini15
-rw-r--r--dom/websocket/tests/websocket_hybi/test_receive-arraybuffer.html97
-rw-r--r--dom/websocket/tests/websocket_hybi/test_receive-blob.html110
-rw-r--r--dom/websocket/tests/websocket_hybi/test_send-arraybuffer.html82
-rw-r--r--dom/websocket/tests/websocket_hybi/test_send-blob.html72
-rw-r--r--dom/websocket/tests/websocket_loadgroup_worker.js26
-rw-r--r--dom/websocket/tests/websocket_sharedWorker.js34
-rw-r--r--dom/websocket/tests/websocket_tests.js1483
-rw-r--r--dom/websocket/tests/websocket_worker1.js19
-rw-r--r--dom/websocket/tests/websocket_worker2.js19
-rw-r--r--dom/websocket/tests/websocket_worker3.js17
-rw-r--r--dom/websocket/tests/websocket_worker4.js19
-rw-r--r--dom/websocket/tests/websocket_worker5.js14
-rw-r--r--dom/websocket/tests/websocket_worker_helpers.js27
-rw-r--r--dom/websocket/tests/websocket_worker_https.html14
-rw-r--r--dom/websocket/tests/window_bug1384658.html19
-rw-r--r--dom/websocket/tests/window_websocket_wss.html65
67 files changed, 7823 insertions, 0 deletions
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<WebSocketImpl> 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<ClientInfo>& aClientInfo,
+ nsICSPEventListener* aCSPEventListener, bool aIsServerSide,
+ const nsAString& aURL, nsTArray<nsString>& aProtocolArray,
+ const nsACString& aScriptFile, uint32_t aScriptLine,
+ uint32_t aScriptColumn);
+
+ nsresult AsyncOpen(nsIPrincipal* aPrincipal, uint64_t aInnerWindowID,
+ nsITransportProvider* aTransportProvider,
+ const nsACString& aNegotiatedExtensions,
+ UniquePtr<SerializedStackHolder> aOriginStack);
+
+ nsresult ParseURL(const nsAString& aURL);
+ nsresult InitializeConnection(nsIPrincipal* aPrincipal,
+ nsICookieJarSettings* aCookieJarSettings);
+
+ // These methods when called can release the WebSocket object
+ void FailConnection(uint16_t reasonCode,
+ const nsACString& aReasonString = ""_ns);
+ nsresult CloseConnection(uint16_t reasonCode,
+ const nsACString& aReasonString = ""_ns);
+ void Disconnect();
+ void DisconnectInternal();
+
+ nsresult ConsoleError();
+ void PrintErrorOnConsole(const char* aBundleURI, const char* aError,
+ nsTArray<nsString>&& aFormatStrings);
+
+ nsresult DoOnMessageAvailable(const nsACString& aMsg, bool isBinary) 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<WebSocket> mWebSocket;
+
+ nsCOMPtr<nsIWebSocketChannel> mChannel;
+
+ bool mIsServerSide; // True if we're implementing the server side of a
+ // websocket connection
+
+ bool mSecure; // if true it is using SSL and the wss scheme,
+ // otherwise it is using the ws scheme with no SSL
+
+ bool mOnCloseScheduled;
+ bool mFailed;
+ Atomic<bool> mDisconnectingOrDisconnected;
+
+ // Set attributes of DOM 'onclose' message
+ bool mCloseEventWasClean;
+ nsString mCloseEventReason;
+ uint16_t mCloseEventCode;
+
+ nsCString mAsciiHost; // hostname
+ uint32_t mPort;
+ nsCString mResource; // [filepath[?query]]
+ nsString mUTF16Origin;
+
+ nsCString mURI;
+ nsCString mRequestedProtocolList;
+
+ nsWeakPtr mOriginDocument;
+
+ // Web Socket owner information:
+ // - the script file name, UTF8 encoded.
+ // - source code line number and column number where the Web Socket object
+ // was constructed.
+ // - the ID of the 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<ThreadSafeWorkerRef> mWorkerRef;
+
+ nsWeakPtr mWeakLoadGroup;
+
+ bool mIsMainThread;
+
+ // This mutex protects mWorkerShuttingDown.
+ mozilla::Mutex mMutex;
+ bool mWorkerShuttingDown MOZ_GUARDED_BY(mMutex);
+
+ RefPtr<WebSocketEventService> mService;
+ nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
+
+ // For dispatching runnables to main thread.
+ nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget;
+
+ RefPtr<WebSocketImplProxy> 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<WebSocketImpl> mWebSocketImpl;
+};
+
+//-----------------------------------------------------------------------------
+// WebSocketImpl
+//-----------------------------------------------------------------------------
+
+namespace {
+
+class PrintErrorOnConsoleRunnable final : public WorkerMainThreadRunnable {
+ public:
+ PrintErrorOnConsoleRunnable(WebSocketImpl* aImpl, const char* aBundleURI,
+ const char* aError,
+ nsTArray<nsString>&& aFormatStrings)
+ : WorkerMainThreadRunnable(aImpl->mWorkerRef->Private(),
+ "WebSocket :: print error on console"_ns),
+ mImpl(aImpl),
+ mBundleURI(aBundleURI),
+ mError(aError),
+ mFormatStrings(std::move(aFormatStrings)) {}
+
+ bool MainThreadRun() override {
+ mImpl->PrintErrorOnConsole(mBundleURI, mError, std::move(mFormatStrings));
+ return true;
+ }
+
+ private:
+ // Raw pointer because this runnable is sync.
+ WebSocketImpl* mImpl;
+
+ const char* mBundleURI;
+ const char* mError;
+ nsTArray<nsString> mFormatStrings;
+};
+
+} // namespace
+
+void WebSocketImpl::PrintErrorOnConsole(const char* aBundleURI,
+ const char* aError,
+ nsTArray<nsString>&& aFormatStrings) {
+ // This method must run on the main thread.
+
+ if (!NS_IsMainThread()) {
+ MOZ_ASSERT(mWorkerRef);
+
+ RefPtr<PrintErrorOnConsoleRunnable> runnable =
+ new PrintErrorOnConsoleRunnable(this, aBundleURI, aError,
+ std::move(aFormatStrings));
+ ErrorResult rv;
+ runnable->Dispatch(Killing, rv);
+ // XXXbz this seems totally broken. We should be propagating this out, but
+ // none of our callers really propagate anything usefully. Come to think of
+ // it, why is this a syncrunnable anyway? Can't this be a fire-and-forget
+ // runnable??
+ rv.SuppressException();
+ return;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIStringBundle> strBundle;
+ rv = bundleService->CreateBundle(aBundleURI, getter_AddRefs(strBundle));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIScriptError> errorObject(
+ do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Localize the error message
+ nsAutoString message;
+ if (!aFormatStrings.IsEmpty()) {
+ rv = strBundle->FormatStringFromName(aError, aFormatStrings, message);
+ } else {
+ rv = strBundle->GetStringFromName(aError, message);
+ }
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ if (mInnerWindowID) {
+ rv = errorObject->InitWithWindowID(
+ message, NS_ConvertUTF8toUTF16(mScriptFile), u""_ns, mScriptLine,
+ mScriptColumn, nsIScriptError::errorFlag, "Web Socket"_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<nsIWebSocketChannel> mChannel;
+ uint16_t mReasonCode;
+ nsCString mReasonString;
+};
+
+class MOZ_STACK_CLASS MaybeDisconnect {
+ public:
+ explicit MaybeDisconnect(WebSocketImpl* aImpl) : mImpl(aImpl) {}
+
+ ~MaybeDisconnect() {
+ bool toDisconnect = false;
+
+ {
+ MutexAutoLock lock(mImpl->mMutex);
+ toDisconnect = mImpl->mWorkerShuttingDown;
+ }
+
+ if (toDisconnect) {
+ mImpl->Disconnect();
+ }
+ }
+
+ private:
+ WebSocketImpl* mImpl;
+};
+
+class CloseConnectionRunnable final : public Runnable {
+ public:
+ CloseConnectionRunnable(WebSocketImpl* aImpl, uint16_t aReasonCode,
+ const nsACString& aReasonString)
+ : Runnable("dom::CloseConnectionRunnable"),
+ mImpl(aImpl),
+ mReasonCode(aReasonCode),
+ mReasonString(aReasonString) {}
+
+ NS_IMETHOD Run() override {
+ return mImpl->CloseConnection(mReasonCode, mReasonString);
+ }
+
+ private:
+ RefPtr<WebSocketImpl> mImpl;
+ uint16_t mReasonCode;
+ const nsCString mReasonString;
+};
+
+} // namespace
+
+nsresult WebSocketImpl::CloseConnection(uint16_t aReasonCode,
+ const nsACString& aReasonString) {
+ if (!IsTargetThread()) {
+ nsCOMPtr<nsIRunnable> runnable =
+ new CloseConnectionRunnable(this, aReasonCode, aReasonString);
+ return Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ }
+
+ AssertIsOnTargetThread();
+
+ if (mDisconnectingOrDisconnected) {
+ return NS_OK;
+ }
+
+ // If this method is called because the worker is going away, we will not
+ // receive the OnStop() method and we have to disconnect the WebSocket and
+ // release the ThreadSafeWorkerRef.
+ MaybeDisconnect md(this);
+
+ uint16_t readyState = mWebSocket->ReadyState();
+ if (readyState == WebSocket::CLOSING || readyState == WebSocket::CLOSED) {
+ return NS_OK;
+ }
+
+ // The common case...
+ if (mChannel) {
+ mWebSocket->SetReadyState(WebSocket::CLOSING);
+
+ // The channel has to be closed on the main-thread.
+
+ if (NS_IsMainThread()) {
+ return mChannel->Close(aReasonCode, aReasonString);
+ }
+
+ RefPtr<CancelWebSocketRunnable> runnable =
+ new CancelWebSocketRunnable(mChannel, aReasonCode, aReasonString);
+ return NS_DispatchToMainThread(runnable);
+ }
+
+ // No channel, but not disconnected: canceled or failed early
+ MOZ_ASSERT(readyState == WebSocket::CONNECTING,
+ "Should only get here for early websocket cancel/error");
+
+ // Server won't be sending us a close code, so use what's passed in here.
+ mCloseEventCode = aReasonCode;
+ CopyUTF8toUTF16(aReasonString, mCloseEventReason);
+
+ mWebSocket->SetReadyState(WebSocket::CLOSING);
+
+ ScheduleConnectionCloseEvents(
+ nullptr, (aReasonCode == nsIWebSocketChannel::CLOSE_NORMAL ||
+ aReasonCode == nsIWebSocketChannel::CLOSE_GOING_AWAY)
+ ? NS_OK
+ : NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+nsresult WebSocketImpl::ConsoleError() {
+ AssertIsOnTargetThread();
+
+ {
+ MutexAutoLock lock(mMutex);
+ if (mWorkerShuttingDown) {
+ // Too late to report anything, bail out.
+ return NS_OK;
+ }
+ }
+
+ nsTArray<nsString> formatStrings;
+ CopyUTF8toUTF16(mURI, *formatStrings.AppendElement());
+
+ if (mWebSocket->ReadyState() < WebSocket::OPEN) {
+ PrintErrorOnConsole("chrome://global/locale/appstrings.properties",
+ "connectionFailure", std::move(formatStrings));
+ } else {
+ PrintErrorOnConsole("chrome://global/locale/appstrings.properties",
+ "netInterrupt", std::move(formatStrings));
+ }
+ /// todo some specific errors - like for message too large
+ return NS_OK;
+}
+
+void WebSocketImpl::FailConnection(uint16_t aReasonCode,
+ const nsACString& aReasonString) {
+ AssertIsOnTargetThread();
+
+ if (mDisconnectingOrDisconnected) {
+ return;
+ }
+
+ ConsoleError();
+ mFailed = true;
+ CloseConnection(aReasonCode, aReasonString);
+
+ 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<WebSocketImpl> kungfuDeathGrip = this;
+
+ // Disconnect can be called from some control event (such as a callback from
+ // StrongWorkerRef). This will be schedulated before any other sync/async
+ // runnable. In order to prevent some double Disconnect() calls, we use this
+ // boolean.
+ mDisconnectingOrDisconnected = true;
+
+ // DisconnectInternal touches observers and nsILoadGroup and it must run on
+ // the main thread.
+
+ if (NS_IsMainThread()) {
+ DisconnectInternal();
+
+ // If we haven't called WebSocket::DisconnectFromOwner yet, update
+ // web socket count here.
+ if (mWebSocket->GetOwner()) {
+ mWebSocket->GetOwner()->UpdateWebSocketCount(-1);
+ }
+ } else {
+ RefPtr<DisconnectInternalRunnable> runnable =
+ new DisconnectInternalRunnable(this);
+ ErrorResult rv;
+ runnable->Dispatch(Killing, rv);
+ // XXXbz this seems totally broken. We should be propagating this out, but
+ // where to, exactly?
+ rv.SuppressException();
+ }
+
+ NS_ReleaseOnMainThread("WebSocketImpl::mChannel", mChannel.forget());
+ NS_ReleaseOnMainThread("WebSocketImpl::mService", mService.forget());
+
+ mWebSocket->DontKeepAliveAnyMore();
+ mWebSocket->mImpl = nullptr;
+
+ if (mWorkerRef) {
+ UnregisterWorkerRef();
+ }
+
+ // We want to release the WebSocket in the correct thread.
+ mWebSocket = nullptr;
+}
+
+void WebSocketImpl::DisconnectInternal() {
+ AssertIsOnMainThread();
+
+ nsCOMPtr<nsILoadGroup> loadGroup = do_QueryReferent(mWeakLoadGroup);
+ if (loadGroup) {
+ loadGroup->RemoveRequest(this, nullptr, NS_OK);
+ // mWeakLoadGroup has to be release on main-thread because WeakReferences
+ // are not thread-safe.
+ mWeakLoadGroup = nullptr;
+ }
+
+ if (!mWorkerRef) {
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ os->RemoveObserver(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<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "WebSocketImpl::SendMessage",
+ [self = RefPtr<WebSocketImpl>(this), message = std::move(message)]() {
+ ErrorResult IgnoredErrorResult;
+ self->mWebSocket->Send(message, IgnoredErrorResult);
+ });
+ return Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+}
+
+//-----------------------------------------------------------------------------
+// WebSocketImpl::nsIWebSocketListener methods:
+//-----------------------------------------------------------------------------
+
+nsresult WebSocketImpl::DoOnMessageAvailable(const nsACString& aMsg,
+ bool isBinary) 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> webSocket = mWebSocket;
+
+ // Call 'onopen'
+ rv = webSocket->CreateAndDispatchSimpleEvent(OPEN_EVENT_STRING);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the open event");
+ }
+
+ webSocket->UpdateMustKeepAlive();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::OnStop(nsISupports* aContext, nsresult aStatusCode) {
+ AssertIsOnTargetThread();
+
+ if (mDisconnectingOrDisconnected) {
+ return NS_OK;
+ }
+
+ // We can be CONNECTING here if connection failed.
+ // We can be OPEN if we have encountered a fatal protocol error
+ // We can be CLOSING if close() was called and/or server initiated close.
+ MOZ_ASSERT(mWebSocket->ReadyState() != WebSocket::CLOSED,
+ "Shouldn't already be CLOSED when OnStop called");
+
+ return ScheduleConnectionCloseEvents(aContext, aStatusCode);
+}
+
+nsresult WebSocketImpl::ScheduleConnectionCloseEvents(nsISupports* aContext,
+ nsresult aStatusCode) {
+ AssertIsOnTargetThread();
+
+ // no-op if some other code has already initiated close event
+ if (!mOnCloseScheduled) {
+ mCloseEventWasClean = NS_SUCCEEDED(aStatusCode);
+
+ if (aStatusCode == NS_BASE_STREAM_CLOSED) {
+ // don't generate an error event just because of an unclean close
+ aStatusCode = NS_OK;
+ }
+
+ if (aStatusCode == NS_ERROR_NET_INADEQUATE_SECURITY) {
+ // TLS negotiation failed so we need to set status code to 1015.
+ mCloseEventCode = 1015;
+ }
+
+ if (NS_FAILED(aStatusCode)) {
+ ConsoleError();
+ mFailed = true;
+ }
+
+ mOnCloseScheduled = true;
+
+ NS_DispatchToCurrentThread(new CallDispatchConnectionCloseEvents(this));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::OnAcknowledge(nsISupports* aContext, uint32_t aSize) {
+ AssertIsOnTargetThread();
+
+ if (mDisconnectingOrDisconnected) {
+ return NS_OK;
+ }
+
+ MOZ_RELEASE_ASSERT(mWebSocket->mOutgoingBufferedAmount.isValid());
+ if (aSize > mWebSocket->mOutgoingBufferedAmount.value()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ CheckedUint64 outgoingBufferedAmount = mWebSocket->mOutgoingBufferedAmount;
+ outgoingBufferedAmount -= aSize;
+ if (!outgoingBufferedAmount.isValid()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mWebSocket->mOutgoingBufferedAmount = outgoingBufferedAmount;
+ MOZ_RELEASE_ASSERT(mWebSocket->mOutgoingBufferedAmount.isValid());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::OnServerClose(nsISupports* aContext, uint16_t aCode,
+ const nsACString& aReason) {
+ AssertIsOnTargetThread();
+
+ if (mDisconnectingOrDisconnected) {
+ return NS_OK;
+ }
+
+ int16_t readyState = mWebSocket->ReadyState();
+
+ MOZ_ASSERT(readyState != WebSocket::CONNECTING,
+ "Received server close before connected?");
+ MOZ_ASSERT(readyState != WebSocket::CLOSED,
+ "Received server close after already closed!");
+
+ // store code/string for onclose DOM event
+ mCloseEventCode = aCode;
+ CopyUTF8toUTF16(aReason, mCloseEventReason);
+
+ if (readyState == WebSocket::OPEN) {
+ // Server initiating close.
+ // RFC 6455, 5.5.1: "When sending a Close frame in response, the endpoint
+ // typically echos the status code it received".
+ // But never send certain codes, per section 7.4.1
+ if (aCode == 1005 || aCode == 1006 || aCode == 1015) {
+ CloseConnection(0, ""_ns);
+ } else {
+ CloseConnection(aCode, aReason);
+ }
+ } else {
+ // We initiated close, and server has replied: OnStop does rest of the work.
+ MOZ_ASSERT(readyState == WebSocket::CLOSING, "unknown state");
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::OnError() {
+ if (!IsTargetThread()) {
+ return Dispatch(
+ NS_NewRunnableFunction("dom::FailConnectionRunnable",
+ [self = RefPtr{this}]() {
+ self->FailConnection(
+ nsIWebSocketChannel::CLOSE_ABNORMAL);
+ }),
+ NS_DISPATCH_NORMAL);
+ }
+
+ AssertIsOnTargetThread();
+ FailConnection(nsIWebSocketChannel::CLOSE_ABNORMAL);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// WebSocketImpl::nsIInterfaceRequestor
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+WebSocketImpl::GetInterface(const nsIID& aIID, void** aResult) {
+ AssertIsOnMainThread();
+
+ if (!mWebSocket || mWebSocket->ReadyState() == WebSocket::CLOSED) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) ||
+ aIID.Equals(NS_GET_IID(nsIAuthPrompt2))) {
+ nsCOMPtr<nsPIDOMWindowInner> win = mWebSocket->GetWindowIfCurrent();
+ if (!win) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPromptFactory> wwatch =
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsPIDOMWindowOuter> outerWindow = win->GetOuterWindow();
+ return wwatch->GetPrompt(outerWindow, aIID, aResult);
+ }
+
+ return QueryInterface(aIID, aResult);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// WebSocket
+////////////////////////////////////////////////////////////////////////////////
+
+WebSocket::WebSocket(nsIGlobalObject* aGlobal)
+ : DOMEventTargetHelper(aGlobal),
+ mIsMainThread(true),
+ mKeepingAlive(false),
+ mCheckMustKeepAlive(true),
+ mOutgoingBufferedAmount(0),
+ mBinaryType(dom::BinaryType::Blob),
+ mMutex("WebSocket::mMutex"),
+ mReadyState(CONNECTING) {
+ MOZ_ASSERT(aGlobal);
+
+ mImpl = new WebSocketImpl(this);
+ mIsMainThread = mImpl->mIsMainThread;
+}
+
+WebSocket::~WebSocket() = default;
+
+mozilla::Maybe<EventCallbackDebuggerNotificationType>
+WebSocket::GetDebuggerNotificationType() const {
+ return mozilla::Some(EventCallbackDebuggerNotificationType::Websocket);
+}
+
+JSObject* WebSocket::WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return WebSocket_Binding::Wrap(cx, this, aGivenProto);
+}
+
+//---------------------------------------------------------------------------
+// WebIDL
+//---------------------------------------------------------------------------
+
+// Constructor:
+already_AddRefed<WebSocket> WebSocket::Constructor(
+ const GlobalObject& aGlobal, const nsAString& aUrl,
+ const StringOrStringSequence& aProtocols, ErrorResult& aRv) {
+ if (aProtocols.IsStringSequence()) {
+ return WebSocket::ConstructorCommon(
+ aGlobal, aUrl, aProtocols.GetAsStringSequence(), nullptr, ""_ns, aRv);
+ }
+
+ Sequence<nsString> protocols;
+ if (!protocols.AppendElement(aProtocols.GetAsString(), fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ return WebSocket::ConstructorCommon(aGlobal, aUrl, protocols, nullptr, ""_ns,
+ aRv);
+}
+
+already_AddRefed<WebSocket> WebSocket::CreateServerWebSocket(
+ const GlobalObject& aGlobal, const nsAString& aUrl,
+ const Sequence<nsString>& aProtocols,
+ nsITransportProvider* aTransportProvider,
+ const nsAString& aNegotiatedExtensions, ErrorResult& aRv) {
+ return WebSocket::ConstructorCommon(
+ aGlobal, aUrl, aProtocols, aTransportProvider,
+ NS_ConvertUTF16toUTF8(aNegotiatedExtensions), aRv);
+}
+
+namespace {
+
+// This class is used to clear any exception.
+class MOZ_STACK_CLASS ClearException {
+ public:
+ explicit ClearException(JSContext* aCx) : mCx(aCx) {}
+
+ ~ClearException() { JS_ClearPendingException(mCx); }
+
+ private:
+ JSContext* mCx;
+};
+
+class WebSocketMainThreadRunnable : public WorkerMainThreadRunnable {
+ public:
+ WebSocketMainThreadRunnable(WorkerPrivate* aWorkerPrivate,
+ const nsACString& aTelemetryKey)
+ : WorkerMainThreadRunnable(aWorkerPrivate, aTelemetryKey) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ }
+
+ bool MainThreadRun() override {
+ AssertIsOnMainThread();
+
+ // Walk up to our containing page
+ WorkerPrivate* wp = mWorkerPrivate;
+ while (wp->GetParent()) {
+ wp = wp->GetParent();
+ }
+
+ nsPIDOMWindowInner* window = wp->GetWindow();
+ if (window) {
+ return InitWithWindow(window);
+ }
+
+ return InitWindowless(wp);
+ }
+
+ protected:
+ virtual bool InitWithWindow(nsPIDOMWindowInner* aWindow) = 0;
+
+ virtual bool InitWindowless(WorkerPrivate* aTopLevelWorkerPrivate) = 0;
+};
+
+class InitRunnable final : public WebSocketMainThreadRunnable {
+ public:
+ InitRunnable(WorkerPrivate* aWorkerPrivate, WebSocketImpl* aImpl,
+ const Maybe<mozilla::dom::ClientInfo>& aClientInfo,
+ bool aIsServerSide, const nsAString& aURL,
+ nsTArray<nsString>& 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<ClientInfo> mClientInfo;
+ bool mIsServerSide;
+ const nsAString& mURL;
+ nsTArray<nsString>& mProtocolArray;
+ nsCString mScriptFile;
+ uint32_t mScriptLine;
+ uint32_t mScriptColumn;
+ nsresult mErrorCode;
+};
+
+class ConnectRunnable final : public WebSocketMainThreadRunnable {
+ public:
+ ConnectRunnable(WorkerPrivate* aWorkerPrivate, WebSocketImpl* aImpl)
+ : WebSocketMainThreadRunnable(aWorkerPrivate, "WebSocket :: init"_ns),
+ mImpl(aImpl),
+ mConnectionFailed(true) {
+ MOZ_ASSERT(mWorkerPrivate);
+ mWorkerPrivate->AssertIsOnWorkerThread();
+ }
+
+ bool ConnectionFailed() const { return mConnectionFailed; }
+
+ protected:
+ virtual bool InitWithWindow(nsPIDOMWindowInner* aWindow) override {
+ Document* doc = aWindow->GetExtantDoc();
+ if (!doc) {
+ return true;
+ }
+
+ mConnectionFailed = NS_FAILED(mImpl->InitializeConnection(
+ doc->NodePrincipal(), mWorkerPrivate->CookieJarSettings()));
+ return true;
+ }
+
+ virtual bool InitWindowless(WorkerPrivate* aTopLevelWorkerPrivate) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aTopLevelWorkerPrivate && !aTopLevelWorkerPrivate->GetWindow());
+
+ mConnectionFailed = NS_FAILED(
+ mImpl->InitializeConnection(aTopLevelWorkerPrivate->GetPrincipal(),
+ mWorkerPrivate->CookieJarSettings()));
+ return true;
+ }
+
+ // Raw pointer. This worker runnable runs synchronously.
+ WebSocketImpl* mImpl;
+
+ bool mConnectionFailed;
+};
+
+class AsyncOpenRunnable final : public WebSocketMainThreadRunnable {
+ public:
+ explicit AsyncOpenRunnable(WebSocketImpl* aImpl,
+ UniquePtr<SerializedStackHolder> aOriginStack)
+ : WebSocketMainThreadRunnable(aImpl->mWorkerRef->Private(),
+ "WebSocket :: AsyncOpen"_ns),
+ mImpl(aImpl),
+ mOriginStack(std::move(aOriginStack)),
+ mErrorCode(NS_OK) {
+ MOZ_ASSERT(mWorkerPrivate);
+ mWorkerPrivate->AssertIsOnWorkerThread();
+ }
+
+ nsresult ErrorCode() const { return mErrorCode; }
+
+ protected:
+ virtual bool InitWithWindow(nsPIDOMWindowInner* aWindow) override {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aWindow);
+
+ Document* doc = aWindow->GetExtantDoc();
+ if (!doc) {
+ mErrorCode = NS_ERROR_FAILURE;
+ return true;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = doc->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<SerializedStackHolder> 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> WebSocket::ConstructorCommon(
+ const GlobalObject& aGlobal, const nsAString& aUrl,
+ const Sequence<nsString>& aProtocols,
+ nsITransportProvider* aTransportProvider,
+ const nsACString& aNegotiatedExtensions, ErrorResult& aRv) {
+ MOZ_ASSERT_IF(!aTransportProvider, aNegotiatedExtensions.IsEmpty());
+ nsCOMPtr<nsIPrincipal> principal;
+ nsCOMPtr<nsIPrincipal> partitionedPrincipal;
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ if (NS_IsMainThread()) {
+ nsCOMPtr<nsIScriptObjectPrincipal> scriptPrincipal =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!scriptPrincipal) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ principal = scriptPrincipal->GetPrincipal();
+ partitionedPrincipal = scriptPrincipal->PartitionedPrincipal();
+ if (!principal || !partitionedPrincipal) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ }
+
+ nsTArray<nsString> protocolArray;
+
+ for (uint32_t index = 0, len = aProtocols.Length(); index < len; ++index) {
+ const nsString& protocolElement = aProtocols[index];
+
+ // 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> webSocket = new WebSocket(global);
+ RefPtr<WebSocketImpl> webSocketImpl = webSocket->mImpl;
+
+ bool connectionFailed = true;
+
+ if (NS_IsMainThread()) {
+ // We're keeping track of all main thread web sockets to be able to
+ // avoid throttling timeouts when we have active web sockets.
+ 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<Document> doc = webSocket->GetDocumentIfCurrent();
+
+ // the constructor should throw a SYNTAX_ERROR only if it fails to parse the
+ // url parameter, so don't throw if InitializeConnection fails, and call
+ // onerror/onclose asynchronously
+ connectionFailed = NS_FAILED(webSocketImpl->InitializeConnection(
+ principal, doc ? doc->CookieJarSettings() : nullptr));
+ } else {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ unsigned lineno, column;
+ JS::AutoFilename file;
+ if (!JS::DescribeScriptedCaller(aGlobal.Context(), &file, &lineno,
+ &column)) {
+ NS_WARNING("Failed to get line number and filename in workers.");
+ }
+
+ RefPtr<InitRunnable> runnable = new InitRunnable(
+ workerPrivate, webSocketImpl,
+ 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> 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<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(global);
+
+ UniquePtr<SerializedStackHolder> 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<SerializedStackHolder> stack;
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ if (workerPrivate->IsWatchedByDevTools()) {
+ stack = GetCurrentStackForNetMonitor(aGlobal.Context());
+ }
+
+ RefPtr<AsyncOpenRunnable> runnable =
+ new AsyncOpenRunnable(webSocket->mImpl, std::move(stack));
+ runnable->Dispatch(Canceling, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ aRv = runnable->ErrorCode();
+ }
+
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // It can be that we have been already disconnected because the WebSocket is
+ // gone away while we where initializing the webSocket.
+ if (!webSocket->mImpl) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Let's inform devtools about this new active WebSocket.
+ webSocket->mImpl->mService->WebSocketCreated(
+ webSocket->mImpl->mChannel->Serial(), webSocket->mImpl->mInnerWindowID,
+ webSocket->mURI, webSocket->mImpl->mRequestedProtocolList);
+ cws.Done();
+
+ return webSocket.forget();
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(WebSocket)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebSocket,
+ DOMEventTargetHelper)
+ if (tmp->mImpl) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImpl->mChannel)
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WebSocket, DOMEventTargetHelper)
+ if (tmp->mImpl) {
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mImpl->mChannel)
+ tmp->mImpl->Disconnect();
+ MOZ_ASSERT(!tmp->mImpl);
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+bool WebSocket::IsCertainlyAliveForCC() const { return mKeepingAlive; }
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebSocket)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(WebSocket, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(WebSocket, DOMEventTargetHelper)
+
+void WebSocket::DisconnectFromOwner() {
+ // If we haven't called WebSocketImpl::Disconnect yet, update web
+ // socket count here.
+ if (NS_IsMainThread() && mImpl && !mImpl->mDisconnectingOrDisconnected &&
+ GetOwner()) {
+ GetOwner()->UpdateWebSocketCount(-1);
+ }
+
+ DOMEventTargetHelper::DisconnectFromOwner();
+
+ if (mImpl) {
+ mImpl->CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY);
+ }
+
+ DontKeepAliveAnyMore();
+}
+
+//-----------------------------------------------------------------------------
+// WebSocketImpl:: initialization
+//-----------------------------------------------------------------------------
+
+nsresult WebSocketImpl::Init(JSContext* aCx, bool aIsSecure,
+ nsIPrincipal* aPrincipal,
+ const Maybe<ClientInfo>& aClientInfo,
+ nsICSPEventListener* aCSPEventListener,
+ bool aIsServerSide, const nsAString& aURL,
+ nsTArray<nsString>& aProtocolArray,
+ const nsACString& aScriptFile,
+ uint32_t aScriptLine, uint32_t aScriptColumn) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aPrincipal);
+
+ mService = WebSocketEventService::GetOrCreate();
+
+ // We need to keep the implementation alive in case the init disconnects it
+ // because of some error.
+ RefPtr<WebSocketImpl> kungfuDeathGrip = this;
+
+ // Attempt to kill "ghost" websocket: but usually too early for check to fail
+ nsresult rv = mWebSocket->CheckCurrentGlobalCorrectness();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Shut down websocket if window is frozen or destroyed (only needed for
+ // "ghost" websockets--see bug 696085)
+ RefPtr<WebSocketImplProxy> proxy;
+ if (mIsMainThread) {
+ nsCOMPtr<nsIObserverService> 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<Document> originDoc = mWebSocket->GetDocumentIfCurrent();
+ if (!originDoc) {
+ rv = mWebSocket->CheckCurrentGlobalCorrectness();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ mOriginDocument = do_GetWeakReference(originDoc);
+
+ if (!mIsServerSide) {
+ nsCOMPtr<nsIURI> uri;
+ {
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), mURI);
+
+ // We crash here because we are sure that mURI is a valid URI, so either
+ // we are OOM'ing or something else bad is happening.
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_CRASH();
+ }
+ }
+
+ // The 'real' nsHttpChannel of the websocket gets opened in the parent.
+ // Since we don't serialize the CSP within child and parent and also not
+ // the context, we have to perform content policy checks here instead of
+ // AsyncOpen().
+ // Please note that websockets can't follow redirects, hence there is no
+ // need to perform a CSP check after redirects.
+ nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo(
+ aPrincipal, // loading principal
+ aPrincipal, // triggering principal
+ originDoc, nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
+ nsIContentPolicy::TYPE_WEBSOCKET, 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<nsIURI> 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<nsString, 2> params;
+ CopyUTF8toUTF16(mURI, *params.AppendElement());
+
+ // upgrade the request from ws:// to wss:// and mark as secure
+ mURI.ReplaceSubstring("ws://", "wss://");
+ if (NS_WARN_IF(mURI.Find("wss://") != 0)) {
+ return NS_OK;
+ }
+ mSecure = true;
+
+ params.AppendElement(u"wss"_ns);
+ CSP_LogLocalizedStr("upgradeInsecureRequest", params,
+ u""_ns, // aSourceFile
+ u""_ns, // aScriptSample
+ 0, // aLineNumber
+ 0, // aColumnNumber
+ nsIScriptError::warningFlag,
+ "upgradeInsecureRequest"_ns, mInnerWindowID,
+ mPrivateBrowsing);
+ }
+
+ // Don't allow https:// to open ws://
+ if (!mIsServerSide && !mSecure &&
+ !Preferences::GetBool("network.websocket.allowInsecureFromHTTPS",
+ false) &&
+ !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost(
+ mAsciiHost)) {
+ if (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<SerializedStackHolder> aOriginStack) {
+ MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
+ MOZ_ASSERT_IF(!aTransportProvider, aNegotiatedExtensions.IsEmpty());
+
+ nsCString asciiOrigin;
+ nsresult rv = aPrincipal->GetAsciiOrigin(asciiOrigin);
+ if (NS_FAILED(rv)) {
+ asciiOrigin.AssignLiteral("null");
+ }
+
+ if (aTransportProvider) {
+ rv = mChannel->SetServerParameters(aTransportProvider,
+ aNegotiatedExtensions);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ ToLowerCase(asciiOrigin);
+
+ nsCOMPtr<nsIURI> uri;
+ if (!aTransportProvider) {
+ rv = NS_NewURI(getter_AddRefs(uri), mURI);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ rv = mChannel->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<WebSocketImpl> mWebSocketImpl;
+};
+
+nsresult WebSocketImpl::InitializeConnection(
+ nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(!mChannel, "mChannel should be null");
+
+ nsCOMPtr<nsIWebSocketChannel> wsChannel;
+ nsAutoCloseWS autoClose(this);
+ nsresult rv;
+
+ if (mSecure) {
+ wsChannel =
+ do_CreateInstance("@mozilla.org/network/protocol;1?name=wss", &rv);
+ } else {
+ wsChannel =
+ do_CreateInstance("@mozilla.org/network/protocol;1?name=ws", &rv);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // add ourselves to the document's load group and
+ // provide the http stack the loadgroup info too
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ rv = GetLoadGroup(getter_AddRefs(loadGroup));
+ if (loadGroup) {
+ rv = wsChannel->SetLoadGroup(loadGroup);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = loadGroup->AddRequest(this, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mWeakLoadGroup = do_GetWeakReference(loadGroup);
+ }
+
+ // manually adding loadinfo to the channel since it
+ // was not set during channel creation.
+ nsCOMPtr<Document> doc = do_QueryReferent(mOriginDocument);
+
+ // mOriginDocument has to be release on main-thread because WeakReferences
+ // are not thread-safe.
+ mOriginDocument = nullptr;
+
+ // The TriggeringPrincipal for websockets must always be a script.
+ // Let's make sure that the doc's principal (if a doc exists)
+ // and aPrincipal are same origin.
+ MOZ_ASSERT(!doc || doc->NodePrincipal()->Equals(aPrincipal));
+
+ rv = wsChannel->InitLoadInfoNative(
+ doc, doc ? doc->NodePrincipal() : aPrincipal, aPrincipal,
+ aCookieJarSettings,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_WEBSOCKET, 0);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ if (!mRequestedProtocolList.IsEmpty()) {
+ rv = wsChannel->SetProtocol(mRequestedProtocolList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(wsChannel);
+ NS_ENSURE_TRUE(rr, NS_ERROR_FAILURE);
+
+ rv = rr->RetargetDeliveryTo(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mChannel = wsChannel;
+
+ if (mIsMainThread) {
+ 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> webSocket = mWebSocket;
+
+ // Call 'onerror' if needed
+ if (mFailed) {
+ nsresult rv = webSocket->CreateAndDispatchSimpleEvent(ERROR_EVENT_STRING);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the error event");
+ }
+ }
+
+ nsresult rv = webSocket->CreateAndDispatchCloseEvent(
+ mCloseEventWasClean, mCloseEventCode, mCloseEventReason);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the close event");
+ }
+
+ webSocket->UpdateMustKeepAlive();
+ Disconnect();
+}
+
+nsresult WebSocket::CreateAndDispatchSimpleEvent(const nsAString& aName) {
+ MOZ_ASSERT(mImpl);
+ AssertIsOnTargetThread();
+
+ nsresult rv = CheckCurrentGlobalCorrectness();
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+
+ RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
+
+ // it doesn't bubble, and it isn't cancelable
+ event->InitEvent(aName, false, false);
+ event->SetTrusted(true);
+
+ ErrorResult err;
+ DispatchEvent(*event, err);
+ return err.StealNSResult();
+}
+
+nsresult WebSocket::CreateAndDispatchMessageEvent(const nsACString& aData,
+ bool aIsBinary) {
+ MOZ_ASSERT(mImpl);
+ AssertIsOnTargetThread();
+
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(GetOwnerGlobal()))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JSContext* cx = jsapi.cx();
+
+ nsresult rv = CheckCurrentGlobalCorrectness();
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+
+ uint16_t messageType = nsIWebSocketEventListener::TYPE_STRING;
+
+ // Create appropriate JS object for message
+ JS::Rooted<JS::Value> jsData(cx);
+ if (aIsBinary) {
+ if (mBinaryType == dom::BinaryType::Blob) {
+ messageType = nsIWebSocketEventListener::TYPE_BLOB;
+
+ RefPtr<Blob> blob =
+ Blob::CreateStringBlob(GetOwnerGlobal(), aData, u""_ns);
+ if (NS_WARN_IF(!blob)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!ToJSValue(cx, blob, &jsData)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ } else if (mBinaryType == dom::BinaryType::Arraybuffer) {
+ messageType = nsIWebSocketEventListener::TYPE_ARRAYBUFFER;
+
+ JS::Rooted<JSObject*> arrayBuf(cx);
+ nsresult rv =
+ nsContentUtils::CreateArrayBuffer(cx, aData, arrayBuf.address());
+ NS_ENSURE_SUCCESS(rv, rv);
+ jsData.setObject(*arrayBuf);
+ } else {
+ MOZ_CRASH("Unknown binary type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+ } else {
+ // JS string
+ nsAutoString utf16Data;
+ if (!AppendUTF8toUTF16(aData, utf16Data, mozilla::fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ JSString* jsString;
+ jsString = JS_NewUCStringCopyN(cx, utf16Data.get(), utf16Data.Length());
+ NS_ENSURE_TRUE(jsString, NS_ERROR_FAILURE);
+
+ jsData.setString(jsString);
+ }
+
+ mImpl->mService->WebSocketMessageAvailable(
+ mImpl->mChannel->Serial(), mImpl->mInnerWindowID, aData, messageType);
+
+ // create an event that uses the MessageEvent interface,
+ // which does not bubble, is not cancelable, and has no default action
+
+ RefPtr<MessageEvent> event = new MessageEvent(this, nullptr, nullptr);
+
+ event->InitMessageEvent(nullptr, MESSAGE_EVENT_STRING, CanBubble::eNo,
+ Cancelable::eNo, jsData, mImpl->mUTF16Origin, u""_ns,
+ nullptr, Sequence<OwningNonNull<MessagePort>>());
+ event->SetTrusted(true);
+
+ ErrorResult err;
+ DispatchEvent(*event, err);
+ return err.StealNSResult();
+}
+
+nsresult WebSocket::CreateAndDispatchCloseEvent(bool aWasClean, uint16_t aCode,
+ const nsAString& aReason) {
+ AssertIsOnTargetThread();
+
+ // This method is called by a runnable and it can happen that, in the
+ // meantime, GC unlinked this object, so mImpl could be null.
+ if (mImpl && mImpl->mChannel) {
+ mImpl->mService->WebSocketClosed(mImpl->mChannel->Serial(),
+ mImpl->mInnerWindowID, aWasClean, aCode,
+ aReason);
+ }
+
+ nsresult rv = CheckCurrentGlobalCorrectness();
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+
+ CloseEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ init.mWasClean = aWasClean;
+ init.mCode = aCode;
+ init.mReason = aReason;
+
+ RefPtr<CloseEvent> event =
+ CloseEvent::Constructor(this, CLOSE_EVENT_STRING, init);
+ event->SetTrusted(true);
+
+ ErrorResult err;
+ DispatchEvent(*event, err);
+ return err.StealNSResult();
+}
+
+nsresult WebSocketImpl::ParseURL(const nsAString& aURL) {
+ AssertIsOnMainThread();
+ NS_ENSURE_TRUE(!aURL.IsEmpty(), NS_ERROR_DOM_SYNTAX_ERR);
+
+ if (mIsServerSide) {
+ mWebSocket->mURI = aURL;
+ CopyUTF16toUTF8(mWebSocket->mURI, mURI);
+
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR);
+
+ nsCOMPtr<nsIURL> parsedURL = do_QueryInterface(uri, &rv);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR);
+
+ bool hasRef;
+ rv = parsedURL->GetHasRef(&hasRef);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && !hasRef, NS_ERROR_DOM_SYNTAX_ERR);
+
+ nsAutoCString scheme;
+ rv = parsedURL->GetScheme(scheme);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && !scheme.IsEmpty(),
+ NS_ERROR_DOM_SYNTAX_ERR);
+
+ nsAutoCString host;
+ rv = parsedURL->GetAsciiHost(host);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && !host.IsEmpty(), NS_ERROR_DOM_SYNTAX_ERR);
+
+ int32_t port;
+ rv = parsedURL->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR);
+
+ nsAutoCString filePath;
+ rv = parsedURL->GetFilePath(filePath);
+ if (filePath.IsEmpty()) {
+ filePath.Assign('/');
+ }
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR);
+
+ nsAutoCString query;
+ rv = parsedURL->GetQuery(query);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR);
+
+ if (scheme.LowerCaseEqualsLiteral("ws")) {
+ mSecure = false;
+ mPort = (port == -1) ? DEFAULT_WS_SCHEME_PORT : port;
+ } else if (scheme.LowerCaseEqualsLiteral("wss")) {
+ mSecure = true;
+ mPort = (port == -1) ? DEFAULT_WSS_SCHEME_PORT : port;
+ } else {
+ return NS_ERROR_DOM_SYNTAX_ERR;
+ }
+
+ rv = nsContentUtils::GetUTFOrigin(parsedURL, mUTF16Origin);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR);
+
+ mAsciiHost = host;
+ ToLowerCase(mAsciiHost);
+
+ mResource = filePath;
+ if (!query.IsEmpty()) {
+ mResource.Append('?');
+ mResource.Append(query);
+ }
+ uint32_t length = mResource.Length();
+ uint32_t i;
+ for (i = 0; i < length; ++i) {
+ if (mResource[i] < static_cast<char16_t>(0x0021) ||
+ mResource[i] > static_cast<char16_t>(0x007E)) {
+ return NS_ERROR_DOM_SYNTAX_ERR;
+ }
+ }
+
+ rv = parsedURL->GetSpec(mURI);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ CopyUTF8toUTF16(mURI, mWebSocket->mURI);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// Methods that keep alive the WebSocket object when:
+// 1. the object has registered event listeners that can be triggered
+// ("strong event listeners");
+// 2. there are outgoing not sent messages.
+//-----------------------------------------------------------------------------
+
+void WebSocket::UpdateMustKeepAlive() {
+ // Here we could not have mImpl.
+ MOZ_ASSERT(NS_IsMainThread() == mIsMainThread);
+
+ if (!mCheckMustKeepAlive || !mImpl) {
+ return;
+ }
+
+ bool shouldKeepAlive = false;
+ uint16_t readyState = ReadyState();
+
+ if (mListenerManager) {
+ switch (readyState) {
+ case CONNECTING: {
+ if (mListenerManager->HasListenersFor(OPEN_EVENT_STRING) ||
+ mListenerManager->HasListenersFor(MESSAGE_EVENT_STRING) ||
+ mListenerManager->HasListenersFor(ERROR_EVENT_STRING) ||
+ mListenerManager->HasListenersFor(CLOSE_EVENT_STRING)) {
+ shouldKeepAlive = true;
+ }
+ } break;
+
+ case OPEN:
+ case CLOSING: {
+ if (mListenerManager->HasListenersFor(MESSAGE_EVENT_STRING) ||
+ mListenerManager->HasListenersFor(ERROR_EVENT_STRING) ||
+ mListenerManager->HasListenersFor(CLOSE_EVENT_STRING) ||
+ mOutgoingBufferedAmount.value() != 0) {
+ shouldKeepAlive = true;
+ }
+ } break;
+
+ case CLOSED: {
+ shouldKeepAlive = false;
+ }
+ }
+ }
+
+ if (mKeepingAlive && !shouldKeepAlive) {
+ mKeepingAlive = false;
+ mImpl->ReleaseObject();
+ } else if (!mKeepingAlive && shouldKeepAlive) {
+ mKeepingAlive = true;
+ mImpl->AddRefObject();
+ }
+}
+
+void WebSocket::DontKeepAliveAnyMore() {
+ // Here we could not have mImpl.
+ MOZ_ASSERT(NS_IsMainThread() == mIsMainThread);
+
+ if (mKeepingAlive) {
+ MOZ_ASSERT(mImpl);
+
+ mKeepingAlive = false;
+ mImpl->ReleaseObject();
+ }
+
+ mCheckMustKeepAlive = false;
+}
+
+void WebSocketImpl::AddRefObject() {
+ AssertIsOnTargetThread();
+ AddRef();
+}
+
+void WebSocketImpl::ReleaseObject() {
+ AssertIsOnTargetThread();
+ Release();
+}
+
+bool WebSocketImpl::RegisterWorkerRef(WorkerPrivate* aWorkerPrivate) {
+ MOZ_ASSERT(aWorkerPrivate);
+
+ RefPtr<WebSocketImpl> self = this;
+
+ // In workers we have to keep the worker alive using a strong reference in
+ // order to dispatch messages correctly.
+ RefPtr<StrongWorkerRef> workerRef =
+ StrongWorkerRef::Create(aWorkerPrivate, "WebSocketImpl", [self]() {
+ {
+ MutexAutoLock lock(self->mMutex);
+ self->mWorkerShuttingDown = true;
+ }
+
+ self->CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY, ""_ns);
+ });
+ if (NS_WARN_IF(!workerRef)) {
+ return false;
+ }
+
+ mWorkerRef = new ThreadSafeWorkerRef(workerRef);
+ MOZ_ASSERT(mWorkerRef);
+
+ return true;
+}
+
+void WebSocketImpl::UnregisterWorkerRef() {
+ MOZ_ASSERT(mDisconnectingOrDisconnected);
+ MOZ_ASSERT(mWorkerRef);
+ mWorkerRef->Private()->AssertIsOnWorkerThread();
+
+ {
+ MutexAutoLock lock(mMutex);
+ mWorkerShuttingDown = true;
+ }
+
+ // The DTOR of this StrongWorkerRef will release the worker for us.
+ mWorkerRef = nullptr;
+}
+
+nsresult WebSocketImpl::UpdateURI() {
+ AssertIsOnTargetThread();
+
+ // Check for Redirections
+ RefPtr<BaseWebSocketChannel> channel;
+ channel = static_cast<BaseWebSocketChannel*>(mChannel.get());
+ MOZ_ASSERT(channel);
+
+ channel->GetEffectiveURL(mWebSocket->mEffectiveURL);
+ mSecure = channel->IsEncrypted();
+
+ return NS_OK;
+}
+
+void WebSocket::EventListenerAdded(nsAtom* aType) {
+ AssertIsOnTargetThread();
+ UpdateMustKeepAlive();
+}
+
+void WebSocket::EventListenerRemoved(nsAtom* aType) {
+ AssertIsOnTargetThread();
+ UpdateMustKeepAlive();
+}
+
+//-----------------------------------------------------------------------------
+// WebSocket - methods
+//-----------------------------------------------------------------------------
+
+// webIDL: readonly attribute unsigned short readyState;
+uint16_t WebSocket::ReadyState() {
+ MutexAutoLock lock(mMutex);
+ return mReadyState;
+}
+
+void WebSocket::SetReadyState(uint16_t aReadyState) {
+ MutexAutoLock lock(mMutex);
+ mReadyState = aReadyState;
+}
+
+// webIDL: readonly attribute unsigned long long bufferedAmount;
+uint64_t WebSocket::BufferedAmount() const {
+ AssertIsOnTargetThread();
+ MOZ_RELEASE_ASSERT(mOutgoingBufferedAmount.isValid());
+ return mOutgoingBufferedAmount.value();
+}
+
+// webIDL: attribute BinaryType binaryType;
+dom::BinaryType WebSocket::BinaryType() const {
+ AssertIsOnTargetThread();
+ return mBinaryType;
+}
+
+// webIDL: attribute BinaryType binaryType;
+void WebSocket::SetBinaryType(dom::BinaryType aData) {
+ AssertIsOnTargetThread();
+ mBinaryType = aData;
+}
+
+// webIDL: readonly attribute DOMString url
+void WebSocket::GetUrl(nsAString& aURL) {
+ AssertIsOnTargetThread();
+
+ if (mEffectiveURL.IsEmpty()) {
+ aURL = mURI;
+ } else {
+ aURL = mEffectiveURL;
+ }
+}
+
+// webIDL: readonly attribute DOMString extensions;
+void WebSocket::GetExtensions(nsAString& aExtensions) {
+ AssertIsOnTargetThread();
+ CopyUTF8toUTF16(mEstablishedExtensions, aExtensions);
+}
+
+// webIDL: readonly attribute DOMString protocol;
+void WebSocket::GetProtocol(nsAString& aProtocol) {
+ AssertIsOnTargetThread();
+ CopyUTF8toUTF16(mEstablishedProtocol, aProtocol);
+}
+
+// webIDL: void send(DOMString data);
+void WebSocket::Send(const nsAString& aData, ErrorResult& aRv) {
+ AssertIsOnTargetThread();
+
+ nsAutoCString msgString;
+ if (!AppendUTF16toUTF8(aData, msgString, mozilla::fallible_t())) {
+ aRv.Throw(NS_ERROR_FILE_TOO_BIG);
+ return;
+ }
+ Send(nullptr, msgString, msgString.Length(), false, aRv);
+}
+
+void WebSocket::Send(Blob& aData, ErrorResult& aRv) {
+ AssertIsOnTargetThread();
+
+ nsCOMPtr<nsIInputStream> msgStream;
+ aData.CreateInputStream(getter_AddRefs(msgStream), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ uint64_t msgLength = aData.GetSize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ if (msgLength > UINT32_MAX) {
+ aRv.Throw(NS_ERROR_FILE_TOO_BIG);
+ return;
+ }
+
+ Send(msgStream, ""_ns, msgLength, true, aRv);
+}
+
+void WebSocket::Send(const ArrayBuffer& aData, ErrorResult& aRv) {
+ AssertIsOnTargetThread();
+
+ aData.ComputeState();
+
+ static_assert(sizeof(*aData.Data()) == 1, "byte-sized data required");
+
+ uint32_t len = aData.Length();
+ char* data = reinterpret_cast<char*>(aData.Data());
+
+ nsDependentCSubstring msgString;
+ if (!msgString.Assign(data, len, mozilla::fallible_t())) {
+ aRv.Throw(NS_ERROR_FILE_TOO_BIG);
+ return;
+ }
+ Send(nullptr, msgString, len, true, aRv);
+}
+
+void WebSocket::Send(const ArrayBufferView& aData, ErrorResult& aRv) {
+ AssertIsOnTargetThread();
+
+ aData.ComputeState();
+
+ static_assert(sizeof(*aData.Data()) == 1, "byte-sized data required");
+
+ uint32_t len = aData.Length();
+ char* data = reinterpret_cast<char*>(aData.Data());
+
+ nsDependentCSubstring msgString;
+ if (!msgString.Assign(data, len, mozilla::fallible_t())) {
+ aRv.Throw(NS_ERROR_FILE_TOO_BIG);
+ return;
+ }
+ Send(nullptr, msgString, len, true, aRv);
+}
+
+void WebSocket::Send(nsIInputStream* aMsgStream, const nsACString& aMsgString,
+ uint32_t aMsgLength, bool aIsBinary, ErrorResult& aRv) {
+ AssertIsOnTargetThread();
+
+ int64_t readyState = ReadyState();
+ if (readyState == CONNECTING) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ CheckedUint64 outgoingBufferedAmount = mOutgoingBufferedAmount;
+ outgoingBufferedAmount += aMsgLength;
+ if (!outgoingBufferedAmount.isValid()) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ // Always increment outgoing buffer len, even if closed
+ mOutgoingBufferedAmount = outgoingBufferedAmount;
+ MOZ_RELEASE_ASSERT(mOutgoingBufferedAmount.isValid());
+
+ if (readyState == CLOSING || readyState == CLOSED) {
+ return;
+ }
+
+ // We must have mImpl when connected.
+ MOZ_ASSERT(mImpl);
+ MOZ_ASSERT(readyState == OPEN, "Unknown state in WebSocket::Send");
+
+ nsresult rv;
+ if (aMsgStream) {
+ rv = mImpl->mChannel->SendBinaryStream(aMsgStream, aMsgLength);
+ } else {
+ if (aIsBinary) {
+ rv = mImpl->mChannel->SendBinaryMsg(aMsgString);
+ } else {
+ rv = mImpl->mChannel->SendMsg(aMsgString);
+ }
+ }
+
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return;
+ }
+
+ UpdateMustKeepAlive();
+}
+
+// webIDL: void close(optional unsigned short code, optional DOMString reason):
+void WebSocket::Close(const Optional<uint16_t>& aCode,
+ const Optional<nsAString>& aReason, ErrorResult& aRv) {
+ AssertIsOnTargetThread();
+
+ // the reason code is optional, but if provided it must be in a specific range
+ uint16_t closeCode = 0;
+ if (aCode.WasPassed()) {
+ if (aCode.Value() != 1000 &&
+ (aCode.Value() < 3000 || aCode.Value() > 4999)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+ return;
+ }
+ closeCode = aCode.Value();
+ }
+
+ nsCString closeReason;
+ if (aReason.WasPassed()) {
+ CopyUTF16toUTF8(aReason.Value(), closeReason);
+
+ // The API requires the UTF-8 string to be 123 or less bytes
+ if (closeReason.Length() > 123) {
+ aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+ return;
+ }
+ }
+
+ int64_t readyState = ReadyState();
+ if (readyState == CLOSING || readyState == CLOSED) {
+ return;
+ }
+
+ // If we don't have mImpl, we are in a shutting down worker where we are still
+ // in CONNECTING state, but already disconnected internally.
+ if (!mImpl) {
+ MOZ_ASSERT(readyState == CONNECTING);
+ SetReadyState(CLOSING);
+ return;
+ }
+
+ RefPtr<WebSocketImpl> 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<nsPIDOMWindowInner> window = do_QueryInterface(aSubject);
+ if (!mWebSocket->GetOwner() || window != mWebSocket->GetOwner()) {
+ return NS_OK;
+ }
+
+ if ((strcmp(aTopic, DOM_WINDOW_FROZEN_TOPIC) == 0) ||
+ (strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) == 0)) {
+ CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY);
+ }
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// WebSocketImpl::nsIRequest
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+WebSocketImpl::GetName(nsACString& aName) {
+ AssertIsOnMainThread();
+
+ CopyUTF16toUTF8(mWebSocket->mURI, aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::IsPending(bool* aValue) {
+ AssertIsOnTargetThread();
+
+ int64_t readyState = mWebSocket->ReadyState();
+ *aValue = (readyState != WebSocket::CLOSED);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::GetStatus(nsresult* aStatus) {
+ AssertIsOnTargetThread();
+
+ *aStatus = NS_OK;
+ return NS_OK;
+}
+
+namespace {
+
+class CancelRunnable final : public MainThreadWorkerRunnable {
+ public:
+ CancelRunnable(ThreadSafeWorkerRef* aWorkerRef, WebSocketImpl* aImpl)
+ : MainThreadWorkerRunnable(aWorkerRef->Private()), mImpl(aImpl) {}
+
+ bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ return !NS_FAILED(mImpl->CancelInternal());
+ }
+
+ private:
+ RefPtr<WebSocketImpl> mImpl;
+};
+
+} // namespace
+
+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<CancelRunnable> runnable = new CancelRunnable(mWorkerRef, this);
+ if (!runnable->Dispatch()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+ }
+
+ return CancelInternal();
+}
+
+nsresult WebSocketImpl::CancelInternal() {
+ AssertIsOnTargetThread();
+
+ // If CancelInternal is called by a runnable, we may already be disconnected
+ // by the time it runs.
+ if (mDisconnectingOrDisconnected) {
+ return NS_OK;
+ }
+
+ int64_t readyState = mWebSocket->ReadyState();
+ if (readyState == WebSocket::CLOSING || readyState == WebSocket::CLOSED) {
+ return NS_OK;
+ }
+
+ return CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY);
+}
+
+NS_IMETHODIMP
+WebSocketImpl::Suspend() {
+ AssertIsOnMainThread();
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::Resume() {
+ AssertIsOnMainThread();
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ AssertIsOnMainThread();
+
+ *aLoadGroup = nullptr;
+
+ if (mIsMainThread) {
+ nsCOMPtr<Document> doc = mWebSocket->GetDocumentIfCurrent();
+ if (doc) {
+ *aLoadGroup = doc->GetDocumentLoadGroup().take();
+ }
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mWorkerRef);
+
+ // Walk up to our containing page
+ WorkerPrivate* wp = mWorkerRef->Private();
+ while (wp->GetParent()) {
+ wp = wp->GetParent();
+ }
+
+ nsPIDOMWindowInner* window = wp->GetWindow();
+ if (!window) {
+ return NS_OK;
+ }
+
+ Document* doc = window->GetExtantDoc();
+ if (doc) {
+ *aLoadGroup = doc->GetDocumentLoadGroup().take();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::SetLoadGroup(nsILoadGroup* aLoadGroup) {
+ AssertIsOnMainThread();
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ AssertIsOnMainThread();
+
+ *aLoadFlags = nsIRequest::LOAD_BACKGROUND;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::SetLoadFlags(nsLoadFlags aLoadFlags) {
+ AssertIsOnMainThread();
+
+ // we won't change the load flags at all.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return GetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP
+WebSocketImpl::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return SetTRRModeImpl(aTRRMode);
+}
+
+namespace {
+
+class WorkerRunnableDispatcher final : public WorkerRunnable {
+ RefPtr<WebSocketImpl> mWebSocketImpl;
+
+ public:
+ WorkerRunnableDispatcher(WebSocketImpl* aImpl,
+ ThreadSafeWorkerRef* aWorkerRef,
+ already_AddRefed<nsIRunnable> aEvent)
+ : WorkerRunnable(aWorkerRef->Private(), WorkerThreadUnchangedBusyCount),
+ mWebSocketImpl(aImpl),
+ mEvent(std::move(aEvent)) {}
+
+ bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ aWorkerPrivate->AssertIsOnWorkerThread();
+
+ // No messages when disconnected.
+ if (mWebSocketImpl->mDisconnectingOrDisconnected) {
+ NS_WARNING("Dispatching a WebSocket event after the disconnection!");
+ return true;
+ }
+
+ return !NS_FAILED(mEvent->Run());
+ }
+
+ void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
+ bool aRunResult) override {}
+
+ bool PreDispatch(WorkerPrivate* aWorkerPrivate) override {
+ // We don't call WorkerRunnable::PreDispatch because it would assert the
+ // wrong thing about which thread we're on. We're on whichever thread the
+ // channel implementation is running on (probably the main thread or socket
+ // transport thread).
+ return true;
+ }
+
+ void PostDispatch(WorkerPrivate* aWorkerPrivate,
+ bool aDispatchResult) override {
+ // We don't call WorkerRunnable::PreDispatch because it would assert the
+ // wrong thing about which thread we're on. We're on whichever thread the
+ // channel implementation is running on (probably the main thread or socket
+ // transport thread).
+ }
+
+ private:
+ nsCOMPtr<nsIRunnable> mEvent;
+};
+
+} // namespace
+
+NS_IMETHODIMP
+WebSocketImpl::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) {
+ nsCOMPtr<nsIRunnable> event(aEvent);
+ return Dispatch(event.forget(), aFlags);
+}
+
+NS_IMETHODIMP
+WebSocketImpl::Dispatch(already_AddRefed<nsIRunnable> aEvent, uint32_t aFlags) {
+ nsCOMPtr<nsIRunnable> event_ref(aEvent);
+ // If the target is the main-thread, we should try to dispatch the runnable
+ // to a labeled event target.
+ if (mIsMainThread) {
+ return mMainThreadEventTarget
+ ? mMainThreadEventTarget->Dispatch(event_ref.forget())
+ : 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<WorkerRunnableDispatcher> event =
+ new WorkerRunnableDispatcher(this, mWorkerRef, event_ref.forget());
+
+ if (!event->Dispatch()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+WebSocketImpl::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<nsIGlobalObject> globalObject(GetEntryGlobal());
+ nsCOMPtr<nsIPrincipal> principal;
+
+ if (globalObject) {
+ principal = globalObject->PrincipalOrNull();
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> innerWindow = do_QueryInterface(globalObject);
+ if (!innerWindow) {
+ // If we are in a XPConnect sandbox or in a JS component,
+ // innerWindow will be null. There is nothing on top of this to be
+ // considered.
+ if (NS_WARN_IF(!principal)) {
+ return NS_OK;
+ }
+ *aValue = principal->SchemeIs("https");
+ return NS_OK;
+ }
+
+ RefPtr<WindowContext> 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<EventCallbackDebuggerNotificationType>
+ GetDebuggerNotificationType() const override;
+
+ // nsWrapperCache
+ virtual JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ public: // static helpers:
+ // Determine if preferences allow WebSocket
+ static bool PrefEnabled(JSContext* aCx = nullptr,
+ JSObject* aGlobal = nullptr);
+
+ public: // WebIDL interface:
+ // Constructor:
+ static already_AddRefed<WebSocket> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aUrl,
+ const StringOrStringSequence& aProtocols, ErrorResult& rv);
+
+ static already_AddRefed<WebSocket> CreateServerWebSocket(
+ const GlobalObject& aGlobal, const nsAString& aUrl,
+ const Sequence<nsString>& aProtocols,
+ nsITransportProvider* aTransportProvider,
+ const nsAString& aNegotiatedExtensions, ErrorResult& rv);
+
+ static already_AddRefed<WebSocket> ConstructorCommon(
+ const GlobalObject& aGlobal, const nsAString& aUrl,
+ const Sequence<nsString>& aProtocols,
+ nsITransportProvider* aTransportProvider,
+ const nsACString& aNegotiatedExtensions, ErrorResult& rv);
+
+ // webIDL: readonly attribute DOMString url
+ void GetUrl(nsAString& aResult);
+
+ // webIDL: readonly attribute unsigned short readyState;
+ uint16_t ReadyState();
+
+ // webIDL: readonly attribute unsigned long long bufferedAmount;
+ uint64_t BufferedAmount() const;
+
+ // webIDL: attribute Function? onopen;
+ IMPL_EVENT_HANDLER(open)
+
+ // webIDL: attribute Function? onerror;
+ IMPL_EVENT_HANDLER(error)
+
+ // webIDL: attribute Function? onclose;
+ IMPL_EVENT_HANDLER(close)
+
+ // webIDL: readonly attribute DOMString extensions;
+ void GetExtensions(nsAString& aResult);
+
+ // webIDL: readonly attribute DOMString protocol;
+ void GetProtocol(nsAString& aResult);
+
+ // webIDL: void close(optional unsigned short code,
+ // optional DOMString reason):
+ void Close(const Optional<uint16_t>& aCode,
+ const Optional<nsAString>& aReason, ErrorResult& aRv);
+
+ // webIDL: attribute Function? onmessage;
+ IMPL_EVENT_HANDLER(message)
+
+ // webIDL: attribute DOMString binaryType;
+ dom::BinaryType BinaryType() const;
+ void SetBinaryType(dom::BinaryType aData);
+
+ // webIDL: void send(DOMString|Blob|ArrayBufferView data);
+ void Send(const nsAString& aData, ErrorResult& aRv);
+ void Send(Blob& aData, ErrorResult& aRv);
+ void Send(const ArrayBuffer& aData, ErrorResult& aRv);
+ void Send(const ArrayBufferView& aData, ErrorResult& aRv);
+
+ private: // constructor && destructor
+ explicit WebSocket(nsIGlobalObject* aGlobal);
+ virtual ~WebSocket();
+
+ void SetReadyState(uint16_t aReadyState);
+
+ // These methods actually do the dispatch for various events.
+ nsresult CreateAndDispatchSimpleEvent(const nsAString& aName);
+ nsresult CreateAndDispatchMessageEvent(const nsACString& aData,
+ bool aIsBinary);
+ nsresult CreateAndDispatchCloseEvent(bool aWasClean, uint16_t aCode,
+ const nsAString& aReason);
+
+ 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 @@
+<!DOCTYPE html>
+<html>
+<script>
+ onload = function() {
+ function done(success) {
+ var bc = new BroadcastChannel("test_channel");
+ bc.postMessage({success});
+ bc.close();
+ }
+ try {
+ new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic");
+ done(true); // no hang!
+ } catch (e) {
+ done(false);
+ }
+ }
+</script>
+</html>
diff --git a/dom/websocket/tests/file_websocket_basic_wsh.py b/dom/websocket/tests/file_websocket_basic_wsh.py
new file mode 100644
index 0000000000..050e7ccc10
--- /dev/null
+++ b/dom/websocket/tests/file_websocket_basic_wsh.py
@@ -0,0 +1,31 @@
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ # must set request.ws_protocol to the selected version from ws_requested_protocols
+ request.ws_protocol = request.ws_requested_protocols[0]
+
+ if request.ws_protocol == "error":
+ raise ValueError("Error")
+ pass
+
+
+def web_socket_transfer_data(request):
+ while True:
+ line = msgutil.receive_message(request)
+ if line == "protocol":
+ msgutil.send_message(request, request.ws_protocol)
+ continue
+
+ if line == "resource":
+ msgutil.send_message(request, request.ws_resource)
+ continue
+
+ if line == "origin":
+ msgutil.send_message(request, request.ws_origin)
+ continue
+
+ msgutil.send_message(request, line)
+
+ if line == "end":
+ return
diff --git a/dom/websocket/tests/file_websocket_bigBlob_wsh.py b/dom/websocket/tests/file_websocket_bigBlob_wsh.py
new file mode 100644
index 0000000000..5ccc85f3a0
--- /dev/null
+++ b/dom/websocket/tests/file_websocket_bigBlob_wsh.py
@@ -0,0 +1,11 @@
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ while True:
+ line = msgutil.receive_message(request)
+ msgutil.send_message(request, line, True, True)
diff --git a/dom/websocket/tests/file_websocket_hello_wsh.py b/dom/websocket/tests/file_websocket_hello_wsh.py
new file mode 100644
index 0000000000..5711d2283c
--- /dev/null
+++ b/dom/websocket/tests/file_websocket_hello_wsh.py
@@ -0,0 +1,12 @@
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ resp = "Test"
+ if msgutil.receive_message(request) == "data":
+ resp = "Hello world!"
+ msgutil.send_message(request, resp)
diff --git a/dom/websocket/tests/file_websocket_http_resource.txt b/dom/websocket/tests/file_websocket_http_resource.txt
new file mode 100644
index 0000000000..35dc67f08d
--- /dev/null
+++ b/dom/websocket/tests/file_websocket_http_resource.txt
@@ -0,0 +1 @@
+server data
diff --git a/dom/websocket/tests/file_websocket_permessage_deflate_disabled_wsh.py b/dom/websocket/tests/file_websocket_permessage_deflate_disabled_wsh.py
new file mode 100644
index 0000000000..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 @@
+<html>
+<body>
+ <form action="file_bug1384658.html" method="GET" id="form">
+ <input type='submit' name='y'>
+ <input type='hidden' name='x'>
+ </form>
+ <script>
+onload = function() {
+ document.getElementById("form").submit();
+};
+ </script>
+</body>
+</html>
diff --git a/dom/websocket/tests/iframe_websocket_sandbox.html b/dom/websocket/tests/iframe_websocket_sandbox.html
new file mode 100644
index 0000000000..0e6d1d97bf
--- /dev/null
+++ b/dom/websocket/tests/iframe_websocket_sandbox.html
@@ -0,0 +1,65 @@
+<html><body>
+<iframe id="frame" sandbox="allow-scripts allow-popups"></iframe>
+<script type="application/javascript">
+onmessage = function(e) {
+ parent.postMessage(e.data, '*');
+}
+
+var ifr = document.getElementById('frame');
+
+if (location.search == '?nested') {
+ var url = new URL(location);
+ url.search = "";
+ ifr.src = url.href;
+} else if (location.search == '?popup') {
+ var url = new URL(location);
+ url.search = "?opener";
+
+ ifr.srcdoc = "<html><script>" +
+ "window.open('" + url.href + "', 'foobar');" +
+ "onmessage = function(e) { " +
+ " parent.postMessage(e.data, '*'); " +
+ "}" +
+ "</scr" + "ipt></html>";
+} else if (location.search == '?opener') {
+ try{
+ var socket = new WebSocket('ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic');
+ socket.onerror = function(e) {
+ opener.postMessage('WS onerror', '*');
+ close();
+ };
+ socket.onopen = function(event) {
+ opener.postMessage('WS onopen', '*');
+ close();
+ };
+ } catch(e) {
+ if (e.name == 'SecurityError') {
+ opener.postMessage('WS Throws!', '*');
+ } else {
+ opener.postMessage('WS Throws something else!', '*');
+ }
+ close();
+ }
+} else {
+ ifr.srcdoc = `
+ <html><script>
+ try{
+ var socket = new WebSocket('ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic');
+ socket.onerror = function(e) {
+ parent.postMessage('WS onerror', '*');
+ };
+ socket.onopen = function(event) {
+ parent.postMessage('WS onopen', '*');
+ };
+ } catch(e) {
+ if (e.name == 'SecurityError') {
+ parent.postMessage('WS Throws!', '*');
+ } else {
+ parent.postMessage('WS Throws something else!', '*');
+ }
+ }
+ </scr`+`ipt>
+ </html>`;
+}
+</script>
+</body></html>
diff --git a/dom/websocket/tests/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 @@
+<html><body>
+Creating WebSocket
+<script type="application/javascript">
+onmessage = function(e) {
+ parent.postMessage(e.data, '*');
+}
+
+try{
+ let socket;
+ if (location.search == '?insecure') {
+ socket = new WebSocket('ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello');
+ }
+ else {
+ socket = new WebSocket('wss://example.com/tests/dom/websocket/tests/file_websocket_hello');
+ }
+ socket.onerror = function(e) {
+ parent.postMessage('WS onerror', '*');
+ close();
+ };
+ socket.onopen = function(event) {
+ socket.close();
+ parent.postMessage('WS onopen', '*');
+ close();
+ };
+ } catch(e) {
+ if (e.name == 'SecurityError') {
+ parent.postMessage('SecurityError', '*');
+ } else {
+ parent.postMessage('WS Throws something else!', '*');
+ }
+ close();
+ }
+
+</script>
+</body></html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+-->
+<head>
+ <title>bug 1081686</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="testWebSocket()">
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var ws;
+
+function forcegc()
+{
+ SpecialPowers.forceGC();
+ SpecialPowers.gc();
+ setTimeout(function()
+ {
+ SpecialPowers.gc();
+ }, 0);
+}
+
+function testWebSocket () {
+ ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello");
+ ws.onopen = function(e) {
+ ws.send("data");
+ }
+ ws.onclose = function(e) {
+ forcegc();
+ setTimeout(function() {
+ is(ws.readyState, 3, 'WebSocket is closed');
+ is(ws.bufferedAmount, 0, 'WebSocket.bufferedAmount should be empty.');
+ is(ws.binaryType, 'blob', 'WebSocket.binaryType is blob');
+ ws.binaryType = 'arraybuffer';
+ is(ws.binaryType, 'arraybuffer', 'WebSocket.binaryType is arraybuffer');
+ is(ws.url, 'ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello', 'WebSocket.url is correct');
+ ws.close();
+ ws.send('foobar');
+ SimpleTest.finish();
+ }, 1000);
+ }
+
+ ws.onerror = function(e) {
+ ok(false, "onerror called!");
+ SimpleTest.finish();
+ }
+ ws.onmessage = function(e) {
+ is(e.data, "Hello world!", "Wrong data");
+ ws.close();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+</script>
+</pre>
+<div>
+
+
+</div>
+
+
+</body>
+</html>
diff --git a/dom/websocket/tests/test_bug1384658.html b/dom/websocket/tests/test_bug1384658.html
new file mode 100644
index 0000000000..dd90a0d5d3
--- /dev/null
+++ b/dom/websocket/tests/test_bug1384658.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1384658
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1384658</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1384658 **/
+
+function test_frameset() {
+ var bc = new BroadcastChannel("test_channel");
+ bc.postMessage("go");
+ bc.onmessage = ev => {
+ ok(ev.data.success, "We didn't hang");
+ bc.close();
+ test_window();
+ };
+}
+
+function test_window() {
+ var win = window.open("http://example.com/tests/dom/websocket/tests/window_bug1384658.html",
+ "_blank", "width=100,height=100");
+ var bc = new BroadcastChannel("test_channel");
+ bc.onmessage = ev => {
+ ok(ev.data.success, "We didn't hang");
+ bc.close();
+ win.close();
+ SimpleTest.finish();
+ };
+}
+
+SimpleTest.waitForExplicitFinish();
+// Use nsICookieService.BEHAVIOR_REJECT_TRACKER to not partition BroadcastChannel
+// by extra first-party domain information.
+SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", 4],
+ // disable third-party storage isolation so the test works as expected
+ ["privacy.partition.always_partition_third_party_non_cookie_storage", false],
+ ["dom.security.https_first", false]
+ ],
+}, test_frameset);
+
+ </script>
+</head>
+<frameset id="frame_set" cols="25%,75%" frameborder="yes" border="5" bordercolor="#008800">
+ <frame id="test_frame" name="test_frame" src="frame_bug1384658.html" marginwidth="20" marginheight="20">
+</frameset>
+</html>
diff --git a/dom/websocket/tests/test_event_listener_leaks.html b/dom/websocket/tests/test_event_listener_leaks.html
new file mode 100644
index 0000000000..9a79fa1354
--- /dev/null
+++ b/dom/websocket/tests/test_event_listener_leaks.html
@@ -0,0 +1,57 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1450358 - Test WebSocket event listener leak conditions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+// Manipulate WebSocket objects in the frame's context.
+// Its important here that we create a listener callback from
+// the DOM objects back to the frame's global in order to
+// exercise the leak condition.
+async function useWebSocket(contentWindow) {
+ const url = "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_bigBlob";
+ let ws = new contentWindow.WebSocket(url);
+
+ ws.onmessage = _ => {
+ contentWindow.messageCount += 1;
+ };
+
+ contentWindow.openCount = 0;
+ await new Promise((resolve, reject) => {
+ ws.onopen = _ => {
+ contentWindow.openCount += 1;
+ resolve();
+ };
+ ws.onerror = e => {
+ contentWindow.errorCount += 1;
+ reject("websocket error");
+ };
+ });
+
+ is(contentWindow.openCount, 1, "open should be received");
+}
+
+async function runTest() {
+ try {
+ await checkForEventListenerLeaks("WebSocket", useWebSocket);
+ } catch (e) {
+ ok(false, e);
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addEventListener("load", runTest, { once: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket1.html b/dom/websocket/tests/test_websocket1.html
new file mode 100644
index 0000000000..1844a67f58
--- /dev/null
+++ b/dom/websocket/tests/test_websocket1.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <script type="text/javascript" src="websocket_tests.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="testWebSocket()">
+<script class="testbody" type="text/javascript">
+
+var tests = [
+ test1, // client tries to connect to a http scheme location;
+ test2, // assure serialization of the connections;
+ test3, // client tries to connect to an non-existent ws server;
+ test4, // client tries to connect using a relative url;
+ test5, // client uses an invalid protocol value;
+ test6, // counter and encoding check;
+ test7, // onmessage event origin property check
+ test8, // client calls close() and the server sends the close frame (with no
+ // code or reason) in acknowledgement;
+ test9, // client closes the connection before the ws connection is established;
+ test10, // client sends a message before the ws connection is established;
+];
+
+function testWebSocket() {
+ doTest();
+}
+
+SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " +
+ "Expect all sorts of flakiness in this test...");
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+<div id="feedback">
+</div>
+
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket2.html b/dom/websocket/tests/test_websocket2.html
new file mode 100644
index 0000000000..afbbaba62f
--- /dev/null
+++ b/dom/websocket/tests/test_websocket2.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <script type="text/javascript" src="websocket_tests.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="testWebSocket()">
+<script class="testbody" type="text/javascript">
+
+var tests = [
+ test11, // a simple hello echo;
+ test12, // client sends a message containing unpaired surrogates
+ test13, //server sends an invalid message;
+ test14, // server sends the close frame, it doesn't close the tcp connection
+ // and it keeps sending normal ws messages;
+ test15, // server closes the tcp connection, but it doesn't send the close
+ // frame;
+ test16, // client calls close() and tries to send a message;
+ test17, // see bug 572975 - all event listeners set
+ test18, // client tries to connect to an http resource;
+ test19, // server closes the tcp connection before establishing the ws
+ // connection;
+ test20, // see bug 572975 - only on error and onclose event listeners set
+];
+
+function testWebSocket() {
+ doTest();
+}
+
+SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " +
+ "Expect all sorts of flakiness in this test...");
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+<div id="feedback">
+</div>
+
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket3.html b/dom/websocket/tests/test_websocket3.html
new file mode 100644
index 0000000000..cc2e091fda
--- /dev/null
+++ b/dom/websocket/tests/test_websocket3.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <script type="text/javascript" src="websocket_tests.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="testWebSocket()">
+<script class="testbody" type="text/javascript">
+
+var tests = [
+ test21, // see bug 572975 - same as test 17, but delete strong event listeners
+ // when receiving the message event;
+ test22, // server takes too long to establish the ws connection;
+ test23, // should detect WebSocket on window object;
+ test24, // server rejects sub-protocol string
+ test25, // ctor with valid empty sub-protocol array
+ test26, // ctor with invalid sub-protocol array containing 1 empty element
+ test27, // ctor with invalid sub-protocol array containing an empty element in
+ // list
+ test28, // ctor using valid 1 element sub-protocol array
+ test29, // ctor using all valid 5 element sub-protocol array
+ test30, // ctor using valid 1 element sub-protocol array with element server
+ // will reject
+];
+
+function testWebSocket() {
+ doTest();
+}
+
+SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " +
+ "Expect all sorts of flakiness in this test...");
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+<div id="feedback">
+</div>
+
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket4.html b/dom/websocket/tests/test_websocket4.html
new file mode 100644
index 0000000000..186a434ab3
--- /dev/null
+++ b/dom/websocket/tests/test_websocket4.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <script type="text/javascript" src="websocket_tests.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="testWebSocket()">
+<script class="testbody" type="text/javascript">
+
+var tests = [
+ test31, // ctor using valid 2 element sub-protocol array with 1 element server
+ // will reject and one server will accept
+ test32, // ctor using invalid sub-protocol array that contains duplicate items
+ test33, // test for sending/receiving custom close code (but no close reason)
+ test34, // test for receiving custom close code and reason
+ test35, // test for sending custom close code and reason
+ test36, // negative test for sending out of range close code
+ test37, // negative test for too long of a close reason
+ test38, // ensure extensions attribute is defined
+ test39, // a basic wss:// connectivity test
+ test40, // negative test for wss:// with no cert
+];
+
+function testWebSocket() {
+ doTest();
+}
+
+SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " +
+ "Expect all sorts of flakiness in this test...");
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+<div id="feedback">
+</div>
+
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket5.html b/dom/websocket/tests/test_websocket5.html
new file mode 100644
index 0000000000..d86752ed5a
--- /dev/null
+++ b/dom/websocket/tests/test_websocket5.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <script type="text/javascript" src="websocket_tests.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="testWebSocket()">
+<script class="testbody" type="text/javascript">
+
+var tests = [
+ test41, // HSTS
+ test42, // non-char utf-8 sequences
+ test43, // Test setting binaryType attribute
+ test44, // Test sending/receving binary ArrayBuffer
+ test45, // Test sending/receving binary Blob
+ test46, // Test that we don't dispatch incoming msgs once in CLOSING state
+ test47, // Make sure onerror/onclose aren't called during close()
+ test48, // see bug 1227136 - client calls close() from onopen() and waits
+ // until WebSocketChannel::mSocketIn is nulled out on socket thread
+ test49, // Test that we fail if subprotocol returned from server doesn't match
+];
+
+function testWebSocket() {
+ doTest();
+}
+
+SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " +
+ "Expect all sorts of flakiness in this test...");
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+<div id="feedback">
+</div>
+
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket_basic.html b/dom/websocket/tests/test_websocket_basic.html
new file mode 100644
index 0000000000..2ae2e690ac
--- /dev/null
+++ b/dom/websocket/tests/test_websocket_basic.html
@@ -0,0 +1,289 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Basic WebSocket test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body onload="testWebSocket()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=472529">Mozilla Bug 472529</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const kUrl = "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic";
+
+var gTestElement;
+var ws;
+
+function forcegc() {
+ SpecialPowers.forceGC();
+ SpecialPowers.gc();
+}
+
+function testWebSocket() {
+ gTestElement = document.getElementById("test");
+
+ SimpleTest.executeSoon(testWebSocket1);
+}
+
+/**
+ * Sends message keywords, then receives their values.
+ */
+function testWebSocket1() {
+ gTestElement.textContent = "Running testWebSocket1()";
+
+ var results = ["test",
+ "/tests/dom/websocket/tests/file_websocket_basic",
+ "http://mochi.test:8888",
+ "end"];
+
+ ws = new WebSocket(kUrl, "test");
+ is(ws.url, kUrl, "[1] WebSocket.url");
+ ws.onopen = function(e) {
+ const params = ["protocol", "resource", "origin", "end"];
+
+ gTestElement.textContent += "\nSending :";
+ for (var i = 0; i < params.length; ++i) {
+ gTestElement.textContent += " " + params[i];
+ ws.send(params[i]);
+ }
+
+ // Set this before onmessage() is called, so it is displayed once only.
+ gTestElement.textContent += "\nReceived:";
+ };
+ ws.onclose = function(e) {
+ is(results.length, 0, "[1] Number of unreceived messages");
+ ok(e.wasClean, "[1] Connection closed cleanly");
+
+ SimpleTest.executeSoon(testWebSocket2);
+ };
+ ws.onerror = function(e) {
+ ok(false, "[1] onerror() should not have been called!");
+ gTestElement.textContent += "\nonerror() should not have been called!";
+ SimpleTest.executeSoon(SimpleTest.finish);
+ };
+ ws.onmessage = function(e) {
+ is(e.data, results[0], "[1] Received message");
+ gTestElement.textContent += " " + e.data;
+ results.shift();
+ };
+}
+
+/**
+ * Sends 1000+1 test messages, then receives them.
+ */
+function testWebSocket2() {
+ gTestElement.textContent = "Running testWebSocket2()";
+
+ const displayInterval = 100;
+ const testCount = 1000;
+ const testMessage = "test message 2.";
+
+ var messageCount = 0;
+
+ ws = new WebSocket(kUrl, "test");
+ ws.onopen = function(e) {
+ gTestElement.textContent += "\nSending :";
+ for (var i = 1; i <= testCount; ++i) {
+ if (i % displayInterval == 1) {
+ gTestElement.textContent += " " + i;
+ }
+ ws.send(testMessage + i);
+ }
+ gTestElement.textContent += " end";
+ ws.send("end");
+
+ // Set this before onmessage() is called, so it is displayed once only.
+ gTestElement.textContent += "\nReceived:";
+ };
+ ws.onclose = function(e) {
+ is(messageCount, testCount + 1, "[2] Number of received messages");
+ ok(e.wasClean, "[2] Connection closed cleanly");
+
+ SimpleTest.executeSoon(testWebSocket3);
+ };
+ ws.onerror = function(e) {
+ ok(false, "[2] onerror() should not have been called!");
+ gTestElement.textContent += "\nonerror() should not have been called!";
+ SimpleTest.executeSoon(SimpleTest.finish);
+ };
+ ws.onmessage = function(e) {
+ ++messageCount;
+ if (messageCount > testCount)
+ is(e.data, "end", "[2] Received message");
+ else
+ is(e.data, testMessage + messageCount, "[2] Received message");
+ if (messageCount % displayInterval == 1) {
+ gTestElement.textContent += " " + messageCount;
+ }
+ };
+}
+
+/**
+ * Sends testcount+1 test messages, then receives them, calling forcegc() at each step.
+ */
+function testWebSocket3() {
+ gTestElement.textContent = "Running testWebSocket3() [can take a little while]";
+
+ const displayInterval = 10;
+ const testCount = 10;
+ const testMessage = "test message 3.";
+
+ var messageCount = 0;
+
+ ws = new WebSocket(kUrl, "test");
+ // Set this before onopen() is called,
+ // otherwise its display would be delayed by forcegc() calls...
+ gTestElement.textContent += "\nSending :";
+ ws.onopen = function(e) {
+ for (var i = 1; i <= testCount; ++i) {
+ forcegc();
+ if (i % displayInterval == 1) {
+ // Actual display is delayed by forcegc() calls...
+ gTestElement.textContent += " " + i;
+ }
+ ws.send(testMessage + i);
+ }
+ forcegc();
+ gTestElement.textContent += " end";
+ ws.send("end");
+
+ // Set this before onmessage() is called, so it is displayed once only.
+ gTestElement.textContent += "\nReceived:";
+ };
+ ws.onclose = function(e) {
+ is(messageCount, testCount + 1, "[3] Number of received messages");
+ ok(e.wasClean, "[3] Connection closed cleanly");
+
+ SimpleTest.executeSoon(testWebSocket4);
+ };
+ ws.onerror = function(e) {
+ ok(false, "[3] onerror() should not have been called!");
+ gTestElement.textContent += "\nonerror() should not have been called!";
+ SimpleTest.executeSoon(SimpleTest.finish);
+ };
+ ws.onmessage = function(e) {
+ forcegc();
+ ++messageCount;
+ if (messageCount > testCount)
+ is(e.data, "end", "[3] Received message");
+ else
+ is(e.data, testMessage + messageCount, "[3] Received message");
+ if (messageCount % displayInterval == 1) {
+ // Actual display is delayed by forcegc() call(s)...
+ gTestElement.textContent += " " + messageCount;
+ }
+ };
+}
+
+/**
+ * Sends a huge test message, then receives it, then closes the WebSocket from client-side.
+ */
+function testWebSocket4() {
+ gTestElement.textContent = "Running testWebSocket4()";
+
+ // String length = 13 + ((10,000 - 1) * 26) + 11 = 259,998 = almost 254 KiB.
+ const longString = "messageStart " + new Array(10000).join(" -huge WebSocket message- ") + " messageEnd";
+
+ ws = new WebSocket(kUrl, "test");
+ ws.onopen = function(e) {
+ is(this, ws, "[4, onopen()] 'this' should point to the WebSocket.");
+ gTestElement.textContent += "\nSending the huge message";
+ ws.send(longString);
+ };
+ ws.onclose = function(e) {
+ is(this, ws, "[4, onclose()] 'this' should point to the WebSocket.");
+ ok(e.wasClean, "[4] Connection closed cleanly");
+
+ SimpleTest.executeSoon(testWebSocket5);
+ };
+ ws.onerror = function(e) {
+ is(this, ws, "[4, onerror()] 'this' should point to the WebSocket.");
+ ok(false, "[4, onerror()] should not have been called!");
+ gTestElement.textContent += "\nonerror() should not have been called!";
+ SimpleTest.executeSoon(SimpleTest.finish);
+ };
+ ws.onmessage = function(e) {
+ is(this, ws, "[4, onmessage()] 'this' should point to the WebSocket.");
+ // Do not use |is(e.data, longString, "...");| that results in a _very_ long line.
+ is(e.data.length, longString.length, "[4] Length of received message");
+ ok(e.data == longString, "[4] Content of received message");
+ gTestElement.textContent += "\nReceived the huge message";
+ this.close();
+ };
+}
+
+/**
+ * Closes the WebSocket from client-side, then sends a test message that should be buffered.
+ */
+function testWebSocket5() {
+ gTestElement.textContent = "Running testWebSocket5()";
+
+ ws = new WebSocket(kUrl, "test");
+ ws.onopen = function(e) {
+ is(this.bufferedAmount, 0, "[5] Length of empty buffer before closing");
+ this.close();
+ };
+ ws.onclose = function(e) {
+ ok(e.wasClean, "[5] Connection closed cleanly");
+ is(this.bufferedAmount, 0, "[5] Length of empty buffer after closing");
+
+ var msg = "test message to be buffered";
+ this.send(msg);
+ is(this.bufferedAmount, msg.length, "[5] Length of buffered message sent after closing");
+
+ gTestElement.textContent += "\ntestWebSocket5() completed";
+
+ SimpleTest.executeSoon(testWebSocket6);
+ };
+ ws.onerror = function(e) {
+ ok(false, "[5] onerror() should not have been called!");
+ gTestElement.textContent += "\nonerror() should not have been called!";
+ SimpleTest.executeSoon(SimpleTest.finish);
+ };
+}
+
+function testWebSocket6() {
+ gTestElement.textContent = "Running testWebSocket6()";
+
+ var msgReceived = false;
+ ws = new WebSocket(kUrl, "test");
+ ws.onopen = function(e) {
+ gTestElement.textContent += "\nSending ©";
+ ws.send("©");
+ gTestElement.textContent += " end";
+ ws.send("end");
+ };
+ ws.onclose = function(e) {
+ ok(msgReceived, "[6] Number of received messages");
+ ok(e.wasClean, "[6] Connection closed cleanly");
+
+ SimpleTest.executeSoon(SimpleTest.finish);
+ };
+ ws.onerror = function(e) {
+ ok(false, "[6] onerror() should not have been called!");
+ gTestElement.textContent += "\nonerror() should not have been called!";
+ SimpleTest.executeSoon(SimpleTest.finish);
+ };
+
+ ws.onmessage = function(e) {
+ if (msgReceived) {
+ is(e.data, "end", "[6] Received message");
+ } else {
+ gTestElement.textContent += "\nReceived: " + e.data;
+ is(e.data, "©", "[6] Received message");
+ msgReceived = true;
+ }
+ };
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket_bigBlob.html b/dom/websocket/tests/test_websocket_bigBlob.html
new file mode 100644
index 0000000000..9db01d6a7f
--- /dev/null
+++ b/dom/websocket/tests/test_websocket_bigBlob.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket test - big blob on content side</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+var ws = CreateTestWS("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_bigBlob");
+is(ws.readyState, 0, "Initial readyState is 0");
+ws.binaryType = "blob";
+
+ws.onopen = function() {
+ is(ws.readyState, 1, "Open readyState is 1");
+ ws.send(new Blob([new Array(1024*1024).join('123456789ABCDEF')]));
+}
+
+let receivedBlob;
+ws.onmessage = function(e) {
+ ok(e.data instanceof Blob, "We should be receiving a Blob");
+ receivedBlob = e.data;
+ ws.close();
+}
+
+ws.onclose = function(e) {
+ is(ws.readyState, 3, "Close readyState is 3");
+
+ // check blob contents
+ var reader = new FileReader();
+ reader.onload = function(event) {
+ is(reader.result, new Array(1024*1024).join('123456789ABCDEF'), "All data matches");
+ }
+
+ reader.onerror = function(event) {
+ ok(false, "Something bad happen.");
+ }
+
+ reader.onloadend = function(event) {
+ SimpleTest.finish();
+ }
+
+ reader.readAsBinaryString(receivedBlob);
+}
+
+SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " +
+ "Expect all sorts of flakiness in this test...");
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket_frame.html b/dom/websocket/tests/test_websocket_frame.html
new file mode 100644
index 0000000000..6c95419bc2
--- /dev/null
+++ b/dom/websocket/tests/test_websocket_frame.html
@@ -0,0 +1,161 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+-->
+<head>
+ <title>Basic websocket frame interception test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+const URI = "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic";
+
+var frameReceivedCounter = 0;
+var frameSentCounter = 0;
+var webSocketCreatedCounter = 0;
+var webSocketOpenedCounter = 0;
+var webSocketMessageAvailableCounter = 0;
+var webSocketClosedCounter = 0;
+
+var tests = [
+ { payload: "Hello world!" },
+ { payload: (function() { var buffer = ""; for (var i = 0; i < 120; ++i) buffer += i; return buffer; }()) },
+]
+
+var innerId = window.windowGlobalChild.innerWindowId;
+ok(innerId, "We have a valid innerWindowID: " + innerId);
+
+var service = Cc["@mozilla.org/websocketevent/service;1"]
+ .getService(Ci.nsIWebSocketEventService);
+ok(!!service, "We have the nsIWebSocketEventService");
+
+var listener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIWebSocketEventListener"]),
+
+ webSocketCreated(aWebSocketSerialID, aURI, aProtocols) {
+ info("WebSocketCreated");
+
+ is(aURI, URI, "URI matches");
+ is(aProtocols, "frame", "Protocol matches");
+
+ webSocketCreatedCounter++;
+ },
+
+ webSocketOpened(aWebSocketSerialID, aEffectiveURI, aProtocols, aExtensions, httpChannelId) {
+ info("WebSocketOpened");
+
+ is(aEffectiveURI, URI, "EffectiveURI matches");
+ is(aProtocols, "frame", "Protocol matches");
+ is(aExtensions, "permessage-deflate", "No extensions");
+ ok(httpChannelId > 0, "Channel ID received");
+
+ webSocketOpenedCounter++;
+ },
+
+ webSocketMessageAvailable(aWebSocketSerialID, aData, aMessageType) {
+ info("WebSocketMessageAvailable");
+
+ if (tests.length) {
+ is(aData, tests[0].payload, "Message matches!");
+ is(aMessageType, Ci.nsIWebSocketEventListener.TYPE_STRING, "The type is 'string'");
+
+ webSocketMessageAvailableCounter++;
+
+ tests.shift();
+ if (tests.length) {
+ ws.send(tests[0].payload);
+ } else {
+ ws.send("end");
+ }
+ }
+ },
+
+ webSocketClosed(aWebSocketSerialID, aWasClean,
+ aCode, aReason) {
+ info("WebSocketClosed");
+
+ ok(aWasClean, "The socket is closed in a clean state");
+ is(aCode, 1000, "Exit code 1000");
+ ok(!aReason.length, "No reason");
+
+ webSocketClosedCounter++;
+ checkListener();
+ },
+
+ frameReceived(aWebSocketSerialID, aFrame) {
+ ok(!!aFrame, "We have received a frame");
+
+ if (tests.length) {
+ ok(aFrame.timeStamp, "Checking timeStamp: " + aFrame.timeStamp);
+ is(aFrame.finBit, true, "Checking finBit");
+ is(aFrame.rsvBit1, true, "Checking rsvBit1");
+ is(aFrame.rsvBit2, false, "Checking rsvBit2");
+ is(aFrame.rsvBit3, false, "Checking rsvBit3");
+ is(aFrame.opCode, aFrame.OPCODE_TEXT, "Checking opCode");
+ is(aFrame.maskBit, false, "Checking maskBit");
+ is(aFrame.mask, 0, "Checking mask");
+ is(aFrame.payload, tests[0].payload, "Checking payload: " + aFrame.payload);
+ }
+
+ frameReceivedCounter++;
+ },
+
+ frameSent(aWebSocketSerialID, aFrame) {
+ ok(!!aFrame, "We have sent a frame");
+
+ if (tests.length) {
+ ok(aFrame.timeStamp, "Checking timeStamp: " + aFrame.timeStamp);
+ is(aFrame.finBit, true, "Checking finBit");
+ is(aFrame.rsvBit1, true, "Checking rsvBit1");
+ is(aFrame.rsvBit2, false, "Checking rsvBit2");
+ is(aFrame.rsvBit3, false, "Checking rsvBit3");
+ is(aFrame.opCode, aFrame.OPCODE_TEXT, "Checking opCode");
+ is(aFrame.maskBit, true, "Checking maskBit");
+ ok(!!aFrame.mask, "Checking mask: " + aFrame.mask);
+ is(aFrame.payload, tests[0].payload, "Checking payload: " + aFrame.payload);
+ }
+
+ frameSentCounter++;
+ }
+};
+
+service.addListener(innerId, listener);
+ok(true, "Listener added");
+
+function checkListener() {
+ service.removeListener(innerId, listener);
+
+ ok(frameReceivedCounter, "We received some frames!");
+ ok(frameSentCounter, "We sent some frames!");
+ ok(webSocketCreatedCounter, "We have a create notification");
+ ok(webSocketOpenedCounter, "We have a open notification");
+ ok(webSocketMessageAvailableCounter, "We have a messageAvailable notification");
+ ok(webSocketClosedCounter, "We have a close notification");
+ SimpleTest.finish();
+}
+
+var ws = new WebSocket(URI, "frame");
+ws.onopen = function(e) {
+ info("onopen");
+
+ ws.send(tests[0].payload);
+}
+
+ws.onclose = function(e) {
+ info("onclose");
+}
+
+ws.onmessage = function(e) {
+ info("onmessage");
+ if (tests.length) {
+ is(e.data, tests[0].payload, "Wrong data");
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket_hello.html b/dom/websocket/tests/test_websocket_hello.html
new file mode 100644
index 0000000000..8508fba97b
--- /dev/null
+++ b/dom/websocket/tests/test_websocket_hello.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+-->
+<head>
+ <title>Basic websocket test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="testWebSocket()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=472529">Mozilla Bug </a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var ws;
+
+function testWebSocket () {
+ ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello");
+ ws.onopen = function(e) {
+ ws.send("data");
+ }
+ ws.onclose = function(e) {
+ }
+ ws.onerror = function(e) {
+ ok(false, "onerror called!");
+ SimpleTest.finish();
+ }
+ ws.onmessage = function(e) {
+ is(e.data, "Hello world!", "Wrong data");
+ ws.close();
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+<div>
+
+
+</div>
+
+
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket_longString.html b/dom/websocket/tests/test_websocket_longString.html
new file mode 100644
index 0000000000..78c8e471c7
--- /dev/null
+++ b/dom/websocket/tests/test_websocket_longString.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket test - big blob on content side</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_basic", "test");
+is(ws.readyState, 0, "Initial readyState is 0");
+
+const longString = new Array(1024*1024).join('123456789ABCDEF');
+
+ws.onopen = function(e) {
+ is(this, ws, "[onopen()] 'this' should point to the WebSocket.");
+ ws.send(longString);
+};
+
+ws.onclose = function(e) {
+ is(this, ws, "[onclose()] 'this' should point to the WebSocket.");
+ ok(e.wasClean, "Connection closed cleanly");
+
+ SimpleTest.executeSoon(SimpleTest.finish);
+};
+
+ws.onerror = function(e) {
+ is(this, ws, "[onerror()] 'this' should point to the WebSocket.");
+ ok(false, "onerror()] should not have been called!");
+ SimpleTest.executeSoon(SimpleTest.finish);
+};
+
+ws.onmessage = function(e) {
+ is(this, ws, "[onmessage()] 'this' should point to the WebSocket.");
+ // Do not use |is(e.data, longString, "...");| that results in a _very_ long line.
+ is(e.data.length, longString.length, "Length of received message");
+ ok(e.data === longString, "Content of received message");
+ this.close();
+};
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket_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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket mixed content tests - load secure and insecure websockets</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="container"></div>
+<iframe id="frame" sandbox="allow-scripts"></iframe>
+<script class="testbody" type="text/javascript">
+
+let iFrameTests = [testWebSocketSecure, testWebSocketInsecure, testSameOriginSandboxInsecure, testSameOriginSandboxSecure, testCrossOriginSandboxInsecure, testCrossOriginSandboxSecure];
+
+function nextIFrameTest() {
+ if(!iFrameTests.length) {
+ document.getElementById("frame").removeAttribute("src");
+ document.getElementById("frame").remove();
+ SimpleTest.finish();
+ }
+ else {
+ let test = iFrameTests.shift();
+ test();
+ }
+}
+
+function testWebSockets () {
+ nextIFrameTest();
+}
+
+function testWebSocketSecure () {
+ let ws = CreateTestWS("wss://example.com/tests/dom/websocket/tests/file_websocket_hello");
+ ws.onopen = function(e) {
+ ws.send("data");
+ }
+ ws.onmessage = function(e) {
+ is(e.data, "Hello world!", "Wrong data");
+ ws.close();
+ nextIFrameTest();
+ }
+}
+
+// Negative test: this should fail as the page was loaded over https
+function testWebSocketInsecure () {
+ try {
+ let ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello");
+ ok(false, "Should throw DOMException");
+ } catch (e) {
+ ok(e instanceof DOMException, "DOMException thrown ");
+ nextIFrameTest();
+ }
+}
+
+// Negative test: this should fail as the page was loaded over https
+function testSameOriginSandboxInsecure() {
+ document.getElementById("frame").src = "https://example.com/tests/dom/websocket/tests/iframe_websocket_wss.html?insecure";
+ onmessage = function(e) {
+ is(e.data, "SecurityError", "ws://URI cannot be used when loaded over https");
+ nextIFrameTest();
+ }
+}
+
+function testSameOriginSandboxSecure() {
+ document.getElementById("frame").src = "https://example.com/tests/dom/websocket/tests/iframe_websocket_wss.html"
+ onmessage = function(e) {
+ is(e.data, "WS onopen", "wss://URI opened");
+ nextIFrameTest();
+ }
+}
+
+// Negative test: this should fail as the page was loaded over https
+function testCrossOriginSandboxInsecure() {
+ document.getElementById("frame").src = "https://example.org/tests/dom/websocket/tests/iframe_websocket_wss.html?insecure";
+ onmessage = function(e) {
+ is(e.data, "SecurityError", "ws://URI cannot be used when loaded over https");
+ nextIFrameTest();
+ }
+}
+
+function testCrossOriginSandboxSecure() {
+ document.getElementById("frame").src = "https://example.org/tests/dom/websocket/tests/iframe_websocket_wss.html"
+
+ onmessage = function(e) {
+ is(e.data, "WS onopen", "wss://URI opened");
+ nextIFrameTest();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+testWebSockets();
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket mixed content opener tests - load secure and insecure websockets in secure and insecure iframes through secure and insecure opened windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="container"></div>
+<script class="testbody" type="text/javascript">
+
+let tests = [ testSecureWindowWSS, testSecureWindowWS, testInsecureWindowWSS, testInsecureWindowWS,
+ testSecureWindowSecureIframeWSS, testSecureWindowSecureIframeWS, testSecureWindowInsecureIframeWSS, testSecureWindowInsecureIframeWS,
+ testInsecureWindowSecureIframeWSS, testInsecureWindowSecureIframeWS, testInsecureWindowInsecureIframeWSS, testInsecureWindowInsecureIframeWS]
+
+function nextTest() {
+ if(!tests.length) {
+ SimpleTest.finish();
+ }
+ else {
+ let test = tests.shift();
+ test();
+ }
+}
+
+function testWebSockets () {
+ SimpleTest.waitForExplicitFinish();
+ nextTest();
+}
+
+function cleanupAndLaunchNextTest(win) {
+ win.close();
+ nextTest();
+}
+
+function testSecureWindowWSS() {
+ let win = window.open("https://example.com/tests/dom/websocket/tests/window_websocket_wss.html", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "WS onopen", "wss://URI opened");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testSecureWindowWS() {
+ let win = window.open("https://example.com/tests/dom/websocket/tests/window_websocket_wss.html?insecure", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "SecurityError", "ws://URI cannot be used when loaded over https");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testInsecureWindowWSS() {
+ let win = window.open("http://example.com/tests/dom/websocket/tests/window_websocket_wss.html", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "WS onopen", "wss://URI opened");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testInsecureWindowWS() {
+ let win = window.open("http://example.com/tests/dom/websocket/tests/window_websocket_wss.html?insecure", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "WS onopen", "ws://URI opened");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testSecureWindowSecureIframeWSS() {
+ let win = window.open("https://example.com/tests/dom/websocket/tests/window_websocket_wss.html?https_iframe_wss", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "WS onopen", "ws://URI opened");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testSecureWindowSecureIframeWS() {
+ let win = window.open("https://example.com/tests/dom/websocket/tests/window_websocket_wss.html?https_iframe_ws", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "SecurityError", "ws://URI cannot be used when loaded over https");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testSecureWindowInsecureIframeWSS() {
+ let win = window.open("https://example.com/tests/dom/websocket/tests/window_websocket_wss.html?http_iframe_wss", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "Error - iframe not loaded", "http iframe cannot be loaded in secure context (mixed content)");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testSecureWindowInsecureIframeWS() {
+ let win = window.open("https://example.com/tests/dom/websocket/tests/window_websocket_wss.html?http_iframe_ws", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "Error - iframe not loaded", "http iframe cannot be loaded in secure context (mixed content)");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testInsecureWindowSecureIframeWSS() {
+ let win = window.open("http://example.com/tests/dom/websocket/tests/window_websocket_wss.html?https_iframe_wss", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "WS onopen", "ws://URI opened");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testInsecureWindowSecureIframeWS() {
+ let win = window.open("http://example.com/tests/dom/websocket/tests/window_websocket_wss.html?https_iframe_ws", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "SecurityError", "ws://URI cannot be used when loaded from an https iframe");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testInsecureWindowInsecureIframeWSS() {
+ let win = window.open("http://example.com/tests/dom/websocket/tests/window_websocket_wss.html?http_iframe_wss", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "WS onopen", "ws://URI opened");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+function testInsecureWindowInsecureIframeWS() {
+ let win = window.open("http://example.com/tests/dom/websocket/tests/window_websocket_wss.html?http_iframe_ws", '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
+ onmessage = function(e) {
+ is(e.data, "WS onopen", "ws://URI opened");
+ cleanupAndLaunchNextTest(win);
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+testWebSockets();
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket_no_duplicate_packet.html b/dom/websocket/tests/test_websocket_no_duplicate_packet.html
new file mode 100644
index 0000000000..7b2b0fc690
--- /dev/null
+++ b/dom/websocket/tests/test_websocket_no_duplicate_packet.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"></meta>
+ <title>WebSocket test - big blob on content side</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="websocket_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+// Test steps:
+// 1. Create a websocket and send 8 chunks of 1MB random data.
+// 2. Store the hash of each chunk (1MB of random data).
+// 3. Websocket server returns the same data back.
+// 4. Calculate the hash again and check the hash is the same as the stored one.
+
+function genRandomPayload() {
+ const count = 128;
+ const chunkSize = 64 * 1024;
+ let buffer = new Uint8Array(chunkSize * count);
+ let offset = 0;
+ for (let i = 0; i < count; i++) {
+ let data = new Uint8Array(chunkSize);
+ crypto.getRandomValues(data);
+ buffer.set(data, offset);
+ offset += chunkSize;
+ }
+
+ return buffer;
+}
+
+function genRandomFile() {
+ return new File([genRandomPayload()], "payload.bin", {
+ type: 'application/octet-stream'
+ });
+}
+
+async function toHexString(buffer) {
+ let hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
+ let hashBytes = new Uint8Array(hashBuffer);
+ let toHex = b => b.toString(16).padStart(2, "0");
+ return Array.from(hashBytes, toHex).join("");
+}
+
+let message_count = 0;
+let sentHashArray = [];
+async function sendFile(file, ws) {
+ const oneMiB = 1 * 1024 * 1024;
+
+ let offset = 0;
+ while (offset < file.size) {
+ let blob = file.slice(offset, offset + oneMiB);
+ let buffer = await blob.arrayBuffer();
+ let hash = await toHexString(buffer);
+ sentHashArray.push(hash);
+ ws.send(buffer);
+ offset += blob.size;
+ message_count++;
+ }
+}
+
+var ws = CreateTestWS("wss://example.com/tests/dom/websocket/tests/file_websocket_bigBlob");
+is(ws.readyState, 0, "Initial readyState is 0");
+ws.binaryType = "blob";
+
+ws.onopen = function() {
+ is(ws.readyState, 1, "Open readyState is 1");
+ let file = genRandomFile();
+ sendFile(file, ws);
+}
+
+let receivedBlobs = [];
+ws.onmessage = function(e) {
+ ok(e.data instanceof Blob, "We should be receiving a Blob");
+ receivedBlobs.push(e.data);
+ message_count--;
+ if (message_count == 0) {
+ ws.close();
+ }
+}
+
+async function checkContent() {
+ is(receivedBlobs.length, sentHashArray.length, "length should be the same");
+ for (let index = 0; index < receivedBlobs.length; index++) {
+ let buffer = await receivedBlobs[index].arrayBuffer();
+ let hash = await toHexString(buffer);
+ is(hash, sentHashArray[index], "hash should be equal");
+ }
+}
+
+ws.onclose = function(e) {
+ is(ws.readyState, 3, "Close readyState is 3");
+ checkContent().then(() => {
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.requestFlakyTimeout("The web socket tests are really fragile, but avoiding timeouts might be hard, since it's testing stuff on the network. " +
+ "Expect all sorts of flakiness in this test...");
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket_permessage_deflate.html b/dom/websocket/tests/test_websocket_permessage_deflate.html
new file mode 100644
index 0000000000..ecf66419f2
--- /dev/null
+++ b/dom/websocket/tests/test_websocket_permessage_deflate.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Basic test of permessage compression websocket extension</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="loadDeflate()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=792831">Mozilla Bug </a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var ws;
+var textMessage = "This is a text message";
+var binaryMessage = "This is a binary message";
+var testIdx = 0;
+var sendText = true;
+
+tests = [
+ // enable PMCE
+ [ true, true, "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_permessage_deflate" ],
+ // disable PMCE
+ [ false, false, "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_permessage_deflate_disabled" ],
+ // server rejects offered PMCE
+ [ true, false, "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_permessage_deflate_rejected" ],
+ // server returns parameters in the handshake
+ [ true, true, "ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_permessage_deflate_params" ]
+]
+
+function ab2str(buf) {
+ return String.fromCharCode.apply(null, new Uint16Array(buf));
+}
+
+function str2ab(str) {
+ var buf = new ArrayBuffer(str.length*2);
+ var bufView = new Uint16Array(buf);
+ for (var i=0, strLen=str.length; i<strLen; i++) {
+ bufView[i] = str.charCodeAt(i);
+ }
+ return buf;
+}
+
+function sendMessage() {
+ if (sendText) {
+ ws.send(textMessage);
+ } else {
+ ws.binaryType = "arraybuffer";
+ ws.send(str2ab(binaryMessage));
+ }
+}
+
+function testDeflate() {
+ ws = new WebSocket(tests[testIdx][2]);
+
+ ws.onopen = function(e) {
+ if (tests[testIdx][1]) {
+ is(ws.extensions, "permessage-deflate", "permessage-deflate not negotiated!");
+ } else {
+ is(ws.extensions, "", "permessage-deflate should not be negotiated!");
+ }
+
+ sendMessage();
+ }
+
+ ws.onclose = function(e) {
+ if (!e.wasClean) {
+ ok(false, "Connection should be closed cleanly!");
+ SimpleTest.finish();
+ }
+ }
+
+ ws.onerror = function(e) {
+ ok(false, "onerror called!");
+ SimpleTest.finish();
+ }
+
+ ws.onmessage = function(e) {
+ if (sendText) {
+ is(e.data, textMessage, "Text message not received successfully!");
+ sendText = false;
+ sendMessage();
+ } else {
+ ok(e.data instanceof ArrayBuffer, "Should receive an arraybuffer!");
+ is(ab2str(e.data), binaryMessage, "Binary message not received successfully!");
+ ws.close();
+
+ sendText = true;
+ testIdx++;
+ if (testIdx < tests.length) {
+ loadDeflate();
+ } else {
+ SimpleTest.finish();
+ }
+ }
+ }
+}
+
+function loadDeflate() {
+ SpecialPowers.pushPrefEnv({"set":[['network.websocket.extensions.permessage-deflate', tests[testIdx][0]]]}, testDeflate);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1252751</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="container"></div>
+<iframe id="frame"></iframe>
+<script type="application/javascript">
+var urls = [ "https://example.com/tests/dom/websocket/tests/iframe_websocket_sandbox.html",
+ "https://example.com/tests/dom/websocket/tests/iframe_websocket_sandbox.html?nested",
+ "https://example.com/tests/dom/websocket/tests/iframe_websocket_sandbox.html?popup" ];
+
+onmessage = function(e) {
+ is(e.data, "WS Throws!", "ws://URI cannot be used by a https iframe");
+ runTest();
+}
+
+function runTest() {
+ if (!urls.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ document.getElementById("frame").src = urls.shift();
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest();
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_websocket_sharedWorker.html b/dom/websocket/tests/test_websocket_sharedWorker.html
new file mode 100644
index 0000000000..b79b0a6ced
--- /dev/null
+++ b/dom/websocket/tests/test_websocket_sharedWorker.html
@@ -0,0 +1,30 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for bug 1090183</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+
+var sw = new SharedWorker('websocket_sharedWorker.js');
+sw.port.onmessage = function(event) {
+ if (event.data.type == 'finish') {
+ SimpleTest.finish();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_worker_websocket1.html b/dom/websocket/tests/test_worker_websocket1.html
new file mode 100644
index 0000000000..71414fbe42
--- /dev/null
+++ b/dom/websocket/tests/test_worker_websocket1.html
@@ -0,0 +1,46 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebSocket object in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<pre id="feedback"></pre>
+<script class="testbody" type="text/javascript">
+
+ var worker = new Worker("websocket_worker1.js");
+
+ worker.onmessage = function(event) {
+ is(event.target, worker, "event.target should be a worker!");
+
+ if (event.data.type == 'finish') {
+ info("All done!");
+ SimpleTest.finish();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ } else if (event.data.type == 'feedback') {
+ info(event.data.msg);
+ document.getElementById('feedback').innerHTML += event.data.msg + "\n";
+ }
+ };
+
+ worker.onerror = function(event) {
+ is(event.target, worker);
+ info("error!");
+ ok(false, "Worker had an error: " + event.data);
+ SimpleTest.finish();
+ };
+
+ worker.postMessage('foobar');
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_worker_websocket2.html b/dom/websocket/tests/test_worker_websocket2.html
new file mode 100644
index 0000000000..d9e16281b9
--- /dev/null
+++ b/dom/websocket/tests/test_worker_websocket2.html
@@ -0,0 +1,46 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebSocket object in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<pre id="feedback"></pre>
+<script class="testbody" type="text/javascript">
+
+ var worker = new Worker("websocket_worker2.js");
+
+ worker.onmessage = function(event) {
+ is(event.target, worker, "event.target should be a worker!");
+
+ if (event.data.type == 'finish') {
+ info("All done!");
+ SimpleTest.finish();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ } else if (event.data.type == 'feedback') {
+ info(event.data.msg);
+ document.getElementById('feedback').innerHTML += event.data.msg + "\n";
+ }
+ };
+
+ worker.onerror = function(event) {
+ is(event.target, worker);
+ info("error!");
+ ok(false, "Worker had an error: " + event.data);
+ SimpleTest.finish();
+ };
+
+ worker.postMessage('foobar');
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_worker_websocket3.html b/dom/websocket/tests/test_worker_websocket3.html
new file mode 100644
index 0000000000..4387192c42
--- /dev/null
+++ b/dom/websocket/tests/test_worker_websocket3.html
@@ -0,0 +1,46 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebSocket object in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<pre id="feedback"></pre>
+<script class="testbody" type="text/javascript">
+
+ var worker = new Worker("websocket_worker3.js");
+
+ worker.onmessage = function(event) {
+ is(event.target, worker, "event.target should be a worker!");
+
+ if (event.data.type == 'finish') {
+ info("All done!");
+ SimpleTest.finish();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ } else if (event.data.type == 'feedback') {
+ info(event.data.msg);
+ document.getElementById('feedback').innerHTML += event.data.msg + "\n";
+ }
+ };
+
+ worker.onerror = function(event) {
+ is(event.target, worker);
+ info("error!");
+ ok(false, "Worker had an error: " + event.data);
+ SimpleTest.finish();
+ };
+
+ worker.postMessage('foobar');
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_worker_websocket4.html b/dom/websocket/tests/test_worker_websocket4.html
new file mode 100644
index 0000000000..5f4c390574
--- /dev/null
+++ b/dom/websocket/tests/test_worker_websocket4.html
@@ -0,0 +1,46 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebSocket object in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<pre id="feedback"></pre>
+<script class="testbody" type="text/javascript">
+
+ var worker = new Worker("websocket_worker4.js");
+
+ worker.onmessage = function(event) {
+ is(event.target, worker, "event.target should be a worker!");
+
+ if (event.data.type == 'finish') {
+ info("All done!");
+ SimpleTest.finish();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ } else if (event.data.type == 'feedback') {
+ info(event.data.msg);
+ document.getElementById('feedback').innerHTML += event.data.msg + "\n";
+ }
+ };
+
+ worker.onerror = function(event) {
+ is(event.target, worker);
+ info("error!");
+ ok(false, "Worker had an error: " + event.data);
+ SimpleTest.finish();
+ };
+
+ worker.postMessage('foobar');
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_worker_websocket5.html b/dom/websocket/tests/test_worker_websocket5.html
new file mode 100644
index 0000000000..0b3450b7b6
--- /dev/null
+++ b/dom/websocket/tests/test_worker_websocket5.html
@@ -0,0 +1,46 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebSocket object in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<pre id="feedback"></pre>
+<script class="testbody" type="text/javascript">
+
+ var worker = new Worker("websocket_worker5.js");
+
+ worker.onmessage = function(event) {
+ is(event.target, worker, "event.target should be a worker!");
+
+ if (event.data.type == 'finish') {
+ info("All done!");
+ SimpleTest.finish();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ } else if (event.data.type == 'feedback') {
+ info(event.data.msg);
+ document.getElementById('feedback').innerHTML += event.data.msg + "\n";
+ }
+ };
+
+ worker.onerror = function(event) {
+ is(event.target, worker);
+ info("error!");
+ ok(false, "Worker had an error: " + event.data);
+ SimpleTest.finish();
+ };
+
+ worker.postMessage('foobar');
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_worker_websocket_basic.html b/dom/websocket/tests/test_worker_websocket_basic.html
new file mode 100644
index 0000000000..39ff1647e7
--- /dev/null
+++ b/dom/websocket/tests/test_worker_websocket_basic.html
@@ -0,0 +1,57 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebSocket object in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var worker = new Worker("websocket_basic_worker.js");
+
+ worker.onmessage = function(event) {
+ is(event.target, worker);
+
+ if (event.data.type == 'finish') {
+ runTest();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ }
+ };
+
+ worker.onerror = function(event) {
+ is(event.target, worker);
+ ok(false, "Worker had an error: " + event.data);
+ SimpleTest.finish();
+ };
+
+ var tests = [
+ function() { worker.postMessage(0); },
+ function() { worker.postMessage(1); }
+ ];
+
+ function runTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+ }
+
+ runTest();
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_worker_websocket_https.html b/dom/websocket/tests/test_worker_websocket_https.html
new file mode 100644
index 0000000000..9283b9bd09
--- /dev/null
+++ b/dom/websocket/tests/test_worker_websocket_https.html
@@ -0,0 +1,30 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that creating insecure websockets from https workers is not possible</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" language="javascript">
+
+ onmessage = function(event) {
+ is(event.data, "not created", "WebSocket object must not be created");
+ SimpleTest.finish();
+ };
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+<iframe src="https://example.com/tests/dom/websocket/tests/websocket_worker_https.html"></iframe>
+</body>
+</html>
diff --git a/dom/websocket/tests/test_worker_websocket_loadgroup.html b/dom/websocket/tests/test_worker_websocket_loadgroup.html
new file mode 100644
index 0000000000..d02b8f149c
--- /dev/null
+++ b/dom/websocket/tests/test_worker_websocket_loadgroup.html
@@ -0,0 +1,61 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebSocket object in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var worker = new Worker("websocket_loadgroup_worker.js");
+
+ var stopped = false;
+ worker.onmessage = function(e) {
+ if (e.data == 'opened') {
+ stopped = true;
+ window.stop();
+ } else if (e.data == 'closed') {
+ ok(stopped, "Good!");
+ stopped = false;
+ runTest();
+ } else {
+ ok(false, "An error has been received");
+ }
+ };
+
+ worker.onerror = function(event) {
+ is(event.target, worker);
+ ok(false, "Worker had an error: " + event.data);
+ SimpleTest.finish();
+ };
+
+ var tests = [
+ function() { worker.postMessage(0); },
+ function() { worker.postMessage(1); }
+ ];
+
+ function runTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+ }
+
+ runTest();
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/websocket/tests/websocket_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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+function debug(msg) {
+ ok(1, msg);
+}
+
+function createArrayBufferContainingHelloWorld()
+{
+ var hello = "Hello, world!";
+ var array = new Uint8Array(hello.length);
+ for (var i = 0; i < hello.length; ++i)
+ array[i] = hello.charCodeAt(i);
+ return array.buffer;
+}
+
+function createEmptyArrayBuffer()
+{
+ return new ArrayBuffer(0);
+}
+
+function createArrayBufferContainingAllDistinctBytes()
+{
+ var array = new Uint8Array(256);
+ for (var i = 0; i < 256; ++i)
+ array[i] = i;
+ return array.buffer;
+}
+
+var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/websocket_hybi/file_binary-frames");
+ws.binaryType = "arraybuffer";
+is(ws.binaryType, "arraybuffer", "should be equal to 'arraybuffer'");
+
+var closeEvent;
+var receivedMessages = [];
+var expectedValues = [createArrayBufferContainingHelloWorld(), createEmptyArrayBuffer(), createArrayBufferContainingAllDistinctBytes()];
+
+ws.onmessage = function(event)
+{
+ receivedMessages.push(event.data);
+};
+
+ws.onclose = function(event)
+{
+ closeEvent = event;
+
+ is(receivedMessages.length, expectedValues.length, "lengths not equal");
+ for (var i = 0; i < expectedValues.length; ++i)
+ check(i);
+ SimpleTest.finish();
+};
+
+var responseType;
+
+function check(index)
+{
+ debug("Checking message #" + index + ".");
+ ok(receivedMessages[index] instanceof ArrayBuffer,
+ "Should receive an arraybuffer!");
+ checkArrayBuffer(index, receivedMessages[index], expectedValues[index]);
+}
+
+var actualArray;
+var expectedArray;
+
+function checkArrayBuffer(testIndex, actual, expected)
+{
+ actualArray = new Uint8Array(actual);
+ expectedArray = new Uint8Array(expected);
+ is(actualArray.length, expectedArray.length, "lengths not equal");
+ // Print only the first mismatched byte in order not to flood console.
+ for (var i = 0; i < expectedArray.length; ++i) {
+ if (actualArray[i] != expectedArray[i]) {
+ ok(false, "Value mismatch: actualArray[" + i + "] = " + actualArray[i] + ", expectedArray[" + i + "] = " + expectedArray[i]);
+ return;
+ }
+ }
+ ok(true, "Passed: Message #" + testIndex + ".");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/websocket_hybi/test_receive-blob.html b/dom/websocket/tests/websocket_hybi/test_receive-blob.html
new file mode 100644
index 0000000000..5589633a7c
--- /dev/null
+++ b/dom/websocket/tests/websocket_hybi/test_receive-blob.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+function debug(msg) {
+ ok(true, msg);
+}
+
+function createArrayBufferContainingHelloWorld()
+{
+ var hello = "Hello, world!";
+ var array = new Uint8Array(hello.length);
+ for (var i = 0; i < hello.length; ++i)
+ array[i] = hello.charCodeAt(i);
+ return array.buffer;
+}
+
+function createEmptyArrayBuffer()
+{
+ return new ArrayBuffer(0);
+}
+
+function createArrayBufferContainingAllDistinctBytes()
+{
+ var array = new Uint8Array(256);
+ for (var i = 0; i < 256; ++i)
+ array[i] = i;
+ return array.buffer;
+}
+
+var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/websocket_hybi/file_binary-frames");
+is(ws.binaryType, "blob", "should be 'blob'");
+
+var closeEvent;
+var receivedMessages = [];
+var expectedValues = [createArrayBufferContainingHelloWorld(), createEmptyArrayBuffer(), createArrayBufferContainingAllDistinctBytes()];
+
+ws.onmessage = function(event)
+{
+ receivedMessages.push(event.data);
+};
+
+ws.onclose = function(event)
+{
+ closeEvent = event;
+
+ is(receivedMessages.length, expectedValues.length, "lengths not same");
+ check(0);
+};
+
+var responseType;
+
+function check(index)
+{
+ if (index == expectedValues.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ debug("Checking message #" + index + ".");
+ ok(receivedMessages[index] instanceof Blob,
+ "We should be receiving a Blob");
+ var reader = new FileReader();
+ reader.readAsArrayBuffer(receivedMessages[index]);
+ reader.onload = function(event)
+ {
+ checkArrayBuffer(index, reader.result, expectedValues[index]);
+ check(index + 1);
+ };
+ reader.onerror = function(event)
+ {
+ ok(false, "Failed to read blob: error code = " + reader.error.code);
+ check(index + 1);
+ };
+}
+
+var actualArray;
+var expectedArray;
+
+function checkArrayBuffer(testIndex, actual, expected)
+{
+ actualArray = new Uint8Array(actual);
+ expectedArray = new Uint8Array(expected);
+ is(actualArray.length, expectedArray.length, "lengths not same");
+ // Print only the first mismatched byte in order not to flood console.
+ for (var i = 0; i < expectedArray.length; ++i) {
+ if (actualArray[i] != expectedArray[i]) {
+ ok(false, "Value mismatch: actualArray[" + i + "] = " + actualArray[i] + ", expectedArray[" + i + "] = " + expectedArray[i]);
+ return;
+ }
+ }
+ ok(true, "Passed: Message #" + testIndex + ".");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/websocket_hybi/test_send-arraybuffer.html b/dom/websocket/tests/websocket_hybi/test_send-arraybuffer.html
new file mode 100644
index 0000000000..6c71ca5415
--- /dev/null
+++ b/dom/websocket/tests/websocket_hybi/test_send-arraybuffer.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+function debug(msg) {
+ ok(1, msg);
+}
+
+function startsWith(target, prefix)
+{
+ return target.indexOf(prefix) === 0;
+}
+
+function createArrayBufferContainingHelloWorld()
+{
+ var hello = "Hello, world!";
+ var array = new Uint8Array(hello.length);
+ for (var i = 0; i < hello.length; ++i)
+ array[i] = hello.charCodeAt(i);
+ return array.buffer;
+}
+
+function createEmptyArrayBuffer()
+{
+ return new ArrayBuffer(0);
+}
+
+function createArrayBufferContainingAllDistinctBytes()
+{
+ var array = new Uint8Array(256);
+ for (var i = 0; i < 256; ++i)
+ array[i] = i;
+ return array.buffer;
+}
+
+var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/websocket_hybi/file_check-binary-messages");
+var closeEvent;
+
+ws.onopen = function()
+{
+ ok(true, "onopen reached");
+ ws.send(createArrayBufferContainingHelloWorld());
+ ws.send(createEmptyArrayBuffer());
+ ws.send(createArrayBufferContainingAllDistinctBytes());
+};
+
+ws.onmessage = function(event)
+{
+ var message = event.data;
+ if (startsWith(message, "PASS"))
+ ok(true, message);
+ else
+ ok(false, message);
+};
+
+ws.onclose = function(event)
+{
+ ok(event.wasClean, "should have closed cleanly");
+ SimpleTest.finish();
+};
+
+ws.onerror = function(event)
+{
+ ok(false, "onerror should not have been called");
+};
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/websocket_hybi/test_send-blob.html b/dom/websocket/tests/websocket_hybi/test_send-blob.html
new file mode 100644
index 0000000000..6af1e2df03
--- /dev/null
+++ b/dom/websocket/tests/websocket_hybi/test_send-blob.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<p id="display">
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+function startsWith(target, prefix)
+{
+ return target.indexOf(prefix) === 0;
+}
+
+function distinctBytes()
+{
+ var array = new Array();
+ for (var i = 0; i < 256; ++i)
+ array[i] = i;
+ // Concatenates chars into a single binary string
+ return String.fromCharCode.apply(null, array);
+}
+
+var filesToCreate = [
+ {name: "hellofile", data: "Hello, world!"},
+ {name: "emptyfile"},
+ {name: "allchars", data: distinctBytes()},
+];
+
+SpecialPowers.createFiles(filesToCreate, function (files) {
+ var ws = new WebSocket("ws://mochi.test:8888/tests/dom/websocket/tests/websocket_hybi/file_check-binary-messages");
+ var closeEvent;
+
+ ws.onopen = function()
+ {
+ ws.send(files[0]);
+ ws.send(files[1]);
+ ws.send(files[2]);
+ };
+
+ ws.onmessage = function(event)
+ {
+ var message = event.data;
+ if (startsWith(message, "PASS"))
+ ok(true, message);
+ else
+ ok(false, message);
+ };
+
+ ws.onclose = function(event)
+ {
+ ok(event.wasClean, "should have closed cleanly");
+ SimpleTest.finish();
+ };
+},
+function (msg) {
+ ok(false, "Failed to create files: " + msg);
+ SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/websocket/tests/websocket_loadgroup_worker.js b/dom/websocket/tests/websocket_loadgroup_worker.js
new file mode 100644
index 0000000000..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 @@
+<!DOCTYPE html>
+<script>
+ var worker = new Worker("https://example.com/tests/dom/websocket/tests/websocket_https_worker.js");
+
+ worker.onmessage = function(event) {
+ parent.postMessage(event.data, "*");
+ };
+
+ worker.onerror = function(event) {
+ parent.postMessage("error", "*");
+ };
+
+ worker.postMessage("start");
+</script>
diff --git a/dom/websocket/tests/window_bug1384658.html b/dom/websocket/tests/window_bug1384658.html
new file mode 100644
index 0000000000..c2d6e16c8d
--- /dev/null
+++ b/dom/websocket/tests/window_bug1384658.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+ onload = function() {
+ if (window.location.search == "") {
+ window.open("window_bug1384658.html?opened", "_top", "");
+ } else {
+ var iframeURL = "http://mochi.test:8888/tests/dom/websocket/tests/file_bug1384658.html";
+ var iframe = document.createElement("iframe");
+ iframe.src = iframeURL;
+ document.body.appendChild(iframe);
+ }
+ };
+</script>
+</head>
+<body>
+</body>
+</html>
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 @@
+<html><body>
+Creating WebSocket
+<iframe id="frame" sandbox="allow-scripts"></iframe>
+<script type="application/javascript">
+
+function cleanup() {
+ window.document.getElementById("frame").removeAttribute("src");
+ window.document.getElementById("frame").remove();
+}
+
+onmessage = function(e) {
+ cleanup();
+ window.opener.postMessage(e.data, '*');
+}
+
+// Mixed content blocker will prevent loading iframes via http, so in that case pass back the error
+window.document.getElementById("frame").onerror = function(e) {
+ cleanup();
+ window.opener.postMessage("Error - iframe not loaded", '*');
+}
+
+// Load one of the iframe variants?
+if (location.search == '?https_iframe_wss') {
+ window.document.getElementById("frame").src = "https://example.com/tests/dom/websocket/tests/iframe_websocket_wss.html";
+} else if (location.search == '?https_iframe_ws') {
+ window.document.getElementById("frame").src = "https://example.com/tests/dom/websocket/tests/iframe_websocket_wss.html?insecure";
+} else if (location.search == '?http_iframe_wss' || location.search == '?http_iframe_ws') {
+ let iFrameUrl = "http://example.com/tests/dom/websocket/tests/iframe_websocket_wss.html";
+ if (location.search == '?http_iframe_ws') {
+ iFrameUrl += "?insecure";
+ }
+ window.document.getElementById("frame").src = iFrameUrl;
+}
+else {
+ try {
+ let socket;
+ if (location.search == '?insecure') {
+ socket = new WebSocket('ws://mochi.test:8888/tests/dom/websocket/tests/file_websocket_hello');
+ }
+ else {
+ socket = new WebSocket('wss://example.com/tests/dom/websocket/tests/file_websocket_hello');
+ }
+ socket.onerror = function(e) {
+ cleanup();
+ window.opener.postMessage("WS onerror", "*");
+ };
+ socket.onopen = function(event) {
+ socket.close();
+ cleanup();
+ window.opener.postMessage("WS onopen", "*");
+ };
+ }
+ catch(e) {
+ if (e.name == 'SecurityError') {
+ cleanup();
+ window.opener.postMessage("SecurityError", "*");
+ } else {
+ cleanup();
+ window.opener.postMessage("Test Throws", "*");
+ }
+ }
+}
+
+</script>
+</body></html>