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

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. 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 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
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