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"; | |
|
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
| |
| 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 } | |
|
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
| |
| 46 | |
| 47 bool IsPinValid(const std::string& pin, const std::string& host_id, | |
| 48 const std::string& host_secret_hash) { | |
| 49 remoting::protocol::SharedSecretHash hash; | |
| 50 if (!hash.Parse(host_secret_hash)) { | |
| 51 NSLog(@"Invalid host_secret_hash."); | |
| 52 return false; | |
| 53 } | |
| 54 std::string result = | |
| 55 remoting::protocol::AuthenticationMethod::ApplyHashFunction( | |
| 56 hash.hash_function, host_id, pin); | |
| 57 return result == hash.value; | |
| 58 } | |
| 59 | |
| 60 } // namespace | |
| 61 | |
| 62 @interface Me2MePreferencePane : NSPreferencePane { | |
| 63 IBOutlet NSTextField* status_message_; | |
| 64 IBOutlet NSButton* disable_button_; | |
| 65 IBOutlet NSTextField* pin_instruction_message_; | |
| 66 IBOutlet NSTextField* email_; | |
| 67 IBOutlet NSTextField* pin_; | |
| 68 IBOutlet NSButton* apply_button_; | |
| 69 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
| |
| 70 | |
| 71 // Holds the new proposed configuration if a temporary config file is | |
| 72 // present. | |
| 73 scoped_ptr<remoting::JsonHostConfig> config_; | |
| 74 | |
| 75 NSTimer* timer_; | |
| 76 | |
| 77 // Flags to control the UI state. These are computed in the update...Status | |
| 78 // methods. The updateUI method updates all the controls according to the | |
| 79 // values of these flags and the data stored in config_. | |
| 80 BOOL is_service_running_; | |
| 81 BOOL is_pane_unlocked_; | |
| 82 // True if a new proposed config file has been created. | |
| 83 BOOL have_new_config_; | |
| 84 } | |
| 85 | |
| 86 - (void)mainViewDidLoad; | |
| 87 - (void)willSelect; | |
| 88 - (void)willUnselect; | |
| 89 - (IBAction)onDisable:(id)sender; | |
| 90 - (IBAction)onApply:(id)sender; | |
| 91 - (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.
| |
| 92 - (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.
| |
| 93 - (void)authorizationViewDidAuthorize:(SFAuthorizationView *)view; | |
| 94 - (void)authorizationViewDidDeauthorize:(SFAuthorizationView *)view; | |
| 95 - (void)updateServiceStatus; | |
| 96 - (void)updateAuthorizationStatus; | |
| 97 - (void)updateConfigStatus; | |
| 98 - (void)updateUI; | |
| 99 | |
| 100 // Save the new config to the system, and either start the service or inform | |
| 101 // the currently-running service of the new config. | |
| 102 - (void)applyNewServiceConfig; | |
| 103 | |
| 104 - (BOOL)runHelperAsRootWithCommand:(const char*)command | |
| 105 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.
| |
| 106 @end | |
| 107 | |
| 108 @implementation Me2MePreferencePane | |
| 109 | |
| 110 - (void)mainViewDidLoad { | |
| 111 [authorization_view_ setDelegate:self]; | |
| 112 [authorization_view_ setString:kAuthorizationRightExecute]; | |
| 113 [authorization_view_ setAutoupdate:YES]; | |
| 114 } | |
| 115 | |
| 116 - (void)willSelect { | |
| 117 have_new_config_ = NO; | |
| 118 | |
| 119 NSDistributedNotificationCenter* center = | |
| 120 [NSDistributedNotificationCenter defaultCenter]; | |
| 121 [center addObserver:self | |
|
dcaiafa
2012/04/27 17:06:00
You have to release this observer.
Lambros
2012/04/28 01:54:19
Done.
| |
| 122 selector:@selector(onNewConfigFile:) | |
| 123 name:@kServiceName | |
| 124 object:nil]; | |
| 125 | |
| 126 timer_ = [[NSTimer scheduledTimerWithTimeInterval:2.0 | |
| 127 target:self | |
| 128 selector:@selector(onTimer:) | |
| 129 userInfo:nil | |
| 130 repeats:YES] retain]; | |
| 131 [self updateServiceStatus]; | |
| 132 [self updateAuthorizationStatus]; | |
| 133 [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
| |
| 134 [self updateUI]; | |
| 135 } | |
| 136 | |
| 137 - (void)willUnselect { | |
| 138 [timer_ invalidate]; | |
| 139 [timer_ release]; | |
|
aharper
2012/04/30 21:23:32
nil timer_
Lambros
2012/05/02 19:05:28
Done.
| |
| 140 } | |
| 141 | |
| 142 - (void)onApply:(id)sender { | |
| 143 if (!have_new_config_) { | |
| 144 // It shouldn't be possible to hit the button if there is no config to | |
| 145 // apply, but check anyway just in case it happens somehow. | |
| 146 return; | |
| 147 } | |
| 148 | |
| 149 // Ensure the authorization token is up-to-date before using it. | |
| 150 [self updateAuthorizationStatus]; | |
| 151 [self updateUI]; | |
| 152 | |
| 153 std::string pin = base::SysNSStringToUTF8([pin_ stringValue]); | |
| 154 std::string host_id, host_secret_hash; | |
| 155 if (!config_->GetString(remoting::kHostIdConfigPath, &host_id) || | |
| 156 !config_->GetString(remoting::kHostSecretHashConfigPath, | |
| 157 &host_secret_hash)) { | |
| 158 LOG(ERROR) << "Failed to get config data for PIN verification"; | |
| 159 return; | |
|
aharper
2012/04/30 21:23:32
Should this be an error alert?
Lambros
2012/05/02 19:05:28
Done.
| |
| 160 } | |
| 161 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
| |
| 162 NSAlert* alert = [[NSAlert alloc] init]; | |
| 163 [alert setMessageText:@"Incorrect PIN entered"]; | |
| 164 [alert setAlertStyle:NSWarningAlertStyle]; | |
| 165 [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.
| |
| 166 [alert release]; | |
| 167 return; | |
| 168 } | |
| 169 | |
| 170 [self applyNewServiceConfig]; | |
| 171 [self updateUI]; | |
| 172 } | |
| 173 | |
| 174 - (void)onDisable:(id)sender { | |
| 175 // Ensure the authorization token is up-to-date before using it. | |
| 176 [self updateAuthorizationStatus]; | |
| 177 [self updateUI]; | |
| 178 | |
| 179 if (![self runHelperAsRootWithCommand:"--disable" | |
| 180 InputData:""]) { | |
| 181 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.
| |
| 182 } | |
| 183 | |
| 184 // Stop the launchd job. This cannot easily be done by the helper tool, | |
| 185 // since the launchd job runs in the current user's context. | |
| 186 base::mac::ScopedLaunchData response( | |
| 187 base::mac::MessageForJob(kServiceName, LAUNCH_KEY_STOPJOB)); | |
| 188 if (!response) { | |
| 189 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
| |
| 190 return; | |
| 191 } | |
| 192 | |
| 193 // Got a response, so check if launchd sent a non-zero error code, otherwise | |
| 194 // 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,
| |
| 195 if (launch_data_get_type(response.get()) == LAUNCH_DATA_ERRNO) { | |
| 196 int error = launch_data_get_errno(response.get()); | |
| 197 if (error) { | |
| 198 LOG(ERROR) << "launchd returned error " << error; | |
| 199 } | |
| 200 } | |
| 201 } | |
| 202 | |
| 203 - (void)onNewConfigFile:(NSNotification *)notification { | |
| 204 [self updateConfigStatus]; | |
| 205 [self updateUI]; | |
| 206 } | |
| 207 | |
| 208 - (void)onTimer:(NSTimer*)timer { | |
| 209 BOOL was_running = is_service_running_; | |
| 210 [self updateServiceStatus]; | |
| 211 if (was_running != is_service_running_) | |
| 212 [self updateUI]; | |
| 213 } | |
| 214 | |
| 215 - (void)authorizationViewDidAuthorize:(SFAuthorizationView *)view { | |
| 216 [self updateAuthorizationStatus]; | |
| 217 [self updateUI]; | |
| 218 } | |
| 219 | |
| 220 - (void)authorizationViewDidDeauthorize:(SFAuthorizationView *)view { | |
| 221 [self updateAuthorizationStatus]; | |
| 222 [self updateUI]; | |
| 223 } | |
| 224 | |
| 225 - (void)updateServiceStatus { | |
| 226 pid_t job_pid = base::mac::PIDForJob(kServiceName); | |
| 227 is_service_running_ = (job_pid > 0); | |
| 228 } | |
| 229 | |
| 230 - (void)updateAuthorizationStatus { | |
| 231 is_pane_unlocked_ = [authorization_view_ updateStatus:authorization_view_]; | |
| 232 } | |
| 233 | |
| 234 - (void)updateConfigStatus { | |
| 235 FilePath file; | |
| 236 if (!GetTemporaryConfigFilePath(&file)) { | |
| 237 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.
| |
| 238 return; | |
| 239 } | |
| 240 if (!file_util::PathExists(file)) { | |
| 241 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.
| |
| 242 } | |
| 243 config_.reset(new remoting::JsonHostConfig(file)); | |
| 244 if (config_->Read()) { | |
| 245 have_new_config_ = YES; | |
| 246 file_util::Delete(file, false); | |
| 247 } else { | |
| 248 NSLog(@"Error reading configuration data from %s", file.value().c_str()); | |
| 249 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
| |
| 250 } | |
| 251 } | |
| 252 | |
| 253 - (void)updateUI { | |
| 254 if (is_service_running_) { | |
| 255 [status_message_ setStringValue:@"Me2Me is enabled"]; | |
| 256 } else { | |
| 257 [status_message_ setStringValue:@"Me2Me is disabled"]; | |
| 258 } | |
| 259 | |
| 260 std::string email; | |
| 261 if (config_.get()) { | |
| 262 if (!config_->GetString(remoting::kXmppLoginConfigPath, &email)) { | |
| 263 NSLog(@"Failed to get config item '%s'", remoting::kXmppLoginConfigPath); | |
| 264 } | |
| 265 } | |
| 266 [email_ setStringValue:base::SysUTF8ToNSString(email)]; | |
| 267 | |
| 268 [disable_button_ setEnabled:(is_pane_unlocked_ && is_service_running_)]; | |
| 269 [pin_instruction_message_ setEnabled:have_new_config_]; | |
| 270 [email_ setEnabled:have_new_config_]; | |
| 271 [pin_ setEnabled:have_new_config_]; | |
| 272 [apply_button_ setEnabled:(is_pane_unlocked_ && have_new_config_)]; | |
| 273 } | |
| 274 | |
| 275 - (void)applyNewServiceConfig { | |
| 276 [self updateServiceStatus]; | |
| 277 std::string serialized_config = config_->GetSerializedData(); | |
| 278 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
| |
| 279 if (![self runHelperAsRootWithCommand:command | |
| 280 InputData:serialized_config]) { | |
| 281 NSLog(@"Failed to run the helper tool"); | |
| 282 return; | |
| 283 } | |
| 284 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 :
| |
| 285 | |
| 286 // If the service was previously running, send a signal to cause it to reload | |
| 287 // 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
| |
| 288 if (is_service_running_) { | |
| 289 pid_t job_pid = base::mac::PIDForJob(kServiceName); | |
| 290 if (job_pid > 0) { | |
| 291 kill(job_pid, SIGHUP); | |
| 292 } else { | |
| 293 NSLog(@"Failed to obtain PID of service %s", kServiceName); | |
| 294 } | |
| 295 } | |
| 296 } | |
| 297 | |
| 298 - (BOOL)runHelperAsRootWithCommand:(const char*)command | |
| 299 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.
| |
| 300 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.
| |
| 301 LOG(ERROR) << "Security check failed for: " << kHelperTool; | |
| 302 return NO; | |
| 303 } | |
| 304 | |
| 305 AuthorizationRef authorization = | |
| 306 [[authorization_view_ authorization] authorizationRef]; | |
| 307 if (!authorization) { | |
| 308 LOG(ERROR) << "Failed to obtain authorizationRef"; | |
| 309 return NO; | |
| 310 } | |
| 311 | |
| 312 const char* arguments[] = { command, NULL }; | |
| 313 FILE* pipe = NULL; | |
| 314 pid_t pid; | |
| 315 OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID( | |
| 316 authorization, | |
| 317 kHelperTool, | |
| 318 kAuthorizationFlagDefaults, | |
| 319 arguments, | |
| 320 &pipe, | |
| 321 &pid); | |
| 322 if (status != errAuthorizationSuccess) { | |
| 323 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.
| |
| 324 return NO; | |
| 325 } | |
| 326 if (pid == -1) { | |
| 327 LOG(ERROR) << "Failed to get child PID"; | |
| 328 return NO; | |
| 329 } | |
| 330 | |
| 331 DCHECK(pipe); | |
| 332 if (!input_data.empty()) { | |
| 333 size_t bytes_written = fwrite(input_data.data(), sizeof(char), | |
| 334 input_data.size(), pipe); | |
| 335 // According to the fwrite manpage, a partial count is returned only if a | |
| 336 // write error has occurred. | |
| 337 if (bytes_written != input_data.size()) { | |
| 338 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
| |
| 339 } | |
| 340 // Need to close, since the child waits for EOF on its stdin. | |
| 341 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
| |
| 342 PLOG(ERROR) << "fclose"; | |
|
Jamie
2012/04/27 18:21:04
Return NO?
Lambros
2012/04/28 01:54:19
Done.
| |
| 343 } | |
| 344 } | |
| 345 | |
| 346 int exit_status; | |
| 347 pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0)); | |
| 348 if (wait_result != pid) { | |
| 349 PLOG(ERROR) << "waitpid"; | |
| 350 return NO; | |
| 351 } | |
| 352 if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) { | |
| 353 return YES; | |
| 354 } else { | |
| 355 LOG(ERROR) << kHelperTool << " failed with exit status " << exit_status; | |
| 356 return NO; | |
| 357 } | |
| 358 } | |
| 359 | |
| 360 @end | |
| OLD | NEW |