// Copyright 2013 The Chromium Authors
// 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 <cstddef>
#include <cstring>
#include <memory>
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/policy/policy_constants.h"
#include "components/webrtc/thread_wrapper.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "remoting/base/auto_thread_task_runner.h"
#include "remoting/base/corp_session_authz_service_client_factory.h"
#include "remoting/base/local_session_policies_provider.h"
#include "remoting/base/logging.h"
#include "remoting/base/rsa_key_pair.h"
#include "remoting/base/session_policies.h"
#include "remoting/host/base/desktop_environment_options.h"
#include "remoting/host/chromeos/chromeos_enterprise_params.h"
#include "remoting/host/chromoting_host.h"
#include "remoting/host/chromoting_host_context.h"
#include "remoting/host/corp_host_status_logger.h"
#include "remoting/host/create_desktop_interaction_strategy_factory.h"
#include "remoting/host/ftl_signaling_connector.h"
#include "remoting/host/host_event_logger.h"
#include "remoting/host/host_event_reporter.h"
#include "remoting/host/host_secret.h"
#include "remoting/host/it2me/it2me_confirmation_dialog.h"
#include "remoting/host/it2me/it2me_confirmation_dialog_proxy.h"
#include "remoting/host/it2me/it2me_helpers.h"
#include "remoting/host/it2me/reconnect_params.h"
#include "remoting/host/it2me_desktop_environment.h"
#include "remoting/host/passthrough_register_support_host_request.h"
#include "remoting/host/peer_session_impl.h"
#include "remoting/host/session_policies_from_dict.h"
#include "remoting/proto/ftl/v1/chromoting_message.pb.h"
#include "remoting/protocol/auth_util.h"
#include "remoting/protocol/chromium_port_allocator_factory.h"
#include "remoting/protocol/errors.h"
#include "remoting/protocol/ice_config_fetcher_default.h"
#include "remoting/protocol/it2me_host_authenticator_factory.h"
#include "remoting/protocol/jingle_session_manager.h"
#include "remoting/protocol/session_manager.h"
#include "remoting/protocol/transport.h"
#include "remoting/protocol/transport_context.h"
#include "remoting/protocol/validating_authenticator.h"
#include "remoting/signaling/signaling_address.h"
#include "remoting/signaling/signaling_id_util.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "base/feature_list.h"
#include "remoting/host/chromeos/features.h"
#endif

#if BUILDFLAG(IS_LINUX)
#include "remoting/host/linux/gnome_remote_desktop_session.h"
#include "remoting/host/linux/portal_remote_desktop_session.h"
#endif

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;

// The amount of time to wait before destroying objects that send messages over
// the network, such as the signal strategy. This delay ensures there is time
// for messages (such as session-terminate) to be sent.
constexpr base::TimeDelta kDestroyMessagingObjectDelay = base::Seconds(2);

#if BUILDFLAG(IS_CHROMEOS)
// Enabled value for ClassManagementEnabled when host belongs to a student and
// their screen can be viewed by a teacher.
constexpr char kClassManagementStudent[] = "student";
// Enabled value for ClassManagementEnabled when host belongs to a teacher and
// they would like to access their host via another device.
constexpr char kClassManagementTeacher[] = "teacher";
#endif  // BUILDFLAG(IS_CHROMEOS)

// STL containers do not have a defined destruction orders for their elements.
// Post(Delayed)Task relies on these containers so the destruction order is also
// undefined, causing problems when there are dependencies between objects to be
// delayed destructed. This class takes ownership of the objects passed to the
// constructor, and destroys them in their order in the parameter list when
// the destructor is called.
template <typename... T>
class OrderedDestruction {
 public:
  explicit OrderedDestruction(std::unique_ptr<T>... objects)
      : objects_{std::move(objects)...} {}

  OrderedDestruction(OrderedDestruction&&) = default;

  ~OrderedDestruction() {
    // Reset the to-be-destroyed pointers in passed order.
    [&]<std::size_t... I>(std::index_sequence<I...>) {
      (get<I>(objects_).reset(), ...);
    }(std::index_sequence_for<T...>());
  }

 private:
  std::tuple<std::unique_ptr<T>...> objects_;
};

}  // namespace

It2MeHost::DeferredConnectContext::DeferredConnectContext() = default;

It2MeHost::DeferredConnectContext::~DeferredConnectContext() = default;

