Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(917)

Side by Side Diff: remoting/host/me2me_preference_pane.mm

Issue 10171020: Preference Pane for chromoting on Mac (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Address comments and fix plist MainNibFile entry Created 8 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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
OLDNEW
« no previous file with comments | « remoting/host/installer/mac/ChromotingHostService.packproj ('k') | remoting/host/me2me_preference_pane.xib » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698