// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <signal.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <string_view>
#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringize_macros.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "remoting/base/logging.h"
#include "remoting/host/base/host_exit_codes.h"
#include "remoting/host/base/switches.h"
#include "remoting/host/base/username.h"
#include "remoting/host/mac/constants_mac.h"
#include "remoting/host/version.h"
namespace remoting {
namespace {
constexpr char kSwitchDisable[] = "disable";
constexpr char kSwitchEnable[] = "enable";
constexpr char kSwitchSaveConfig[] = "save-config";
constexpr char kSwitchHostVersion[] = "host-version";
constexpr char kSwitchHostRunFromLaunchd[] = "run-from-launchd";
constexpr char kHostExeFileName[] = "remoting_me2me_host";
constexpr char kNativeMessagingHostPath[] =
"Contents/MacOS/native_messaging_host";
// The exit code returned by 'wait' when a process is terminated by SIGTERM.
constexpr int kSigtermExitCode = 128 + SIGTERM;
// Constants controlling the host process relaunch throttling.
constexpr base::TimeDelta kMinimumRelaunchInterval = base::Minutes(1);
constexpr int kMaximumHostFailures = 10;
// Exit code 126 is defined by Posix to mean "Command found, but not
// executable", and is returned if the process cannot be launched due to
// parental control.
constexpr int kPermissionDeniedParentalControl = 126;
// This executable works as a proxy between launchd and the host. Signals of
// interest to the host must be forwarded.
constexpr int kSignalList[] = {
SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGEMT,
SIGFPE, SIGBUS, SIGSEGV, SIGSYS, SIGPIPE, SIGALRM, SIGTERM,
SIGURG, SIGTSTP, SIGCONT, SIGTTIN, SIGTTOU, SIGIO, SIGXCPU,
SIGXFSZ, SIGVTALRM, SIGPROF, SIGWINCH, SIGINFO, SIGUSR1, SIGUSR2};
// Current host PID used to forward signals. 0 if host is not running.
static base::ProcessId g_host_pid = 0;
void HandleSignal(int signum) {
if (g_host_pid) {
// All other signals are forwarded to host then ignored except SIGTERM.
// launchd sends SIGTERM when service is being stopped so both the host and
// the host service need to terminate.
HOST_LOG << "Forwarding signal " << signum << " to host process "
<< g_host_pid;
kill(g_host_pid, signum);
if (signum == SIGTERM) {
HOST_LOG << "Host service is terminating upon reception of SIGTERM";
exit(kSigtermExitCode);
}
} else {
HOST_LOG << "Signal " << signum
<< " will not be forwarded since host is not running.";
exit(128 + signum);
}
}
void RegisterSignalHandler() {
struct sigaction action = {};
sigfillset(&action.sa_mask);
action.sa_flags = 0;
action.sa_handler = &HandleSignal;
for (int signum : kSignalList) {
if (sigaction(signum, &action, nullptr) == -1) {
PLOG(DFATAL) << "Failed to register signal handler for signal " << signum;
}
}
}
class HostService {
public:
HostService();
~HostService();
bool Disable();
bool Enable();
bool WriteStdinToConfig();
int RunHost();
void PrintHostVersion();
void PrintPid();
private:
int RunHostFromOldScript();
// Runs the permission-checker built into the native-messaging host. Returns
// true if all permissions were granted (or no permission check is needed for
// the version of MacOS).
bool CheckPermission();
bool HostIsEnabled();
base::FilePath old_host_helper_file_;
base::FilePath enabled_file_;
base::FilePath config_file_;
base::FilePath host_exe_file_;
base::FilePath native_messaging_host_exe_file_;
};
HostService::HostService() {
old_host_helper_file_ = base::FilePath(kOldHostHelperScriptPath);
enabled_file_ = base::FilePath(kHostEnabledPath);
config_file_ = base::FilePath(kHostConfigFilePath);
base::FilePath host_service_dir;
base::PathService::Get(base::DIR_EXE, &host_service_dir);
host_exe_file_ = host_service_dir.AppendASCII(kHostExeFileName);
native_messaging_host_exe_file_ =
host_service_dir.AppendASCII(NATIVE_MESSAGING_HOST_BUNDLE_NAME)
.AppendASCII(kNativeMessagingHostPath);
}
HostService::~HostService() = default;
int HostService::RunHost() {
if (geteuid() != 0 && HostIsEnabled()) {
// Only check for non-root users, as the permission wizard is not actionable
// at the login screen. Also, permission is only needed when host is
// enabled - the launchd service should exit immediately if the host is
// disabled.
if (!CheckPermission()) {
return 1;
}
}
int host_failure_count = 0;
base::TimeTicks host_start_time;
while (true) {
if (!HostIsEnabled()) {
HOST_LOG << "Daemon is disabled.";
return 0;
}
// If this is not the first time the host has run, make sure we don't
// relaunch it too soon.
if (!host_start_time.is_null()) {
base::TimeDelta host_lifetime = base::TimeTicks::Now() - host_start_time;
HOST_LOG << "Host ran for " << host_lifetime;
if (host_lifetime < kMinimumRelaunchInterval) {
// If the host didn't run for very long, assume it crashed. Relaunch
// only after a suitable delay and increase the failure count.
host_failure_count++;
LOG(WARNING) << "Host failure count " << host_failure_count << "/"
<< kMaximumHostFailures;
if (host_failure_count >= kMaximumHostFailures) {
LOG(ERROR) << "Too many host failures. Giving up.";
return 1;
}
base::TimeDelta relaunch_in = kMinimumRelaunchInterval - host_lifetime;
HOST_LOG << "Relaunching in " << relaunch_in;
base::PlatformThread::Sleep(relaunch_in);
} else {
// If the host ran for long enough, reset the crash counter.
host_failure_count = 0;
}
}
host_start_time = base::TimeTicks::Now();
base::CommandLine cmdline(host_exe_file_);
cmdline.AppendSwitchPath("host-config", config_file_);
std::string ssh_auth_sockname =
"/tmp/chromoting." + GetUsername() + ".ssh_auth_sock";
cmdline.AppendSwitchASCII("ssh-auth-sockname", ssh_auth_sockname);
base::Process process = base::LaunchProcess(cmdline, base::LaunchOptions());
if (!process.IsValid()) {
LOG(ERROR) << "Failed to launch host process for unknown reason.";
return 1;
}
g_host_pid = process.Pid();
int exit_code;
process.WaitForExit(&exit_code);
g_host_pid = 0;
const char* exit_code_string_ptr = ExitCodeToStringUnchecked(exit_code);
std::string exit_code_string =
exit_code_string_ptr ? (std::string(exit_code_string_ptr) + " (" +
base::NumberToString(exit_code) + ")")
: base::NumberToString(exit_code);
if (exit_code == 0 || exit_code == kSigtermExitCode ||
exit_code == kPermissionDeniedParentalControl ||
(exit_code >= kMinPermanentErrorExitCode &&
exit_code <= kMaxPermanentErrorExitCode)) {
HOST_LOG << "Host returned permanent exit code " << exit_code_string
<< " at " << base::Time::Now();
if (exit_code == kInvalidHostIdExitCode ||
exit_code == kHostDeletedExitCode) {
// The host was taken off-line remotely. To prevent the host being
// restarted when the login context changes, try to delete the "enabled"
// file. Since this requires root privileges, this is only possible when
// this executable is launched in the "login" context. In the "aqua"
// context, just exit and try again next time.
HOST_LOG << "Host deleted - disabling";
Disable();
}
return exit_code;
}
// Ignore non-permanent error-code and launch host again. Stop handling
// signals temporarily in case the executable has to sleep to throttle host
// relaunches. While throttling, there is no host process to which to
// forward the signal, so the default behaviour should be restored.
HOST_LOG << "Host returned non-permanent exit code " << exit_code_string
<< " at " << base::Time::Now();
}
}
bool HostService::Disable() {
return base::DeleteFile(enabled_file_);
}
bool HostService::Enable() {
// Ensure the config file is private whilst being written.
base::DeleteFile(config_file_);
if (!WriteStdinToConfig()) {
return false;
}
if (!base::SetPosixFilePermissions(config_file_, 0600)) {
LOG(ERROR) << "Failed to set posix permission";
return false;
}
// Ensure the config is readable by the user registering the host.
// We don't seem to have API for adding Mac ACL entry for file. This code just
// uses the chmod binary to do so.
base::CommandLine chmod_cmd(base::FilePath("/bin/chmod"));
chmod_cmd.AppendArg("+a");
chmod_cmd.AppendArg("user:" + GetUsername() + ":allow:read");
chmod_cmd.AppendArgPath(config_file_);
std::string output;
if (!base::GetAppOutputAndError(chmod_cmd, &output)) {
LOG(ERROR) << "Failed to chmod file " << config_file_;
return false;
}
if (!output.empty()) {
HOST_LOG << "Message from chmod: " << output;
}
if (!base::WriteFile(enabled_file_, std::string_view())) {
LOG(ERROR) << "Failed to write enabled file";
return false;
}
return true;
}
bool HostService::WriteStdinToConfig() {
// Reads from stdin and writes it to the config file.
std::istreambuf_iterator<char> begin(std::cin);
std::istreambuf_iterator<char> end;
std::string config(begin, end);
if (!base::WriteFile(config_file_, config)) {
LOG(ERROR) << "Failed to write config file";
return false;
}
return true;
}
void HostService::PrintHostVersion() {
printf("%s\n", STRINGIZE(VERSION));
}
void HostService::PrintPid() {
// Caller running host service with privilege waits for the PID to continue,
// so we need to flush it immediately.
printf("%d\n", base::Process::Current().Pid());
fflush(stdout);
}
int HostService::RunHostFromOldScript() {
base::CommandLine cmdline(old_host_helper_file_);
cmdline.AppendSwitch(kSwitchHostRunFromLaunchd);
base::LaunchOptions options;
options.disclaim_responsibility = true;
base::Process process = base::LaunchProcess(cmdline, options);
if (!process.IsValid()) {
LOG(ERROR) << "Failed to launch the old host script for unknown reason.";
return 1;
}
g_host_pid = process.Pid();
int exit_code;
process.WaitForExit(&exit_code);
g_host_pid = 0;
return exit_code;
}
bool HostService::CheckPermission() {
LOG(INFO) << "Checking for host permissions.";
base::CommandLine cmdLine(native_messaging_host_exe_file_);
cmdLine.AppendSwitch(kCheckPermissionSwitchName);
// No need to disclaim responsibility here - the native-messaging host already
// takes care of that.
base::Process process = base::LaunchProcess(cmdLine, base::LaunchOptions());
if (!process.IsValid()) {
LOG(ERROR) << "Unable to launch native-messaging host process";
return false;
}
int exit_code;
process.WaitForExit(&exit_code);
if (exit_code != 0) {
LOG(ERROR) << "A required permission was not granted.";
return false;
}
LOG(INFO) << "All permissions granted!";
return true;
}
bool HostService::HostIsEnabled() {
return base::PathExists(enabled_file_);
}
} // namespace
} // namespace remoting
int main(int argc, char const* argv[]) {
base::AtExitManager exitManager;
base::CommandLine::Init(argc, argv);
remoting::InitHostLogging();
remoting::HostService service;
auto* current_cmdline = base::CommandLine::ForCurrentProcess();
std::string pid = base::NumberToString(base::Process::Current().Pid());
if (current_cmdline->HasSwitch(remoting::kSwitchDisable)) {
service.PrintPid();
if (!service.Disable()) {
LOG(ERROR) << "Failed to disable";
return 1;
}
} else if (current_cmdline->HasSwitch(remoting::kSwitchEnable)) {
service.PrintPid();
if (!service.Enable()) {
LOG(ERROR) << "Failed to enable";
return 1;
}
} else if (current_cmdline->HasSwitch(remoting::kSwitchSaveConfig)) {
service.PrintPid();
if (!service.WriteStdinToConfig()) {
LOG(ERROR) << "Failed to save config";
return 1;
}
} else if (current_cmdline->HasSwitch(remoting::kSwitchHostVersion)) {
service.PrintHostVersion();
} else if (current_cmdline->HasSwitch(remoting::kSwitchHostRunFromLaunchd)) {
remoting::RegisterSignalHandler();
HOST_LOG << "Host started for user " << remoting::GetUsername() << " at "
<< base::Time::Now();
return service.RunHost();
} else {
service.PrintPid();
return 1;
}
return 0;
}