It2MeHost::It2MeHost() {
#if BUILDFLAG(IS_CHROMEOS)
  host_event_reporter_factory_ =
      base::BindRepeating(&HostEventReporter::Create);
#endif
}

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_chrome_os_enterprise_params(
    ChromeOsEnterpriseParams params) {
#if BUILDFLAG(IS_CHROMEOS) || !defined(NDEBUG)
  CHECK_NE(params.request_origin, ChromeOsEnterpriseRequestOrigin::kUnknown);
  CHECK_NE(params.audio_playback, ChromeOsEnterpriseAudioPlayback::kUnknown);
  chrome_os_enterprise_params_ = std::move(params);
#else
  NOTREACHED() << "It2MeHost::set_chrome_os_enterprise_params is only "
                  "supported on ChromeOS";
#endif
}

void It2MeHost::set_authorized_helper(const std::string& authorized_helper) {
  authorized_helper_ = authorized_helper;
}

void It2MeHost::set_reconnect_params(ReconnectParams reconnect_params) {
#if BUILDFLAG(IS_CHROMEOS) || !defined(NDEBUG)
  reconnect_params_.emplace(std::move(reconnect_params));
#else
  NOTREACHED() << "It2MeHost::set_reconnect_params is only supported on CrOS";
#endif
}

bool It2MeHost::SessionSupportsReconnections() const {
#if BUILDFLAG(IS_CHROMEOS) || !defined(NDEBUG)
  return is_enterprise_session() &&
         chrome_os_enterprise_params_->allow_reconnections;
#else
  return false;
#endif
}

std::optional<ReconnectParams> It2MeHost::CreateReconnectParams() const {
  std::optional<ReconnectParams> reconnect_params;
#if BUILDFLAG(IS_CHROMEOS) || !defined(NDEBUG)
  if (!SessionSupportsReconnections()) {
    return reconnect_params;
  }
  // This function is meant to be queried just after the remote client connects,
  // otherwise the required fields will not be set.
  CHECK_EQ(state_, It2MeHostState::kConnected);

  reconnect_params.emplace();
  reconnect_params->support_id = support_id_;
  reconnect_params->host_secret = host_secret_;
  reconnect_params->private_key = host_key_pair_->ToString();
  reconnect_params->ftl_device_id = ftl_device_id_;
  reconnect_params->client_ftl_address = connecting_jid_;
#endif

  return reconnect_params;
}

void It2MeHost::SendReconnectSessionMessage() const {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  if (state_ != It2MeHostState::kReceivedAccessCode) {
    // If the host state has changed since the task was posted, just bail early.
    return;
  }

  ftl::ChromotingMessage crd_message;
  crd_message.mutable_reconnect()->set_support_id(
      reconnect_params_->support_id);
  SignalingAddress signaling_address(reconnect_params_->client_ftl_address);

  signal_strategy_->SendFtlMessage(signaling_address, std::move(crd_message));
}

void It2MeHost::Connect(
    std::unique_ptr<ChromotingHostContext> host_context,
    base::DictValue policies,
    std::unique_ptr<It2MeConfirmationDialogFactory> dialog_factory,
    base::WeakPtr<It2MeHost::Observer> observer,
    CreateDeferredConnectContext create_context,
    const std::string& username) {
  DCHECK(host_context->ui_task_runner()->BelongsToCurrentThread());

  host_context_ = std::move(host_context);
  observer_ = std::move(observer);
  confirmation_dialog_factory_ = std::move(dialog_factory);
  local_session_policies_provider_ =
      std::make_unique<LocalSessionPoliciesProvider>();

  if (is_enterprise_session()) {
    // Don't notify on local policy changes for Admin sessions as the policies
    // can change as they log into different sessions and this should not cause
    // them to be disconnected: See crbug.com/380421478.
    local_session_policies_provider_->send_policy_change_notifications(false);
  }

  OnPolicyUpdate(std::move(policies));

  desktop_environment_factory_ =
      std::make_unique<It2MeDesktopEnvironmentFactory>(
          host_context_->network_task_runner(), host_context_->ui_task_runner(),
          CreateDesktopInteractionStrategyFactory(
              host_context_->network_task_runner(),
              host_context_->ui_task_runner(),
              host_context_->video_capture_task_runner(),
              host_context_->input_task_runner()));

  // Switch to the network thread to start the actual connection.
  host_context_->network_task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&It2MeHost::ConnectOnNetworkThread, this,
                                username, std::move(create_context)));
}

