blob: d07fdb4da6fb0400fe905a0f66e8a8b3f7be514f [file] [log] [blame]
// 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 <Cocoa/Cocoa.h>
#include <CommonCrypto/CommonHMAC.h>
#include <errno.h>
#include <launch.h>
#import <PreferencePanes/PreferencePanes.h>
#import <SecurityInterface/SFAuthorizationView.h>
#include <stdlib.h>
#include <unistd.h>
#include <fstream>
#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<int>(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<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.
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::kHostOwnerEmailConfigPath, &email);
if (!result) {
result = config_->GetString(remoting::kHostOwnerConfigPath, &email);
if (!result) {
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<int>(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