// 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/daemon_controller_delegate_mac.h"

#include <launch.h>
#include <sys/types.h>
#include <utility>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/osstatus_logging.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/mac/authorization_util.h"
#include "base/mac/launchd.h"
#include "base/mac/scoped_authorizationref.h"
#include "base/mac/scoped_launch_data.h"
#include "base/memory/ptr_util.h"
#include "base/message_loop/message_pump_type.h"
#include "base/posix/eintr_wrapper.h"
#import "base/task/single_thread_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "remoting/base/string_resources.h"
#include "remoting/host/host_config.h"
#include "remoting/host/mac/constants_mac.h"
#include "remoting/host/mac/permission_checker.h"
#include "remoting/host/mac/permission_wizard.h"
#include "remoting/host/resources.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"

namespace remoting {

namespace {

constexpr char kIoThreadName[] = "DaemonControllerDelegateMac IO thread";

// Simple RAII class to ensure that waitpid() gets called on a child process.
// Neither std::unique_ptr nor base::ScopedGeneric are well suited, because the
// caller wants to examine the returned status of waitpid() from the scoper's
// deleter function.
class ScopedWaitpid {
 public:
  // -1 is treated as an invalid PID and waitpit() will not be called in this
  // case. Note that -1 is the value returned from
  // base::mac::ExecuteWithPrivilegesAndGetPID() when the child PID could not be
  // determined.
  explicit ScopedWaitpid(pid_t pid) : pid_(pid) {}
  ~ScopedWaitpid() { MaybeWait(); }

  // Executes the waitpid() and resets the scoper. After this, the caller may
  // examine error() and exit_status().
  void Reset() { MaybeWait(); }

  bool error() { return error_; }
  int exit_status() { return exit_status_; }

 private:
  pid_t pid_ = -1;

  // Set if waitpid() failed (returned a value not equal to |pid_|).
  bool error_ = false;

  // The exit status, if waitpid() succeeded.
  int exit_status_ = 0;