void It2MeHost::Disconnect() {
  DCHECK(host_context_->ui_task_runner()->BelongsToCurrentThread());
  host_context_->network_task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&It2MeHost::DisconnectOnNetworkThread, this,
                                protocol::ErrorCode::OK));
}

void It2MeHost::ConnectOnNetworkThread(
    const std::string& username,
    CreateDeferredConnectContext create_context) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());
  DCHECK_EQ(It2MeHostState::kDisconnected, state_);
  // This thread is used as a network thread in WebRTC.
  webrtc::ThreadWrapper::EnsureForCurrentMessageLoop();

  if (!remote_support_connections_allowed_) {
    SetState(It2MeHostState::kError, ErrorCode::DISALLOWED_BY_POLICY);
    return;
  }

  SetState(It2MeHostState::kStarting, ErrorCode::OK);

#if BUILDFLAG(IS_LINUX)
  if (!GnomeRemoteDesktopSession::IsRunningUnderGnome()) {
    PortalRemoteDesktopSession::GetInstance()->SetCreateVirtualMonitor(false);
  }
#endif

  auto connection_context = std::move(create_context).Run(host_context_.get());
  signal_strategy_ = std::move(connection_context->signal_strategy);
  api_token_getter_ = std::move(connection_context->api_token_getter);
  DCHECK(signal_strategy_);

  // We want to make sure that the signaling channel will reconnect if a
  // transient network error occurs.
  // FtlSignalingConnector takes a callback which will indicate whether an
  // auth error has occurred (e.g. token expired). For our purposes, there
  // isn't anything we need to do in this case since a new token will be
  // generated for the next connection.
  ftl_signaling_connector_ = std::make_unique<FtlSignalingConnector>(
      signal_strategy_.get(), base::DoNothing());
  ftl_signaling_connector_->Start();
  ftl_device_id_ = connection_context->ftl_device_id;

  // Check the host domain policy.
  // Skip this check for enterprise sessions, as they use the device specific
  // robot account as host, and we should not expect the customers to add this
  // internal account to their host domain list.
  if (!required_host_domain_list_.empty() && !is_enterprise_session()) {
    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(It2MeHostState::kInvalidDomainError, ErrorCode::OK);
      return;
    }
  }

  if (connection_context->is_corp_user) {
    use_corp_session_authz_ = true;
  }

  if (!reconnect_params_.has_value()) {
    // Generate a key pair for the Host to use.
    host_key_pair_ = RsaKeyPair::Generate();

    // Shared secret auth is not supported when SessionAuthz is in use.
    if (!use_corp_session_authz_) {
      // Generate a new host secret for this instance.
      host_secret_ = GenerateSupportHostSecret();
    }

    // Register this host instance in the backend service.
    register_request_ = std::move(connection_context->register_request);
  } else {
    // Reconnections are only allowed for Chrome OS enterprise sessions.
    CHECK(SessionSupportsReconnections());

    // Reconnections are not allowed when SessionAuthz is in use.
    CHECK(!use_corp_session_authz_);

    // Regenerate the key pair from the private key.
    host_key_pair_ = RsaKeyPair::FromString(reconnect_params_->private_key);

    // Restore the host_secret from the previous connection.
    host_secret_ = reconnect_params_->host_secret;

    // Skip the registration service call as the entry will be retrievable by
    // the `authorized_helper` for ~24 hours when 'allow_reconnections' is set.
    register_request_ = std::make_unique<PassthroughRegisterSupportHostRequest>(
        reconnect_params_->support_id);
  }
  register_request_->StartRequest(
      signal_strategy_.get(), host_context_->CreateClientCertStore(),
      host_key_pair_, authorized_helper_,
      std::move(chrome_os_enterprise_params_),
      base::BindOnce(&It2MeHost::OnReceivedSupportID,
                     weak_factory_.GetWeakPtr()));

  auto get_ice_config_fetcher_cb = base::BindRepeating(
      [](scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
         OAuthTokenGetter* api_token_getter)
          -> std::unique_ptr<protocol::IceConfigFetcher> {
        return std::make_unique<protocol::IceConfigFetcherDefault>(
            url_loader_factory, api_token_getter);
      },
      host_context_->url_loader_factory(), api_token_getter_.get());

  std::unique_ptr<protocol::SessionManager> session_manager(
      new protocol::JingleSessionManager(signal_strategy_.get()));

  if (use_corp_session_authz_) {
    corp_host_status_logger_ = CorpHostStatusLogger::CreateForRemoteSupport(
        host_context_->url_loader_factory(),
        host_context_->CreateClientCertStore(),
        local_session_policies_provider_.get(),
        api_token_getter_->GetWeakPtr());
    corp_host_status_logger_->StartObserving(*session_manager);
  }

  // Set up the desktop environment options.
  DesktopEnvironmentOptions options(DesktopEnvironmentOptions::CreateDefault());

