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