blob: 7da5ab8f295e181708bbeda41aeeb92f61f858b6 [file] [log] [blame]
// 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/win/elevated_controller.h"
#include "base/file_util.h"
#include "base/file_version_info.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/path_service.h"
#include "base/process/memory.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "base/win/scoped_handle.h"
#include "remoting/host/branding.h"
#include "remoting/host/usage_stats_consent.h"
#include "remoting/host/verify_config_window_win.h"
#include "remoting/host/win/core_resource.h"
#include "remoting/host/win/security_descriptor.h"
namespace remoting {
namespace {
// The maximum size of the configuration file. "1MB ought to be enough" for any
// reasonable configuration we will ever need. 1MB is low enough to make
// the probability of out of memory situation fairly low. OOM is still possible
// and we will crash if it occurs.
const size_t kMaxConfigFileSize = 1024 * 1024;
// The host configuration file name.
const base::FilePath::CharType kConfigFileName[] = FILE_PATH_LITERAL("host.json");
// The unprivileged configuration file name.
const base::FilePath::CharType kUnprivilegedConfigFileName[] =
FILE_PATH_LITERAL("host_unprivileged.json");
// The extension for the temporary file.
const base::FilePath::CharType kTempFileExtension[] = FILE_PATH_LITERAL("json~");
// The host configuration file security descriptor that enables full access to
// Local System and built-in administrators only.
const char kConfigFileSecurityDescriptor[] =
"O:BAG:BAD:(A;;GA;;;SY)(A;;GA;;;BA)";
const char kUnprivilegedConfigFileSecurityDescriptor[] =
"O:BAG:BAD:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GR;;;AU)";
// Configuration keys.
const char kHostId[] = "host_id";
const char kXmppLogin[] = "xmpp_login";
const char kHostOwner[] = "host_owner";
const char kHostSecretHash[] = "host_secret_hash";
// The configuration keys that cannot be specified in UpdateConfig().
const char* const kReadonlyKeys[] = { kHostId, kHostOwner, kXmppLogin };
// The configuration keys whose values may be read by GetConfig().
const char* const kUnprivilegedConfigKeys[] = { kHostId, kXmppLogin };
// Determines if the client runs in the security context that allows performing
// administrative tasks (i.e. the user belongs to the adminstrators group and
// the client runs elevated).
bool IsClientAdmin() {
HRESULT hr = CoImpersonateClient();
if (FAILED(hr)) {
return false;
}
SID_IDENTIFIER_AUTHORITY nt_authority = SECURITY_NT_AUTHORITY;
PSID administrators_group = NULL;
BOOL result = AllocateAndInitializeSid(&nt_authority,
2,
SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&administrators_group);
if (result) {
if (!CheckTokenMembership(NULL, administrators_group, &result)) {
result = false;
}
FreeSid(administrators_group);
}
hr = CoRevertToSelf();
CHECK(SUCCEEDED(hr));
return !!result;
}
// Reads and parses the configuration file up to |kMaxConfigFileSize| in
// size.
HRESULT ReadConfig(const base::FilePath& filename,
scoped_ptr<base::DictionaryValue>* config_out) {
// Read raw data from the configuration file.
base::win::ScopedHandle file(
CreateFileW(filename.value().c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL));
if (!file.IsValid()) {
DWORD error = GetLastError();
PLOG(ERROR) << "Failed to open '" << filename.value() << "'";
return HRESULT_FROM_WIN32(error);
}
scoped_ptr<char[]> buffer(new char[kMaxConfigFileSize]);
DWORD size = kMaxConfigFileSize;
if (!::ReadFile(file, &buffer[0], size, &size, NULL)) {
DWORD error = GetLastError();
PLOG(ERROR) << "Failed to read '" << filename.value() << "'";
return HRESULT_FROM_WIN32(error);
}
// Parse the JSON configuration, expecting it to contain a dictionary.
std::string file_content(buffer.get(), size);
scoped_ptr<base::Value> value(
base::JSONReader::Read(file_content, base::JSON_ALLOW_TRAILING_COMMAS));
base::DictionaryValue* dictionary;
if (value.get() == NULL || !value->GetAsDictionary(&dictionary)) {
LOG(ERROR) << "Failed to read '" << filename.value() << "'.";
return E_FAIL;
}
value.release();
config_out->reset(dictionary);
return S_OK;
}
base::FilePath GetTempLocationFor(const base::FilePath& filename) {
return filename.ReplaceExtension(kTempFileExtension);
}
// Writes a config file to a temporary location.
HRESULT WriteConfigFileToTemp(const base::FilePath& filename,
const char* security_descriptor,
const char* content,
size_t length) {
// Create the security descriptor for the configuration file.
ScopedSd sd = ConvertSddlToSd(security_descriptor);
if (!sd) {
DWORD error = GetLastError();
PLOG(ERROR)
<< "Failed to create a security descriptor for the configuration file";
return HRESULT_FROM_WIN32(error);
}
SECURITY_ATTRIBUTES security_attributes = {0};
security_attributes.nLength = sizeof(security_attributes);
security_attributes.lpSecurityDescriptor = sd.get();
security_attributes.bInheritHandle = FALSE;
// Create a temporary file and write configuration to it.
base::FilePath tempname = GetTempLocationFor(filename);
base::win::ScopedHandle file(
CreateFileW(tempname.value().c_str(),
GENERIC_WRITE,
0,
&security_attributes,
CREATE_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL));
if (!file.IsValid()) {
DWORD error = GetLastError();
PLOG(ERROR) << "Failed to create '" << filename.value() << "'";
return HRESULT_FROM_WIN32(error);
}
DWORD written;
if (!WriteFile(file, content, static_cast<DWORD>(length), &written, NULL)) {
DWORD error = GetLastError();
PLOG(ERROR) << "Failed to write to '" << filename.value() << "'";
return HRESULT_FROM_WIN32(error);
}
return S_OK;
}
// Moves a config file from its temporary location to its permanent location.
HRESULT MoveConfigFileFromTemp(const base::FilePath& filename) {
// Now that the configuration is stored successfully replace the actual
// configuration file.
base::FilePath tempname = GetTempLocationFor(filename);
if (!MoveFileExW(tempname.value().c_str(),
filename.value().c_str(),
MOVEFILE_REPLACE_EXISTING)) {
DWORD error = GetLastError();
PLOG(ERROR) << "Failed to rename '" << tempname.value() << "' to '"
<< filename.value() << "'";
return HRESULT_FROM_WIN32(error);
}
return S_OK;
}
// Writes the configuration file up to |kMaxConfigFileSize| in size.
HRESULT WriteConfig(const char* content, size_t length, HWND owner_window) {
if (length > kMaxConfigFileSize) {
return E_FAIL;
}
// Extract the configuration data that the user will verify.
scoped_ptr<base::Value> config_value(base::JSONReader::Read(content));
if (!config_value.get()) {
return E_FAIL;
}
base::DictionaryValue* config_dict = NULL;
if (!config_value->GetAsDictionary(&config_dict)) {
return E_FAIL;
}
std::string email;
if (!config_dict->GetString(kHostOwner, &email)) {
if (!config_dict->GetString(kXmppLogin, &email)) {
return E_FAIL;
}
}
std::string host_id, host_secret_hash;
if (!config_dict->GetString(kHostId, &host_id) ||
!config_dict->GetString(kHostSecretHash, &host_secret_hash)) {
return E_FAIL;
}
// Ask the user to verify the configuration (unless the client is admin
// already).
if (!IsClientAdmin()) {
remoting::VerifyConfigWindowWin verify_win(email, host_id,
host_secret_hash);
DWORD error = verify_win.DoModal(owner_window);
if (error != ERROR_SUCCESS) {
return HRESULT_FROM_WIN32(error);
}
}
// Extract the unprivileged fields from the configuration.
base::DictionaryValue unprivileged_config_dict;
for (int i = 0; i < arraysize(kUnprivilegedConfigKeys); ++i) {
const char* key = kUnprivilegedConfigKeys[i];
base::string16 value;
if (config_dict->GetString(key, &value)) {
unprivileged_config_dict.SetString(key, value);
}
}
std::string unprivileged_config_str;
base::JSONWriter::Write(&unprivileged_config_dict, &unprivileged_config_str);
// Write the full configuration file to a temporary location.
base::FilePath full_config_file_path =
remoting::GetConfigDir().Append(kConfigFileName);
HRESULT hr = WriteConfigFileToTemp(full_config_file_path,
kConfigFileSecurityDescriptor,
content,
length);
if (FAILED(hr)) {
return hr;
}
// Write the unprivileged configuration file to a temporary location.
base::FilePath unprivileged_config_file_path =
remoting::GetConfigDir().Append(kUnprivilegedConfigFileName);
hr = WriteConfigFileToTemp(unprivileged_config_file_path,
kUnprivilegedConfigFileSecurityDescriptor,
unprivileged_config_str.data(),
unprivileged_config_str.size());
if (FAILED(hr)) {
return hr;
}
// Move the full configuration file to its permanent location.
hr = MoveConfigFileFromTemp(full_config_file_path);
if (FAILED(hr)) {
return hr;
}
// Move the unprivileged configuration file to its permanent location.
hr = MoveConfigFileFromTemp(unprivileged_config_file_path);
if (FAILED(hr)) {
return hr;
}
return S_OK;
}
} // namespace
ElevatedController::ElevatedController() : owner_window_(NULL) {
}
HRESULT ElevatedController::FinalConstruct() {
return S_OK;
}
void ElevatedController::FinalRelease() {
}
STDMETHODIMP ElevatedController::GetConfig(BSTR* config_out) {
base::FilePath config_dir = remoting::GetConfigDir();
// Read the unprivileged part of host configuration.
scoped_ptr<base::DictionaryValue> config;
HRESULT hr = ReadConfig(config_dir.Append(kUnprivilegedConfigFileName),
&config);
if (FAILED(hr)) {
return hr;
}
// Convert the config back to a string and return it to the caller.
std::string file_content;
base::JSONWriter::Write(config.get(), &file_content);
*config_out = ::SysAllocString(base::UTF8ToUTF16(file_content).c_str());
if (config_out == NULL) {
return E_OUTOFMEMORY;
}
return S_OK;
}
STDMETHODIMP ElevatedController::GetVersion(BSTR* version_out) {
// Report the product version number of the daemon controller binary as
// the host version.
HMODULE binary = base::GetModuleFromAddress(
reinterpret_cast<void*>(&ReadConfig));
scoped_ptr<FileVersionInfo> version_info(
FileVersionInfo::CreateFileVersionInfoForModule(binary));
base::string16 version;
if (version_info.get()) {
version = version_info->product_version();
}
*version_out = ::SysAllocString(version.c_str());
if (version_out == NULL) {
return E_OUTOFMEMORY;
}
return S_OK;
}
STDMETHODIMP ElevatedController::SetConfig(BSTR config) {
// Determine the config directory path and create it if necessary.
base::FilePath config_dir = remoting::GetConfigDir();
if (!base::CreateDirectory(config_dir)) {
return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
}
std::string file_content = base::UTF16ToUTF8(
base::string16(static_cast<base::char16*>(config), ::SysStringLen(config)));
return WriteConfig(file_content.c_str(), file_content.size(), owner_window_);
}
STDMETHODIMP ElevatedController::SetOwnerWindow(LONG_PTR window_handle) {
owner_window_ = reinterpret_cast<HWND>(window_handle);
return S_OK;
}
STDMETHODIMP ElevatedController::StartDaemon() {
ScopedScHandle service;
HRESULT hr = OpenService(&service);
if (FAILED(hr)) {
return hr;
}
// Change the service start type to 'auto'.
if (!::ChangeServiceConfigW(service,
SERVICE_NO_CHANGE,
SERVICE_AUTO_START,
SERVICE_NO_CHANGE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL)) {
DWORD error = GetLastError();
PLOG(ERROR) << "Failed to change the '" << kWindowsServiceName
<< "'service start type to 'auto'";
return HRESULT_FROM_WIN32(error);
}
// Start the service.
if (!StartService(service, 0, NULL)) {
DWORD error = GetLastError();
if (error != ERROR_SERVICE_ALREADY_RUNNING) {
PLOG(ERROR) << "Failed to start the '" << kWindowsServiceName
<< "'service";
return HRESULT_FROM_WIN32(error);
}
}
return S_OK;
}
STDMETHODIMP ElevatedController::StopDaemon() {
ScopedScHandle service;
HRESULT hr = OpenService(&service);
if (FAILED(hr)) {
return hr;
}
// Change the service start type to 'manual'.
if (!::ChangeServiceConfigW(service,
SERVICE_NO_CHANGE,
SERVICE_DEMAND_START,
SERVICE_NO_CHANGE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL)) {
DWORD error = GetLastError();
PLOG(ERROR) << "Failed to change the '" << kWindowsServiceName
<< "'service start type to 'manual'";
return HRESULT_FROM_WIN32(error);
}
// Stop the service.
SERVICE_STATUS status;
if (!ControlService(service, SERVICE_CONTROL_STOP, &status)) {
DWORD error = GetLastError();
if (error != ERROR_SERVICE_NOT_ACTIVE) {
PLOG(ERROR) << "Failed to stop the '" << kWindowsServiceName
<< "'service";
return HRESULT_FROM_WIN32(error);
}
}
return S_OK;
}
STDMETHODIMP ElevatedController::UpdateConfig(BSTR config) {
// Parse the config.
std::string config_str = base::UTF16ToUTF8(
base::string16(static_cast<base::char16*>(config), ::SysStringLen(config)));
scoped_ptr<base::Value> config_value(base::JSONReader::Read(config_str));
if (!config_value.get()) {
return E_FAIL;
}
base::DictionaryValue* config_dict = NULL;
if (!config_value->GetAsDictionary(&config_dict)) {
return E_FAIL;
}
// Check for bad keys.
for (int i = 0; i < arraysize(kReadonlyKeys); ++i) {
if (config_dict->HasKey(kReadonlyKeys[i])) {
return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
}
}
// Get the old config.
base::FilePath config_dir = remoting::GetConfigDir();
scoped_ptr<base::DictionaryValue> config_old;
HRESULT hr = ReadConfig(config_dir.Append(kConfigFileName), &config_old);
if (FAILED(hr)) {
return hr;
}
// Merge items from the given config into the old config.
config_old->MergeDictionary(config_dict);
// Write the updated config.
std::string config_updated_str;
base::JSONWriter::Write(config_old.get(), &config_updated_str);
return WriteConfig(config_updated_str.c_str(), config_updated_str.size(),
owner_window_);
}
STDMETHODIMP ElevatedController::GetUsageStatsConsent(BOOL* allowed,
BOOL* set_by_policy) {
bool local_allowed;
bool local_set_by_policy;
if (remoting::GetUsageStatsConsent(&local_allowed, &local_set_by_policy)) {
*allowed = local_allowed;
*set_by_policy = local_set_by_policy;
return S_OK;
} else {
return E_FAIL;
}
}
STDMETHODIMP ElevatedController::SetUsageStatsConsent(BOOL allowed) {
if (remoting::SetUsageStatsConsent(!!allowed)) {
return S_OK;
} else {
return E_FAIL;
}
}
HRESULT ElevatedController::OpenService(ScopedScHandle* service_out) {
DWORD error;
ScopedScHandle scmanager(
::OpenSCManagerW(NULL, SERVICES_ACTIVE_DATABASE,
SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE));
if (!scmanager.IsValid()) {
error = GetLastError();
PLOG(ERROR) << "Failed to connect to the service control manager";
return HRESULT_FROM_WIN32(error);
}
DWORD desired_access = SERVICE_CHANGE_CONFIG | SERVICE_QUERY_STATUS |
SERVICE_START | SERVICE_STOP;
ScopedScHandle service(
::OpenServiceW(scmanager, kWindowsServiceName, desired_access));
if (!service.IsValid()) {
error = GetLastError();
PLOG(ERROR) << "Failed to open to the '" << kWindowsServiceName
<< "' service";
return HRESULT_FROM_WIN32(error);
}
service_out->Set(service.Take());
return S_OK;
}
} // namespace remoting