#if BUILDFLAG(IS_CHROMEOS) || !defined(NDEBUG)
  if (is_enterprise_session()) {
    options.set_enable_user_interface(
        !chrome_os_enterprise_params_->suppress_user_dialogs);
    options.set_enable_notifications(
        !chrome_os_enterprise_params_->suppress_notifications);
    options.set_terminate_upon_input(
        chrome_os_enterprise_params_->terminate_upon_input);
    options.set_maximum_session_duration(
        chrome_os_enterprise_params_->maximum_session_duration);
    switch (chrome_os_enterprise_params_->audio_playback) {
      case ChromeOsEnterpriseAudioPlayback::kLocalOnly:
        options.set_audio_playback_mode(AudioPlaybackMode::kLocalOnly);
        break;
      case ChromeOsEnterpriseAudioPlayback::kRemoteOnly:
        options.set_audio_playback_mode(AudioPlaybackMode::kRemoteOnly);
        break;
      case ChromeOsEnterpriseAudioPlayback::kRemoteAndLocal:
        options.set_audio_playback_mode(AudioPlaybackMode::kRemoteAndLocal);
        break;
      default:
        NOTREACHED() << "audio_playback_mode not supported: "
                     << ConvertChromeOsEnterpriseAudioPlaybackToString(
                            chrome_os_enterprise_params_->audio_playback);
    }
  } else {
    // Audio remoting is disabled for non-enterprise ChromeOS connections.
    options.set_audio_playback_mode(AudioPlaybackMode::kLocalOnly);
  }
#endif  // BUILDFLAG(IS_CHROMEOS) || !defined(NDEBUG)

  auto peer_session_factory = std::make_unique<PeerSessionImplFactory>(
      desktop_environment_factory_.get(), std::move(get_ice_config_fetcher_cb));

  // Create the host.
  host_ = std::make_unique<ChromotingHost>(
      std::move(peer_session_factory), std::move(session_manager),
      /* secondary_session_manager */ nullptr, options,
      base::BindRepeating(&It2MeHost::OnEffectiveSessionPoliciesReceived,
                          base::Unretained(this)),
      local_session_policies_provider_.get());
  host_->status_monitor()->AddStatusObserver(this);

  // Create event logger.
  host_event_logger_ =
      HostEventLogger::Create(host_->status_monitor(), kApplicationName);
#if BUILDFLAG(IS_CHROMEOS)
  host_event_reporter_ =
      host_event_reporter_factory_.Run(host_->status_monitor());
#endif  // BUILDFLAG(IS_CHROMEOS)

  // Connect signaling and start the host.
  signal_strategy_->Connect();
  host_->Start(username);

  SetState(It2MeHostState::kRequestedAccessCode, ErrorCode::OK);
  return;
}

void It2MeHost::OnClientAccessDenied(const std::string& signaling_id) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  ++failed_login_attempts_;
  if (failed_login_attempts_ == kMaxLoginAttempts) {
    DisconnectOnNetworkThread();
  } else if (connecting_jid_ == NormalizeSignalingId(signaling_id)) {
    DCHECK_EQ(state_, It2MeHostState::kConnecting);
    connecting_jid_.clear();
    confirmation_dialog_proxy_.reset();
    SetState(It2MeHostState::kReceivedAccessCode, ErrorCode::OK);
  }
}

void It2MeHost::OnClientConnected(const std::string& signaling_id) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  // ChromotingHost doesn't allow concurrent connections and the host is
  // destroyed in OnClientDisconnected() after the first connection.
  CHECK_NE(state_, It2MeHostState::kConnected);

  std::string client_username;
  if (!SplitSignalingIdResource(signaling_id, &client_username,
                                /*resource=*/nullptr)) {
    LOG(WARNING) << "Incorrectly formatted signaling ID received: "
                 << signaling_id;
    client_username = signaling_id;
  }

  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::BindOnce(&It2MeHost::Observer::OnClientAuthenticated,
                                observer_, client_username));

  SetState(It2MeHostState::kConnected, ErrorCode::OK);
}

