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

#include "components/ui_devtools/devtools_server.h"

#include <memory>
#include <string_view>

#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/format_macros.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/notimplemented.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/thread_annotations.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "components/ui_devtools/switches.h"
#include "net/base/net_errors.h"
#include "net/log/net_log.h"
#include "net/server/http_server.h"
#include "net/server/http_server_request_info.h"
#include "net/socket/tcp_server_socket.h"
#include "net/traffic_annotation/network_traffic_annotation.h"

namespace ui_devtools {

namespace {
const char kChromeDeveloperToolsPrefix[] =
    "devtools://devtools/bundled/devtools_app.html?uiDevTools=true&ws=";

const base::FilePath::CharType kUIDevToolsActivePortFileName[] =
    FILE_PATH_LITERAL("UIDevToolsActivePort");

void WriteUIDevtoolsPortToFile(base::FilePath output_dir, int port) {
  base::FilePath path = output_dir.Append(kUIDevToolsActivePortFileName);
  std::string port_target_string = base::StringPrintf("%d", port);
  if (!base::WriteFile(path, port_target_string)) {
    LOG(ERROR) << "Error writing UIDevTools active port to file";
  }
}
}  // namespace

UiDevToolsServer* UiDevToolsServer::devtools_server_ = nullptr;

const net::NetworkTrafficAnnotationTag UiDevToolsServer::kUIDevtoolsServerTag =
    net::DefineNetworkTrafficAnnotation("ui_devtools_server", R"(
      semantics {
        sender: "UI Devtools Server"
        description:
          "Backend for UI DevTools, to inspect Aura/Views UI."
        trigger:
          "Run with '--enable-ui-devtools' switch."
        data: "Debugging data, including any data on the open pages."
        destination: OTHER
        destination_other: "The data can be sent to any destination."
      }
      policy {
        cookies_allowed: NO
        setting:
          "This request cannot be disabled in settings. However it will never "
          "be made if user does not run with '--enable-ui-devtools' switch."
        policy_exception_justification:
          "Not implemented, only used in Devtools and is behind a switch."
      })");

class UiDevToolsServer::IOThreadData : public net::HttpServer::Delegate {
 public:
  IOThreadData(base::WeakPtr<UiDevToolsServer> owner,
               net::NetworkTrafficAnnotationTag tag);
  IOThreadData(IOThreadData&) = delete;
  ~IOThreadData() override = default;

  IOThreadData& operator=(IOThreadData&) = delete;

  // Creates a ServerSocket and an HttpServer and starts listening for
  // connections.
  void MakeServer(int port, base::FilePath active_port_output_directory);

  // These wrap the corresponding HttpServer functions.
  void AcceptWebSocket(int connection_id,
                       const net::HttpServerRequestInfo& request);
  void SendOverWebSocket(int connection_id, std::string data);

 private:
  // HttpServer::Delegate:
  void OnConnect(int connection_id) override;
  void OnHttpRequest(int connection_id,
                     const net::HttpServerRequestInfo& info) override;
  void OnWebSocketRequest(int connection_id,
                          const net::HttpServerRequestInfo& info) override;
  void OnWebSocketMessage(int connection_id, std::string data) override;
  void OnClose(int connection_id) override;

  base::WeakPtr<UiDevToolsServer> owner_;
  const scoped_refptr<base::SequencedTaskRunner> main_sequence_task_runner_;
  std::unique_ptr<net::HttpServer> server_
      GUARDED_BY_CONTEXT(io_thread_sequence_);

  const net::NetworkTrafficAnnotationTag tag_;

