Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(318)

Unified Diff: remoting/host/me2me_preference_pane.mm

Issue 10171020: Preference Pane for chromoting on Mac (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Address comments. Created 8 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « remoting/host/installer/mac/ChromotingHostService.packproj ('k') | remoting/host/me2me_preference_pane.xib » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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..a922c33e6fce7228164f09005d85653269b5777b
--- /dev/null
+++ b/remoting/host/me2me_preference_pane.mm
@@ -0,0 +1,453 @@
+// 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 <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 trigger file.
+// The trigger file (defined in the service's plist file) informs launchd
+// whether the Host service should be running. Creating the trigger file causes
+// launchd to immediately start the service. Deleting the trigger file has no
+// immediate effect, but it prevents the service from being restarted if it
+// becomes stopped.
+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
+
+@interface Me2MePreferencePane : NSPreferencePane {
+ IBOutlet NSTextField* status_message_;
+ IBOutlet NSButton* disable_button_;
+ IBOutlet NSTextField* pin_instruction_message_;
+ IBOutlet NSTextField* email_;
+ IBOutlet NSTextField* pin_;
+ IBOutlet NSButton* apply_button_;
+ IBOutlet SFAuthorizationView* authorization_view_;
+
+ // Holds the new proposed configuration if a temporary config file is
+ // present.
+ scoped_ptr<remoting::JsonHostConfig> config_;
+
+ NSTimer* service_status_timer_;
+
+ // These flags determine the UI state. These are computed in the
+ // update...Status methods.
+ BOOL is_service_running_;
+ BOOL is_pane_unlocked_;
+
+ // True if a new proposed config file has been loaded into memory.
+ BOOL have_new_config_;
+}
+
+- (void)mainViewDidLoad;
+- (void)willSelect;
+- (void)willUnselect;
+- (IBAction)onDisable:(id)sender;
+- (IBAction)onApply:(id)sender;
+- (void)onNewConfigFile:(NSNotification*)notification;
+- (void)refreshServiceStatus:(NSTimer*)timer;
+- (void)authorizationViewDidAuthorize:(SFAuthorizationView*)view;
+- (void)authorizationViewDidDeauthorize:(SFAuthorizationView*)view;
+- (void)updateServiceStatus;
+- (void)updateAuthorizationStatus;
+
+// Read any new config file if present. If a config file is successfully read,
+// this deletes the file and keeps the config data loaded in memory. If this
+// method is called a second time (when the file has been deleted), the current
+// config is remembered, so this method acts as a latch: it can change
+// |have_new_config_| from NO to YES, but never from YES to NO.
+//
+// This scheme means that this method can delete the file immediately (to avoid
+// leaving a stale file around in case of a crash), but this method can safely
+// be called multiple times without forgetting the loaded config. To explicitly
+// forget the current config, set |have_new_config_| to NO.
+- (void)readNewConfig;
+
+// Update all UI controls according to any stored flags and loaded config.
+// This should be called after any sequence of operations that might change the
+// UI state.
+- (void)updateUI;
+
+// Alert the user to a generic error condition.
+- (void)showError;
+
+// Alert the user that the typed PIN is incorrect.
+- (void)showIncorrectPinMessage;
+
+// Save the new config to the system, and either start the service or inform
+// the currently-running service of the new config.
+- (void)applyNewServiceConfig;
+
+- (BOOL)runHelperAsRootWithCommand:(const char*)command
+ inputData:(const std::string&)input_data;
+@end
+
+@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.
+ base::mac::ScopedLaunchData response(
+ base::mac::MessageForJob(kServiceName, LAUNCH_KEY_STOPJOB));
+ if (!response) {
+ LOG(ERROR) << "Failed to send message to launchd";
+ [self showError];
+ return;
+ }
+
+ // 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;
+ }
+
+ int error = launch_data_get_errno(response.get());
+ if (error) {
+ LOG(ERROR) << "launchd returned error: " << error;
+ [self showError];
+ }
+}
+
+- (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 {
+ if (is_service_running_) {
+ // TODO(lambroslambrou): These strings should be branded and localized.
+ [status_message_ setStringValue:@"Chrome Remote Desktop is enabled"];
+ } else {
+ [status_message_ setStringValue:@"Chrome Remote Desktop is disabled"];
+ }
+
+ std::string email;
+ if (config_.get()) {
+ bool result = config_->GetString(remoting::kXmppLoginConfigPath, &email);
+ DCHECK(result);
Jamie 2012/05/03 16:32:26 DCHECK doesn't feel right here, since you're valid
Lambros 2012/05/03 18:24:30 Added comment.
+ }
+ [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 was previously running, send a signal to cause it to reload
+ // its configuration.
+ 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];
+ }
+ }
+}
+
+- (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;
+ }
+}
+
+@end
« no previous file with comments | « remoting/host/installer/mac/ChromotingHostService.packproj ('k') | remoting/host/me2me_preference_pane.xib » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698