void It2MeHost::OnClientDisconnected(const std::string& signaling_id) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  // Handling HostStatusObserver events should not cause the destruction of the
  // ChromotingHost instance, however that is exactly what happens inside of
  // DisconnectOnNetworkThread() so we post a task to disconnect asynchronously
  // which will allow any other HostStatusObservers to handle the event as well
  // before everything is torn down.
  host_context_->network_task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&It2MeHost::DisconnectOnNetworkThread, this,
                                protocol::ErrorCode::OK));
}

ValidationCallback It2MeHost::GetValidationCallbackForTesting() {
  return base::BindRepeating(&It2MeHost::ValidateConnectionDetails,
                             base::Unretained(this));
}

#if BUILDFLAG(IS_CHROMEOS)
void It2MeHost::SetHostEventReporterFactoryForTesting(
    HostEventReporterFactory factory) {
  host_event_reporter_factory_ = factory;
}
#endif  // BUILDFLAG(IS_CHROMEOS)

void It2MeHost::OnPolicyUpdate(base::DictValue 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::BindOnce(&It2MeHost::OnPolicyUpdate, this, std::move(policies)));
    return;
  }

  if (is_enterprise_session() && IsRunning()) {
    // Don't notify on policy changes for Admin sessions as the policies can
    // change as they log into different sessions and this should not cause
    // them to be disconnected: See crbug.com/380421478.
    HOST_LOG << "Dropping policy update during enterprise connection.";
    return;
  }

  // Retrieve the policy value on whether to allow connections but don't apply
  // it until after we've finished reading the rest of the policies and started
  // the connection process.
  remote_support_connections_allowed_ =
      RemoteSupportConnectionsAllowed(policies);

  const base::ListValue* host_domain_list =
      policies.FindList(policy::key::kRemoteAccessHostDomainList);
  if (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 =
      policies.FindList(policy::key::kRemoteAccessHostClientDomainList);
  if (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));
  }

  UpdateLocalSessionPolicies(policies);

  // If |session_policies_finalized_| is true, then local policy changes will
  // either disconnect the session, or have no effects if the effective policies
  // come from the authenticator. In either case, we do not need to report the
  // local NAT policies.
  if (local_session_policies_provider_ && !session_policies_finalized_) {
    ReportNatPolicies(local_session_policies_provider_->get_local_policies());
  }
}

std::optional<ErrorCode> It2MeHost::OnEffectiveSessionPoliciesReceived(
    const SessionPolicies& session_policies) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  session_policies_finalized_ = true;
  ReportNatPolicies(session_policies);
  return std::nullopt;
}

void It2MeHost::ReportNatPolicies(const SessionPolicies& session_policies) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  bool nat_policy_value =
      session_policies.allow_stun_connections.value_or(true);
  VLOG(2) << "UpdateNatPolicies: nat_policy_value: " << nat_policy_value;
  bool nat_traversal_value_changed =
      !last_reported_nat_traversal_enabled_.has_value() ||
      *last_reported_nat_traversal_enabled_ != nat_policy_value;
  last_reported_nat_traversal_enabled_ = nat_policy_value;

  bool relay_policy_value =
      session_policies.allow_relayed_connections.value_or(true);
  VLOG(2) << "UpdateNatPolicies: relay_policy_value: " << relay_policy_value;
  bool relay_value_changed =
      !last_reported_relay_connections_allowed_.has_value() ||
      *last_reported_relay_connections_allowed_ != relay_policy_value;
  last_reported_relay_connections_allowed_ = relay_policy_value;

  // Notify listeners of the policy setting change.
  if (nat_traversal_value_changed || relay_value_changed) {
    host_context_->ui_task_runner()->PostTask(
        FROM_HERE,
        base::BindOnce(&It2MeHost::Observer::OnNatPoliciesChanged, observer_,
                       nat_policy_value, relay_policy_value));
  }
}

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::UpdateLocalSessionPolicies(
    const base::DictValue& platform_policies) {
  // |local_session_policies_provider_| is null if there is no active
  // connection. Connect() calls OnPolicyUpdate() with the platform policies, so
  // we don't need to track session policies when there is no active connection.
  if (!local_session_policies_provider_) {
    return;
  }

  std::optional<SessionPolicies> local_session_policies =
      SessionPoliciesFromDict(platform_policies);
  if (!local_session_policies.has_value()) {
    LOG(FATAL) << "Failed to parse local session policies.";
  }

  // These are currently disallowed for IT2ME connections by default.
  // TODO: yuweih - Figure out what should be done when we add SessionAuthz
  // policies support for IT2ME. Given the current logic, these features can be
  // enabled by SessionAuthz policies, which is not possible by local Chrome
  // policies.
  local_session_policies->allow_file_transfer = false;
  local_session_policies->allow_uri_forwarding = false;

  local_session_policies->allow_remote_input = true;

#if BUILDFLAG(IS_CHROMEOS) || !defined(NDEBUG)
  if (is_enterprise_session()) {
    local_session_policies->curtain_required =
        chrome_os_enterprise_params_->curtain_local_user_session;

    local_session_policies->allow_remote_input =
        chrome_os_enterprise_params_->allow_remote_input;

    if (!chrome_os_enterprise_params_->allow_clipboard_sync) {
      local_session_policies->clipboard_size_bytes = 0;
    }

    if (!chrome_os_enterprise_params_->maximum_session_duration.is_zero()) {
      local_session_policies->maximum_session_duration =
          chrome_os_enterprise_params_->maximum_session_duration;
    }

#if BUILDFLAG(IS_CHROMEOS)
    bool enterprise_file_transfer_allowed =
        platform_policies
            .FindBool(policy::key::kRemoteAccessHostAllowEnterpriseFileTransfer)
            .value_or(false);
#else
    bool enterprise_file_transfer_allowed = false;
#endif
    local_session_policies->allow_file_transfer =
        chrome_os_enterprise_params_->allow_file_transfer &&
        enterprise_file_transfer_allowed;
  }
#endif

  local_session_policies_provider_->set_local_policies(*local_session_policies);
}

