diff options
Diffstat (limited to 'dom/network')
46 files changed, 6610 insertions, 0 deletions
diff --git a/dom/network/Connection.cpp b/dom/network/Connection.cpp new file mode 100644 index 0000000000..217a937cf6 --- /dev/null +++ b/dom/network/Connection.cpp @@ -0,0 +1,88 @@ +/* -*- 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 "Connection.h" +#include "ConnectionMainThread.h" +#include "ConnectionWorker.h" +#include "Constants.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/WorkerPrivate.h" + +/** + * We have to use macros here because our leak analysis tool things we are + * leaking strings when we have |static const nsString|. Sad :( + */ +#define CHANGE_EVENT_NAME u"typechange"_ns + +namespace mozilla::dom::network { + +// Don't use |Connection| alone, since that confuses nsTraceRefcnt since +// we're not the only class with that name. +NS_IMPL_ISUPPORTS_INHERITED0(dom::network::Connection, DOMEventTargetHelper) + +Connection::Connection(nsPIDOMWindowInner* aWindow, + bool aShouldResistFingerprinting) + : DOMEventTargetHelper(aWindow), + mShouldResistFingerprinting(aShouldResistFingerprinting), + mType(static_cast<ConnectionType>(kDefaultType)), + mIsWifi(kDefaultIsWifi), + mDHCPGateway(kDefaultDHCPGateway), + mBeenShutDown(false) { + Telemetry::Accumulate(Telemetry::NETWORK_CONNECTION_COUNT, 1); +} + +Connection::~Connection() { + NS_ASSERT_OWNINGTHREAD(Connection); + MOZ_ASSERT(mBeenShutDown); +} + +void Connection::Shutdown() { + NS_ASSERT_OWNINGTHREAD(Connection); + + if (mBeenShutDown) { + return; + } + + mBeenShutDown = true; + ShutdownInternal(); +} + +JSObject* Connection::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return NetworkInformation_Binding::Wrap(aCx, this, aGivenProto); +} + +void Connection::Update(ConnectionType aType, bool aIsWifi, + uint32_t aDHCPGateway, bool aNotify) { + NS_ASSERT_OWNINGTHREAD(Connection); + + ConnectionType previousType = mType; + + mType = aType; + mIsWifi = aIsWifi; + mDHCPGateway = aDHCPGateway; + + if (aNotify && previousType != aType && !mShouldResistFingerprinting) { + DispatchTrustedEvent(CHANGE_EVENT_NAME); + } +} + +/* static */ +Connection* Connection::CreateForWindow(nsPIDOMWindowInner* aWindow, + bool aShouldResistFingerprinting) { + MOZ_ASSERT(aWindow); + return new ConnectionMainThread(aWindow, aShouldResistFingerprinting); +} + +/* static */ +already_AddRefed<Connection> Connection::CreateForWorker( + WorkerPrivate* aWorkerPrivate, ErrorResult& aRv) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + return ConnectionWorker::Create(aWorkerPrivate, aRv); +} + +} // namespace mozilla::dom::network diff --git a/dom/network/Connection.h b/dom/network/Connection.h new file mode 100644 index 0000000000..9d18ad089b --- /dev/null +++ b/dom/network/Connection.h @@ -0,0 +1,99 @@ +/* -*- 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 mozilla_dom_network_Connection_h +#define mozilla_dom_network_Connection_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/NetworkInformationBinding.h" +#include "nsCycleCollectionParticipant.h" + +namespace mozilla { + +namespace hal { +class NetworkInformation; +} // namespace hal + +namespace dom { + +class WorkerPrivate; + +namespace network { + +class Connection : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + + static Connection* CreateForWindow(nsPIDOMWindowInner* aWindow, + bool aShouldResistFingerprinting); + + static already_AddRefed<Connection> CreateForWorker( + WorkerPrivate* aWorkerPrivate, ErrorResult& aRv); + + void Shutdown(); + + // WebIDL + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + ConnectionType Type() const { + return mShouldResistFingerprinting + ? static_cast<ConnectionType>(ConnectionType::Unknown) + : mType; + } + + bool GetIsWifi() const { + NS_ASSERT_OWNINGTHREAD(Connection); + + return mIsWifi; + } + uint32_t GetDhcpGateway() const { + NS_ASSERT_OWNINGTHREAD(Connection); + + return mDHCPGateway; + } + + IMPL_EVENT_HANDLER(typechange) + + protected: + Connection(nsPIDOMWindowInner* aWindow, bool aShouldResistFingerprinting); + virtual ~Connection(); + + void Update(ConnectionType aType, bool aIsWifi, uint32_t aDHCPGateway, + bool aNotify); + + virtual void ShutdownInternal() = 0; + + private: + /** + * If ResistFingerprinting is enabled or disabled. + */ + bool mShouldResistFingerprinting; + + /** + * The type of current connection. + */ + ConnectionType mType; + + /** + * If the connection is WIFI + */ + bool mIsWifi; + + /** + * DHCP Gateway information for IPV4, in network byte order. 0 if unassigned. + */ + uint32_t mDHCPGateway; + + bool mBeenShutDown; +}; + +} // namespace network +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_network_Connection_h diff --git a/dom/network/ConnectionMainThread.cpp b/dom/network/ConnectionMainThread.cpp new file mode 100644 index 0000000000..210cbaf015 --- /dev/null +++ b/dom/network/ConnectionMainThread.cpp @@ -0,0 +1,40 @@ +/* -*- 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 <limits> +#include "mozilla/Hal.h" +#include "ConnectionMainThread.h" + +namespace mozilla::dom::network { + +ConnectionMainThread::ConnectionMainThread(nsPIDOMWindowInner* aWindow, + bool aShouldResistFingerprinting) + : Connection(aWindow, aShouldResistFingerprinting) { + hal::RegisterNetworkObserver(this); + + hal::NetworkInformation networkInfo; + hal::GetCurrentNetworkInformation(&networkInfo); + + UpdateFromNetworkInfo(networkInfo, false); +} + +ConnectionMainThread::~ConnectionMainThread() { Shutdown(); } + +void ConnectionMainThread::ShutdownInternal() { + hal::UnregisterNetworkObserver(this); +} + +void ConnectionMainThread::UpdateFromNetworkInfo( + const hal::NetworkInformation& aNetworkInfo, bool aNotify) { + Update(static_cast<ConnectionType>(aNetworkInfo.type()), + aNetworkInfo.isWifi(), aNetworkInfo.dhcpGateway(), aNotify); +} + +void ConnectionMainThread::Notify(const hal::NetworkInformation& aNetworkInfo) { + UpdateFromNetworkInfo(aNetworkInfo, true); +} + +} // namespace mozilla::dom::network diff --git a/dom/network/ConnectionMainThread.h b/dom/network/ConnectionMainThread.h new file mode 100644 index 0000000000..1e6a12f9b1 --- /dev/null +++ b/dom/network/ConnectionMainThread.h @@ -0,0 +1,40 @@ +/* -*- 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 mozilla_dom_network_ConnectionMainThread_h +#define mozilla_dom_network_ConnectionMainThread_h + +#include "Connection.h" +#include "mozilla/Hal.h" +#include "mozilla/Observer.h" + +namespace mozilla::dom::network { + +class ConnectionMainThread final : public Connection, + public hal::NetworkObserver { + public: + explicit ConnectionMainThread(nsPIDOMWindowInner* aWindow, + bool aShouldResistFingerprinting); + + // For IObserver + void Notify(const hal::NetworkInformation& aNetworkInfo) override; + + private: + ~ConnectionMainThread(); + + virtual void ShutdownInternal() override; + + /** + * Update the connection information stored in the object using a + * NetworkInformation object. + */ + void UpdateFromNetworkInfo(const hal::NetworkInformation& aNetworkInfo, + bool aNotify); +}; + +} // namespace mozilla::dom::network + +#endif // mozilla_dom_network_ConnectionMainThread_h diff --git a/dom/network/ConnectionWorker.cpp b/dom/network/ConnectionWorker.cpp new file mode 100644 index 0000000000..e0773455df --- /dev/null +++ b/dom/network/ConnectionWorker.cpp @@ -0,0 +1,212 @@ +/* -*- 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 <limits> +#include "mozilla/Hal.h" +#include "ConnectionWorker.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" + +namespace mozilla::dom::network { + +class ConnectionProxy final : public hal::NetworkObserver { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ConnectionProxy) + + static already_AddRefed<ConnectionProxy> Create( + WorkerPrivate* aWorkerPrivate, ConnectionWorker* aConnection) { + RefPtr<ConnectionProxy> proxy = new ConnectionProxy(aConnection); + + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + aWorkerPrivate, "ConnectionProxy", [proxy]() { proxy->Shutdown(); }); + if (NS_WARN_IF(!workerRef)) { + return nullptr; + } + + proxy->mWorkerRef = new ThreadSafeWorkerRef(workerRef); + return proxy.forget(); + } + + ThreadSafeWorkerRef* WorkerRef() const { return mWorkerRef; } + + // For IObserver - main-thread only. + void Notify(const hal::NetworkInformation& aNetworkInfo) override; + + void Shutdown(); + + void Update(ConnectionType aType, bool aIsWifi, uint32_t aDHCPGateway) { + MOZ_ASSERT(mConnection); + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + mConnection->Update(aType, aIsWifi, aDHCPGateway, true); + } + + private: + explicit ConnectionProxy(ConnectionWorker* aConnection) + : mConnection(aConnection) {} + + ~ConnectionProxy() = default; + + // Raw pointer because the ConnectionWorker keeps alive the proxy. + // This is touched only on the worker-thread and it's nullified when the + // shutdown procedure starts. + ConnectionWorker* mConnection; + + RefPtr<ThreadSafeWorkerRef> mWorkerRef; +}; + +namespace { + +// This class initializes the hal observer on the main-thread. +class InitializeRunnable : public WorkerMainThreadRunnable { + private: + // raw pointer because this is a sync runnable. + ConnectionProxy* mProxy; + hal::NetworkInformation& mNetworkInfo; + + public: + InitializeRunnable(WorkerPrivate* aWorkerPrivate, ConnectionProxy* aProxy, + hal::NetworkInformation& aNetworkInfo) + : WorkerMainThreadRunnable(aWorkerPrivate, + "ConnectionWorker :: Initialize"_ns), + mProxy(aProxy), + mNetworkInfo(aNetworkInfo) { + MOZ_ASSERT(aProxy); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + bool MainThreadRun() override { + MOZ_ASSERT(NS_IsMainThread()); + hal::RegisterNetworkObserver(mProxy); + hal::GetCurrentNetworkInformation(&mNetworkInfo); + return true; + } +}; + +// This class turns down the hal observer on the main-thread. +class ShutdownRunnable : public WorkerMainThreadRunnable { + private: + // raw pointer because this is a sync runnable. + ConnectionProxy* mProxy; + + public: + ShutdownRunnable(WorkerPrivate* aWorkerPrivate, ConnectionProxy* aProxy) + : WorkerMainThreadRunnable(aWorkerPrivate, + "ConnectionWorker :: Shutdown"_ns), + mProxy(aProxy) { + MOZ_ASSERT(aProxy); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + bool MainThreadRun() override { + MOZ_ASSERT(NS_IsMainThread()); + hal::UnregisterNetworkObserver(mProxy); + return true; + } +}; + +class NotifyRunnable final : public WorkerRunnable { + private: + RefPtr<ConnectionProxy> mProxy; + + const ConnectionType mConnectionType; + const bool mIsWifi; + const uint32_t mDHCPGateway; + + public: + NotifyRunnable(WorkerPrivate* aWorkerPrivate, ConnectionProxy* aProxy, + ConnectionType aType, bool aIsWifi, uint32_t aDHCPGateway) + : WorkerRunnable(aWorkerPrivate, "NotifyRunnable"), + mProxy(aProxy), + mConnectionType(aType), + mIsWifi(aIsWifi), + mDHCPGateway(aDHCPGateway) { + MOZ_ASSERT(aProxy); + MOZ_ASSERT(NS_IsMainThread()); + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->AssertIsOnWorkerThread(); + mProxy->Update(mConnectionType, mIsWifi, mDHCPGateway); + return true; + } +}; + +} // anonymous namespace + +/* static */ +already_AddRefed<ConnectionWorker> ConnectionWorker::Create( + WorkerPrivate* aWorkerPrivate, ErrorResult& aRv) { + bool shouldResistFingerprinting = aWorkerPrivate->ShouldResistFingerprinting( + RFPTarget::NavigatorConnection); + RefPtr<ConnectionWorker> c = new ConnectionWorker(shouldResistFingerprinting); + c->mProxy = ConnectionProxy::Create(aWorkerPrivate, c); + if (!c->mProxy) { + aRv.ThrowTypeError("The Worker thread is shutting down."); + return nullptr; + } + + hal::NetworkInformation networkInfo; + RefPtr<InitializeRunnable> runnable = + new InitializeRunnable(aWorkerPrivate, c->mProxy, networkInfo); + + runnable->Dispatch(Canceling, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + c->Update(static_cast<ConnectionType>(networkInfo.type()), + networkInfo.isWifi(), networkInfo.dhcpGateway(), false); + return c.forget(); +} + +ConnectionWorker::ConnectionWorker(bool aShouldResistFingerprinting) + : Connection(nullptr, aShouldResistFingerprinting) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); +} + +ConnectionWorker::~ConnectionWorker() { Shutdown(); } + +void ConnectionWorker::ShutdownInternal() { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + mProxy->Shutdown(); +} + +void ConnectionProxy::Notify(const hal::NetworkInformation& aNetworkInfo) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<NotifyRunnable> runnable = + new NotifyRunnable(mWorkerRef->Private(), this, + static_cast<ConnectionType>(aNetworkInfo.type()), + aNetworkInfo.isWifi(), aNetworkInfo.dhcpGateway()); + runnable->Dispatch(); +} + +void ConnectionProxy::Shutdown() { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + + // Already shut down. + if (!mConnection) { + return; + } + + mConnection = nullptr; + + RefPtr<ShutdownRunnable> runnable = + new ShutdownRunnable(mWorkerRef->Private(), this); + + ErrorResult rv; + // This runnable _must_ be executed. + runnable->Dispatch(Killing, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + + mWorkerRef = nullptr; +} + +} // namespace mozilla::dom::network diff --git a/dom/network/ConnectionWorker.h b/dom/network/ConnectionWorker.h new file mode 100644 index 0000000000..7a705e2f08 --- /dev/null +++ b/dom/network/ConnectionWorker.h @@ -0,0 +1,34 @@ +/* -*- 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 mozilla_dom_network_ConnectionWorker_h +#define mozilla_dom_network_ConnectionWorker_h + +#include "Connection.h" + +namespace mozilla::dom::network { + +class ConnectionProxy; + +class ConnectionWorker final : public Connection { + friend class ConnectionProxy; + + public: + static already_AddRefed<ConnectionWorker> Create( + WorkerPrivate* aWorkerPrivate, ErrorResult& aRv); + + private: + explicit ConnectionWorker(bool aShouldResistFingerprinting); + ~ConnectionWorker(); + + virtual void ShutdownInternal() override; + + RefPtr<ConnectionProxy> mProxy; +}; + +} // namespace mozilla::dom::network + +#endif // mozilla_dom_network_ConnectionWorker_h diff --git a/dom/network/Constants.h b/dom/network/Constants.h new file mode 100644 index 0000000000..53817bd6b7 --- /dev/null +++ b/dom/network/Constants.h @@ -0,0 +1,21 @@ +/* -*- 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 mozilla_dom_network_Constants_h__ +#define mozilla_dom_network_Constants_h__ + +/** + * A set of constants to be used by network backends. + */ +namespace mozilla::dom::network { + +static const uint32_t kDefaultType = 5; // ConnectionType::None +static const bool kDefaultIsWifi = false; +static const uint32_t kDefaultDHCPGateway = 0; + +} // namespace mozilla::dom::network + +#endif // mozilla_dom_network_Constants_h__ diff --git a/dom/network/PTCPServerSocket.ipdl b/dom/network/PTCPServerSocket.ipdl new file mode 100644 index 0000000000..8370cf945c --- /dev/null +++ b/dom/network/PTCPServerSocket.ipdl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; +include protocol PTCPSocket; + +include "mozilla/net/NeckoMessageUtils.h"; +include "mozilla/dom/network/TCPServerSocketParent.h"; +include "mozilla/dom/network/TCPServerSocketChild.h"; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +[ManualDealloc, ChildImpl="mozilla::dom::TCPServerSocketChild", ParentImpl="mozilla::dom::TCPServerSocketParent"] +protocol PTCPServerSocket +{ + manager PNecko; + +parent: + async Close(); + async RequestDelete(); + +child: + async CallbackAccept(PTCPSocket socket); + async __delete__(); +}; + +} // namespace net +} // namespace mozilla + diff --git a/dom/network/PTCPSocket.ipdl b/dom/network/PTCPSocket.ipdl new file mode 100644 index 0000000000..c255408dd7 --- /dev/null +++ b/dom/network/PTCPSocket.ipdl @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; + +include "mozilla/net/NeckoMessageUtils.h"; +include "mozilla/dom/network/TCPSocketParent.h"; +include "mozilla/dom/network/TCPSocketChild.h"; + +using struct mozilla::void_t from "mozilla/ipc/IPCCore.h"; + +struct TCPError { + nsString name; + nsString message; + nsresult errorCode; +}; + +union SendableData { + uint8_t[]; + nsCString; +}; + +union CallbackData { + void_t; + SendableData; + TCPError; +}; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +[ManualDealloc, ChildImpl="mozilla::dom::TCPSocketChild", ParentImpl="mozilla::dom::TCPSocketParent"] +protocol PTCPSocket +{ + manager PNecko; + +parent: + // Forward calling to child's open() method to parent, expect TCPOptions + // is expanded to |useSSL| (from TCPOptions.useSecureTransport) and + // |binaryType| (from TCPOption.binaryType). + async Open(nsString host, uint16_t port, bool useSSL, bool useArrayBuffers); + + async Data(SendableData data); + + // Forward calling to child's upgradeToSecure() method to parent. + async StartTLS(); + + // Forward calling to child's send() method to parent. + async Suspend(); + + // Forward calling to child's resume() method to parent. + async Resume(); + + // Forward calling to child's close() method to parent. + async Close(); + +child: + // Forward events that are dispatched by parent. + async Callback(nsString type, CallbackData data, uint32_t readyState); + + // Update child's bufferedAmount when parent's bufferedAmount is updated. + // trackingNumber is also passed back to child to ensure the bufferedAmount + // is corresponding the last call to send(). + async UpdateBufferedAmount(uint32_t bufferedAmount, uint32_t trackingNumber); + +both: + async RequestDelete(); + async __delete__(); +}; + + +} // namespace net +} // namespace mozilla + diff --git a/dom/network/PUDPSocket.ipdl b/dom/network/PUDPSocket.ipdl new file mode 100644 index 0000000000..cc5a1729f3 --- /dev/null +++ b/dom/network/PUDPSocket.ipdl @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; +include protocol PBackground; + +include IPCStream; + +include "mozilla/net/NeckoMessageUtils.h"; +include "mozilla/net/DNS.h"; +include "prio.h"; +include "mozilla/dom/network/UDPSocketParent.h"; +include "mozilla/dom/network/UDPSocketChild.h"; + +using mozilla::net::NetAddr from "mozilla/net/DNS.h"; +using struct mozilla::void_t from "mozilla/ipc/IPCCore.h"; + +struct UDPAddressInfo { + nsCString addr; + uint16_t port; +}; + +union UDPSocketAddr { + UDPAddressInfo; + NetAddr; +}; + +union UDPData { + uint8_t[]; + IPCStream; +}; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +[ManualDealloc, ChildImpl="mozilla::dom::UDPSocketChild", ParentImpl="mozilla::dom::UDPSocketParent"] +protocol PUDPSocket +{ + manager PNecko or PBackground; + +parent: + async Bind(UDPAddressInfo addressInfo, bool addressReuse, bool loopback, + uint32_t recvBufferSize, uint32_t sendBufferSize); + async Connect(UDPAddressInfo addressInfo); + + async OutgoingData(UDPData data, UDPSocketAddr addr); + + async JoinMulticast(nsCString multicastAddress, nsCString iface); + async LeaveMulticast(nsCString multicastAddress, nsCString iface); + + async Close(); + + async RequestDelete(); + +child: + async CallbackOpened(UDPAddressInfo addressInfo); + async CallbackConnected(UDPAddressInfo addressInfo); + async CallbackClosed(); + async CallbackReceivedData(UDPAddressInfo addressInfo, uint8_t[] data); + async CallbackError(nsCString message, nsCString filename, uint32_t lineNumber); + async __delete__(); +}; + + +} // namespace net +} // namespace mozilla + diff --git a/dom/network/TCPServerSocket.cpp b/dom/network/TCPServerSocket.cpp new file mode 100644 index 0000000000..b39cad0e14 --- /dev/null +++ b/dom/network/TCPServerSocket.cpp @@ -0,0 +1,172 @@ +/* -*- 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 "mozilla/dom/TCPServerSocketBinding.h" +#include "mozilla/dom/TCPServerSocketEvent.h" +#include "mozilla/dom/TCPSocketBinding.h" +#include "TCPServerSocketParent.h" +#include "TCPServerSocketChild.h" +#include "mozilla/dom/Event.h" +#include "mozilla/ErrorResult.h" +#include "TCPServerSocket.h" +#include "TCPSocket.h" +#include "nsComponentManagerUtils.h" + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_CLASS(TCPServerSocket) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(TCPServerSocket, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TCPServerSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServerSocket) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServerBridgeChild) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServerBridgeParent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TCPServerSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mServerSocket) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mServerBridgeChild) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mServerBridgeParent) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ADDREF_INHERITED(TCPServerSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(TCPServerSocket, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPServerSocket) + NS_INTERFACE_MAP_ENTRY(nsIServerSocketListener) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +TCPServerSocket::TCPServerSocket(nsIGlobalObject* aGlobal, uint16_t aPort, + bool aUseArrayBuffers, uint16_t aBacklog) + : DOMEventTargetHelper(aGlobal), + mPort(aPort), + mBacklog(aBacklog), + mUseArrayBuffers(aUseArrayBuffers) {} + +TCPServerSocket::~TCPServerSocket() = default; + +nsresult TCPServerSocket::Init() { + if (mServerSocket || mServerBridgeChild) { + NS_WARNING("Child TCPServerSocket is already listening."); + return NS_ERROR_FAILURE; + } + + if (XRE_GetProcessType() == GeckoProcessType_Content) { + mServerBridgeChild = + new TCPServerSocketChild(this, mPort, mBacklog, mUseArrayBuffers); + return NS_OK; + } + + nsresult rv; + mServerSocket = + do_CreateInstance("@mozilla.org/network/server-socket;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mServerSocket->Init(mPort, false, mBacklog); + NS_ENSURE_SUCCESS(rv, rv); + rv = mServerSocket->GetPort(&mPort); + NS_ENSURE_SUCCESS(rv, rv); + rv = mServerSocket->AsyncListen(this); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +already_AddRefed<TCPServerSocket> TCPServerSocket::Constructor( + const GlobalObject& aGlobal, uint16_t aPort, + const ServerSocketOptions& aOptions, uint16_t aBacklog, + mozilla::ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv = NS_ERROR_FAILURE; + return nullptr; + } + bool useArrayBuffers = + aOptions.mBinaryType == TCPSocketBinaryType::Arraybuffer; + RefPtr<TCPServerSocket> socket = + new TCPServerSocket(global, aPort, useArrayBuffers, aBacklog); + nsresult rv = socket->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv = NS_ERROR_FAILURE; + return nullptr; + } + return socket.forget(); +} + +uint16_t TCPServerSocket::LocalPort() const { return mPort; } + +void TCPServerSocket::Close() { + if (mServerBridgeChild) { + mServerBridgeChild->Close(); + } + if (mServerSocket) { + mServerSocket->Close(); + } +} + +void TCPServerSocket::FireEvent(const nsAString& aType, TCPSocket* aSocket) { + TCPServerSocketEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mSocket = aSocket; + + RefPtr<TCPServerSocketEvent> event = + TCPServerSocketEvent::Constructor(this, aType, init); + event->SetTrusted(true); + DispatchEvent(*event); + + if (mServerBridgeParent) { + mServerBridgeParent->OnConnect(event); + } +} + +NS_IMETHODIMP +TCPServerSocket::OnSocketAccepted(nsIServerSocket* aServer, + nsISocketTransport* aTransport) { + nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); + RefPtr<TCPSocket> socket = + TCPSocket::CreateAcceptedSocket(global, aTransport, mUseArrayBuffers); + FireEvent(u"connect"_ns, socket); + return NS_OK; +} + +NS_IMETHODIMP +TCPServerSocket::OnStopListening(nsIServerSocket* aServer, nsresult aStatus) { + if (aStatus != NS_BINDING_ABORTED) { + RefPtr<Event> event = new Event(GetOwner()); + event->InitEvent(u"error"_ns, false, false); + event->SetTrusted(true); + DispatchEvent(*event); + + NS_WARNING("Server socket was closed by unexpected reason."); + return NS_ERROR_FAILURE; + } + mServerSocket = nullptr; + return NS_OK; +} + +nsresult TCPServerSocket::AcceptChildSocket(TCPSocketChild* aSocketChild) { + nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); + NS_ENSURE_TRUE(global, NS_ERROR_FAILURE); + RefPtr<TCPSocket> socket = + TCPSocket::CreateAcceptedSocket(global, aSocketChild, mUseArrayBuffers); + NS_ENSURE_TRUE(socket, NS_ERROR_FAILURE); + FireEvent(u"connect"_ns, socket); + return NS_OK; +} + +void TCPServerSocket::SetServerBridgeParent( + TCPServerSocketParent* aBridgeParent) { + mServerBridgeParent = aBridgeParent; +} + +JSObject* TCPServerSocket::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TCPServerSocket_Binding::Wrap(aCx, this, aGivenProto); +} diff --git a/dom/network/TCPServerSocket.h b/dom/network/TCPServerSocket.h new file mode 100644 index 0000000000..550cba683e --- /dev/null +++ b/dom/network/TCPServerSocket.h @@ -0,0 +1,81 @@ +/* -*- 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 mozilla_dom_TCPServerSocket_h +#define mozilla_dom_TCPServerSocket_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "nsIServerSocket.h" + +namespace mozilla { +class ErrorResult; +namespace dom { + +struct ServerSocketOptions; +class GlobalObject; +class TCPSocket; +class TCPSocketChild; +class TCPServerSocketChild; +class TCPServerSocketParent; + +class TCPServerSocket final : public DOMEventTargetHelper, + public nsIServerSocketListener { + public: + TCPServerSocket(nsIGlobalObject* aGlobal, uint16_t aPort, + bool aUseArrayBuffers, uint16_t aBacklog); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(TCPServerSocket, + DOMEventTargetHelper) + NS_DECL_NSISERVERSOCKETLISTENER + + nsPIDOMWindowInner* GetParentObject() const { return GetOwner(); } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsresult Init(); + + uint16_t LocalPort() const; + void Close(); + + static already_AddRefed<TCPServerSocket> Constructor( + const GlobalObject& aGlobal, uint16_t aPort, + const ServerSocketOptions& aOptions, uint16_t aBacklog, + mozilla::ErrorResult& aRv); + + IMPL_EVENT_HANDLER(connect); + IMPL_EVENT_HANDLER(error); + + // Relay an accepted socket notification from the parent process and + // initialize this object with an existing child actor for the new socket. + nsresult AcceptChildSocket(TCPSocketChild* aSocketChild); + // Associate this object with an IPC actor in the parent process to relay + // notifications to content processes. + void SetServerBridgeParent(TCPServerSocketParent* aBridgeParent); + + private: + ~TCPServerSocket(); + // Dispatch a TCPServerSocketEvent event of a given type at this object. + void FireEvent(const nsAString& aType, TCPSocket* aSocket); + + // The server socket associated with this object. + nsCOMPtr<nsIServerSocket> mServerSocket; + // The IPC actor in the content process. + RefPtr<TCPServerSocketChild> mServerBridgeChild; + // The IPC actor in the parent process. + RefPtr<TCPServerSocketParent> mServerBridgeParent; + int32_t mPort; + uint16_t mBacklog; + // True if any accepted sockets should use array buffers for received + // messages. + bool mUseArrayBuffers; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TCPServerSocket_h diff --git a/dom/network/TCPServerSocketChild.cpp b/dom/network/TCPServerSocketChild.cpp new file mode 100644 index 0000000000..544c5391ee --- /dev/null +++ b/dom/network/TCPServerSocketChild.cpp @@ -0,0 +1,75 @@ +/* -*- 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 "TCPServerSocketChild.h" +#include "TCPSocketChild.h" +#include "TCPServerSocket.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/dom/PBrowserChild.h" +#include "mozilla/dom/BrowserChild.h" +#include "nsJSUtils.h" +#include "jsfriendapi.h" + +using mozilla::net::gNeckoChild; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION(TCPServerSocketChildBase, mServerSocket) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TCPServerSocketChildBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TCPServerSocketChildBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPServerSocketChildBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TCPServerSocketChildBase::TCPServerSocketChildBase() : mIPCOpen(false) {} + +TCPServerSocketChildBase::~TCPServerSocketChildBase() = default; + +NS_IMETHODIMP_(MozExternalRefCountType) TCPServerSocketChild::Release(void) { + nsrefcnt refcnt = TCPServerSocketChildBase::Release(); + if (refcnt == 1 && mIPCOpen) { + PTCPServerSocketChild::SendRequestDelete(); + return 1; + } + return refcnt; +} + +TCPServerSocketChild::TCPServerSocketChild(TCPServerSocket* aServerSocket, + uint16_t aLocalPort, + uint16_t aBacklog, + bool aUseArrayBuffers) { + mServerSocket = aServerSocket; + AddIPDLReference(); + gNeckoChild->SendPTCPServerSocketConstructor(this, aLocalPort, aBacklog, + aUseArrayBuffers); +} + +void TCPServerSocketChildBase::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + this->Release(); +} + +void TCPServerSocketChildBase::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +TCPServerSocketChild::~TCPServerSocketChild() = default; + +mozilla::ipc::IPCResult TCPServerSocketChild::RecvCallbackAccept( + PTCPSocketChild* psocket) { + RefPtr<TCPSocketChild> socket = static_cast<TCPSocketChild*>(psocket); + nsresult rv = mServerSocket->AcceptChildSocket(socket); + NS_ENSURE_SUCCESS(rv, IPC_OK()); + return IPC_OK(); +} + +void TCPServerSocketChild::Close() { SendClose(); } + +} // namespace mozilla::dom diff --git a/dom/network/TCPServerSocketChild.h b/dom/network/TCPServerSocketChild.h new file mode 100644 index 0000000000..b6773cb61b --- /dev/null +++ b/dom/network/TCPServerSocketChild.h @@ -0,0 +1,59 @@ +/* -*- 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 mozilla_dom_TCPServerSocketChild_h +#define mozilla_dom_TCPServerSocketChild_h + +#include "mozilla/net/PTCPServerSocketChild.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" + +#define TCPSERVERSOCKETCHILD_CID \ + { \ + 0x41a77ec8, 0xfd86, 0x409e, { \ + 0xae, 0xa9, 0xaf, 0x2c, 0xa4, 0x07, 0xef, 0x8e \ + } \ + } + +class nsITCPServerSocketInternal; + +namespace mozilla::dom { + +class TCPServerSocket; + +class TCPServerSocketChildBase : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTION_CLASS(TCPServerSocketChildBase) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + protected: + TCPServerSocketChildBase(); + virtual ~TCPServerSocketChildBase(); + + RefPtr<TCPServerSocket> mServerSocket; + bool mIPCOpen; +}; + +class TCPServerSocketChild : public mozilla::net::PTCPServerSocketChild, + public TCPServerSocketChildBase { + public: + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + TCPServerSocketChild(TCPServerSocket* aServerSocket, uint16_t aLocalPort, + uint16_t aBacklog, bool aUseArrayBuffers); + ~TCPServerSocketChild(); + + void Close(); + + mozilla::ipc::IPCResult RecvCallbackAccept(PTCPSocketChild* socket); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TCPServerSocketChild_h diff --git a/dom/network/TCPServerSocketParent.cpp b/dom/network/TCPServerSocketParent.cpp new file mode 100644 index 0000000000..c73ee7e0ff --- /dev/null +++ b/dom/network/TCPServerSocketParent.cpp @@ -0,0 +1,118 @@ +/* -*- 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 "TCPServerSocket.h" +#include "TCPServerSocketParent.h" +#include "nsJSUtils.h" +#include "TCPSocket.h" +#include "TCPSocketParent.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/TCPServerSocketEvent.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION(TCPServerSocketParent, mServerSocket) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TCPServerSocketParent) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TCPServerSocketParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPServerSocketParent) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +void TCPServerSocketParent::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + this->Release(); +} + +void TCPServerSocketParent::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +TCPServerSocketParent::TCPServerSocketParent(PNeckoParent* neckoParent, + uint16_t aLocalPort, + uint16_t aBacklog, + bool aUseArrayBuffers) + : mNeckoParent(neckoParent), mIPCOpen(false) { + mServerSocket = + new TCPServerSocket(nullptr, aLocalPort, aUseArrayBuffers, aBacklog); + mServerSocket->SetServerBridgeParent(this); +} + +TCPServerSocketParent::~TCPServerSocketParent() = default; + +void TCPServerSocketParent::Init() { + NS_ENSURE_SUCCESS_VOID(mServerSocket->Init()); +} + +nsresult TCPServerSocketParent::SendCallbackAccept(TCPSocketParent* socket) { + nsresult rv; + + nsString host; + rv = socket->GetHost(host); + if (NS_FAILED(rv)) { + NS_ERROR("Failed to get host from nsITCPSocketParent"); + return NS_ERROR_FAILURE; + } + + uint16_t port; + rv = socket->GetPort(&port); + if (NS_FAILED(rv)) { + NS_ERROR("Failed to get port from nsITCPSocketParent"); + return NS_ERROR_FAILURE; + } + + if (mNeckoParent) { + if (mNeckoParent->SendPTCPSocketConstructor(socket, host, port)) { + // Call |AddIPDLReference| after the consructor message is sent + // successfully, otherwise |socket| could be leaked. + socket->AddIPDLReference(); + + mozilla::Unused << PTCPServerSocketParent::SendCallbackAccept( + WrapNotNull(socket)); + } else { + NS_ERROR("Sending data from PTCPSocketParent was failed."); + } + } else { + NS_ERROR("The member value for NeckoParent is wrong."); + } + return NS_OK; +} + +mozilla::ipc::IPCResult TCPServerSocketParent::RecvClose() { + NS_ENSURE_TRUE(mServerSocket, IPC_OK()); + mServerSocket->Close(); + return IPC_OK(); +} + +void TCPServerSocketParent::ActorDestroy(ActorDestroyReason why) { + if (mServerSocket) { + mServerSocket->Close(); + mServerSocket = nullptr; + } + mNeckoParent = nullptr; +} + +mozilla::ipc::IPCResult TCPServerSocketParent::RecvRequestDelete() { + mozilla::Unused << Send__delete__(this); + return IPC_OK(); +} + +void TCPServerSocketParent::OnConnect(TCPServerSocketEvent* event) { + RefPtr<TCPSocket> socket = event->Socket(); + + RefPtr<TCPSocketParent> socketParent = new TCPSocketParent(); + socketParent->SetSocket(socket); + + socket->SetSocketBridgeParent(socketParent); + + SendCallbackAccept(socketParent); +} + +} // namespace mozilla::dom diff --git a/dom/network/TCPServerSocketParent.h b/dom/network/TCPServerSocketParent.h new file mode 100644 index 0000000000..bbebaa1860 --- /dev/null +++ b/dom/network/TCPServerSocketParent.h @@ -0,0 +1,54 @@ +/* -*- 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 mozilla_dom_TCPServerSocketParent_h +#define mozilla_dom_TCPServerSocketParent_h + +#include "mozilla/net/PNeckoParent.h" +#include "mozilla/net/PTCPServerSocketParent.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" + +namespace mozilla::dom { + +class TCPServerSocket; +class TCPServerSocketEvent; +class TCPSocketParent; + +class TCPServerSocketParent : public mozilla::net::PTCPServerSocketParent, + public nsISupports { + public: + NS_DECL_CYCLE_COLLECTION_CLASS(TCPServerSocketParent) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + TCPServerSocketParent(PNeckoParent* neckoParent, uint16_t aLocalPort, + uint16_t aBacklog, bool aUseArrayBuffers); + + void Init(); + + mozilla::ipc::IPCResult RecvClose(); + mozilla::ipc::IPCResult RecvRequestDelete(); + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + void OnConnect(TCPServerSocketEvent* event); + + private: + ~TCPServerSocketParent(); + + nsresult SendCallbackAccept(TCPSocketParent* socket); + + virtual void ActorDestroy(ActorDestroyReason why) override; + + PNeckoParent* mNeckoParent; + RefPtr<TCPServerSocket> mServerSocket; + bool mIPCOpen; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TCPServerSocketParent_h diff --git a/dom/network/TCPSocket.cpp b/dom/network/TCPSocket.cpp new file mode 100644 index 0000000000..f7ac7d3f9b --- /dev/null +++ b/dom/network/TCPSocket.cpp @@ -0,0 +1,1169 @@ +/* -*- 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 "TCPServerSocket.h" +#include "TCPSocket.h" +#include "TCPSocketChild.h" +#include "TCPSocketParent.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/TCPSocketBinding.h" +#include "mozilla/dom/TCPSocketErrorEvent.h" +#include "mozilla/dom/TCPSocketErrorEventBinding.h" +#include "mozilla/dom/TCPSocketEvent.h" +#include "mozilla/dom/TCPSocketEventBinding.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIArrayBufferInputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncStreamCopier.h" +#include "nsIBinaryInputStream.h" +#include "nsICancelable.h" +#include "nsIChannel.h" +#include "nsIInputStream.h" +#include "nsIInputStreamPump.h" +#include "nsIMultiplexInputStream.h" +#include "nsINSSErrorsService.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsIProtocolProxyService.h" +#include "nsIScriptableInputStream.h" +#include "nsISocketTransport.h" +#include "nsISocketTransportService.h" +#include "nsISupportsPrimitives.h" +#include "nsITLSSocketControl.h" +#include "nsITransport.h" +#include "nsIURIMutator.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringStream.h" +#include "secerr.h" +#include "sslerr.h" + +#define BUFFER_SIZE 65536 +#define NETWORK_STATS_THRESHOLD 65536 + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION(LegacyMozTCPSocket, mGlobal) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(LegacyMozTCPSocket) +NS_IMPL_CYCLE_COLLECTING_RELEASE(LegacyMozTCPSocket) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LegacyMozTCPSocket) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +LegacyMozTCPSocket::LegacyMozTCPSocket(nsPIDOMWindowInner* aWindow) + : mGlobal(do_QueryInterface(aWindow)) {} + +LegacyMozTCPSocket::~LegacyMozTCPSocket() = default; + +already_AddRefed<TCPSocket> LegacyMozTCPSocket::Open( + const nsAString& aHost, uint16_t aPort, const SocketOptions& aOptions, + mozilla::ErrorResult& aRv) { + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(mGlobal))) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + GlobalObject globalObj(api.cx(), mGlobal->GetGlobalJSObject()); + return TCPSocket::Constructor(globalObj, aHost, aPort, aOptions, aRv); +} + +already_AddRefed<TCPServerSocket> LegacyMozTCPSocket::Listen( + uint16_t aPort, const ServerSocketOptions& aOptions, uint16_t aBacklog, + mozilla::ErrorResult& aRv) { + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(mGlobal))) { + return nullptr; + } + GlobalObject globalObj(api.cx(), mGlobal->GetGlobalJSObject()); + return TCPServerSocket::Constructor(globalObj, aPort, aOptions, aBacklog, + aRv); +} + +bool LegacyMozTCPSocket::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector) { + return LegacyMozTCPSocket_Binding::Wrap(aCx, this, aGivenProto, aReflector); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(TCPSocket) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(TCPSocket, DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TCPSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransport) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketInputStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketOutputStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStreamPump) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStreamScriptable) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStreamBinary) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingDataAfterStartTLS) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketBridgeChild) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocketBridgeParent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TCPSocket, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransport) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketInputStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketOutputStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStreamPump) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStreamScriptable) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStreamBinary) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingDataAfterStartTLS) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketBridgeChild) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocketBridgeParent) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ADDREF_INHERITED(TCPSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(TCPSocket, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPSocket) + NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsITCPSocketCallback) + NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +TCPSocket::TCPSocket(nsIGlobalObject* aGlobal, const nsAString& aHost, + uint16_t aPort, bool aSsl, bool aUseArrayBuffers) + : DOMEventTargetHelper(aGlobal), + mReadyState(TCPReadyState::Closed), + mUseArrayBuffers(aUseArrayBuffers), + mHost(aHost), + mPort(aPort), + mSsl(aSsl), + mAsyncCopierActive(false), + mWaitingForDrain(false), + mInnerWindowID(0), + mBufferedAmount(0), + mSuspendCount(0), + mTrackingNumber(0), + mWaitingForStartTLS(false), + mObserversActive(false) { + if (aGlobal) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); + if (window) { + mInnerWindowID = window->WindowID(); + } + } +} + +TCPSocket::~TCPSocket() { + if (mObserversActive) { + nsCOMPtr<nsIObserverService> obs = + do_GetService("@mozilla.org/observer-service;1"); + if (obs) { + obs->RemoveObserver(this, "inner-window-destroyed"); + obs->RemoveObserver(this, "profile-change-net-teardown"); + } + } +} + +nsresult TCPSocket::CreateStream() { + nsresult rv = + mTransport->OpenInputStream(0, 0, 0, getter_AddRefs(mSocketInputStream)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mTransport->OpenOutputStream(nsITransport::OPEN_UNBUFFERED, 0, 0, + getter_AddRefs(mSocketOutputStream)); + NS_ENSURE_SUCCESS(rv, rv); + + // If the other side is not listening, we will + // get an onInputStreamReady callback where available + // raises to indicate the connection was refused. + nsCOMPtr<nsIAsyncInputStream> asyncStream = + do_QueryInterface(mSocketInputStream); + NS_ENSURE_TRUE(asyncStream, NS_ERROR_NOT_AVAILABLE); + + nsCOMPtr<nsISerialEventTarget> mainTarget = GetMainThreadSerialEventTarget(); + rv = asyncStream->AsyncWait(this, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, + mainTarget); + NS_ENSURE_SUCCESS(rv, rv); + + if (mUseArrayBuffers) { + mInputStreamBinary = + do_CreateInstance("@mozilla.org/binaryinputstream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mInputStreamBinary->SetInputStream(mSocketInputStream); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mInputStreamScriptable = + do_CreateInstance("@mozilla.org/scriptableinputstream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mInputStreamScriptable->Init(mSocketInputStream); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult TCPSocket::InitWithUnconnectedTransport( + nsISocketTransport* aTransport) { + mReadyState = TCPReadyState::Connecting; + mTransport = aTransport; + + MOZ_ASSERT(XRE_GetProcessType() != GeckoProcessType_Content); + + nsCOMPtr<nsISerialEventTarget> mainTarget = GetMainThreadSerialEventTarget(); + mTransport->SetEventSink(this, mainTarget); + + nsresult rv = CreateStream(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult TCPSocket::Init(nsIProxyInfo* aProxyInfo) { + nsCOMPtr<nsIObserverService> obs = + do_GetService("@mozilla.org/observer-service;1"); + if (obs) { + mObserversActive = true; + obs->AddObserver(this, "inner-window-destroyed", true); // weak reference + obs->AddObserver(this, "profile-change-net-teardown", true); // weak ref + } + + if (XRE_IsContentProcess()) { + mReadyState = TCPReadyState::Connecting; + + nsCOMPtr<nsISerialEventTarget> target; + if (nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal()) { + target = global->SerialEventTarget(); + } + mSocketBridgeChild = new TCPSocketChild(mHost, mPort, target); + mSocketBridgeChild->SendOpen(this, mSsl, mUseArrayBuffers); + return NS_OK; + } + + nsCOMPtr<nsISocketTransportService> sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + + AutoTArray<nsCString, 1> socketTypes; + if (mSsl) { + socketTypes.AppendElement("ssl"_ns); + } else { + socketTypes.AppendElement("starttls"_ns); + } + nsCOMPtr<nsISocketTransport> transport; + nsresult rv = + sts->CreateTransport(socketTypes, NS_ConvertUTF16toUTF8(mHost), mPort, + aProxyInfo, nullptr, getter_AddRefs(transport)); + NS_ENSURE_SUCCESS(rv, rv); + + return InitWithUnconnectedTransport(transport); +} + +void TCPSocket::InitWithSocketChild(TCPSocketChild* aSocketBridge) { + mSocketBridgeChild = aSocketBridge; + mReadyState = TCPReadyState::Open; + mSocketBridgeChild->SetSocket(this); + mSocketBridgeChild->GetHost(mHost); + mSocketBridgeChild->GetPort(&mPort); +} + +nsresult TCPSocket::InitWithTransport(nsISocketTransport* aTransport) { + mTransport = aTransport; + nsresult rv = CreateStream(); + NS_ENSURE_SUCCESS(rv, rv); + + mReadyState = TCPReadyState::Open; + rv = CreateInputStreamPump(); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString host; + mTransport->GetHost(host); + CopyUTF8toUTF16(host, mHost); + int32_t port; + mTransport->GetPort(&port); + mPort = port; + + return NS_OK; +} + +void TCPSocket::UpgradeToSecure(mozilla::ErrorResult& aRv) { + if (mReadyState != TCPReadyState::Open) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mSsl) { + return; + } + + mSsl = true; + + if (mSocketBridgeChild) { + mSocketBridgeChild->SendStartTLS(); + return; + } + + if (!mAsyncCopierActive) { + ActivateTLS(); + } else { + mWaitingForStartTLS = true; + } +} + +namespace { +class CopierCallbacks final : public nsIRequestObserver { + RefPtr<TCPSocket> mOwner; + + public: + explicit CopierCallbacks(TCPSocket* aSocket) : mOwner(aSocket) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + private: + ~CopierCallbacks() = default; +}; + +NS_IMPL_ISUPPORTS(CopierCallbacks, nsIRequestObserver) + +NS_IMETHODIMP +CopierCallbacks::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } + +NS_IMETHODIMP +CopierCallbacks::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + mOwner->NotifyCopyComplete(aStatus); + mOwner = nullptr; + return NS_OK; +} +} // unnamed namespace + +void TCPSocket::CalculateBufferedAmount() { + // Let's update the buffered amount of data. + uint64_t bufferedAmount = 0; + for (uint32_t i = 0, len = mPendingData.Length(); i < len; ++i) { + nsCOMPtr<nsIInputStream> stream = mPendingData[i]; + uint64_t available = 0; + if (NS_SUCCEEDED(stream->Available(&available))) { + bufferedAmount += available; + } + } + mBufferedAmount = bufferedAmount; +} + +nsresult TCPSocket::EnsureCopying() { + if (mAsyncCopierActive) { + return NS_OK; + } + + mAsyncCopierActive = true; + + nsresult rv; + + nsCOMPtr<nsIMultiplexInputStream> multiplexStream = + do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIInputStream> stream = do_QueryInterface(multiplexStream); + + while (!mPendingData.IsEmpty()) { + nsCOMPtr<nsIInputStream> stream = mPendingData[0]; + multiplexStream->AppendStream(stream); + mPendingData.RemoveElementAt(0); + } + + nsCOMPtr<nsIAsyncStreamCopier> copier = + do_CreateInstance("@mozilla.org/network/async-stream-copier;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISocketTransportService> sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + + nsCOMPtr<nsISerialEventTarget> target = do_QueryInterface(sts); + rv = copier->Init(stream, mSocketOutputStream, target, + true, /* source buffered */ + false, /* sink buffered */ + BUFFER_SIZE, false, /* close source */ + false); /* close sink */ + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<CopierCallbacks> callbacks = new CopierCallbacks(this); + rv = copier->AsyncCopy(callbacks, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void TCPSocket::NotifyCopyComplete(nsresult aStatus) { + mAsyncCopierActive = false; + CalculateBufferedAmount(); + + if (mSocketBridgeParent && mSocketBridgeParent->IPCOpen()) { + mozilla::Unused << mSocketBridgeParent->SendUpdateBufferedAmount( + BufferedAmount(), mTrackingNumber); + } + + if (NS_FAILED(aStatus)) { + MaybeReportErrorAndCloseIfOpen(aStatus); + return; + } + + if (BufferedAmount() != 0) { + EnsureCopying(); + return; + } + + // Maybe we have some empty stream. We want to have an empty queue now. + mPendingData.Clear(); + + // If we are waiting for initiating starttls, we can begin to + // activate tls now. + if (mWaitingForStartTLS && mReadyState == TCPReadyState::Open) { + ActivateTLS(); + mWaitingForStartTLS = false; + // If we have pending data, we should send them, or fire + // a drain event if we are waiting for it. + if (!mPendingDataAfterStartTLS.IsEmpty()) { + mPendingData = std::move(mPendingDataAfterStartTLS); + EnsureCopying(); + return; + } + } + + // If we have a connected child, we let the child decide whether + // ondrain should be dispatched. + if (mWaitingForDrain && !mSocketBridgeParent) { + mWaitingForDrain = false; + FireEvent(u"drain"_ns); + } + + if (mReadyState == TCPReadyState::Closing) { + if (mSocketOutputStream) { + mSocketOutputStream->Close(); + mSocketOutputStream = nullptr; + } + mReadyState = TCPReadyState::Closed; + FireEvent(u"close"_ns); + } +} + +void TCPSocket::ActivateTLS() { + nsresult rv; + nsCOMPtr<nsIEventTarget> socketThread = + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return; + } + + bool alreadyOnSTST = false; + if (NS_FAILED(socketThread->IsOnCurrentThread(&alreadyOnSTST))) { + return; + } + + if (alreadyOnSTST) { + ActivateTLSHelper(); + return; + } + + auto CallActivateTLS = [sock = RefPtr{this}]() mutable { + sock->ActivateTLSHelper(); + }; + mozilla::SyncRunnable::DispatchToThread( + socketThread, + NS_NewRunnableFunction("TCPSocket::UpgradeToSecure->ActivateTLSHelper", + CallActivateTLS)); +} + +void TCPSocket::ActivateTLSHelper() { + nsCOMPtr<nsITLSSocketControl> tlsSocketControl; + mTransport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl)); + if (tlsSocketControl) { + tlsSocketControl->StartTLS(); + } +} + +NS_IMETHODIMP +TCPSocket::FireErrorEvent(const nsAString& aName, const nsAString& aType, + nsresult aErrorCode) { + if (mSocketBridgeParent) { + mSocketBridgeParent->FireErrorEvent(aName, aType, aErrorCode, mReadyState); + return NS_OK; + } + + TCPSocketErrorEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mName = aName; + init.mMessage = aType; + static_assert(std::is_same_v<std::underlying_type_t<nsresult>, uint32_t>); + init.mErrorCode = uint32_t(aErrorCode); + + RefPtr<TCPSocketErrorEvent> event = + TCPSocketErrorEvent::Constructor(this, u"error"_ns, init); + MOZ_ASSERT(event); + event->SetTrusted(true); + DispatchEvent(*event); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::FireEvent(const nsAString& aType) { + if (mSocketBridgeParent) { + mSocketBridgeParent->FireEvent(aType, mReadyState); + return NS_OK; + } + + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + JS::Rooted<JS::Value> val(api.cx()); + return FireDataEvent(api.cx(), aType, val); +} + +NS_IMETHODIMP +TCPSocket::FireDataArrayEvent(const nsAString& aType, + const nsTArray<uint8_t>& buffer) { + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + JS::Rooted<JS::Value> val(cx); + + bool ok = IPC::DeserializeArrayBuffer(cx, buffer, &val); + if (ok) { + return FireDataEvent(cx, aType, val); + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TCPSocket::FireDataStringEvent(const nsAString& aType, + const nsACString& aString) { + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(GetOwnerGlobal()))) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + JS::Rooted<JS::Value> val(cx); + + bool ok = ToJSValue(cx, NS_ConvertASCIItoUTF16(aString), &val); + if (ok) { + return FireDataEvent(cx, aType, val); + } + return NS_ERROR_FAILURE; +} + +nsresult TCPSocket::FireDataEvent(JSContext* aCx, const nsAString& aType, + JS::Handle<JS::Value> aData) { + MOZ_ASSERT(!mSocketBridgeParent); + + RootedDictionary<TCPSocketEventInit> init(aCx); + init.mBubbles = false; + init.mCancelable = false; + init.mData = aData; + + RefPtr<TCPSocketEvent> event = TCPSocketEvent::Constructor(this, aType, init); + event->SetTrusted(true); + DispatchEvent(*event); + return NS_OK; +} + +JSObject* TCPSocket::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TCPSocket_Binding::Wrap(aCx, this, aGivenProto); +} + +void TCPSocket::GetHost(nsAString& aHost) { aHost.Assign(mHost); } + +uint32_t TCPSocket::Port() const { return mPort; } + +bool TCPSocket::Ssl() const { return mSsl; } + +void TCPSocket::Suspend() { + if (mSocketBridgeChild) { + mSocketBridgeChild->SendSuspend(); + return; + } + if (mInputStreamPump) { + mInputStreamPump->Suspend(); + } + mSuspendCount++; +} + +void TCPSocket::Resume(mozilla::ErrorResult& aRv) { + if (mSocketBridgeChild) { + mSocketBridgeChild->SendResume(); + return; + } + if (!mSuspendCount) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mInputStreamPump) { + mInputStreamPump->Resume(); + } + mSuspendCount--; +} + +nsresult TCPSocket::MaybeReportErrorAndCloseIfOpen(nsresult status) { + // If we're closed, we've already reported the error or just don't need to + // report the error. + if (mReadyState == TCPReadyState::Closed) { + return NS_OK; + } + + // go through ::Closing state and then mark ::Closed + Close(); + mReadyState = TCPReadyState::Closed; + + if (NS_FAILED(status)) { + // Convert the status code to an appropriate error message. + + nsString errorType, errName; + + // security module? (and this is an error) + if ((static_cast<uint32_t>(status) & 0xFF0000) == 0x5a0000) { + nsCOMPtr<nsINSSErrorsService> errSvc = + do_GetService("@mozilla.org/nss_errors_service;1"); + // getErrorClass will throw a generic NS_ERROR_FAILURE if the error code + // is somehow not in the set of covered errors. + uint32_t errorClass; + nsresult rv = errSvc->GetErrorClass(status, &errorClass); + if (NS_FAILED(rv)) { + errorType.AssignLiteral("SecurityProtocol"); + } else { + switch (errorClass) { + case nsINSSErrorsService::ERROR_CLASS_BAD_CERT: + errorType.AssignLiteral("SecurityCertificate"); + break; + default: + errorType.AssignLiteral("SecurityProtocol"); + break; + } + } + + // NSS_SEC errors (happen below the base value because of negative vals) + if ((static_cast<int32_t>(status) & 0xFFFF) < + abs(nsINSSErrorsService::NSS_SEC_ERROR_BASE)) { + switch (static_cast<SECErrorCodes>(status)) { + case SEC_ERROR_EXPIRED_CERTIFICATE: + errName.AssignLiteral("SecurityExpiredCertificateError"); + break; + case SEC_ERROR_REVOKED_CERTIFICATE: + errName.AssignLiteral("SecurityRevokedCertificateError"); + break; + // per bsmith, we will be unable to tell these errors apart very + // soon, so it makes sense to just folder them all together already. + case SEC_ERROR_UNKNOWN_ISSUER: + case SEC_ERROR_UNTRUSTED_ISSUER: + case SEC_ERROR_UNTRUSTED_CERT: + case SEC_ERROR_CA_CERT_INVALID: + errName.AssignLiteral("SecurityUntrustedCertificateIssuerError"); + break; + case SEC_ERROR_INADEQUATE_KEY_USAGE: + errName.AssignLiteral("SecurityInadequateKeyUsageError"); + break; + case SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED: + errName.AssignLiteral( + "SecurityCertificateSignatureAlgorithmDisabledError"); + break; + default: + errName.AssignLiteral("SecurityError"); + break; + } + } else { + // NSS_SSL errors + switch (static_cast<SSLErrorCodes>(status)) { + case SSL_ERROR_NO_CERTIFICATE: + errName.AssignLiteral("SecurityNoCertificateError"); + break; + case SSL_ERROR_BAD_CERTIFICATE: + errName.AssignLiteral("SecurityBadCertificateError"); + break; + case SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE: + errName.AssignLiteral("SecurityUnsupportedCertificateTypeError"); + break; + case SSL_ERROR_UNSUPPORTED_VERSION: + errName.AssignLiteral("SecurityUnsupportedTLSVersionError"); + break; + case SSL_ERROR_BAD_CERT_DOMAIN: + errName.AssignLiteral("SecurityCertificateDomainMismatchError"); + break; + default: + errName.AssignLiteral("SecurityError"); + break; + } + } + } else { + // must be network + errorType.AssignLiteral("Network"); + + switch (status) { + // connect to host:port failed + case NS_ERROR_CONNECTION_REFUSED: + errName.AssignLiteral("ConnectionRefusedError"); + break; + // network timeout error + case NS_ERROR_NET_TIMEOUT: + errName.AssignLiteral("NetworkTimeoutError"); + break; + // hostname lookup failed + case NS_ERROR_UNKNOWN_HOST: + errName.AssignLiteral("DomainNotFoundError"); + break; + case NS_ERROR_NET_INTERRUPT: + errName.AssignLiteral("NetworkInterruptError"); + break; + default: + errName.AssignLiteral("NetworkError"); + break; + } + } + + Unused << NS_WARN_IF(NS_FAILED(FireErrorEvent(errName, errorType, status))); + } + + return FireEvent(u"close"_ns); +} + +void TCPSocket::Close() { CloseHelper(true); } + +void TCPSocket::CloseImmediately() { CloseHelper(false); } + +void TCPSocket::CloseHelper(bool waitForUnsentData) { + if (mReadyState == TCPReadyState::Closed || + mReadyState == TCPReadyState::Closing) { + return; + } + + mReadyState = TCPReadyState::Closing; + + if (mProxyRequest) { + mProxyRequest->Cancel(NS_BINDING_ABORTED); + mProxyRequest = nullptr; + } + + if (mSocketBridgeChild) { + mSocketBridgeChild->SendClose(); + return; + } + + if (!mAsyncCopierActive || !waitForUnsentData) { + mPendingData.Clear(); + mPendingDataAfterStartTLS.Clear(); + + if (mSocketOutputStream) { + mSocketOutputStream->Close(); + mSocketOutputStream = nullptr; + } + } + + if (mSocketInputStream) { + mSocketInputStream->Close(); + mSocketInputStream = nullptr; + } +} + +bool TCPSocket::Send(const nsACString& aData, mozilla::ErrorResult& aRv) { + if (mReadyState != TCPReadyState::Open) { + aRv.Throw(NS_ERROR_FAILURE); + return false; + } + + uint64_t byteLength; + nsCOMPtr<nsIInputStream> stream; + if (mSocketBridgeChild) { + mSocketBridgeChild->SendSend(aData); + byteLength = aData.Length(); + } else { + nsresult rv = NS_NewCStringInputStream(getter_AddRefs(stream), aData); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + rv = stream->Available(&byteLength); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + } + return Send(stream, byteLength); +} + +bool TCPSocket::Send(const ArrayBuffer& aData, uint32_t aByteOffset, + const Optional<uint32_t>& aByteLength, + mozilla::ErrorResult& aRv) { + if (mReadyState != TCPReadyState::Open) { + aRv.Throw(NS_ERROR_FAILURE); + return false; + } + + nsCOMPtr<nsIArrayBufferInputStream> stream; + + uint32_t nbytes; + auto calculateOffsetAndCount = [&](uint32_t aLength) { + uint32_t offset = std::min(aLength, aByteOffset); + nbytes = std::min(aLength - aByteOffset, + aByteLength.WasPassed() ? aByteLength.Value() : aLength); + return std::pair(offset, nbytes); + }; + + if (mSocketBridgeChild) { + nsTArray<uint8_t> arrayBuffer; + if (!aData.AppendDataTo(arrayBuffer, calculateOffsetAndCount)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + + mSocketBridgeChild->SendSend(std::move(arrayBuffer)); + } else { + mozilla::Maybe<mozilla::UniquePtr<uint8_t[]>> arrayBuffer = + aData.CreateFromData<mozilla::UniquePtr<uint8_t[]>>( + calculateOffsetAndCount); + if (arrayBuffer.isNothing()) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + + stream = do_CreateInstance("@mozilla.org/io/arraybuffer-input-stream;1"); + nsresult rv = stream->SetData(arrayBuffer.extract(), nbytes); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return false; + } + } + return Send(stream, nbytes); +} + +bool TCPSocket::Send(nsIInputStream* aStream, uint32_t aByteLength) { + uint64_t newBufferedAmount = BufferedAmount() + aByteLength; + bool bufferFull = newBufferedAmount > BUFFER_SIZE; + if (bufferFull) { + // If we buffered more than some arbitrary amount of data, + // (65535 right now) we should tell the caller so they can + // wait until ondrain is called if they so desire. Once all the + // buffered data has been written to the socket, ondrain is + // called. + mWaitingForDrain = true; + } + + if (mSocketBridgeChild) { + // In the child, we just add the buffer length to our bufferedAmount and let + // the parent update our bufferedAmount when the data have been sent. + mBufferedAmount = newBufferedAmount; + return !bufferFull; + } + + // This is used to track how many packets we have been told to send. Signaling + // this back to the user of this API allows the user to know how many packets + // are currently in flight over IPC. + ++mTrackingNumber; + if (mWaitingForStartTLS) { + // When we are waiting for starttls, newStream is added to pendingData + // and will be appended to multiplexStream after tls had been set up. + mPendingDataAfterStartTLS.AppendElement(aStream); + } else { + mPendingData.AppendElement(aStream); + } + + CalculateBufferedAmount(); + EnsureCopying(); + + return !bufferFull; +} + +TCPReadyState TCPSocket::ReadyState() { return mReadyState; } + +TCPSocketBinaryType TCPSocket::BinaryType() const { + if (mUseArrayBuffers) { + return TCPSocketBinaryType::Arraybuffer; + } + return TCPSocketBinaryType::String; +} + +already_AddRefed<TCPSocket> TCPSocket::CreateAcceptedSocket( + nsIGlobalObject* aGlobal, nsISocketTransport* aTransport, + bool aUseArrayBuffers) { + RefPtr<TCPSocket> socket = + new TCPSocket(aGlobal, u""_ns, 0, false, aUseArrayBuffers); + nsresult rv = socket->InitWithTransport(aTransport); + NS_ENSURE_SUCCESS(rv, nullptr); + return socket.forget(); +} + +already_AddRefed<TCPSocket> TCPSocket::CreateAcceptedSocket( + nsIGlobalObject* aGlobal, TCPSocketChild* aBridge, bool aUseArrayBuffers) { + RefPtr<TCPSocket> socket = + new TCPSocket(aGlobal, u""_ns, 0, false, aUseArrayBuffers); + socket->InitWithSocketChild(aBridge); + return socket.forget(); +} + +already_AddRefed<TCPSocket> TCPSocket::Constructor( + const GlobalObject& aGlobal, const nsAString& aHost, uint16_t aPort, + const SocketOptions& aOptions, mozilla::ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<TCPSocket> socket = + new TCPSocket(global, aHost, aPort, aOptions.mUseSecureTransport, + aOptions.mBinaryType == TCPSocketBinaryType::Arraybuffer); + socket->ResolveProxy(); + + return socket.forget(); +} + +nsresult TCPSocket::ResolveProxy() { + nsresult rv; + nsCOMPtr<nsIProtocolProxyService> pps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIURI> uri; + nsCString spec = mSsl ? "https://"_ns : "http://"_ns; + bool maybeIPv6 = mHost.FindChar(':') != -1; + if (maybeIPv6) spec.Append('['); + if (!AppendUTF16toUTF8(mHost, spec, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + if (maybeIPv6) spec.Append(']'); + rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(spec) + .SetPort(mPort) + .Finalize(uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), uri, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = pps->AsyncResolve(channel, + nsIProtocolProxyService::RESOLVE_PREFER_SOCKS_PROXY, + this, nullptr, getter_AddRefs(mProxyRequest)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnProxyAvailable(nsICancelable* aRequest, nsIChannel* aChannel, + nsIProxyInfo* aProxyInfo, nsresult aResult) { + mProxyRequest = nullptr; + if (NS_SUCCEEDED(aResult) && aProxyInfo) { + nsCString proxyType; + nsresult rv = aProxyInfo->GetType(proxyType); + if (NS_WARN_IF(NS_FAILED(rv))) { + Close(); + return rv; + } + // Only supports SOCKS proxy for now. + if (proxyType == "socks" || proxyType == "socks4") { + return Init(aProxyInfo); + } + } + return Init(nullptr); +} + +nsresult TCPSocket::CreateInputStreamPump() { + if (!mSocketInputStream) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv; + mInputStreamPump = + do_CreateInstance("@mozilla.org/network/input-stream-pump;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mInputStreamPump->Init(mSocketInputStream, 0, 0, false, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + uint64_t suspendCount = mSuspendCount; + while (suspendCount--) { + mInputStreamPump->Suspend(); + } + + rv = mInputStreamPump->AsyncRead(this); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnTransportStatus(nsITransport* aTransport, nsresult aStatus, + int64_t aProgress, int64_t aProgressMax) { + if (static_cast<uint32_t>(aStatus) != + static_cast<uint32_t>(nsISocketTransport::STATUS_CONNECTED_TO)) { + return NS_OK; + } + + mReadyState = TCPReadyState::Open; + nsresult rv = CreateInputStreamPump(); + NS_ENSURE_SUCCESS(rv, rv); + FireEvent(u"open"_ns); + + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnInputStreamReady(nsIAsyncInputStream* aStream) { + // Only used for detecting if the connection was refused. + + uint64_t dummy; + nsresult rv = aStream->Available(&dummy); + if (NS_FAILED(rv)) { + MaybeReportErrorAndCloseIfOpen(NS_ERROR_CONNECTION_REFUSED); + } + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } + +NS_IMETHODIMP +TCPSocket::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + if (mUseArrayBuffers) { + nsTArray<uint8_t> buffer; + buffer.SetCapacity(aCount); + uint32_t actual; + nsresult rv = aStream->Read(reinterpret_cast<char*>(buffer.Elements()), + aCount, &actual); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(actual == aCount); + buffer.SetLength(actual); + + if (mSocketBridgeParent) { + mSocketBridgeParent->FireArrayBufferDataEvent(buffer, mReadyState); + return NS_OK; + } + + AutoJSAPI api; + if (!api.Init(GetOwnerGlobal())) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + + JS::Rooted<JS::Value> value(cx); + if (!ToJSValue(cx, TypedArrayCreator<ArrayBuffer>(buffer), &value)) { + return NS_ERROR_FAILURE; + } + FireDataEvent(cx, u"data"_ns, value); + return NS_OK; + } + + nsCString data; + nsresult rv = mInputStreamScriptable->ReadBytes(aCount, data); + NS_ENSURE_SUCCESS(rv, rv); + + if (mSocketBridgeParent) { + mSocketBridgeParent->FireStringDataEvent(data, mReadyState); + return NS_OK; + } + + AutoJSAPI api; + if (!api.Init(GetOwnerGlobal())) { + return NS_ERROR_FAILURE; + } + JSContext* cx = api.cx(); + + JS::Rooted<JS::Value> value(cx); + if (!ToJSValue(cx, NS_ConvertASCIItoUTF16(data), &value)) { + return NS_ERROR_FAILURE; + } + FireDataEvent(cx, u"data"_ns, value); + + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + mInputStreamPump = nullptr; + + if (mAsyncCopierActive && NS_SUCCEEDED(aStatus)) { + // If we have some buffered output still, and status is not an + // error, the other side has done a half-close, but we don't + // want to be in the close state until we are done sending + // everything that was buffered. We also don't want to call onclose + // yet. + return NS_OK; + } + + // We call this even if there is no error. + MaybeReportErrorAndCloseIfOpen(aStatus); + return NS_OK; +} + +void TCPSocket::SetSocketBridgeParent(TCPSocketParent* aBridgeParent) { + MOZ_ASSERT(NS_IsMainThread()); + + mSocketBridgeParent = aBridgeParent; +} + +NS_IMETHODIMP +TCPSocket::UpdateReadyState(uint32_t aReadyState) { + MOZ_ASSERT(mSocketBridgeChild); + mReadyState = static_cast<TCPReadyState>(aReadyState); + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::UpdateBufferedAmount(uint32_t aBufferedAmount, + uint32_t aTrackingNumber) { + if (aTrackingNumber != mTrackingNumber) { + return NS_OK; + } + mBufferedAmount = aBufferedAmount; + if (!mBufferedAmount) { + if (mWaitingForDrain) { + mWaitingForDrain = false; + return FireEvent(u"drain"_ns); + } + } + return NS_OK; +} + +NS_IMETHODIMP +TCPSocket::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "inner-window-destroyed")) { + nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + uint64_t innerID; + nsresult rv = wrapper->GetData(&innerID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (innerID == mInnerWindowID) { + Close(); + } + } else if (!strcmp(aTopic, "profile-change-net-teardown")) { + Close(); + } + + return NS_OK; +} + +/* static */ +bool TCPSocket::ShouldTCPSocketExist(JSContext* aCx, JSObject* aGlobal) { + JS::Rooted<JSObject*> global(aCx, aGlobal); + return nsContentUtils::ObjectPrincipal(global)->IsSystemPrincipal(); +} diff --git a/dom/network/TCPSocket.h b/dom/network/TCPSocket.h new file mode 100644 index 0000000000..a6d807ef54 --- /dev/null +++ b/dom/network/TCPSocket.h @@ -0,0 +1,247 @@ +/* -*- 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 mozilla_dom_TCPSocket_h +#define mozilla_dom_TCPSocket_h + +#include "mozilla/dom/TCPSocketBinding.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "nsIProxyInfo.h" +#include "nsITransport.h" +#include "nsIStreamListener.h" +#include "nsIAsyncInputStream.h" +#include "nsISupportsImpl.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nsITCPSocketCallback.h" +#include "nsIProtocolProxyCallback.h" +#include "js/RootingAPI.h" + +class nsISocketTransport; +class nsIInputStreamPump; +class nsIScriptableInputStream; +class nsIBinaryInputStream; +class nsIMultiplexInputStream; +class nsIAsyncStreamCopier; +class nsIInputStream; +class nsINetworkInfo; + +namespace mozilla { +class ErrorResult; +namespace dom { + +struct ServerSocketOptions; +class TCPServerSocket; +class TCPSocketChild; +class TCPSocketParent; + +// This interface is only used for legacy navigator.mozTCPSocket API +// compatibility. +class LegacyMozTCPSocket : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(LegacyMozTCPSocket) + + explicit LegacyMozTCPSocket(nsPIDOMWindowInner* aWindow); + + already_AddRefed<TCPServerSocket> Listen(uint16_t aPort, + const ServerSocketOptions& aOptions, + uint16_t aBacklog, ErrorResult& aRv); + + already_AddRefed<TCPSocket> Open(const nsAString& aHost, uint16_t aPort, + const SocketOptions& aOptions, + ErrorResult& aRv); + + bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector); + + private: + virtual ~LegacyMozTCPSocket(); + + nsCOMPtr<nsIGlobalObject> mGlobal; +}; + +class TCPSocket final : public DOMEventTargetHelper, + public nsIStreamListener, + public nsITransportEventSink, + public nsIInputStreamCallback, + public nsIObserver, + public nsSupportsWeakReference, + public nsITCPSocketCallback, + public nsIProtocolProxyCallback { + public: + TCPSocket(nsIGlobalObject* aGlobal, const nsAString& aHost, uint16_t aPort, + bool aSsl, bool aUseArrayBuffers); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(TCPSocket, + DOMEventTargetHelper) + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOBSERVER + NS_DECL_NSITCPSOCKETCALLBACK + NS_DECL_NSIPROTOCOLPROXYCALLBACK + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static bool ShouldTCPSocketExist(JSContext* aCx, JSObject* aGlobal); + + nsISocketTransport* GetTransport() const { return mTransport.get(); } + + void GetHost(nsAString& aHost); + uint32_t Port() const; + bool Ssl() const; + uint64_t BufferedAmount() const { return mBufferedAmount; } + void Suspend(); + void Resume(ErrorResult& aRv); + void Close(); + void CloseImmediately(); + bool Send(const nsACString& aData, ErrorResult& aRv); + bool Send(const ArrayBuffer& aData, uint32_t aByteOffset, + const Optional<uint32_t>& aByteLength, ErrorResult& aRv); + TCPReadyState ReadyState(); + TCPSocketBinaryType BinaryType() const; + void UpgradeToSecure(ErrorResult& aRv); + + static already_AddRefed<TCPSocket> Constructor(const GlobalObject& aGlobal, + const nsAString& aHost, + uint16_t aPort, + const SocketOptions& aOptions, + ErrorResult& aRv); + + // Create a TCPSocket object from an existing low-level socket connection. + // Used by the TCPServerSocket implementation when a new connection is + // accepted. + static already_AddRefed<TCPSocket> CreateAcceptedSocket( + nsIGlobalObject* aGlobal, nsISocketTransport* aTransport, + bool aUseArrayBuffers); + // Create a TCPSocket object from an existing child-side IPC actor. + // Used by the TCPServerSocketChild implementation when a new connection is + // accepted. + static already_AddRefed<TCPSocket> CreateAcceptedSocket( + nsIGlobalObject* aGlobal, TCPSocketChild* aBridge, bool aUseArrayBuffers); + + // Initialize this socket's associated IPC actor in the parent process. + void SetSocketBridgeParent(TCPSocketParent* aBridgeParent); + + static bool SocketEnabled(); + + IMPL_EVENT_HANDLER(open); + IMPL_EVENT_HANDLER(drain); + IMPL_EVENT_HANDLER(data); + IMPL_EVENT_HANDLER(error); + IMPL_EVENT_HANDLER(close); + + nsresult Init(nsIProxyInfo* aProxyInfo); + + // Inform this socket that a buffered send() has completed sending. + void NotifyCopyComplete(nsresult aStatus); + + // Initialize this socket from a low-level connection that hasn't connected + // yet (called from RecvOpenBind() in TCPSocketParent). + nsresult InitWithUnconnectedTransport(nsISocketTransport* aTransport); + + private: + ~TCPSocket(); + + // Initialize this socket with an existing IPC actor. + void InitWithSocketChild(TCPSocketChild* aSocketBridge); + // Initialize this socket from an existing low-level connection. + nsresult InitWithTransport(nsISocketTransport* aTransport); + // Initialize the input/output streams for this socket object. + nsresult CreateStream(); + // Initialize the asynchronous read operation from this socket's input stream. + nsresult CreateInputStreamPump(); + // Send the contents of the provided input stream, which is assumed to be the + // given length for reporting and buffering purposes. + bool Send(nsIInputStream* aStream, uint32_t aByteLength); + // Begin an asynchronous copy operation if one is not already in progress. + nsresult EnsureCopying(); + // Re-calculate buffered amount. + void CalculateBufferedAmount(); + // Helper function, should be called by ActivateTLS(), only. + void ActivateTLSHelper(); + // Enable TLS on this socket, dispatch to STSThread if necessary. + void ActivateTLS(); + // Dispatch an error event if necessary, then dispatch a "close" event. + nsresult MaybeReportErrorAndCloseIfOpen(nsresult status); + + // Helper for FireDataStringEvent/FireDataArrayEvent. + nsresult FireDataEvent(JSContext* aCx, const nsAString& aType, + JS::Handle<JS::Value> aData); + // Helper for Close/CloseImmediately + void CloseHelper(bool waitForUnsentData); + + nsresult ResolveProxy(); + + TCPReadyState mReadyState; + // Whether to use strings or array buffers for the "data" event. + bool mUseArrayBuffers; + nsString mHost; + uint16_t mPort; + // Whether this socket is using a secure transport. + bool mSsl; + + // The associated IPC actor in a child process. + RefPtr<TCPSocketChild> mSocketBridgeChild; + // The associated IPC actor in a parent process. + RefPtr<TCPSocketParent> mSocketBridgeParent; + + // Raw socket streams + nsCOMPtr<nsISocketTransport> mTransport; + nsCOMPtr<nsIInputStream> mSocketInputStream; + nsCOMPtr<nsIOutputStream> mSocketOutputStream; + + nsCOMPtr<nsICancelable> mProxyRequest; + + // Input stream machinery + nsCOMPtr<nsIInputStreamPump> mInputStreamPump; + nsCOMPtr<nsIScriptableInputStream> mInputStreamScriptable; + nsCOMPtr<nsIBinaryInputStream> mInputStreamBinary; + + // Is there an async copy operation in progress? + bool mAsyncCopierActive; + // True if the buffer is full and a "drain" event is expected by the client. + bool mWaitingForDrain; + + // The id of the window that created this socket. + uint64_t mInnerWindowID; + + // The current number of buffered bytes. Only used in content processes when + // IPC is enabled. + uint64_t mBufferedAmount; + + // The number of times this socket has had `Suspend` called without a + // corresponding `Resume`. + uint32_t mSuspendCount; + + // The current sequence number (ie. number of send operations) that have been + // processed. This is used in the IPC scenario by the child process to filter + // out outdated notifications about the amount of buffered data present in the + // parent process. + uint32_t mTrackingNumber; + + // True if this socket has been upgraded to secure after the initial + // connection, but the actual upgrade is waiting for an in-progress copy + // operation to complete. + bool mWaitingForStartTLS; + // The buffered data awaiting the TLS upgrade to finish. + nsTArray<nsCOMPtr<nsIInputStream>> mPendingDataAfterStartTLS; + + // The data to be sent. + nsTArray<nsCOMPtr<nsIInputStream>> mPendingData; + + bool mObserversActive; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TCPSocket_h diff --git a/dom/network/TCPSocketChild.cpp b/dom/network/TCPSocketChild.cpp new file mode 100644 index 0000000000..2420171df3 --- /dev/null +++ b/dom/network/TCPSocketChild.cpp @@ -0,0 +1,163 @@ +/* -*- 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 <algorithm> +#include "TCPSocketChild.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/Unused.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/dom/PBrowserChild.h" +#include "mozilla/dom/BrowserChild.h" +#include "nsITCPSocketCallback.h" +#include "TCPSocket.h" +#include "nsContentUtils.h" +#include "js/ArrayBuffer.h" // JS::NewArrayBufferWithContents +#include "js/RootingAPI.h" // JS::MutableHandle +#include "js/Utility.h" // js::ArrayBufferContentsArena, JS::FreePolicy, js_pod_arena_malloc +#include "js/Value.h" // JS::Value + +using mozilla::net::gNeckoChild; + +namespace IPC { + +bool DeserializeArrayBuffer(JSContext* cx, const nsTArray<uint8_t>& aBuffer, + JS::MutableHandle<JS::Value> aVal) { + mozilla::UniquePtr<uint8_t[], JS::FreePolicy> data( + js_pod_arena_malloc<uint8_t>(js::ArrayBufferContentsArena, + aBuffer.Length())); + if (!data) return false; + memcpy(data.get(), aBuffer.Elements(), aBuffer.Length()); + + JSObject* obj = + JS::NewArrayBufferWithContents(cx, aBuffer.Length(), std::move(data)); + if (!obj) return false; + + aVal.setObject(*obj); + return true; +} + +} // namespace IPC + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(TCPSocketChildBase) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TCPSocketChildBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSocket) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TCPSocketChildBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSocket) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(TCPSocketChildBase) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TCPSocketChildBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TCPSocketChildBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPSocketChildBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TCPSocketChildBase::TCPSocketChildBase() : mIPCOpen(false) { + mozilla::HoldJSObjects(this); +} + +TCPSocketChildBase::~TCPSocketChildBase() { mozilla::DropJSObjects(this); } + +NS_IMETHODIMP_(MozExternalRefCountType) TCPSocketChild::Release(void) { + nsrefcnt refcnt = TCPSocketChildBase::Release(); + if (refcnt == 1 && mIPCOpen) { + PTCPSocketChild::SendRequestDelete(); + return 1; + } + return refcnt; +} + +TCPSocketChild::TCPSocketChild(const nsAString& aHost, const uint16_t& aPort, + nsISerialEventTarget* aTarget) + : mHost(aHost), mPort(aPort), mIPCEventTarget(aTarget) {} + +void TCPSocketChild::SendOpen(nsITCPSocketCallback* aSocket, bool aUseSSL, + bool aUseArrayBuffers) { + mSocket = aSocket; + + AddIPDLReference(); + gNeckoChild->SendPTCPSocketConstructor(this, mHost, mPort); + PTCPSocketChild::SendOpen(mHost, mPort, aUseSSL, aUseArrayBuffers); +} + +void TCPSocketChildBase::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + mSocket = nullptr; + this->Release(); +} + +void TCPSocketChildBase::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +TCPSocketChild::~TCPSocketChild() = default; + +mozilla::ipc::IPCResult TCPSocketChild::RecvUpdateBufferedAmount( + const uint32_t& aBuffered, const uint32_t& aTrackingNumber) { + mSocket->UpdateBufferedAmount(aBuffered, aTrackingNumber); + return IPC_OK(); +} + +mozilla::ipc::IPCResult TCPSocketChild::RecvCallback( + const nsString& aType, const CallbackData& aData, + const uint32_t& aReadyState) { + mSocket->UpdateReadyState(aReadyState); + + if (aData.type() == CallbackData::Tvoid_t) { + mSocket->FireEvent(aType); + + } else if (aData.type() == CallbackData::TTCPError) { + const TCPError& err(aData.get_TCPError()); + mSocket->FireErrorEvent(err.name(), err.message(), err.errorCode()); + + } else if (aData.type() == CallbackData::TSendableData) { + const SendableData& data = aData.get_SendableData(); + + if (data.type() == SendableData::TArrayOfuint8_t) { + mSocket->FireDataArrayEvent(aType, data.get_ArrayOfuint8_t()); + } else if (data.type() == SendableData::TnsCString) { + mSocket->FireDataStringEvent(aType, data.get_nsCString()); + } else { + MOZ_CRASH("Invalid callback data type!"); + } + } else { + MOZ_CRASH("Invalid callback type!"); + } + return IPC_OK(); +} + +void TCPSocketChild::SendSend(const nsACString& aData) { + SendData(nsCString(aData)); +} + +void TCPSocketChild::SendSend(nsTArray<uint8_t>&& aData) { + SendData(SendableData{std::move(aData)}); +} + +void TCPSocketChild::SetSocket(TCPSocket* aSocket) { mSocket = aSocket; } + +void TCPSocketChild::GetHost(nsAString& aHost) { aHost = mHost; } + +void TCPSocketChild::GetPort(uint16_t* aPort) const { *aPort = mPort; } + +mozilla::ipc::IPCResult TCPSocketChild::RecvRequestDelete() { + mozilla::Unused << Send__delete__(this); + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/network/TCPSocketChild.h b/dom/network/TCPSocketChild.h new file mode 100644 index 0000000000..69907a3f29 --- /dev/null +++ b/dom/network/TCPSocketChild.h @@ -0,0 +1,76 @@ +/* -*- 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 mozilla_dom_TCPSocketChild_h +#define mozilla_dom_TCPSocketChild_h + +#include "mozilla/dom/TypedArray.h" +#include "mozilla/net/PTCPSocketChild.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" +#include "js/TypeDecls.h" + +class nsITCPSocketCallback; + +namespace IPC { +bool DeserializeArrayBuffer(JSContext* cx, const nsTArray<uint8_t>& aBuffer, + JS::MutableHandle<JS::Value> aVal); +} + +namespace mozilla::dom { + +class TCPSocket; + +class TCPSocketChildBase : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(TCPSocketChildBase) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + protected: + TCPSocketChildBase(); + virtual ~TCPSocketChildBase(); + + nsCOMPtr<nsITCPSocketCallback> mSocket; + bool mIPCOpen; +}; + +class TCPSocketChild : public mozilla::net::PTCPSocketChild, + public TCPSocketChildBase { + public: + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + TCPSocketChild(const nsAString& aHost, const uint16_t& aPort, + nsISerialEventTarget* aTarget); + ~TCPSocketChild(); + + void SendOpen(nsITCPSocketCallback* aSocket, bool aUseSSL, + bool aUseArrayBuffers); + void SendSend(const nsACString& aData); + void SendSend(nsTArray<uint8_t>&& aData); + void SetSocket(TCPSocket* aSocket); + + void GetHost(nsAString& aHost); + void GetPort(uint16_t* aPort) const; + + mozilla::ipc::IPCResult RecvCallback(const nsString& aType, + const CallbackData& aData, + const uint32_t& aReadyState); + mozilla::ipc::IPCResult RecvRequestDelete(); + mozilla::ipc::IPCResult RecvUpdateBufferedAmount( + const uint32_t& aBufferred, const uint32_t& aTrackingNumber); + + private: + nsString mHost; + uint16_t mPort; + nsCOMPtr<nsISerialEventTarget> mIPCEventTarget; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/network/TCPSocketParent.cpp b/dom/network/TCPSocketParent.cpp new file mode 100644 index 0000000000..dc7bc3c530 --- /dev/null +++ b/dom/network/TCPSocketParent.cpp @@ -0,0 +1,223 @@ +/* -*- 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 "TCPSocketParent.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "nsJSUtils.h" +#include "mozilla/Unused.h" +#include "mozilla/net/NeckoCommon.h" +#include "mozilla/net/PNeckoParent.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/HoldDropJSObjects.h" +#include "nsISocketTransport.h" +#include "nsNetUtil.h" +#include "TCPSocket.h" + +namespace IPC { + +// Defined in TCPSocketChild.cpp +extern bool DeserializeArrayBuffer(JSContext* aCx, + const nsTArray<uint8_t>& aBuffer, + JS::MutableHandle<JS::Value> aVal); + +} // namespace IPC + +namespace mozilla { + +namespace net { +// +// set MOZ_LOG=TCPSocket:5 +// +extern LazyLogModule gTCPSocketLog; +#define TCPSOCKET_LOG(args) MOZ_LOG(gTCPSocketLog, LogLevel::Debug, args) +#define TCPSOCKET_LOG_ENABLED() MOZ_LOG_TEST(gTCPSocketLog, LogLevel::Debug) +} // namespace net + +using namespace net; + +namespace dom { + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TCPSocketParentBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(TCPSocketParentBase, mSocket) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TCPSocketParentBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TCPSocketParentBase) + +TCPSocketParentBase::TCPSocketParentBase() : mIPCOpen(false) {} + +TCPSocketParentBase::~TCPSocketParentBase() = default; + +void TCPSocketParentBase::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + this->Release(); +} + +void TCPSocketParentBase::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +NS_IMETHODIMP_(MozExternalRefCountType) TCPSocketParent::Release(void) { + nsrefcnt refcnt = TCPSocketParentBase::Release(); + if (refcnt == 1 && mIPCOpen) { + mozilla::Unused << PTCPSocketParent::SendRequestDelete(); + return 1; + } + return refcnt; +} + +mozilla::ipc::IPCResult TCPSocketParent::RecvOpen( + const nsString& aHost, const uint16_t& aPort, const bool& aUseSSL, + const bool& aUseArrayBuffers) { + mSocket = new TCPSocket(nullptr, aHost, aPort, aUseSSL, aUseArrayBuffers); + mSocket->SetSocketBridgeParent(this); + NS_ENSURE_SUCCESS(mSocket->Init(nullptr), IPC_OK()); + return IPC_OK(); +} + +mozilla::ipc::IPCResult TCPSocketParent::RecvStartTLS() { + NS_ENSURE_TRUE(mSocket, IPC_OK()); + ErrorResult rv; + mSocket->UpgradeToSecure(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult TCPSocketParent::RecvSuspend() { + NS_ENSURE_TRUE(mSocket, IPC_OK()); + mSocket->Suspend(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult TCPSocketParent::RecvResume() { + NS_ENSURE_TRUE(mSocket, IPC_OK()); + ErrorResult rv; + mSocket->Resume(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult TCPSocketParent::RecvData(const SendableData& aData) { + NS_ENSURE_TRUE(mSocket, IPC_OK()); + ErrorResult rv; + + switch (aData.type()) { + case SendableData::TArrayOfuint8_t: { + AutoSafeJSContext autoCx; + JS::Rooted<JS::Value> val(autoCx); + const nsTArray<uint8_t>& buffer = aData.get_ArrayOfuint8_t(); + bool ok = IPC::DeserializeArrayBuffer(autoCx, buffer, &val); + NS_ENSURE_TRUE(ok, IPC_OK()); + RootedSpiderMonkeyInterface<ArrayBuffer> data(autoCx); + if (!data.Init(&val.toObject())) { + TCPSOCKET_LOG(("%s: Failed to allocate memory", __FUNCTION__)); + return IPC_FAIL_NO_REASON(this); + } + Optional<uint32_t> byteLength(buffer.Length()); + mSocket->Send(data, 0, byteLength, rv); + break; + } + + case SendableData::TnsCString: { + const nsCString& strData = aData.get_nsCString(); + mSocket->Send(strData, rv); + break; + } + + default: + MOZ_CRASH("unexpected SendableData type"); + } + NS_ENSURE_SUCCESS(rv.StealNSResult(), IPC_OK()); + return IPC_OK(); +} + +mozilla::ipc::IPCResult TCPSocketParent::RecvClose() { + NS_ENSURE_TRUE(mSocket, IPC_OK()); + mSocket->Close(); + return IPC_OK(); +} + +void TCPSocketParent::FireErrorEvent(const nsAString& aName, + const nsAString& aType, nsresult aError, + TCPReadyState aReadyState) { + SendEvent(u"error"_ns, TCPError(nsString(aName), nsString(aType), aError), + aReadyState); +} + +void TCPSocketParent::FireEvent(const nsAString& aType, + TCPReadyState aReadyState) { + return SendEvent(aType, mozilla::void_t(), aReadyState); +} + +void TCPSocketParent::FireArrayBufferDataEvent(nsTArray<uint8_t>& aBuffer, + TCPReadyState aReadyState) { + nsTArray<uint8_t> arr = std::move(aBuffer); + + SendableData data(arr); + SendEvent(u"data"_ns, data, aReadyState); +} + +void TCPSocketParent::FireStringDataEvent(const nsACString& aData, + TCPReadyState aReadyState) { + SendableData data((nsCString(aData))); + + SendEvent(u"data"_ns, data, aReadyState); +} + +void TCPSocketParent::SendEvent(const nsAString& aType, CallbackData aData, + TCPReadyState aReadyState) { + if (mIPCOpen) { + mozilla::Unused << PTCPSocketParent::SendCallback( + nsString(aType), aData, static_cast<uint32_t>(aReadyState)); + } +} + +void TCPSocketParent::SetSocket(TCPSocket* socket) { mSocket = socket; } + +nsresult TCPSocketParent::GetHost(nsAString& aHost) { + if (!mSocket) { + NS_ERROR("No internal socket instance mSocket!"); + return NS_ERROR_FAILURE; + } + mSocket->GetHost(aHost); + return NS_OK; +} + +nsresult TCPSocketParent::GetPort(uint16_t* aPort) { + if (!mSocket) { + NS_ERROR("No internal socket instance mSocket!"); + return NS_ERROR_FAILURE; + } + *aPort = mSocket->Port(); + return NS_OK; +} + +void TCPSocketParent::ActorDestroy(ActorDestroyReason why) { + if (mSocket) { + mSocket->Close(); + } + mSocket = nullptr; +} + +mozilla::ipc::IPCResult TCPSocketParent::RecvRequestDelete() { + mozilla::Unused << Send__delete__(this); + return IPC_OK(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/network/TCPSocketParent.h b/dom/network/TCPSocketParent.h new file mode 100644 index 0000000000..388add862d --- /dev/null +++ b/dom/network/TCPSocketParent.h @@ -0,0 +1,82 @@ +/* -*- 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 mozilla_dom_TCPSocketParent_h +#define mozilla_dom_TCPSocketParent_h + +#include "mozilla/dom/TCPSocketBinding.h" +#include "mozilla/net/PTCPSocketParent.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" +#include "js/TypeDecls.h" + +#define TCPSOCKETPARENT_CID \ + { \ + 0x4e7246c6, 0xa8b3, 0x426d, { \ + 0x9c, 0x17, 0x76, 0xda, 0xb1, 0xe1, 0xe1, 0x4a \ + } \ + } + +namespace mozilla::dom { + +class TCPSocket; + +class TCPSocketParentBase : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTION_CLASS(TCPSocketParentBase) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + bool IPCOpen() const { return mIPCOpen; } + + protected: + TCPSocketParentBase(); + virtual ~TCPSocketParentBase(); + + RefPtr<TCPSocket> mSocket; + bool mIPCOpen; +}; + +class TCPSocketParent : public mozilla::net::PTCPSocketParent, + public TCPSocketParentBase { + public: + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + TCPSocketParent() = default; + + mozilla::ipc::IPCResult RecvOpen(const nsString& aHost, const uint16_t& aPort, + const bool& useSSL, + const bool& aUseArrayBuffers); + + mozilla::ipc::IPCResult RecvStartTLS(); + mozilla::ipc::IPCResult RecvSuspend(); + mozilla::ipc::IPCResult RecvResume(); + mozilla::ipc::IPCResult RecvClose(); + mozilla::ipc::IPCResult RecvData(const SendableData& aData); + mozilla::ipc::IPCResult RecvRequestDelete(); + + void FireErrorEvent(const nsAString& aName, const nsAString& aType, + nsresult aError, TCPReadyState aReadyState); + void FireEvent(const nsAString& aType, TCPReadyState aReadyState); + void FireArrayBufferDataEvent(nsTArray<uint8_t>& aBuffer, + TCPReadyState aReadyState); + void FireStringDataEvent(const nsACString& aData, TCPReadyState aReadyState); + + void SetSocket(TCPSocket* socket); + nsresult GetHost(nsAString& aHost); + nsresult GetPort(uint16_t* aPort); + + private: + virtual void ActorDestroy(ActorDestroyReason why) override; + void SendEvent(const nsAString& aType, CallbackData aData, + TCPReadyState aReadyState); +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/network/UDPSocket.cpp b/dom/network/UDPSocket.cpp new file mode 100644 index 0000000000..7dfcae3652 --- /dev/null +++ b/dom/network/UDPSocket.cpp @@ -0,0 +1,711 @@ +/* -*- 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 "UDPSocket.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/network/UDPSocketChild.h" +#include "mozilla/dom/UDPMessageEvent.h" +#include "mozilla/dom/UDPSocketBinding.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/net/DNS.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsINetAddr.h" +#include "nsStringStream.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(UDPSocket::ListenerProxy, nsIUDPSocketListener, + nsIUDPSocketInternal) + +NS_IMPL_CYCLE_COLLECTION_CLASS(UDPSocket) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(UDPSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOpened) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mClosed) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(UDPSocket, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOpened) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mClosed) + tmp->CloseWithReason(NS_OK); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ADDREF_INHERITED(UDPSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(UDPSocket, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UDPSocket) + NS_INTERFACE_MAP_ENTRY(nsIUDPSocketListener) + NS_INTERFACE_MAP_ENTRY(nsIUDPSocketInternal) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +/* static */ +already_AddRefed<UDPSocket> UDPSocket::Constructor(const GlobalObject& aGlobal, + const UDPOptions& aOptions, + ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> ownerWindow = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!ownerWindow) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + bool addressReuse = aOptions.mAddressReuse; + bool loopback = aOptions.mLoopback; + + nsCString remoteAddress; + if (aOptions.mRemoteAddress.WasPassed()) { + CopyUTF16toUTF8(aOptions.mRemoteAddress.Value(), remoteAddress); + } else { + remoteAddress.SetIsVoid(true); + } + + Nullable<uint16_t> remotePort; + if (aOptions.mRemotePort.WasPassed()) { + remotePort.SetValue(aOptions.mRemotePort.Value()); + + if (remotePort.Value() == 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + } + + nsString localAddress; + if (aOptions.mLocalAddress.WasPassed()) { + localAddress = aOptions.mLocalAddress.Value(); + + // check if localAddress is a valid IPv4/6 address + NS_ConvertUTF16toUTF8 address(localAddress); + if (!net::HostIsIPLiteral(address)) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + } else { + SetDOMStringToNull(localAddress); + } + + Nullable<uint16_t> localPort; + if (aOptions.mLocalPort.WasPassed()) { + localPort.SetValue(aOptions.mLocalPort.Value()); + + if (localPort.Value() == 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + } + + RefPtr<UDPSocket> socket = + new UDPSocket(ownerWindow, remoteAddress, remotePort); + aRv = socket->Init(localAddress, localPort, addressReuse, loopback); + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return socket.forget(); +} + +UDPSocket::UDPSocket(nsPIDOMWindowInner* aOwner, + const nsCString& aRemoteAddress, + const Nullable<uint16_t>& aRemotePort) + : DOMEventTargetHelper(aOwner), + mRemoteAddress(aRemoteAddress), + mRemotePort(aRemotePort), + mAddressReuse(false), + mLoopback(false), + mReadyState(SocketReadyState::Opening) { + MOZ_ASSERT(aOwner); + + Document* aDoc = aOwner->GetExtantDoc(); + if (aDoc) { + aDoc->DisallowBFCaching(); + } +} + +UDPSocket::~UDPSocket() { CloseWithReason(NS_OK); } + +JSObject* UDPSocket::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return UDPSocket_Binding::Wrap(aCx, this, aGivenProto); +} + +void UDPSocket::DisconnectFromOwner() { + DOMEventTargetHelper::DisconnectFromOwner(); + CloseWithReason(NS_OK); +} + +already_AddRefed<Promise> UDPSocket::Close() { + MOZ_ASSERT(mClosed); + + RefPtr<Promise> promise = mClosed; + + if (mReadyState == SocketReadyState::Closed) { + return promise.forget(); + } + + CloseWithReason(NS_OK); + return promise.forget(); +} + +void UDPSocket::CloseWithReason(nsresult aReason) { + if (mReadyState == SocketReadyState::Closed) { + return; + } + + if (mOpened) { + if (mReadyState == SocketReadyState::Opening) { + // reject openedPromise with AbortError if socket is closed without error + nsresult openFailedReason = + NS_FAILED(aReason) ? aReason : NS_ERROR_DOM_ABORT_ERR; + mOpened->MaybeReject(openFailedReason); + } + } + + mReadyState = SocketReadyState::Closed; + + if (mListenerProxy) { + mListenerProxy->Disconnect(); + mListenerProxy = nullptr; + } + + if (mSocket) { + mSocket->Close(); + mSocket = nullptr; + } + + if (mSocketChild) { + mSocketChild->Close(); + mSocketChild = nullptr; + } + + if (mClosed) { + if (NS_SUCCEEDED(aReason)) { + mClosed->MaybeResolveWithUndefined(); + } else { + mClosed->MaybeReject(aReason); + } + } + + mPendingMcastCommands.Clear(); +} + +void UDPSocket::JoinMulticastGroup(const nsAString& aMulticastGroupAddress, + ErrorResult& aRv) { + if (mReadyState == SocketReadyState::Closed) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mReadyState == SocketReadyState::Opening) { + MulticastCommand joinCommand(MulticastCommand::Join, + aMulticastGroupAddress); + mPendingMcastCommands.AppendElement(joinCommand); + return; + } + + MOZ_ASSERT(mSocket || mSocketChild); + + NS_ConvertUTF16toUTF8 address(aMulticastGroupAddress); + + if (mSocket) { + MOZ_ASSERT(!mSocketChild); + + aRv = mSocket->JoinMulticast(address, ""_ns); + NS_WARNING_ASSERTION(!aRv.Failed(), "JoinMulticast failed"); + + return; + } + + MOZ_ASSERT(mSocketChild); + + mSocketChild->JoinMulticast(address, ""_ns); +} + +void UDPSocket::LeaveMulticastGroup(const nsAString& aMulticastGroupAddress, + ErrorResult& aRv) { + if (mReadyState == SocketReadyState::Closed) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mReadyState == SocketReadyState::Opening) { + MulticastCommand leaveCommand(MulticastCommand::Leave, + aMulticastGroupAddress); + mPendingMcastCommands.AppendElement(leaveCommand); + return; + } + + MOZ_ASSERT(mSocket || mSocketChild); + + nsCString address = NS_ConvertUTF16toUTF8(aMulticastGroupAddress); + if (mSocket) { + MOZ_ASSERT(!mSocketChild); + + aRv = mSocket->LeaveMulticast(address, ""_ns); + NS_WARNING_ASSERTION(!aRv.Failed(), "mSocket->LeaveMulticast failed"); + return; + } + + MOZ_ASSERT(mSocketChild); + + mSocketChild->LeaveMulticast(address, ""_ns); +} + +nsresult UDPSocket::DoPendingMcastCommand() { + MOZ_ASSERT(mReadyState == SocketReadyState::Open, + "Multicast command can only be executed after socket opened"); + + for (uint32_t i = 0; i < mPendingMcastCommands.Length(); ++i) { + MulticastCommand& command = mPendingMcastCommands[i]; + ErrorResult rv; + + switch (command.mCommand) { + case MulticastCommand::Join: { + JoinMulticastGroup(command.mAddress, rv); + break; + } + case MulticastCommand::Leave: { + LeaveMulticastGroup(command.mAddress, rv); + break; + } + } + + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + } + + mPendingMcastCommands.Clear(); + return NS_OK; +} + +bool UDPSocket::Send(const StringOrBlobOrArrayBufferOrArrayBufferView& aData, + const Optional<nsAString>& aRemoteAddress, + const Optional<Nullable<uint16_t>>& aRemotePort, + ErrorResult& aRv) { + if (mReadyState != SocketReadyState::Open) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return false; + } + + MOZ_ASSERT(mSocket || mSocketChild); + + // If the remote address and port were not specified in the constructor or as + // arguments, throw InvalidAccessError. + nsCString remoteAddress; + if (aRemoteAddress.WasPassed()) { + CopyUTF16toUTF8(aRemoteAddress.Value(), remoteAddress); + UDPSOCKET_LOG(("%s: Send to %s", __FUNCTION__, remoteAddress.get())); + } else if (!mRemoteAddress.IsVoid()) { + remoteAddress = mRemoteAddress; + UDPSOCKET_LOG(("%s: Send to %s", __FUNCTION__, remoteAddress.get())); + } else { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return false; + } + + uint16_t remotePort; + if (aRemotePort.WasPassed() && !aRemotePort.Value().IsNull()) { + remotePort = aRemotePort.Value().Value(); + } else if (!mRemotePort.IsNull()) { + remotePort = mRemotePort.Value(); + } else { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return false; + } + + nsCOMPtr<nsIInputStream> stream; + if (aData.IsBlob()) { + Blob& blob = aData.GetAsBlob(); + + blob.CreateInputStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return false; + } + } else { + nsresult rv; + nsCOMPtr<nsIStringInputStream> strStream = + do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return false; + } + + if (aData.IsString()) { + NS_ConvertUTF16toUTF8 data(aData.GetAsString()); + aRv = strStream->SetData(data.BeginReading(), data.Length()); + } else { + Vector<char> data; + if (!AppendTypedArrayDataTo(aData, data)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + size_t length = data.length(); + aRv = strStream->AdoptData(data.extractOrCopyRawBuffer(), length); + } + + if (NS_WARN_IF(aRv.Failed())) { + return false; + } + + stream = strStream; + } + + if (mSocket) { + aRv = mSocket->SendBinaryStream(remoteAddress, remotePort, stream); + } else if (mSocketChild) { + aRv = mSocketChild->SendBinaryStream(remoteAddress, remotePort, stream); + } + + if (NS_WARN_IF(aRv.Failed())) { + return false; + } + + return true; +} + +nsresult UDPSocket::InitLocal(const nsAString& aLocalAddress, + const uint16_t& aLocalPort) { + nsresult rv; + + nsCOMPtr<nsIUDPSocket> sock = + do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner(), &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull(); + if (!principal) { + return NS_ERROR_FAILURE; + } + + if (aLocalAddress.IsEmpty()) { + rv = sock->Init(aLocalPort, /* loopback = */ false, principal, + mAddressReuse, /* optionalArgc = */ 1); + } else { + PRNetAddr prAddr; + PR_InitializeNetAddr(PR_IpAddrAny, aLocalPort, &prAddr); + PR_StringToNetAddr(NS_ConvertUTF16toUTF8(aLocalAddress).BeginReading(), + &prAddr); + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, + NS_ConvertUTF16toUTF8(aLocalAddress).get(), aLocalPort)); + + mozilla::net::NetAddr addr(&prAddr); + rv = sock->InitWithAddress(&addr, principal, mAddressReuse, + /* optionalArgc = */ 1); + } + if (NS_FAILED(rv)) { + return rv; + } + + rv = sock->SetMulticastLoopback(mLoopback); + if (NS_FAILED(rv)) { + return rv; + } + + mSocket = sock; + + // Get real local address and local port + nsCOMPtr<nsINetAddr> localAddr; + rv = mSocket->GetLocalAddr(getter_AddRefs(localAddr)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCString localAddress; + rv = localAddr->GetAddress(localAddress); + if (NS_FAILED(rv)) { + return rv; + } + CopyUTF8toUTF16(localAddress, mLocalAddress); + + uint16_t localPort; + rv = localAddr->GetPort(&localPort); + if (NS_FAILED(rv)) { + return rv; + } + mLocalPort.SetValue(localPort); + + mListenerProxy = new ListenerProxy(this); + + rv = mSocket->AsyncListen(mListenerProxy); + if (NS_FAILED(rv)) { + return rv; + } + + mReadyState = SocketReadyState::Open; + rv = DoPendingMcastCommand(); + if (NS_FAILED(rv)) { + return rv; + } + + mOpened->MaybeResolveWithUndefined(); + + return NS_OK; +} + +nsresult UDPSocket::InitRemote(const nsAString& aLocalAddress, + const uint16_t& aLocalPort) { + nsresult rv; + + RefPtr<UDPSocketChild> sock = new UDPSocketChild(); + + mListenerProxy = new ListenerProxy(this); + + nsCOMPtr<nsIGlobalObject> obj = do_QueryInterface(GetOwner(), &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIPrincipal> principal = obj->PrincipalOrNull(); + if (!principal) { + return NS_ERROR_FAILURE; + } + + rv = sock->Bind(mListenerProxy, principal, + NS_ConvertUTF16toUTF8(aLocalAddress), aLocalPort, + mAddressReuse, mLoopback, 0, 0); + + if (NS_FAILED(rv)) { + return rv; + } + + mSocketChild = sock; + + return NS_OK; +} + +nsresult UDPSocket::Init(const nsString& aLocalAddress, + const Nullable<uint16_t>& aLocalPort, + const bool& aAddressReuse, const bool& aLoopback) { + MOZ_ASSERT(!mSocket && !mSocketChild); + + mLocalAddress = aLocalAddress; + mLocalPort = aLocalPort; + mAddressReuse = aAddressReuse; + mLoopback = aLoopback; + + ErrorResult rv; + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner()); + + mOpened = Promise::Create(global, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + mClosed = Promise::Create(global, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + class OpenSocketRunnable final : public Runnable { + public: + explicit OpenSocketRunnable(UDPSocket* aSocket) + : mozilla::Runnable("OpenSocketRunnable"), mSocket(aSocket) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(mSocket); + + if (mSocket->mReadyState != SocketReadyState::Opening) { + return NS_OK; + } + + uint16_t localPort = 0; + if (!mSocket->mLocalPort.IsNull()) { + localPort = mSocket->mLocalPort.Value(); + } + + nsresult rv; + if (!XRE_IsParentProcess()) { + rv = mSocket->InitRemote(mSocket->mLocalAddress, localPort); + } else { + rv = mSocket->InitLocal(mSocket->mLocalAddress, localPort); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + mSocket->CloseWithReason(NS_ERROR_DOM_NETWORK_ERR); + } + + return NS_OK; + } + + private: + RefPtr<UDPSocket> mSocket; + }; + + nsCOMPtr<nsIRunnable> runnable = new OpenSocketRunnable(this); + + return NS_DispatchToMainThread(runnable); +} + +void UDPSocket::HandleReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const nsTArray<uint8_t>& aData) { + if (mReadyState != SocketReadyState::Open) { + return; + } + + if (NS_FAILED(CheckCurrentGlobalCorrectness())) { + return; + } + + if (NS_FAILED(DispatchReceivedData(aRemoteAddress, aRemotePort, aData))) { + CloseWithReason(NS_ERROR_UNEXPECTED); + } +} + +nsresult UDPSocket::DispatchReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const nsTArray<uint8_t>& aData) { + AutoJSAPI jsapi; + + if (NS_WARN_IF(!jsapi.Init(GetOwner()))) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + + // Copy packet data to ArrayBuffer + ErrorResult error; + JS::Rooted<JSObject*> arrayBuf(cx, ArrayBuffer::Create(cx, aData, error)); + + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + JS::Rooted<JS::Value> jsData(cx, JS::ObjectValue(*arrayBuf)); + + // Create DOM event + RootedDictionary<UDPMessageEventInit> init(cx); + CopyUTF8toUTF16(aRemoteAddress, init.mRemoteAddress); + init.mRemotePort = aRemotePort; + init.mData = jsData; + + RefPtr<UDPMessageEvent> udpEvent = + UDPMessageEvent::Constructor(this, u"message"_ns, init); + + if (NS_WARN_IF(!udpEvent)) { + return NS_ERROR_FAILURE; + } + + udpEvent->SetTrusted(true); + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, udpEvent.forget()); + + return asyncDispatcher->PostDOMEvent(); +} + +// nsIUDPSocketListener + +NS_IMETHODIMP +UDPSocket::OnPacketReceived(nsIUDPSocket* aSocket, nsIUDPMessage* aMessage) { + // nsIUDPSocketListener callbacks should be invoked on main thread. + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + + // Create appropriate JS object for message + FallibleTArray<uint8_t>& buffer = aMessage->GetDataAsTArray(); + + nsCOMPtr<nsINetAddr> addr; + if (NS_WARN_IF(NS_FAILED(aMessage->GetFromAddr(getter_AddRefs(addr))))) { + return NS_OK; + } + + nsCString remoteAddress; + if (NS_WARN_IF(NS_FAILED(addr->GetAddress(remoteAddress)))) { + return NS_OK; + } + + uint16_t remotePort; + if (NS_WARN_IF(NS_FAILED(addr->GetPort(&remotePort)))) { + return NS_OK; + } + + HandleReceivedData(remoteAddress, remotePort, buffer); + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::OnStopListening(nsIUDPSocket* aSocket, nsresult aStatus) { + // nsIUDPSocketListener callbacks should be invoked on main thread. + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + + CloseWithReason(aStatus); + + return NS_OK; +} + +// nsIUDPSocketInternal + +NS_IMETHODIMP +UDPSocket::CallListenerError(const nsACString& aMessage, + const nsACString& aFilename, + uint32_t aLineNumber) { + CloseWithReason(NS_ERROR_DOM_NETWORK_ERR); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerReceivedData(const nsACString& aRemoteAddress, + uint16_t aRemotePort, + const nsTArray<uint8_t>& aData) { + HandleReceivedData(aRemoteAddress, aRemotePort, aData); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerOpened() { + if (mReadyState != SocketReadyState::Opening) { + return NS_OK; + } + + MOZ_ASSERT(mSocketChild); + + // Get real local address and local port + CopyUTF8toUTF16(mSocketChild->LocalAddress(), mLocalAddress); + + mLocalPort.SetValue(mSocketChild->LocalPort()); + + mReadyState = SocketReadyState::Open; + nsresult rv = DoPendingMcastCommand(); + + if (NS_WARN_IF(NS_FAILED(rv))) { + CloseWithReason(rv); + return NS_OK; + } + + mOpened->MaybeResolveWithUndefined(); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerConnected() { + // This shouldn't be called here. + MOZ_CRASH(); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerClosed() { + CloseWithReason(NS_OK); + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/network/UDPSocket.h b/dom/network/UDPSocket.h new file mode 100644 index 0000000000..5453fe5448 --- /dev/null +++ b/dom/network/UDPSocket.h @@ -0,0 +1,177 @@ +/* -*- 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 mozilla_dom_UDPSocket_h__ +#define mozilla_dom_UDPSocket_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/SocketCommonBinding.h" +#include "nsIUDPSocket.h" +#include "nsIUDPSocketChild.h" +#include "nsTArray.h" + +struct JSContext; + +// +// set MOZ_LOG=UDPSocket:5 +// + +namespace mozilla { +class ErrorResult; +class LazyLogModule; + +namespace net { +extern LazyLogModule gUDPSocketLog; +#define UDPSOCKET_LOG(args) \ + MOZ_LOG(::mozilla::net::gUDPSocketLog, LogLevel::Debug, args) +#define UDPSOCKET_LOG_ENABLED() \ + MOZ_LOG_TEST(::mozilla::net::gUDPSocketLog, LogLevel::Debug) +} // namespace net + +namespace dom { + +struct UDPOptions; +class StringOrBlobOrArrayBufferOrArrayBufferView; +class UDPSocketChild; + +class UDPSocket final : public DOMEventTargetHelper, + public nsIUDPSocketListener, + public nsIUDPSocketInternal { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(UDPSocket, DOMEventTargetHelper) + NS_DECL_NSIUDPSOCKETLISTENER + NS_DECL_NSIUDPSOCKETINTERNAL + + public: + nsPIDOMWindowInner* GetParentObject() const { return GetOwner(); } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual void DisconnectFromOwner() override; + + static already_AddRefed<UDPSocket> Constructor(const GlobalObject& aGlobal, + const UDPOptions& aOptions, + ErrorResult& aRv); + + void GetLocalAddress(nsString& aRetVal) const { aRetVal = mLocalAddress; } + + Nullable<uint16_t> GetLocalPort() const { return mLocalPort; } + + void GetRemoteAddress(nsString& aRetVal) const { + if (mRemoteAddress.IsVoid()) { + SetDOMStringToNull(aRetVal); + return; + } + + CopyUTF8toUTF16(mRemoteAddress, aRetVal); + } + + Nullable<uint16_t> GetRemotePort() const { return mRemotePort; } + + bool AddressReuse() const { return mAddressReuse; } + + bool Loopback() const { return mLoopback; } + + SocketReadyState ReadyState() const { return mReadyState; } + + Promise* Opened() const { return mOpened; } + + Promise* Closed() const { return mClosed; } + + IMPL_EVENT_HANDLER(message) + + already_AddRefed<Promise> Close(); + + void JoinMulticastGroup(const nsAString& aMulticastGroupAddress, + ErrorResult& aRv); + + void LeaveMulticastGroup(const nsAString& aMulticastGroupAddress, + ErrorResult& aRv); + + bool Send(const StringOrBlobOrArrayBufferOrArrayBufferView& aData, + const Optional<nsAString>& aRemoteAddress, + const Optional<Nullable<uint16_t>>& aRemotePort, ErrorResult& aRv); + + private: + class ListenerProxy : public nsIUDPSocketListener, + public nsIUDPSocketInternal { + public: + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIUDPSOCKETLISTENER(mSocket) + NS_FORWARD_SAFE_NSIUDPSOCKETINTERNAL(mSocket) + + explicit ListenerProxy(UDPSocket* aSocket) : mSocket(aSocket) {} + + void Disconnect() { mSocket = nullptr; } + + private: + virtual ~ListenerProxy() = default; + + UDPSocket* mSocket; + }; + + UDPSocket(nsPIDOMWindowInner* aOwner, const nsCString& aRemoteAddress, + const Nullable<uint16_t>& aRemotePort); + + virtual ~UDPSocket(); + + nsresult Init(const nsString& aLocalAddress, + const Nullable<uint16_t>& aLocalPort, const bool& aAddressReuse, + const bool& aLoopback); + + nsresult InitLocal(const nsAString& aLocalAddress, + const uint16_t& aLocalPort); + + nsresult InitRemote(const nsAString& aLocalAddress, + const uint16_t& aLocalPort); + + void HandleReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const nsTArray<uint8_t>& aData); + + nsresult DispatchReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const nsTArray<uint8_t>& aData); + + void CloseWithReason(nsresult aReason); + + nsresult DoPendingMcastCommand(); + + nsString mLocalAddress; + Nullable<uint16_t> mLocalPort; + nsCString mRemoteAddress; + Nullable<uint16_t> mRemotePort; + bool mAddressReuse; + bool mLoopback; + SocketReadyState mReadyState; + RefPtr<Promise> mOpened; + RefPtr<Promise> mClosed; + + nsCOMPtr<nsIUDPSocket> mSocket; + RefPtr<UDPSocketChild> mSocketChild; + RefPtr<ListenerProxy> mListenerProxy; + + struct MulticastCommand { + enum CommandType { Join, Leave }; + + MulticastCommand(CommandType aCommand, const nsAString& aAddress) + : mCommand(aCommand), mAddress(aAddress) {} + + CommandType mCommand; + nsString mAddress; + }; + + nsTArray<MulticastCommand> mPendingMcastCommands; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_UDPSocket_h__ diff --git a/dom/network/UDPSocketChild.cpp b/dom/network/UDPSocketChild.cpp new file mode 100644 index 0000000000..e5a09d56dd --- /dev/null +++ b/dom/network/UDPSocketChild.cpp @@ -0,0 +1,234 @@ +/* -*- 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 "UDPSocketChild.h" +#include "UDPSocket.h" +#include "mozilla/Unused.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" + +using mozilla::net::gNeckoChild; + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(UDPSocketChildBase, nsISupports) + +UDPSocketChildBase::UDPSocketChildBase() : mIPCOpen(false) {} + +UDPSocketChildBase::~UDPSocketChildBase() = default; + +void UDPSocketChildBase::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + mSocket = nullptr; + this->Release(); +} + +void UDPSocketChildBase::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen); + mIPCOpen = true; + this->AddRef(); +} + +NS_IMETHODIMP_(MozExternalRefCountType) UDPSocketChild::Release(void) { + nsrefcnt refcnt = UDPSocketChildBase::Release(); + if (refcnt == 1 && mIPCOpen) { + PUDPSocketChild::SendRequestDelete(); + return 1; + } + return refcnt; +} + +UDPSocketChild::UDPSocketChild() : mBackgroundManager(nullptr), mLocalPort(0) {} + +UDPSocketChild::~UDPSocketChild() = default; + +nsresult UDPSocketChild::SetBackgroundSpinsEvents() { + using mozilla::ipc::BackgroundChild; + + mBackgroundManager = BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!mBackgroundManager)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult UDPSocketChild::Bind(nsIUDPSocketInternal* aSocket, + nsIPrincipal* aPrincipal, const nsACString& aHost, + uint16_t aPort, bool aAddressReuse, + bool aLoopback, uint32_t recvBufferSize, + uint32_t sendBufferSize) { + UDPSOCKET_LOG( + ("%s: %s:%u", __FUNCTION__, PromiseFlatCString(aHost).get(), aPort)); + + NS_ENSURE_ARG(aSocket); + + if (NS_IsMainThread()) { + if (!gNeckoChild->SendPUDPSocketConstructor(this, aPrincipal, + mFilterName)) { + return NS_ERROR_FAILURE; + } + } else { + if (!mBackgroundManager) { + return NS_ERROR_NOT_AVAILABLE; + } + + // If we want to support a passed-in principal here we'd need to + // convert it to a PrincipalInfo + MOZ_ASSERT(!aPrincipal); + if (!mBackgroundManager->SendPUDPSocketConstructor(this, Nothing(), + mFilterName)) { + return NS_ERROR_FAILURE; + } + } + + mSocket = aSocket; + AddIPDLReference(); + + SendBind(UDPAddressInfo(nsCString(aHost), aPort), aAddressReuse, aLoopback, + recvBufferSize, sendBufferSize); + return NS_OK; +} + +void UDPSocketChild::Connect(nsIUDPSocketInternal* aSocket, + const nsACString& aHost, uint16_t aPort) { + UDPSOCKET_LOG( + ("%s: %s:%u", __FUNCTION__, PromiseFlatCString(aHost).get(), aPort)); + + mSocket = aSocket; + + SendConnect(UDPAddressInfo(nsCString(aHost), aPort)); +} + +void UDPSocketChild::Close() { SendClose(); } + +nsresult UDPSocketChild::SendWithAddress(const NetAddr* aAddr, + const uint8_t* aData, + uint32_t aByteLength) { + NS_ENSURE_ARG(aAddr); + NS_ENSURE_ARG(aData); + + UDPSOCKET_LOG(("%s: %u bytes", __FUNCTION__, aByteLength)); + return SendDataInternal(UDPSocketAddr(*aAddr), aData, aByteLength); +} + +nsresult UDPSocketChild::SendDataInternal(const UDPSocketAddr& aAddr, + const uint8_t* aData, + const uint32_t aByteLength) { + NS_ENSURE_ARG(aData); + + FallibleTArray<uint8_t> fallibleArray; + if (!fallibleArray.InsertElementsAt(0, aData, aByteLength, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + SendOutgoingData(UDPData{std::move(fallibleArray)}, aAddr); + + return NS_OK; +} + +nsresult UDPSocketChild::SendBinaryStream(const nsACString& aHost, + uint16_t aPort, + nsIInputStream* aStream) { + NS_ENSURE_ARG(aStream); + + mozilla::ipc::IPCStream stream; + if (NS_WARN_IF(!mozilla::ipc::SerializeIPCStream(do_AddRef(aStream), stream, + /* aAllowLazy */ false))) { + return NS_ERROR_UNEXPECTED; + } + + UDPSOCKET_LOG( + ("%s: %s:%u", __FUNCTION__, PromiseFlatCString(aHost).get(), aPort)); + SendOutgoingData(UDPData(stream), + UDPSocketAddr(UDPAddressInfo(nsCString(aHost), aPort))); + + return NS_OK; +} + +void UDPSocketChild::JoinMulticast(const nsACString& aMulticastAddress, + const nsACString& aInterface) { + SendJoinMulticast(aMulticastAddress, aInterface); +} + +void UDPSocketChild::LeaveMulticast(const nsACString& aMulticastAddress, + const nsACString& aInterface) { + SendLeaveMulticast(aMulticastAddress, aInterface); +} + +nsresult UDPSocketChild::SetFilterName(const nsACString& aFilterName) { + if (!mFilterName.IsEmpty()) { + // filter name can only be set once. + return NS_ERROR_FAILURE; + } + mFilterName = aFilterName; + return NS_OK; +} + +// PUDPSocketChild Methods +mozilla::ipc::IPCResult UDPSocketChild::RecvCallbackOpened( + const UDPAddressInfo& aAddressInfo) { + mLocalAddress = aAddressInfo.addr(); + mLocalPort = aAddressInfo.port(); + + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, mLocalAddress.get(), mLocalPort)); + nsresult rv = mSocket->CallListenerOpened(); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return IPC_OK(); +} + +// PUDPSocketChild Methods +mozilla::ipc::IPCResult UDPSocketChild::RecvCallbackConnected( + const UDPAddressInfo& aAddressInfo) { + mLocalAddress = aAddressInfo.addr(); + mLocalPort = aAddressInfo.port(); + + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, mLocalAddress.get(), mLocalPort)); + nsresult rv = mSocket->CallListenerConnected(); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult UDPSocketChild::RecvCallbackClosed() { + nsresult rv = mSocket->CallListenerClosed(); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult UDPSocketChild::RecvCallbackReceivedData( + const UDPAddressInfo& aAddressInfo, nsTArray<uint8_t>&& aData) { + UDPSOCKET_LOG(("%s: %s:%u length %zu", __FUNCTION__, + aAddressInfo.addr().get(), aAddressInfo.port(), + aData.Length())); + nsresult rv = mSocket->CallListenerReceivedData(aAddressInfo.addr(), + aAddressInfo.port(), aData); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult UDPSocketChild::RecvCallbackError( + const nsCString& aMessage, const nsCString& aFilename, + const uint32_t& aLineNumber) { + UDPSOCKET_LOG(("%s: %s:%s:%u", __FUNCTION__, aMessage.get(), aFilename.get(), + aLineNumber)); + nsresult rv = mSocket->CallListenerError(aMessage, aFilename, aLineNumber); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/network/UDPSocketChild.h b/dom/network/UDPSocketChild.h new file mode 100644 index 0000000000..81e5e39776 --- /dev/null +++ b/dom/network/UDPSocketChild.h @@ -0,0 +1,109 @@ +/* -*- 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 mozilla_dom_UDPSocketChild_h__ +#define mozilla_dom_UDPSocketChild_h__ + +#include "mozilla/net/PUDPSocketChild.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" + +#define UDPSOCKETCHILD_CID \ + { \ + 0xb47e5a0f, 0xd384, 0x48ef, { \ + 0x88, 0x85, 0x42, 0x59, 0x79, 0x3d, 0x9c, 0xf0 \ + } \ + } + +class nsIInputStream; +class nsIPrincipal; +class nsIUDPSocketInternal; + +namespace mozilla::dom { + +class UDPSocketChildBase : public nsISupports { + public: + NS_DECL_ISUPPORTS + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + protected: + UDPSocketChildBase(); + virtual ~UDPSocketChildBase(); + nsCOMPtr<nsIUDPSocketInternal> mSocket; + bool mIPCOpen; +}; + +class UDPSocketChild : public mozilla::net::PUDPSocketChild, + public UDPSocketChildBase { + public: + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + UDPSocketChild(); + virtual ~UDPSocketChild(); + + uint16_t LocalPort() const { return mLocalPort; } + // Local address as UTF-8. + const nsACString& LocalAddress() const { return mLocalAddress; } + + nsresult SetFilterName(const nsACString& aFilterName); + + // Allow hosting this over PBackground instead of PNecko + nsresult SetBackgroundSpinsEvents(); + + // Tell the chrome process to bind the UDP socket to a given local host and + // port + nsresult Bind(nsIUDPSocketInternal* aSocket, nsIPrincipal* aPrincipal, + const nsACString& aHost, uint16_t aPort, bool aAddressReuse, + bool aLoopback, uint32_t recvBufferSize, + uint32_t sendBufferSize); + + // Tell the chrome process to connect the UDP socket to a given remote host + // and port + void Connect(nsIUDPSocketInternal* aSocket, const nsACString& aHost, + uint16_t aPort); + + // Send the given data to the given address. + nsresult SendWithAddress(const NetAddr* aAddr, const uint8_t* aData, + uint32_t aByteLength); + + // Send input stream. This must be a buffered stream implementation. + nsresult SendBinaryStream(const nsACString& aHost, uint16_t aPort, + nsIInputStream* aStream); + + void Close(); + + // Address and interface are both UTF-8. + void JoinMulticast(const nsACString& aMulticastAddress, + const nsACString& aInterface); + void LeaveMulticast(const nsACString& aMulticastAddress, + const nsACString& aInterface); + + mozilla::ipc::IPCResult RecvCallbackOpened( + const UDPAddressInfo& aAddressInfo); + mozilla::ipc::IPCResult RecvCallbackConnected( + const UDPAddressInfo& aAddressInfo); + mozilla::ipc::IPCResult RecvCallbackClosed(); + mozilla::ipc::IPCResult RecvCallbackReceivedData( + const UDPAddressInfo& aAddressInfo, nsTArray<uint8_t>&& aData); + mozilla::ipc::IPCResult RecvCallbackError(const nsCString& aMessage, + const nsCString& aFilename, + const uint32_t& aLineNumber); + + private: + nsresult SendDataInternal(const UDPSocketAddr& aAddr, const uint8_t* aData, + const uint32_t aByteLength); + + mozilla::ipc::PBackgroundChild* mBackgroundManager; + uint16_t mLocalPort; + nsCString mLocalAddress; + nsCString mFilterName; +}; + +} // namespace mozilla::dom + +#endif // !defined(mozilla_dom_UDPSocketChild_h__) diff --git a/dom/network/UDPSocketParent.cpp b/dom/network/UDPSocketParent.cpp new file mode 100644 index 0000000000..d536f3afe4 --- /dev/null +++ b/dom/network/UDPSocketParent.cpp @@ -0,0 +1,552 @@ +/* -*- 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 "UDPSocketParent.h" +#include "UDPSocket.h" +#include "nsComponentManagerUtils.h" +#include "nsIUDPSocket.h" +#include "nsINetAddr.h" +#include "nsNetCID.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/NeckoCommon.h" +#include "mozilla/net/PNeckoParent.h" +#include "nsIPermissionManager.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "transport/runnable_utils.h" + +namespace mozilla { + +using namespace net; + +namespace dom { + +NS_IMPL_ISUPPORTS(UDPSocketParent, nsIUDPSocketListener) + +UDPSocketParent::UDPSocketParent(PBackgroundParent* aManager) + : mBackgroundManager(aManager), mIPCOpen(true) {} + +UDPSocketParent::UDPSocketParent(PNeckoParent* aManager) + : mBackgroundManager(nullptr), mIPCOpen(true) {} + +UDPSocketParent::~UDPSocketParent() = default; + +bool UDPSocketParent::Init(nsIPrincipal* aPrincipal, + const nsACString& aFilter) { + MOZ_ASSERT_IF(mBackgroundManager, !aPrincipal); + // will be used once we move all UDPSocket to PBackground, or + // if we add in Principal checking for dom/media/webrtc/transport + Unused << mBackgroundManager; + + mPrincipal = aPrincipal; + + if (!aFilter.IsEmpty()) { + nsAutoCString contractId(NS_NETWORK_UDP_SOCKET_FILTER_HANDLER_PREFIX); + contractId.Append(aFilter); + nsCOMPtr<nsISocketFilterHandler> filterHandler = + do_GetService(contractId.get()); + if (filterHandler) { + nsresult rv = filterHandler->NewFilter(getter_AddRefs(mFilter)); + if (NS_FAILED(rv)) { + printf_stderr( + "Cannot create filter that content specified. " + "filter name: %s, error code: %u.", + aFilter.BeginReading(), static_cast<uint32_t>(rv)); + return false; + } + } else { + printf_stderr( + "Content doesn't have a valid filter. " + "filter name: %s.", + aFilter.BeginReading()); + return false; + } + } + + return true; +} + +// PUDPSocketParent methods + +mozilla::ipc::IPCResult UDPSocketParent::RecvBind( + const UDPAddressInfo& aAddressInfo, const bool& aAddressReuse, + const bool& aLoopback, const uint32_t& recvBufferSize, + const uint32_t& sendBufferSize) { + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, aAddressInfo.addr().get(), + aAddressInfo.port())); + + if (NS_FAILED(BindInternal(aAddressInfo.addr(), aAddressInfo.port(), + aAddressReuse, aLoopback, recvBufferSize, + sendBufferSize))) { + FireInternalError(__LINE__); + return IPC_OK(); + } + + nsCOMPtr<nsINetAddr> localAddr; + mSocket->GetLocalAddr(getter_AddRefs(localAddr)); + + nsCString addr; + if (NS_FAILED(localAddr->GetAddress(addr))) { + FireInternalError(__LINE__); + return IPC_OK(); + } + + uint16_t port; + if (NS_FAILED(localAddr->GetPort(&port))) { + FireInternalError(__LINE__); + return IPC_OK(); + } + + UDPSOCKET_LOG( + ("%s: SendCallbackOpened: %s:%u", __FUNCTION__, addr.get(), port)); + mAddress = {addr, port}; + mozilla::Unused << SendCallbackOpened(UDPAddressInfo(addr, port)); + + return IPC_OK(); +} + +nsresult UDPSocketParent::BindInternal(const nsCString& aHost, + const uint16_t& aPort, + const bool& aAddressReuse, + const bool& aLoopback, + const uint32_t& recvBufferSize, + const uint32_t& sendBufferSize) { + nsresult rv; + + UDPSOCKET_LOG( + ("%s: [this=%p] %s:%u addressReuse: %d loopback: %d recvBufferSize: " + "%" PRIu32 ", sendBufferSize: %" PRIu32, + __FUNCTION__, this, nsCString(aHost).get(), aPort, aAddressReuse, + aLoopback, recvBufferSize, sendBufferSize)); + + nsCOMPtr<nsIUDPSocket> sock = + do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aHost.IsEmpty()) { + rv = sock->Init(aPort, false, mPrincipal, aAddressReuse, + /* optional_argc = */ 1); + } else { + PRNetAddr prAddr; + PR_InitializeNetAddr(PR_IpAddrAny, aPort, &prAddr); + PRStatus status = PR_StringToNetAddr(aHost.BeginReading(), &prAddr); + if (status != PR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + mozilla::net::NetAddr addr(&prAddr); + rv = sock->InitWithAddress(&addr, mPrincipal, aAddressReuse, + /* optional_argc = */ 1); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsINetAddr> laddr; + rv = sock->GetLocalAddr(getter_AddRefs(laddr)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + uint16_t family; + rv = laddr->GetFamily(&family); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (family == nsINetAddr::FAMILY_INET) { + rv = sock->SetMulticastLoopback(aLoopback); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + // TODO: once bug 1252759 is fixed query buffer first and only increase + if (recvBufferSize != 0) { + rv = sock->SetRecvBufferSize(recvBufferSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + UDPSOCKET_LOG( + ("%s: [this=%p] %s:%u failed to set recv buffer size to: %" PRIu32, + __FUNCTION__, this, nsCString(aHost).get(), aPort, recvBufferSize)); + } + } + if (sendBufferSize != 0) { + rv = sock->SetSendBufferSize(sendBufferSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + UDPSOCKET_LOG( + ("%s: [this=%p] %s:%u failed to set send buffer size to: %" PRIu32, + __FUNCTION__, this, nsCString(aHost).get(), aPort, sendBufferSize)); + } + } + + // register listener + rv = sock->AsyncListen(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mSocket = sock; + + return NS_OK; +} + +static nsCOMPtr<nsIEventTarget> GetSTSThread() { + nsresult rv; + + nsCOMPtr<nsIEventTarget> sts_thread; + + sts_thread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + return sts_thread; +} + +static void CheckSTSThread() { + DebugOnly<nsCOMPtr<nsIEventTarget>> sts_thread = GetSTSThread(); + + ASSERT_ON_THREAD(sts_thread.value); +} + +// Proxy the Connect() request to the STS thread, since it may block and +// should be done there. +mozilla::ipc::IPCResult UDPSocketParent::RecvConnect( + const UDPAddressInfo& aAddressInfo) { + nsCOMPtr<nsIEventTarget> target = GetCurrentSerialEventTarget(); + Unused << NS_WARN_IF(NS_FAILED(GetSTSThread()->Dispatch( + WrapRunnable(RefPtr<UDPSocketParent>(this), &UDPSocketParent::DoConnect, + mSocket, target, aAddressInfo), + NS_DISPATCH_NORMAL))); + return IPC_OK(); +} + +void UDPSocketParent::DoSendConnectResponse( + const UDPAddressInfo& aAddressInfo) { + // can't use directly with WrapRunnable due to warnings + mozilla::Unused << SendCallbackConnected(aAddressInfo); +} + +void UDPSocketParent::SendConnectResponse( + const nsCOMPtr<nsIEventTarget>& aThread, + const UDPAddressInfo& aAddressInfo) { + Unused << NS_WARN_IF(NS_FAILED(aThread->Dispatch( + WrapRunnable(RefPtr<UDPSocketParent>(this), + &UDPSocketParent::DoSendConnectResponse, aAddressInfo), + NS_DISPATCH_NORMAL))); +} + +// Runs on STS thread +void UDPSocketParent::DoConnect(const nsCOMPtr<nsIUDPSocket>& aSocket, + const nsCOMPtr<nsIEventTarget>& aReturnThread, + const UDPAddressInfo& aAddressInfo) { + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, aAddressInfo.addr().get(), + aAddressInfo.port())); + if (NS_FAILED(ConnectInternal(aAddressInfo.addr(), aAddressInfo.port()))) { + SendInternalError(aReturnThread, __LINE__); + return; + } + CheckSTSThread(); + + nsCOMPtr<nsINetAddr> localAddr; + aSocket->GetLocalAddr(getter_AddRefs(localAddr)); + + nsCString addr; + if (NS_FAILED(localAddr->GetAddress(addr))) { + SendInternalError(aReturnThread, __LINE__); + return; + } + + uint16_t port; + if (NS_FAILED(localAddr->GetPort(&port))) { + SendInternalError(aReturnThread, __LINE__); + return; + } + + UDPSOCKET_LOG( + ("%s: SendConnectResponse: %s:%u", __FUNCTION__, addr.get(), port)); + SendConnectResponse(aReturnThread, UDPAddressInfo(addr, port)); +} + +nsresult UDPSocketParent::ConnectInternal(const nsCString& aHost, + const uint16_t& aPort) { + nsresult rv; + + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, nsCString(aHost).get(), aPort)); + + if (!mSocket) { + return NS_ERROR_NOT_AVAILABLE; + } + + PRNetAddr prAddr; + memset(&prAddr, 0, sizeof(prAddr)); + PR_InitializeNetAddr(PR_IpAddrAny, aPort, &prAddr); + PRStatus status = PR_StringToNetAddr(aHost.BeginReading(), &prAddr); + if (status != PR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + mozilla::net::NetAddr addr(&prAddr); + rv = mSocket->Connect(&addr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +mozilla::ipc::IPCResult UDPSocketParent::RecvOutgoingData( + const UDPData& aData, const UDPSocketAddr& aAddr) { + if (!mSocket) { + NS_WARNING("sending socket is closed"); + FireInternalError(__LINE__); + return IPC_OK(); + } + + nsresult rv; + if (mFilter) { + if (aAddr.type() != UDPSocketAddr::TNetAddr) { + return IPC_OK(); + } + + // TODO, Packet filter doesn't support input stream yet. + if (aData.type() != UDPData::TArrayOfuint8_t) { + return IPC_OK(); + } + + bool allowed; + const nsTArray<uint8_t>& data(aData.get_ArrayOfuint8_t()); + UDPSOCKET_LOG(("%s(%s:%d): Filtering outgoing packet", __FUNCTION__, + mAddress.addr().get(), mAddress.port())); + + rv = mFilter->FilterPacket(&aAddr.get_NetAddr(), data.Elements(), + data.Length(), nsISocketFilter::SF_OUTGOING, + &allowed); + + // Sending unallowed data, kill content. + if (NS_WARN_IF(NS_FAILED(rv)) || !allowed) { + return IPC_FAIL(this, "Content tried to send non STUN packet"); + } + } + + switch (aData.type()) { + case UDPData::TArrayOfuint8_t: + Send(aData.get_ArrayOfuint8_t(), aAddr); + break; + case UDPData::TIPCStream: + Send(aData.get_IPCStream(), aAddr); + break; + default: + MOZ_ASSERT(false, "Invalid data type!"); + return IPC_OK(); + } + + return IPC_OK(); +} + +void UDPSocketParent::Send(const nsTArray<uint8_t>& aData, + const UDPSocketAddr& aAddr) { + nsresult rv; + uint32_t count; + switch (aAddr.type()) { + case UDPSocketAddr::TUDPAddressInfo: { + const UDPAddressInfo& addrInfo(aAddr.get_UDPAddressInfo()); + rv = mSocket->Send(addrInfo.addr(), addrInfo.port(), aData, &count); + break; + } + case UDPSocketAddr::TNetAddr: { + const NetAddr& addr(aAddr.get_NetAddr()); + rv = mSocket->SendWithAddress(&addr, aData.Elements(), aData.Length(), + &count); + break; + } + default: + MOZ_ASSERT(false, "Invalid address type!"); + return; + } + + if (NS_WARN_IF(NS_FAILED(rv)) || count == 0) { + FireInternalError(__LINE__); + } +} + +void UDPSocketParent::Send(const IPCStream& aStream, + const UDPSocketAddr& aAddr) { + nsCOMPtr<nsIInputStream> stream = DeserializeIPCStream(aStream); + + if (NS_WARN_IF(!stream)) { + return; + } + + nsresult rv; + switch (aAddr.type()) { + case UDPSocketAddr::TUDPAddressInfo: { + const UDPAddressInfo& addrInfo(aAddr.get_UDPAddressInfo()); + rv = mSocket->SendBinaryStream(addrInfo.addr(), addrInfo.port(), stream); + break; + } + case UDPSocketAddr::TNetAddr: { + const NetAddr& addr(aAddr.get_NetAddr()); + rv = mSocket->SendBinaryStreamWithAddress(&addr, stream); + break; + } + default: + MOZ_ASSERT(false, "Invalid address type!"); + return; + } + + if (NS_FAILED(rv)) { + FireInternalError(__LINE__); + } +} + +mozilla::ipc::IPCResult UDPSocketParent::RecvJoinMulticast( + const nsCString& aMulticastAddress, const nsCString& aInterface) { + if (!mSocket) { + NS_WARNING("multicast socket is closed"); + FireInternalError(__LINE__); + return IPC_OK(); + } + + nsresult rv = mSocket->JoinMulticast(aMulticastAddress, aInterface); + + if (NS_WARN_IF(NS_FAILED(rv))) { + FireInternalError(__LINE__); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult UDPSocketParent::RecvLeaveMulticast( + const nsCString& aMulticastAddress, const nsCString& aInterface) { + if (!mSocket) { + NS_WARNING("multicast socket is closed"); + FireInternalError(__LINE__); + return IPC_OK(); + } + + nsresult rv = mSocket->LeaveMulticast(aMulticastAddress, aInterface); + + if (NS_WARN_IF(NS_FAILED(rv))) { + FireInternalError(__LINE__); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult UDPSocketParent::RecvClose() { + if (!mSocket) { + return IPC_OK(); + } + + nsresult rv = mSocket->Close(); + mSocket = nullptr; + + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult UDPSocketParent::RecvRequestDelete() { + mozilla::Unused << Send__delete__(this); + return IPC_OK(); +} + +void UDPSocketParent::ActorDestroy(ActorDestroyReason why) { + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; + if (mSocket) { + mSocket->Close(); + } + mSocket = nullptr; +} + +// nsIUDPSocketListener + +NS_IMETHODIMP +UDPSocketParent::OnPacketReceived(nsIUDPSocket* aSocket, + nsIUDPMessage* aMessage) { + // receiving packet from remote host, forward the message content to child + // process + if (!mIPCOpen) { + return NS_OK; + } + + uint16_t port; + nsCString ip; + nsCOMPtr<nsINetAddr> fromAddr; + aMessage->GetFromAddr(getter_AddRefs(fromAddr)); + fromAddr->GetPort(&port); + fromAddr->GetAddress(ip); + + nsCString data; + aMessage->GetData(data); + + const char* buffer = data.get(); + uint32_t len = data.Length(); + UDPSOCKET_LOG(("%s: %s:%u, length %u", __FUNCTION__, ip.get(), port, len)); + + if (mFilter) { + bool allowed; + mozilla::net::NetAddr addr; + fromAddr->GetNetAddr(&addr); + UDPSOCKET_LOG(("%s(%s:%d): Filtering incoming packet", __FUNCTION__, + mAddress.addr().get(), mAddress.port())); + nsresult rv = mFilter->FilterPacket(&addr, (const uint8_t*)buffer, len, + nsISocketFilter::SF_INCOMING, &allowed); + // Receiving unallowed data, drop. + if (NS_WARN_IF(NS_FAILED(rv)) || !allowed) { + if (!allowed) { + UDPSOCKET_LOG(("%s: not allowed", __FUNCTION__)); + } + return NS_OK; + } + } + + FallibleTArray<uint8_t> fallibleArray; + if (!fallibleArray.InsertElementsAt(0, buffer, len, fallible)) { + FireInternalError(__LINE__); + return NS_ERROR_OUT_OF_MEMORY; + } + nsTArray<uint8_t> infallibleArray{std::move(fallibleArray)}; + + // compose callback + mozilla::Unused << SendCallbackReceivedData(UDPAddressInfo(ip, port), + infallibleArray); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocketParent::OnStopListening(nsIUDPSocket* aSocket, nsresult aStatus) { + // underlying socket is dead, send state update to child process + if (mIPCOpen) { + mozilla::Unused << SendCallbackClosed(); + } + return NS_OK; +} + +void UDPSocketParent::FireInternalError(uint32_t aLineNo) { + if (!mIPCOpen) { + return; + } + + mozilla::Unused << SendCallbackError("Internal error"_ns, + nsLiteralCString(__FILE__), aLineNo); +} + +void UDPSocketParent::SendInternalError(const nsCOMPtr<nsIEventTarget>& aThread, + uint32_t aLineNo) { + UDPSOCKET_LOG(("SendInternalError: %u", aLineNo)); + Unused << NS_WARN_IF(NS_FAILED(aThread->Dispatch( + WrapRunnable(RefPtr<UDPSocketParent>(this), + &UDPSocketParent::FireInternalError, aLineNo), + NS_DISPATCH_NORMAL))); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/network/UDPSocketParent.h b/dom/network/UDPSocketParent.h new file mode 100644 index 0000000000..6d4a982bae --- /dev/null +++ b/dom/network/UDPSocketParent.h @@ -0,0 +1,84 @@ +/* -*- 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 mozilla_dom_UDPSocketParent_h__ +#define mozilla_dom_UDPSocketParent_h__ + +#include "mozilla/net/PUDPSocketParent.h" +#include "nsCOMPtr.h" +#include "nsIUDPSocket.h" +#include "nsISocketFilter.h" +#include "mozilla/dom/PermissionMessageUtils.h" + +namespace mozilla { +namespace net { +class PNeckoParent; +} // namespace net + +namespace dom { + +class UDPSocketParent : public mozilla::net::PUDPSocketParent, + public nsIUDPSocketListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUDPSOCKETLISTENER + + explicit UDPSocketParent(PBackgroundParent* aManager); + explicit UDPSocketParent(PNeckoParent* aManager); + + bool Init(nsIPrincipal* aPrincipal, const nsACString& aFilter); + + mozilla::ipc::IPCResult RecvBind(const UDPAddressInfo& aAddressInfo, + const bool& aAddressReuse, + const bool& aLoopback, + const uint32_t& recvBufferSize, + const uint32_t& sendBufferSize); + mozilla::ipc::IPCResult RecvConnect(const UDPAddressInfo& aAddressInfo); + void DoSendConnectResponse(const UDPAddressInfo& aAddressInfo); + void SendConnectResponse(const nsCOMPtr<nsIEventTarget>& aThread, + const UDPAddressInfo& aAddressInfo); + void DoConnect(const nsCOMPtr<nsIUDPSocket>& aSocket, + const nsCOMPtr<nsIEventTarget>& aReturnThread, + const UDPAddressInfo& aAddressInfo); + + mozilla::ipc::IPCResult RecvOutgoingData(const UDPData& aData, + const UDPSocketAddr& aAddr); + + mozilla::ipc::IPCResult RecvClose(); + mozilla::ipc::IPCResult RecvRequestDelete(); + mozilla::ipc::IPCResult RecvJoinMulticast(const nsCString& aMulticastAddress, + const nsCString& aInterface); + mozilla::ipc::IPCResult RecvLeaveMulticast(const nsCString& aMulticastAddress, + const nsCString& aInterface); + + private: + virtual ~UDPSocketParent(); + + virtual void ActorDestroy(ActorDestroyReason why) override; + void Send(const nsTArray<uint8_t>& aData, const UDPSocketAddr& aAddr); + void Send(const IPCStream& aStream, const UDPSocketAddr& aAddr); + nsresult BindInternal(const nsCString& aHost, const uint16_t& aPort, + const bool& aAddressReuse, const bool& aLoopback, + const uint32_t& recvBufferSize, + const uint32_t& sendBufferSize); + nsresult ConnectInternal(const nsCString& aHost, const uint16_t& aPort); + void FireInternalError(uint32_t aLineNo); + void SendInternalError(const nsCOMPtr<nsIEventTarget>& aThread, + uint32_t aLineNo); + + PBackgroundParent* mBackgroundManager; + + bool mIPCOpen; + nsCOMPtr<nsIUDPSocket> mSocket; + nsCOMPtr<nsISocketFilter> mFilter; + nsCOMPtr<nsIPrincipal> mPrincipal; + UDPAddressInfo mAddress; +}; + +} // namespace dom +} // namespace mozilla + +#endif // !defined(mozilla_dom_UDPSocketParent_h__) diff --git a/dom/network/interfaces/moz.build b/dom/network/interfaces/moz.build new file mode 100644 index 0000000000..362ad4c228 --- /dev/null +++ b/dom/network/interfaces/moz.build @@ -0,0 +1,12 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + "nsITCPSocketCallback.idl", + "nsIUDPSocketChild.idl", +] + +XPIDL_MODULE = "dom_network" diff --git a/dom/network/interfaces/nsITCPSocketCallback.idl b/dom/network/interfaces/nsITCPSocketCallback.idl new file mode 100644 index 0000000000..4a2b7a9ef2 --- /dev/null +++ b/dom/network/interfaces/nsITCPSocketCallback.idl @@ -0,0 +1,58 @@ +/* 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/. */ + +/** + * MozTCPSocket exposes a TCP client and server sockets + * to highly privileged apps. It provides a buffered, non-blocking + * interface for sending. For receiving, it uses an asynchronous, + * event handler based interface. + */ + +#include "domstubs.idl" + +%{C++ +#include "nsTArrayForwardDeclare.h" +%} +[ref] native nsUint8TArrayRef(nsTArray<uint8_t>); +[ptr] native JSContextPtr(JSContext); + + +/* + * This interface is implemented in TCPSocket.cpp as an internal interface + * for use in cross-process socket implementation. + * Needed to account for multiple possible types that can be provided to + * the socket callbacks as arguments. + */ +[scriptable, uuid(ac2c4b69-cb79-4767-b1ce-bcf62945cd39)] +interface nsITCPSocketCallback : nsISupports { + // Limitation of TCPSocket's buffer size. + const unsigned long BUFFER_SIZE = 65536; + + // Dispatch an "error" event at this object with the given name and type. + void fireErrorEvent(in AString name, in AString type, in nsresult errorCode); + + // Dispatch a "data" event at this object with a string + void fireDataStringEvent(in AString type, in ACString data); + + // Dispatch a "data" event at this object with an Array + void fireDataArrayEvent(in AString type, [const] in nsUint8TArrayRef data); + + // Dispatch an event of the given type at this object. + void fireEvent(in AString type); + + // Update the DOM object's readyState. + // @param readyState + // new ready state + void updateReadyState(in unsigned long readystate); + + // Update the DOM object's bufferedAmount value with a tracking number to + // to allow tracking of which writes are "in-flight" + // @param bufferedAmount + // TCPSocket parent's bufferedAmount. + // @param trackingNumber + // A number to ensure the bufferedAmount is updated after data + // from child are sent to parent. + void updateBufferedAmount(in uint32_t bufferedAmount, + in uint32_t trackingNumber); +}; diff --git a/dom/network/interfaces/nsIUDPSocketChild.idl b/dom/network/interfaces/nsIUDPSocketChild.idl new file mode 100644 index 0000000000..38a8e43576 --- /dev/null +++ b/dom/network/interfaces/nsIUDPSocketChild.idl @@ -0,0 +1,30 @@ +/* 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 "nsISupports.idl" +#include "nsINetAddr.idl" + +interface nsIUDPSocketInternal; +interface nsIInputStream; +interface nsIPrincipal; +interface nsIEventTarget; + +/* + * Internal interface for callback from chrome process + */ +[scriptable, builtinclass, uuid(613dd3ad-598b-4da9-ad63-bbda50c20098)] +interface nsIUDPSocketInternal : nsISupports +{ + // callback while socket is opened. localPort and localAddress is ready until this time. + void callListenerOpened(); + // callback while socket is connected. + void callListenerConnected(); + // callback while socket is closed. + void callListenerClosed(); + // callback while incoming packet is received. + void callListenerReceivedData(in AUTF8String host, in unsigned short port, + in Array<uint8_t> data); + // callback while any error happened. + void callListenerError(in AUTF8String message, in AUTF8String filename, in uint32_t lineNumber); +}; diff --git a/dom/network/moz.build b/dom/network/moz.build new file mode 100644 index 0000000000..7b7e174a6d --- /dev/null +++ b/dom/network/moz.build @@ -0,0 +1,55 @@ +# -*- 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", "Networking") + +DIRS += ["interfaces"] + +MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.toml"] +MOCHITEST_MANIFESTS += ["tests/mochitest.toml"] + +EXPORTS.mozilla.dom += [ + "TCPServerSocket.h", + "TCPSocket.h", + "UDPSocket.h", +] + +EXPORTS.mozilla.dom.network += [ + "Connection.h", + "Constants.h", + "TCPServerSocketChild.h", + "TCPServerSocketParent.h", + "TCPSocketChild.h", + "TCPSocketParent.h", + "UDPSocketChild.h", + "UDPSocketParent.h", +] + +UNIFIED_SOURCES += [ + "Connection.cpp", + "ConnectionMainThread.cpp", + "ConnectionWorker.cpp", + "TCPServerSocket.cpp", + "TCPServerSocketChild.cpp", + "TCPServerSocketParent.cpp", + "TCPSocket.cpp", + "TCPSocketChild.cpp", + "TCPSocketParent.cpp", + "UDPSocket.cpp", + "UDPSocketChild.cpp", + "UDPSocketParent.cpp", +] + +IPDL_SOURCES += [ + "PTCPServerSocket.ipdl", + "PTCPSocket.ipdl", + "PUDPSocket.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/network/tests/chrome.toml b/dom/network/tests/chrome.toml new file mode 100644 index 0000000000..c22fe38572 --- /dev/null +++ b/dom/network/tests/chrome.toml @@ -0,0 +1,15 @@ +[DEFAULT] +support-files = [ + "tcpsocket_test.sys.mjs", + "test_tcpsocket_client_and_server_basics.js", + "file_postMessage_opener.html", + "file_udpsocket_iframe.html", +] + +["test_tcpsocket_client_and_server_basics.html"] + +["test_tcpsocket_jsm.html"] + +["test_tcpsocket_legacy.html"] + +["test_udpsocket.html"] diff --git a/dom/network/tests/file_postMessage_opener.html b/dom/network/tests/file_postMessage_opener.html new file mode 100644 index 0000000000..87359e25c2 --- /dev/null +++ b/dom/network/tests/file_postMessage_opener.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <script> + opener.postMessage("hello", "*"); + </script> +</head> +<body> +</body> +</html> diff --git a/dom/network/tests/file_udpsocket_iframe.html b/dom/network/tests/file_udpsocket_iframe.html new file mode 100644 index 0000000000..d48e03ee2b --- /dev/null +++ b/dom/network/tests/file_udpsocket_iframe.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test UDPSocket BFCache</title> +</head> +<body> +<script type="application/javascript"> +'use strict'; +window.addEventListener('load', function() { + let remotePort = parseInt(window.location.search.substring(1), 10); + let socket = new UDPSocket(); + socket.addEventListener('message', function () { + socket.send('fail', '127.0.0.1', remotePort); + }); + + socket.opened.then(function() { + socket.send('ready', '127.0.0.1', remotePort); + }); +}, {once: true}); +</script> +</body> +</html> diff --git a/dom/network/tests/mochitest.toml b/dom/network/tests/mochitest.toml new file mode 100644 index 0000000000..95597f856e --- /dev/null +++ b/dom/network/tests/mochitest.toml @@ -0,0 +1,10 @@ +[DEFAULT] +support-files = ["worker_network_basics.js"] + +["test_network_basics.html"] +skip-if = ["os == 'android'"] + +["test_network_basics_worker.html"] +skip-if = ["os == 'android'"] + +["test_tcpsocket_not_exposed_to_content.html"] diff --git a/dom/network/tests/tcpsocket_test.sys.mjs b/dom/network/tests/tcpsocket_test.sys.mjs new file mode 100644 index 0000000000..7a51aced41 --- /dev/null +++ b/dom/network/tests/tcpsocket_test.sys.mjs @@ -0,0 +1,12 @@ +export var createSocket = function (host, port, options) { + return new TCPSocket(host, port, options); +}; + +export var createServer = function (port, options, backlog) { + return new TCPServerSocket(port, options, backlog); +}; + +// See test_tcpsocket_client_and_server_basics.html's version for rationale. +export var socketCompartmentInstanceOfArrayBuffer = function (obj) { + return obj instanceof ArrayBuffer; +}; diff --git a/dom/network/tests/test_network_basics.html b/dom/network/tests/test_network_basics.html new file mode 100644 index 0000000000..e657af916d --- /dev/null +++ b/dom/network/tests/test_network_basics.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Network API</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 type="application/javascript"> + +/** Test for Network Information API **/ +function test() { + ok('connection' in navigator, "navigator.connection should exist"); + + ok(navigator.connection, "navigator.connection returns an object"); + + ok(navigator.connection instanceof EventTarget, + "navigator.connection is a EventTarget object"); + + ok('type' in navigator.connection, + "type should be a Connection attribute"); + is(navigator.connection.type, "none", + "By default connection.type equals to none"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({'set': [["dom.netinfo.enabled", true]]}, test); + +</script> +</pre> +</body> +</html> diff --git a/dom/network/tests/test_network_basics_worker.html b/dom/network/tests/test_network_basics_worker.html new file mode 100644 index 0000000000..5763ee361b --- /dev/null +++ b/dom/network/tests/test_network_basics_worker.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Network in workers API</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 type="application/javascript"> + +/** Test for Network Information API **/ +function test() { + let w = new Worker('worker_network_basics.js'); + w.onmessage = function(e) { + if (e.data.type == 'status') { + ok(e.data.status, e.data.msg); + } else if (e.data.type == 'finish') { + SimpleTest.finish(); + } else { + ok(false, "Unknown message type"); + } + } +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({'set': [["dom.netinfo.enabled", true]]}, test); + +</script> +</pre> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_client_and_server_basics.html b/dom/network/tests/test_tcpsocket_client_and_server_basics.html new file mode 100644 index 0000000000..d7e40a4328 --- /dev/null +++ b/dom/network/tests/test_tcpsocket_client_and_server_basics.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +Core tests for TCPSocket and TCPServerSocket that replace their previous +separate xpcshell incarnations. This migration and cleanup occurred as part +of bug 1084245 in order to get coverage of the tests from content. + +https://bugzilla.mozilla.org/show_bug.cgi?id=1084245 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1084245</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"/> + <script type="application/javascript"> + function createServer(port, options, backlog) { + return new TCPServerSocket(port, options, backlog); + } + + function createSocket(host, port, options) { + return new TCPSocket(host, port, options); + } + + // In the JSM case, ArrayBuffers will be created in the compartment of the + // JSM with different globals than the + // test_tcpsocket_client_and_server_basics.js test logic sees, so we (and + // tcpsocket_test.sys.mjs) need to do something. To avoid complexity relating + // to wrappers and the varying nuances of the module scope and global scope + // in JSM's (they differ on B2G), we hardcode ArrayBuffer rather than taking + // a string that we look up, etc. + function socketCompartmentInstanceOfArrayBuffer(obj) { + return obj instanceof ArrayBuffer; + } + </script> + <script type="application/javascript" src="test_tcpsocket_client_and_server_basics.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1084245">Mozilla Bug 1084245</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_client_and_server_basics.js b/dom/network/tests/test_tcpsocket_client_and_server_basics.js new file mode 100644 index 0000000000..bbb91e410f --- /dev/null +++ b/dom/network/tests/test_tcpsocket_client_and_server_basics.js @@ -0,0 +1,617 @@ +"use strict"; + +// These are defined in test_tcpsocket_client_and_server_basics.html +/* global createServer, createSocket, socketCompartmentInstanceOfArrayBuffer */ + +// Bug 788960 and later bug 1329245 have taught us that attempting to connect to +// a port that is not listening or is no longer listening fails to consistently +// result in the error (or any) event we expect on Darwin/OSX/"OS X". +const isOSX = Services.appinfo.OS === "Darwin"; +const testConnectingToNonListeningPort = !isOSX; + +const SERVER_BACKLOG = -1; + +const SOCKET_EVENTS = ["open", "data", "drain", "error", "close"]; + +function concatUint8Arrays(a, b) { + let newArr = new Uint8Array(a.length + b.length); + newArr.set(a, 0); + newArr.set(b, a.length); + return newArr; +} + +function assertUint8ArraysEqual(a, b, comparingWhat) { + if (a.length !== b.length) { + ok( + false, + comparingWhat + + " arrays do not have the same length; " + + a.length + + " versus " + + b.length + ); + return; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + ok( + false, + comparingWhat + + " arrays differ at index " + + i + + a[i] + + " versus " + + b[i] + ); + return; + } + } + ok(true, comparingWhat + " arrays were equivalent."); +} + +/** + * Helper method to add event listeners to a socket and provide two Promise-returning + * helpers (see below for docs on them). This *must* be called during the turn of + * the event loop where TCPSocket's constructor is called or the onconnect method is being + * invoked. + */ +function listenForEventsOnSocket(socket, socketType) { + let wantDataLength = null; + let wantDataAndClose = false; + let pendingResolve = null; + let receivedEvents = []; + let receivedData = null; + let handleGenericEvent = function (event) { + dump("(" + socketType + " event: " + event.type + ")\n"); + if (pendingResolve && wantDataLength === null) { + pendingResolve(event); + pendingResolve = null; + } else { + receivedEvents.push(event); + } + }; + + socket.onopen = handleGenericEvent; + socket.ondrain = handleGenericEvent; + socket.onerror = handleGenericEvent; + socket.onclose = function (event) { + if (!wantDataAndClose) { + handleGenericEvent(event); + } else if (pendingResolve) { + dump("(" + socketType + " event: close)\n"); + pendingResolve(receivedData); + pendingResolve = null; + wantDataAndClose = false; + } + }; + socket.ondata = function (event) { + dump( + "(" + + socketType + + " event: " + + event.type + + " length: " + + event.data.byteLength + + ")\n" + ); + ok( + socketCompartmentInstanceOfArrayBuffer(event.data), + "payload is ArrayBuffer" + ); + var arr = new Uint8Array(event.data); + if (receivedData === null) { + receivedData = arr; + } else { + receivedData = concatUint8Arrays(receivedData, arr); + } + if (wantDataLength !== null && receivedData.length >= wantDataLength) { + pendingResolve(receivedData); + pendingResolve = null; + receivedData = null; + wantDataLength = null; + } + }; + + return { + /** + * Return a Promise that will be resolved with the next (non-data) event + * received by the socket. If there are queued events, the Promise will + * be immediately resolved (but you won't see that until a future turn of + * the event loop). + */ + waitForEvent() { + if (pendingResolve) { + throw new Error("only one wait allowed at a time."); + } + + if (receivedEvents.length) { + return Promise.resolve(receivedEvents.shift()); + } + + dump("(" + socketType + " waiting for event)\n"); + return new Promise(function (resolve, reject) { + pendingResolve = resolve; + }); + }, + /** + * Return a Promise that will be resolved with a Uint8Array of at least the + * given length. We buffer / accumulate received data until we have enough + * data. Data is buffered even before you call this method, so be sure to + * explicitly wait for any and all data sent by the other side. + */ + waitForDataWithAtLeastLength(length) { + if (pendingResolve) { + throw new Error("only one wait allowed at a time."); + } + if (receivedData && receivedData.length >= length) { + let promise = Promise.resolve(receivedData); + receivedData = null; + return promise; + } + dump("(" + socketType + " waiting for " + length + " bytes)\n"); + return new Promise(function (resolve, reject) { + pendingResolve = resolve; + wantDataLength = length; + }); + }, + waitForAnyDataAndClose() { + if (pendingResolve) { + throw new Error("only one wait allowed at a time."); + } + + return new Promise(function (resolve, reject) { + pendingResolve = resolve; + // we may receive no data before getting close, in which case we want to + // return an empty array + receivedData = new Uint8Array(); + wantDataAndClose = true; + }); + }, + }; +} + +/** + * Return a promise that is resolved when the server receives a connection. The + * promise is resolved with { socket, queue } where `queue` is the result of + * calling listenForEventsOnSocket(socket). This must be done because we need + * to add the event listener during the connection. + */ +function waitForConnection(listeningServer) { + return new Promise(function (resolve, reject) { + // Because of the event model of sockets, we can't use the + // listenForEventsOnSocket mechanism; we need to hook up listeners during + // the connect event. + listeningServer.onconnect = function (event) { + // Clobber the listener to get upset if it receives any more connections + // after this. + listeningServer.onconnect = function () { + ok(false, "Received a connection when not expecting one."); + }; + ok(true, "Listening server accepted socket"); + resolve({ + socket: event.socket, + queue: listenForEventsOnSocket(event.socket, "server"), + }); + }; + }); +} + +function defer() { + var deferred = {}; + deferred.promise = new Promise(function (resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); + return deferred; +} + +async function test_basics() { + // See bug 903830; in e10s mode we never get to find out the localPort if we + // let it pick a free port by choosing 0. This is the same port the xpcshell + // test was using. + let serverPort = 8085; + + // - Start up a listening socket. + let listeningServer = createServer( + serverPort, + { binaryType: "arraybuffer" }, + SERVER_BACKLOG + ); + + let connectedPromise = waitForConnection(listeningServer); + + // -- Open a connection to the server + let clientSocket = createSocket("127.0.0.1", serverPort, { + binaryType: "arraybuffer", + }); + let clientQueue = listenForEventsOnSocket(clientSocket, "client"); + + // (the client connects) + is((await clientQueue.waitForEvent()).type, "open", "got open event"); + is(clientSocket.readyState, "open", "client readyState is open"); + + // (the server connected) + let { socket: serverSocket, queue: serverQueue } = await connectedPromise; + is(serverSocket.readyState, "open", "server readyState is open"); + + // -- Simple send / receive + // - Send data from client to server + // (But not so much we cross the drain threshold.) + let smallUint8Array = new Uint8Array(256); + for (let i = 0; i < smallUint8Array.length; i++) { + smallUint8Array[i] = i; + } + is( + clientSocket.send(smallUint8Array.buffer, 0, smallUint8Array.length), + true, + "Client sending less than 64k, buffer should not be full." + ); + + let serverReceived = await serverQueue.waitForDataWithAtLeastLength(256); + assertUint8ArraysEqual( + serverReceived, + smallUint8Array, + "Server received/client sent" + ); + + // - Send data from server to client + // (But not so much we cross the drain threshold.) + is( + serverSocket.send(smallUint8Array.buffer, 0, smallUint8Array.length), + true, + "Server sending less than 64k, buffer should not be full." + ); + + let clientReceived = await clientQueue.waitForDataWithAtLeastLength(256); + assertUint8ArraysEqual( + clientReceived, + smallUint8Array, + "Client received/server sent" + ); + + // -- Perform sending multiple times with different buffer slices + // - Send data from client to server + // (But not so much we cross the drain threshold.) + is( + clientSocket.send(smallUint8Array.buffer, 0, 7), + true, + "Client sending less than 64k, buffer should not be full." + ); + is( + clientSocket.send(smallUint8Array.buffer, 7, smallUint8Array.length - 7), + true, + "Client sending less than 64k, buffer should not be full." + ); + + serverReceived = await serverQueue.waitForDataWithAtLeastLength(256); + assertUint8ArraysEqual( + serverReceived, + smallUint8Array, + "Server received/client sent" + ); + + // - Send data from server to client + // (But not so much we cross the drain threshold.) + is( + serverSocket.send(smallUint8Array.buffer, 0, 7), + true, + "Server sending less than 64k, buffer should not be full." + ); + is( + serverSocket.send(smallUint8Array.buffer, 7, smallUint8Array.length - 7), + true, + "Server sending less than 64k, buffer should not be full." + ); + + clientReceived = await clientQueue.waitForDataWithAtLeastLength(256); + assertUint8ArraysEqual( + clientReceived, + smallUint8Array, + "Client received/server sent" + ); + + // -- Send "big" data in both directions + // (Enough to cross the buffering/drain threshold; 64KiB) + let bigUint8Array = new Uint8Array(65536 + 3); + for (let i = 0; i < bigUint8Array.length; i++) { + bigUint8Array[i] = i % 256; + } + // This can be anything from 1 to 65536. The idea is spliting and sending + // bigUint8Array in two chunks should trigger ondrain the same as sending + // bigUint8Array in one chunk. + let lengthOfChunk1 = 65536; + is( + clientSocket.send(bigUint8Array.buffer, 0, lengthOfChunk1), + true, + "Client sending chunk1 should not result in the buffer being full." + ); + // Do this twice so we have confidence that the 'drain' event machinery + // doesn't break after the first use. The first time we send bigUint8Array in + // two chunks, the second time we send bigUint8Array in one chunk. + for (let iSend = 0; iSend < 2; iSend++) { + // - Send "big" data from the client to the server + let offset = iSend == 0 ? lengthOfChunk1 : 0; + is( + clientSocket.send(bigUint8Array.buffer, offset, bigUint8Array.length), + false, + "Client sending more than 64k should result in the buffer being full." + ); + is( + (await clientQueue.waitForEvent()).type, + "drain", + "The drain event should fire after a large send that indicated full." + ); + + serverReceived = await serverQueue.waitForDataWithAtLeastLength( + bigUint8Array.length + ); + assertUint8ArraysEqual( + serverReceived, + bigUint8Array, + "server received/client sent" + ); + + if (iSend == 0) { + is( + serverSocket.send(bigUint8Array.buffer, 0, lengthOfChunk1), + true, + "Server sending chunk1 should not result in the buffer being full." + ); + } + // - Send "big" data from the server to the client + is( + serverSocket.send(bigUint8Array.buffer, offset, bigUint8Array.length), + false, + "Server sending more than 64k should result in the buffer being full." + ); + is( + (await serverQueue.waitForEvent()).type, + "drain", + "The drain event should fire after a large send that indicated full." + ); + + clientReceived = await clientQueue.waitForDataWithAtLeastLength( + bigUint8Array.length + ); + assertUint8ArraysEqual( + clientReceived, + bigUint8Array, + "client received/server sent" + ); + } + + // -- Server closes the connection + serverSocket.close(); + is( + serverSocket.readyState, + "closing", + "readyState should be closing immediately after calling close" + ); + + is( + (await clientQueue.waitForEvent()).type, + "close", + "The client should get a close event when the server closes." + ); + is( + clientSocket.readyState, + "closed", + "client readyState should be closed after close event" + ); + is( + (await serverQueue.waitForEvent()).type, + "close", + "The server should get a close event when it closes itself." + ); + is( + serverSocket.readyState, + "closed", + "server readyState should be closed after close event" + ); + + // -- Re-establish connection + connectedPromise = waitForConnection(listeningServer); + clientSocket = createSocket("127.0.0.1", serverPort, { + binaryType: "arraybuffer", + }); + clientQueue = listenForEventsOnSocket(clientSocket, "client"); + is((await clientQueue.waitForEvent()).type, "open", "got open event"); + + let connectedResult = await connectedPromise; + // destructuring assignment is not yet ES6 compliant, must manually unpack + serverSocket = connectedResult.socket; + serverQueue = connectedResult.queue; + + // -- Client closes the connection + clientSocket.close(); + is( + clientSocket.readyState, + "closing", + "client readyState should be losing immediately after calling close" + ); + + is( + (await clientQueue.waitForEvent()).type, + "close", + "The client should get a close event when it closes itself." + ); + is( + clientSocket.readyState, + "closed", + "client readyState should be closed after the close event is received" + ); + is( + (await serverQueue.waitForEvent()).type, + "close", + "The server should get a close event when the client closes." + ); + is( + serverSocket.readyState, + "closed", + "server readyState should be closed after the close event is received" + ); + + // -- Re-establish connection + connectedPromise = waitForConnection(listeningServer); + clientSocket = createSocket("127.0.0.1", serverPort, { + binaryType: "arraybuffer", + }); + clientQueue = listenForEventsOnSocket(clientSocket, "client"); + is((await clientQueue.waitForEvent()).type, "open", "got open event"); + + connectedResult = await connectedPromise; + // destructuring assignment is not yet ES6 compliant, must manually unpack + serverSocket = connectedResult.socket; + serverQueue = connectedResult.queue; + + // -- Call close after enqueueing a lot of data, make sure it goes through. + // We'll have the client send and close. + is( + clientSocket.send(bigUint8Array.buffer, 0, bigUint8Array.length), + false, + "Client sending more than 64k should result in the buffer being full." + ); + clientSocket.close(); + // The drain will still fire + is( + (await clientQueue.waitForEvent()).type, + "drain", + "The drain event should fire after a large send that returned true." + ); + // Then we'll get a close + is( + (await clientQueue.waitForEvent()).type, + "close", + "The close event should fire after the drain event." + ); + + // The server will get its data + serverReceived = await serverQueue.waitForDataWithAtLeastLength( + bigUint8Array.length + ); + assertUint8ArraysEqual( + serverReceived, + bigUint8Array, + "server received/client sent" + ); + // And a close. + is( + (await serverQueue.waitForEvent()).type, + "close", + "The drain event should fire after a large send that returned true." + ); + + // -- Re-establish connection + connectedPromise = waitForConnection(listeningServer); + clientSocket = createSocket("127.0.0.1", serverPort, { + binaryType: "string", + }); + clientQueue = listenForEventsOnSocket(clientSocket, "client"); + is((await clientQueue.waitForEvent()).type, "open", "got open event"); + + connectedResult = await connectedPromise; + // destructuring assignment is not yet ES6 compliant, must manually unpack + serverSocket = connectedResult.socket; + serverQueue = connectedResult.queue; + + // -- Attempt to send non-string data. + // Restore the original behavior by replacing toString with + // Object.prototype.toString. (bug 1121938) + bigUint8Array.toString = Object.prototype.toString; + is( + clientSocket.send(bigUint8Array), + true, + "Client sending a large non-string should only send a small string." + ); + clientSocket.close(); + // The server will get its data + serverReceived = await serverQueue.waitForDataWithAtLeastLength( + bigUint8Array.toString().length + ); + // Then we'll get a close + is( + (await clientQueue.waitForEvent()).type, + "close", + "The close event should fire after the drain event." + ); + + // -- Re-establish connection (Test for Close Immediately) + connectedPromise = waitForConnection(listeningServer); + clientSocket = createSocket("127.0.0.1", serverPort, { + binaryType: "arraybuffer", + }); + clientQueue = listenForEventsOnSocket(clientSocket, "client"); + is((await clientQueue.waitForEvent()).type, "open", "got open event"); + + connectedResult = await connectedPromise; + // destructuring assignment is not yet ES6 compliant, must manually unpack + serverSocket = connectedResult.socket; + serverQueue = connectedResult.queue; + + // -- Attempt to send two non-string data. + is( + clientSocket.send(bigUint8Array.buffer, 0, bigUint8Array.length), + false, + "Server sending more than 64k should result in the buffer being full." + ); + is( + clientSocket.send(bigUint8Array.buffer, 0, bigUint8Array.length), + false, + "Server sending more than 64k should result in the buffer being full." + ); + clientSocket.closeImmediately(); + + serverReceived = await serverQueue.waitForAnyDataAndClose(); + + is( + serverReceived.length < 2 * bigUint8Array.length, + true, + "Received array length less than sent array length" + ); + + // -- Close the listening server (and try to connect) + // We want to verify that the server actually closes / stops listening when + // we tell it to. + listeningServer.close(); + + // (We don't run this check on OS X where it's flakey; see definition up top.) + if (testConnectingToNonListeningPort) { + // - try and connect, get an error + clientSocket = createSocket("127.0.0.1", serverPort, { + binaryType: "arraybuffer", + }); + clientQueue = listenForEventsOnSocket(clientSocket, "client"); + is((await clientQueue.waitForEvent()).type, "error", "fail to connect"); + is( + clientSocket.readyState, + "closed", + "client readyState should be closed after the failure to connect" + ); + } +} + +add_task(test_basics); + +/** + * Test that TCPSocket works with ipv6 address. + */ +add_task(async function test_ipv6() { + const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" + ); + let deferred = defer(); + let httpServer = new HttpServer(); + httpServer.start_ipv6(-1); + + let clientSocket = new TCPSocket("::1", httpServer.identity.primaryPort); + clientSocket.onopen = () => { + ok(true, "Connect to ipv6 address succeeded"); + deferred.resolve(); + }; + clientSocket.onerror = () => { + ok(false, "Connect to ipv6 address failed"); + deferred.reject(); + }; + await deferred.promise; + await httpServer.stop(); +}); diff --git a/dom/network/tests/test_tcpsocket_jsm.html b/dom/network/tests/test_tcpsocket_jsm.html new file mode 100644 index 0000000000..026d71ed14 --- /dev/null +++ b/dom/network/tests/test_tcpsocket_jsm.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <meta charset="utf-8"> + <title>Test for 1207090</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"/> + <script type="application/javascript"> + // These are used in the loaded script file. + // eslint-disable-next-line no-unused-vars + let { + createSocket, + createServer, + socketCompartmentInstanceOfArrayBuffer + } = ChromeUtils.importESModule("chrome://mochitests/content/chrome/dom/network/tests/tcpsocket_test.sys.mjs"); + </script> + <script type="application/javascript" src="test_tcpsocket_client_and_server_basics.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1207090">Mozilla Bug 1207090</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<div id="container"></div> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_legacy.html b/dom/network/tests/test_tcpsocket_legacy.html new file mode 100644 index 0000000000..f5c76fb37f --- /dev/null +++ b/dom/network/tests/test_tcpsocket_legacy.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test of legacy navigator interface for opening TCPSocket/TCPServerSocket. +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 885982</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> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1084245">Mozilla Bug 1084245</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script> + SimpleTest.waitForExplicitFinish(); + + function runTest() { + // See bug 903830; in e10s mode we never get to find out the localPort if we + // let it pick a free port by choosing 0. This is the same port the xpcshell + // test was using. + var serverPort = 8085; + + var listeningServer = navigator.mozTCPSocket.listen(serverPort, + { binaryType: 'arraybuffer' }, + -1); + listeningServer.onconnect = function(ev) { + ok(true, "got server connect"); + listeningServer.close(); + listeningServer = null; + ev.socket.close() + } + + var clientSocket = navigator.mozTCPSocket.open('127.0.0.1', serverPort, + { binaryType: 'arraybuffer' }); + clientSocket.onopen = function() { ok(true, "got client open"); } + clientSocket.onclose = function() { + ok(true, "got client close"); + clientSocket.close(); + clientSocket = null; + setTimeout(function() { + // This just helps the test harness clean up quickly + SpecialPowers.forceCC(); + SpecialPowers.forceGC(); + SimpleTest.finish(); + }, 0); + } + } + runTest(); // we used to invoke this as part of a moot pref-setting callback +</script> +</body> +</html> diff --git a/dom/network/tests/test_tcpsocket_not_exposed_to_content.html b/dom/network/tests/test_tcpsocket_not_exposed_to_content.html new file mode 100644 index 0000000000..4154eeca5c --- /dev/null +++ b/dom/network/tests/test_tcpsocket_not_exposed_to_content.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test to ensure TCPSocket permission enabled and no tcp-socket perm does not allow open</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 type="application/javascript"> +/** + * TCPSocket and its legacy mozTCPSocket variant should never be exposed to + * content. + */ + +is('TCPSocket' in this, false, "TCPSocket should not be accessible to content"); +is('TCPServerSocket' in this, false, "TCPServerSocket should not be accessible to content"); +is('mozTCPSocket' in navigator, false, "mozTCPSocket should not be accessible to content"); +</script> +</pre> +</body> +</html> diff --git a/dom/network/tests/test_udpsocket.html b/dom/network/tests/test_udpsocket.html new file mode 100644 index 0000000000..1ca42f2432 --- /dev/null +++ b/dom/network/tests/test_udpsocket.html @@ -0,0 +1,403 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test UDPSocket API</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> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<iframe id="iframe"></iframe> +<pre id="test"> +<script type="application/javascript"> +'use strict'; +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +const HELLO_WORLD = 'hlo wrld. '; +const DATA_ARRAY = [0, 255, 254, 0, 1, 2, 3, 0, 255, 255, 254, 0]; +const DATA_ARRAY_BUFFER = new ArrayBuffer(DATA_ARRAY.length); +const TYPED_DATA_ARRAY = new Uint8Array(DATA_ARRAY_BUFFER); +const BIG_ARRAY = new Array(4096); +const BIG_ARRAY_BUFFER = new ArrayBuffer(BIG_ARRAY.length); +const BIG_TYPED_ARRAY = new Uint8Array(BIG_ARRAY_BUFFER); + +for (let i = 0; i < BIG_ARRAY.length; i++) { + BIG_ARRAY[i] = Math.floor(Math.random() * 256); +} + +TYPED_DATA_ARRAY.set(DATA_ARRAY); +BIG_TYPED_ARRAY.set(BIG_ARRAY); + +function is_same_buffer(recv_data, expect_data) { + let recv_dataview = new Uint8Array(recv_data); + let expected_dataview = new Uint8Array(expect_data); + + if (recv_dataview.length !== expected_dataview.length) { + return false; + } + + for (let i = 0; i < recv_dataview.length; i++) { + if (recv_dataview[i] != expected_dataview[i]) { + info('discover byte differenct at ' + i); + return false; + } + } + return true; +} + +function testOpen() { + info('test for creating an UDP Socket'); + let socket = new UDPSocket(); + is(socket.localPort, null, 'expect no local port before socket opened'); + is(socket.localAddress, null, 'expect no local address before socket opened'); + is(socket.remotePort, null, 'expected no default remote port'); + is(socket.remoteAddress, null, 'expected no default remote address'); + is(socket.readyState, 'opening', 'expected ready state = opening'); + is(socket.loopback, false, 'expected no loopback'); + is(socket.addressReuse, true, 'expect to reuse address'); + + return socket.opened.then(function() { + ok(true, 'expect openedPromise to be resolved after successful socket binding'); + ok(!(socket.localPort === 0), 'expect allocated a local port'); + is(socket.localAddress, '0.0.0.0', 'expect assigned to default address'); + is(socket.readyState, 'open', 'expected ready state = open'); + + return socket; + }); +} + +function testSendString(socket) { + info('test for sending string data'); + + socket.send(HELLO_WORLD, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function(msg) { + let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data)); + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + is(recvData, HELLO_WORLD, 'expected same string data'); + resolve(socket); + }, {once: true}); + }); +} + +function testSendArrayBuffer(socket) { + info('test for sending ArrayBuffer'); + + socket.send(DATA_ARRAY_BUFFER, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function(msg) { + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + ok(is_same_buffer(msg.data, DATA_ARRAY_BUFFER), 'expected same buffer data'); + resolve(socket); + }, {once: true}); + }); +} + +function testSendArrayBufferView(socket) { + info('test for sending ArrayBufferView'); + + socket.send(TYPED_DATA_ARRAY, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function(msg) { + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + ok(is_same_buffer(msg.data, TYPED_DATA_ARRAY), 'expected same buffer data'); + resolve(socket); + }, {once: true}); + }); +} + +function testSendBlob(socket) { + info('test for sending Blob'); + + let blob = new Blob([HELLO_WORLD], {type : 'text/plain'}); + socket.send(blob, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function(msg) { + let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data)); + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + is(recvData, HELLO_WORLD, 'expected same string data'); + resolve(socket); + }, {once: true}); + }); +} + +function testSendBigArray(socket) { + info('test for sending Big ArrayBuffer'); + + socket.send(BIG_TYPED_ARRAY, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + let byteReceived = 0; + socket.addEventListener('message', function recv_callback(msg) { + let byteBegin = byteReceived; + byteReceived += msg.data.byteLength; + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + ok(is_same_buffer(msg.data, BIG_TYPED_ARRAY.subarray(byteBegin, byteReceived)), 'expected same buffer data [' + byteBegin+ '-' + byteReceived + ']'); + if (byteReceived >= BIG_TYPED_ARRAY.length) { + socket.removeEventListener('message', recv_callback); + resolve(socket); + } + }); + }); +} + +function testSendBigBlob(socket) { + info('test for sending Big Blob'); + + let blob = new Blob([BIG_TYPED_ARRAY]); + socket.send(blob, '127.0.0.1', socket.localPort); + + return new Promise(function(resolve, reject) { + let byteReceived = 0; + socket.addEventListener('message', function recv_callback(msg) { + let byteBegin = byteReceived; + byteReceived += msg.data.byteLength; + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + ok(is_same_buffer(msg.data, BIG_TYPED_ARRAY.subarray(byteBegin, byteReceived)), 'expected same buffer data [' + byteBegin+ '-' + byteReceived + ']'); + if (byteReceived >= BIG_TYPED_ARRAY.length) { + socket.removeEventListener('message', recv_callback); + resolve(socket); + } + }); + }); +} + +function testUDPOptions(socket) { + info('test for UDP init options'); + + let remoteSocket = new UDPSocket({addressReuse: false, + loopback: true, + localAddress: '127.0.0.1', + remoteAddress: '127.0.0.1', + remotePort: socket.localPort}); + is(remoteSocket.localAddress, '127.0.0.1', 'expected local address'); + is(remoteSocket.remoteAddress, '127.0.0.1', 'expected remote address'); + is(remoteSocket.remotePort, socket.localPort, 'expected remote port'); + is(remoteSocket.addressReuse, false, 'expected address not reusable'); + is(remoteSocket.loopback, true, 'expected loopback mode is on'); + + return remoteSocket.opened.then(function() { + remoteSocket.send(HELLO_WORLD); + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function(msg) { + let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data)); + is(msg.remotePort, remoteSocket.localPort, 'expected packet from ' + remoteSocket.localPort); + is(recvData, HELLO_WORLD, 'expected same string data'); + resolve(socket); + }, {once: true}); + }); + }); +} + +function testClose(socket) { + info('test for close'); + + socket.close(); + is(socket.readyState, 'closed', 'expect ready state to be "closed"'); + try { + socket.send(HELLO_WORLD, '127.0.0.1', socket.localPort); + ok(false, 'unexpect to send successfully'); + } catch (e) { + ok(true, 'expected send fail after socket closed'); + } + + return socket.closed.then(function() { + ok(true, 'expected closedPromise is resolved after socket.close()'); + }); +} + +function testMulticast() { + info('test for multicast'); + + let socket = new UDPSocket({loopback: true}); + + const MCAST_ADDRESS = '224.0.0.255'; + socket.joinMulticastGroup(MCAST_ADDRESS); + + return socket.opened.then(function() { + socket.send(HELLO_WORLD, MCAST_ADDRESS, socket.localPort); + + return new Promise(function(resolve, reject) { + socket.addEventListener('message', function(msg) { + let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data)); + is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort); + is(recvData, HELLO_WORLD, 'expected same string data'); + socket.leaveMulticastGroup(MCAST_ADDRESS); + resolve(); + }, {once: true}); + }); + }); +} + +function testInvalidUDPOptions() { + info('test for invalid UDPOptions'); + try { + new UDPSocket({localAddress: 'not-a-valid-address'}); + ok(false, 'should not create an UDPSocket with an invalid localAddress'); + } catch (e) { + is(e.name, 'InvalidAccessError', 'expected InvalidAccessError will be thrown if localAddress is not a valid IPv4/6 address'); + } + + try { + new UDPSocket({localPort: 0}); + ok(false, 'should not create an UDPSocket with an invalid localPort'); + } catch (e) { + is(e.name, 'InvalidAccessError', 'expected InvalidAccessError will be thrown if localPort is not a valid port number'); + } + + try { + new UDPSocket({remotePort: 0}); + ok(false, 'should not create an UDPSocket with an invalid remotePort'); + } catch (e) { + is(e.name, 'InvalidAccessError', 'expected InvalidAccessError will be thrown if localPort is not a valid port number'); + } +} + +function testOpenFailed() { + info('test for falied on open'); + + //according to RFC5737, address block 192.0.2.0/24 should not be used in both local and public contexts + let socket = new UDPSocket({localAddress: '192.0.2.0'}); + + return socket.opened.then(function() { + ok(false, 'should not resolve openedPromise while fail to bind socket'); + socket.close(); + }).catch(function(reason) { + is(reason.name, 'NetworkError', 'expected openedPromise to be rejected while fail to bind socket'); + }); +} + +function testSendBeforeOpen() { + info('test for send before open'); + + let socket = new UDPSocket(); + + try { + socket.send(HELLO_WORLD, '127.0.0.1', 9); + ok(false, 'unexpect to send successfully'); + } catch (e) { + ok(true, 'expected send fail before openedPromise is resolved'); + } + + return socket.opened.then(function() { + socket.close(); + }); +} + +function testCloseBeforeOpened() { + info('test for close socket before opened'); + + let socket = new UDPSocket(); + socket.opened.then(function() { + ok(false, 'should not resolve openedPromise if it has already been closed'); + }).catch(function(reason) { + is(reason.name, 'AbortError', 'expected openedPromise to be rejected while socket is closed during opening'); + }); + + return socket.close().then(function() { + ok(true, 'expected closedPromise to be resolved'); + }).then(socket.opened); +} + +function testOpenWithoutClose() { + info('test for open without close'); + + let closed = []; + for (let i = 0; i < 50; i++) { + let socket = new UDPSocket(); + closed.push(socket.closed); + } + + SpecialPowers.gc(); + info('all unrefereced socket should be closed right after GC'); + + return Promise.all(closed); +} + +function awaitEvent(target, event) { + return new Promise(resolve => { + target.addEventListener(event, resolve, { once: true }); + }); +} + +async function testBFCache() { + info('test for bfcache behavior'); + + let socket = new UDPSocket(); + + await socket.opened; + + let win = window.open(`file_udpsocket_iframe.html?${socket.localPort}`); + + let msg = await awaitEvent(socket, "message"); + + win.location = "file_postMessage_opener.html"; + await awaitEvent(window, "message"); + + socket.send(HELLO_WORLD, '127.0.0.1', msg.remotePort); + + await new Promise(resolve => { + function recv_again_callback(message) { + socket.removeEventListener('message', recv_again_callback); + ok(false, 'should not receive packet after page unload'); + } + + socket.addEventListener('message', recv_again_callback); + + setTimeout(function() { + socket.removeEventListener('message', recv_again_callback); + socket.close(); + resolve(); + }, 5000); + }); + + win.close(); +} + +function runTest() { + testOpen() + .then(testSendString) + .then(testSendArrayBuffer) + .then(testSendArrayBufferView) + .then(testSendBlob) + .then(testSendBigArray) + .then(testSendBigBlob) + .then(testUDPOptions) + .then(testClose) + .then(testMulticast) + .then(testInvalidUDPOptions) + .then(testOpenFailed) + .then(testSendBeforeOpen) + .then(testCloseBeforeOpened) + .then(testOpenWithoutClose) + .then(testBFCache) + .then(function() { + info('test finished'); + SimpleTest.finish(); + }) + .catch(function(err) { + ok(false, 'test failed due to: ' + err); + SimpleTest.finish(); + }); +} + +window.addEventListener('load', function () { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['dom.udpsocket.enabled', true], + ['browser.sessionhistory.max_total_viewers', 10] + ] + }, runTest); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/network/tests/worker_network_basics.js b/dom/network/tests/worker_network_basics.js new file mode 100644 index 0000000000..25e8c5dd8e --- /dev/null +++ b/dom/network/tests/worker_network_basics.js @@ -0,0 +1,28 @@ +function ok(a, msg) { + postMessage({ type: "status", status: !!a, msg }); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function finish() { + postMessage({ type: "finish" }); +} + +ok("connection" in navigator, "navigator.connection should exist"); + +ok(navigator.connection, "navigator.connection returns an object"); + +ok( + navigator.connection instanceof EventTarget, + "navigator.connection is a EventTarget object" +); + +ok("type" in navigator.connection, "type should be a Connection attribute"); +is( + navigator.connection.type, + "none", + "By default connection.type equals to none" +); +finish(); |