// Copyright (c) 2012 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. #import "remoting/host/me2me_preference_pane.h" #import #include #import #import #include "base/eintr_wrapper.h" #include "base/file_path.h" #include "base/file_util.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_launch_data.h" #include "base/memory/scoped_ptr.h" #include "base/stringprintf.h" #include "base/sys_string_conversions.h" #include "remoting/host/host_config.h" #include "remoting/host/json_host_config.h" #include "remoting/protocol/me2me_host_authenticator_factory.h" namespace { // The name of the Remoting Host service that is registered with launchd. #define kServiceName "org.chromium.chromoting" #define kConfigDir "/Library/PrivilegedHelperTools/" // This helper script is executed as root. It is passed a command-line option // (--enable or --disable), which causes it to create or remove a file that // informs the host's launch script of whether the host is enabled or disabled. const char kHelperTool[] = kConfigDir kServiceName ".me2me.sh"; bool GetTemporaryConfigFilePath(FilePath* path) { if (!file_util::GetTempDir(path)) return false; *path = path->Append(kServiceName ".json"); return true; } bool IsConfigValid(const remoting::JsonHostConfig* config) { std::string value; return (config->GetString(remoting::kHostIdConfigPath, &value) && config->GetString(remoting::kHostSecretHashConfigPath, &value) && config->GetString(remoting::kXmppLoginConfigPath, &value)); } bool IsPinValid(const std::string& pin, const std::string& host_id, const std::string& host_secret_hash) { remoting::protocol::SharedSecretHash hash; if (!hash.Parse(host_secret_hash)) { LOG(ERROR) << "Invalid host_secret_hash."; return false; } std::string result = remoting::protocol::AuthenticationMethod::ApplyHashFunction( hash.hash_function, host_id, pin); return result == hash.value; } } // namespace @implementation Me2MePreferencePane - (void)mainViewDidLoad { [authorization_view_ setDelegate:self]; [authorization_view_ setString:kAuthorizationRightExecute]; [authorization_view_ setAutoupdate:YES]; } - (void)willSelect { have_new_config_ = NO; NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(onNewConfigFile:) name:@kServiceName object:nil]; service_status_timer_ = [[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(refreshServiceStatus:) userInfo:nil repeats:YES] retain]; [self updateServiceStatus]; [self updateAuthorizationStatus]; [self readNewConfig]; [self updateUI]; } - (void)willUnselect { NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter]; [center removeObserver:self]; [service_status_timer_ invalidate]; [service_status_timer_ release]; service_status_timer_ = nil; } - (void)onApply:(id)sender { if (!have_new_config_) { // It shouldn't be possible to hit the button if there is no config to // apply, but check anyway just in case it happens somehow. return; } // Ensure the authorization token is up-to-date before using it. [self updateAuthorizationStatus]; [self updateUI]; std::string pin = base::SysNSStringToUTF8([pin_ stringValue]); std::string host_id, host_secret_hash; bool result = (config_->GetString(remoting::kHostIdConfigPath, &host_id) && config_->GetString(remoting::kHostSecretHashConfigPath, &host_secret_hash)); DCHECK(result); if (!IsPinValid(pin, host_id, host_secret_hash)) { [self showIncorrectPinMessage]; return; } [self applyNewServiceConfig]; [self updateUI]; } - (void)onDisable:(id)sender { // Ensure the authorization token is up-to-date before using it. [self updateAuthorizationStatus]; [self updateUI]; if (!is_pane_unlocked_) return; if (![self runHelperAsRootWithCommand:"--disable" inputData:""]) { LOG(ERROR) << "Failed to run the helper tool"; [self showError]; 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. [self sendJobControlMessage:LAUNCH_KEY_STOPJOB]; } - (void)onNewConfigFile:(NSNotification*)notification { [self readNewConfig]; [self updateUI]; } - (void)refreshServiceStatus:(NSTimer*)timer { BOOL was_running = is_service_running_; [self updateServiceStatus]; if (was_running != is_service_running_) [self updateUI]; } - (void)authorizationViewDidAuthorize:(SFAuthorizationView*)view { [self updateAuthorizationStatus]; [self updateUI]; } - (void)authorizationViewDidDeauthorize:(SFAuthorizationView*)view { [self updateAuthorizationStatus]; [self updateUI]; } - (void)updateServiceStatus { pid_t job_pid = base::mac::PIDForJob(kServiceName); is_service_running_ = (job_pid > 0); } - (void)updateAuthorizationStatus { is_pane_unlocked_ = [authorization_view_ updateStatus:authorization_view_]; } - (void)readNewConfig { FilePath file; if (!GetTemporaryConfigFilePath(&file)) { LOG(ERROR) << "Failed to get path of configuration data."; [self showError]; return; } if (!file_util::PathExists(file)) return; scoped_ptr new_config_( new remoting::JsonHostConfig(file)); if (!new_config_->Read()) { // Report the error, because the file exists but couldn't be read. The // case of non-existence is normal and expected. LOG(ERROR) << "Error reading configuration data from " << file.value(); [self showError]; return; } file_util::Delete(file, false); if (!IsConfigValid(new_config_.get())) { LOG(ERROR) << "Invalid configuration data read."; [self showError]; return; } config_.swap(new_config_); have_new_config_ = YES; } - (void)updateUI { // TODO(lambroslambrou): These strings should be localized. #ifdef OFFICIAL_BUILD NSString* name = @"Chrome Remote Desktop"; #else NSString* name = @"Chromoting"; #endif NSString* message; if (is_service_running_) { message = [NSString stringWithFormat:@"%@ is enabled", name]; } else { message = [NSString stringWithFormat:@"%@ is disabled", name]; } [status_message_ setStringValue:message]; std::string email; if (config_.get()) { bool result = config_->GetString(remoting::kXmppLoginConfigPath, &email); // The config has already been checked by |IsConfigValid|. DCHECK(result); } [email_ setStringValue:base::SysUTF8ToNSString(email)]; [disable_button_ setEnabled:(is_pane_unlocked_ && is_service_running_)]; [pin_instruction_message_ setEnabled:have_new_config_]; [email_ setEnabled:have_new_config_]; [pin_ setEnabled:have_new_config_]; [apply_button_ setEnabled:(is_pane_unlocked_ && have_new_config_)]; } - (void)showError { NSAlert* alert = [[NSAlert alloc] init]; [alert setMessageText:@"An unexpected error occurred."]; [alert setInformativeText:@"Check the system log for more information."]; [alert setAlertStyle:NSWarningAlertStyle]; [alert beginSheetModalForWindow:[[self mainView] window] modalDelegate:nil didEndSelector:nil contextInfo:nil]; [alert release]; } - (void)showIncorrectPinMessage { NSAlert* alert = [[NSAlert alloc] init]; [alert setMessageText:@"Incorrect PIN entered."]; [alert setAlertStyle:NSWarningAlertStyle]; [alert beginSheetModalForWindow:[[self mainView] window] modalDelegate:nil didEndSelector:nil contextInfo:nil]; [alert release]; } - (void)applyNewServiceConfig { [self updateServiceStatus]; std::string serialized_config = config_->GetSerializedData(); const char* command = is_service_running_ ? "--save-config" : "--enable"; if (![self runHelperAsRootWithCommand:command inputData:serialized_config]) { LOG(ERROR) << "Failed to run the helper tool"; [self showError]; return; } have_new_config_ = NO; // If the service is running, send a signal to cause it to reload its // configuration, otherwise start the service. if (is_service_running_) { pid_t job_pid = base::mac::PIDForJob(kServiceName); if (job_pid > 0) { kill(job_pid, SIGHUP); } else { LOG(ERROR) << "Failed to obtain PID of service " << kServiceName; [self showError]; } } else { [self sendJobControlMessage:LAUNCH_KEY_STARTJOB]; } } - (BOOL)runHelperAsRootWithCommand:(const char*)command inputData:(const std::string&)input_data { AuthorizationRef authorization = [[authorization_view_ authorization] authorizationRef]; if (!authorization) { LOG(ERROR) << "Failed to obtain authorizationRef"; return NO; } // 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, NULL }; FILE* pipe = NULL; pid_t pid; OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID( authorization, kHelperTool, kAuthorizationFlagDefaults, arguments, &pipe, &pid); if (status != errAuthorizationSuccess) { OSSTATUS_LOG(ERROR, status) << "AuthorizationExecuteWithPrivileges"; return NO; } if (pid == -1) { LOG(ERROR) << "Failed to get child PID"; if (pipe) fclose(pipe); return NO; } if (!pipe) { LOG(ERROR) << "Unexpected NULL pipe"; return NO; } // Some cleanup is needed (closing the pipe and waiting for the child // process), so flag any errors before returning. BOOL error = NO; 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"; error = YES; } } // In all cases, fclose() should be called with the returned FILE*. In the // case of sending data to the child, this needs to be done before calling // waitpid(), since the child reads until EOF on its stdin, so calling // waitpid() first would result in deadlock. if (fclose(pipe) != 0) { PLOG(ERROR) << "fclose"; error = YES; } int exit_status; pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0)); if (wait_result != pid) { PLOG(ERROR) << "waitpid"; error = YES; } // No more cleanup needed. if (error) return NO; if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) { return YES; } else { LOG(ERROR) << kHelperTool << " failed with exit status " << exit_status; return NO; } } - (BOOL)sendJobControlMessage:(const char*)launch_key { base::mac::ScopedLaunchData response( base::mac::MessageForJob(kServiceName, launch_key)); if (!response) { LOG(ERROR) << "Failed to send message to launchd"; [self showError]; return NO; } // Expect a response of type LAUNCH_DATA_ERRNO. launch_data_type_t type = launch_data_get_type(response.get()); if (type != LAUNCH_DATA_ERRNO) { LOG(ERROR) << "launchd returned unexpected type: " << type; [self showError]; return NO; } int error = launch_data_get_errno(response.get()); if (error) { LOG(ERROR) << "launchd returned error: " << error; [self showError]; return NO; } return YES; } @end