OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "remoting/host/plugin/daemon_controller.h" | 5 #include "remoting/host/plugin/daemon_controller.h" |
6 | 6 |
7 #include <launch.h> | 7 #include <launch.h> |
8 #include <sys/types.h> | 8 #include <sys/types.h> |
9 | 9 |
10 #include "base/basictypes.h" | 10 #include "base/basictypes.h" |
11 #include "base/bind.h" | 11 #include "base/bind.h" |
12 #include "base/compiler_specific.h" | 12 #include "base/compiler_specific.h" |
13 #include "base/eintr_wrapper.h" | |
14 #include "base/file_path.h" | 13 #include "base/file_path.h" |
15 #include "base/file_util.h" | 14 #include "base/file_util.h" |
16 #include "base/json/json_writer.h" | 15 #include "base/json/json_writer.h" |
17 #include "base/logging.h" | 16 #include "base/logging.h" |
18 #include "base/mac/authorization_util.h" | 17 #include "base/mac/foundation_util.h" |
19 #include "base/mac/launchd.h" | 18 #include "base/mac/launchd.h" |
20 #include "base/mac/mac_logging.h" | 19 #include "base/mac/mac_logging.h" |
21 #include "base/mac/scoped_authorizationref.h" | 20 #include "base/mac/mac_util.h" |
22 #include "base/mac/scoped_launch_data.h" | 21 #include "base/mac/scoped_launch_data.h" |
23 #include "base/threading/thread.h" | 22 #include "base/threading/thread.h" |
24 #include "base/time.h" | 23 #include "base/time.h" |
25 #include "base/values.h" | 24 #include "base/values.h" |
26 #include "remoting/host/json_host_config.h" | 25 #include "remoting/host/json_host_config.h" |
27 | 26 |
28 namespace remoting { | 27 namespace remoting { |
29 | 28 |
30 namespace { | 29 namespace { |
31 | 30 |
| 31 // The NSSystemDirectories.h header has a conflicting definition of |
| 32 // NSSearchPathDirectory with the one in base/mac/foundation_util.h. |
| 33 // Foundation.h would work, but it can only be included from Objective-C files. |
| 34 // Therefore, we define the needed constants here. |
| 35 const int NSLibraryDirectory = 5; |
| 36 |
32 // The name of the Remoting Host service that is registered with launchd. | 37 // The name of the Remoting Host service that is registered with launchd. |
33 #define kServiceName "org.chromium.chromoting" | 38 #define kServiceName "org.chromium.chromoting" |
34 #define kConfigDir "/Library/PrivilegedHelperTools/" | 39 #define kConfigDir "/Library/PrivilegedHelperTools/" |
35 | 40 |
36 // This helper script is executed as root. It is passed a command-line option | 41 // This helper script is executed as root. It is passed a command-line option |
37 // (--enable or --disable), which causes it to create or remove a file that | 42 // (--enable or --disable), which causes it to create or remove a file that |
38 // informs the host's launch script of whether the host is enabled or disabled. | 43 // informs the host's launch script of whether the host is enabled or disabled. |
39 const char kStartStopTool[] = kConfigDir kServiceName ".me2me.sh"; | 44 const char kStartStopTool[] = kConfigDir kServiceName ".me2me.sh"; |
40 | 45 |
41 // Use a single configuration file, instead of separate "auth" and "host" files. | 46 // Use a single configuration file, instead of separate "auth" and "host" files. |
(...skipping 23 matching lines...) Expand all Loading... |
65 private: | 70 private: |
66 void DoGetConfig(const GetConfigCallback& callback); | 71 void DoGetConfig(const GetConfigCallback& callback); |
67 void DoSetConfigAndStart(scoped_ptr<base::DictionaryValue> config, | 72 void DoSetConfigAndStart(scoped_ptr<base::DictionaryValue> config, |
68 const CompletionCallback& done_callback); | 73 const CompletionCallback& done_callback); |
69 void DoUpdateConfig(scoped_ptr<base::DictionaryValue> config, | 74 void DoUpdateConfig(scoped_ptr<base::DictionaryValue> config, |
70 const CompletionCallback& done_callback); | 75 const CompletionCallback& done_callback); |
71 void DoStop(const CompletionCallback& done_callback); | 76 void DoStop(const CompletionCallback& done_callback); |
72 void NotifyWhenStopped(const CompletionCallback& done_callback, | 77 void NotifyWhenStopped(const CompletionCallback& done_callback, |
73 int tries_remaining, | 78 int tries_remaining, |
74 const base::TimeDelta& sleep); | 79 const base::TimeDelta& sleep); |
| 80 bool ShowPreferencePane(const std::string& config_data); |
75 | 81 |
76 bool RunToolScriptAsRoot(const char* command, const std::string& input_data); | |
77 bool SendJobControlMessage(const char* launchKey); | |
78 | |
79 // The API for gaining root privileges is blocking (it prompts the user for | |
80 // a password). Since Start() and Stop() must not block the main thread, they | |
81 // need to post their tasks to a separate thread. | |
82 base::Thread auth_thread_; | 82 base::Thread auth_thread_; |
83 | 83 |
84 DISALLOW_COPY_AND_ASSIGN(DaemonControllerMac); | 84 DISALLOW_COPY_AND_ASSIGN(DaemonControllerMac); |
85 }; | 85 }; |
86 | 86 |
87 DaemonControllerMac::DaemonControllerMac() | 87 DaemonControllerMac::DaemonControllerMac() |
88 : auth_thread_("Auth thread") { | 88 : auth_thread_("Auth thread") { |
89 auth_thread_.Start(); | 89 auth_thread_.Start(); |
90 } | 90 } |
91 | 91 |
92 DaemonControllerMac::~DaemonControllerMac() { | 92 DaemonControllerMac::~DaemonControllerMac() { |
93 // This will block if the thread is waiting on a root password prompt. There | |
94 // doesn't seem to be an easy solution for this, other than to spawn a | |
95 // separate process to do the root elevation. | |
96 | |
97 // TODO(lambroslambrou): Improve this, either by finding a way to terminate | |
98 // the thread, or by moving to a separate process. | |
99 auth_thread_.Stop(); | 93 auth_thread_.Stop(); |
100 } | 94 } |
101 | 95 |
102 DaemonController::State DaemonControllerMac::GetState() { | 96 DaemonController::State DaemonControllerMac::GetState() { |
103 pid_t job_pid = base::mac::PIDForJob(kServiceName); | 97 pid_t job_pid = base::mac::PIDForJob(kServiceName); |
104 if (job_pid < 0) { | 98 if (job_pid < 0) { |
105 return DaemonController::STATE_NOT_INSTALLED; | 99 return DaemonController::STATE_NOT_INSTALLED; |
106 } else if (job_pid == 0) { | 100 } else if (job_pid == 0) { |
107 // Service is stopped, or a start attempt failed. | 101 // Service is stopped, or a start attempt failed. |
108 return DaemonController::STATE_STOPPED; | 102 return DaemonController::STATE_STOPPED; |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
160 if (host_config.GetString(kXmppLoginConfigPath, &value)) | 154 if (host_config.GetString(kXmppLoginConfigPath, &value)) |
161 config.get()->SetString(kXmppLoginConfigPath, value); | 155 config.get()->SetString(kXmppLoginConfigPath, value); |
162 } | 156 } |
163 | 157 |
164 callback.Run(config.Pass()); | 158 callback.Run(config.Pass()); |
165 } | 159 } |
166 | 160 |
167 void DaemonControllerMac::DoSetConfigAndStart( | 161 void DaemonControllerMac::DoSetConfigAndStart( |
168 scoped_ptr<base::DictionaryValue> config, | 162 scoped_ptr<base::DictionaryValue> config, |
169 const CompletionCallback& done_callback) { | 163 const CompletionCallback& done_callback) { |
170 std::string file_content; | 164 std::string config_data; |
171 base::JSONWriter::Write(config.get(), &file_content); | 165 base::JSONWriter::Write(config.get(), &config_data); |
172 if (!RunToolScriptAsRoot("--enable", file_content)) { | 166 |
173 done_callback.Run(RESULT_FAILED); | 167 bool result = ShowPreferencePane(config_data); |
174 return; | 168 |
175 } | |
176 bool result = SendJobControlMessage(LAUNCH_KEY_STARTJOB); | |
177 done_callback.Run(result ? RESULT_OK : RESULT_FAILED); | 169 done_callback.Run(result ? RESULT_OK : RESULT_FAILED); |
178 } | 170 } |
179 | 171 |
180 void DaemonControllerMac::DoUpdateConfig( | 172 void DaemonControllerMac::DoUpdateConfig( |
181 scoped_ptr<base::DictionaryValue> config, | 173 scoped_ptr<base::DictionaryValue> config, |
182 const CompletionCallback& done_callback) { | 174 const CompletionCallback& done_callback) { |
183 FilePath config_file_path(kHostConfigFile); | 175 FilePath config_file_path(kHostConfigFile); |
184 JsonHostConfig config_file(config_file_path); | 176 JsonHostConfig config_file(config_file_path); |
185 if (!config_file.Read()) { | 177 if (!config_file.Read()) { |
186 done_callback.Run(RESULT_FAILED); | 178 done_callback.Run(RESULT_FAILED); |
187 return; | 179 return; |
188 } | 180 } |
189 for (DictionaryValue::key_iterator key(config->begin_keys()); | 181 for (DictionaryValue::key_iterator key(config->begin_keys()); |
190 key != config->end_keys(); ++key) { | 182 key != config->end_keys(); ++key) { |
191 std::string value; | 183 std::string value; |
192 if (!config->GetString(*key, &value)) { | 184 if (!config->GetString(*key, &value)) { |
193 LOG(ERROR) << *key << " is not a string."; | 185 LOG(ERROR) << *key << " is not a string."; |
194 done_callback.Run(RESULT_FAILED); | 186 done_callback.Run(RESULT_FAILED); |
195 return; | 187 return; |
196 } | 188 } |
197 config_file.SetString(*key, value); | 189 config_file.SetString(*key, value); |
198 } | 190 } |
199 | 191 |
200 std::string file_content = config_file.GetSerializedData(); | 192 std::string config_data = config_file.GetSerializedData(); |
201 if (!RunToolScriptAsRoot("--save-config", file_content)) { | 193 bool success = ShowPreferencePane(config_data); |
202 done_callback.Run(RESULT_FAILED); | 194 |
203 return; | 195 done_callback.Run(success ? RESULT_OK : RESULT_FAILED); |
| 196 } |
| 197 |
| 198 bool DaemonControllerMac::ShowPreferencePane(const std::string& config_data) { |
| 199 if (!config_data.empty()) { |
| 200 FilePath config_path; |
| 201 if (!file_util::GetTempDir(&config_path)) { |
| 202 LOG(ERROR) << "Failed to get filename for saving configuration data."; |
| 203 return false; |
| 204 } |
| 205 config_path = config_path.Append(kServiceName ".json"); |
| 206 |
| 207 int written = file_util::WriteFile(config_path, config_data.data(), |
| 208 config_data.size()); |
| 209 if (written != static_cast<int>(config_data.size())) { |
| 210 LOG(ERROR) << "Failed to save configuration data to: " |
| 211 << config_path.value(); |
| 212 return false; |
| 213 } |
204 } | 214 } |
205 | 215 |
206 done_callback.Run(RESULT_OK); | 216 FilePath pane_path; |
207 pid_t job_pid = base::mac::PIDForJob(kServiceName); | 217 // TODO(lambroslambrou): Use NSPreferencePanesDirectory once we start |
208 if (job_pid > 0) { | 218 // building against SDK 10.6. |
209 kill(job_pid, SIGHUP); | 219 if (!base::mac::GetLocalDirectory(NSLibraryDirectory, &pane_path)) { |
| 220 LOG(ERROR) << "Failed to get directory for local preference panes."; |
| 221 return false; |
210 } | 222 } |
| 223 pane_path = pane_path.Append("PreferencePanes") |
| 224 .Append(kServiceName ".prefPane"); |
| 225 |
| 226 FSRef pane_path_ref; |
| 227 if (!base::mac::FSRefFromPath(pane_path.value(), &pane_path_ref)) { |
| 228 LOG(ERROR) << "Failed to create FSRef"; |
| 229 return false; |
| 230 } |
| 231 OSStatus status = LSOpenFSRef(&pane_path_ref, NULL); |
| 232 if (status != noErr) { |
| 233 OSSTATUS_LOG(ERROR, status) << "LSOpenFSRef failed for path: " |
| 234 << pane_path.value(); |
| 235 return false; |
| 236 } |
| 237 |
| 238 CFNotificationCenterRef center = |
| 239 CFNotificationCenterGetDistributedCenter(); |
| 240 CFNotificationCenterPostNotification(center, CFSTR(kServiceName), NULL, NULL, |
| 241 TRUE); |
| 242 return true; |
211 } | 243 } |
212 | 244 |
213 void DaemonControllerMac::DoStop(const CompletionCallback& done_callback) { | 245 void DaemonControllerMac::DoStop(const CompletionCallback& done_callback) { |
214 if (!RunToolScriptAsRoot("--disable", "")) { | 246 if (!ShowPreferencePane("")) { |
215 done_callback.Run(RESULT_FAILED); | 247 done_callback.Run(RESULT_FAILED); |
216 return; | 248 return; |
217 } | 249 } |
218 | 250 |
219 // Deleting the trigger file does not cause launchd to stop the service. | |
220 // Since the service is running for the local user's desktop (not as root), | |
221 // it has to be stopped for that user. This cannot easily be done in the | |
222 // shell-script running as root, so it is done here instead. | |
223 if (!SendJobControlMessage(LAUNCH_KEY_STOPJOB)) { | |
224 done_callback.Run(RESULT_FAILED); | |
225 return; | |
226 } | |
227 | |
228 // SendJobControlMessage does not wait for the stop to take effect, so we | |
229 // can't return immediately. Instead, we wait up to 10s. | |
230 NotifyWhenStopped(done_callback, | 251 NotifyWhenStopped(done_callback, |
231 kStopWaitRetryLimit, | 252 kStopWaitRetryLimit, |
232 base::TimeDelta::FromMilliseconds(kStopWaitTimeout)); | 253 base::TimeDelta::FromMilliseconds(kStopWaitTimeout)); |
233 } | 254 } |
234 | 255 |
235 void DaemonControllerMac::NotifyWhenStopped( | 256 void DaemonControllerMac::NotifyWhenStopped( |
236 const CompletionCallback& done_callback, | 257 const CompletionCallback& done_callback, |
237 int tries_remaining, | 258 int tries_remaining, |
238 const base::TimeDelta& sleep) { | 259 const base::TimeDelta& sleep) { |
239 if (GetState() == DaemonController::STATE_STOPPED) { | 260 if (GetState() == DaemonController::STATE_STOPPED) { |
240 done_callback.Run(RESULT_OK); | 261 done_callback.Run(RESULT_OK); |
241 } else if (tries_remaining == 0) { | 262 } else if (tries_remaining == 0) { |
242 done_callback.Run(RESULT_FAILED); | 263 done_callback.Run(RESULT_FAILED); |
243 } else { | 264 } else { |
244 auth_thread_.message_loop_proxy()->PostDelayedTask( | 265 auth_thread_.message_loop_proxy()->PostDelayedTask( |
245 FROM_HERE, | 266 FROM_HERE, |
246 base::Bind(&DaemonControllerMac::NotifyWhenStopped, | 267 base::Bind(&DaemonControllerMac::NotifyWhenStopped, |
247 base::Unretained(this), | 268 base::Unretained(this), |
248 done_callback, | 269 done_callback, |
249 tries_remaining - 1, | 270 tries_remaining - 1, |
250 sleep), | 271 sleep), |
251 sleep); | 272 sleep); |
252 } | 273 } |
253 } | 274 } |
254 | 275 |
255 bool DaemonControllerMac::RunToolScriptAsRoot(const char* command, | |
256 const std::string& input_data) { | |
257 // TODO(lambroslambrou): Supply a localized prompt string here. | |
258 base::mac::ScopedAuthorizationRef authorization( | |
259 base::mac::AuthorizationCreateToRunAsRoot(CFSTR(""))); | |
260 if (!authorization) { | |
261 LOG(ERROR) << "Failed to get root privileges."; | |
262 return false; | |
263 } | |
264 | |
265 if (!file_util::VerifyPathControlledByAdmin(FilePath(kStartStopTool))) { | |
266 LOG(ERROR) << "Security check failed for: " << kStartStopTool; | |
267 return false; | |
268 } | |
269 | |
270 // TODO(lambroslambrou): Use sandbox-exec to minimize exposure - | |
271 // http://crbug.com/120903 | |
272 const char* arguments[] = { command, NULL }; | |
273 FILE* pipe = NULL; | |
274 pid_t pid; | |
275 OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID( | |
276 authorization.get(), | |
277 kStartStopTool, | |
278 kAuthorizationFlagDefaults, | |
279 arguments, | |
280 &pipe, | |
281 &pid); | |
282 if (status != errAuthorizationSuccess) { | |
283 OSSTATUS_LOG(ERROR, status) << "AuthorizationExecuteWithPrivileges"; | |
284 return false; | |
285 } | |
286 if (pid == -1) { | |
287 LOG(ERROR) << "Failed to get child PID"; | |
288 return false; | |
289 } | |
290 | |
291 DCHECK(pipe); | |
292 if (!input_data.empty()) { | |
293 size_t bytes_written = fwrite(input_data.data(), sizeof(char), | |
294 input_data.size(), pipe); | |
295 // According to the fwrite manpage, a partial count is returned only if a | |
296 // write error has occurred. | |
297 if (bytes_written != input_data.size()) { | |
298 LOG(ERROR) << "Failed to write data to child process"; | |
299 } | |
300 // Need to close, since the child waits for EOF on its stdin. | |
301 if (fclose(pipe) != 0) { | |
302 PLOG(ERROR) << "fclose"; | |
303 } | |
304 } | |
305 | |
306 int exit_status; | |
307 pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0)); | |
308 if (wait_result != pid) { | |
309 PLOG(ERROR) << "waitpid"; | |
310 return false; | |
311 } | |
312 if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) { | |
313 return true; | |
314 } else { | |
315 LOG(ERROR) << kStartStopTool << " failed with exit status " << exit_status; | |
316 return false; | |
317 } | |
318 } | |
319 | |
320 bool DaemonControllerMac::SendJobControlMessage(const char* launchKey) { | |
321 base::mac::ScopedLaunchData response( | |
322 base::mac::MessageForJob(kServiceName, launchKey)); | |
323 if (!response) { | |
324 LOG(ERROR) << "Failed to send message to launchd"; | |
325 return false; | |
326 } | |
327 | |
328 // Got a response, so check if launchd sent a non-zero error code, otherwise | |
329 // assume the command was successful. | |
330 if (launch_data_get_type(response.get()) == LAUNCH_DATA_ERRNO) { | |
331 int error = launch_data_get_errno(response.get()); | |
332 if (error) { | |
333 LOG(ERROR) << "launchd returned error " << error; | |
334 return false; | |
335 } | |
336 } | |
337 return true; | |
338 } | |
339 | |
340 } // namespace | 276 } // namespace |
341 | 277 |
342 scoped_ptr<DaemonController> remoting::DaemonController::Create() { | 278 scoped_ptr<DaemonController> remoting::DaemonController::Create() { |
343 return scoped_ptr<DaemonController>(new DaemonControllerMac()); | 279 return scoped_ptr<DaemonController>(new DaemonControllerMac()); |
344 } | 280 } |
345 | 281 |
346 } // namespace remoting | 282 } // namespace remoting |
OLD | NEW |