  void MaybeWait() {
    if (pid_ != -1) {
      pid_t wait_result = HANDLE_EINTR(waitpid(pid_, &exit_status_, 0));
      if (wait_result != pid_) {
        PLOG(ERROR) << "waitpid failed";
        error_ = true;
      }
      pid_ = -1;
    }
  }
};

// Runs the helper script as root with the given command-line argument.
// If |input_data| is non-empty, it will be piped to the script via standard
// input. Returns true if successful.
bool RunHelperAsRoot(const std::string& command,
                     const std::string& input_data) {
  NSString* prompt = l10n_util::GetNSStringFWithFixup(
      IDS_HOST_AUTHENTICATION_PROMPT,
      l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
  base::mac::ScopedAuthorizationRef authorization =
      base::mac::AuthorizationCreateToRunAsRoot(
          base::apple::NSToCFPtrCast(prompt));
  if (!authorization.get()) {
    LOG(ERROR) << "Failed to obtain authorizationRef";
    return false;
  }

  // TODO(lambroslambrou): Replace the deprecated ExecuteWithPrivileges
  // call with a launchd-based helper tool, which is more secure.
  // http://crbug.com/120903
  const char* arguments[] = {command.c_str(), nullptr};
  FILE* pipe = nullptr;
  pid_t pid;
  OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID(
      authorization.get(), remoting::kHostServiceBinaryPath,
      kAuthorizationFlagDefaults, arguments, &pipe, &pid);
  if (status != errAuthorizationSuccess) {
    LOG(ERROR) << "AuthorizationExecuteWithPrivileges: "
               << logging::DescriptionFromOSStatus(status)
               << static_cast<int>(status);
    return false;
  }

  // It is safer to order the scopers this way round, to ensure that the pipe is
  // closed before calling waitpid(). In the case of sending data to the child,
  // the child reads until EOF on its stdin, so calling waitpid() first would
  // result in deadlock in this situation.
  ScopedWaitpid scoped_pid(pid);
  base::ScopedFILE scoped_pipe(pipe);

  if (pid == -1) {
    LOG(ERROR) << "Failed to get child PID";
    return false;
  }
  if (!pipe) {
    LOG(ERROR) << "Unexpected nullptr pipe";
    return false;
  }

  if (!input_data.empty()) {
    size_t bytes_written =
        fwrite(input_data.data(), sizeof(char), input_data.size(), pipe);
    // According to the fwrite manpage, a partial count is returned only if a
    // write error has occurred.
    if (bytes_written != input_data.size()) {
      LOG(ERROR) << "Failed to write data to child process";
      return false;
    }

    // Flush any buffers here to avoid doing it in fclose(), because the
    // ScopedFILE does not allow checking for errors from fclose().
    if (fflush(pipe) != 0) {
      PLOG(ERROR) << "Failed to flush data to child process";
      return false;
    }
  }

  // Close the pipe (to send EOF) and wait for the child process to run.
  scoped_pipe.reset();
  scoped_pid.Reset();

  if (scoped_pid.error()) {
    PLOG(ERROR) << "waitpid failed";
    return false;
  }
  const int exit_status = scoped_pid.exit_status();
  if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) {
    return true;
  }

  LOG(ERROR) << remoting::kHostServiceBinaryPath << " failed with exit status "
             << exit_status;
  return false;
}

void ElevateAndSetConfig(base::Value::Dict config,
                         DaemonController::CompletionCallback done) {
  // Find out if the host service is running.
  pid_t job_pid = base::mac::PIDForJob(remoting::kServiceName);
  bool service_running = (job_pid > 0);

  const char* command = service_running ? "--save-config" : "--enable";
  std::string input_data = HostConfigToJson(std::move(config));
  if (!RunHelperAsRoot(command, input_data)) {
    LOG(ERROR) << "Failed to run the helper tool.";
    std::move(done).Run(DaemonController::RESULT_FAILED);
    return;
  }

  if (!service_running) {
    base::mac::ScopedLaunchData response(
        base::mac::MessageForJob(remoting::kServiceName, LAUNCH_KEY_STARTJOB));
    if (!response.is_valid()) {
      LOG(ERROR) << "Failed to send STARTJOB to launchd";
      std::move(done).Run(DaemonController::RESULT_FAILED);
      return;
    }
  }
  std::move(done).Run(DaemonController::RESULT_OK);
}

void ElevateAndStopHost(DaemonController::CompletionCallback done) {
  if (!RunHelperAsRoot("--disable", std::string())) {
    LOG(ERROR) << "Failed to run the helper tool.";
    std::move(done).Run(DaemonController::RESULT_FAILED);
    return;
  }

  // Stop the launchd job.  This cannot easily be done by the helper tool,
  // since the launchd job runs in the current user's context.
  base::mac::ScopedLaunchData response(
      base::mac::MessageForJob(remoting::kServiceName, LAUNCH_KEY_STOPJOB));
  if (!response.is_valid()) {
    LOG(ERROR) << "Failed to send STOPJOB to launchd";
    std::move(done).Run(DaemonController::RESULT_FAILED);
    return;
  }
  std::move(done).Run(DaemonController::RESULT_OK);
}

}  // namespace

DaemonControllerDelegateMac::DaemonControllerDelegateMac()
    : io_thread_(kIoThreadName) {
  LoadResources(std::string());
  io_task_runner_ = io_thread_.StartWithType(base::MessagePumpType::IO);
}

DaemonControllerDelegateMac::~DaemonControllerDelegateMac() {
  UnloadResources();
}

DaemonController::State DaemonControllerDelegateMac::GetState() {
  pid_t job_pid = base::mac::PIDForJob(kServiceName);
  if (job_pid < 0) {
    return DaemonController::STATE_UNKNOWN;
  } else if (job_pid == 0) {
    // Service is stopped, or a start attempt failed.
    return DaemonController::STATE_STOPPED;
  } else {
    return DaemonController::STATE_STARTED;
  }
}

absl::optional<base::Value::Dict> DaemonControllerDelegateMac::GetConfig() {
  base::FilePath config_path(kHostConfigFilePath);
  absl::optional<base::Value::Dict> host_config(
      HostConfigFromJsonFile(config_path));
  if (!host_config.has_value()) {
    return absl::nullopt;
  }

  base::Value::Dict config;
  std::string* value = host_config->FindString(kHostIdConfigPath);
  if (value) {
    config.Set(kHostIdConfigPath, *value);
  }

  value = host_config->FindString(kXmppLoginConfigPath);
  if (value) {
    config.Set(kXmppLoginConfigPath, *value);
  }

  return config;
}

void DaemonControllerDelegateMac::CheckPermission(
    bool it2me,
    DaemonController::BoolCallback callback) {
  auto checker = std::make_unique<mac::PermissionChecker>(
      it2me ? mac::HostMode::IT2ME : mac::HostMode::ME2ME, io_task_runner_);
  permission_wizard_ =
      std::make_unique<mac::PermissionWizard>(std::move(checker));
  permission_wizard_->SetCompletionCallback(std::move(callback));
  permission_wizard_->Start(base::SingleThreadTaskRunner::GetCurrentDefault());
}

void DaemonControllerDelegateMac::SetConfigAndStart(
    base::Value::Dict config,
    bool consent,
    DaemonController::CompletionCallback done) {
  config.Set(kUsageStatsConsentConfigPath, consent);
  ElevateAndSetConfig(std::move(config), std::move(done));
}

void DaemonControllerDelegateMac::UpdateConfig(
    base::Value::Dict config,
    DaemonController::CompletionCallback done) {
  base::FilePath config_file_path(kHostConfigFilePath);
  absl::optional<base::Value::Dict> host_config(
      HostConfigFromJsonFile(config_file_path));
  if (!host_config.has_value()) {
    std::move(done).Run(DaemonController::RESULT_FAILED);
    return;
  }

  host_config->Merge(std::move(config));
  ElevateAndSetConfig(std::move(host_config.value()), std::move(done));
}

void DaemonControllerDelegateMac::Stop(
    DaemonController::CompletionCallback done) {
  ElevateAndStopHost(std::move(done));
}

DaemonController::UsageStatsConsent
DaemonControllerDelegateMac::GetUsageStatsConsent() {
  DaemonController::UsageStatsConsent consent;
  consent.supported = true;
  consent.allowed = true;
  // set_by_policy is not yet supported.
  consent.set_by_policy = false;

  base::FilePath config_file_path(kHostConfigFilePath);
  absl::optional<base::Value::Dict> host_config(
      HostConfigFromJsonFile(config_file_path));
  if (host_config.has_value()) {
    absl::optional<bool> host_config_value =
        host_config->FindBool(kUsageStatsConsentConfigPath);
    if (host_config_value.has_value()) {
      consent.allowed = host_config_value.value();
    }
  }

  return consent;
}

scoped_refptr<DaemonController> DaemonController::Create() {
  return new DaemonController(
      base::WrapUnique(new DaemonControllerDelegateMac()));
}

}  // namespace remoting