  SEQUENCE_CHECKER(io_thread_sequence_);
};

UiDevToolsServer::IOThreadData::IOThreadData(
    base::WeakPtr<UiDevToolsServer> owner,
    net::NetworkTrafficAnnotationTag tag)
    : owner_(owner),
      main_sequence_task_runner_(
          base::SequencedTaskRunner::GetCurrentDefault()),
      tag_(tag) {
  DETACH_FROM_SEQUENCE(io_thread_sequence_);
}

void UiDevToolsServer::IOThreadData::MakeServer(
    int port,
    base::FilePath active_port_output_directory) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_thread_sequence_);
  DCHECK(!server_);

  // Create the socket using the address 127.0.0.1 to listen on all interfaces.
  constexpr int kBacklog = 128;
  std::unique_ptr<net::ServerSocket> socket =
      std::make_unique<net::TCPServerSocket>(nullptr, net::NetLogSource());
  if (socket->Listen(net::IPEndPoint(net::IPAddress::IPv4Localhost(), port),
                     kBacklog, /*ipv6_only=*/std::nullopt) == net::OK) {
    server_ = std::make_unique<net::HttpServer>(std::move(socket), this);
    // When --enable-ui-devtools=0, the browser will pick an available port and
    // write to |kUIDevToolsActivePortFileName|. The file is useful for other
    // programs such as Telemetry to know which port to listen to.
    net::IPEndPoint local_addr;
    if (port == 0 && server_->GetLocalAddress(&local_addr) == net::OK) {
      port = local_addr.port();
      if (!active_port_output_directory.empty()) {
        base::ThreadPool::PostTask(
            FROM_HERE, {base::MayBlock()},
            base::BindOnce(&WriteUIDevtoolsPortToFile,
                           std::move(active_port_output_directory), port));
      }
    }
  }

  main_sequence_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&UiDevToolsServer::ServerCreated, owner_, port));
}

void UiDevToolsServer::IOThreadData::OnConnect(int connection_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_thread_sequence_);
  base::RecordAction(base::UserMetricsAction("UI_DevTools_Connect"));
}

void UiDevToolsServer::IOThreadData::OnHttpRequest(
    int connection_id,
    const net::HttpServerRequestInfo& info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_thread_sequence_);
  NOTIMPLEMENTED();
}

void UiDevToolsServer::IOThreadData::OnWebSocketRequest(
    int connection_id,
    const net::HttpServerRequestInfo& info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_thread_sequence_);
  main_sequence_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&UiDevToolsServer::OnWebSocketRequest, owner_,
                                connection_id, info));
}

void UiDevToolsServer::IOThreadData::OnWebSocketMessage(int connection_id,
                                                        std::string data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_thread_sequence_);
  main_sequence_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&UiDevToolsServer::OnWebSocketMessage, owner_,
                                connection_id, std::move(data)));
}

void UiDevToolsServer::IOThreadData::OnClose(int connection_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_thread_sequence_);
  main_sequence_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&UiDevToolsServer::OnClose, owner_, connection_id));
}

void UiDevToolsServer::ServerCreated(int port) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  connected_ = true;
  port_ = port;
  if (on_socket_connected_) {
    std::move(on_socket_connected_).Run();
  }
}

void UiDevToolsServer::IOThreadData::AcceptWebSocket(
    int connection_id,
    const net::HttpServerRequestInfo& request) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_thread_sequence_);
  server_->AcceptWebSocket(connection_id, request, tag_);
}

void UiDevToolsServer::IOThreadData::SendOverWebSocket(int connection_id,
                                                       std::string data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_thread_sequence_);
  server_->SendOverWebSocket(connection_id, data, tag_);
}

UiDevToolsServer::UiDevToolsServer(
    scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner,
    int port,
    net::NetworkTrafficAnnotationTag tag,
    const base::FilePath& active_port_output_directory)
    : io_thread_task_runner_(std::move(io_thread_task_runner)), port_(port) {
  DCHECK(!devtools_server_);
  devtools_server_ = this;

  // Can't initialize this in the initializer list, since it depends on
  // `weak_ptr_factory_`, which tooling requires be declared last in the class,
  // and thus is the last field to be initialized.
  io_thread_data_ =
      std::make_unique<IOThreadData>(weak_ptr_factory_.GetWeakPtr(), tag);

  io_thread_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&UiDevToolsServer::IOThreadData::MakeServer,
                                base::Unretained(io_thread_data_.get()), port,
                                active_port_output_directory));
}

UiDevToolsServer::~UiDevToolsServer() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  io_thread_task_runner_->DeleteSoon(FROM_HERE, std::move(io_thread_data_));
  devtools_server_ = nullptr;
}

// static
std::unique_ptr<UiDevToolsServer> UiDevToolsServer::CreateForViews(
    scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner,
    int port,
    const base::FilePath& active_port_output_directory) {
  // TODO(mhashmi): Change port if more than one inspectable clients
  return base::WrapUnique(new UiDevToolsServer(std::move(io_thread_task_runner),
                                               port, kUIDevtoolsServerTag,
                                               active_port_output_directory));
}

