blob: 29ada31a35204608110c9837135f3b7a44cbeb96 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// 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 "base/bind.h"
#include "base/files/file_path.h"
#include "base/files/scoped_file.h"
#include "base/logging.h"
#include "base/mac/authorization_util.h"
#include "base/mac/foundation_util.h"
#include "base/mac/launchd.h"
#include "base/mac/mac_logging.h"
#include "base/mac/scoped_authorizationref.h"
#include "base/mac/scoped_launch_data.h"
#include "base/memory/ptr_util.h"
#include "base/posix/eintr_wrapper.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/resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
namespace remoting {
namespace {
// 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.
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::mac::NSToCFCast(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::kHostHelperScriptPath,
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::kHostHelperScriptPath << " failed with exit status "
<< exit_status;
return false;
}
void ElevateAndSetConfig(const base::DictionaryValue& config,
const 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(config);
if (!RunHelperAsRoot(command, input_data)) {
LOG(ERROR) << "Failed to run the helper tool.";
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";
done.Run(DaemonController::RESULT_FAILED);
return;
}
}
done.Run(DaemonController::RESULT_OK);
}
void ElevateAndStopHost(const DaemonController::CompletionCallback& done) {
if (!RunHelperAsRoot("--disable", std::string())) {
LOG(ERROR) << "Failed to run the helper tool.";
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";
done.Run(DaemonController::RESULT_FAILED);
return;
}
done.Run(DaemonController::RESULT_OK);
}
} // namespace
DaemonControllerDelegateMac::DaemonControllerDelegateMac() {
LoadResources(std::string());
}
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;
}
}
std::unique_ptr<base::DictionaryValue>
DaemonControllerDelegateMac::GetConfig() {
base::FilePath config_path(kHostConfigFilePath);
std::unique_ptr<base::DictionaryValue> host_config(
HostConfigFromJsonFile(config_path));
if (!host_config)
return nullptr;
std::unique_ptr<base::DictionaryValue> config(new base::DictionaryValue);
std::string value;
if (host_config->GetString(kHostIdConfigPath, &value))
config->SetString(kHostIdConfigPath, value);
if (host_config->GetString(kXmppLoginConfigPath, &value))
config->SetString(kXmppLoginConfigPath, value);
return config;
}
void DaemonControllerDelegateMac::SetConfigAndStart(
std::unique_ptr<base::DictionaryValue> config,
bool consent,
const DaemonController::CompletionCallback& done) {
config->SetBoolean(kUsageStatsConsentConfigPath, consent);
ElevateAndSetConfig(*config, done);
}
void DaemonControllerDelegateMac::UpdateConfig(
std::unique_ptr<base::DictionaryValue> config,
const DaemonController::CompletionCallback& done) {
base::FilePath config_file_path(kHostConfigFilePath);
std::unique_ptr<base::DictionaryValue> host_config(
HostConfigFromJsonFile(config_file_path));
if (!host_config) {
done.Run(DaemonController::RESULT_FAILED);
return;
}
host_config->MergeDictionary(config.get());
ElevateAndSetConfig(*host_config, done);
}
void DaemonControllerDelegateMac::Stop(
const DaemonController::CompletionCallback& done) {
ElevateAndStopHost(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);
std::unique_ptr<base::DictionaryValue> host_config(
HostConfigFromJsonFile(config_file_path));
if (host_config) {
host_config->GetBoolean(kUsageStatsConsentConfigPath, &consent.allowed);
}
return consent;
}
scoped_refptr<DaemonController> DaemonController::Create() {
return new DaemonController(
base::WrapUnique(new DaemonControllerDelegateMac()));
}
} // namespace remoting