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