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/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/thread_task_runner_handle.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
56base::FilePath GetConfigPath() {
57  std::string filename = "host#" + GetMd5(net::GetHostName()) + ".json";
58  return base::GetHomeDir().
59      Append(".config/chrome-remote-desktop").Append(filename);
60}
61
62bool GetScriptPath(base::FilePath* result) {
63  base::FilePath candidate_exe(kDaemonScript);
64  if (access(candidate_exe.value().c_str(), X_OK) == 0) {
65    *result = candidate_exe;
66    return true;
67  }
68  return false;
69}
70
71bool RunHostScriptWithTimeout(
72    const std::vector<std::string>& args,
73    base::TimeDelta timeout,
74    int* exit_code) {
75  DCHECK(exit_code);
76
77  // As long as we're relying on running an external binary from the
78  // PATH, don't do it as root.
79  if (getuid() == 0) {
80    LOG(ERROR) << "Refusing to run script as root.";
81    return false;
82  }
83  base::FilePath script_path;
84  if (!GetScriptPath(&script_path)) {
85    LOG(ERROR) << "GetScriptPath() failed.";
86    return false;
87  }
88  CommandLine command_line(script_path);
89  for (unsigned int i = 0; i < args.size(); ++i) {
90    command_line.AppendArg(args[i]);
91  }
92  base::ProcessHandle process_handle;
93
94  // Redirect the child's stdout to the parent's stderr. In the case where this
95  // parent process is a Native Messaging host, its stdout is used to send
96  // messages to the web-app.
97  base::FileHandleMappingVector fds_to_remap;
98  fds_to_remap.push_back(std::pair<int, int>(STDERR_FILENO, STDOUT_FILENO));
99  base::LaunchOptions options;
100  options.fds_to_remap = &fds_to_remap;
101  if (!base::LaunchProcess(command_line, options, &process_handle)) {
102    LOG(ERROR) << "Failed to run command: "
103               << command_line.GetCommandLineString();
104    return false;
105  }
106
107  if (!base::WaitForExitCodeWithTimeout(process_handle, exit_code, timeout)) {
108    base::KillProcess(process_handle, 0, false);
109    LOG(ERROR) << "Timeout exceeded for command: "
110               << command_line.GetCommandLineString();
111    return false;
112  }
113
114  return true;
115}
116
117bool RunHostScript(const std::vector<std::string>& args,
118                          int* exit_code) {
119  return RunHostScriptWithTimeout(
120      args, base::TimeDelta::FromMilliseconds(kDaemonTimeoutMs), exit_code);
121}
122
123}  // namespace
124
125DaemonControllerDelegateLinux::DaemonControllerDelegateLinux() {
126}
127
128DaemonControllerDelegateLinux::~DaemonControllerDelegateLinux() {
129}
130
131DaemonController::State DaemonControllerDelegateLinux::GetState() {
132  std::vector<std::string> args;
133  args.push_back("--check-running");
134  int exit_code = 0;
135  if (!RunHostScript(args, &exit_code)) {
136    // TODO(jamiewalch): When we have a good story for installing, return
137    // NOT_INSTALLED rather than NOT_IMPLEMENTED (the former suppresses
138    // the relevant UI in the web-app).
139    return DaemonController::STATE_NOT_IMPLEMENTED;
140  }
141
142  if (exit_code == 0) {
143    return DaemonController::STATE_STARTED;
144  } else {
145    return DaemonController::STATE_STOPPED;
146  }
147}
148
149scoped_ptr<base::DictionaryValue> DaemonControllerDelegateLinux::GetConfig() {
150  scoped_ptr<base::DictionaryValue> result(new base::DictionaryValue());
151
152  if (GetState() != DaemonController::STATE_NOT_IMPLEMENTED) {
153    JsonHostConfig config(GetConfigPath());
154    if (config.Read()) {
155      std::string value;
156      if (config.GetString(kHostIdConfigPath, &value)) {
157        result->SetString(kHostIdConfigPath, value);
158      }
159      if (config.GetString(kXmppLoginConfigPath, &value)) {
160        result->SetString(kXmppLoginConfigPath, value);
161      }
162    } else {
163      result.reset(); // Return NULL in case of error.
164    }
165  }
166
167  return result.Pass();
168}
169
170void DaemonControllerDelegateLinux::SetConfigAndStart(
171    scoped_ptr<base::DictionaryValue> config,
172    bool consent,
173    const DaemonController::CompletionCallback& done) {
174  // Add the user to chrome-remote-desktop group first.
175  std::vector<std::string> args;
176  args.push_back("--add-user");
177  int exit_code;
178  if (!RunHostScriptWithTimeout(
179          args, base::TimeDelta::FromSeconds(kSudoTimeoutSeconds),
180          &exit_code) ||
181      exit_code != 0) {
182    LOG(ERROR) << "Failed to add user to chrome-remote-desktop group.";
183    done.Run(DaemonController::RESULT_FAILED);
184    return;
185  }
186
187  // Ensure the configuration directory exists.
188  base::FilePath config_dir = GetConfigPath().DirName();
189  if (!base::DirectoryExists(config_dir) &&
190      !base::CreateDirectory(config_dir)) {
191    LOG(ERROR) << "Failed to create config directory " << config_dir.value();
192    done.Run(DaemonController::RESULT_FAILED);
193    return;
194  }
195
196  // Write config.
197  JsonHostConfig config_file(GetConfigPath());
198  if (!config_file.CopyFrom(config.get()) ||
199      !config_file.Save()) {
200    LOG(ERROR) << "Failed to update config file.";
201    done.Run(DaemonController::RESULT_FAILED);
202    return;
203  }
204
205  // Finally start the host.
206  args.clear();
207  args.push_back("--start");
208  DaemonController::AsyncResult result = DaemonController::RESULT_FAILED;
209  if (RunHostScript(args, &exit_code) && (exit_code == 0))
210    result = DaemonController::RESULT_OK;
211
212  done.Run(result);
213}
214
215void DaemonControllerDelegateLinux::UpdateConfig(
216    scoped_ptr<base::DictionaryValue> config,
217    const DaemonController::CompletionCallback& done) {
218  JsonHostConfig config_file(GetConfigPath());
219  if (!config_file.Read() ||
220      !config_file.CopyFrom(config.get()) ||
221      !config_file.Save()) {
222    LOG(ERROR) << "Failed to update config file.";
223    done.Run(DaemonController::RESULT_FAILED);
224    return;
225  }
226
227  std::vector<std::string> args;
228  args.push_back("--reload");
229  int exit_code = 0;
230  DaemonController::AsyncResult result = DaemonController::RESULT_FAILED;
231  if (RunHostScript(args, &exit_code) && (exit_code == 0))
232    result = DaemonController::RESULT_OK;
233
234  done.Run(result);
235}
236
237void DaemonControllerDelegateLinux::Stop(
238    const DaemonController::CompletionCallback& done) {
239  std::vector<std::string> args;
240  args.push_back("--stop");
241  int exit_code = 0;
242  DaemonController::AsyncResult result = DaemonController::RESULT_FAILED;
243  if (RunHostScript(args, &exit_code) && (exit_code == 0))
244    result = DaemonController::RESULT_OK;
245
246  done.Run(result);
247}
248
249void DaemonControllerDelegateLinux::SetWindow(void* window_handle) {
250  // noop
251}
252
253std::string DaemonControllerDelegateLinux::GetVersion() {
254  base::FilePath script_path;
255  if (!GetScriptPath(&script_path)) {
256    return std::string();
257  }
258  CommandLine command_line(script_path);
259  command_line.AppendArg("--host-version");
260
261  std::string version;
262  int exit_code = 0;
263  int result =
264      base::GetAppOutputWithExitCode(command_line, &version, &exit_code);
265  if (!result || exit_code != 0) {
266    LOG(ERROR) << "Failed to run \"" << command_line.GetCommandLineString()
267               << "\". Exit code: " << exit_code;
268    return std::string();
269  }
270
271  TrimWhitespaceASCII(version, TRIM_ALL, &version);
272  if (!ContainsOnlyChars(version, "0123456789.")) {
273    LOG(ERROR) << "Received invalid host version number: " << version;
274    return std::string();
275  }
276
277  return version;
278}
279
280DaemonController::UsageStatsConsent
281DaemonControllerDelegateLinux::GetUsageStatsConsent() {
282  // Crash dump collection is not implemented on Linux yet.
283  // http://crbug.com/130678.
284  DaemonController::UsageStatsConsent consent;
285  consent.supported = false;
286  consent.allowed = false;
287  consent.set_by_policy = false;
288  return consent;
289}
290
291scoped_refptr<DaemonController> DaemonController::Create() {
292  scoped_ptr<DaemonController::Delegate> delegate(
293      new DaemonControllerDelegateLinux());
294  return new DaemonController(delegate.Pass());
295}
296
297}  // namespace remoting
298