Index: remoting/host/me2me_preference_pane.mm |
diff --git a/remoting/host/me2me_preference_pane.mm b/remoting/host/me2me_preference_pane.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..d092983ba081d82ff257c5f6970fa9519e77be2f |
--- /dev/null |
+++ b/remoting/host/me2me_preference_pane.mm |
@@ -0,0 +1,403 @@ |
+// 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 <Cocoa/Cocoa.h> |
+#include <launch.h> |
+#import <PreferencePanes/PreferencePanes.h> |
+#import <SecurityInterface/SFAuthorizationView.h> |
+ |
+#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<remoting::JsonHostConfig> 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 |