/* -*- 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 if (aData.IsArrayBuffer()) {
      const ArrayBuffer& data = aData.GetAsArrayBuffer();
      data.ComputeState();
      aRv = strStream->SetData(reinterpret_cast<const char*>(data.Data()),
                               data.Length());
    } else {
      const ArrayBufferView& data = aData.GetAsArrayBufferView();
      data.ComputeState();
      aRv = strStream->SetData(reinterpret_cast<const char*>(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<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
  JS::Rooted<JSObject*> arrayBuf(
      cx, ArrayBuffer::Create(cx, aData.Length(), aData.Elements()));

  if (NS_WARN_IF(!arrayBuf)) {
    return NS_ERROR_FAILURE;
  }

  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);

  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
