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

#include "remoting/host/setup/me2me_native_messaging_host.h"

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/strings/stringize_macros.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "google_apis/gaia/gaia_oauth_client.h"
#include "google_apis/google_api_keys.h"
#include "net/base/network_interfaces.h"
#include "remoting/base/auto_thread_task_runner.h"
#include "remoting/base/oauth_client.h"
#include "remoting/base/rsa_key_pair.h"
#include "remoting/host/chromoting_host_context.h"
#include "remoting/host/elevated_native_messaging_host.h"
#include "remoting/host/native_messaging/log_message_handler.h"
#include "remoting/host/pin_hash.h"
#include "remoting/protocol/pairing_registry.h"

namespace {

const int kElevatedHostTimeoutSeconds = 300;

// Features supported in addition to the base protocol.
constexpr const char* kSupportedFeatures[] = {
    "pairingRegistry",
    "oauthClient",
    "getRefreshTokenFromAuthCode",
#if BUILDFLAG(IS_APPLE)
    "it2mePermissionCheck",
#endif  // BUILDFLAG(IS_APPLE)
};

// Helper to extract the "config" part of a message as a base::DictValue.
// Returns nullptr on failure, and logs an error message.
std::optional<base::DictValue> ConfigDictionaryFromMessage(
    base::DictValue message) {
  if (base::DictValue* config_dict = message.FindDict("config")) {
    return std::move(*config_dict);
  }
  return std::nullopt;
}

}  // namespace

namespace remoting {

Me2MeNativeMessagingHost::Me2MeNativeMessagingHost(
    intptr_t parent_window_handle,
    std::unique_ptr<ChromotingHostContext> host_context,
    scoped_refptr<DaemonController> daemon_controller,
    scoped_refptr<protocol::PairingRegistry> pairing_registry,
    std::unique_ptr<OAuthClient> oauth_client)
    : parent_window_handle_(parent_window_handle),
      host_context_(std::move(host_context)),
      daemon_controller_(daemon_controller),
      pairing_registry_(pairing_registry),
      oauth_client_(std::move(oauth_client)) {
  weak_ptr_ = weak_factory_.GetWeakPtr();
}

Me2MeNativeMessagingHost::~Me2MeNativeMessagingHost() {
  DCHECK(task_runner()->BelongsToCurrentThread());
}

void Me2MeNativeMessagingHost::OnMessage(const std::string& message) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  base::DictValue response;
  std::optional<base::DictValue> message_dict =
      base::JSONReader::ReadDict(message, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!message_dict) {
    OnError("Received a message that's not a dictionary.");
    return;
  }

  // If the client supplies an ID, it will expect it in the response. This
  // might be a string or a number, so cope with both.
  if (const base::Value* id = message_dict->Find("id")) {
    response.Set("id", id->Clone());
  }

  const std::string* type = message_dict->FindString("type");
  if (!type) {
    OnError("'type' not found");
    return;
  }

  response.Set("type", *type + "Response");

  if (*type == "hello") {
    ProcessHello(std::move(*message_dict), std::move(response));
  } else if (*type == "clearPairedClients") {
    ProcessClearPairedClients(std::move(*message_dict), std::move(response));
  } else if (*type == "deletePairedClient") {
    ProcessDeletePairedClient(std::move(*message_dict), std::move(response));
  } else if (*type == "getHostName") {
    ProcessGetHostName(std::move(*message_dict), std::move(response));
  } else if (*type == "getPinHash") {
    ProcessGetPinHash(std::move(*message_dict), std::move(response));
  } else if (*type == "generateKeyPair") {
    ProcessGenerateKeyPair(std::move(*message_dict), std::move(response));
  } else if (*type == "updateDaemonConfig") {
    ProcessUpdateDaemonConfig(std::move(*message_dict), std::move(response));
  } else if (*type == "getDaemonConfig") {
    ProcessGetDaemonConfig(std::move(*message_dict), std::move(response));
  } else if (*type == "getPairedClients") {
    ProcessGetPairedClients(std::move(*message_dict), std::move(response));
  } else if (*type == "getUsageStatsConsent") {
    ProcessGetUsageStatsConsent(std::move(*message_dict), std::move(response));
  } else if (*type == "startDaemon") {
    ProcessStartDaemon(std::move(*message_dict), std::move(response));
  } else if (*type == "stopDaemon") {
    ProcessStopDaemon(std::move(*message_dict), std::move(response));
  } else if (*type == "getDaemonState") {
    ProcessGetDaemonState(std::move(*message_dict), std::move(response));
  } else if (*type == "getHostClientId") {
    ProcessGetHostClientId(std::move(*message_dict), std::move(response));
  } else if (*type == "getCredentialsFromAuthCode") {
    ProcessGetCredentialsFromAuthCode(std::move(*message_dict),
                                      std::move(response), true);
  } else if (*type == "getRefreshTokenFromAuthCode") {
    ProcessGetCredentialsFromAuthCode(std::move(*message_dict),
                                      std::move(response), false);
  } else if (*type == "it2mePermissionCheck") {
    ProcessIt2mePermissionCheck(std::move(*message_dict), std::move(response));
  } else {
    OnError("Unsupported request type: " + *type);
  }
}

