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#include "remoting/host/setup/daemon_controller.h"
6
7#include <unistd.h>
8
9#include "base/basictypes.h"
10#include "base/bind.h"
11#include "base/command_line.h"
12#include "base/compiler_specific.h"
13#include "base/environment.h"
14#include "base/file_util.h"
15#include "base/files/file_path.h"
16#include "base/json/json_writer.h"
17#include "base/logging.h"
18#include "base/md5.h"
19#include "base/process/kill.h"
20#include "base/process/launch.h"
21#include "base/process/process_handle.h"
22#include "base/strings/string_number_conversions.h"
23#include "base/strings/string_split.h"
24#include "base/strings/string_util.h"
25#include "base/threading/thread.h"
26#include "base/values.h"
27#include "net/base/net_util.h"
28#include "remoting/host/host_config.h"
29#include "remoting/host/json_host_config.h"
30#include "remoting/host/usage_stats_consent.h"
31
32namespace remoting {
33
34namespace {
35
36const char kDaemonScript[] =
37    "/opt/google/chrome-remote-desktop/chrome-remote-desktop";
38
39// Timeout for running daemon script. The script itself sets a timeout when
40// waiting for the host to come online, so the setting here should be at least
41// as long.
42const int64 kDaemonTimeoutMs = 60000;
43
44// Timeout for commands that require password prompt - 5 minutes.
45const int64 kSudoTimeoutSeconds = 5 * 60;
46
47std::string GetMd5(const std::string& value) {
48  base::MD5Context ctx;
49  base::MD5Init(&ctx);
50  base::MD5Update(&ctx, value);
51  base::MD5Digest digest;
52  base::MD5Final(&digest, &ctx);
53  return StringToLowerASCII(base::HexEncode(digest.a, sizeof(digest.a)));
54}
55
56class DaemonControllerLinux : public remoting::DaemonController {
57 public:
58  DaemonControllerLinux();
59
60  virtual State GetState() OVERRIDE;
61  virtual void GetConfig(const GetConfigCallback& callback) OVERRIDE;
62  virtual void SetConfigAndStart(
63      scoped_ptr<base::DictionaryValue> config,
64      bool consent,
65      const CompletionCallback& done) OVERRIDE;
66  virtual void UpdateConfig(scoped_ptr<base::DictionaryValue> config,
67                            const CompletionCallback& done_callback) OVERRIDE;
68  virtual void Stop(const CompletionCallback& done_callback) OVERRIDE;
69  virtual void SetWindow(void* window_handle) OVERRIDE;
70  virtual void GetVersion(const GetVersionCallback& done_callback) OVERRIDE;
71  virtual void GetUsageStatsConsent(
72      const GetUsageStatsConsentCallback& done) OVERRIDE;
73
74 private:
75  base::FilePath GetConfigPath();
76
77  void DoGetConfig(const GetConfigCallback& callback);
78  void DoSetConfigAndStart(scoped_ptr<base::DictionaryValue> config,
79                           const CompletionCallback& done);
80  void DoUpdateConfig(scoped_ptr<base::DictionaryValue> config,
81                      const CompletionCallback& done_callback);
82  void DoStop(const CompletionCallback& done_callback);
83  void DoGetVersion(const GetVersionCallback& done_callback);
84
85  base::Thread file_io_thread_;
86
87  DISALLOW_COPY_AND_ASSIGN(DaemonControllerLinux);
88};
89
90DaemonControllerLinux::DaemonControllerLinux()
91    : file_io_thread_("DaemonControllerFileIO") {
92  file_io_thread_.Start();
93}
94
95static bool GetScriptPath(base::FilePath* result) {
96  base::FilePath candidate_exe(kDaemonScript);
97  if (access(candidate_exe.value().c_str(), X_OK) == 0) {
98    *result = candidate_exe;
99    return true;
100  }
101  return false;
102}
103
104static bool RunHostScriptWithTimeout(
105    const std::vector<std::string>& args,
106    base::TimeDelta timeout,
107    int* exit_code) {
108  DCHECK(exit_code);
109
110  // As long as we're relying on running an external binary from the
111  // PATH, don't do it as root.
112  if (getuid() == 0) {
113    return false;
114  }
115  base::FilePath script_path;
116  if (!GetScriptPath(&script_path)) {
117    return false;
118  }
119  CommandLine command_line(script_path);
120  for (unsigned int i = 0; i < args.size(); ++i) {
121    command_line.AppendArg(args[i]);
122  }
123  base::ProcessHandle process_handle;
124
125  // Redirect the child's stdout to the parent's stderr. In the case where this
126  // parent process is a Native Messaging host, its stdout is used to send
127  // messages to the web-app.
128  base::FileHandleMappingVector fds_to_remap;
129  fds_to_remap.push_back(std::pair<int, int>(STDERR_FILENO, STDOUT_FILENO));
130  base::LaunchOptions options;
131  options.fds_to_remap = &fds_to_remap;
132  if (!base::LaunchProcess(command_line, options, &process_handle)) {
133    return false;
134  }
135
136  if (!base::WaitForExitCodeWithTimeout(process_handle, exit_code, timeout)) {
137    base::KillProcess(process_handle, 0, false);
138    return false;
139  }
140
141  return true;
142}
143
144static bool RunHostScript(const std::vector<std::string>& args,
145                          int* exit_code) {
146  return RunHostScriptWithTimeout(
147      args, base::TimeDelta::FromMilliseconds(kDaemonTimeoutMs), exit_code);
148}
149
150remoting::DaemonController::State DaemonControllerLinux::GetState() {
151  std::vector<std::string> args;
152  args.push_back("--check-running");
153  int exit_code = 0;
154  if (!RunHostScript(args, &exit_code)) {
155    // TODO(jamiewalch): When we have a good story for installing, return
156    // NOT_INSTALLED rather than NOT_IMPLEMENTED (the former suppresses
157    // the relevant UI in the web-app).
158    return remoting::DaemonController::STATE_NOT_IMPLEMENTED;
159  }
160
161  if (exit_code == 0) {
162    return remoting::DaemonController::STATE_STARTED;
163  } else {
164    return remoting::DaemonController::STATE_STOPPED;
165  }
166}
167
168void DaemonControllerLinux::GetConfig(const GetConfigCallback& callback) {
169  // base::Unretained() is safe because we control lifetime of the thread.
170  file_io_thread_.message_loop()->PostTask(FROM_HERE, base::Bind(
171      &DaemonControllerLinux::DoGetConfig, base::Unretained(this), callback));
172}
173
174void DaemonControllerLinux::GetUsageStatsConsent(
175    const GetUsageStatsConsentCallback& done) {
176  // Crash dump collection is not implemented on Linux yet.
177  // http://crbug.com/130678.
178  done.Run(false, false, false);
179}
180
181void DaemonControllerLinux::SetConfigAndStart(
182    scoped_ptr<base::DictionaryValue> config,
183    bool /* consent */,
184    const CompletionCallback& done) {
185  // base::Unretained() is safe because we control lifetime of the thread.
186  file_io_thread_.message_loop()->PostTask(FROM_HERE, base::Bind(
187      &DaemonControllerLinux::DoSetConfigAndStart, base::Unretained(this),
188      base::Passed(&config), done));
189}
190
191void DaemonControllerLinux::UpdateConfig(
192    scoped_ptr<base::DictionaryValue> config,
193    const CompletionCallback& done_callback) {
194  file_io_thread_.message_loop()->PostTask(FROM_HERE, base::Bind(
195      &DaemonControllerLinux::DoUpdateConfig, base::Unretained(this),
196      base::Passed(&config), done_callback));
197}
198
199void DaemonControllerLinux::Stop(const CompletionCallback& done_callback) {
200  file_io_thread_.message_loop()->PostTask(FROM_HERE, base::Bind(
201      &DaemonControllerLinux::DoStop, base::Unretained(this),
202      done_callback));
203}
204
205void DaemonControllerLinux::SetWindow(void* window_handle) {
206  // noop
207}
208
209void DaemonControllerLinux::GetVersion(
210    const GetVersionCallback& done_callback) {
211  file_io_thread_.message_loop()->PostTask(FROM_HERE, base::Bind(
212      &DaemonControllerLinux::DoGetVersion, base::Unretained(this),
213      done_callback));
214}
215
216base::FilePath DaemonControllerLinux::GetConfigPath() {
217  std::string filename = "host#" + GetMd5(net::GetHostName()) + ".json";
218  return file_util::GetHomeDir().
219      Append(".config/chrome-remote-desktop").Append(filename);
220}
221
222void DaemonControllerLinux::DoGetConfig(const GetConfigCallback& callback) {
223  scoped_ptr<base::DictionaryValue> result(new base::DictionaryValue());
224
225  if (GetState() != remoting::DaemonController::STATE_NOT_IMPLEMENTED) {
226    JsonHostConfig config(GetConfigPath());
227    if (config.Read()) {
228      std::string value;
229      if (config.GetString(kHostIdConfigPath, &value)) {
230        result->SetString(kHostIdConfigPath, value);
231      }
232      if (config.GetString(kXmppLoginConfigPath, &value)) {
233        result->SetString(kXmppLoginConfigPath, value);
234      }
235    } else {
236      result.reset(); // Return NULL in case of error.
237    }
238  }
239
240  callback.Run(result.Pass());
241}
242
243void DaemonControllerLinux::DoSetConfigAndStart(
244    scoped_ptr<base::DictionaryValue> config,
245    const CompletionCallback& done_callback) {
246
247  // Add the user to chrome-remote-desktop group first.
248  std::vector<std::string> args;
249  args.push_back("--add-user");
250  int exit_code;
251  if (!RunHostScriptWithTimeout(
252          args, base::TimeDelta::FromSeconds(kSudoTimeoutSeconds),
253          &exit_code) ||
254      exit_code != 0) {
255    LOG(ERROR) << "Failed to add user to chrome-remote-desktop group.";
256    done_callback.Run(RESULT_FAILED);
257    return;
258  }
259
260  // Ensure the configuration directory exists.
261  base::FilePath config_dir = GetConfigPath().DirName();
262  if (!base::DirectoryExists(config_dir) &&
263      !file_util::CreateDirectory(config_dir)) {
264    LOG(ERROR) << "Failed to create config directory " << config_dir.value();
265    done_callback.Run(RESULT_FAILED);
266    return;
267  }
268
269  // Write config.
270  JsonHostConfig config_file(GetConfigPath());
271  if (!config_file.CopyFrom(config.get()) ||
272      !config_file.Save()) {
273    LOG(ERROR) << "Failed to update config file.";
274    done_callback.Run(RESULT_FAILED);
275    return;
276  }
277
278  // Finally start the host.
279  args.clear();
280  args.push_back("--start");
281  AsyncResult result;
282  if (RunHostScript(args, &exit_code)) {
283    result = (exit_code == 0) ? RESULT_OK : RESULT_FAILED;
284  } else {
285    result = RESULT_FAILED;
286  }
287  done_callback.Run(result);
288}
289
290void DaemonControllerLinux::DoUpdateConfig(
291    scoped_ptr<base::DictionaryValue> config,
292    const CompletionCallback& done_callback) {
293  JsonHostConfig config_file(GetConfigPath());
294  if (!config_file.Read() ||
295      !config_file.CopyFrom(config.get()) ||
296      !config_file.Save()) {
297    LOG(ERROR) << "Failed to update config file.";
298    done_callback.Run(RESULT_FAILED);
299    return;
300  }
301
302  std::vector<std::string> args;
303  args.push_back("--reload");
304  AsyncResult result;
305  int exit_code;
306  if (RunHostScript(args, &exit_code)) {
307    result = (exit_code == 0) ? RESULT_OK : RESULT_FAILED;
308  } else {
309    result = RESULT_FAILED;
310  }
311
312  done_callback.Run(result);
313}
314
315void DaemonControllerLinux::DoStop(const CompletionCallback& done_callback) {
316  std::vector<std::string> args;
317  args.push_back("--stop");
318  int exit_code = 0;
319  AsyncResult result;
320  if (RunHostScript(args, &exit_code)) {
321    result = (exit_code == 0) ? RESULT_OK : RESULT_FAILED;
322  } else {
323    result = RESULT_FAILED;
324  }
325  done_callback.Run(result);
326}
327
328void DaemonControllerLinux::DoGetVersion(
329    const GetVersionCallback& done_callback) {
330  base::FilePath script_path;
331  if (!GetScriptPath(&script_path)) {
332    done_callback.Run(std::string());
333    return;
334  }
335  CommandLine command_line(script_path);
336  command_line.AppendArg("--host-version");
337
338  std::string version;
339  int exit_code = 0;
340  int result =
341      base::GetAppOutputWithExitCode(command_line, &version, &exit_code);
342  if (!result || exit_code != 0) {
343    LOG(ERROR) << "Failed to run \"" << command_line.GetCommandLineString()
344               << "\". Exit code: " << exit_code;
345    done_callback.Run(std::string());
346    return;
347  }
348
349  TrimWhitespaceASCII(version, TRIM_ALL, &version);
350  if (!ContainsOnlyChars(version, "0123456789.")) {
351    LOG(ERROR) << "Received invalid host version number: " << version;
352    done_callback.Run(std::string());
353    return;
354  }
355
356  done_callback.Run(version);
357}
358
359}  // namespace
360
361scoped_ptr<DaemonController> remoting::DaemonController::Create() {
362  return scoped_ptr<DaemonController>(new DaemonControllerLinux());
363}
364
365}  // namespace remoting
366