| // 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/client/plugin/chromoting_instance.h" |
| |
| #include <nacl_io/nacl_io.h> |
| #include <sys/mount.h> |
| |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/lazy_instance.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/synchronization/lock.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/threading/thread.h" |
| #include "base/values.h" |
| #include "crypto/random.h" |
| #include "jingle/glue/thread_wrapper.h" |
| #include "net/socket/ssl_server_socket.h" |
| #include "ppapi/cpp/completion_callback.h" |
| #include "ppapi/cpp/dev/url_util_dev.h" |
| #include "ppapi/cpp/image_data.h" |
| #include "ppapi/cpp/input_event.h" |
| #include "ppapi/cpp/private/uma_private.h" |
| #include "ppapi/cpp/rect.h" |
| #include "ppapi/cpp/var_array_buffer.h" |
| #include "ppapi/cpp/var_dictionary.h" |
| #include "remoting/base/constants.h" |
| #include "remoting/base/util.h" |
| #include "remoting/client/chromoting_client.h" |
| #include "remoting/client/normalizing_input_filter_cros.h" |
| #include "remoting/client/normalizing_input_filter_mac.h" |
| #include "remoting/client/normalizing_input_filter_win.h" |
| #include "remoting/client/plugin/delegating_signal_strategy.h" |
| #include "remoting/client/plugin/pepper_audio_player.h" |
| #include "remoting/client/plugin/pepper_main_thread_task_runner.h" |
| #include "remoting/client/plugin/pepper_mouse_locker.h" |
| #include "remoting/client/plugin/pepper_port_allocator_factory.h" |
| #include "remoting/client/plugin/pepper_url_request.h" |
| #include "remoting/client/plugin/pepper_video_renderer_2d.h" |
| #include "remoting/client/plugin/pepper_video_renderer_3d.h" |
| #include "remoting/client/software_video_renderer.h" |
| #include "remoting/proto/control.pb.h" |
| #include "remoting/protocol/connection_to_host.h" |
| #include "remoting/protocol/host_stub.h" |
| #include "remoting/protocol/transport_context.h" |
| #include "third_party/webrtc/base/helpers.h" |
| #include "third_party/webrtc/modules/desktop_capture/desktop_region.h" |
| #include "url/gurl.h" |
| |
| namespace remoting { |
| |
| namespace { |
| |
| // Default DPI to assume for old clients that use notifyClientResolution. |
| const int kDefaultDPI = 96; |
| |
| // Size of the random seed blob used to initialize RNG in libjingle. OpenSSL |
| // needs at least 32 bytes of entropy (see |
| // http://wiki.openssl.org/index.php/Random_Numbers), but stores 1039 bytes of |
| // state, so we initialize it with 1k or random data. |
| const int kRandomSeedSize = 1024; |
| |
| // The connection times and duration values are stored in UMA custom-time |
| // histograms, that are log-scaled by default. The histogram specifications are |
| // based off values seen over a recent 7-day period. |
| // The connection times histograms are in milliseconds and the connection |
| // duration histograms are in minutes. |
| const char kTimeToAuthenticateHistogram[] = |
| "Chromoting.Connections.Times.ToAuthenticate"; |
| const char kTimeToConnectHistogram[] = "Chromoting.Connections.Times.ToConnect"; |
| const char kClosedSessionDurationHistogram[] = |
| "Chromoting.Connections.Durations.Closed"; |
| const char kFailedSessionDurationHistogram[] = |
| "Chromoting.Connections.Durations.Failed"; |
| const int kConnectionTimesHistogramMinMs = 1; |
| const int kConnectionTimesHistogramMaxMs = 30000; |
| const int kConnectionTimesHistogramBuckets = 50; |
| const int kConnectionDurationHistogramMinMinutes = 1; |
| const int kConnectionDurationHistogramMaxMinutes = 24 * 60; |
| const int kConnectionDurationHistogramBuckets = 50; |
| |
| // Input event latency is expected to be below 10ms. |
| const char kInputEventLatencyHistogram[] = "Chromoting.Input.EventLatency"; |
| const int kInputEventLatencyHistogramMinUs = 1; |
| const int kInputEventLatencyHistogramMaxUs = 10000; |
| const int kInputEventLatencyHistogramBuckets = 50; |
| |
| // Update perf stats in the UI every second. |
| const int kUIStatsUpdatePeriodSeconds = 1; |
| |
| // TODO(sergeyu): Ideally we should just pass ErrorCode to the webapp |
| // and let it handle it, but it would be hard to fix it now because |
| // client plugin and webapp versions may not be in sync. It should be |
| // easy to do after we are finished moving the client plugin to NaCl. |
| std::string ConnectionErrorToString(protocol::ErrorCode error) { |
| // Values returned by this function must match the |
| // remoting.ClientSession.Error enum in JS code. |
| switch (error) { |
| case protocol::OK: |
| return "NONE"; |
| |
| case protocol::PEER_IS_OFFLINE: |
| return "HOST_IS_OFFLINE"; |
| |
| case protocol::SESSION_REJECTED: |
| case protocol::AUTHENTICATION_FAILED: |
| return "SESSION_REJECTED"; |
| |
| case protocol::INVALID_ACCOUNT: |
| return "INVALID_ACCOUNT"; |
| |
| case protocol::INCOMPATIBLE_PROTOCOL: |
| return "INCOMPATIBLE_PROTOCOL"; |
| |
| case protocol::HOST_OVERLOAD: |
| return "HOST_OVERLOAD"; |
| |
| case protocol::MAX_SESSION_LENGTH: |
| return "MAX_SESSION_LENGTH"; |
| |
| case protocol::HOST_CONFIGURATION_ERROR: |
| return "HOST_CONFIGURATION_ERROR"; |
| |
| case protocol::CHANNEL_CONNECTION_ERROR: |
| case protocol::SIGNALING_ERROR: |
| case protocol::SIGNALING_TIMEOUT: |
| case protocol::UNKNOWN_ERROR: |
| return "NETWORK_FAILURE"; |
| } |
| DLOG(FATAL) << "Unknown error code" << error; |
| return std::string(); |
| } |
| |
| PP_Instance g_logging_instance = 0; |
| base::LazyInstance<base::Lock>::Leaky g_logging_lock = |
| LAZY_INSTANCE_INITIALIZER; |
| |
| } // namespace |
| |
| ChromotingInstance::ChromotingInstance(PP_Instance pp_instance) |
| : pp::Instance(pp_instance), |
| initialized_(false), |
| plugin_task_runner_(new PepperMainThreadTaskRunner()), |
| context_(plugin_task_runner_.get()), |
| touch_input_scaler_(&mouse_input_filter_), |
| key_mapper_(&touch_input_scaler_), |
| input_handler_(&input_tracker_), |
| cursor_setter_(this), |
| empty_cursor_filter_(&cursor_setter_), |
| text_input_controller_(this), |
| use_async_pin_dialog_(false), |
| weak_factory_(this) { |
| // In NaCl global resources need to be initialized differently because they |
| // are not shared with Chrome. |
| thread_task_runner_handle_.reset( |
| new base::ThreadTaskRunnerHandle(plugin_task_runner_)); |
| thread_wrapper_ = |
| jingle_glue::JingleThreadWrapper::WrapTaskRunner(plugin_task_runner_); |
| |
| // Register a global log handler. |
| ChromotingInstance::RegisterLogMessageHandler(); |
| |
| nacl_io_init_ppapi(pp_instance, pp::Module::Get()->get_browser_interface()); |
| mount("", "/etc", "memfs", 0, ""); |
| mount("", "/usr", "memfs", 0, ""); |
| |
| // Register for mouse, wheel and keyboard events. |
| RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE | PP_INPUTEVENT_CLASS_WHEEL); |
| RequestFilteringInputEvents(PP_INPUTEVENT_CLASS_KEYBOARD); |
| |
| // Disable the client-side IME in Chrome. |
| text_input_controller_.SetTextInputType(PP_TEXTINPUT_TYPE_NONE); |
| |
| // Resister this instance to handle debug log messsages. |
| RegisterLoggingInstance(); |
| |
| // Initialize random seed for libjingle. It's necessary only with OpenSSL. |
| char random_seed[kRandomSeedSize]; |
| crypto::RandBytes(random_seed, sizeof(random_seed)); |
| rtc::InitRandom(random_seed, sizeof(random_seed)); |
| |
| // Send hello message. |
| PostLegacyJsonMessage("hello", base::MakeUnique<base::DictionaryValue>()); |
| } |
| |
| ChromotingInstance::~ChromotingInstance() { |
| DCHECK(plugin_task_runner_->BelongsToCurrentThread()); |
| |
| // Disconnect the client. |
| Disconnect(); |
| |
| // Unregister this instance so that debug log messages will no longer be sent |
| // to it. This will stop all logging in all Chromoting instances. |
| UnregisterLoggingInstance(); |
| |
| // Stopping the context shuts down all chromoting threads. |
| context_.Stop(); |
| } |
| |
| bool ChromotingInstance::Init(uint32_t argc, |
| const char* argn[], |
| const char* argv[]) { |
| CHECK(!initialized_); |
| initialized_ = true; |
| |
| VLOG(1) << "Started ChromotingInstance::Init"; |
| |
| // Start all the threads. |
| context_.Start(); |
| |
| return true; |
| } |
| |
| void ChromotingInstance::HandleMessage(const pp::Var& message) { |
| if (!message.is_string()) { |
| LOG(ERROR) << "Received a message that is not a string."; |
| return; |
| } |
| |
| std::unique_ptr<base::Value> json = base::JSONReader::Read( |
| message.AsString(), base::JSON_ALLOW_TRAILING_COMMAS); |
| base::DictionaryValue* message_dict = nullptr; |
| std::string method; |
| base::DictionaryValue* data = nullptr; |
| if (!json.get() || |
| !json->GetAsDictionary(&message_dict) || |
| !message_dict->GetString("method", &method) || |
| !message_dict->GetDictionary("data", &data)) { |
| LOG(ERROR) << "Received invalid message:" << message.AsString(); |
| return; |
| } |
| |
| if (method == "connect") { |
| HandleConnect(*data); |
| } else if (method == "disconnect") { |
| HandleDisconnect(*data); |
| } else if (method == "incomingIq") { |
| HandleOnIncomingIq(*data); |
| } else if (method == "releaseAllKeys") { |
| HandleReleaseAllKeys(*data); |
| } else if (method == "injectKeyEvent") { |
| HandleInjectKeyEvent(*data); |
| } else if (method == "remapKey") { |
| HandleRemapKey(*data); |
| } else if (method == "trapKey") { |
| HandleTrapKey(*data); |
| } else if (method == "sendClipboardItem") { |
| HandleSendClipboardItem(*data); |
| } else if (method == "notifyClientResolution") { |
| HandleNotifyClientResolution(*data); |
| } else if (method == "videoControl") { |
| HandleVideoControl(*data); |
| } else if (method == "pauseAudio") { |
| HandlePauseAudio(*data); |
| } else if (method == "useAsyncPinDialog") { |
| use_async_pin_dialog_ = true; |
| } else if (method == "onPinFetched") { |
| HandleOnPinFetched(*data); |
| } else if (method == "onThirdPartyTokenFetched") { |
| HandleOnThirdPartyTokenFetched(*data); |
| } else if (method == "requestPairing") { |
| HandleRequestPairing(*data); |
| } else if (method == "extensionMessage") { |
| HandleExtensionMessage(*data); |
| } else if (method == "allowMouseLock") { |
| HandleAllowMouseLockMessage(); |
| } else if (method == "sendMouseInputWhenUnfocused") { |
| HandleSendMouseInputWhenUnfocused(); |
| } else if (method == "delegateLargeCursors") { |
| HandleDelegateLargeCursors(); |
| } else if (method == "enableDebugRegion") { |
| HandleEnableDebugRegion(*data); |
| } else if (method == "enableTouchEvents") { |
| HandleEnableTouchEvents(*data); |
| } |
| } |
| |
| void ChromotingInstance::DidChangeFocus(bool has_focus) { |
| DCHECK(plugin_task_runner_->BelongsToCurrentThread()); |
| |
| if (!IsConnected()) |
| return; |
| |
| input_handler_.DidChangeFocus(has_focus); |
| if (mouse_locker_) |
| mouse_locker_->DidChangeFocus(has_focus); |
| } |
| |
| void ChromotingInstance::DidChangeView(const pp::View& view) { |
| DCHECK(plugin_task_runner_->BelongsToCurrentThread()); |
| |
| plugin_view_ = view; |
| webrtc::DesktopSize size( |
| webrtc::DesktopSize(view.GetRect().width(), view.GetRect().height())); |
| mouse_input_filter_.set_input_size(size); |
| touch_input_scaler_.set_input_size(size); |
| |
| if (video_renderer_) |
| video_renderer_->OnViewChanged(view); |
| } |
| |
| bool ChromotingInstance::HandleInputEvent(const pp::InputEvent& event) { |
| DCHECK(plugin_task_runner_->BelongsToCurrentThread()); |
| |
| if (!IsConnected()) |
| return false; |
| |
| PP_TimeTicks latency = |
| pp::Module::Get()->core()->GetTimeTicks() - event.GetTimeStamp(); |
| pp::UMAPrivate uma(this); |
| uma.HistogramCustomTimes( |
| kInputEventLatencyHistogram, static_cast<int64_t>(latency * 1000000), |
| kInputEventLatencyHistogramMinUs, kInputEventLatencyHistogramMaxUs, |
| kInputEventLatencyHistogramBuckets); |
| |
| return input_handler_.HandleInputEvent(event); |
| } |
| |
| void ChromotingInstance::OnVideoDecodeError() { |
| Disconnect(); |
| |
| // Assume that the decoder failure was caused by the host not encoding video |
| // correctly and report it as a protocol error. |
| // TODO(sergeyu): Consider using a different error code in case the decoder |
| // error was caused by some other problem. |
| OnConnectionState(protocol::ConnectionToHost::FAILED, |
| protocol::INCOMPATIBLE_PROTOCOL); |
| } |
| |
| void ChromotingInstance::OnVideoFirstFrameReceived() { |
| PostLegacyJsonMessage("onFirstFrameReceived", |
| base::MakeUnique<base::DictionaryValue>()); |
| } |
| |
| void ChromotingInstance::OnVideoFrameDirtyRegion( |
| const webrtc::DesktopRegion& dirty_region) { |
| std::unique_ptr<base::ListValue> rects_value(new base::ListValue()); |
| for (webrtc::DesktopRegion::Iterator i(dirty_region); !i.IsAtEnd(); |
| i.Advance()) { |
| const webrtc::DesktopRect& rect = i.rect(); |
| std::unique_ptr<base::ListValue> rect_value(new base::ListValue()); |
| rect_value->AppendInteger(rect.left()); |
| rect_value->AppendInteger(rect.top()); |
| rect_value->AppendInteger(rect.width()); |
| rect_value->AppendInteger(rect.height()); |
| rects_value->Append(std::move(rect_value)); |
| } |
| |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->Set("rects", rects_value.release()); |
| PostLegacyJsonMessage("onDebugRegion", std::move(data)); |
| } |
| |
| void ChromotingInstance::OnConnectionState( |
| protocol::ConnectionToHost::State state, |
| protocol::ErrorCode error) { |
| pp::UMAPrivate uma(this); |
| |
| switch (state) { |
| case protocol::ConnectionToHost::INITIALIZING: |
| NOTREACHED(); |
| break; |
| case protocol::ConnectionToHost::CONNECTING: |
| connection_started_time = base::TimeTicks::Now(); |
| break; |
| case protocol::ConnectionToHost::AUTHENTICATED: |
| connection_authenticated_time_ = base::TimeTicks::Now(); |
| uma.HistogramCustomTimes( |
| kTimeToAuthenticateHistogram, |
| (connection_authenticated_time_ - connection_started_time) |
| .InMilliseconds(), |
| kConnectionTimesHistogramMinMs, kConnectionTimesHistogramMaxMs, |
| kConnectionTimesHistogramBuckets); |
| break; |
| case protocol::ConnectionToHost::CONNECTED: |
| connection_connected_time_ = base::TimeTicks::Now(); |
| uma.HistogramCustomTimes( |
| kTimeToConnectHistogram, |
| (connection_connected_time_ - connection_authenticated_time_) |
| .InMilliseconds(), |
| kConnectionTimesHistogramMinMs, kConnectionTimesHistogramMaxMs, |
| kConnectionTimesHistogramBuckets); |
| break; |
| case protocol::ConnectionToHost::CLOSED: |
| if (!connection_connected_time_.is_null()) { |
| uma.HistogramCustomTimes( |
| kClosedSessionDurationHistogram, |
| (base::TimeTicks::Now() - connection_connected_time_) |
| .InMilliseconds(), |
| kConnectionDurationHistogramMinMinutes, |
| kConnectionDurationHistogramMaxMinutes, |
| kConnectionDurationHistogramBuckets); |
| } |
| break; |
| case protocol::ConnectionToHost::FAILED: |
| if (!connection_connected_time_.is_null()) { |
| uma.HistogramCustomTimes( |
| kFailedSessionDurationHistogram, |
| (base::TimeTicks::Now() - connection_connected_time_) |
| .InMilliseconds(), |
| kConnectionDurationHistogramMinMinutes, |
| kConnectionDurationHistogramMaxMinutes, |
| kConnectionDurationHistogramBuckets); |
| } |
| break; |
| } |
| |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetString("state", protocol::ConnectionToHost::StateToString(state)); |
| data->SetString("error", ConnectionErrorToString(error)); |
| PostLegacyJsonMessage("onConnectionStatus", std::move(data)); |
| } |
| |
| void ChromotingInstance::FetchThirdPartyToken( |
| const std::string& host_public_key, |
| const std::string& token_url, |
| const std::string& scope, |
| const protocol::ThirdPartyTokenFetchedCallback& token_fetched_callback) { |
| // Once the Session object calls this function, it won't continue the |
| // authentication until the callback is called (or connection is canceled). |
| // So, it's impossible to reach this with a callback already registered. |
| DCHECK(third_party_token_fetched_callback_.is_null()); |
| third_party_token_fetched_callback_ = token_fetched_callback; |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetString("tokenUrl", token_url); |
| data->SetString("hostPublicKey", host_public_key); |
| data->SetString("scope", scope); |
| PostLegacyJsonMessage("fetchThirdPartyToken", std::move(data)); |
| } |
| |
| void ChromotingInstance::OnConnectionReady(bool ready) { |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetBoolean("ready", ready); |
| PostLegacyJsonMessage("onConnectionReady", std::move(data)); |
| } |
| |
| void ChromotingInstance::OnRouteChanged(const std::string& channel_name, |
| const protocol::TransportRoute& route) { |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetString("channel", channel_name); |
| data->SetString("connectionType", |
| protocol::TransportRoute::GetTypeString(route.type)); |
| PostLegacyJsonMessage("onRouteChanged", std::move(data)); |
| } |
| |
| void ChromotingInstance::SetCapabilities(const std::string& capabilities) { |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetString("capabilities", capabilities); |
| PostLegacyJsonMessage("setCapabilities", std::move(data)); |
| } |
| |
| void ChromotingInstance::SetPairingResponse( |
| const protocol::PairingResponse& pairing_response) { |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetString("clientId", pairing_response.client_id()); |
| data->SetString("sharedSecret", pairing_response.shared_secret()); |
| PostLegacyJsonMessage("pairingResponse", std::move(data)); |
| } |
| |
| void ChromotingInstance::DeliverHostMessage( |
| const protocol::ExtensionMessage& message) { |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetString("type", message.type()); |
| data->SetString("data", message.data()); |
| PostLegacyJsonMessage("extensionMessage", std::move(data)); |
| } |
| |
| void ChromotingInstance::SetDesktopSize(const webrtc::DesktopSize& size, |
| const webrtc::DesktopVector& dpi) { |
| DCHECK(!dpi.is_zero()); |
| |
| mouse_input_filter_.set_output_size(size); |
| touch_input_scaler_.set_output_size(size); |
| |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetInteger("width", size.width()); |
| data->SetInteger("height", size.height()); |
| data->SetInteger("x_dpi", dpi.x()); |
| data->SetInteger("y_dpi", dpi.y()); |
| PostLegacyJsonMessage("onDesktopSize", std::move(data)); |
| } |
| |
| void ChromotingInstance::FetchSecretFromDialog( |
| bool pairing_supported, |
| const protocol::SecretFetchedCallback& secret_fetched_callback) { |
| // Once the Session object calls this function, it won't continue the |
| // authentication until the callback is called (or connection is canceled). |
| // So, it's impossible to reach this with a callback already registered. |
| DCHECK(secret_fetched_callback_.is_null()); |
| secret_fetched_callback_ = secret_fetched_callback; |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetBoolean("pairingSupported", pairing_supported); |
| PostLegacyJsonMessage("fetchPin", std::move(data)); |
| } |
| |
| void ChromotingInstance::FetchSecretFromString( |
| const std::string& shared_secret, |
| bool pairing_supported, |
| const protocol::SecretFetchedCallback& secret_fetched_callback) { |
| secret_fetched_callback.Run(shared_secret); |
| } |
| |
| protocol::ClipboardStub* ChromotingInstance::GetClipboardStub() { |
| // TODO(sergeyu): Move clipboard handling to a separate class. |
| // crbug.com/138108 |
| return this; |
| } |
| |
| protocol::CursorShapeStub* ChromotingInstance::GetCursorShapeStub() { |
| return &empty_cursor_filter_; |
| } |
| |
| void ChromotingInstance::InjectClipboardEvent( |
| const protocol::ClipboardEvent& event) { |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetString("mimeType", event.mime_type()); |
| data->SetString("item", event.data()); |
| PostLegacyJsonMessage("injectClipboardItem", std::move(data)); |
| } |
| |
| void ChromotingInstance::SetCursorShape( |
| const protocol::CursorShapeInfo& cursor_shape) { |
| // If the delegated cursor is empty then stop rendering a DOM cursor. |
| if (IsCursorShapeEmpty(cursor_shape)) { |
| PostChromotingMessage("unsetCursorShape", pp::VarDictionary()); |
| return; |
| } |
| |
| // Cursor is not empty, so pass it to JS to render. |
| const int kBytesPerPixel = sizeof(uint32_t); |
| const size_t buffer_size = |
| cursor_shape.height() * cursor_shape.width() * kBytesPerPixel; |
| |
| pp::VarArrayBuffer array_buffer(buffer_size); |
| void* dst = array_buffer.Map(); |
| memcpy(dst, cursor_shape.data().data(), buffer_size); |
| array_buffer.Unmap(); |
| |
| pp::VarDictionary dictionary; |
| dictionary.Set(pp::Var("width"), cursor_shape.width()); |
| dictionary.Set(pp::Var("height"), cursor_shape.height()); |
| dictionary.Set(pp::Var("hotspotX"), cursor_shape.hotspot_x()); |
| dictionary.Set(pp::Var("hotspotY"), cursor_shape.hotspot_y()); |
| dictionary.Set(pp::Var("data"), array_buffer); |
| PostChromotingMessage("setCursorShape", dictionary); |
| } |
| |
| void ChromotingInstance::HandleConnect(const base::DictionaryValue& data) { |
| protocol::ClientAuthenticationConfig client_auth_config; |
| |
| std::string local_jid; |
| std::string host_jid; |
| std::string host_public_key; |
| if (!data.GetString("hostJid", &host_jid) || |
| !data.GetString("hostPublicKey", &host_public_key) || |
| !data.GetString("localJid", &local_jid) || |
| !data.GetString("hostId", &client_auth_config.host_id)) { |
| LOG(ERROR) << "Invalid connect() data."; |
| return; |
| } |
| |
| data.GetString("clientPairingId", &client_auth_config.pairing_client_id); |
| data.GetString("clientPairedSecret", &client_auth_config.pairing_secret); |
| |
| if (use_async_pin_dialog_) { |
| client_auth_config.fetch_secret_callback = base::Bind( |
| &ChromotingInstance::FetchSecretFromDialog, weak_factory_.GetWeakPtr()); |
| } else { |
| std::string shared_secret; |
| if (!data.GetString("sharedSecret", &shared_secret)) { |
| LOG(ERROR) << "sharedSecret not specified in connect()."; |
| return; |
| } |
| client_auth_config.fetch_secret_callback = |
| base::Bind(&ChromotingInstance::FetchSecretFromString, shared_secret); |
| } |
| |
| client_auth_config.fetch_third_party_token_callback = |
| base::Bind(&ChromotingInstance::FetchThirdPartyToken, |
| weak_factory_.GetWeakPtr(), host_public_key); |
| |
| // Read the list of capabilities, if any. |
| std::string capabilities; |
| if (data.HasKey("capabilities")) { |
| if (!data.GetString("capabilities", &capabilities)) { |
| LOG(ERROR) << "Invalid connect() data."; |
| return; |
| } |
| } |
| |
| // Read and parse list of experiments. |
| std::string experiments; |
| std::vector<std::string> experiments_list; |
| if (data.GetString("experiments", &experiments)) { |
| experiments_list = base::SplitString( |
| experiments, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
| } |
| |
| VLOG(0) << "Connecting to " << host_jid |
| << ". Local jid: " << local_jid << "."; |
| |
| std::string key_filter; |
| if (!data.GetString("keyFilter", &key_filter)) { |
| NOTREACHED(); |
| normalizing_input_filter_.reset(new protocol::InputFilter(&key_mapper_)); |
| } else if (key_filter == "mac") { |
| normalizing_input_filter_.reset( |
| new NormalizingInputFilterMac(&key_mapper_)); |
| } else if (key_filter == "cros") { |
| normalizing_input_filter_.reset( |
| new NormalizingInputFilterCros(&key_mapper_)); |
| } else if (key_filter == "windows") { |
| normalizing_input_filter_.reset( |
| new NormalizingInputFilterWin(&key_mapper_)); |
| } else { |
| DCHECK(key_filter.empty()); |
| normalizing_input_filter_.reset(new protocol::InputFilter(&key_mapper_)); |
| } |
| input_tracker_.set_input_stub(normalizing_input_filter_.get()); |
| |
| // Try initializing 3D video renderer. |
| video_renderer_.reset(new PepperVideoRenderer3D()); |
| video_renderer_->SetPepperContext(this, this); |
| if (!video_renderer_->Initialize(context_, &perf_tracker_)) { |
| video_renderer_.reset(); |
| } |
| |
| // If we didn't initialize 3D renderer then use the 2D renderer. |
| if (!video_renderer_) { |
| LOG(WARNING) |
| << "Failed to initialize 3D renderer. Using 2D renderer instead."; |
| video_renderer_.reset(new PepperVideoRenderer2D()); |
| video_renderer_->SetPepperContext(this, this); |
| if (!video_renderer_->Initialize(context_, &perf_tracker_)) { |
| video_renderer_.reset(); |
| } |
| } |
| |
| CHECK(video_renderer_); |
| |
| perf_tracker_.SetUpdateUmaCallbacks( |
| base::Bind(&ChromotingInstance::UpdateUmaCustomHistogram, |
| weak_factory_.GetWeakPtr(), true), |
| base::Bind(&ChromotingInstance::UpdateUmaCustomHistogram, |
| weak_factory_.GetWeakPtr(), false), |
| base::Bind(&ChromotingInstance::UpdateUmaEnumHistogram, |
| weak_factory_.GetWeakPtr())); |
| |
| if (!plugin_view_.is_null()) |
| video_renderer_->OnViewChanged(plugin_view_); |
| |
| if (!audio_player_) { |
| audio_player_.reset(new PepperAudioPlayer(this)); |
| } |
| |
| client_.reset(new ChromotingClient(&context_, this, video_renderer_.get(), |
| audio_player_->GetWeakPtr())); |
| |
| // Setup the signal strategy. |
| signal_strategy_.reset(new DelegatingSignalStrategy( |
| local_jid, base::Bind(&ChromotingInstance::SendOutgoingIq, |
| weak_factory_.GetWeakPtr()))); |
| |
| // Create TransportContext. |
| scoped_refptr<protocol::TransportContext> transport_context( |
| new protocol::TransportContext( |
| signal_strategy_.get(), |
| base::MakeUnique<PepperPortAllocatorFactory>(this), |
| base::MakeUnique<PepperUrlRequestFactory>(this), |
| protocol::NetworkSettings( |
| protocol::NetworkSettings::NAT_TRAVERSAL_FULL), |
| protocol::TransportRole::CLIENT)); |
| |
| std::unique_ptr<protocol::CandidateSessionConfig> config = |
| protocol::CandidateSessionConfig::CreateDefault(); |
| if (std::find(experiments_list.begin(), experiments_list.end(), "vp9") != |
| experiments_list.end()) { |
| config->set_vp9_experiment_enabled(true); |
| } |
| client_->set_protocol_config(std::move(config)); |
| |
| // Kick off the connection. |
| client_->Start(signal_strategy_.get(), client_auth_config, transport_context, |
| host_jid, capabilities); |
| |
| // Connect the input pipeline to the protocol stub. |
| mouse_input_filter_.set_input_stub(client_->input_stub()); |
| if (!plugin_view_.is_null()) { |
| webrtc::DesktopSize size(plugin_view_.GetRect().width(), |
| plugin_view_.GetRect().height()); |
| mouse_input_filter_.set_input_size(size); |
| touch_input_scaler_.set_input_size(size); |
| } |
| |
| // Start timer that periodically sends perf stats. |
| stats_update_timer_.Start( |
| FROM_HERE, base::TimeDelta::FromSeconds(kUIStatsUpdatePeriodSeconds), |
| base::Bind(&ChromotingInstance::UpdatePerfStatsInUI, |
| base::Unretained(this))); |
| } |
| |
| void ChromotingInstance::HandleDisconnect(const base::DictionaryValue& data) { |
| DCHECK(plugin_task_runner_->BelongsToCurrentThread()); |
| Disconnect(); |
| } |
| |
| void ChromotingInstance::HandleOnIncomingIq(const base::DictionaryValue& data) { |
| std::string iq; |
| if (!data.GetString("iq", &iq)) { |
| LOG(ERROR) << "Invalid incomingIq() data."; |
| return; |
| } |
| |
| // Just ignore the message if it's received before Connect() is called. It's |
| // likely to be a leftover from a previous session, so it's safe to ignore it. |
| if (signal_strategy_) |
| signal_strategy_->OnIncomingMessage(iq); |
| } |
| |
| void ChromotingInstance::HandleReleaseAllKeys( |
| const base::DictionaryValue& data) { |
| if (IsConnected()) |
| input_tracker_.ReleaseAll(); |
| } |
| |
| void ChromotingInstance::HandleInjectKeyEvent( |
| const base::DictionaryValue& data) { |
| int usb_keycode = 0; |
| bool is_pressed = false; |
| if (!data.GetInteger("usbKeycode", &usb_keycode) || |
| !data.GetBoolean("pressed", &is_pressed)) { |
| LOG(ERROR) << "Invalid injectKeyEvent."; |
| return; |
| } |
| |
| protocol::KeyEvent event; |
| event.set_usb_keycode(usb_keycode); |
| event.set_pressed(is_pressed); |
| |
| // Inject after the KeyEventMapper, so the event won't get mapped or trapped. |
| if (IsConnected()) |
| touch_input_scaler_.InjectKeyEvent(event); |
| } |
| |
| void ChromotingInstance::HandleRemapKey(const base::DictionaryValue& data) { |
| int from_keycode = 0; |
| int to_keycode = 0; |
| if (!data.GetInteger("fromKeycode", &from_keycode) || |
| !data.GetInteger("toKeycode", &to_keycode)) { |
| LOG(ERROR) << "Invalid remapKey."; |
| return; |
| } |
| |
| key_mapper_.RemapKey(from_keycode, to_keycode); |
| } |
| |
| void ChromotingInstance::HandleTrapKey(const base::DictionaryValue& data) { |
| int keycode = 0; |
| bool trap = false; |
| if (!data.GetInteger("keycode", &keycode) || |
| !data.GetBoolean("trap", &trap)) { |
| LOG(ERROR) << "Invalid trapKey."; |
| return; |
| } |
| |
| key_mapper_.TrapKey(keycode, trap); |
| } |
| |
| void ChromotingInstance::HandleSendClipboardItem( |
| const base::DictionaryValue& data) { |
| std::string mime_type; |
| std::string item; |
| if (!data.GetString("mimeType", &mime_type) || |
| !data.GetString("item", &item)) { |
| LOG(ERROR) << "Invalid sendClipboardItem data."; |
| return; |
| } |
| if (!IsConnected()) { |
| return; |
| } |
| protocol::ClipboardEvent event; |
| event.set_mime_type(mime_type); |
| event.set_data(item); |
| client_->clipboard_forwarder()->InjectClipboardEvent(event); |
| } |
| |
| void ChromotingInstance::HandleNotifyClientResolution( |
| const base::DictionaryValue& data) { |
| int width = 0; |
| int height = 0; |
| int x_dpi = kDefaultDPI; |
| int y_dpi = kDefaultDPI; |
| if (!data.GetInteger("width", &width) || |
| !data.GetInteger("height", &height) || |
| !data.GetInteger("x_dpi", &x_dpi) || |
| !data.GetInteger("y_dpi", &y_dpi) || |
| width <= 0 || height <= 0 || |
| x_dpi <= 0 || y_dpi <= 0) { |
| LOG(ERROR) << "Invalid notifyClientResolution."; |
| return; |
| } |
| |
| if (!IsConnected()) { |
| return; |
| } |
| |
| protocol::ClientResolution client_resolution; |
| client_resolution.set_x_dpi(x_dpi); |
| client_resolution.set_y_dpi(y_dpi); |
| client_resolution.set_dips_width((width * kDefaultDPI) / x_dpi); |
| client_resolution.set_dips_height((height * kDefaultDPI) / y_dpi); |
| |
| // Include the legacy width & height in physical pixels for use by older |
| // hosts. |
| client_resolution.set_width_deprecated(width); |
| client_resolution.set_height_deprecated(height); |
| |
| client_->host_stub()->NotifyClientResolution(client_resolution); |
| } |
| |
| void ChromotingInstance::HandleVideoControl(const base::DictionaryValue& data) { |
| protocol::VideoControl video_control; |
| bool pause_video = false; |
| if (data.GetBoolean("pause", &pause_video)) { |
| video_control.set_enable(!pause_video); |
| perf_tracker_.OnPauseStateChanged(pause_video); |
| } |
| bool lossless_encode = false; |
| if (data.GetBoolean("losslessEncode", &lossless_encode)) { |
| video_control.set_lossless_encode(lossless_encode); |
| } |
| bool lossless_color = false; |
| if (data.GetBoolean("losslessColor", &lossless_color)) { |
| video_control.set_lossless_color(lossless_color); |
| } |
| if (!IsConnected()) { |
| return; |
| } |
| client_->host_stub()->ControlVideo(video_control); |
| } |
| |
| void ChromotingInstance::HandlePauseAudio(const base::DictionaryValue& data) { |
| bool pause = false; |
| if (!data.GetBoolean("pause", &pause)) { |
| LOG(ERROR) << "Invalid pauseAudio."; |
| return; |
| } |
| if (!IsConnected()) { |
| return; |
| } |
| protocol::AudioControl audio_control; |
| audio_control.set_enable(!pause); |
| client_->host_stub()->ControlAudio(audio_control); |
| } |
| void ChromotingInstance::HandleOnPinFetched(const base::DictionaryValue& data) { |
| std::string pin; |
| if (!data.GetString("pin", &pin)) { |
| LOG(ERROR) << "Invalid onPinFetched."; |
| return; |
| } |
| if (!secret_fetched_callback_.is_null()) { |
| base::ResetAndReturn(&secret_fetched_callback_).Run(pin); |
| } else { |
| LOG(WARNING) << "Ignored OnPinFetched received without a pending fetch."; |
| } |
| } |
| |
| void ChromotingInstance::HandleOnThirdPartyTokenFetched( |
| const base::DictionaryValue& data) { |
| std::string token; |
| std::string shared_secret; |
| if (!data.GetString("token", &token) || |
| !data.GetString("sharedSecret", &shared_secret)) { |
| LOG(ERROR) << "Invalid onThirdPartyTokenFetched data."; |
| return; |
| } |
| if (!third_party_token_fetched_callback_.is_null()) { |
| base::ResetAndReturn(&third_party_token_fetched_callback_) |
| .Run(token, shared_secret); |
| } else { |
| LOG(WARNING) << "Ignored OnThirdPartyTokenFetched without a pending fetch."; |
| } |
| } |
| |
| void ChromotingInstance::HandleRequestPairing( |
| const base::DictionaryValue& data) { |
| std::string client_name; |
| if (!data.GetString("clientName", &client_name)) { |
| LOG(ERROR) << "Invalid requestPairing"; |
| return; |
| } |
| if (!IsConnected()) { |
| return; |
| } |
| protocol::PairingRequest pairing_request; |
| pairing_request.set_client_name(client_name); |
| client_->host_stub()->RequestPairing(pairing_request); |
| } |
| |
| void ChromotingInstance::HandleExtensionMessage( |
| const base::DictionaryValue& data) { |
| std::string type; |
| std::string message_data; |
| if (!data.GetString("type", &type) || |
| !data.GetString("data", &message_data)) { |
| LOG(ERROR) << "Invalid extensionMessage."; |
| return; |
| } |
| if (!IsConnected()) { |
| return; |
| } |
| protocol::ExtensionMessage message; |
| message.set_type(type); |
| message.set_data(message_data); |
| client_->host_stub()->DeliverClientMessage(message); |
| } |
| |
| void ChromotingInstance::HandleAllowMouseLockMessage() { |
| // Create the mouse lock handler and route cursor shape messages through it. |
| mouse_locker_.reset(new PepperMouseLocker( |
| this, base::Bind(&PepperInputHandler::set_send_mouse_move_deltas, |
| base::Unretained(&input_handler_)), |
| &cursor_setter_)); |
| empty_cursor_filter_.set_cursor_stub(mouse_locker_.get()); |
| } |
| |
| void ChromotingInstance::HandleSendMouseInputWhenUnfocused() { |
| input_handler_.set_send_mouse_input_when_unfocused(true); |
| } |
| |
| void ChromotingInstance::HandleDelegateLargeCursors() { |
| cursor_setter_.set_delegate_stub(this); |
| } |
| |
| void ChromotingInstance::HandleEnableDebugRegion( |
| const base::DictionaryValue& data) { |
| bool enable = false; |
| if (!data.GetBoolean("enable", &enable)) { |
| LOG(ERROR) << "Invalid enableDebugRegion."; |
| return; |
| } |
| |
| video_renderer_->EnableDebugDirtyRegion(enable); |
| } |
| |
| void ChromotingInstance::HandleEnableTouchEvents( |
| const base::DictionaryValue& data) { |
| bool enable = false; |
| if (!data.GetBoolean("enable", &enable)) { |
| LOG(ERROR) << "Invalid handleTouchEvents."; |
| return; |
| } |
| |
| if (enable) { |
| RequestInputEvents(PP_INPUTEVENT_CLASS_TOUCH); |
| } else { |
| ClearInputEventRequest(PP_INPUTEVENT_CLASS_TOUCH); |
| } |
| } |
| |
| void ChromotingInstance::Disconnect() { |
| DCHECK(plugin_task_runner_->BelongsToCurrentThread()); |
| |
| VLOG(0) << "Disconnecting from host."; |
| |
| // Disconnect the input pipeline and teardown the connection. |
| mouse_input_filter_.set_input_stub(nullptr); |
| client_.reset(); |
| video_renderer_.reset(); |
| audio_player_.reset(); |
| stats_update_timer_.Stop(); |
| } |
| |
| void ChromotingInstance::PostChromotingMessage(const std::string& method, |
| const pp::VarDictionary& data) { |
| pp::VarDictionary message; |
| message.Set(pp::Var("method"), pp::Var(method)); |
| message.Set(pp::Var("data"), data); |
| PostMessage(message); |
| } |
| |
| void ChromotingInstance::PostLegacyJsonMessage( |
| const std::string& method, |
| std::unique_ptr<base::DictionaryValue> data) { |
| base::DictionaryValue message; |
| message.SetString("method", method); |
| message.Set("data", data.release()); |
| |
| std::string message_json; |
| base::JSONWriter::Write(message, &message_json); |
| PostMessage(pp::Var(message_json)); |
| } |
| |
| void ChromotingInstance::SendTrappedKey(uint32_t usb_keycode, bool pressed) { |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetInteger("usbKeycode", usb_keycode); |
| data->SetBoolean("pressed", pressed); |
| PostLegacyJsonMessage("trappedKeyEvent", std::move(data)); |
| } |
| |
| void ChromotingInstance::SendOutgoingIq(const std::string& iq) { |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetString("iq", iq); |
| PostLegacyJsonMessage("sendOutgoingIq", std::move(data)); |
| } |
| |
| void ChromotingInstance::UpdatePerfStatsInUI() { |
| // Fetch performance stats from the VideoRenderer and send them to the client |
| // for display to users. |
| std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| data->SetDouble("videoBandwidth", perf_tracker_.video_bandwidth()); |
| data->SetDouble("videoFrameRate", perf_tracker_.video_frame_rate()); |
| data->SetDouble("captureLatency", perf_tracker_.video_capture_ms().Average()); |
| data->SetDouble("maxCaptureLatency", perf_tracker_.video_capture_ms().Max()); |
| data->SetDouble("encodeLatency", perf_tracker_.video_encode_ms().Average()); |
| data->SetDouble("maxEncodeLatency", perf_tracker_.video_encode_ms().Max()); |
| data->SetDouble("decodeLatency", perf_tracker_.video_decode_ms().Average()); |
| data->SetDouble("maxDecodeLatency", perf_tracker_.video_decode_ms().Max()); |
| data->SetDouble("renderLatency", perf_tracker_.video_paint_ms().Average()); |
| data->SetDouble("maxRenderLatency", perf_tracker_.video_paint_ms().Max()); |
| data->SetDouble("roundtripLatency", perf_tracker_.round_trip_ms().Average()); |
| data->SetDouble("maxRoundtripLatency", perf_tracker_.round_trip_ms().Max()); |
| PostLegacyJsonMessage("onPerfStats", std::move(data)); |
| } |
| |
| // static |
| void ChromotingInstance::RegisterLogMessageHandler() { |
| base::AutoLock lock(g_logging_lock.Get()); |
| |
| // Set up log message handler. |
| // This is not thread-safe so we need it within our lock. |
| logging::SetLogMessageHandler(&LogToUI); |
| } |
| |
| void ChromotingInstance::RegisterLoggingInstance() { |
| base::AutoLock lock(g_logging_lock.Get()); |
| g_logging_instance = pp_instance(); |
| } |
| |
| void ChromotingInstance::UnregisterLoggingInstance() { |
| base::AutoLock lock(g_logging_lock.Get()); |
| |
| // Don't unregister unless we're the currently registered instance. |
| if (pp_instance() != g_logging_instance) |
| return; |
| |
| // Unregister this instance for logging. |
| g_logging_instance = 0; |
| } |
| |
| // static |
| bool ChromotingInstance::LogToUI(int severity, const char* file, int line, |
| size_t message_start, |
| const std::string& str) { |
| PP_LogLevel log_level = PP_LOGLEVEL_ERROR; |
| switch (severity) { |
| case logging::LOG_INFO: |
| log_level = PP_LOGLEVEL_TIP; |
| break; |
| case logging::LOG_WARNING: |
| log_level = PP_LOGLEVEL_WARNING; |
| break; |
| case logging::LOG_ERROR: |
| case logging::LOG_FATAL: |
| log_level = PP_LOGLEVEL_ERROR; |
| break; |
| } |
| |
| PP_Instance pp_instance = 0; |
| { |
| base::AutoLock lock(g_logging_lock.Get()); |
| if (g_logging_instance) |
| pp_instance = g_logging_instance; |
| } |
| if (pp_instance) { |
| const PPB_Console* console = reinterpret_cast<const PPB_Console*>( |
| pp::Module::Get()->GetBrowserInterface(PPB_CONSOLE_INTERFACE)); |
| if (console) |
| console->Log(pp_instance, log_level, pp::Var(str).pp_var()); |
| } |
| |
| // If this is a fatal message the log handler is going to crash after this |
| // function returns. In that case sleep for 1 second, Otherwise the plugin |
| // may crash before the message is delivered to the console. |
| if (severity == logging::LOG_FATAL) |
| base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(1)); |
| |
| return false; |
| } |
| |
| bool ChromotingInstance::IsConnected() { |
| return client_ && |
| (client_->connection_state() == protocol::ConnectionToHost::CONNECTED); |
| } |
| |
| void ChromotingInstance::UpdateUmaEnumHistogram( |
| const std::string& histogram_name, |
| int64_t value, |
| int histogram_max) { |
| pp::UMAPrivate uma(this); |
| uma.HistogramEnumeration(histogram_name, value, histogram_max); |
| } |
| |
| void ChromotingInstance::UpdateUmaCustomHistogram( |
| bool is_custom_counts_histogram, |
| const std::string& histogram_name, |
| int64_t value, |
| int histogram_min, |
| int histogram_max, |
| int histogram_buckets) { |
| pp::UMAPrivate uma(this); |
| |
| if (is_custom_counts_histogram) { |
| uma.HistogramCustomCounts(histogram_name, value, histogram_min, |
| histogram_max, histogram_buckets); |
| } else { |
| uma.HistogramCustomTimes(histogram_name, value, histogram_min, |
| histogram_max, histogram_buckets); |
| } |
| } |
| |
| } // namespace remoting |