void UiDevToolsServer::SetOnSocketConnectedForTesting(
    base::OnceClosure on_socket_connected) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  if (connected_) {
    std::move(on_socket_connected).Run();
    return;
  }
  on_socket_connected_ = std::move(on_socket_connected);
}

void UiDevToolsServer::SetOnClientConnectedForTesting(
    base::OnceClosure on_client_connected) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  on_client_connected_ = std::move(on_client_connected);
}

void UiDevToolsServer::OnWebSocketRequestForTesting(
    int connection_id,
    net::HttpServerRequestInfo info) {
  OnWebSocketRequest(connection_id, std::move(info));
}

// static
std::vector<UiDevToolsServer::NameUrlPair>
UiDevToolsServer::GetClientNamesAndUrls() {
  std::vector<NameUrlPair> pairs;
  if (!devtools_server_)
    return pairs;

  DCHECK_CALLED_ON_VALID_SEQUENCE(devtools_server_->main_sequence_);
  for (ClientsList::size_type i = 0; i != devtools_server_->clients_.size();
       i++) {
    pairs.push_back(std::pair<std::string, std::string>(
        devtools_server_->clients_[i]->name(),
        base::StringPrintf("%s127.0.0.1:%d/%" PRIuS,
                           kChromeDeveloperToolsPrefix,
                           devtools_server_->port(), i)));
  }
  return pairs;
}

// static
bool UiDevToolsServer::IsUiDevToolsEnabled(const char* enable_devtools_flag) {
  return base::CommandLine::ForCurrentProcess()->HasSwitch(
      enable_devtools_flag);
}

// static
int UiDevToolsServer::GetUiDevToolsPort(const char* enable_devtools_flag,
                                        int default_port) {
  // `enable_devtools_flag` is specified only when UiDevTools were started with
  // browser start. If not specified at run time, we should use default port.
  if (!IsUiDevToolsEnabled(enable_devtools_flag))
    return default_port;

  std::string switch_value =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          enable_devtools_flag);
  int port = 0;
  return base::StringToInt(switch_value, &port) ? port : default_port;
}

void UiDevToolsServer::AttachClient(std::unique_ptr<UiDevToolsClient> client) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  clients_.push_back(std::move(client));
}

void UiDevToolsServer::SendOverWebSocket(int connection_id,
                                         std::string_view message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  io_thread_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&IOThreadData::SendOverWebSocket,
                                base::Unretained(io_thread_data_.get()),
                                connection_id, std::string(message)));
}

void UiDevToolsServer::SetOnSessionEnded(base::OnceClosure callback) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  on_session_ended_ = std::move(callback);
}

void UiDevToolsServer::OnWebSocketRequest(int connection_id,
                                          net::HttpServerRequestInfo info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  size_t target_id = 0;
  if (info.path.empty() ||
      !base::StringToSizeT(info.path.substr(1), &target_id) ||
      target_id >= clients_.size()) {
    return;
  }

  UiDevToolsClient* client = clients_[target_id].get();
  // Only one user can inspect the client at a time
  if (client->connected())
    return;
  client->set_connection_id(connection_id);
  connections_[connection_id] = client;
  io_thread_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&IOThreadData::AcceptWebSocket,
                                base::Unretained(io_thread_data_.get()),
                                connection_id, std::move(info)));
  if (on_client_connected_) {
    std::move(on_client_connected_).Run();
  }
}

void UiDevToolsServer::OnWebSocketMessage(int connection_id, std::string data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  auto it = connections_.find(connection_id);
  CHECK(it != connections_.end());
  UiDevToolsClient* client = it->second;
  client->Dispatch(data);
}

void UiDevToolsServer::OnClose(int connection_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(main_sequence_);
  auto it = connections_.find(connection_id);
  if (it == connections_.end())
    return;
  UiDevToolsClient* client = it->second;
  client->Disconnect();
  connections_.erase(it);

  if (connections_.empty() && on_session_ended_) {
    // `on_session_ended_` may destroy this, but there's nothing else on the
    // call stack, so this should be safe.
    std::move(on_session_ended_).Run();
  }

  // `this` may have been destroyed at this point.
}

}  // namespace ui_devtools
