1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
|
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/plugin/daemon_controller.h"
#include <launch.h>
#include <sys/types.h>
#include "base/basictypes.h"
#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/eintr_wrapper.h"
#include "base/file_path.h"
#include "base/file_util.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/mac/authorization_util.h"
#include "base/mac/launchd.h"
#include "base/mac/mac_logging.h"
#include "base/mac/scoped_authorizationref.h"
#include "base/mac/scoped_launch_data.h"
#include "base/threading/thread.h"
#include "base/time.h"
#include "base/values.h"
#include "remoting/host/json_host_config.h"
namespace remoting {
namespace {
// The name of the Remoting Host service that is registered with launchd.
#define kServiceName "org.chromium.chromoting"
#define kConfigDir "/Library/PrivilegedHelperTools/"
// This helper script is executed as root. It is passed a command-line option
// (--enable or --disable), which causes it to create or remove a trigger file.
// The trigger file (defined in the service's plist file) informs launchd
// whether the Host service should be running. Creating the trigger file causes
// launchd to immediately start the service. Deleting the trigger file has no
// immediate effect, but it prevents the service from being restarted if it
// becomes stopped.
const char kStartStopTool[] = kConfigDir kServiceName ".me2me.sh";
// Use a single configuration file, instead of separate "auth" and "host" files.
// This is because the SetConfigAndStart() API only provides a single
// dictionary, and splitting this into two dictionaries would require
// knowledge of which keys belong in which files.
const char kHostConfigFile[] = kConfigDir kServiceName ".json";
const int kStopWaitRetryLimit = 20;
const int kStopWaitTimeout = 500;
class DaemonControllerMac : public remoting::DaemonController {
public:
DaemonControllerMac();
virtual ~DaemonControllerMac();
virtual State GetState() OVERRIDE;
virtual void GetConfig(const GetConfigCallback& callback) OVERRIDE;
virtual void SetConfigAndStart(
scoped_ptr<base::DictionaryValue> config,
const CompletionCallback& done_callback) OVERRIDE;
virtual void UpdateConfig(scoped_ptr<base::DictionaryValue> config,
const CompletionCallback& done_callback) OVERRIDE;
virtual void Stop(const CompletionCallback& done_callback) OVERRIDE;
private:
void DoGetConfig(const GetConfigCallback& callback);
void DoSetConfigAndStart(scoped_ptr<base::DictionaryValue> config,
const CompletionCallback& done_callback);
void DoUpdateConfig(scoped_ptr<base::DictionaryValue> config,
const CompletionCallback& done_callback);
void DoStop(const CompletionCallback& done_callback);
void NotifyWhenStopped(const CompletionCallback& done_callback,
int tries_remaining,
const base::TimeDelta& sleep);
bool RunToolScriptAsRoot(const char* command, const std::string& input_data);
bool StopService();
// The API for gaining root privileges is blocking (it prompts the user for
// a password). Since Start() and Stop() must not block the main thread, they
// need to post their tasks to a separate thread.
base::Thread auth_thread_;
DISALLOW_COPY_AND_ASSIGN(DaemonControllerMac);
};
DaemonControllerMac::DaemonControllerMac()
: auth_thread_("Auth thread") {
auth_thread_.Start();
}
DaemonControllerMac::~DaemonControllerMac() {
// This will block if the thread is waiting on a root password prompt. There
// doesn't seem to be an easy solution for this, other than to spawn a
// separate process to do the root elevation.
// TODO(lambroslambrou): Improve this, either by finding a way to terminate
// the thread, or by moving to a separate process.
auth_thread_.Stop();
}
DaemonController::State DaemonControllerMac::GetState() {
pid_t job_pid = base::mac::PIDForJob(kServiceName);
if (job_pid < 0) {
return DaemonController::STATE_NOT_INSTALLED;
} else if (job_pid == 0) {
// Service is stopped, or a start attempt failed.
return DaemonController::STATE_STOPPED;
} else {
return DaemonController::STATE_STARTED;
}
}
void DaemonControllerMac::GetConfig(const GetConfigCallback& callback) {
// base::Unretained() is safe, since this object owns the thread and therefore
// outlives it.
auth_thread_.message_loop_proxy()->PostTask(
FROM_HERE,
base::Bind(&DaemonControllerMac::DoGetConfig, base::Unretained(this),
callback));
}
void DaemonControllerMac::SetConfigAndStart(
scoped_ptr<base::DictionaryValue> config,
const CompletionCallback& done_callback) {
auth_thread_.message_loop_proxy()->PostTask(
FROM_HERE, base::Bind(
&DaemonControllerMac::DoSetConfigAndStart, base::Unretained(this),
base::Passed(&config), done_callback));
}
void DaemonControllerMac::UpdateConfig(
scoped_ptr<base::DictionaryValue> config,
const CompletionCallback& done_callback) {
auth_thread_.message_loop()->PostTask(FROM_HERE, base::Bind(
&DaemonControllerMac::DoUpdateConfig, base::Unretained(this),
base::Passed(&config), done_callback));
}
void DaemonControllerMac::Stop(const CompletionCallback& done_callback) {
auth_thread_.message_loop_proxy()->PostTask(
FROM_HERE, base::Bind(
&DaemonControllerMac::DoStop, base::Unretained(this), done_callback));
}
void DaemonControllerMac::DoGetConfig(const GetConfigCallback& callback) {
FilePath config_path(kHostConfigFile);
JsonHostConfig host_config(config_path);
scoped_ptr<base::DictionaryValue> config;
if (host_config.Read()) {
config.reset(new base::DictionaryValue());
std::string value;
if (host_config.GetString(kHostIdConfigPath, &value))
config.get()->SetString(kHostIdConfigPath, value);
if (host_config.GetString(kXmppLoginConfigPath, &value))
config.get()->SetString(kXmppLoginConfigPath, value);
}
callback.Run(config.Pass());
}
void DaemonControllerMac::DoSetConfigAndStart(
scoped_ptr<base::DictionaryValue> config,
const CompletionCallback& done_callback) {
std::string file_content;
base::JSONWriter::Write(config.get(), &file_content);
// Creating the trigger file causes launchd to start the service, so the
// extra step performed in DoStop() is not necessary here.
bool result = RunToolScriptAsRoot("--enable", file_content);
done_callback.Run(result ? RESULT_OK : RESULT_FAILED);
}
void DaemonControllerMac::DoUpdateConfig(
scoped_ptr<base::DictionaryValue> config,
const CompletionCallback& done_callback) {
FilePath config_file_path(kHostConfigFile);
JsonHostConfig config_file(config_file_path);
if (!config_file.Read()) {
done_callback.Run(RESULT_FAILED);
}
for (DictionaryValue::key_iterator key(config->begin_keys());
key != config->end_keys(); ++key) {
std::string value;
if (!config->GetString(*key, &value)) {
LOG(ERROR) << *key << " is not a string.";
done_callback.Run(RESULT_FAILED);
}
config_file.SetString(*key, value);
}
std::string file_content = config_file.GetSerializedData();
bool success = RunToolScriptAsRoot("--save-config", file_content);
done_callback.Run(success ? RESULT_OK : RESULT_FAILED);
pid_t job_pid = base::mac::PIDForJob(kServiceName);
if (job_pid > 0) {
kill(job_pid, SIGHUP);
}
}
void DaemonControllerMac::DoStop(const CompletionCallback& done_callback) {
if (!RunToolScriptAsRoot("--disable", "")) {
done_callback.Run(RESULT_FAILED);
return;
}
// Deleting the trigger file does not cause launchd to stop the service.
// Since the service is running for the local user's desktop (not as root),
// it has to be stopped for that user. This cannot easily be done in the
// shell-script running as root, so it is done here instead.
if (!StopService()) {
done_callback.Run(RESULT_FAILED);
return;
}
// StopService does not wait for the stop to take effect, so we can't return
// immediately. Instead, we wait up to 10s.
NotifyWhenStopped(done_callback,
kStopWaitRetryLimit,
base::TimeDelta::FromMilliseconds(kStopWaitTimeout));
}
void DaemonControllerMac::NotifyWhenStopped(
const CompletionCallback& done_callback,
int tries_remaining,
const base::TimeDelta& sleep) {
if (GetState() == DaemonController::STATE_STOPPED) {
done_callback.Run(RESULT_OK);
} else if (tries_remaining == 0) {
done_callback.Run(RESULT_FAILED);
} else {
auth_thread_.message_loop_proxy()->PostDelayedTask(
FROM_HERE,
base::Bind(&DaemonControllerMac::NotifyWhenStopped,
base::Unretained(this),
done_callback,
tries_remaining - 1,
sleep),
sleep);
}
}
bool DaemonControllerMac::RunToolScriptAsRoot(const char* command,
const std::string& input_data) {
// TODO(lambroslambrou): Supply a localized prompt string here.
base::mac::ScopedAuthorizationRef authorization(
base::mac::AuthorizationCreateToRunAsRoot(CFSTR("")));
if (!authorization) {
LOG(ERROR) << "Failed to get root privileges.";
return false;
}
if (!file_util::VerifyPathControlledByAdmin(FilePath(kStartStopTool))) {
LOG(ERROR) << "Security check failed for: " << kStartStopTool;
return false;
}
// TODO(lambroslambrou): Use sandbox-exec to minimize exposure -
// http://crbug.com/120903
const char* arguments[] = { command, NULL };
FILE* pipe = NULL;
pid_t pid;
OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID(
authorization.get(),
kStartStopTool,
kAuthorizationFlagDefaults,
arguments,
&pipe,
&pid);
if (status != errAuthorizationSuccess) {
OSSTATUS_LOG(ERROR, status) << "AuthorizationExecuteWithPrivileges";
return false;
}
if (pid == -1) {
LOG(ERROR) << "Failed to get child PID";
return false;
}
DCHECK(pipe);
if (!input_data.empty()) {
size_t bytes_written = fwrite(input_data.data(), sizeof(char),
input_data.size(), pipe);
// According to the fwrite manpage, a partial count is returned only if a
// write error has occurred.
if (bytes_written != input_data.size()) {
LOG(ERROR) << "Failed to write data to child process";
}
// Need to close, since the child waits for EOF on its stdin.
if (fclose(pipe) != 0) {
PLOG(ERROR) << "fclose";
}
}
int exit_status;
pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0));
if (wait_result != pid) {
PLOG(ERROR) << "waitpid";
return false;
}
if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) {
return true;
} else {
LOG(ERROR) << kStartStopTool << " failed with exit status " << exit_status;
return false;
}
}
bool DaemonControllerMac::StopService() {
base::mac::ScopedLaunchData response(
base::mac::MessageForJob(kServiceName, LAUNCH_KEY_STOPJOB));
if (!response) {
LOG(ERROR) << "Failed to send message to launchd";
return false;
}
// Got a response, so check if launchd sent a non-zero error code, otherwise
// assume the command was successful.
if (launch_data_get_type(response.get()) == LAUNCH_DATA_ERRNO) {
int error = launch_data_get_errno(response.get());
if (error) {
LOG(ERROR) << "launchd returned error " << error;
return false;
}
}
return true;
}
} // namespace
scoped_ptr<DaemonController> remoting::DaemonController::Create() {
return scoped_ptr<DaemonController>(new DaemonControllerMac());
}
} // namespace remoting
|