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