blob: 84c60c90f9befdd35b4af973b72fa60707c8458b [file] [log] [blame]
// Copyright 2013 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/it2me/it2me_host.h"
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_util.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/policy/policy_constants.h"
#include "net/url_request/url_request_context_getter.h"
#include "remoting/base/auto_thread.h"
#include "remoting/base/chromium_url_request.h"
#include "remoting/base/logging.h"
#include "remoting/base/rsa_key_pair.h"
#include "remoting/base/service_urls.h"
#include "remoting/host/chromoting_host.h"
#include "remoting/host/chromoting_host_context.h"
#include "remoting/host/host_event_logger.h"
#include "remoting/host/host_secret.h"
#include "remoting/host/host_status_logger.h"
#include "remoting/host/it2me/it2me_confirmation_dialog.h"
#include "remoting/host/it2me_desktop_environment.h"
#include "remoting/host/register_support_host_request.h"
#include "remoting/protocol/auth_util.h"
#include "remoting/protocol/chromium_port_allocator_factory.h"
#include "remoting/protocol/ice_transport.h"
#include "remoting/protocol/it2me_host_authenticator_factory.h"
#include "remoting/protocol/jingle_session_manager.h"
#include "remoting/protocol/network_settings.h"
#include "remoting/protocol/transport_context.h"
#include "remoting/protocol/validating_authenticator.h"
#include "remoting/signaling/jid_util.h"
#include "remoting/signaling/server_log_entry.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace remoting {
using protocol::ErrorCode;
namespace {
// This is used for tagging system event logs.
const char kApplicationName[] = "chromoting";
const int kMaxLoginAttempts = 5;
using protocol::ValidatingAuthenticator;
typedef ValidatingAuthenticator::Result ValidationResult;
typedef ValidatingAuthenticator::ValidationCallback ValidationCallback;
typedef ValidatingAuthenticator::ResultCallback ValidationResultCallback;
} // namespace
It2MeHost::It2MeHost() = default;
It2MeHost::~It2MeHost() {
// Check that resources that need to be torn down on the UI thread are gone.
DCHECK(!desktop_environment_factory_.get());
}
void It2MeHost::set_enable_dialogs(bool enable) {
#if defined(OS_CHROMEOS)
enable_dialogs_ = enable;
#else
NOTREACHED() << "It2MeHost::set_enable_dialogs is only supported on ChromeOS";
#endif
}
void It2MeHost::Connect(
std::unique_ptr<ChromotingHostContext> host_context,
std::unique_ptr<base::DictionaryValue> policies,
std::unique_ptr<It2MeConfirmationDialogFactory> dialog_factory,
base::WeakPtr<It2MeHost::Observer> observer,
std::unique_ptr<SignalStrategy> signal_strategy,
const std::string& username,
const std::string& directory_bot_jid,
const protocol::IceConfig& ice_config) {
DCHECK(host_context->ui_task_runner()->BelongsToCurrentThread());
host_context_ = std::move(host_context);
observer_ = std::move(observer);
confirmation_dialog_factory_ = std::move(dialog_factory);
signal_strategy_ = std::move(signal_strategy);
OnPolicyUpdate(std::move(policies));
desktop_environment_factory_.reset(new It2MeDesktopEnvironmentFactory(
host_context_->network_task_runner(),
host_context_->video_capture_task_runner(),
host_context_->input_task_runner(), host_context_->ui_task_runner(),
host_context_->system_input_injector_factory()));
// Switch to the network thread to start the actual connection.
host_context_->network_task_runner()->PostTask(
FROM_HERE, base::Bind(&It2MeHost::ConnectOnNetworkThread, this, username,
directory_bot_jid, ice_config));
}
void It2MeHost::Disconnect() {
DCHECK(host_context_->ui_task_runner()->BelongsToCurrentThread());
host_context_->network_task_runner()->PostTask(
FROM_HERE, base::Bind(&It2MeHost::DisconnectOnNetworkThread, this));
}
void It2MeHost::ConnectOnNetworkThread(const std::string& username,
const std::string& directory_bot_jid,
const protocol::IceConfig& ice_config) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
DCHECK_EQ(kDisconnected, state_);
SetState(kStarting, ErrorCode::OK);
// Check the host domain policy.
if (!required_host_domain_list_.empty()) {
bool matched = false;
for (const auto& domain : required_host_domain_list_) {
if (base::EndsWith(username, std::string("@") + domain,
base::CompareCase::INSENSITIVE_ASCII)) {
matched = true;
break;
}
}
if (!matched) {
SetState(kInvalidDomainError, ErrorCode::OK);
return;
}
}
// Generate a key pair for the Host to use.
// TODO(wez): Move this to the worker thread.
host_key_pair_ = RsaKeyPair::Generate();
// Request registration of the host for support.
std::unique_ptr<RegisterSupportHostRequest> register_request(
new RegisterSupportHostRequest(
signal_strategy_.get(), host_key_pair_, directory_bot_jid,
base::Bind(&It2MeHost::OnReceivedSupportID, base::Unretained(this))));
// Beyond this point nothing can fail, so save the config and request.
register_request_ = std::move(register_request);
HOST_LOG << "NAT state: " << nat_traversal_enabled_;
protocol::NetworkSettings network_settings(
nat_traversal_enabled_ ?
protocol::NetworkSettings::NAT_TRAVERSAL_FULL :
protocol::NetworkSettings::NAT_TRAVERSAL_DISABLED);
if (!udp_port_range_.is_null()) {
network_settings.port_range = udp_port_range_;
} else if (!nat_traversal_enabled_) {
// For legacy reasons we have to restrict the port range to a set of default
// values when nat traversal is disabled, even if the port range was not
// set in policy.
network_settings.port_range.min_port =
protocol::NetworkSettings::kDefaultMinPort;
network_settings.port_range.max_port =
protocol::NetworkSettings::kDefaultMaxPort;
}
scoped_refptr<protocol::TransportContext> transport_context =
new protocol::TransportContext(
signal_strategy_.get(),
base::WrapUnique(new protocol::ChromiumPortAllocatorFactory()),
base::WrapUnique(new ChromiumUrlRequestFactory(
host_context_->url_loader_factory())),
network_settings, protocol::TransportRole::SERVER);
transport_context->set_turn_ice_config(ice_config);
std::unique_ptr<protocol::SessionManager> session_manager(
new protocol::JingleSessionManager(signal_strategy_.get()));
std::unique_ptr<protocol::CandidateSessionConfig> protocol_config =
protocol::CandidateSessionConfig::CreateDefault();
// Disable audio by default.
// TODO(sergeyu): Add UI to enable it.
protocol_config->DisableAudioChannel();
protocol_config->set_webrtc_supported(true);
session_manager->set_protocol_config(std::move(protocol_config));
// Create the host.
DesktopEnvironmentOptions options(DesktopEnvironmentOptions::CreateDefault());
options.set_enable_user_interface(enable_dialogs_);
host_.reset(new ChromotingHost(
desktop_environment_factory_.get(), std::move(session_manager),
transport_context, host_context_->audio_task_runner(),
host_context_->video_encode_task_runner(), options));
host_->status_monitor()->AddStatusObserver(this);
host_status_logger_.reset(
new HostStatusLogger(host_->status_monitor(), ServerLogEntry::IT2ME,
signal_strategy_.get(), directory_bot_jid));
// Create event logger.
host_event_logger_ =
HostEventLogger::Create(host_->status_monitor(), kApplicationName);
// Connect signaling and start the host.
signal_strategy_->Connect();
host_->Start(username);
SetState(kRequestedAccessCode, ErrorCode::OK);
return;
}
void It2MeHost::OnAccessDenied(const std::string& jid) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
++failed_login_attempts_;
if (failed_login_attempts_ == kMaxLoginAttempts) {
DisconnectOnNetworkThread();
} else if (connecting_jid_ == jid) {
DCHECK_EQ(state_, kConnecting);
connecting_jid_.clear();
confirmation_dialog_proxy_.reset();
SetState(kReceivedAccessCode, ErrorCode::OK);
}
}
void It2MeHost::OnClientConnected(const std::string& jid) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
// ChromotingHost doesn't allow multiple concurrent connection and the
// host is destroyed in OnClientDisconnected() after the first connection.
CHECK_NE(state_, kConnected);
std::string client_username;
if (!SplitJidResource(jid, &client_username, /*resource=*/nullptr)) {
LOG(WARNING) << "Incorrectly formatted JID received: " << jid;
client_username = jid;
}
HOST_LOG << "Client " << client_username << " connected.";
// Pass the client user name to the script object before changing state.
host_context_->ui_task_runner()->PostTask(
FROM_HERE, base::Bind(&It2MeHost::Observer::OnClientAuthenticated,
observer_, client_username));
SetState(kConnected, ErrorCode::OK);
}
void It2MeHost::OnClientDisconnected(const std::string& jid) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
DisconnectOnNetworkThread();
}
ValidationCallback It2MeHost::GetValidationCallbackForTesting() {
return base::Bind(&It2MeHost::ValidateConnectionDetails,
base::Unretained(this));
}
void It2MeHost::OnPolicyUpdate(
std::unique_ptr<base::DictionaryValue> policies) {
// The policy watcher runs on the |ui_task_runner|.
if (!host_context_->network_task_runner()->BelongsToCurrentThread()) {
host_context_->network_task_runner()->PostTask(
FROM_HERE,
base::Bind(&It2MeHost::OnPolicyUpdate, this, base::Passed(&policies)));
return;
}
bool nat_policy;
if (policies->GetBoolean(policy::key::kRemoteAccessHostFirewallTraversal,
&nat_policy)) {
UpdateNatPolicy(nat_policy);
}
const base::ListValue* host_domain_list;
if (policies->GetList(policy::key::kRemoteAccessHostDomainList,
&host_domain_list)) {
std::vector<std::string> host_domain_list_vector;
for (const auto& value : *host_domain_list) {
host_domain_list_vector.push_back(value.GetString());
}
UpdateHostDomainListPolicy(std::move(host_domain_list_vector));
}
const base::ListValue* client_domain_list;
if (policies->GetList(policy::key::kRemoteAccessHostClientDomainList,
&client_domain_list)) {
std::vector<std::string> client_domain_list_vector;
for (const auto& value : *client_domain_list) {
client_domain_list_vector.push_back(value.GetString());
}
UpdateClientDomainListPolicy(std::move(client_domain_list_vector));
}
std::string port_range_string;
if (policies->GetString(policy::key::kRemoteAccessHostUdpPortRange,
&port_range_string)) {
UpdateHostUdpPortRangePolicy(port_range_string);
}
}
void It2MeHost::UpdateNatPolicy(bool nat_traversal_enabled) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
VLOG(2) << "UpdateNatPolicy: " << nat_traversal_enabled;
// When transitioning from enabled to disabled, force disconnect any
// existing session.
if (nat_traversal_enabled_ && !nat_traversal_enabled && IsRunning()) {
DisconnectOnNetworkThread();
}
nat_traversal_enabled_ = nat_traversal_enabled;
// Notify the web-app of the policy setting.
host_context_->ui_task_runner()->PostTask(
FROM_HERE, base::Bind(&It2MeHost::Observer::OnNatPolicyChanged, observer_,
nat_traversal_enabled_));
}
void It2MeHost::UpdateHostDomainListPolicy(
std::vector<std::string> host_domain_list) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
VLOG(2) << "UpdateHostDomainListPolicy: "
<< base::JoinString(host_domain_list, ", ");
// When setting a host domain policy, force disconnect any existing session.
if (!host_domain_list.empty() && IsRunning()) {
DisconnectOnNetworkThread();
}
required_host_domain_list_ = std::move(host_domain_list);
}
void It2MeHost::UpdateClientDomainListPolicy(
std::vector<std::string> client_domain_list) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
VLOG(2) << "UpdateClientDomainPolicy: "
<< base::JoinString(client_domain_list, ", ");
// When setting a client domain policy, disconnect any existing session.
if (!client_domain_list.empty() && IsRunning()) {
DisconnectOnNetworkThread();
}
required_client_domain_list_ = std::move(client_domain_list);
}
void It2MeHost::UpdateHostUdpPortRangePolicy(
const std::string& port_range_string) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
VLOG(2) << "UpdateHostUdpPortRangePolicy: " << port_range_string;
if (IsRunning()) {
DisconnectOnNetworkThread();
}
if (!PortRange::Parse(port_range_string, &udp_port_range_)) {
// PolicyWatcher verifies that the value is formatted correctly.
LOG(FATAL) << "Invalid port range: " << port_range_string;
}
}
void It2MeHost::SetState(It2MeHostState state, ErrorCode error_code) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
switch (state_) {
case kDisconnected:
DCHECK(state == kStarting ||
state == kError) << state;
break;
case kStarting:
DCHECK(state == kRequestedAccessCode ||
state == kDisconnected ||
state == kError ||
state == kInvalidDomainError) << state;
break;
case kRequestedAccessCode:
DCHECK(state == kReceivedAccessCode ||
state == kDisconnected ||
state == kError) << state;
break;
case kReceivedAccessCode:
DCHECK(state == kConnecting ||
state == kDisconnected ||
state == kError) << state;
break;
case kConnecting:
DCHECK(state == kConnected ||
state == kDisconnected ||
state == kError) << state;
break;
case kConnected:
DCHECK(state == kDisconnected ||
state == kError) << state;
break;
case kError:
DCHECK(state == kDisconnected) << state;
break;
case kInvalidDomainError:
DCHECK(state == kDisconnected) << state;
break;
};
state_ = state;
// Post a state-change notification to the web-app.
host_context_->ui_task_runner()->PostTask(
FROM_HERE, base::Bind(&It2MeHost::Observer::OnStateChanged, observer_,
state, error_code));
}
bool It2MeHost::IsRunning() const {
return state_ == kRequestedAccessCode || state_ == kReceivedAccessCode ||
state_ == kConnected || state_ == kConnecting;
}
void It2MeHost::OnReceivedSupportID(const std::string& support_id,
const base::TimeDelta& lifetime,
const ErrorCode error_code) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
if (error_code != ErrorCode::OK) {
SetState(kError, error_code);
DisconnectOnNetworkThread();
return;
}
std::string host_secret = GenerateSupportHostSecret();
std::string access_code = support_id + host_secret;
std::string access_code_hash =
protocol::GetSharedSecretHash(support_id, access_code);
std::string local_certificate = host_key_pair_->GenerateCertificate();
if (local_certificate.empty()) {
LOG(ERROR) << "Failed to generate host certificate.";
SetState(kError, ErrorCode::HOST_CERTIFICATE_ERROR);
DisconnectOnNetworkThread();
return;
}
std::unique_ptr<protocol::AuthenticatorFactory> factory(
new protocol::It2MeHostAuthenticatorFactory(
local_certificate, host_key_pair_, access_code_hash,
base::Bind(&It2MeHost::ValidateConnectionDetails,
base::Unretained(this))));
host_->SetAuthenticatorFactory(std::move(factory));
// Pass the Access Code to the script object before changing state.
host_context_->ui_task_runner()->PostTask(
FROM_HERE, base::Bind(&It2MeHost::Observer::OnStoreAccessCode, observer_,
access_code, lifetime));
SetState(kReceivedAccessCode, ErrorCode::OK);
}
void It2MeHost::DisconnectOnNetworkThread() {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
// Disconnect() may be called even when after the host been already stopped.
// Ignore repeated calls.
if (state_ == kDisconnected) {
return;
}
confirmation_dialog_proxy_.reset();
if (host_) {
host_->status_monitor()->RemoveStatusObserver(this);
host_ = nullptr;
}
register_request_ = nullptr;
host_status_logger_ = nullptr;
signal_strategy_ = nullptr;
host_event_logger_ = nullptr;
// Post tasks to delete UI objects on the UI thread.
host_context_->ui_task_runner()->DeleteSoon(
FROM_HERE, desktop_environment_factory_.release());
SetState(kDisconnected, ErrorCode::OK);
}
void It2MeHost::ValidateConnectionDetails(
const std::string& remote_jid,
const ValidationResultCallback& result_callback) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
// First ensure the JID we received is valid.
std::string client_username;
if (!SplitJidResource(remote_jid, &client_username, /*resource=*/nullptr)) {
LOG(ERROR) << "Rejecting incoming connection from " << remote_jid
<< ": Invalid JID.";
result_callback.Run(
protocol::ValidatingAuthenticator::Result::ERROR_INVALID_ACCOUNT);
DisconnectOnNetworkThread();
return;
}
if (client_username.empty()) {
LOG(ERROR) << "Invalid user name passed in: " << remote_jid;
result_callback.Run(
protocol::ValidatingAuthenticator::Result::ERROR_INVALID_ACCOUNT);
DisconnectOnNetworkThread();
return;
}
// Check the client domain policy.
if (!required_client_domain_list_.empty()) {
bool matched = false;
for (const auto& domain : required_client_domain_list_) {
if (base::EndsWith(client_username, std::string("@") + domain,
base::CompareCase::INSENSITIVE_ASCII)) {
matched = true;
break;
}
}
if (!matched) {
LOG(ERROR) << "Rejecting incoming connection from " << remote_jid
<< ": Domain not allowed.";
result_callback.Run(ValidationResult::ERROR_INVALID_ACCOUNT);
DisconnectOnNetworkThread();
return;
}
}
// If we receive valid connection details multiple times, then we don't know
// which remote user (if either) is valid so disconnect everyone.
if (state_ != kReceivedAccessCode) {
DCHECK_EQ(kConnecting, state_);
LOG(ERROR) << "Received too many connection requests.";
result_callback.Run(ValidationResult::ERROR_TOO_MANY_CONNECTIONS);
DisconnectOnNetworkThread();
return;
}
HOST_LOG << "Client " << client_username << " connecting.";
connecting_jid_ = remote_jid;
SetState(kConnecting, ErrorCode::OK);
// Show a confirmation dialog to the user to allow them to confirm/reject it.
// If dialogs are suppressed, just call the callback directly.
if (enable_dialogs_) {
confirmation_dialog_proxy_.reset(new It2MeConfirmationDialogProxy(
host_context_->ui_task_runner(),
confirmation_dialog_factory_->Create()));
confirmation_dialog_proxy_->Show(
client_username, base::Bind(&It2MeHost::OnConfirmationResult,
base::Unretained(this), result_callback));
} else {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(result_callback, ValidationResult::SUCCESS));
}
}
void It2MeHost::OnConfirmationResult(
const ValidationResultCallback& result_callback,
It2MeConfirmationDialog::Result result) {
DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
connecting_jid_.clear();
switch (result) {
case It2MeConfirmationDialog::Result::OK:
result_callback.Run(ValidationResult::SUCCESS);
break;
case It2MeConfirmationDialog::Result::CANCEL:
result_callback.Run(ValidationResult::ERROR_REJECTED_BY_USER);
DisconnectOnNetworkThread();
break;
}
}
It2MeHostFactory::It2MeHostFactory() = default;
It2MeHostFactory::~It2MeHostFactory() = default;
scoped_refptr<It2MeHost> It2MeHostFactory::CreateIt2MeHost() {
return new It2MeHost();
}
} // namespace remoting