blob: d3caf7c493ae20ad18901b022e41238ce699fb58 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/win/rdp_client_window.h"
#include <wtsdefs.h>
#include <list>
#include "base/bind.h"
#include "base/lazy_instance.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/stl_util.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_local.h"
#include "base/win/scoped_bstr.h"
namespace remoting {
namespace {
// RDP session disconnect reason codes that should not be interpreted as errors.
constexpr long kDisconnectReasonNoInfo = 0;
constexpr long kDisconnectReasonLocalNotError = 1;
constexpr long kDisconnectReasonRemoteByUser = 2;
constexpr long kDisconnectReasonByServer = 3;
// Maximum length of a window class name including the terminating nullptr.
constexpr int kMaxWindowClassLength = 256;
// Each member of the array returned by GetKeyboardState() contains status data
// for a virtual key. If the high-order bit is 1, the key is down; otherwise, it
// is up.
constexpr BYTE kKeyPressedFlag = 0x80;
constexpr int kKeyboardStateLength = 256;
constexpr base::TimeDelta kReapplyResolutionPeriod =
base::TimeDelta::FromMilliseconds(250);
// We want to try to reapply resolution changes for ~5 seconds (20 * 250ms).
constexpr int kMaxResolutionReapplyAttempts = 20;
// The RDP control creates 'IHWindowClass' window to handle keyboard input.
constexpr wchar_t kRdpInputWindowClass[] = L"IHWindowClass";
enum RdpAudioMode {
// Redirect sounds to the client. This is the default value.
kRdpAudioModeRedirect = 0,
// Play sounds at the remote computer. Equivalent to |kRdpAudioModeNone| if
// the remote computer is running a server SKU.
kRdpAudioModePlayOnServer = 1,
// Disable sound redirection; do not play sounds at the remote computer.
kRdpAudioModeNone = 2
};
// Points to a per-thread instance of the window activation hook handle.
base::LazyInstance<base::ThreadLocalPointer<RdpClientWindow::WindowHook>>::
DestructorAtExit g_window_hook = LAZY_INSTANCE_INITIALIZER;
// Finds a child window with the class name matching |class_name|. Unlike
// FindWindowEx() this function walks the tree of windows recursively. The walk
// is done in breadth-first order. The function returns nullptr if the child
// window could not be found.
HWND FindWindowRecursively(HWND parent, const base::string16& class_name) {
std::list<HWND> windows;
windows.push_back(parent);
while (!windows.empty()) {
HWND child = FindWindowEx(windows.front(), nullptr, nullptr, nullptr);
while (child != nullptr) {
// See if the window class name matches |class_name|.
WCHAR name[kMaxWindowClassLength];
int length = GetClassName(child, name, base::size(name));
if (base::string16(name, length) == class_name)
return child;
// Remember the window to look through its children.
windows.push_back(child);
// Go to the next child.
child = FindWindowEx(windows.front(), child, nullptr, nullptr);
}
windows.pop_front();
}
return nullptr;
}
} // namespace
// Used to close any windows activated on a particular thread. It installs
// a WH_CBT window hook to track window activations and close all activated
// windows. There should be only one instance of |WindowHook| per thread
// at any given moment.
class RdpClientWindow::WindowHook
: public base::RefCounted<WindowHook> {
public:
static scoped_refptr<WindowHook> Create();
private:
friend class base::RefCounted<WindowHook>;
WindowHook();
virtual ~WindowHook();
static LRESULT CALLBACK CloseWindowOnActivation(
int code, WPARAM wparam, LPARAM lparam);
HHOOK hook_;
DISALLOW_COPY_AND_ASSIGN(WindowHook);
};
RdpClientWindow::RdpClientWindow(const net::IPEndPoint& server_endpoint,
const std::string& terminal_id,
EventHandler* event_handler)
: event_handler_(event_handler),
server_endpoint_(server_endpoint),
terminal_id_(terminal_id) {
}
RdpClientWindow::~RdpClientWindow() {
if (m_hWnd) {
DestroyWindow();
}
DCHECK(!client_.Get());
DCHECK(!client_9_.Get());
DCHECK(!client_settings_.Get());
}
bool RdpClientWindow::Connect(const ScreenResolution& resolution) {
DCHECK(!m_hWnd);
screen_resolution_ = resolution;
RECT rect = {0, 0, screen_resolution_.dimensions().width(),
screen_resolution_.dimensions().height()};
bool result = Create(nullptr, rect, nullptr) != nullptr;
// Hide the window since this class is about establishing a connection, not
// about showing a UI to the user.
if (result) {
ShowWindow(SW_HIDE);
}
return result;
}
void RdpClientWindow::Disconnect() {
if (m_hWnd) {
SendMessage(WM_CLOSE);
}
}
void RdpClientWindow::InjectSas() {
if (!m_hWnd) {
return;
}
// Find the window handling the keyboard input.
HWND input_window = FindWindowRecursively(m_hWnd, kRdpInputWindowClass);
if (!input_window) {
LOG(ERROR) << "Failed to find the window handling the keyboard input.";
return;
}
VLOG(3) << "Injecting Ctrl+Alt+End to emulate SAS.";
BYTE keyboard_state[kKeyboardStateLength];
if (!GetKeyboardState(keyboard_state)) {
PLOG(ERROR) << "Failed to get the keyboard state.";
return;
}
// This code is running in Session 0, so we expect no keys to be pressed.
DCHECK(!(keyboard_state[VK_CONTROL] & kKeyPressedFlag));
DCHECK(!(keyboard_state[VK_MENU] & kKeyPressedFlag));
DCHECK(!(keyboard_state[VK_END] & kKeyPressedFlag));
// Map virtual key codes to scan codes.
UINT control = MapVirtualKey(VK_CONTROL, MAPVK_VK_TO_VSC);
UINT alt = MapVirtualKey(VK_MENU, MAPVK_VK_TO_VSC);
UINT end = MapVirtualKey(VK_END, MAPVK_VK_TO_VSC) | KF_EXTENDED;
UINT up = KF_UP | KF_REPEAT;
// Press 'Ctrl'.
keyboard_state[VK_CONTROL] |= kKeyPressedFlag;
keyboard_state[VK_LCONTROL] |= kKeyPressedFlag;
CHECK(SetKeyboardState(keyboard_state));
SendMessage(input_window, WM_KEYDOWN, VK_CONTROL, MAKELPARAM(1, control));
// Press 'Alt'.
keyboard_state[VK_MENU] |= kKeyPressedFlag;
keyboard_state[VK_LMENU] |= kKeyPressedFlag;
CHECK(SetKeyboardState(keyboard_state));
SendMessage(input_window, WM_KEYDOWN, VK_MENU,
MAKELPARAM(1, alt | KF_ALTDOWN));
// Press and release 'End'.
SendMessage(input_window, WM_KEYDOWN, VK_END,
MAKELPARAM(1, end | KF_ALTDOWN));
SendMessage(input_window, WM_KEYUP, VK_END,
MAKELPARAM(1, end | up | KF_ALTDOWN));
// Release 'Alt'.
keyboard_state[VK_MENU] &= ~kKeyPressedFlag;
keyboard_state[VK_LMENU] &= ~kKeyPressedFlag;
CHECK(SetKeyboardState(keyboard_state));
SendMessage(input_window, WM_KEYUP, VK_MENU, MAKELPARAM(1, alt | up));
// Release 'Ctrl'.
keyboard_state[VK_CONTROL] &= ~kKeyPressedFlag;
keyboard_state[VK_LCONTROL] &= ~kKeyPressedFlag;
CHECK(SetKeyboardState(keyboard_state));
SendMessage(input_window, WM_KEYUP, VK_CONTROL, MAKELPARAM(1, control | up));
}
void RdpClientWindow::ChangeResolution(const ScreenResolution& resolution) {
// Stop any pending resolution changes.
apply_resolution_timer_.Stop();
screen_resolution_ = resolution;
HRESULT result = UpdateDesktopResolution();
if (FAILED(result)) {
LOG(WARNING) << "UpdateSessionDisplaySettings() failed: 0x" << std::hex
<< result;
}
}
void RdpClientWindow::OnClose() {
if (!client_.Get()) {
NotifyDisconnected();
return;
}
// Request a graceful shutdown.
mstsc::ControlCloseStatus close_status;
HRESULT result = client_->RequestClose(&close_status);
if (FAILED(result)) {
LOG(ERROR) << "Failed to request a graceful shutdown of an RDP connection"
<< ", result=0x" << std::hex << result;
NotifyDisconnected();
return;
}
if (close_status != mstsc::controlCloseWaitForEvents) {
NotifyDisconnected();
return;
}
// Expect IMsTscAxEvents::OnConfirmClose() or IMsTscAxEvents::OnDisconnect()
// to be called if mstsc::controlCloseWaitForEvents was returned.
}
LRESULT RdpClientWindow::OnCreate(CREATESTRUCT* create_struct) {
CAxWindow2 activex_window;
Microsoft::WRL::ComPtr<IUnknown> control;
HRESULT result = E_FAIL;
Microsoft::WRL::ComPtr<mstsc::IMsTscSecuredSettings> secured_settings;
Microsoft::WRL::ComPtr<mstsc::IMsRdpClientSecuredSettings> secured_settings2;
base::win::ScopedBstr server_name(
base::UTF8ToUTF16(server_endpoint_.ToStringWithoutPort()));
base::win::ScopedBstr terminal_id(base::UTF8ToUTF16(terminal_id_));
// Create the child window that actually hosts the ActiveX control.
RECT rect = {0, 0, screen_resolution_.dimensions().width(),
screen_resolution_.dimensions().height()};
activex_window.Create(m_hWnd, rect, nullptr,
WS_CHILD | WS_VISIBLE | WS_BORDER);
if (activex_window.m_hWnd == nullptr)
return LogOnCreateError(HRESULT_FROM_WIN32(GetLastError()));
// Instantiate the RDP ActiveX control.
result = activex_window.CreateControlEx(
OLESTR("MsTscAx.MsTscAx"), nullptr, nullptr, control.GetAddressOf(),
__uuidof(mstsc::IMsTscAxEvents),
reinterpret_cast<IUnknown*>(static_cast<RdpEventsSink*>(this)));
if (FAILED(result))
return LogOnCreateError(result);
result = control.CopyTo(client_.GetAddressOf());
if (FAILED(result))
return LogOnCreateError(result);
// Use 32-bit color.
result = client_->put_ColorDepth(32);
if (FAILED(result))
return LogOnCreateError(result);
// Set dimensions of the remote desktop.
result = client_->put_DesktopWidth(screen_resolution_.dimensions().width());
if (FAILED(result))
return LogOnCreateError(result);
result = client_->put_DesktopHeight(screen_resolution_.dimensions().height());
if (FAILED(result))
return LogOnCreateError(result);
// Check to see if the platform exposes the interface used for resizing.
result = client_.CopyTo(client_9_.GetAddressOf());
if (FAILED(result) && result != E_NOINTERFACE) {
return LogOnCreateError(result);
}
// Set the server name to connect to.
result = client_->put_Server(server_name);
if (FAILED(result))
return LogOnCreateError(result);
// Fetch IMsRdpClientAdvancedSettings interface for the client.
result = client_->get_AdvancedSettings2(client_settings_.GetAddressOf());
if (FAILED(result))
return LogOnCreateError(result);
// Disable background input mode.
result = client_settings_->put_allowBackgroundInput(0);
if (FAILED(result))
return LogOnCreateError(result);
// Do not use bitmap cache.
result = client_settings_->put_BitmapPersistence(0);
if (SUCCEEDED(result))
result = client_settings_->put_CachePersistenceActive(0);
if (FAILED(result))
return LogOnCreateError(result);
// Do not use compression.
result = client_settings_->put_Compress(0);
if (FAILED(result))
return LogOnCreateError(result);
// Enable the Ctrl+Alt+Del screen.
result = client_settings_->put_DisableCtrlAltDel(0);
if (FAILED(result))
return LogOnCreateError(result);
// Disable printer and clipboard redirection.
result = client_settings_->put_DisableRdpdr(FALSE);
if (FAILED(result))
return LogOnCreateError(result);
// Do not display the connection bar.
result = client_settings_->put_DisplayConnectionBar(VARIANT_FALSE);
if (FAILED(result))
return LogOnCreateError(result);
// Do not grab focus on connect.
result = client_settings_->put_GrabFocusOnConnect(VARIANT_FALSE);
if (FAILED(result))
return LogOnCreateError(result);
// Enable enhanced graphics, font smoothing and desktop composition.
const LONG kDesiredFlags = WTS_PERF_ENABLE_ENHANCED_GRAPHICS |
WTS_PERF_ENABLE_FONT_SMOOTHING |
WTS_PERF_ENABLE_DESKTOP_COMPOSITION;
result = client_settings_->put_PerformanceFlags(kDesiredFlags);
if (FAILED(result))
return LogOnCreateError(result);
// Set the port to connect to.
result = client_settings_->put_RDPPort(server_endpoint_.port());
if (FAILED(result))
return LogOnCreateError(result);
result = client_->get_SecuredSettings2(secured_settings2.GetAddressOf());
if (SUCCEEDED(result)) {
result =
secured_settings2->put_AudioRedirectionMode(kRdpAudioModeRedirect);
if (FAILED(result))
return LogOnCreateError(result);
}
result = client_->get_SecuredSettings(secured_settings.GetAddressOf());
if (FAILED(result))
return LogOnCreateError(result);
// Set the terminal ID as the working directory for the initial program. It is
// observed that |WorkDir| is used only if an initial program is also
// specified, but is still passed to the RDP server and can then be read back
// from the session parameters. This makes it possible to use |WorkDir| to
// match the RDP connection with the session it is attached to.
//
// This code should be in sync with WtsTerminalMonitor::LookupTerminalId().
result = secured_settings->put_WorkDir(terminal_id);
if (FAILED(result))
return LogOnCreateError(result);
result = client_->Connect();
if (FAILED(result))
return LogOnCreateError(result);
return 0;
}
void RdpClientWindow::OnDestroy() {
client_.Reset();
client_9_.Reset();
client_settings_.Reset();
apply_resolution_timer_.Stop();
}
HRESULT RdpClientWindow::OnAuthenticationWarningDisplayed() {
LOG(WARNING) << "RDP: authentication warning is about to be shown.";
// Hook window activation to cancel any modal UI shown by the RDP control.
// This does not affect creation of other instances of the RDP control on this
// thread because the RDP control's window is hidden and is not activated.
window_activate_hook_ = WindowHook::Create();
return S_OK;
}
HRESULT RdpClientWindow::OnAuthenticationWarningDismissed() {
LOG(WARNING) << "RDP: authentication warning has been dismissed.";
window_activate_hook_ = nullptr;
return S_OK;
}
HRESULT RdpClientWindow::OnConnected() {
VLOG(1) << "RDP: successfully connected to " << server_endpoint_.ToString();
NotifyConnected();
return S_OK;
}
HRESULT RdpClientWindow::OnLoginComplete() {
VLOG(1) << "RDP: user successfully logged in.";
user_logged_in_ = true;
// Set up a timer to periodically apply pending screen size changes to the
// desktop. Attempting to set the resolution now seems to fail consistently,
// but succeeds after a brief timeout.
if (client_9_) {
apply_resolution_attempts_ = 0;
apply_resolution_timer_.Start(
FROM_HERE, kReapplyResolutionPeriod,
base::BindRepeating(&RdpClientWindow::ReapplyDesktopResolution,
Microsoft::WRL::ComPtr<RdpClientWindow>(this)));
}
return S_OK;
}
HRESULT RdpClientWindow::OnDisconnected(long reason) {
if (reason == kDisconnectReasonNoInfo ||
reason == kDisconnectReasonLocalNotError ||
reason == kDisconnectReasonRemoteByUser ||
reason == kDisconnectReasonByServer) {
VLOG(1) << "RDP: disconnected from " << server_endpoint_.ToString()
<< ", reason=" << reason;
NotifyDisconnected();
return S_OK;
}
// Get the extended disconnect reason code.
mstsc::ExtendedDisconnectReasonCode extended_code;
HRESULT result = client_->get_ExtendedDisconnectReason(&extended_code);
if (FAILED(result))
extended_code = mstsc::exDiscReasonNoInfo;
// Get the error message as well.
base::win::ScopedBstr error_message;
Microsoft::WRL::ComPtr<mstsc::IMsRdpClient5> client5;
result = client_.CopyTo(client5.GetAddressOf());
if (SUCCEEDED(result)) {
result = client5->GetErrorDescription(reason, extended_code,
error_message.Receive());
if (FAILED(result))
error_message.Reset();
}
LOG(ERROR) << "RDP: disconnected from " << server_endpoint_.ToString()
<< ": " << error_message << " (reason=" << reason
<< ", extended_code=" << extended_code << ")";
NotifyDisconnected();
return S_OK;
}
HRESULT RdpClientWindow::OnFatalError(long error_code) {
LOG(ERROR) << "RDP: an error occured: error_code="
<< error_code;
NotifyDisconnected();
return S_OK;
}
HRESULT RdpClientWindow::OnConfirmClose(VARIANT_BOOL* allow_close) {
*allow_close = VARIANT_TRUE;
NotifyDisconnected();
return S_OK;
}
int RdpClientWindow::LogOnCreateError(HRESULT error) {
LOG(ERROR) << "RDP: failed to initiate a connection to "
<< server_endpoint_.ToString() << ": error=" << std::hex << error;
client_.Reset();
client_9_.Reset();
client_settings_.Reset();
return -1;
}
void RdpClientWindow::NotifyConnected() {
if (event_handler_) {
event_handler_->OnConnected();
}
}
void RdpClientWindow::NotifyDisconnected() {
if (event_handler_) {
EventHandler* event_handler = event_handler_;
event_handler_ = nullptr;
event_handler->OnDisconnected();
}
}
HRESULT RdpClientWindow::UpdateDesktopResolution() {
if (!client_9_ || !user_logged_in_) {
return S_FALSE;
}
// UpdateSessionDisplaySettings() is poorly documented in MSDN and has a few
// quirks that should be noted.
// 1.) This method will only work when the user is logged into their session.
// 2.) The method may return E_UNEXPECTED until some amount of time (seconds)
// have elapsed after logging in to the user's session.
return client_9_->UpdateSessionDisplaySettings(
screen_resolution_.dimensions().width(),
screen_resolution_.dimensions().height(),
screen_resolution_.dimensions().width(),
screen_resolution_.dimensions().height(),
/*ulOrientation=*/0,
screen_resolution_.dpi().x(),
screen_resolution_.dpi().y());
}
void RdpClientWindow::ReapplyDesktopResolution() {
DCHECK_LT(apply_resolution_attempts_, kMaxResolutionReapplyAttempts);
HRESULT result = UpdateDesktopResolution();
apply_resolution_attempts_++;
if (SUCCEEDED(result)) {
// Successfully applied the new resolution so stop the retry timer.
apply_resolution_timer_.Stop();
} else if (apply_resolution_attempts_ == kMaxResolutionReapplyAttempts) {
// Only log an error on the last attempt to reduce log spam since a few
// errors can be expected and don't signal an actual failure.
LOG(WARNING) << "All UpdateSessionDisplaySettings() retries failed: 0x"
<< std::hex << result;
apply_resolution_timer_.Stop();
}
}
scoped_refptr<RdpClientWindow::WindowHook>
RdpClientWindow::WindowHook::Create() {
scoped_refptr<WindowHook> window_hook = g_window_hook.Pointer()->Get();
if (!window_hook.get()) {
window_hook = new WindowHook();
}
return window_hook;
}
RdpClientWindow::WindowHook::WindowHook() : hook_(nullptr) {
DCHECK(!g_window_hook.Pointer()->Get());
// Install a window hook to be called on window activation.
hook_ = SetWindowsHookEx(WH_CBT,
&WindowHook::CloseWindowOnActivation,
nullptr,
GetCurrentThreadId());
// Without the hook installed, RdpClientWindow will not be able to cancel
// modal UI windows. This will block the UI message loop so it is better to
// terminate the process now.
CHECK(hook_);
// Let CloseWindowOnActivation() to access the hook handle.
g_window_hook.Pointer()->Set(this);
}
RdpClientWindow::WindowHook::~WindowHook() {
DCHECK(g_window_hook.Pointer()->Get() == this);
g_window_hook.Pointer()->Set(nullptr);
BOOL result = UnhookWindowsHookEx(hook_);
DCHECK(result);
}
// static
LRESULT CALLBACK RdpClientWindow::WindowHook::CloseWindowOnActivation(
int code, WPARAM wparam, LPARAM lparam) {
// Get the hook handle.
HHOOK hook = g_window_hook.Pointer()->Get()->hook_;
if (code != HCBT_ACTIVATE) {
return CallNextHookEx(hook, code, wparam, lparam);
}
// Close the window once all pending window messages are processed.
HWND window = reinterpret_cast<HWND>(wparam);
LOG(WARNING) << "RDP: closing a window: " << std::hex << window;
::PostMessage(window, WM_CLOSE, 0, 0);
return 0;
}
} // namespace remoting