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 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 LOG(ERROR) << "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_; | |
70 | |
71 // Holds the new proposed configuration if a temporary config file is | |
72 // present. | |
73 scoped_ptr<remoting::JsonHostConfig> config_; | |
74 | |
75 NSTimer* service_status_timer_; | |
76 | |
77 // These flags determine the UI state. These are computed in the | |
78 // update...Status methods. | |
79 BOOL is_service_running_; | |
80 BOOL is_pane_unlocked_; | |
81 | |
82 // True if a new proposed config file has been loaded into memory. | |
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; | |
92 - (void)refreshServiceStatus:(NSTimer*)timer; | |
93 - (void)authorizationViewDidAuthorize:(SFAuthorizationView*)view; | |
94 - (void)authorizationViewDidDeauthorize:(SFAuthorizationView*)view; | |
95 - (void)updateServiceStatus; | |
96 - (void)updateAuthorizationStatus; | |
97 | |
98 // Read any new config file if present. If a config file is successfully read, | |
99 // this deletes the file and keeps the config data loaded in memory. If this | |
100 // method is called a second time (when the file has been deleted), the current | |
101 // config is remembered, so this method acts as a latch: it can change | |
102 // |have_new_config_| from NO to YES, but never from YES to NO. | |
103 // | |
104 // This scheme means that this method can delete the file immediately (to avoid | |
105 // leaving a stale file around in case of a crash), but this method can safely | |
106 // be called multiple times without forgetting the loaded config. To explicitly | |
107 // forget the current config, set |have_new_config_| to NO. | |
108 - (void)readNewConfig; | |
109 | |
110 // Update all UI controls according to any stored flags and loaded config. | |
111 // This should be called after any sequence of operations that might change the | |
112 // UI state. | |
113 - (void)updateUI; | |
114 | |
115 // Alert the user to a generic error condition. | |
116 - (void)showError; | |
117 | |
118 // Alert the user that the typed PIN is incorrect. | |
119 - (void)showIncorrectPinMessage; | |
120 | |
121 // Save the new config to the system, and either start the service or inform | |
122 // the currently-running service of the new config. | |
123 - (void)applyNewServiceConfig; | |
124 | |
125 - (BOOL)runHelperAsRootWithCommand:(const char*)command | |
126 inputData:(const std::string&)input_data; | |
127 @end | |
128 | |
129 @implementation Me2MePreferencePane | |
130 | |
131 - (void)mainViewDidLoad { | |
132 [authorization_view_ setDelegate:self]; | |
133 [authorization_view_ setString:kAuthorizationRightExecute]; | |
134 [authorization_view_ setAutoupdate:YES]; | |
135 } | |
136 | |
137 - (void)willSelect { | |
138 have_new_config_ = NO; | |
139 | |
140 NSDistributedNotificationCenter* center = | |
141 [NSDistributedNotificationCenter defaultCenter]; | |
142 [center addObserver:self | |
143 selector:@selector(onNewConfigFile:) | |
144 name:@kServiceName | |
145 object:nil]; | |
146 | |
147 service_status_timer_ = | |
148 [[NSTimer scheduledTimerWithTimeInterval:2.0 | |
Jamie
2012/05/02 22:30:24
How costly is the refresh operation? 2s is quite a
Lambros
2012/05/03 01:47:04
I don't think there's any disk access to worry abo
| |
149 target:self | |
150 selector:@selector(refreshServiceStatus:) | |
151 userInfo:nil | |
152 repeats:YES] retain]; | |
153 [self updateServiceStatus]; | |
154 [self updateAuthorizationStatus]; | |
155 [self readNewConfig]; | |
156 [self updateUI]; | |
157 } | |
158 | |
159 - (void)willUnselect { | |
160 NSDistributedNotificationCenter* center = | |
161 [NSDistributedNotificationCenter defaultCenter]; | |
162 [center removeObserver:self]; | |
163 | |
164 [service_status_timer_ invalidate]; | |
165 [service_status_timer_ release]; | |
166 service_status_timer_ = nil; | |
167 } | |
168 | |
169 - (void)onApply:(id)sender { | |
170 if (!have_new_config_) { | |
171 // It shouldn't be possible to hit the button if there is no config to | |
172 // apply, but check anyway just in case it happens somehow. | |
173 return; | |
174 } | |
175 | |
176 // Ensure the authorization token is up-to-date before using it. | |
177 [self updateAuthorizationStatus]; | |
178 [self updateUI]; | |
179 | |
180 std::string pin = base::SysNSStringToUTF8([pin_ stringValue]); | |
181 std::string host_id, host_secret_hash; | |
182 if (!config_->GetString(remoting::kHostIdConfigPath, &host_id) || | |
183 !config_->GetString(remoting::kHostSecretHashConfigPath, | |
184 &host_secret_hash)) { | |
185 LOG(ERROR) << "Failed to get config data for PIN verification"; | |
186 [self showError]; | |
187 return; | |
188 } | |
189 if (!IsPinValid(pin, host_id, host_secret_hash)) { | |
190 [self showIncorrectPinMessage]; | |
191 return; | |
192 } | |
193 | |
194 [self applyNewServiceConfig]; | |
195 [self updateUI]; | |
196 } | |
197 | |
198 - (void)onDisable:(id)sender { | |
199 // Ensure the authorization token is up-to-date before using it. | |
200 [self updateAuthorizationStatus]; | |
Jamie
2012/05/02 22:30:24
Can this fail? If so, shouldn't we return early?
Lambros
2012/05/03 01:47:04
Done.
| |
201 [self updateUI]; | |
202 | |
203 if (![self runHelperAsRootWithCommand:"--disable" | |
204 inputData:""]) { | |
205 LOG(ERROR) << "Failed to run the helper tool"; | |
206 [self showError]; | |
207 return; | |
208 } | |
209 | |
210 // Stop the launchd job. This cannot easily be done by the helper tool, | |
211 // since the launchd job runs in the current user's context. | |
212 base::mac::ScopedLaunchData response( | |
213 base::mac::MessageForJob(kServiceName, LAUNCH_KEY_STOPJOB)); | |
214 if (!response) { | |
215 LOG(ERROR) << "Failed to send message to launchd"; | |
216 [self showError]; | |
217 return; | |
218 } | |
219 | |
220 // Expect a response of type LAUNCH_DATA_ERRNO. | |
221 launch_data_type_t type = launch_data_get_type(response.get()); | |
222 if (type != LAUNCH_DATA_ERRNO) { | |
223 LOG(ERROR) << "launchd returned unexpected type: " << type; | |
224 [self showError]; | |
225 return; | |
226 } | |
227 | |
228 int error = launch_data_get_errno(response.get()); | |
229 if (error) { | |
230 LOG(ERROR) << "launchd returned error: " << error; | |
231 [self showError]; | |
232 } | |
233 } | |
234 | |
235 - (void)onNewConfigFile:(NSNotification*)notification { | |
236 [self readNewConfig]; | |
237 [self updateUI]; | |
238 } | |
239 | |
240 - (void)refreshServiceStatus:(NSTimer*)timer { | |
241 BOOL was_running = is_service_running_; | |
242 [self updateServiceStatus]; | |
243 if (was_running != is_service_running_) | |
244 [self updateUI]; | |
245 } | |
246 | |
247 - (void)authorizationViewDidAuthorize:(SFAuthorizationView*)view { | |
248 [self updateAuthorizationStatus]; | |
249 [self updateUI]; | |
250 } | |
251 | |
252 - (void)authorizationViewDidDeauthorize:(SFAuthorizationView*)view { | |
253 [self updateAuthorizationStatus]; | |
254 [self updateUI]; | |
255 } | |
256 | |
257 - (void)updateServiceStatus { | |
258 pid_t job_pid = base::mac::PIDForJob(kServiceName); | |
259 is_service_running_ = (job_pid > 0); | |
260 } | |
261 | |
262 - (void)updateAuthorizationStatus { | |
263 is_pane_unlocked_ = [authorization_view_ updateStatus:authorization_view_]; | |
264 } | |
265 | |
266 - (void)readNewConfig { | |
267 FilePath file; | |
268 if (!GetTemporaryConfigFilePath(&file)) { | |
269 LOG(ERROR) << "Failed to get path of configuration data."; | |
270 [self showError]; | |
271 return; | |
272 } | |
273 if (!file_util::PathExists(file)) { | |
274 return; | |
275 } | |
276 scoped_ptr<remoting::JsonHostConfig> new_config_( | |
277 new remoting::JsonHostConfig(file)); | |
278 if (new_config_->Read()) { | |
279 file_util::Delete(file, false); | |
280 config_.swap(new_config_); | |
281 have_new_config_ = YES; | |
282 } else { | |
283 // Report the error, because the file exists but couldn't be read. The | |
284 // case of non-existence is normal and expected. | |
285 LOG(ERROR) << "Error reading configuration data from " << file.value(); | |
286 [self showError]; | |
287 } | |
288 } | |
289 | |
290 - (void)updateUI { | |
291 if (is_service_running_) { | |
292 [status_message_ setStringValue:@"Me2Me is enabled"]; | |
Jamie
2012/05/02 22:30:24
Is this user-facing? If so then it should say Chro
Lambros
2012/05/03 01:47:04
Changed to Chrome Remote Desktop for now. Needs t
Jamie
2012/05/03 16:32:25
It should just be an ifdef. If it's more complex t
Lambros
2012/05/03 18:24:30
Done.
| |
293 } else { | |
294 [status_message_ setStringValue:@"Me2Me is disabled"]; | |
295 } | |
296 | |
297 std::string email; | |
298 if (config_.get()) { | |
299 if (!config_->GetString(remoting::kXmppLoginConfigPath, &email)) { | |
300 LOG(ERROR) << "Failed to get config item:" | |
301 << remoting::kXmppLoginConfigPath; | |
Jamie
2012/05/02 22:30:24
No user-visible error here? Also, you've already s
Lambros
2012/05/03 01:47:04
Done.
Jamie
2012/05/03 16:32:25
Perhaps I'm missing something, but I think both of
Lambros
2012/05/03 18:24:30
The new code moves the config validation out of up
| |
302 } | |
303 } | |
304 [email_ setStringValue:base::SysUTF8ToNSString(email)]; | |
305 | |
306 [disable_button_ setEnabled:(is_pane_unlocked_ && is_service_running_)]; | |
307 [pin_instruction_message_ setEnabled:have_new_config_]; | |
308 [email_ setEnabled:have_new_config_]; | |
309 [pin_ setEnabled:have_new_config_]; | |
310 [apply_button_ setEnabled:(is_pane_unlocked_ && have_new_config_)]; | |
311 } | |
312 | |
313 - (void)showError { | |
314 NSAlert* alert = [[NSAlert alloc] init]; | |
315 [alert setMessageText:@"An unexpected error occurred."]; | |
316 [alert setInformativeText:@"Check the system log for more information."]; | |
317 [alert setAlertStyle:NSWarningAlertStyle]; | |
318 [alert beginSheetModalForWindow:[[self mainView] window] | |
319 modalDelegate:nil | |
320 didEndSelector:nil | |
321 contextInfo:nil]; | |
322 [alert release]; | |
323 } | |
324 | |
325 - (void)showIncorrectPinMessage { | |
326 NSAlert* alert = [[NSAlert alloc] init]; | |
327 [alert setMessageText:@"Incorrect PIN entered."]; | |
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)applyNewServiceConfig { | |
337 [self updateServiceStatus]; | |
338 std::string serialized_config = config_->GetSerializedData(); | |
339 const char* command = is_service_running_ ? "--save-config" : "--enable"; | |
340 if (![self runHelperAsRootWithCommand:command | |
341 inputData:serialized_config]) { | |
342 LOG(ERROR) << "Failed to run the helper tool"; | |
343 [self showError]; | |
344 return; | |
345 } | |
346 have_new_config_ = NO; | |
347 | |
348 // If the service was previously running, send a signal to cause it to reload | |
349 // its configuration. | |
350 if (is_service_running_) { | |
351 pid_t job_pid = base::mac::PIDForJob(kServiceName); | |
352 if (job_pid > 0) { | |
353 kill(job_pid, SIGHUP); | |
354 } else { | |
355 LOG(ERROR) << "Failed to obtain PID of service " << kServiceName; | |
356 [self showError]; | |
357 } | |
358 } | |
359 } | |
360 | |
361 - (BOOL)runHelperAsRootWithCommand:(const char*)command | |
362 inputData:(const std::string&)input_data { | |
363 if (!file_util::VerifyPathControlledByAdmin(FilePath(kHelperTool))) { | |
364 LOG(ERROR) << "Security check failed for: " << kHelperTool; | |
365 return NO; | |
366 } | |
367 | |
368 AuthorizationRef authorization = | |
369 [[authorization_view_ authorization] authorizationRef]; | |
370 if (!authorization) { | |
371 LOG(ERROR) << "Failed to obtain authorizationRef"; | |
372 return NO; | |
Jamie
2012/05/02 22:30:24
Will this show "An unexpected error occurred" if t
Lambros
2012/05/03 01:47:04
It shouldn't do. If the user cancels the elevatio
| |
373 } | |
374 | |
375 const char* arguments[] = { command, NULL }; | |
376 FILE* pipe = NULL; | |
377 pid_t pid; | |
378 OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID( | |
379 authorization, | |
380 kHelperTool, | |
381 kAuthorizationFlagDefaults, | |
382 arguments, | |
383 &pipe, | |
384 &pid); | |
385 if (status != errAuthorizationSuccess) { | |
386 OSSTATUS_LOG(ERROR, status) << "AuthorizationExecuteWithPrivileges"; | |
387 return NO; | |
388 } | |
389 if (pid == -1) { | |
390 LOG(ERROR) << "Failed to get child PID"; | |
391 if (pipe) | |
392 fclose(pipe); | |
Jamie
2012/05/02 22:30:24
Can this be merged with the general error-detectio
Lambros
2012/05/03 01:47:04
I couldn't see an easy way to do it.
| |
393 return NO; | |
394 } | |
395 if (!pipe) { | |
396 LOG(ERROR) << "Unexpected NULL pipe"; | |
397 return NO; | |
398 } | |
399 | |
400 // Some cleanup is needed (closing the pipe and waiting for the child | |
401 // process), so flag any errors before returning. | |
402 BOOL error = NO; | |
403 | |
404 if (!input_data.empty()) { | |
405 size_t bytes_written = fwrite(input_data.data(), sizeof(char), | |
406 input_data.size(), pipe); | |
407 // According to the fwrite manpage, a partial count is returned only if a | |
408 // write error has occurred. | |
409 if (bytes_written != input_data.size()) { | |
410 LOG(ERROR) << "Failed to write data to child process"; | |
411 error = YES; | |
412 } | |
413 } | |
414 | |
415 // In all cases, fclose() should be called with the returned FILE*. In the | |
416 // case of sending data to the child, this needs to be done before calling | |
417 // waitpid(), since the child reads until EOF on its stdin, so calling | |
418 // waitpid() first would result in deadlock. | |
419 if (fclose(pipe) != 0) { | |
420 PLOG(ERROR) << "fclose"; | |
421 error = YES; | |
422 } | |
423 | |
424 int exit_status; | |
425 pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0)); | |
426 if (wait_result != pid) { | |
427 PLOG(ERROR) << "waitpid"; | |
428 error = YES; | |
429 } | |
430 | |
431 // No more cleanup needed. | |
432 if (error) { | |
433 return NO; | |
434 } | |
435 | |
436 if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) { | |
437 return YES; | |
438 } else { | |
439 LOG(ERROR) << kHelperTool << " failed with exit status " << exit_status; | |
440 return NO; | |
441 } | |
442 } | |
443 | |
444 @end | |
OLD | NEW |