void It2MeHost::SetState(It2MeHostState state, ErrorCode error_code) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  switch (state_) {
    case It2MeHostState::kDisconnected:
      DCHECK(state == It2MeHostState::kStarting ||
             state == It2MeHostState::kError)
          << It2MeHostStateToString(state);
      break;
    case It2MeHostState::kStarting:
      DCHECK(state == It2MeHostState::kRequestedAccessCode ||
             state == It2MeHostState::kDisconnected ||
             state == It2MeHostState::kError ||
             state == It2MeHostState::kInvalidDomainError)
          << It2MeHostStateToString(state);
      break;
    case It2MeHostState::kRequestedAccessCode:
      DCHECK(state == It2MeHostState::kReceivedAccessCode ||
             state == It2MeHostState::kDisconnected ||
             state == It2MeHostState::kError)
          << It2MeHostStateToString(state);
      break;
    case It2MeHostState::kReceivedAccessCode:
      DCHECK(state == It2MeHostState::kConnecting ||
             state == It2MeHostState::kDisconnected ||
             state == It2MeHostState::kError)
          << It2MeHostStateToString(state);
      break;
    case It2MeHostState::kConnecting:
      DCHECK(state == It2MeHostState::kConnected ||
             state == It2MeHostState::kDisconnected ||
             state == It2MeHostState::kError)
          << It2MeHostStateToString(state);
      break;
    case It2MeHostState::kConnected:
      DCHECK(state == It2MeHostState::kDisconnected ||
             state == It2MeHostState::kError)
          << It2MeHostStateToString(state);
      break;
    case It2MeHostState::kError:
      DCHECK(state == It2MeHostState::kDisconnected)
          << It2MeHostStateToString(state);
      break;
    case It2MeHostState::kInvalidDomainError:
      DCHECK(state == It2MeHostState::kDisconnected)
          << It2MeHostStateToString(state);
      break;
  }

  state_ = state;

  // Post a state-change notification to the web-app.
  host_context_->ui_task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&It2MeHost::Observer::OnStateChanged, observer_,
                                state, error_code));
}