void Me2MeNativeMessagingHost::Start(Client* client) {
  DCHECK(task_runner()->BelongsToCurrentThread());
  client_ = client;
  log_message_handler_ =
      std::make_unique<LogMessageHandler>(base::BindRepeating(
          &Me2MeNativeMessagingHost::SendMessageToClient, weak_ptr_));
}

scoped_refptr<base::SingleThreadTaskRunner>
Me2MeNativeMessagingHost::task_runner() const {
  return host_context_->ui_task_runner();
}

void Me2MeNativeMessagingHost::ProcessHello(base::DictValue message,
                                            base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  response.Set("version", STRINGIZE(VERSION));
  base::ListValue supported_features_list;
  for (const char* feature : kSupportedFeatures) {
    supported_features_list.Append(feature);
  }
  response.Set("supportedFeatures", std::move(supported_features_list));
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::ProcessClearPairedClients(
    base::DictValue message,
    base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  if (!daemon_controller_->is_privileged()) {
    if (DelegateToElevatedHost(std::move(message)) != DELEGATION_SUCCESS) {
      SendBooleanResult(std::move(response), false);
    }
    return;
  }

  if (pairing_registry_.get()) {
    pairing_registry_->ClearAllPairings(
        base::BindOnce(&Me2MeNativeMessagingHost::SendBooleanResult, weak_ptr_,
                       std::move(response)));
  } else {
    SendBooleanResult(std::move(response), false);
  }
}

void Me2MeNativeMessagingHost::ProcessDeletePairedClient(
    base::DictValue message,
    base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  if (!daemon_controller_->is_privileged()) {
    if (DelegateToElevatedHost(std::move(message)) != DELEGATION_SUCCESS) {
      SendBooleanResult(std::move(response), false);
    }
    return;
  }

  std::string* client_id =
      message.FindString(protocol::PairingRegistry::kClientIdKey);
  if (!client_id) {
    OnError("'" + std::string(protocol::PairingRegistry::kClientIdKey) +
            "' string not found.");
    return;
  }

  if (pairing_registry_.get()) {
    pairing_registry_->DeletePairing(
        std::move(*client_id),
        base::BindOnce(&Me2MeNativeMessagingHost::SendBooleanResult, weak_ptr_,
                       std::move(response)));
  } else {
    SendBooleanResult(std::move(response), false);
  }
}

void Me2MeNativeMessagingHost::ProcessGetHostName(base::DictValue message,
                                                  base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  response.Set("hostname", net::GetHostName());
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::ProcessGetPinHash(base::DictValue message,
                                                 base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  std::string* host_id = message.FindString("hostId");
  if (!host_id) {
    OnError("'hostId' not found: " + base::WriteJson(message).value_or(""));
    return;
  }
  std::string* pin = message.FindString("pin");
  if (!pin) {
    OnError("'pin' not found: " + base::WriteJson(message).value_or(""));
    return;
  }
  response.Set("hash", MakeHostPinHash(std::move(*host_id), std::move(*pin)));
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::ProcessGenerateKeyPair(
    base::DictValue message,
    base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  scoped_refptr<RsaKeyPair> key_pair = RsaKeyPair::Generate();
  response.Set("privateKey", key_pair->ToString());
  response.Set("publicKey", key_pair->GetPublicKey());
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::ProcessUpdateDaemonConfig(
    base::DictValue message,
    base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  if (!daemon_controller_->is_privileged()) {
    DelegationResult result = DelegateToElevatedHost(std::move(message));
    switch (result) {
      case DELEGATION_SUCCESS:
        return;  // Result will be returned by elevated host.
      case DELEGATION_CANCELLED:
        SendAsyncResult(std::move(response),
                        DaemonController::RESULT_CANCELLED);
        return;
      default:
        SendAsyncResult(std::move(response), DaemonController::RESULT_FAILED);
        return;
    }
  }

  std::optional<base::DictValue> config_dict =
      ConfigDictionaryFromMessage(std::move(message));
  if (!config_dict) {
    OnError("'config' dictionary not found");
    return;
  }

  daemon_controller_->UpdateConfig(
      std::move(*config_dict),
      base::BindOnce(&Me2MeNativeMessagingHost::SendAsyncResult, weak_ptr_,
                     std::move(response)));
}

void Me2MeNativeMessagingHost::ProcessGetDaemonConfig(
    base::DictValue message,
    base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  daemon_controller_->GetConfig(
      base::BindOnce(&Me2MeNativeMessagingHost::SendConfigResponse, weak_ptr_,
                     std::move(response)));
}

void Me2MeNativeMessagingHost::ProcessGetPairedClients(
    base::DictValue message,
    base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  if (pairing_registry_.get()) {
    pairing_registry_->GetAllPairings(
        base::BindOnce(&Me2MeNativeMessagingHost::SendPairedClientsResponse,
                       weak_ptr_, std::move(response)));
  } else {
    SendPairedClientsResponse(std::move(response),
                              /*pairings=*/base::ListValue());
  }
}

void Me2MeNativeMessagingHost::ProcessGetUsageStatsConsent(
    base::DictValue message,
    base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  daemon_controller_->GetUsageStatsConsent(
      base::BindOnce(&Me2MeNativeMessagingHost::SendUsageStatsConsentResponse,
                     weak_ptr_, std::move(response)));
}

void Me2MeNativeMessagingHost::ProcessStartDaemon(base::DictValue message,
                                                  base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

#if BUILDFLAG(IS_LINUX)
  // See: b/215406223, crbug.com/514525547
  // TODO: yuweih - consider re-enabling starting host via native messaging once
  // the GDM-managed host becomes the default.
  LOG(ERROR) << "Starting host is not supported on Linux.";
  SendAsyncResult(std::move(response), DaemonController::RESULT_FAILED);
#else
  if (!daemon_controller_->is_privileged()) {
    DelegationResult result = DelegateToElevatedHost(std::move(message));
    switch (result) {
      case DELEGATION_SUCCESS:
        return;  // Result will be returned by elevated host.
      case DELEGATION_CANCELLED:
        SendAsyncResult(std::move(response),
                        DaemonController::RESULT_CANCELLED);
        return;
      default:
        SendAsyncResult(std::move(response), DaemonController::RESULT_FAILED);
        return;
    }
  }

  std::optional<bool> consent = message.FindBool("consent");
  if (!consent) {
    OnError("'consent' not found.");
    return;
  }

  std::optional<base::DictValue> config_dict =
      ConfigDictionaryFromMessage(std::move(message));
  if (!config_dict) {
    OnError("'config' dictionary not found");
    return;
  }

  daemon_controller_->SetConfigAndStart(
      std::move(*config_dict), *consent,
      base::BindOnce(&Me2MeNativeMessagingHost::SendAsyncResult, weak_ptr_,
                     std::move(response)));
#endif  // !BUILDFLAG(IS_LINUX)
}

void Me2MeNativeMessagingHost::ProcessStopDaemon(base::DictValue message,
                                                 base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  if (!daemon_controller_->is_privileged()) {
    DelegationResult result = DelegateToElevatedHost(std::move(message));
    switch (result) {
      case DELEGATION_SUCCESS:
        return;  // Result will be returned by elevated host.
      case DELEGATION_CANCELLED:
        SendAsyncResult(std::move(response),
                        DaemonController::RESULT_CANCELLED);
        return;
      default:
        SendAsyncResult(std::move(response), DaemonController::RESULT_FAILED);
        return;
    }
  }

  daemon_controller_->Stop(
      base::BindOnce(&Me2MeNativeMessagingHost::SendAsyncResult, weak_ptr_,
                     std::move(response)));
}

void Me2MeNativeMessagingHost::ProcessGetDaemonState(base::DictValue message,
                                                     base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  DaemonController::State state = daemon_controller_->GetState();
  switch (state) {
    case DaemonController::STATE_NOT_IMPLEMENTED:
      response.Set("state", "NOT_IMPLEMENTED");
      break;
    case DaemonController::STATE_STOPPED:
      response.Set("state", "STOPPED");
      break;
    case DaemonController::STATE_STARTING:
      response.Set("state", "STARTING");
      break;
    case DaemonController::STATE_STARTED:
      response.Set("state", "STARTED");
      break;
    case DaemonController::STATE_STOPPING:
      response.Set("state", "STOPPING");
      break;
    case DaemonController::STATE_UNKNOWN:
      response.Set("state", "UNKNOWN");
      break;
  }
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::ProcessGetHostClientId(
    base::DictValue message,
    base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  response.Set("clientId", google_apis::GetOAuth2ClientID(
                               google_apis::CLIENT_REMOTING_HOST));
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::ProcessGetCredentialsFromAuthCode(
    base::DictValue message,
    base::DictValue response,
    bool need_user_email) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  std::string* auth_code = message.FindString("authorizationCode");
  if (!auth_code) {
    OnError("'authorizationCode' string not found.");
    return;
  }

  // Pass an empty redirect_uri value since it's not needed when exchanging
  // robot auth codes. See b/231442487 for more details.
  std::string redirect_uri;
  gaia::OAuthClientInfo oauth_client_info = {
      google_apis::GetOAuth2ClientID(google_apis::CLIENT_REMOTING_HOST),
      google_apis::GetOAuth2ClientSecret(google_apis::CLIENT_REMOTING_HOST),
      redirect_uri};

  oauth_client_->GetCredentialsFromAuthCode(
      oauth_client_info, std::move(*auth_code), need_user_email,
      base::BindOnce(&Me2MeNativeMessagingHost::SendCredentialsResponse,
                     weak_ptr_, std::move(response)));
}

void Me2MeNativeMessagingHost::ProcessIt2mePermissionCheck(
    base::DictValue message,
    base::DictValue response) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  daemon_controller_->CheckPermission(
      /* it2me */ true,
      base::BindOnce(&Me2MeNativeMessagingHost::SendBooleanResult, weak_ptr_,
                     std::move(response)));
}

void Me2MeNativeMessagingHost::SendConfigResponse(
    base::DictValue response,
    std::optional<base::DictValue> config) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  if (config) {
    response.Set("config", std::move(*config));
  } else {
    response.Set("config", base::Value());
  }
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::SendPairedClientsResponse(
    base::DictValue response,
    base::ListValue pairings) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  response.Set("pairedClients", std::move(pairings));
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::SendUsageStatsConsentResponse(
    base::DictValue response,
    const DaemonController::UsageStatsConsent& consent) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  response.Set("supported", consent.supported);
  response.Set("allowed", consent.allowed);
  response.Set("setByPolicy", consent.set_by_policy);
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::SendAsyncResult(
    base::DictValue response,
    DaemonController::AsyncResult result) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  switch (result) {
    case DaemonController::RESULT_OK:
      response.Set("result", "OK");
      break;
    case DaemonController::RESULT_FAILED:
      response.Set("result", "FAILED");
      break;
    case DaemonController::RESULT_CANCELLED:
      response.Set("result", "CANCELLED");
      break;
  }
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::SendBooleanResult(base::DictValue response,
                                                 bool result) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  response.Set("result", result);
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::SendCredentialsResponse(
    base::DictValue response,
    const std::string& user_email,
    const std::string& refresh_token) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  if (!user_email.empty()) {
    response.Set("userEmail", user_email);
  }
  response.Set("refreshToken", refresh_token);
  SendMessageToClient(std::move(response));
}

void Me2MeNativeMessagingHost::SendMessageToClient(
    base::DictValue message) const {
  DCHECK(task_runner()->BelongsToCurrentThread());
  client_->PostMessageFromNativeHost(base::WriteJson(message).value_or(""));
}

void Me2MeNativeMessagingHost::OnError(const std::string& error_message) {
  DCHECK(task_runner()->BelongsToCurrentThread());

  if (!error_message.empty()) {
    LOG(ERROR) << error_message;
  }

  // Trigger a host shutdown by sending an empty message.
  client_->CloseChannel(std::string());
}

Me2MeNativeMessagingHost::DelegationResult
Me2MeNativeMessagingHost::DelegateToElevatedHost(base::DictValue message) {
  DCHECK(task_runner()->BelongsToCurrentThread());
  DCHECK(!daemon_controller_->is_privileged());

  if (!elevated_host_) {
    elevated_host_ = std::make_unique<ElevatedNativeMessagingHost>(
        base::CommandLine::ForCurrentProcess()->GetProgram(),
        parent_window_handle_,
        /*elevate_process=*/true, base::Seconds(kElevatedHostTimeoutSeconds),
        client_);
  }

  ProcessLaunchResult result = elevated_host_->EnsureElevatedHostCreated();
  if (result == PROCESS_LAUNCH_RESULT_SUCCESS) {
    elevated_host_->SendMessage(message);
  }

  switch (result) {
    case PROCESS_LAUNCH_RESULT_SUCCESS:
      return DELEGATION_SUCCESS;
    case PROCESS_LAUNCH_RESULT_CANCELLED:
      return DELEGATION_CANCELLED;
    case PROCESS_LAUNCH_RESULT_FAILED:
      return DELEGATION_FAILED;
  }
}

}  // namespace remoting
