| // 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 |