| // Copyright 2025 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/client/common/remoting_client.h" |
| |
| #include <cstdint> |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/functional/callback.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/notimplemented.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/time.h" |
| #include "components/webrtc/thread_wrapper.h" |
| #include "remoting/base/directory_service_client.h" |
| #include "remoting/base/oauth_token_info.h" |
| #include "remoting/base/passthrough_oauth_token_getter.h" |
| #include "remoting/client/common/client_status_observer.h" |
| #include "remoting/client/common/frame_consumer_wrapper.h" |
| #include "remoting/client/common/logging.h" |
| #include "remoting/proto/control.pb.h" |
| #include "remoting/proto/remoting/v1/host_info.pb.h" |
| #include "remoting/proto/remoting/v1/remote_support_host_messages.pb.h" |
| #include "remoting/protocol/chromium_port_allocator_factory.h" |
| #include "remoting/protocol/chromium_socket_factory.h" |
| #include "remoting/protocol/client_authentication_config.h" |
| #include "remoting/protocol/connection_to_host.h" |
| #include "remoting/protocol/errors.h" |
| #include "remoting/protocol/host_stub.h" |
| #include "remoting/protocol/ice_config_fetcher_default.h" |
| #include "remoting/protocol/jingle_session.h" |
| #include "remoting/protocol/jingle_session_manager.h" |
| #include "remoting/protocol/negotiating_client_authenticator.h" |
| #include "remoting/protocol/network_settings.h" |
| #include "remoting/protocol/session_config.h" |
| #include "remoting/protocol/transport.h" |
| #include "remoting/protocol/transport_context.h" |
| #include "remoting/protocol/webrtc_connection_to_host.h" |
| #include "remoting/signaling/ftl_client_uuid_device_id_provider.h" |
| #include "remoting/signaling/ftl_signal_strategy.h" |
| #include "remoting/signaling/signaling_address.h" |
| #include "remoting/signaling/signaling_id_util.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "third_party/webrtc/api/video_codecs/sdp_video_format.h" |
| |
| namespace remoting { |
| |
| namespace { |
| constexpr std::int32_t kMinBitrateBps = 10485760; |
| } |
| |
| RemotingClient::RemotingClient( |
| base::OnceClosure quit_closure, |
| protocol::FrameConsumer* frame_consumer, |
| base::WeakPtr<protocol::AudioStub> audio_stream_consumer, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) |
| : quit_closure_(std::move(quit_closure)), |
| frame_consumer_(frame_consumer), |
| audio_stream_consumer_(std::move(audio_stream_consumer)), |
| url_loader_factory_(url_loader_factory) { |
| CHECK(frame_consumer_); |
| } |
| |
| RemotingClient::~RemotingClient() { |
| if (signal_strategy_) { |
| signal_strategy_->RemoveListener(this); |
| } |
| observers_.Notify(&ClientStatusObserver::OnClientDestroyed); |
| } |
| |
| void RemotingClient::StartSession(std::string_view support_access_code, |
| OAuthTokenInfo oauth_token_info) { |
| CHECK(!session_manager_); |
| CHECK_EQ(support_access_code.length(), 12UL); |
| CHECK_GT(oauth_token_info.access_token().length(), 0UL); |
| CHECK_GT(oauth_token_info.user_email().length(), 0UL); |
| |
| host_id_ = support_access_code.substr(0, 7); |
| // Though the host-side impl generates a 5 digit 'secret', the authenticator |
| // considers the full 12-digit access code to be the secret. |
| host_secret_ = support_access_code; |
| |
| // TODO: joedow - If we need to support sessions > 1 hour, we will need to |
| // provide a method for refreshing the access token. |
| oauth_token_info_ = std::move(oauth_token_info); |
| oauth_token_getter_ = |
| std::make_unique<PassthroughOAuthTokenGetter>(oauth_token_info_); |
| directory_service_client_ = std::make_unique<DirectoryServiceClient>( |
| oauth_token_getter_.get(), url_loader_factory_); |
| |
| // base::Unretained is sound because this instance owns the service client |
| // and callbacks will not be run after destruction. |
| CLIENT_LOG << "Retrieving host information for id: " << host_id_; |
| directory_service_client_->GetManagedChromeOsHost( |
| host_id_, |
| base::BindOnce(&RemotingClient::OnGetManagedChromeOsHostRetrieved, |
| base::Unretained(this))); |
| } |
| |
| void RemotingClient::OnGetManagedChromeOsHostRetrieved( |
| const HttpStatus& status, |
| std::unique_ptr<apis::v1::GetManagedChromeOsHostResponse> response) { |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to retrieve host information. code: " |
| << static_cast<int>(status.error_code()) |
| << ", message: " << status.error_message(); |
| RunQuitClosure(); |
| return; |
| } |
| |
| if (!response->has_chrome_os_host()) { |
| LOG(ERROR) << "Directory response did not include a chrome_os_host value"; |
| RunQuitClosure(); |
| return; |
| } |
| |
| chrome_os_host_.reset(response->release_chrome_os_host()); |
| if (!chrome_os_host_->has_ftl_id()) { |
| LOG(ERROR) << "Directory response did not include an ftl_id value"; |
| RunQuitClosure(); |
| return; |
| } |
| |
| CLIENT_LOG << "Initializing signaling..."; |
| signal_strategy_ = std::make_unique<FtlSignalStrategy>( |
| std::make_unique<PassthroughOAuthTokenGetter>(oauth_token_info_), |
| url_loader_factory_, std::make_unique<FtlClientUuidDeviceIdProvider>()); |
| signal_strategy_->AddListener(this); |
| signal_strategy_->Connect(); |
| } |
| |
| void RemotingClient::StartConnection() { |
| CLIENT_LOG << "Creating transport context..."; |
| webrtc::ThreadWrapper::EnsureForCurrentMessageLoop(); |
| scoped_refptr<protocol::TransportContext> transport_context = |
| new protocol::TransportContext( |
| std::make_unique<protocol::ChromiumPortAllocatorFactory>(), |
| webrtc::ThreadWrapper::current()->SocketServer(), |
| std::make_unique<protocol::IceConfigFetcherDefault>( |
| url_loader_factory_, oauth_token_getter_.get()), |
| protocol::TransportRole::CLIENT); |
| // WebrtcVideoRendererAdapter only supports I420 so we request AV1-Profile0. |
| transport_context->set_preferred_video_format( |
| webrtc::SdpVideoFormat::AV1Profile0()); |
| |
| CLIENT_LOG << "Creating session manager..."; |
| auto protocol_config = protocol::CandidateSessionConfig::CreateDefault(); |
| protocol_config->set_webrtc_supported(true); |
| if (!audio_stream_consumer_) { |
| protocol_config->DisableAudioChannel(); |
| } |
| session_manager_ = |
| std::make_unique<protocol::JingleSessionManager>(signal_strategy_.get()); |
| session_manager_->set_protocol_config(std::move(protocol_config)); |
| |
| CLIENT_LOG << "Creating session..."; |
| auto host_signaling_id = NormalizeSignalingId(chrome_os_host_->ftl_id()); |
| protocol::ClientAuthenticationConfig client_auth_config = {}; |
| client_auth_config.host_id = host_id_; |
| client_auth_config.fetch_secret_callback = base::BindRepeating( |
| [](const std::string& secret, bool pairing_supported, |
| const protocol::SecretFetchedCallback& secret_fetched_callback) { |
| secret_fetched_callback.Run(secret); |
| }, |
| host_secret_); |
| auto session = session_manager_->Connect( |
| SignalingAddress(host_signaling_id), |
| std::make_unique<protocol::NegotiatingClientAuthenticator>( |
| signal_strategy_->GetLocalAddress().id(), host_signaling_id, |
| std::move(client_auth_config))); |
| |
| CLIENT_LOG << "Creating video renderer..."; |
| video_renderer_ = std::make_unique<FrameConsumerWrapper>(frame_consumer_); |
| |
| CLIENT_LOG << "Establishing connection to host..."; |
| connection_ = std::make_unique<protocol::WebrtcConnectionToHost>(); |
| connection_->set_client_stub(this); |
| connection_->set_clipboard_stub(this); |
| connection_->set_video_renderer(video_renderer_.get()); |
| if (audio_stream_consumer_) { |
| connection_->InitializeAudio( |
| /*audio_decode_task_runner=*/base::ThreadPool:: |
| CreateSingleThreadTaskRunner( |
| {base::TaskPriority::HIGHEST}, |
| base::SingleThreadTaskRunnerThreadMode::DEDICATED), |
| audio_stream_consumer_); |
| } |
| connection_->Connect(std::move(session), transport_context, this); |
| protocol::NetworkSettings network_settings{ |
| protocol::NetworkSettings::NAT_TRAVERSAL_FULL}; |
| connection_->ApplyNetworkSettings(network_settings); |
| } |
| |
| void RemotingClient::StopSession() { |
| CLIENT_LOG << "Shutting down connection to host..."; |
| if (connection_) { |
| connection_->Disconnect(ErrorCode::OK); |
| connection_.reset(); |
| } |
| if (signal_strategy_) { |
| // Delay tearing down the signaling channel to increase the likelihood that |
| // the host processes the connection state change. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&SignalStrategy::Disconnect, |
| base::Unretained(signal_strategy_.get())), |
| base::Seconds(2)); |
| } |
| return; |
| } |
| |
| void RemotingClient::AddObserver(ClientStatusObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void RemotingClient::RemoveObserver(ClientStatusObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| base::WeakPtr<RemotingClient> RemotingClient::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void RemotingClient::SetCapabilities( |
| const protocol::Capabilities& capabilities) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::SetPairingResponse( |
| const protocol::PairingResponse& pairing_response) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::DeliverHostMessage( |
| const protocol::ExtensionMessage& message) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::SetVideoLayout(const protocol::VideoLayout& layout) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::SetTransportInfo( |
| const protocol::TransportInfo& transport_info) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::SetActiveDisplay( |
| const protocol::ActiveDisplay& active_display) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::InjectClipboardEvent( |
| const protocol::ClipboardEvent& event) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::SetCursorShape( |
| const protocol::CursorShapeInfo& cursor_shape) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::SetHostCursorPosition( |
| const protocol::HostCursorPosition& position) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::SetKeyboardLayout(const protocol::KeyboardLayout& layout) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void RemotingClient::OnConnectionState(protocol::ConnectionToHost::State state, |
| protocol::ErrorCode error) { |
| CLIENT_LOG << "OnConnectionState change: " |
| << protocol::ConnectionToHost::StateToString(state); |
| if (state == protocol::ConnectionToHost::State::CONNECTED && |
| connection_->host_stub()) { |
| protocol::PeerConnectionParameters peer_connection_params; |
| peer_connection_params.set_preferred_min_bitrate_bps(kMinBitrateBps); |
| connection_->host_stub()->ControlPeerConnection(peer_connection_params); |
| observers_.Notify(&ClientStatusObserver::OnConnected); |
| } else if (state == protocol::ConnectionToHost::State::CLOSED) { |
| StopSession(); |
| observers_.Notify(&ClientStatusObserver::OnDisconnected); |
| } else if (state == protocol::ConnectionToHost::State::FAILED) { |
| StopSession(); |
| observers_.Notify(&ClientStatusObserver::OnConnectionFailed); |
| } |
| } |
| |
| void RemotingClient::OnConnectionReady(bool ready) { |
| CLIENT_LOG << "RemotingClient::OnConnectionReady: " << ready; |
| } |
| |
| void RemotingClient::OnRouteChanged(const std::string& channel_name, |
| const protocol::TransportRoute& route) { |
| CLIENT_LOG << "Using " << protocol::TransportRoute::GetTypeString(route.type) |
| << " connection for " << channel_name << " channel"; |
| } |
| |
| void RemotingClient::OnSignalStrategyStateChange(SignalStrategy::State state) { |
| switch (state) { |
| case SignalStrategy::CONNECTING: |
| CLIENT_LOG << "Signaling channel is being established."; |
| break; |
| case SignalStrategy::CONNECTED: |
| CLIENT_LOG << "Signaling channel has been established for: " |
| << signal_strategy_->GetLocalAddress().id(); |
| StartConnection(); |
| break; |
| case SignalStrategy::DISCONNECTED: |
| auto error = signal_strategy_->GetError(); |
| auto error_code = ErrorCode::OK; |
| if (error != SignalStrategy::Error::OK) { |
| LOG(ERROR) << "Signaling channel has been closed due to error: " |
| << error; |
| // TODO: joedow - Map error to error_code. |
| } else { |
| CLIENT_LOG << "Signaling channel has been closed."; |
| } |
| if (connection_) { |
| connection_->Disconnect(error_code); |
| } |
| RunQuitClosure(); |
| break; |
| } |
| } |
| |
| bool RemotingClient::OnSignalStrategyIncomingStanza( |
| const jingle_xmpp::XmlElement* stanza) { |
| return false; |
| } |
| |
| void RemotingClient::RunQuitClosure() { |
| if (quit_closure_) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(quit_closure_)); |
| } |
| } |
| |
| } // namespace remoting |