// 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 <algorithm> #include <string> #include <vector> #if defined(OS_NACL) #include <sys/mount.h> #include <nacl_io/nacl_io.h> #endif #include "base/bind.h" #include "base/callback.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/lazy_instance.h" #include "base/logging.h" #include "base/strings/string_split.h" #include "base/strings/stringprintf.h" #include "base/synchronization/lock.h" #include "base/threading/thread.h" #include "base/values.h" #include "crypto/random.h" #include "jingle/glue/thread_wrapper.h" #include "media/base/yuv_convert.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/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/client_config.h" #include "remoting/client/frame_consumer_proxy.h" #include "remoting/client/plugin/delegating_signal_strategy.h" #include "remoting/client/plugin/media_source_video_renderer.h" #include "remoting/client/plugin/normalizing_input_filter_cros.h" #include "remoting/client/plugin/normalizing_input_filter_mac.h" #include "remoting/client/plugin/pepper_audio_player.h" #include "remoting/client/plugin/pepper_input_handler.h" #include "remoting/client/plugin/pepper_port_allocator.h" #include "remoting/client/plugin/pepper_view.h" #include "remoting/client/software_video_renderer.h" #include "remoting/client/token_fetcher_proxy.h" #include "remoting/protocol/connection_to_host.h" #include "remoting/protocol/host_stub.h" #include "remoting/protocol/libjingle_transport_factory.h" #include "third_party/libjingle/source/talk/base/helpers.h" #include "third_party/libjingle/source/talk/base/ssladapter.h" #include "url/gurl.h" // Windows defines 'PostMessage', so we have to undef it. #if defined(PostMessage) #undef PostMessage #endif namespace remoting { namespace { // 32-bit BGRA is 4 bytes per pixel. const int kBytesPerPixel = 4; #if defined(ARCH_CPU_LITTLE_ENDIAN) const uint32_t kPixelAlphaMask = 0xff000000; #else // !defined(ARCH_CPU_LITTLE_ENDIAN) const uint32_t kPixelAlphaMask = 0x000000ff; #endif // !defined(ARCH_CPU_LITTLE_ENDIAN) // Default DPI to assume for old clients that use notifyClientResolution. const int kDefaultDPI = 96; // Interval at which to sample performance statistics. const int kPerfStatsIntervalMs = 1000; // URL scheme used by Chrome apps and extensions. const char kChromeExtensionUrlScheme[] = "chrome-extension"; // Maximum width and height of a mouse cursor supported by PPAPI. const int kMaxCursorWidth = 32; const int kMaxCursorHeight = 32; #if defined(USE_OPENSSL) // Size of the random seed blob used to initialize RNG in libjingle. Libjingle // uses the seed only for OpenSSL builds. 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; #endif // defined(USE_OPENSSL) std::string ConnectionStateToString(protocol::ConnectionToHost::State state) { // Values returned by this function must match the // remoting.ClientSession.State enum in JS code. switch (state) { case protocol::ConnectionToHost::INITIALIZING: return "INITIALIZING"; case protocol::ConnectionToHost::CONNECTING: return "CONNECTING"; case protocol::ConnectionToHost::AUTHENTICATED: // Report the authenticated state as 'CONNECTING' to avoid changing // the interface between the plugin and webapp. return "CONNECTING"; case protocol::ConnectionToHost::CONNECTED: return "CONNECTED"; case protocol::ConnectionToHost::CLOSED: return "CLOSED"; case protocol::ConnectionToHost::FAILED: return "FAILED"; } NOTREACHED(); return std::string(); } // 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::INCOMPATIBLE_PROTOCOL: return "INCOMPATIBLE_PROTOCOL"; case protocol::HOST_OVERLOAD: return "HOST_OVERLOAD"; 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(); } // Returns true if |pixel| is not completely transparent. bool IsVisiblePixel(uint32_t pixel) { return (pixel & kPixelAlphaMask) != 0; } // Returns true if there is at least one visible pixel in the given range. bool IsVisibleRow(const uint32_t* begin, const uint32_t* end) { return std::find_if(begin, end, &IsVisiblePixel) != end; } // This flag blocks LOGs to the UI if we're already in the middle of logging // to the UI. This prevents a potential infinite loop if we encounter an error // while sending the log message to the UI. bool g_logging_to_plugin = false; bool g_has_logging_instance = false; base::LazyInstance<scoped_refptr<base::SingleThreadTaskRunner> >::Leaky g_logging_task_runner = LAZY_INSTANCE_INITIALIZER; base::LazyInstance<base::WeakPtr<ChromotingInstance> >::Leaky g_logging_instance = LAZY_INSTANCE_INITIALIZER; base::LazyInstance<base::Lock>::Leaky g_logging_lock = LAZY_INSTANCE_INITIALIZER; logging::LogMessageHandlerFunction g_logging_old_handler = NULL; } // namespace // String sent in the "hello" message to the webapp to describe features. const char ChromotingInstance::kApiFeatures[] = "highQualityScaling injectKeyEvent sendClipboardItem remapKey trapKey " "notifyClientResolution pauseVideo pauseAudio asyncPin thirdPartyAuth " "pinlessAuth extensionMessage allowMouseLock mediaSourceRendering " "videoControl"; const char ChromotingInstance::kRequestedCapabilities[] = ""; const char ChromotingInstance::kSupportedCapabilities[] = "desktopShape"; bool ChromotingInstance::ParseAuthMethods(const std::string& auth_methods_str, ClientConfig* config) { std::vector<std::string> auth_methods; base::SplitString(auth_methods_str, ',', &auth_methods); for (std::vector<std::string>::iterator it = auth_methods.begin(); it != auth_methods.end(); ++it) { protocol::AuthenticationMethod authentication_method = protocol::AuthenticationMethod::FromString(*it); if (authentication_method.is_valid()) config->authentication_methods.push_back(authentication_method); } if (config->authentication_methods.empty()) { LOG(ERROR) << "No valid authentication methods specified."; return false; } return true; } ChromotingInstance::ChromotingInstance(PP_Instance pp_instance) : pp::Instance(pp_instance), initialized_(false), plugin_task_runner_(new PluginThreadTaskRunner(&plugin_thread_delegate_)), context_(plugin_task_runner_.get()), input_tracker_(&mouse_input_filter_), key_mapper_(&input_tracker_), input_handler_(this), use_async_pin_dialog_(false), use_media_source_rendering_(false), delegate_large_cursors_(false), weak_factory_(this) { #if defined(OS_NACL) // 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_.reset( new jingle_glue::JingleThreadWrapper(plugin_task_runner_)); media::InitializeCPUSpecificYUVConversions(); #else jingle_glue::JingleThreadWrapper::EnsureForCurrentMessageLoop(); #endif #if defined(OS_NACL) nacl_io_init_ppapi(pp_instance, pp::Module::Get()->get_browser_interface()); mount("", "/etc", "memfs", 0, ""); mount("", "/usr", "memfs", 0, ""); #endif RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE | PP_INPUTEVENT_CLASS_WHEEL); RequestFilteringInputEvents(PP_INPUTEVENT_CLASS_KEYBOARD); // Resister this instance to handle debug log messsages. RegisterLoggingInstance(); #if defined(USE_OPENSSL) // Initialize random seed for libjingle. It's necessary only with OpenSSL. char random_seed[kRandomSeedSize]; crypto::RandBytes(random_seed, sizeof(random_seed)); talk_base::InitRandom(random_seed, sizeof(random_seed)); #else // Libjingle's SSL implementation is not really used, but it has to be // initialized for NSS builds to make sure that RNG is initialized in NSS, // because libjingle uses it. talk_base::InitializeSSL(); #endif // !defined(USE_OPENSSL) // Send hello message. scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetInteger("apiVersion", kApiVersion); data->SetString("apiFeatures", kApiFeatures); data->SetInteger("apiMinVersion", kApiMinMessagingVersion); data->SetString("requestedCapabilities", kRequestedCapabilities); data->SetString("supportedCapabilities", kSupportedCapabilities); PostLegacyJsonMessage("hello", data.Pass()); } ChromotingInstance::~ChromotingInstance() { DCHECK(plugin_task_runner_->BelongsToCurrentThread()); // 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(); // PepperView must be destroyed before the client. view_weak_factory_.reset(); view_.reset(); client_.reset(); plugin_task_runner_->Quit(); // Ensure that nothing touches the plugin thread delegate after this point. plugin_task_runner_->DetachAndRunShutdownLoop(); // 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"; // Check that the calling content is part of an app or extension. This is only // necessary for non-PNaCl version of the plugin. Also PPB_URLUtil_Dev doesn't // work in NaCl at the moment so the check fails in NaCl builds. #if !defined(OS_NACL) if (!IsCallerAppOrExtension()) { LOG(ERROR) << "Not an app or extension"; return false; } #endif // 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; } scoped_ptr<base::Value> json( base::JSONReader::Read(message.AsString(), base::JSON_ALLOW_TRAILING_COMMAS)); base::DictionaryValue* message_dict = NULL; std::string method; base::DictionaryValue* data = NULL; 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 == "pauseVideo") { HandlePauseVideo(*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 == "enableMediaSourceRendering") { HandleEnableMediaSourceRendering(); } else if (method == "sendMouseInputWhenUnfocused") { HandleSendMouseInputWhenUnfocused(); } else if (method == "delegateLargeCursors") { HandleDelegateLargeCursors(); } } void ChromotingInstance::DidChangeFocus(bool has_focus) { DCHECK(plugin_task_runner_->BelongsToCurrentThread()); if (!IsConnected()) return; input_handler_.DidChangeFocus(has_focus); } void ChromotingInstance::DidChangeView(const pp::View& view) { DCHECK(plugin_task_runner_->BelongsToCurrentThread()); plugin_view_ = view; mouse_input_filter_.set_input_size( webrtc::DesktopSize(view.GetRect().width(), view.GetRect().height())); if (view_) view_->SetView(view); } bool ChromotingInstance::HandleInputEvent(const pp::InputEvent& event) { DCHECK(plugin_task_runner_->BelongsToCurrentThread()); if (!IsConnected()) return false; return input_handler_.HandleInputEvent(event); } void ChromotingInstance::SetDesktopSize(const webrtc::DesktopSize& size, const webrtc::DesktopVector& dpi) { mouse_input_filter_.set_output_size(size); scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetInteger("width", size.width()); data->SetInteger("height", size.height()); if (dpi.x()) data->SetInteger("x_dpi", dpi.x()); if (dpi.y()) data->SetInteger("y_dpi", dpi.y()); PostLegacyJsonMessage("onDesktopSize", data.Pass()); } void ChromotingInstance::SetDesktopShape(const webrtc::DesktopRegion& shape) { if (desktop_shape_ && shape.Equals(*desktop_shape_)) return; desktop_shape_.reset(new webrtc::DesktopRegion(shape)); scoped_ptr<base::ListValue> rects_value(new base::ListValue()); for (webrtc::DesktopRegion::Iterator i(shape); !i.IsAtEnd(); i.Advance()) { const webrtc::DesktopRect& rect = i.rect(); scoped_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(rect_value.release()); } scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->Set("rects", rects_value.release()); PostLegacyJsonMessage("onDesktopShape", data.Pass()); } void ChromotingInstance::OnConnectionState( protocol::ConnectionToHost::State state, protocol::ErrorCode error) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetString("state", ConnectionStateToString(state)); data->SetString("error", ConnectionErrorToString(error)); PostLegacyJsonMessage("onConnectionStatus", data.Pass()); } void ChromotingInstance::FetchThirdPartyToken( const GURL& token_url, const std::string& host_public_key, const std::string& scope, base::WeakPtr<TokenFetcherProxy> token_fetcher_proxy) { // 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(!token_fetcher_proxy_.get()); token_fetcher_proxy_ = token_fetcher_proxy; scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetString("tokenUrl", token_url.spec()); data->SetString("hostPublicKey", host_public_key); data->SetString("scope", scope); PostLegacyJsonMessage("fetchThirdPartyToken", data.Pass()); } void ChromotingInstance::OnConnectionReady(bool ready) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetBoolean("ready", ready); PostLegacyJsonMessage("onConnectionReady", data.Pass()); } void ChromotingInstance::OnRouteChanged(const std::string& channel_name, const protocol::TransportRoute& route) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); std::string message = "Channel " + channel_name + " using " + protocol::TransportRoute::GetTypeString(route.type) + " connection."; data->SetString("message", message); PostLegacyJsonMessage("logDebugMessage", data.Pass()); } void ChromotingInstance::SetCapabilities(const std::string& capabilities) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetString("capabilities", capabilities); PostLegacyJsonMessage("setCapabilities", data.Pass()); } void ChromotingInstance::SetPairingResponse( const protocol::PairingResponse& pairing_response) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetString("clientId", pairing_response.client_id()); data->SetString("sharedSecret", pairing_response.shared_secret()); PostLegacyJsonMessage("pairingResponse", data.Pass()); } void ChromotingInstance::DeliverHostMessage( const protocol::ExtensionMessage& message) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetString("type", message.type()); data->SetString("data", message.data()); PostLegacyJsonMessage("extensionMessage", data.Pass()); } 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; scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetBoolean("pairingSupported", pairing_supported); PostLegacyJsonMessage("fetchPin", data.Pass()); } 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() { // TODO(sergeyu): Move cursor shape code to a separate class. // crbug.com/138108 return this; } scoped_ptr<protocol::ThirdPartyClientAuthenticator::TokenFetcher> ChromotingInstance::GetTokenFetcher(const std::string& host_public_key) { return scoped_ptr<protocol::ThirdPartyClientAuthenticator::TokenFetcher>( new TokenFetcherProxy( base::Bind(&ChromotingInstance::FetchThirdPartyToken, weak_factory_.GetWeakPtr()), host_public_key)); } void ChromotingInstance::InjectClipboardEvent( const protocol::ClipboardEvent& event) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetString("mimeType", event.mime_type()); data->SetString("item", event.data()); PostLegacyJsonMessage("injectClipboardItem", data.Pass()); } void ChromotingInstance::SetCursorShape( const protocol::CursorShapeInfo& cursor_shape) { COMPILE_ASSERT(sizeof(uint32_t) == kBytesPerPixel, rgba_pixels_are_32bit); // pp::MouseCursor requires image to be in the native format. if (pp::ImageData::GetNativeImageDataFormat() != PP_IMAGEDATAFORMAT_BGRA_PREMUL) { LOG(WARNING) << "Unable to set cursor shape - native image format is not" " premultiplied BGRA"; return; } int width = cursor_shape.width(); int height = cursor_shape.height(); int hotspot_x = cursor_shape.hotspot_x(); int hotspot_y = cursor_shape.hotspot_y(); int bytes_per_row = width * kBytesPerPixel; int src_stride = width; const uint32_t* src_row_data = reinterpret_cast<const uint32_t*>( cursor_shape.data().data()); const uint32_t* src_row_data_end = src_row_data + src_stride * height; scoped_ptr<pp::ImageData> cursor_image; pp::Point cursor_hotspot; // Check if the cursor is visible. if (IsVisibleRow(src_row_data, src_row_data_end)) { // If the cursor exceeds the size permitted by PPAPI then crop it, keeping // the hotspot as close to the center of the new cursor shape as possible. if (height > kMaxCursorHeight && !delegate_large_cursors_) { int y = hotspot_y - (kMaxCursorHeight / 2); y = std::max(y, 0); y = std::min(y, height - kMaxCursorHeight); src_row_data += src_stride * y; height = kMaxCursorHeight; hotspot_y -= y; } if (width > kMaxCursorWidth && !delegate_large_cursors_) { int x = hotspot_x - (kMaxCursorWidth / 2); x = std::max(x, 0); x = std::min(x, height - kMaxCursorWidth); src_row_data += x; width = kMaxCursorWidth; bytes_per_row = width * kBytesPerPixel; hotspot_x -= x; } cursor_image.reset(new pp::ImageData(this, PP_IMAGEDATAFORMAT_BGRA_PREMUL, pp::Size(width, height), false)); cursor_hotspot = pp::Point(hotspot_x, hotspot_y); uint8* dst_row_data = reinterpret_cast<uint8*>(cursor_image->data()); for (int row = 0; row < height; row++) { memcpy(dst_row_data, src_row_data, bytes_per_row); src_row_data += src_stride; dst_row_data += cursor_image->stride(); } } if (height > kMaxCursorHeight || width > kMaxCursorWidth) { DCHECK(delegate_large_cursors_); size_t buffer_size = height * bytes_per_row; pp::VarArrayBuffer array_buffer(buffer_size); void* dst = array_buffer.Map(); memcpy(dst, cursor_image->data(), buffer_size); array_buffer.Unmap(); pp::VarDictionary dictionary; dictionary.Set(pp::Var("width"), width); dictionary.Set(pp::Var("height"), height); dictionary.Set(pp::Var("hotspotX"), cursor_hotspot.x()); dictionary.Set(pp::Var("hotspotY"), cursor_hotspot.y()); dictionary.Set(pp::Var("data"), array_buffer); PostChromotingMessage("setCursorShape", dictionary); input_handler_.SetMouseCursor(scoped_ptr<pp::ImageData>(), cursor_hotspot); } else { if (delegate_large_cursors_) { pp::VarDictionary dictionary; PostChromotingMessage("unsetCursorShape", dictionary); } input_handler_.SetMouseCursor(cursor_image.Pass(), cursor_hotspot); } } void ChromotingInstance::OnFirstFrameReceived() { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); PostLegacyJsonMessage("onFirstFrameReceived", data.Pass()); } void ChromotingInstance::HandleConnect(const base::DictionaryValue& data) { ClientConfig config; std::string local_jid; std::string auth_methods; if (!data.GetString("hostJid", &config.host_jid) || !data.GetString("hostPublicKey", &config.host_public_key) || !data.GetString("localJid", &local_jid) || !data.GetString("authenticationMethods", &auth_methods) || !ParseAuthMethods(auth_methods, &config) || !data.GetString("authenticationTag", &config.authentication_tag)) { LOG(ERROR) << "Invalid connect() data."; return; } data.GetString("clientPairingId", &config.client_pairing_id); data.GetString("clientPairedSecret", &config.client_paired_secret); if (use_async_pin_dialog_) { 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; } config.fetch_secret_callback = base::Bind(&ChromotingInstance::FetchSecretFromString, shared_secret); } // Read the list of capabilities, if any. if (data.HasKey("capabilities")) { if (!data.GetString("capabilities", &config.capabilities)) { LOG(ERROR) << "Invalid connect() data."; return; } } #if defined(OS_NACL) 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 { DCHECK(key_filter.empty()); normalizing_input_filter_.reset(new protocol::InputFilter(&key_mapper_)); } #elif defined(OS_MACOSX) normalizing_input_filter_.reset(new NormalizingInputFilterMac(&key_mapper_)); #elif defined(OS_CHROMEOS) normalizing_input_filter_.reset(new NormalizingInputFilterCros(&key_mapper_)); #else normalizing_input_filter_.reset(new protocol::InputFilter(&key_mapper_)); #endif input_handler_.set_input_stub(normalizing_input_filter_.get()); ConnectWithConfig(config, local_jid); } void ChromotingInstance::ConnectWithConfig(const ClientConfig& config, const std::string& local_jid) { DCHECK(plugin_task_runner_->BelongsToCurrentThread()); if (use_media_source_rendering_) { video_renderer_.reset(new MediaSourceVideoRenderer(this)); } else { view_.reset(new PepperView(this, &context_)); view_weak_factory_.reset( new base::WeakPtrFactory<FrameConsumer>(view_.get())); // SoftwareVideoRenderer runs on a separate thread so for now we wrap // PepperView with a ref-counted proxy object. scoped_refptr<FrameConsumerProxy> consumer_proxy = new FrameConsumerProxy(plugin_task_runner_, view_weak_factory_->GetWeakPtr()); SoftwareVideoRenderer* renderer = new SoftwareVideoRenderer(context_.main_task_runner(), context_.decode_task_runner(), consumer_proxy); view_->Initialize(renderer); if (!plugin_view_.is_null()) view_->SetView(plugin_view_); video_renderer_.reset(renderer); } host_connection_.reset(new protocol::ConnectionToHost(true)); scoped_ptr<AudioPlayer> audio_player(new PepperAudioPlayer(this)); client_.reset(new ChromotingClient(config, &context_, host_connection_.get(), this, video_renderer_.get(), audio_player.Pass())); // Connect the input pipeline to the protocol stub & initialize components. mouse_input_filter_.set_input_stub(host_connection_->input_stub()); if (!plugin_view_.is_null()) { mouse_input_filter_.set_input_size(webrtc::DesktopSize( plugin_view_.GetRect().width(), plugin_view_.GetRect().height())); } VLOG(0) << "Connecting to " << config.host_jid << ". Local jid: " << local_jid << "."; // Setup the signal strategy. signal_strategy_.reset(new DelegatingSignalStrategy( local_jid, base::Bind(&ChromotingInstance::SendOutgoingIq, weak_factory_.GetWeakPtr()))); scoped_ptr<cricket::HttpPortAllocatorBase> port_allocator( PepperPortAllocator::Create(this)); scoped_ptr<protocol::TransportFactory> transport_factory( new protocol::LibjingleTransportFactory( signal_strategy_.get(), port_allocator.Pass(), NetworkSettings(NetworkSettings::NAT_TRAVERSAL_FULL))); // Kick off the connection. client_->Start(signal_strategy_.get(), transport_factory.Pass()); // Start timer that periodically sends perf stats. plugin_task_runner_->PostDelayedTask( FROM_HERE, base::Bind(&ChromotingInstance::SendPerfStats, weak_factory_.GetWeakPtr()), base::TimeDelta::FromMilliseconds(kPerfStatsIntervalMs)); } void ChromotingInstance::HandleDisconnect(const base::DictionaryValue& data) { DCHECK(plugin_task_runner_->BelongsToCurrentThread()); // PepperView must be destroyed before the client. view_weak_factory_.reset(); view_.reset(); VLOG(0) << "Disconnecting from host."; client_.reset(); // Disconnect the input pipeline and teardown the connection. mouse_input_filter_.set_input_stub(NULL); host_connection_.reset(); } 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()) input_tracker_.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); host_connection_->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_width(width); client_resolution.set_height(height); client_resolution.set_x_dpi(x_dpi); client_resolution.set_y_dpi(y_dpi); // Include the legacy width & height in DIPs for use by older hosts. client_resolution.set_dips_width((width * kDefaultDPI) / x_dpi); client_resolution.set_dips_height((height * kDefaultDPI) / y_dpi); host_connection_->host_stub()->NotifyClientResolution(client_resolution); } void ChromotingInstance::HandlePauseVideo(const base::DictionaryValue& data) { if (!data.HasKey("pause")) { LOG(ERROR) << "Invalid pauseVideo."; return; } HandleVideoControl(data); } 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); } 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; } host_connection_->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); host_connection_->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()) { secret_fetched_callback_.Run(pin); secret_fetched_callback_.Reset(); } 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 (token_fetcher_proxy_.get()) { token_fetcher_proxy_->OnTokenFetched(token, shared_secret); token_fetcher_proxy_.reset(); } 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); host_connection_->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); host_connection_->host_stub()->DeliverClientMessage(message); } void ChromotingInstance::HandleAllowMouseLockMessage() { input_handler_.AllowMouseLock(); } void ChromotingInstance::HandleEnableMediaSourceRendering() { use_media_source_rendering_ = true; } void ChromotingInstance::HandleSendMouseInputWhenUnfocused() { input_handler_.set_send_mouse_input_when_unfocused(true); } void ChromotingInstance::HandleDelegateLargeCursors() { delegate_large_cursors_ = true; } ChromotingStats* ChromotingInstance::GetStats() { if (!video_renderer_.get()) return NULL; return video_renderer_->GetStats(); } 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, scoped_ptr<base::DictionaryValue> data) { scoped_ptr<base::DictionaryValue> message(new base::DictionaryValue()); message->SetString("method", method); message->Set("data", data.release()); std::string message_json; base::JSONWriter::Write(message.get(), &message_json); PostMessage(pp::Var(message_json)); } void ChromotingInstance::SendTrappedKey(uint32 usb_keycode, bool pressed) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetInteger("usbKeycode", usb_keycode); data->SetBoolean("pressed", pressed); PostLegacyJsonMessage("trappedKeyEvent", data.Pass()); } void ChromotingInstance::SendOutgoingIq(const std::string& iq) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetString("iq", iq); PostLegacyJsonMessage("sendOutgoingIq", data.Pass()); } void ChromotingInstance::SendPerfStats() { if (!video_renderer_.get()) { return; } plugin_task_runner_->PostDelayedTask( FROM_HERE, base::Bind(&ChromotingInstance::SendPerfStats, weak_factory_.GetWeakPtr()), base::TimeDelta::FromMilliseconds(kPerfStatsIntervalMs)); scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); ChromotingStats* stats = video_renderer_->GetStats(); data->SetDouble("videoBandwidth", stats->video_bandwidth()->Rate()); data->SetDouble("videoFrameRate", stats->video_frame_rate()->Rate()); data->SetDouble("captureLatency", stats->video_capture_ms()->Average()); data->SetDouble("encodeLatency", stats->video_encode_ms()->Average()); data->SetDouble("decodeLatency", stats->video_decode_ms()->Average()); data->SetDouble("renderLatency", stats->video_paint_ms()->Average()); data->SetDouble("roundtripLatency", stats->round_trip_ms()->Average()); PostLegacyJsonMessage("onPerfStats", data.Pass()); } // static void ChromotingInstance::RegisterLogMessageHandler() { base::AutoLock lock(g_logging_lock.Get()); VLOG(1) << "Registering global log handler"; // Record previous handler so we can call it in a chain. g_logging_old_handler = logging::GetLogMessageHandler(); // 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()); // Register this instance as the one that will handle all logging calls // and display them to the user. // If multiple plugins are run, then the last one registered will handle all // logging for all instances. g_logging_instance.Get() = weak_factory_.GetWeakPtr(); g_logging_task_runner.Get() = plugin_task_runner_; g_has_logging_instance = true; } void ChromotingInstance::UnregisterLoggingInstance() { base::AutoLock lock(g_logging_lock.Get()); // Don't unregister unless we're the currently registered instance. if (this != g_logging_instance.Get().get()) return; // Unregister this instance for logging. g_has_logging_instance = false; g_logging_instance.Get().reset(); g_logging_task_runner.Get() = NULL; VLOG(1) << "Unregistering global log handler"; } // static bool ChromotingInstance::LogToUI(int severity, const char* file, int line, size_t message_start, const std::string& str) { // Note that we're reading |g_has_logging_instance| outside of a lock. // This lockless read is done so that we don't needlessly slow down global // logging with a lock for each log message. // // This lockless read is safe because: // // Misreading a false value (when it should be true) means that we'll simply // skip processing a few log messages. // // Misreading a true value (when it should be false) means that we'll take // the lock and check |g_logging_instance| unnecessarily. This is not // problematic because we always set |g_logging_instance| inside a lock. if (g_has_logging_instance) { scoped_refptr<base::SingleThreadTaskRunner> logging_task_runner; base::WeakPtr<ChromotingInstance> logging_instance; { base::AutoLock lock(g_logging_lock.Get()); // If we're on the logging thread and |g_logging_to_plugin| is set then // this LOG message came from handling a previous LOG message and we // should skip it to avoid an infinite loop of LOG messages. if (!g_logging_task_runner.Get()->BelongsToCurrentThread() || !g_logging_to_plugin) { logging_task_runner = g_logging_task_runner.Get(); logging_instance = g_logging_instance.Get(); } } if (logging_task_runner.get()) { std::string message = remoting::GetTimestampString(); message += (str.c_str() + message_start); logging_task_runner->PostTask( FROM_HERE, base::Bind(&ChromotingInstance::ProcessLogToUI, logging_instance, message)); } } if (g_logging_old_handler) return (g_logging_old_handler)(severity, file, line, message_start, str); return false; } void ChromotingInstance::ProcessLogToUI(const std::string& message) { DCHECK(plugin_task_runner_->BelongsToCurrentThread()); // This flag (which is set only here) is used to prevent LogToUI from posting // new tasks while we're in the middle of servicing a LOG call. This can // happen if the call to LogDebugInfo tries to LOG anything. // Since it is read on the plugin thread, we don't need to lock to set it. g_logging_to_plugin = true; scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetString("message", message); PostLegacyJsonMessage("logDebugMessage", data.Pass()); g_logging_to_plugin = false; } bool ChromotingInstance::IsCallerAppOrExtension() { const pp::URLUtil_Dev* url_util = pp::URLUtil_Dev::Get(); if (!url_util) return false; PP_URLComponents_Dev url_components; pp::Var url_var = url_util->GetDocumentURL(this, &url_components); if (!url_var.is_string()) return false; std::string url = url_var.AsString(); std::string url_scheme = url.substr(url_components.scheme.begin, url_components.scheme.len); return url_scheme == kChromeExtensionUrlScheme; } bool ChromotingInstance::IsConnected() { return host_connection_.get() && (host_connection_->state() == protocol::ConnectionToHost::CONNECTED); } void ChromotingInstance::OnMediaSourceSize(const webrtc::DesktopSize& size, const webrtc::DesktopVector& dpi) { SetDesktopSize(size, dpi); } void ChromotingInstance::OnMediaSourceShape( const webrtc::DesktopRegion& shape) { SetDesktopShape(shape); } void ChromotingInstance::OnMediaSourceReset(const std::string& format) { scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue()); data->SetString("format", format); PostLegacyJsonMessage("mediaSourceReset", data.Pass()); } void ChromotingInstance::OnMediaSourceData(uint8_t* buffer, size_t buffer_size, bool keyframe) { pp::VarArrayBuffer array_buffer(buffer_size); void* data_ptr = array_buffer.Map(); memcpy(data_ptr, buffer, buffer_size); array_buffer.Unmap(); pp::VarDictionary data_dictionary; data_dictionary.Set(pp::Var("buffer"), array_buffer); data_dictionary.Set(pp::Var("keyframe"), keyframe); PostChromotingMessage("mediaSourceData", data_dictionary); } } // namespace remoting