Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #import <Cocoa/Cocoa.h> | |
| 6 #include <launch.h> | |
| 7 #import <PreferencePanes/PreferencePanes.h> | |
| 8 #import <SecurityInterface/SFAuthorizationView.h> | |
| 9 | |
| 10 #include "base/eintr_wrapper.h" | |
| 11 #include "base/file_path.h" | |
| 12 #include "base/file_util.h" | |
| 13 #include "base/logging.h" | |
| 14 #include "base/mac/authorization_util.h" | |
| 15 #include "base/mac/foundation_util.h" | |
| 16 #include "base/mac/launchd.h" | |
| 17 #include "base/mac/mac_logging.h" | |
| 18 #include "base/mac/scoped_launch_data.h" | |
| 19 #include "base/memory/scoped_ptr.h" | |
| 20 #include "base/stringprintf.h" | |
| 21 #include "base/sys_string_conversions.h" | |
| 22 #include "remoting/host/host_config.h" | |
| 23 #include "remoting/host/json_host_config.h" | |
| 24 #include "remoting/protocol/me2me_host_authenticator_factory.h" | |
| 25 | |
| 26 namespace { | |
| 27 // The name of the Remoting Host service that is registered with launchd. | |
| 28 #define kServiceName "org.chromium.chromoting" | |
| 29 #define kConfigDir "/Library/PrivilegedHelperTools/" | |
| 30 | |
| 31 // This helper script is executed as root. It is passed a command-line option | |
| 32 // (--enable or --disable), which causes it to create or remove a trigger file. | |
| 33 // The trigger file (defined in the service's plist file) informs launchd | |
| 34 // whether the Host service should be running. Creating the trigger file causes | |
| 35 // launchd to immediately start the service. Deleting the trigger file has no | |
| 36 // immediate effect, but it prevents the service from being restarted if it | |
| 37 // becomes stopped. | |
| 38 const char kHelperTool[] = kConfigDir kServiceName ".me2me.sh"; | |
| 39 | |
| 40 bool GetTemporaryConfigFilePath(FilePath* path) { | |
| 41 if (!file_util::GetTempDir(path)) | |
| 42 return false; | |
| 43 *path = path->Append(kServiceName ".json"); | |
| 44 return true; | |
| 45 } | |
| 46 | |
| 47 bool IsConfigValid(const remoting::JsonHostConfig* config) { | |
| 48 std::string value; | |
| 49 return (config->GetString(remoting::kHostIdConfigPath, &value) && | |
| 50 config->GetString(remoting::kHostSecretHashConfigPath, &value) && | |
| 51 config->GetString(remoting::kXmppLoginConfigPath, &value)); | |
| 52 } | |
| 53 | |
| 54 bool IsPinValid(const std::string& pin, const std::string& host_id, | |
| 55 const std::string& host_secret_hash) { | |
| 56 remoting::protocol::SharedSecretHash hash; | |
| 57 if (!hash.Parse(host_secret_hash)) { | |
| 58 LOG(ERROR) << "Invalid host_secret_hash."; | |
| 59 return false; | |
| 60 } | |
| 61 std::string result = | |
| 62 remoting::protocol::AuthenticationMethod::ApplyHashFunction( | |
| 63 hash.hash_function, host_id, pin); | |
| 64 return result == hash.value; | |
| 65 } | |
| 66 | |
| 67 } // namespace | |
| 68 | |
| 69 @interface Me2MePreferencePane : NSPreferencePane { | |
| 70 IBOutlet NSTextField* status_message_; | |
| 71 IBOutlet NSButton* disable_button_; | |
| 72 IBOutlet NSTextField* pin_instruction_message_; | |
| 73 IBOutlet NSTextField* email_; | |
| 74 IBOutlet NSTextField* pin_; | |
| 75 IBOutlet NSButton* apply_button_; | |
| 76 IBOutlet SFAuthorizationView* authorization_view_; | |
| 77 | |
| 78 // Holds the new proposed configuration if a temporary config file is | |
| 79 // present. | |
| 80 scoped_ptr<remoting::JsonHostConfig> config_; | |
| 81 | |
| 82 NSTimer* service_status_timer_; | |
| 83 | |
| 84 // These flags determine the UI state. These are computed in the | |
| 85 // update...Status methods. | |
| 86 BOOL is_service_running_; | |
| 87 BOOL is_pane_unlocked_; | |
| 88 | |
| 89 // True if a new proposed config file has been loaded into memory. | |
| 90 BOOL have_new_config_; | |
| 91 } | |
| 92 | |
| 93 - (void)mainViewDidLoad; | |
| 94 - (void)willSelect; | |
| 95 - (void)willUnselect; | |
| 96 - (IBAction)onDisable:(id)sender; | |
| 97 - (IBAction)onApply:(id)sender; | |
| 98 - (void)onNewConfigFile:(NSNotification*)notification; | |
| 99 - (void)refreshServiceStatus:(NSTimer*)timer; | |
| 100 - (void)authorizationViewDidAuthorize:(SFAuthorizationView*)view; | |
| 101 - (void)authorizationViewDidDeauthorize:(SFAuthorizationView*)view; | |
| 102 - (void)updateServiceStatus; | |
| 103 - (void)updateAuthorizationStatus; | |
| 104 | |
| 105 // Read any new config file if present. If a config file is successfully read, | |
| 106 // this deletes the file and keeps the config data loaded in memory. If this | |
| 107 // method is called a second time (when the file has been deleted), the current | |
| 108 // config is remembered, so this method acts as a latch: it can change | |
| 109 // |have_new_config_| from NO to YES, but never from YES to NO. | |
| 110 // | |
| 111 // This scheme means that this method can delete the file immediately (to avoid | |
| 112 // leaving a stale file around in case of a crash), but this method can safely | |
| 113 // be called multiple times without forgetting the loaded config. To explicitly | |
| 114 // forget the current config, set |have_new_config_| to NO. | |
| 115 - (void)readNewConfig; | |
| 116 | |
| 117 // Update all UI controls according to any stored flags and loaded config. | |
| 118 // This should be called after any sequence of operations that might change the | |
| 119 // UI state. | |
| 120 - (void)updateUI; | |
| 121 | |
| 122 // Alert the user to a generic error condition. | |
| 123 - (void)showError; | |
| 124 | |
| 125 // Alert the user that the typed PIN is incorrect. | |
| 126 - (void)showIncorrectPinMessage; | |
| 127 | |
| 128 // Save the new config to the system, and either start the service or inform | |
| 129 // the currently-running service of the new config. | |
| 130 - (void)applyNewServiceConfig; | |
| 131 | |
| 132 - (BOOL)runHelperAsRootWithCommand:(const char*)command | |
| 133 inputData:(const std::string&)input_data; | |
| 134 @end | |
| 135 | |
| 136 @implementation Me2MePreferencePane | |
| 137 | |
| 138 - (void)mainViewDidLoad { | |
| 139 [authorization_view_ setDelegate:self]; | |
| 140 [authorization_view_ setString:kAuthorizationRightExecute]; | |
| 141 [authorization_view_ setAutoupdate:YES]; | |
| 142 } | |
| 143 | |
| 144 - (void)willSelect { | |
| 145 have_new_config_ = NO; | |
| 146 | |
| 147 NSDistributedNotificationCenter* center = | |
| 148 [NSDistributedNotificationCenter defaultCenter]; | |
| 149 [center addObserver:self | |
| 150 selector:@selector(onNewConfigFile:) | |
| 151 name:@kServiceName | |
| 152 object:nil]; | |
| 153 | |
| 154 service_status_timer_ = | |
| 155 [[NSTimer scheduledTimerWithTimeInterval:2.0 | |
| 156 target:self | |
| 157 selector:@selector(refreshServiceStatus:) | |
| 158 userInfo:nil | |
| 159 repeats:YES] retain]; | |
| 160 [self updateServiceStatus]; | |
| 161 [self updateAuthorizationStatus]; | |
| 162 [self readNewConfig]; | |
| 163 [self updateUI]; | |
| 164 } | |
| 165 | |
| 166 - (void)willUnselect { | |
| 167 NSDistributedNotificationCenter* center = | |
| 168 [NSDistributedNotificationCenter defaultCenter]; | |
| 169 [center removeObserver:self]; | |
| 170 | |
| 171 [service_status_timer_ invalidate]; | |
| 172 [service_status_timer_ release]; | |
| 173 service_status_timer_ = nil; | |
| 174 } | |
| 175 | |
| 176 - (void)onApply:(id)sender { | |
| 177 if (!have_new_config_) { | |
| 178 // It shouldn't be possible to hit the button if there is no config to | |
| 179 // apply, but check anyway just in case it happens somehow. | |
| 180 return; | |
| 181 } | |
| 182 | |
| 183 // Ensure the authorization token is up-to-date before using it. | |
| 184 [self updateAuthorizationStatus]; | |
| 185 [self updateUI]; | |
| 186 | |
| 187 std::string pin = base::SysNSStringToUTF8([pin_ stringValue]); | |
| 188 std::string host_id, host_secret_hash; | |
| 189 bool result = (config_->GetString(remoting::kHostIdConfigPath, &host_id) && | |
| 190 config_->GetString(remoting::kHostSecretHashConfigPath, | |
| 191 &host_secret_hash)); | |
| 192 DCHECK(result); | |
| 193 if (!IsPinValid(pin, host_id, host_secret_hash)) { | |
| 194 [self showIncorrectPinMessage]; | |
| 195 return; | |
| 196 } | |
| 197 | |
| 198 [self applyNewServiceConfig]; | |
| 199 [self updateUI]; | |
| 200 } | |
| 201 | |
| 202 - (void)onDisable:(id)sender { | |
| 203 // Ensure the authorization token is up-to-date before using it. | |
| 204 [self updateAuthorizationStatus]; | |
| 205 [self updateUI]; | |
| 206 if (!is_pane_unlocked_) | |
| 207 return; | |
| 208 | |
| 209 if (![self runHelperAsRootWithCommand:"--disable" | |
| 210 inputData:""]) { | |
| 211 LOG(ERROR) << "Failed to run the helper tool"; | |
| 212 [self showError]; | |
| 213 return; | |
| 214 } | |
| 215 | |
| 216 // Stop the launchd job. This cannot easily be done by the helper tool, | |
| 217 // since the launchd job runs in the current user's context. | |
| 218 base::mac::ScopedLaunchData response( | |
| 219 base::mac::MessageForJob(kServiceName, LAUNCH_KEY_STOPJOB)); | |
| 220 if (!response) { | |
| 221 LOG(ERROR) << "Failed to send message to launchd"; | |
| 222 [self showError]; | |
| 223 return; | |
| 224 } | |
| 225 | |
| 226 // Expect a response of type LAUNCH_DATA_ERRNO. | |
| 227 launch_data_type_t type = launch_data_get_type(response.get()); | |
| 228 if (type != LAUNCH_DATA_ERRNO) { | |
| 229 LOG(ERROR) << "launchd returned unexpected type: " << type; | |
| 230 [self showError]; | |
| 231 return; | |
| 232 } | |
| 233 | |
| 234 int error = launch_data_get_errno(response.get()); | |
| 235 if (error) { | |
| 236 LOG(ERROR) << "launchd returned error: " << error; | |
| 237 [self showError]; | |
| 238 } | |
| 239 } | |
| 240 | |
| 241 - (void)onNewConfigFile:(NSNotification*)notification { | |
| 242 [self readNewConfig]; | |
| 243 [self updateUI]; | |
| 244 } | |
| 245 | |
| 246 - (void)refreshServiceStatus:(NSTimer*)timer { | |
| 247 BOOL was_running = is_service_running_; | |
| 248 [self updateServiceStatus]; | |
| 249 if (was_running != is_service_running_) | |
| 250 [self updateUI]; | |
| 251 } | |
| 252 | |
| 253 - (void)authorizationViewDidAuthorize:(SFAuthorizationView*)view { | |
| 254 [self updateAuthorizationStatus]; | |
| 255 [self updateUI]; | |
| 256 } | |
| 257 | |
| 258 - (void)authorizationViewDidDeauthorize:(SFAuthorizationView*)view { | |
| 259 [self updateAuthorizationStatus]; | |
| 260 [self updateUI]; | |
| 261 } | |
| 262 | |
| 263 - (void)updateServiceStatus { | |
| 264 pid_t job_pid = base::mac::PIDForJob(kServiceName); | |
| 265 is_service_running_ = (job_pid > 0); | |
| 266 } | |
| 267 | |
| 268 - (void)updateAuthorizationStatus { | |
| 269 is_pane_unlocked_ = [authorization_view_ updateStatus:authorization_view_]; | |
| 270 } | |
| 271 | |
| 272 - (void)readNewConfig { | |
| 273 FilePath file; | |
| 274 if (!GetTemporaryConfigFilePath(&file)) { | |
| 275 LOG(ERROR) << "Failed to get path of configuration data."; | |
| 276 [self showError]; | |
| 277 return; | |
| 278 } | |
| 279 if (!file_util::PathExists(file)) | |
| 280 return; | |
| 281 | |
| 282 scoped_ptr<remoting::JsonHostConfig> new_config_( | |
| 283 new remoting::JsonHostConfig(file)); | |
| 284 if (!new_config_->Read()) { | |
| 285 // Report the error, because the file exists but couldn't be read. The | |
| 286 // case of non-existence is normal and expected. | |
| 287 LOG(ERROR) << "Error reading configuration data from " << file.value(); | |
| 288 [self showError]; | |
| 289 return; | |
| 290 } | |
| 291 file_util::Delete(file, false); | |
| 292 if (!IsConfigValid(new_config_.get())) { | |
| 293 LOG(ERROR) << "Invalid configuration data read."; | |
| 294 [self showError]; | |
| 295 return; | |
| 296 } | |
| 297 | |
| 298 config_.swap(new_config_); | |
| 299 have_new_config_ = YES; | |
| 300 } | |
| 301 | |
| 302 - (void)updateUI { | |
| 303 if (is_service_running_) { | |
| 304 // TODO(lambroslambrou): These strings should be branded and localized. | |
| 305 [status_message_ setStringValue:@"Chrome Remote Desktop is enabled"]; | |
| 306 } else { | |
| 307 [status_message_ setStringValue:@"Chrome Remote Desktop is disabled"]; | |
| 308 } | |
| 309 | |
| 310 std::string email; | |
| 311 if (config_.get()) { | |
| 312 bool result = config_->GetString(remoting::kXmppLoginConfigPath, &email); | |
| 313 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.
| |
| 314 } | |
| 315 [email_ setStringValue:base::SysUTF8ToNSString(email)]; | |
| 316 | |
| 317 [disable_button_ setEnabled:(is_pane_unlocked_ && is_service_running_)]; | |
| 318 [pin_instruction_message_ setEnabled:have_new_config_]; | |
| 319 [email_ setEnabled:have_new_config_]; | |
| 320 [pin_ setEnabled:have_new_config_]; | |
| 321 [apply_button_ setEnabled:(is_pane_unlocked_ && have_new_config_)]; | |
| 322 } | |
| 323 | |
| 324 - (void)showError { | |
| 325 NSAlert* alert = [[NSAlert alloc] init]; | |
| 326 [alert setMessageText:@"An unexpected error occurred."]; | |
| 327 [alert setInformativeText:@"Check the system log for more information."]; | |
| 328 [alert setAlertStyle:NSWarningAlertStyle]; | |
| 329 [alert beginSheetModalForWindow:[[self mainView] window] | |
| 330 modalDelegate:nil | |
| 331 didEndSelector:nil | |
| 332 contextInfo:nil]; | |
| 333 [alert release]; | |
| 334 } | |
| 335 | |
| 336 - (void)showIncorrectPinMessage { | |
| 337 NSAlert* alert = [[NSAlert alloc] init]; | |
| 338 [alert setMessageText:@"Incorrect PIN entered."]; | |
| 339 [alert setAlertStyle:NSWarningAlertStyle]; | |
| 340 [alert beginSheetModalForWindow:[[self mainView] window] | |
| 341 modalDelegate:nil | |
| 342 didEndSelector:nil | |
| 343 contextInfo:nil]; | |
| 344 [alert release]; | |
| 345 } | |
| 346 | |
| 347 - (void)applyNewServiceConfig { | |
| 348 [self updateServiceStatus]; | |
| 349 std::string serialized_config = config_->GetSerializedData(); | |
| 350 const char* command = is_service_running_ ? "--save-config" : "--enable"; | |
| 351 if (![self runHelperAsRootWithCommand:command | |
| 352 inputData:serialized_config]) { | |
| 353 LOG(ERROR) << "Failed to run the helper tool"; | |
| 354 [self showError]; | |
| 355 return; | |
| 356 } | |
| 357 have_new_config_ = NO; | |
| 358 | |
| 359 // If the service was previously running, send a signal to cause it to reload | |
| 360 // its configuration. | |
| 361 if (is_service_running_) { | |
| 362 pid_t job_pid = base::mac::PIDForJob(kServiceName); | |
| 363 if (job_pid > 0) { | |
| 364 kill(job_pid, SIGHUP); | |
| 365 } else { | |
| 366 LOG(ERROR) << "Failed to obtain PID of service " << kServiceName; | |
| 367 [self showError]; | |
| 368 } | |
| 369 } | |
| 370 } | |
| 371 | |
| 372 - (BOOL)runHelperAsRootWithCommand:(const char*)command | |
| 373 inputData:(const std::string&)input_data { | |
| 374 AuthorizationRef authorization = | |
| 375 [[authorization_view_ authorization] authorizationRef]; | |
| 376 if (!authorization) { | |
| 377 LOG(ERROR) << "Failed to obtain authorizationRef"; | |
| 378 return NO; | |
| 379 } | |
| 380 | |
| 381 // TODO(lambroslambrou): Replace the deprecated ExecuteWithPrivileges | |
| 382 // call with a launchd-based helper tool, which is more secure. | |
| 383 // http://crbug.com/120903 | |
| 384 const char* arguments[] = { command, NULL }; | |
| 385 FILE* pipe = NULL; | |
| 386 pid_t pid; | |
| 387 OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID( | |
| 388 authorization, | |
| 389 kHelperTool, | |
| 390 kAuthorizationFlagDefaults, | |
| 391 arguments, | |
| 392 &pipe, | |
| 393 &pid); | |
| 394 if (status != errAuthorizationSuccess) { | |
| 395 OSSTATUS_LOG(ERROR, status) << "AuthorizationExecuteWithPrivileges"; | |
| 396 return NO; | |
| 397 } | |
| 398 if (pid == -1) { | |
| 399 LOG(ERROR) << "Failed to get child PID"; | |
| 400 if (pipe) | |
| 401 fclose(pipe); | |
| 402 | |
| 403 return NO; | |
| 404 } | |
| 405 if (!pipe) { | |
| 406 LOG(ERROR) << "Unexpected NULL pipe"; | |
| 407 return NO; | |
| 408 } | |
| 409 | |
| 410 // Some cleanup is needed (closing the pipe and waiting for the child | |
| 411 // process), so flag any errors before returning. | |
| 412 BOOL error = NO; | |
| 413 | |
| 414 if (!input_data.empty()) { | |
| 415 size_t bytes_written = fwrite(input_data.data(), sizeof(char), | |
| 416 input_data.size(), pipe); | |
| 417 // According to the fwrite manpage, a partial count is returned only if a | |
| 418 // write error has occurred. | |
| 419 if (bytes_written != input_data.size()) { | |
| 420 LOG(ERROR) << "Failed to write data to child process"; | |
| 421 error = YES; | |
| 422 } | |
| 423 } | |
| 424 | |
| 425 // In all cases, fclose() should be called with the returned FILE*. In the | |
| 426 // case of sending data to the child, this needs to be done before calling | |
| 427 // waitpid(), since the child reads until EOF on its stdin, so calling | |
| 428 // waitpid() first would result in deadlock. | |
| 429 if (fclose(pipe) != 0) { | |
| 430 PLOG(ERROR) << "fclose"; | |
| 431 error = YES; | |
| 432 } | |
| 433 | |
| 434 int exit_status; | |
| 435 pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0)); | |
| 436 if (wait_result != pid) { | |
| 437 PLOG(ERROR) << "waitpid"; | |
| 438 error = YES; | |
| 439 } | |
| 440 | |
| 441 // No more cleanup needed. | |
| 442 if (error) | |
| 443 return NO; | |
| 444 | |
| 445 if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) { | |
| 446 return YES; | |
| 447 } else { | |
| 448 LOG(ERROR) << kHelperTool << " failed with exit status " << exit_status; | |
| 449 return NO; | |
| 450 } | |
| 451 } | |
| 452 | |
| 453 @end | |
| OLD | NEW |