/* -*- 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/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::Constructor(const GlobalObject& aGlobal, const UDPOptions& aOptions, ErrorResult& aRv) { nsCOMPtr 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 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); PRNetAddr prAddr; PRStatus status = PR_StringToNetAddr(address.BeginReading(), &prAddr); if (status != PR_SUCCESS) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return nullptr; } } else { SetDOMStringToNull(localAddress); } Nullable 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 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& 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 aGivenProto) { return UDPSocket_Binding::Wrap(aCx, this, aGivenProto); } void UDPSocket::DisconnectFromOwner() { DOMEventTargetHelper::DisconnectFromOwner(); CloseWithReason(NS_OK); } already_AddRefed UDPSocket::Close() { MOZ_ASSERT(mClosed); RefPtr 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& aRemoteAddress, const Optional>& 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 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 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 if (aData.IsArrayBuffer()) { const ArrayBuffer& data = aData.GetAsArrayBuffer(); data.ComputeState(); aRv = strStream->SetData(reinterpret_cast(data.Data()), data.Length()); } else { const ArrayBufferView& data = aData.GetAsArrayBufferView(); data.ComputeState(); aRv = strStream->SetData(reinterpret_cast(data.Data()), data.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 sock = do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); if (NS_FAILED(rv)) { return rv; } nsCOMPtr global = do_QueryInterface(GetOwner(), &rv); if (NS_FAILED(rv)) { return rv; } nsCOMPtr 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 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 sock = new UDPSocketChild(); mListenerProxy = new ListenerProxy(this); nsCOMPtr obj = do_QueryInterface(GetOwner(), &rv); if (NS_FAILED(rv)) { return rv; } nsCOMPtr principal = obj->PrincipalOrNull(); if (!principal) { return NS_ERROR_FAILURE; } nsCOMPtr target; if (nsCOMPtr global = GetOwnerGlobal()) { target = global->EventTargetFor(TaskCategory::Other); } rv = sock->Bind(mListenerProxy, principal, NS_ConvertUTF16toUTF8(aLocalAddress), aLocalPort, mAddressReuse, mLoopback, 0, 0, target); if (NS_FAILED(rv)) { return rv; } mSocketChild = sock; return NS_OK; } nsresult UDPSocket::Init(const nsString& aLocalAddress, const Nullable& aLocalPort, const bool& aAddressReuse, const bool& aLoopback) { MOZ_ASSERT(!mSocket && !mSocketChild); mLocalAddress = aLocalAddress; mLocalPort = aLocalPort; mAddressReuse = aAddressReuse; mLoopback = aLoopback; ErrorResult rv; nsCOMPtr 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 mSocket; }; nsCOMPtr runnable = new OpenSocketRunnable(this); return NS_DispatchToMainThread(runnable); } void UDPSocket::HandleReceivedData(const nsACString& aRemoteAddress, const uint16_t& aRemotePort, const nsTArray& 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& aData) { AutoJSAPI jsapi; if (NS_WARN_IF(!jsapi.Init(GetOwner()))) { return NS_ERROR_FAILURE; } JSContext* cx = jsapi.cx(); // Copy packet data to ArrayBuffer JS::Rooted arrayBuf( cx, ArrayBuffer::Create(cx, aData.Length(), aData.Elements())); if (NS_WARN_IF(!arrayBuf)) { return NS_ERROR_FAILURE; } JS::Rooted jsData(cx, JS::ObjectValue(*arrayBuf)); // Create DOM event RootedDictionary init(cx); CopyUTF8toUTF16(aRemoteAddress, init.mRemoteAddress); init.mRemotePort = aRemotePort; init.mData = jsData; RefPtr udpEvent = UDPMessageEvent::Constructor(this, u"message"_ns, init); if (NS_WARN_IF(!udpEvent)) { return NS_ERROR_FAILURE; } udpEvent->SetTrusted(true); RefPtr asyncDispatcher = new AsyncEventDispatcher(this, udpEvent); 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& buffer = aMessage->GetDataAsTArray(); nsCOMPtr 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& 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