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

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: 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";
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698