// 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/mac/me2me_preference_pane.h" #import #include #include #include #import #import #include #include #include #include "base/mac/scoped_launch_data.h" #include "base/memory/scoped_ptr.h" #include "base/posix/eintr_wrapper.h" #include "remoting/host/constants_mac.h" #include "remoting/host/host_config.h" #import "remoting/host/mac/me2me_preference_pane_confirm_pin.h" #import "remoting/host/mac/me2me_preference_pane_disable.h" #include "third_party/jsoncpp/source/include/json/reader.h" #include "third_party/jsoncpp/source/include/json/writer.h" #include "third_party/modp_b64/modp_b64.h" namespace { bool GetTemporaryConfigFilePath(std::string* path) { NSString* filename = NSTemporaryDirectory(); if (filename == nil) return false; *path = [[NSString stringWithFormat:@"%@/%s", filename, remoting::kHostConfigFileName] UTF8String]; 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) { // TODO(lambroslambrou): Once the "base" target supports building for 64-bit // on Mac OS X, remove this code and replace it with |VerifyHostPinHash()| // from host/pin_hash.h. size_t separator = host_secret_hash.find(':'); if (separator == std::string::npos) return false; std::string method = host_secret_hash.substr(0, separator); if (method != "hmac") { NSLog(@"Authentication method '%s' not supported", method.c_str()); return false; } std::string hash_base64 = host_secret_hash.substr(separator + 1); // Convert |hash_base64| to |hash|, based on code from base/base64.cc. int hash_base64_size = static_cast(hash_base64.size()); std::string hash; hash.resize(modp_b64_decode_len(hash_base64_size)); // modp_b64_decode_len() returns at least 1, so hash[0] is safe here. int hash_size = modp_b64_decode(&(hash[0]), hash_base64.data(), hash_base64_size); if (hash_size < 0) { NSLog(@"Failed to parse host_secret_hash"); return false; } hash.resize(hash_size); std::string computed_hash; computed_hash.resize(CC_SHA256_DIGEST_LENGTH); CCHmac(kCCHmacAlgSHA256, host_id.data(), host_id.size(), pin.data(), pin.size(), &(computed_hash[0])); // Normally, a constant-time comparison function would be used, but it is // unnecessary here as the "secret" is already readable by the user // supplying input to this routine. return computed_hash == hash; } } // namespace // These methods are copied from base/mac, but with the logging changed to use // NSLog(). // // TODO(lambroslambrou): Once the "base" target supports building for 64-bit // on Mac OS X, remove these implementations and use the ones in base/mac. namespace base { namespace mac { // MessageForJob sends a single message to launchd with a simple dictionary // mapping |operation| to |job_label|, and returns the result of calling // launch_msg to send that message. On failure, returns NULL. The caller // assumes ownership of the returned launch_data_t object. launch_data_t MessageForJob(const std::string& job_label, const char* operation) { // launch_data_alloc returns something that needs to be freed. ScopedLaunchData message(launch_data_alloc(LAUNCH_DATA_DICTIONARY)); if (!message) { NSLog(@"launch_data_alloc"); return NULL; } // launch_data_new_string returns something that needs to be freed, but // the dictionary will assume ownership when launch_data_dict_insert is // called, so put it in a scoper and .release() it when given to the // dictionary. ScopedLaunchData job_label_launchd(launch_data_new_string(job_label.c_str())); if (!job_label_launchd) { NSLog(@"launch_data_new_string"); return NULL; } if (!launch_data_dict_insert(message, job_label_launchd.release(), operation)) { return NULL; } return launch_msg(message); } pid_t PIDForJob(const std::string& job_label) { ScopedLaunchData response(MessageForJob(job_label, LAUNCH_KEY_GETJOB)); if (!response) { return -1; } launch_data_type_t response_type = launch_data_get_type(response); if (response_type != LAUNCH_DATA_DICTIONARY) { if (response_type == LAUNCH_DATA_ERRNO) { NSLog(@"PIDForJob: error %d", launch_data_get_errno(response)); } else { NSLog(@"PIDForJob: expected dictionary, got %d", response_type); } return -1; } launch_data_t pid_data = launch_data_dict_lookup(response, LAUNCH_JOBKEY_PID); if (!pid_data) return 0; if (launch_data_get_type(pid_data) != LAUNCH_DATA_INTEGER) { NSLog(@"PIDForJob: expected integer"); return -1; } return launch_data_get_integer(pid_data); } OSStatus ExecuteWithPrivilegesAndGetPID(AuthorizationRef authorization, const char* tool_path, AuthorizationFlags options, const char** arguments, FILE** pipe, pid_t* pid) { // pipe may be NULL, but this function needs one. In that case, use a local // pipe. FILE* local_pipe; FILE** pipe_pointer; if (pipe) { pipe_pointer = pipe; } else { pipe_pointer = &local_pipe; } // AuthorizationExecuteWithPrivileges wants |char* const*| for |arguments|, // but it doesn't actually modify the arguments, and that type is kind of // silly and callers probably aren't dealing with that. Put the cast here // to make things a little easier on callers. OSStatus status = AuthorizationExecuteWithPrivileges(authorization, tool_path, options, (char* const*)arguments, pipe_pointer); if (status != errAuthorizationSuccess) { return status; } long line_pid = -1; size_t line_length = 0; char* line_c = fgetln(*pipe_pointer, &line_length); if (line_c) { if (line_length > 0 && line_c[line_length - 1] == '\n') { // line_c + line_length is the start of the next line if there is one. // Back up one character. --line_length; } std::string line(line_c, line_length); // The version in base/mac used base::StringToInt() here. line_pid = strtol(line.c_str(), NULL, 10); if (line_pid == 0) { NSLog(@"ExecuteWithPrivilegesAndGetPid: funny line: %s", line.c_str()); line_pid = -1; } } else { NSLog(@"ExecuteWithPrivilegesAndGetPid: no line"); } if (!pipe) { fclose(*pipe_pointer); } if (pid) { *pid = line_pid; } return status; } } // namespace mac } // namespace base namespace remoting { JsonHostConfig::JsonHostConfig(const std::string& filename) : filename_(filename) { } JsonHostConfig::~JsonHostConfig() { } bool JsonHostConfig::Read() { std::ifstream file(filename_.c_str()); Json::Reader reader; return reader.parse(file, config_, false /* ignore comments */); } bool JsonHostConfig::GetString(const std::string& path, std::string* out_value) const { if (!config_.isObject()) return false; if (!config_.isMember(path)) return false; Json::Value value = config_[path]; if (!value.isString()) return false; *out_value = value.asString(); return true; } std::string JsonHostConfig::GetSerializedData() const { Json::FastWriter writer; return writer.write(config_); } } // namespace remoting @implementation Me2MePreferencePane - (void)mainViewDidLoad { [authorization_view_ setDelegate:self]; [authorization_view_ setString:kAuthorizationRightExecute]; [authorization_view_ setAutoupdate:YES interval:60]; confirm_pin_view_ = [[Me2MePreferencePaneConfirmPin alloc] init]; [confirm_pin_view_ setDelegate:self]; disable_view_ = [[Me2MePreferencePaneDisable alloc] init]; [disable_view_ setDelegate:self]; } - (void)willSelect { have_new_config_ = NO; awaiting_service_stop_ = NO; NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(onNewConfigFile:) name:[NSString stringWithUTF8String:remoting::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 checkInstalledVersion]; if (!restart_pending_or_canceled_) [self readNewConfig]; [self updateUI]; } - (void)didSelect { [self checkInstalledVersion]; } - (void)willUnselect { NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter]; [center removeObserver:self]; [service_status_timer_ invalidate]; [service_status_timer_ release]; service_status_timer_ = nil; [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME]; } - (void)applyConfiguration:(id)sender pin:(NSString*)pin { 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_utf8 = [pin UTF8String]; std::string host_id, host_secret_hash; bool result = (config_->GetString(remoting::kHostIdConfigPath, &host_id) && config_->GetString(remoting::kHostSecretHashConfigPath, &host_secret_hash)); if (!result) { [self showError]; return; } if (!IsPinValid(pin_utf8, 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:""]) { NSLog(@"Failed to run the helper tool"); [self showError]; [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME]; 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]; awaiting_service_stop_ = YES; } - (void)onNewConfigFile:(NSNotification*)notification { [self checkInstalledVersion]; if (!restart_pending_or_canceled_) [self readNewConfig]; [self updateUI]; } - (void)refreshServiceStatus:(NSTimer*)timer { BOOL was_running = is_service_running_; [self updateServiceStatus]; if (awaiting_service_stop_ && !is_service_running_) { awaiting_service_stop_ = NO; [self notifyPlugin:UPDATE_SUCCEEDED_NOTIFICATION_NAME]; } 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(remoting::kServiceName); is_service_running_ = (job_pid > 0); } - (void)updateAuthorizationStatus { is_pane_unlocked_ = [authorization_view_ updateStatus:authorization_view_]; } - (void)readNewConfig { std::string file; if (!GetTemporaryConfigFilePath(&file)) { NSLog(@"Failed to get path of configuration data."); [self showError]; return; } if (access(file.c_str(), F_OK) != 0) 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. NSLog(@"Error reading configuration data from %s", file.c_str()); [self showError]; return; } remove(file.c_str()); if (!IsConfigValid(new_config_.get())) { NSLog(@"Invalid configuration data read."); [self showError]; return; } config_.swap(new_config_); have_new_config_ = YES; [confirm_pin_view_ resetPin]; } - (void)updateUI { if (have_new_config_) { [box_ setContentView:[confirm_pin_view_ view]]; } else { [box_ setContentView:[disable_view_ view]]; } // TODO(lambroslambrou): Show "enabled" and "disabled" in bold font. NSString* message; if (is_service_running_) { if (have_new_config_) { message = @"Please confirm your new PIN."; } else { message = @"Remote connections to this computer are enabled."; } } else { if (have_new_config_) { message = @"Remote connections to this computer are disabled. To enable " "remote connections you must confirm your PIN."; } else { message = @"Remote connections to this computer are disabled."; } } [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|. if (!result) { [self showError]; return; } } [disable_view_ setEnabled:(is_pane_unlocked_ && is_service_running_ && !restart_pending_or_canceled_)]; [confirm_pin_view_ setEnabled:(is_pane_unlocked_ && !restart_pending_or_canceled_)]; [confirm_pin_view_ setEmail:[NSString stringWithUTF8String:email.c_str()]]; NSString* applyButtonText = is_service_running_ ? @"Confirm" : @"Enable"; [confirm_pin_view_ setButtonText:applyButtonText]; if (restart_pending_or_canceled_) [authorization_view_ setEnabled:NO]; } - (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]) { NSLog(@"Failed to run the helper tool"); [self showError]; return; } have_new_config_ = NO; // Ensure the service is started. if (!is_service_running_) { [self sendJobControlMessage:LAUNCH_KEY_STARTJOB]; } // Broadcast a distributed notification to inform the plugin that the // configuration has been applied. [self notifyPlugin:UPDATE_SUCCEEDED_NOTIFICATION_NAME]; } - (BOOL)runHelperAsRootWithCommand:(const char*)command inputData:(const std::string&)input_data { AuthorizationRef authorization = [[authorization_view_ authorization] authorizationRef]; if (!authorization) { NSLog(@"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, remoting::kHostHelperScriptPath, kAuthorizationFlagDefaults, arguments, &pipe, &pid); if (status != errAuthorizationSuccess) { NSLog(@"AuthorizationExecuteWithPrivileges: %s (%d)", GetMacOSStatusErrorString(status), static_cast(status)); return NO; } if (pid == -1) { NSLog(@"Failed to get child PID"); if (pipe) fclose(pipe); return NO; } if (!pipe) { NSLog(@"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()) { NSLog(@"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) { NSLog(@"fclose failed with error %d", errno); error = YES; } int exit_status; pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0)); if (wait_result != pid) { NSLog(@"waitpid failed with error %d", errno); error = YES; } // No more cleanup needed. if (error) return NO; if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) { return YES; } else { NSLog(@"%s failed with exit status %d", remoting::kHostHelperScriptPath, exit_status); return NO; } } - (BOOL)sendJobControlMessage:(const char*)launch_key { base::mac::ScopedLaunchData response( base::mac::MessageForJob(remoting::kServiceName, launch_key)); if (!response) { NSLog(@"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) { NSLog(@"launchd returned unexpected type: %d", type); [self showError]; return NO; } int error = launch_data_get_errno(response.get()); if (error) { NSLog(@"launchd returned error: %d", error); [self showError]; return NO; } return YES; } - (void)notifyPlugin:(const char*)message { NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter]; NSString* name = [NSString stringWithUTF8String:message]; [center postNotificationName:name object:nil userInfo:nil]; } - (void)checkInstalledVersion { // There's no point repeating the check if the pane has already been disabled // from a previous call to this method. The pane only gets disabled when a // version-mismatch has been detected here, so skip the check, but continue to // handle the version-mismatch case. if (!restart_pending_or_canceled_) { NSBundle* this_bundle = [NSBundle bundleForClass:[self class]]; NSDictionary* this_plist = [this_bundle infoDictionary]; NSString* this_version = [this_plist objectForKey:@"CFBundleVersion"]; NSString* bundle_path = [this_bundle bundlePath]; NSString* plist_path = [bundle_path stringByAppendingString:@"/Contents/Info.plist"]; NSDictionary* disk_plist = [NSDictionary dictionaryWithContentsOfFile:plist_path]; NSString* disk_version = [disk_plist objectForKey:@"CFBundleVersion"]; if (disk_version == nil) { NSLog(@"Failed to get installed version information"); [self showError]; return; } if ([this_version isEqualToString:disk_version]) return; restart_pending_or_canceled_ = YES; [self updateUI]; } NSWindow* window = [[self mainView] window]; if (window == nil) { // Defer the alert until |didSelect| is called, which happens just after // the window is created. return; } // This alert appears as a sheet over the top of the Chromoting pref-pane, // underneath the title, so it's OK to refer to "this preference pane" rather // than repeat the title "Chromoting" here. NSAlert* alert = [[NSAlert alloc] init]; [alert setMessageText:@"System update detected"]; [alert setInformativeText:@"To use this preference pane, System Preferences " "needs to be restarted"]; [alert addButtonWithTitle:@"OK"]; NSButton* cancel_button = [alert addButtonWithTitle:@"Cancel"]; [cancel_button setKeyEquivalent:@"\e"]; [alert setAlertStyle:NSWarningAlertStyle]; [alert beginSheetModalForWindow:window modalDelegate:self didEndSelector:@selector( mismatchAlertDidEnd:returnCode:contextInfo:) contextInfo:nil]; [alert release]; } - (void)mismatchAlertDidEnd:(NSAlert*)alert returnCode:(NSInteger)returnCode contextInfo:(void*)contextInfo { if (returnCode == NSAlertFirstButtonReturn) { // OK was pressed. // Dismiss the alert window here, so that the application will respond to // the NSApp terminate: message. [[alert window] orderOut:nil]; [self restartSystemPreferences]; } else { // Cancel was pressed. // If there is a new config file, delete it and notify the web-app of // failure to apply the config. Otherwise, the web-app will remain in a // spinning state until System Preferences eventually gets restarted and // the user visits this pane again. std::string file; if (!GetTemporaryConfigFilePath(&file)) { // There's no point in alerting the user here. The same error would // happen when the pane is eventually restarted, so the user would be // alerted at that time. NSLog(@"Failed to get path of configuration data."); return; } remove(file.c_str()); [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME]; } } - (void)restartSystemPreferences { NSTask* task = [[NSTask alloc] init]; NSString* command = [NSString stringWithUTF8String:remoting::kHostHelperScriptPath]; NSArray* arguments = [NSArray arrayWithObjects:@"--relaunch-prefpane", nil]; [task setLaunchPath:command]; [task setArguments:arguments]; [task setStandardInput:[NSPipe pipe]]; [task launch]; [task release]; [NSApp terminate:nil]; } @end