bool It2MeHost::IsRunning() const {
  return state_ == It2MeHostState::kRequestedAccessCode ||
         state_ == It2MeHostState::kReceivedAccessCode ||
         state_ == It2MeHostState::kConnected ||
         state_ == It2MeHostState::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(It2MeHostState::kError, error_code);
    DisconnectOnNetworkThread();
    return;
  }

  support_id_ = support_id;
  std::string access_code = support_id_ + host_secret_;

  std::string local_certificate = host_key_pair_->GenerateCertificate();
  if (local_certificate.empty()) {
    LOG(ERROR) << "Failed to generate host certificate.";
    SetState(It2MeHostState::kError, ErrorCode::HOST_CERTIFICATE_ERROR);
    DisconnectOnNetworkThread();
    return;
  }

  auto factory = std::make_unique<protocol::It2MeHostAuthenticatorFactory>(
      local_certificate, host_key_pair_,
      base::BindRepeating(&It2MeHost::ValidateConnectionDetails,
                          base::Unretained(this)));
  if (use_corp_session_authz_) {
    factory->AddSessionAuthzAuth(
        base::MakeRefCounted<CorpSessionAuthzServiceClientFactory>(
            host_context_->url_loader_factory(),
            host_context_->create_client_cert_store_callback(),
            api_token_getter_->GetWeakPtr(), support_id_));
  } else {
    CHECK(!host_secret_.empty());
    std::string access_code_hash =
        protocol::GetSharedSecretHash(support_id_, access_code);
    factory->AddSharedSecretAuth(access_code_hash);
  }
  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::BindOnce(&It2MeHost::Observer::OnStoreAccessCode,
                                observer_, access_code, lifetime));

  SetState(It2MeHostState::kReceivedAccessCode, ErrorCode::OK);

  // If this host instance was started using |reconnect_params_| then send a
  // signaling message to the client address from the previous connection to let
  // it know that it needs to reconnect. The client address is regenerated for
  // every connection (and reconnection) which is important because this message
  // will only be delivered if the client hasn't already restarted the
  // connection process.
  if (reconnect_params_.has_value()) {
    host_context_->network_task_runner()->PostTask(
        FROM_HERE, base::BindOnce(&It2MeHost::SendReconnectSessionMessage,
                                  weak_factory_.GetWeakPtr()));
  }
}

void It2MeHost::DisconnectOnNetworkThread(protocol::ErrorCode error_code) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  // Disconnect() may be called even after the host has already been stopped.
  // Ignore repeated calls.
  if (state_ == It2MeHostState::kDisconnected) {
    return;
  }

  confirmation_dialog_proxy_.reset();

  if (host_) {
    host_->status_monitor()->RemoveStatusObserver(this);
    host_ = nullptr;
  }

  register_request_ = nullptr;
  ftl_signaling_connector_ = nullptr;
  reconnect_params_.reset();

  // Delay destruction of the objects that send messages over the network by a
  // few seconds to give them a chance to send any outgoing messages (e.g.
  // session-terminate) so the other end of the connection can display and log
  // an accurate disconnect reason.
  OrderedDestruction ordered_destruction{
      std::move(signal_strategy_), std::move(corp_host_status_logger_),
      // Needed by |corp_host_status_logger_|.
      std::move(local_session_policies_provider_)};
  host_context_->network_task_runner()->PostDelayedTask(
      FROM_HERE, base::DoNothingWithBoundArgs(std::move(ordered_destruction)),
      kDestroyMessagingObjectDelay);

#if BUILDFLAG(IS_CHROMEOS)
  host_event_reporter_.reset();
#endif  // BUILDFLAG(IS_CHROMEOS)
  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(It2MeHostState::kDisconnected, error_code);
}

