1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "remoting/host/win/rdp_client_window.h"
11 #include "base/lazy_instance.h"
12 #include "base/logging.h"
13 #include "base/strings/string16.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/threading/thread_local.h"
16 #include "base/win/scoped_bstr.h"
22 // RDP connection disconnect reasons codes that should not be interpreted as
24 const long kDisconnectReasonNoInfo
= 0;
25 const long kDisconnectReasonLocalNotError
= 1;
26 const long kDisconnectReasonRemoteByUser
= 2;
27 const long kDisconnectReasonByServer
= 3;
29 // Maximum length of a window class name including the terminating nullptr.
30 const int kMaxWindowClassLength
= 256;
32 // Each member of the array returned by GetKeyboardState() contains status data
33 // for a virtual key. If the high-order bit is 1, the key is down; otherwise, it
35 const BYTE kKeyPressedFlag
= 0x80;
37 const int kKeyboardStateLength
= 256;
39 // The RDP control creates 'IHWindowClass' window to handle keyboard input.
40 const wchar_t kRdpInputWindowClass
[] = L
"IHWindowClass";
43 // Redirect sounds to the client. This is the default value.
44 kRdpAudioModeRedirect
= 0,
46 // Play sounds at the remote computer. Equivalent to |kRdpAudioModeNone| if
47 // the remote computer is running a server SKU.
48 kRdpAudioModePlayOnServer
= 1,
50 // Disable sound redirection; do not play sounds at the remote computer.
54 // Points to a per-thread instance of the window activation hook handle.
55 base::LazyInstance
<base::ThreadLocalPointer
<RdpClientWindow::WindowHook
> >
56 g_window_hook
= LAZY_INSTANCE_INITIALIZER
;
58 // Finds a child window with the class name matching |class_name|. Unlike
59 // FindWindowEx() this function walks the tree of windows recursively. The walk
60 // is done in breadth-first order. The function returns nullptr if the child
61 // window could not be found.
62 HWND
FindWindowRecursively(HWND parent
, const base::string16
& class_name
) {
63 std::list
<HWND
> windows
;
64 windows
.push_back(parent
);
66 while (!windows
.empty()) {
67 HWND child
= FindWindowEx(windows
.front(), nullptr, nullptr, nullptr);
68 while (child
!= nullptr) {
69 // See if the window class name matches |class_name|.
70 WCHAR name
[kMaxWindowClassLength
];
71 int length
= GetClassName(child
, name
, arraysize(name
));
72 if (base::string16(name
, length
) == class_name
)
75 // Remember the window to look through its children.
76 windows
.push_back(child
);
78 // Go to the next child.
79 child
= FindWindowEx(windows
.front(), child
, nullptr, nullptr);
90 // Used to close any windows activated on a particular thread. It installs
91 // a WH_CBT window hook to track window activations and close all activated
92 // windows. There should be only one instance of |WindowHook| per thread
93 // at any given moment.
94 class RdpClientWindow::WindowHook
95 : public base::RefCounted
<WindowHook
> {
97 static scoped_refptr
<WindowHook
> Create();
100 friend class base::RefCounted
<WindowHook
>;
103 virtual ~WindowHook();
105 static LRESULT CALLBACK
CloseWindowOnActivation(
106 int code
, WPARAM wparam
, LPARAM lparam
);
110 DISALLOW_COPY_AND_ASSIGN(WindowHook
);
113 RdpClientWindow::RdpClientWindow(const net::IPEndPoint
& server_endpoint
,
114 const std::string
& terminal_id
,
115 EventHandler
* event_handler
)
116 : event_handler_(event_handler
),
117 server_endpoint_(server_endpoint
),
118 terminal_id_(terminal_id
) {
121 RdpClientWindow::~RdpClientWindow() {
125 DCHECK(!client_
.get());
126 DCHECK(!client_settings_
.get());
129 bool RdpClientWindow::Connect(const webrtc::DesktopSize
& screen_size
) {
132 screen_size_
= screen_size
;
133 RECT rect
= { 0, 0, screen_size_
.width(), screen_size_
.height() };
134 bool result
= Create(nullptr, rect
, nullptr) != nullptr;
136 // Hide the window since this class is about establishing a connection, not
137 // about showing a UI to the user.
144 void RdpClientWindow::Disconnect() {
146 SendMessage(WM_CLOSE
);
149 void RdpClientWindow::InjectSas() {
153 // Fins the window handling the keyboard input.
154 HWND input_window
= FindWindowRecursively(m_hWnd
, kRdpInputWindowClass
);
156 LOG(ERROR
) << "Failed to find the window handling the keyboard input.";
160 VLOG(3) << "Injecting Ctrl+Alt+End to emulate SAS.";
162 BYTE keyboard_state
[kKeyboardStateLength
];
163 if (!GetKeyboardState(keyboard_state
)) {
164 PLOG(ERROR
) << "Failed to get the keyboard state.";
168 // This code is running in Session 0, so we expect no keys to be pressed.
169 DCHECK(!(keyboard_state
[VK_CONTROL
] & kKeyPressedFlag
));
170 DCHECK(!(keyboard_state
[VK_MENU
] & kKeyPressedFlag
));
171 DCHECK(!(keyboard_state
[VK_END
] & kKeyPressedFlag
));
173 // Map virtual key codes to scan codes.
174 UINT control
= MapVirtualKey(VK_CONTROL
, MAPVK_VK_TO_VSC
);
175 UINT alt
= MapVirtualKey(VK_MENU
, MAPVK_VK_TO_VSC
);
176 UINT end
= MapVirtualKey(VK_END
, MAPVK_VK_TO_VSC
) | KF_EXTENDED
;
177 UINT up
= KF_UP
| KF_REPEAT
;
180 keyboard_state
[VK_CONTROL
] |= kKeyPressedFlag
;
181 keyboard_state
[VK_LCONTROL
] |= kKeyPressedFlag
;
182 CHECK(SetKeyboardState(keyboard_state
));
183 SendMessage(input_window
, WM_KEYDOWN
, VK_CONTROL
, MAKELPARAM(1, control
));
186 keyboard_state
[VK_MENU
] |= kKeyPressedFlag
;
187 keyboard_state
[VK_LMENU
] |= kKeyPressedFlag
;
188 CHECK(SetKeyboardState(keyboard_state
));
189 SendMessage(input_window
, WM_KEYDOWN
, VK_MENU
,
190 MAKELPARAM(1, alt
| KF_ALTDOWN
));
192 // Press and release 'End'.
193 SendMessage(input_window
, WM_KEYDOWN
, VK_END
,
194 MAKELPARAM(1, end
| KF_ALTDOWN
));
195 SendMessage(input_window
, WM_KEYUP
, VK_END
,
196 MAKELPARAM(1, end
| up
| KF_ALTDOWN
));
199 keyboard_state
[VK_MENU
] &= ~kKeyPressedFlag
;
200 keyboard_state
[VK_LMENU
] &= ~kKeyPressedFlag
;
201 CHECK(SetKeyboardState(keyboard_state
));
202 SendMessage(input_window
, WM_KEYUP
, VK_MENU
, MAKELPARAM(1, alt
| up
));
205 keyboard_state
[VK_CONTROL
] &= ~kKeyPressedFlag
;
206 keyboard_state
[VK_LCONTROL
] &= ~kKeyPressedFlag
;
207 CHECK(SetKeyboardState(keyboard_state
));
208 SendMessage(input_window
, WM_KEYUP
, VK_CONTROL
, MAKELPARAM(1, control
| up
));
211 void RdpClientWindow::OnClose() {
212 if (!client_
.get()) {
213 NotifyDisconnected();
217 // Request a graceful shutdown.
218 mstsc::ControlCloseStatus close_status
;
219 HRESULT result
= client_
->RequestClose(&close_status
);
220 if (FAILED(result
)) {
221 LOG(ERROR
) << "Failed to request a graceful shutdown of an RDP connection"
222 << ", result=0x" << std::hex
<< result
<< std::dec
;
223 NotifyDisconnected();
227 if (close_status
!= mstsc::controlCloseWaitForEvents
) {
228 NotifyDisconnected();
232 // Expect IMsTscAxEvents::OnConfirmClose() or IMsTscAxEvents::OnDisconnect()
233 // to be called if mstsc::controlCloseWaitForEvents was returned.
236 LRESULT
RdpClientWindow::OnCreate(CREATESTRUCT
* create_struct
) {
237 CAxWindow2 activex_window
;
238 base::win::ScopedComPtr
<IUnknown
> control
;
239 HRESULT result
= E_FAIL
;
240 base::win::ScopedComPtr
<mstsc::IMsTscSecuredSettings
> secured_settings
;
241 base::win::ScopedComPtr
<mstsc::IMsRdpClientSecuredSettings
> secured_settings2
;
242 base::win::ScopedBstr
server_name(
243 base::UTF8ToUTF16(server_endpoint_
.ToStringWithoutPort()).c_str());
244 base::win::ScopedBstr
terminal_id(base::UTF8ToUTF16(terminal_id_
).c_str());
246 // Create the child window that actually hosts the ActiveX control.
247 RECT rect
= { 0, 0, screen_size_
.width(), screen_size_
.height() };
248 activex_window
.Create(m_hWnd
, rect
, nullptr,
249 WS_CHILD
| WS_VISIBLE
| WS_BORDER
);
250 if (activex_window
.m_hWnd
== nullptr) {
251 result
= HRESULT_FROM_WIN32(GetLastError());
255 // Instantiate the RDP ActiveX control.
256 result
= activex_window
.CreateControlEx(
257 OLESTR("MsTscAx.MsTscAx"),
261 __uuidof(mstsc::IMsTscAxEvents
),
262 reinterpret_cast<IUnknown
*>(static_cast<RdpEventsSink
*>(this)));
266 result
= control
.QueryInterface(client_
.Receive());
271 result
= client_
->put_ColorDepth(32);
275 // Set dimensions of the remote desktop.
276 result
= client_
->put_DesktopWidth(screen_size_
.width());
279 result
= client_
->put_DesktopHeight(screen_size_
.height());
283 // Set the server name to connect to.
284 result
= client_
->put_Server(server_name
);
288 // Fetch IMsRdpClientAdvancedSettings interface for the client.
289 result
= client_
->get_AdvancedSettings2(client_settings_
.Receive());
293 // Disable background input mode.
294 result
= client_settings_
->put_allowBackgroundInput(0);
298 // Do not use bitmap cache.
299 result
= client_settings_
->put_BitmapPersistence(0);
300 if (SUCCEEDED(result
))
301 result
= client_settings_
->put_CachePersistenceActive(0);
305 // Do not use compression.
306 result
= client_settings_
->put_Compress(0);
310 // Enable the Ctrl+Alt+Del screen.
311 result
= client_settings_
->put_DisableCtrlAltDel(0);
315 // Disable printer and clipboard redirection.
316 result
= client_settings_
->put_DisableRdpdr(FALSE
);
320 // Do not display the connection bar.
321 result
= client_settings_
->put_DisplayConnectionBar(VARIANT_FALSE
);
325 // Do not grab focus on connect.
326 result
= client_settings_
->put_GrabFocusOnConnect(VARIANT_FALSE
);
330 // Enable enhanced graphics, font smoothing and desktop composition.
331 const LONG kDesiredFlags
= WTS_PERF_ENABLE_ENHANCED_GRAPHICS
|
332 WTS_PERF_ENABLE_FONT_SMOOTHING
|
333 WTS_PERF_ENABLE_DESKTOP_COMPOSITION
;
334 result
= client_settings_
->put_PerformanceFlags(kDesiredFlags
);
338 // Set the port to connect to.
339 result
= client_settings_
->put_RDPPort(server_endpoint_
.port());
343 // Disable audio in the session.
344 // TODO(alexeypa): re-enable audio redirection when http://crbug.com/242312 is
346 result
= client_
->get_SecuredSettings2(secured_settings2
.Receive());
347 if (SUCCEEDED(result
)) {
348 result
= secured_settings2
->put_AudioRedirectionMode(kRdpAudioModeNone
);
353 result
= client_
->get_SecuredSettings(secured_settings
.Receive());
357 // Set the terminal ID as the working directory for the initial program. It is
358 // observed that |WorkDir| is used only if an initial program is also
359 // specified, but is still passed to the RDP server and can then be read back
360 // from the session parameters. This makes it possible to use |WorkDir| to
361 // match the RDP connection with the session it is attached to.
363 // This code should be in sync with WtsTerminalMonitor::LookupTerminalId().
364 result
= secured_settings
->put_WorkDir(terminal_id
);
368 result
= client_
->Connect();
373 if (FAILED(result
)) {
374 LOG(ERROR
) << "RDP: failed to initiate a connection to "
375 << server_endpoint_
.ToString() << ": error="
376 << std::hex
<< result
<< std::dec
;
378 client_settings_
.Release();
385 void RdpClientWindow::OnDestroy() {
387 client_settings_
.Release();
390 HRESULT
RdpClientWindow::OnAuthenticationWarningDisplayed() {
391 LOG(WARNING
) << "RDP: authentication warning is about to be shown.";
393 // Hook window activation to cancel any modal UI shown by the RDP control.
394 // This does not affect creation of other instances of the RDP control on this
395 // thread because the RDP control's window is hidden and is not activated.
396 window_activate_hook_
= WindowHook::Create();
400 HRESULT
RdpClientWindow::OnAuthenticationWarningDismissed() {
401 LOG(WARNING
) << "RDP: authentication warning has been dismissed.";
403 window_activate_hook_
= nullptr;
407 HRESULT
RdpClientWindow::OnConnected() {
408 VLOG(1) << "RDP: successfully connected to " << server_endpoint_
.ToString();
414 HRESULT
RdpClientWindow::OnDisconnected(long reason
) {
415 if (reason
== kDisconnectReasonNoInfo
||
416 reason
== kDisconnectReasonLocalNotError
||
417 reason
== kDisconnectReasonRemoteByUser
||
418 reason
== kDisconnectReasonByServer
) {
419 VLOG(1) << "RDP: disconnected from " << server_endpoint_
.ToString()
420 << ", reason=" << reason
;
421 NotifyDisconnected();
425 // Get the extended disconnect reason code.
426 mstsc::ExtendedDisconnectReasonCode extended_code
;
427 HRESULT result
= client_
->get_ExtendedDisconnectReason(&extended_code
);
429 extended_code
= mstsc::exDiscReasonNoInfo
;
431 // Get the error message as well.
432 base::win::ScopedBstr error_message
;
433 base::win::ScopedComPtr
<mstsc::IMsRdpClient5
> client5
;
434 result
= client_
.QueryInterface(client5
.Receive());
435 if (SUCCEEDED(result
)) {
436 result
= client5
->GetErrorDescription(reason
, extended_code
,
437 error_message
.Receive());
439 error_message
.Reset();
442 LOG(ERROR
) << "RDP: disconnected from " << server_endpoint_
.ToString()
443 << ": " << error_message
<< " (reason=" << reason
444 << ", extended_code=" << extended_code
<< ")";
446 NotifyDisconnected();
450 HRESULT
RdpClientWindow::OnFatalError(long error_code
) {
451 LOG(ERROR
) << "RDP: an error occured: error_code="
454 NotifyDisconnected();
458 HRESULT
RdpClientWindow::OnConfirmClose(VARIANT_BOOL
* allow_close
) {
459 *allow_close
= VARIANT_TRUE
;
461 NotifyDisconnected();
465 void RdpClientWindow::NotifyConnected() {
467 event_handler_
->OnConnected();
470 void RdpClientWindow::NotifyDisconnected() {
471 if (event_handler_
) {
472 EventHandler
* event_handler
= event_handler_
;
473 event_handler_
= nullptr;
474 event_handler
->OnDisconnected();
478 scoped_refptr
<RdpClientWindow::WindowHook
>
479 RdpClientWindow::WindowHook::Create() {
480 scoped_refptr
<WindowHook
> window_hook
= g_window_hook
.Pointer()->Get();
482 if (!window_hook
.get())
483 window_hook
= new WindowHook();
488 RdpClientWindow::WindowHook::WindowHook() : hook_(nullptr) {
489 DCHECK(!g_window_hook
.Pointer()->Get());
491 // Install a window hook to be called on window activation.
492 hook_
= SetWindowsHookEx(WH_CBT
,
493 &WindowHook::CloseWindowOnActivation
,
495 GetCurrentThreadId());
496 // Without the hook installed, RdpClientWindow will not be able to cancel
497 // modal UI windows. This will block the UI message loop so it is better to
498 // terminate the process now.
501 // Let CloseWindowOnActivation() to access the hook handle.
502 g_window_hook
.Pointer()->Set(this);
505 RdpClientWindow::WindowHook::~WindowHook() {
506 DCHECK(g_window_hook
.Pointer()->Get() == this);
508 g_window_hook
.Pointer()->Set(nullptr);
510 BOOL result
= UnhookWindowsHookEx(hook_
);
515 LRESULT CALLBACK
RdpClientWindow::WindowHook::CloseWindowOnActivation(
516 int code
, WPARAM wparam
, LPARAM lparam
) {
517 // Get the hook handle.
518 HHOOK hook
= g_window_hook
.Pointer()->Get()->hook_
;
520 if (code
!= HCBT_ACTIVATE
)
521 return CallNextHookEx(hook
, code
, wparam
, lparam
);
523 // Close the window once all pending window messages are processed.
524 HWND window
= reinterpret_cast<HWND
>(wparam
);
525 LOG(WARNING
) << "RDP: closing a window: " << std::hex
<< window
<< std::dec
;
526 ::PostMessage(window
, WM_CLOSE
, 0, 0);
530 } // namespace remoting