Chromium Code Reviews| 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..13f7b27d119c88d0d0b86c1bd2255acb4dec9c9e |
| --- /dev/null |
| +++ b/remoting/host/me2me_preference_pane.mm |
| @@ -0,0 +1,360 @@ |
| +// 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"; |
|
garykac
2012/04/27 17:14:09
At some point, it would be nice to have all these
Lambros
2012/04/28 01:54:19
Agreed, though I'd prefer to wait until this PrefP
|
| + |
| +bool GetTemporaryConfigFilePath(FilePath* path) { |
| + if (!file_util::GetTempDir(path)) |
| + return false; |
| + *path = path->Append(kServiceName ".json"); |
| + return true; |
| +} |
|
Jamie
2012/04/27 18:21:04
I don't think this is a problem, because we presen
Lambros
2012/04/28 01:54:19
Mac OS temp directories are private per-user, so w
|
| + |
| +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)) { |
| + NSLog(@"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_; |
|
Jamie
2012/04/27 18:21:04
This is probably my ObjC ignorance, but why no @sy
dcaiafa
2012/04/27 19:25:00
Because these are instance variables, not properti
|
| + |
| + // Holds the new proposed configuration if a temporary config file is |
| + // present. |
| + scoped_ptr<remoting::JsonHostConfig> config_; |
| + |
| + NSTimer* timer_; |
| + |
| + // Flags to control the UI state. These are computed in the update...Status |
| + // methods. The updateUI method updates all the controls according to the |
| + // values of these flags and the data stored in config_. |
| + BOOL is_service_running_; |
| + BOOL is_pane_unlocked_; |
| + // True if a new proposed config file has been created. |
| + BOOL have_new_config_; |
| +} |
| + |
| +- (void)mainViewDidLoad; |
| +- (void)willSelect; |
| +- (void)willUnselect; |
| +- (IBAction)onDisable:(id)sender; |
| +- (IBAction)onApply:(id)sender; |
| +- (void)onNewConfigFile:(NSNotification *)notification; |
|
Jamie
2012/04/27 18:21:04
Nit: No space before *, here and elsewhere.
Lambros
2012/04/28 01:54:19
Done.
|
| +- (void)onTimer:(NSTimer*)timer; |
|
Jamie
2012/04/27 18:21:04
onTimer is a rather generic name. Can you name it
Lambros
2012/04/28 01:54:19
Done.
|
| +- (void)authorizationViewDidAuthorize:(SFAuthorizationView *)view; |
| +- (void)authorizationViewDidDeauthorize:(SFAuthorizationView *)view; |
| +- (void)updateServiceStatus; |
| +- (void)updateAuthorizationStatus; |
| +- (void)updateConfigStatus; |
| +- (void)updateUI; |
| + |
| +// 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; |
|
Jamie
2012/04/27 18:21:04
Nit: s/InputData/inputData/
Lambros
2012/04/28 01:54:19
Done.
|
| +@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 |
|
dcaiafa
2012/04/27 17:06:00
You have to release this observer.
Lambros
2012/04/28 01:54:19
Done.
|
| + selector:@selector(onNewConfigFile:) |
| + name:@kServiceName |
| + object:nil]; |
| + |
| + timer_ = [[NSTimer scheduledTimerWithTimeInterval:2.0 |
| + target:self |
| + selector:@selector(onTimer:) |
| + userInfo:nil |
| + repeats:YES] retain]; |
| + [self updateServiceStatus]; |
| + [self updateAuthorizationStatus]; |
| + [self updateConfigStatus]; |
|
Jamie
2012/04/27 18:21:04
Why do this here? Don't you only expect a new conf
Lambros
2012/04/28 01:54:19
Yes, it's to cope with the not-already-running cas
|
| + [self updateUI]; |
| +} |
| + |
| +- (void)willUnselect { |
| + [timer_ invalidate]; |
| + [timer_ release]; |
|
aharper
2012/04/30 21:23:32
nil timer_
Lambros
2012/05/02 19:05:28
Done.
|
| +} |
| + |
| +- (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; |
| + if (!config_->GetString(remoting::kHostIdConfigPath, &host_id) || |
| + !config_->GetString(remoting::kHostSecretHashConfigPath, |
| + &host_secret_hash)) { |
| + LOG(ERROR) << "Failed to get config data for PIN verification"; |
| + return; |
|
aharper
2012/04/30 21:23:32
Should this be an error alert?
Lambros
2012/05/02 19:05:28
Done.
|
| + } |
| + if (!IsPinValid(pin, host_id, host_secret_hash)) { |
|
aharper
2012/04/30 21:23:32
I'm not seeing how this validation ties the email
Lambros
2012/05/02 19:05:28
Almost. The host registration was done using the
Wez
2012/05/03 00:13:44
Alex's point is that we're displaying the email ad
|
| + NSAlert* alert = [[NSAlert alloc] init]; |
| + [alert setMessageText:@"Incorrect PIN entered"]; |
| + [alert setAlertStyle:NSWarningAlertStyle]; |
| + [alert runModal]; |
|
aharper
2012/04/30 21:23:32
This runs application modal, which is probably ina
Lambros
2012/05/02 19:05:28
Done.
|
| + [alert release]; |
| + 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 (![self runHelperAsRootWithCommand:"--disable" |
| + InputData:""]) { |
| + NSLog(@"Failed to run the helper tool"); |
|
aharper
2012/04/30 21:23:32
Should this be an error alert?
Lambros
2012/05/02 19:05:28
Done.
|
| + } |
| + |
| + // 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"; |
|
Jamie
2012/04/27 18:21:04
Is logging an error sufficient here (and similarly
Lambros
2012/04/28 01:54:19
I expect this particular error would hardly ever h
|
| + return; |
| + } |
| + |
| + // Got a response, so check if launchd sent a non-zero error code, otherwise |
| + // assume the command was successful. |
|
Jamie
2012/04/27 18:21:04
The word "assume" makes me nervous. Why do we need
Lambros
2012/04/28 01:54:19
The thing that could go wrong (theoretically) is,
|
| + if (launch_data_get_type(response.get()) == LAUNCH_DATA_ERRNO) { |
| + int error = launch_data_get_errno(response.get()); |
| + if (error) { |
| + LOG(ERROR) << "launchd returned error " << error; |
| + } |
| + } |
| +} |
| + |
| +- (void)onNewConfigFile:(NSNotification *)notification { |
| + [self updateConfigStatus]; |
| + [self updateUI]; |
| +} |
| + |
| +- (void)onTimer:(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)updateConfigStatus { |
| + FilePath file; |
| + if (!GetTemporaryConfigFilePath(&file)) { |
| + NSLog(@"Failed to get path of configuration data."); |
|
Jamie
2012/04/27 18:21:04
You're mixing LOG with NSLog; please stick to one
Lambros
2012/04/28 01:54:19
Done.
|
| + return; |
| + } |
| + if (!file_util::PathExists(file)) { |
| + return; |
|
Jamie
2012/04/27 18:21:04
No log here? If this is called even when there's n
Lambros
2012/04/28 01:54:19
This gets called whenever the pref-pane is opened.
|
| + } |
| + config_.reset(new remoting::JsonHostConfig(file)); |
| + if (config_->Read()) { |
| + have_new_config_ = YES; |
| + file_util::Delete(file, false); |
| + } else { |
| + NSLog(@"Error reading configuration data from %s", file.value().c_str()); |
| + have_new_config_ = NO; |
|
Jamie
2012/04/27 18:21:04
This should be set at the top of the function, sin
Lambros
2012/04/28 01:54:19
I've documented the intent of this function, clari
|
| + } |
| +} |
| + |
| +- (void)updateUI { |
| + if (is_service_running_) { |
| + [status_message_ setStringValue:@"Me2Me is enabled"]; |
| + } else { |
| + [status_message_ setStringValue:@"Me2Me is disabled"]; |
| + } |
| + |
| + std::string email; |
| + if (config_.get()) { |
| + if (!config_->GetString(remoting::kXmppLoginConfigPath, &email)) { |
| + NSLog(@"Failed to get config item '%s'", remoting::kXmppLoginConfigPath); |
| + } |
| + } |
| + [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)applyNewServiceConfig { |
| + [self updateServiceStatus]; |
| + std::string serialized_config = config_->GetSerializedData(); |
| + const char* command = is_service_running_ ? "--save-config" : "--enable"; |
|
Jamie
2012/04/27 18:21:04
It doesn't need to be part of this CL, but I think
Lambros
2012/04/28 01:54:19
This would be nicer. Unfortunately, I don't think
|
| + if (![self runHelperAsRootWithCommand:command |
| + InputData:serialized_config]) { |
| + NSLog(@"Failed to run the helper tool"); |
| + return; |
| + } |
| + have_new_config_ = NO; |
|
Jamie
2012/04/27 18:21:04
Shouldn't we clear this earlier? If we get an erro
Lambros
2012/04/28 01:54:19
Well, the user could push the Apply button again :
|
| + |
| + // If the service was previously running, send a signal to cause it to reload |
| + // its configuration. |
|
Jamie
2012/04/27 18:21:04
This also feels like it would be better done by th
Lambros
2012/04/28 01:54:19
Same problem as before (although if we could someh
|
| + if (is_service_running_) { |
| + pid_t job_pid = base::mac::PIDForJob(kServiceName); |
| + if (job_pid > 0) { |
| + kill(job_pid, SIGHUP); |
| + } else { |
| + NSLog(@"Failed to obtain PID of service %s", kServiceName); |
| + } |
| + } |
| +} |
| + |
| +- (BOOL)runHelperAsRootWithCommand:(const char*)command |
| + InputData:(const std::string&)input_data { |
|
Jamie
2012/04/27 18:21:04
Nit: s/InputData/inputData/ and align colons.
Lambros
2012/04/28 01:54:19
Done.
|
| + if (!file_util::VerifyPathControlledByAdmin(FilePath(kHelperTool))) { |
|
aharper
2012/04/30 21:23:32
This method name made me nervous, because the task
Lambros
2012/05/02 19:05:28
This check was added in response to a comment made
Lambros
2012/05/03 01:47:04
Dropped, and added TODO comment.
|
| + LOG(ERROR) << "Security check failed for: " << kHelperTool; |
| + return NO; |
| + } |
| + |
| + AuthorizationRef authorization = |
| + [[authorization_view_ authorization] authorizationRef]; |
| + if (!authorization) { |
| + LOG(ERROR) << "Failed to obtain authorizationRef"; |
| + return NO; |
| + } |
| + |
| + 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"; |
|
Jamie
2012/04/27 18:21:04
Yet another type of log? Any reason this needs to
Lambros
2012/04/28 01:54:19
I've ditched NSLog, so I think we're OK now.
|
| + return NO; |
| + } |
| + if (pid == -1) { |
| + LOG(ERROR) << "Failed to get child PID"; |
| + return NO; |
| + } |
| + |
| + DCHECK(pipe); |
| + 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"; |
|
Jamie
2012/04/27 18:21:04
Close and return NO?
Lambros
2012/04/28 01:54:19
Sadly, no, because we still want to call waitpid t
|
| + } |
| + // Need to close, since the child waits for EOF on its stdin. |
| + if (fclose(pipe) != 0) { |
|
dcaiafa
2012/04/27 17:06:00
Shouldn't you close the pipe even if input_data.em
Jamie
2012/04/27 18:21:04
+1
Lambros
2012/04/28 01:54:19
Ooh, well spotted! That would leak the FILE* if no
|
| + PLOG(ERROR) << "fclose"; |
|
Jamie
2012/04/27 18:21:04
Return NO?
Lambros
2012/04/28 01:54:19
Done.
|
| + } |
| + } |
| + |
| + int exit_status; |
| + pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0)); |
| + if (wait_result != pid) { |
| + PLOG(ERROR) << "waitpid"; |
| + 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 |