void It2MeHost::ValidateConnectionDetails(
    const std::string& original_remote_jid,
    ValidationResultCallback result_callback) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  // First ensure the JID we received is valid.
  std::string client_username;
  if (!SplitSignalingIdResource(original_remote_jid, &client_username,
                                /*resource=*/nullptr)) {
    LOG(ERROR) << "Rejecting incoming connection from " << original_remote_jid
               << ": Invalid JID.";
    std::move(result_callback)
        .Run(protocol::ValidatingAuthenticator::Result::ERROR_INVALID_ACCOUNT);
    DisconnectOnNetworkThread();
    return;
  }
  std::string remote_jid = NormalizeSignalingId(original_remote_jid);

  if (client_username.empty()) {
    LOG(ERROR) << "Invalid user name passed in: " << remote_jid;
    std::move(result_callback)
        .Run(protocol::ValidatingAuthenticator::Result::ERROR_INVALID_ACCOUNT);
    DisconnectOnNetworkThread();
    return;
  }

  // Check the client domain policy.
  // Skip this check for class management sessions, as they use the device
  // specific robot account as client, and we should not expect the customers to
  // add this internal account to their client domain list.
  if (!required_client_domain_list_.empty() && !is_class_management_session()) {
    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.";
      std::move(result_callback).Run(ValidationResult::ERROR_INVALID_ACCOUNT);
      DisconnectOnNetworkThread();
      return;
    }
  }

  if (is_class_management_session() && authorized_helper_.empty()) {
    LOG(ERROR) << "Rejecting class management connection request from ("
               << client_username << "as an authorized_helper was not provided";
    std::move(result_callback)
        .Run(ValidationResult::ERROR_UNAUTHORIZED_ACCOUNT);
    DisconnectOnNetworkThread();
    return;
  }

  if (!authorized_helper_.empty() &&
      !gaia::AreEmailsSame(authorized_helper_, client_username)) {
    LOG(ERROR) << "Rejecting connection request from (" << client_username
               << ") as it does not match the authorized_helper ("
               << authorized_helper_ << ")";
    std::move(result_callback)
        .Run(ValidationResult::ERROR_UNAUTHORIZED_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_ != It2MeHostState::kReceivedAccessCode) {
    DCHECK_EQ(It2MeHostState::kConnecting, state_);
    LOG(ERROR) << "Received too many connection requests.";
    std::move(result_callback)
        .Run(ValidationResult::ERROR_TOO_MANY_CONNECTIONS);
    DisconnectOnNetworkThread();
    return;
  }

  HOST_LOG << "Client " << client_username << " connecting.";
  connecting_jid_ = remote_jid;
  SetState(It2MeHostState::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 (is_enterprise_session() &&
      chrome_os_enterprise_params_->suppress_user_dialogs) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(result_callback), ValidationResult::SUCCESS));
  } else {
    confirmation_dialog_proxy_ = std::make_unique<It2MeConfirmationDialogProxy>(
        host_context_->ui_task_runner(),
        confirmation_dialog_factory_->Create());
    confirmation_dialog_proxy_->Show(
        client_username,
        base::BindOnce(&It2MeHost::OnConfirmationResult, base::Unretained(this),
                       std::move(result_callback)));
  }
}

void It2MeHost::OnConfirmationResult(ValidationResultCallback result_callback,
                                     It2MeConfirmationDialog::Result result) {
  DCHECK(host_context_->network_task_runner()->BelongsToCurrentThread());

  connecting_jid_.clear();
  switch (result) {
    case It2MeConfirmationDialog::Result::OK:
      std::move(result_callback).Run(ValidationResult::SUCCESS);
      break;

    case It2MeConfirmationDialog::Result::CANCEL:
      std::move(result_callback).Run(ValidationResult::ERROR_REJECTED_BY_USER);
      DisconnectOnNetworkThread(ErrorCode::SESSION_REJECTED);
      break;
  }
}

bool It2MeHost::RemoteSupportConnectionsAllowed(
    const base::DictValue& policies) {
#if BUILDFLAG(IS_CHROMEOS)
  // The policy to disallow remote support connections
  // (RemoteAccessHostAllowRemoteSupportConnections) does not apply to support
  // sessions initiated by the enterprise admin via a RemoteCommand or by Class
  // tools. These two cases are handled specifically by the policy to disallow
  // enterprise remote support connections
  // (RemoteAccessHostAllowEnterpriseRemoteSupportConnections) and the policy
  // to disallow teachers from viewing student screens
  // (ClassManagementEnabled).
  if (is_enterprise_session()) {
    switch (chrome_os_enterprise_params_->request_origin) {
      case remoting::ChromeOsEnterpriseRequestOrigin::kClassManagement:
        if (const std::string* class_management_enabled_value =
                policies.FindString(policy::key::kClassManagementEnabled)) {
          return *class_management_enabled_value == kClassManagementStudent ||
                 *class_management_enabled_value == kClassManagementTeacher;
        }
        return false;
      case remoting::ChromeOsEnterpriseRequestOrigin::kEnterpriseAdmin:
        return policies
            .FindBool(
                policy::key::
                    kRemoteAccessHostAllowEnterpriseRemoteSupportConnections)
            .value_or(true);
      case remoting::ChromeOsEnterpriseRequestOrigin::kUnknown:
        NOTREACHED() << "RequestOrigin is validated to be known when "
                        "enterprise parameters are set";
    }
  }
#endif
  return policies
      .FindBool(policy::key::kRemoteAccessHostAllowRemoteSupportConnections)
      .value_or(true);
}

It2MeHostFactory::It2MeHostFactory() = default;
It2MeHostFactory::~It2MeHostFactory() = default;

std::unique_ptr<It2MeHostFactory> It2MeHostFactory::Clone() const {
  return std::make_unique<It2MeHostFactory>();
}

scoped_refptr<It2MeHost> It2MeHostFactory::CreateIt2MeHost() {
  return new It2MeHost();
}

}  // namespace remoting
