// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/modules/direct_sockets/socket.h"

#include <utility>

#include "base/notreached.h"
#include "base/task/single_thread_task_runner.h"
#include "net/base/net_errors.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
#include "third_party/blink/public/mojom/frame/lifecycle.mojom-shared.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/scheduler/public/scheduling_policy.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"

namespace blink {

namespace {

std::pair<DOMExceptionCode, String>
CreateDOMExceptionCodeAndMessageFromNetErrorCode(int32_t net_error) {
  switch (net_error) {
    case net::ERR_NAME_NOT_RESOLVED:
      return {DOMExceptionCode::kNetworkError,
              "Hostname couldn't be resolved."};
    case net::ERR_INVALID_URL:
      return {DOMExceptionCode::kDataError, "Supplied url is not valid."};
    case net::ERR_UNEXPECTED:
      return {DOMExceptionCode::kUnknownError, "Unexpected error occured."};
    case net::ERR_ACCESS_DENIED:
      return {DOMExceptionCode::kInvalidAccessError,
              "Access to the requested host or port is blocked."};
    case net::ERR_NETWORK_ACCESS_DENIED:
      return {DOMExceptionCode::kInvalidAccessError, "Firewall error."};
    case net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS:
      return {DOMExceptionCode::kInvalidAccessError,
              "Access to local network is blocked."};
    case net::ERR_NOT_IMPLEMENTED:
      return {DOMExceptionCode::kNotSupportedError,
              "This operation is not supported on the current platform."};
    case net::ERR_SOCKET_NOT_CONNECTED:
      return {DOMExceptionCode::kInvalidStateError,
              "The socket is not in a state that allows this operation."};
    case net::ERR_ADDRESS_INVALID:
      return {DOMExceptionCode::kDataError, "The address is not valid."};
    default:
      return {DOMExceptionCode::kNetworkError, "Network Error."};
  }
}

}  // namespace

ScriptPromise<IDLUndefined> Socket::closed(ScriptState* script_state) const {
  return closed_->Promise(script_state->World());
}

Socket::Socket(ScriptState* script_state)
    : ExecutionContextLifecycleStateObserver(
          ExecutionContext::From(script_state)),
      script_state_(script_state),
      service_(GetExecutionContext()),
      feature_handle_for_scheduler_(
          GetExecutionContext()->GetScheduler()->RegisterFeature(
              SchedulingPolicy::Feature::kOutstandingNetworkRequestDirectSocket,
              {SchedulingPolicy::DisableBackForwardCache()})),
      closed_(MakeGarbageCollected<ScriptPromiseProperty<IDLUndefined, IDLAny>>(
          GetExecutionContext())) {
  UpdateStateIfNeeded();

  GetExecutionContext()->GetBrowserInterfaceBroker().GetInterface(
      service_.BindNewPipeAndPassReceiver(
          GetExecutionContext()->GetTaskRunner(TaskType::kNetworking)));
  service_.set_disconnect_handler(
      BindOnce(&Socket::OnServiceConnectionError, WrapPersistent(this)));

  // |closed| promise is just one of the ways to learn that the socket state has
  // changed. Therefore it's not necessary to force developers to handle
  // rejections.
  closed_->MarkAsHandled();
}

Socket::~Socket() = default;

// static
bool Socket::CheckContextAndPermissions(ScriptState* script_state,
                                        ExceptionState& exception_state) {
  if (!script_state->ContextIsValid()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Current context is detached.");
    return false;
  }

  ExecutionContext* execution_context = ExecutionContext::From(script_state);
  if (execution_context->IsIsolatedContext()) {
    if (!execution_context->CrossOriginIsolatedCapability()) {
      exception_state.ThrowDOMException(
          DOMExceptionCode::kNotAllowedError,
          "Frame is not sufficiently isolated to use Direct Sockets.");
      return false;
    }
    if (execution_context->IsWindow() &&
        !execution_context->IsFeatureEnabled(
            network::mojom::PermissionsPolicyFeature::kDirectSockets)) {
      exception_state.ThrowDOMException(
          DOMExceptionCode::kNotAllowedError,
          "Permissions-Policy: direct-sockets are disabled.");
      return false;
    }
    return true;
  } else {
    // Embedder-enabled Direct Sockets run in custom contexts and do not rely on
    // permissions policies.
    return true;
  }
}

// static
DOMException* Socket::CreateDOMExceptionFromNetErrorCode(int32_t net_error) {
  auto [code, message] =
      CreateDOMExceptionCodeAndMessageFromNetErrorCode(net_error);
  return MakeGarbageCollected<DOMException>(code, std::move(message));
}

void Socket::Trace(Visitor* visitor) const {
  visitor->Trace(script_state_);
  visitor->Trace(service_);
  visitor->Trace(closed_);
  ExecutionContextLifecycleStateObserver::Trace(visitor);
}

void Socket::ResetServiceAndFeatureHandle() {
  feature_handle_for_scheduler_.reset();
  service_.reset();
}

// static
protocol::Network::DirectSocketDnsQueryType Socket::MapProbeDnsQueryType(
    V8SocketDnsQueryType dns_query_type) {
  switch (dns_query_type.AsEnum()) {
    case V8SocketDnsQueryType::Enum::kIpv4:
      return protocol::Network::DirectSocketDnsQueryTypeEnum::Ipv4;
    case V8SocketDnsQueryType::Enum::kIpv6:
      return protocol::Network::DirectSocketDnsQueryTypeEnum::Ipv6;
  }
}

}  // namespace blink
