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 return LogOnCreateError(HRESULT_FROM_WIN32(GetLastError()));
253 // Instantiate the RDP ActiveX control.
254 result
= activex_window
.CreateControlEx(
255 OLESTR("MsTscAx.MsTscAx"),
259 __uuidof(mstsc::IMsTscAxEvents
),
260 reinterpret_cast<IUnknown
*>(static_cast<RdpEventsSink
*>(this)));
262 return LogOnCreateError(result
);
264 result
= control
.QueryInterface(client_
.Receive());
266 return LogOnCreateError(result
);
269 result
= client_
->put_ColorDepth(32);
271 return LogOnCreateError(result
);
273 // Set dimensions of the remote desktop.
274 result
= client_
->put_DesktopWidth(screen_size_
.width());
276 return LogOnCreateError(result
);
277 result
= client_
->put_DesktopHeight(screen_size_
.height());
279 return LogOnCreateError(result
);
281 // Set the server name to connect to.
282 result
= client_
->put_Server(server_name
);
284 return LogOnCreateError(result
);
286 // Fetch IMsRdpClientAdvancedSettings interface for the client.
287 result
= client_
->get_AdvancedSettings2(client_settings_
.Receive());
289 return LogOnCreateError(result
);
291 // Disable background input mode.
292 result
= client_settings_
->put_allowBackgroundInput(0);
294 return LogOnCreateError(result
);
296 // Do not use bitmap cache.
297 result
= client_settings_
->put_BitmapPersistence(0);
298 if (SUCCEEDED(result
))
299 result
= client_settings_
->put_CachePersistenceActive(0);
301 return LogOnCreateError(result
);
303 // Do not use compression.
304 result
= client_settings_
->put_Compress(0);
306 return LogOnCreateError(result
);
308 // Enable the Ctrl+Alt+Del screen.
309 result
= client_settings_
->put_DisableCtrlAltDel(0);
311 return LogOnCreateError(result
);
313 // Disable printer and clipboard redirection.
314 result
= client_settings_
->put_DisableRdpdr(FALSE
);
316 return LogOnCreateError(result
);
318 // Do not display the connection bar.
319 result
= client_settings_
->put_DisplayConnectionBar(VARIANT_FALSE
);
321 return LogOnCreateError(result
);
323 // Do not grab focus on connect.
324 result
= client_settings_
->put_GrabFocusOnConnect(VARIANT_FALSE
);
326 return LogOnCreateError(result
);
328 // Enable enhanced graphics, font smoothing and desktop composition.
329 const LONG kDesiredFlags
= WTS_PERF_ENABLE_ENHANCED_GRAPHICS
|
330 WTS_PERF_ENABLE_FONT_SMOOTHING
|
331 WTS_PERF_ENABLE_DESKTOP_COMPOSITION
;
332 result
= client_settings_
->put_PerformanceFlags(kDesiredFlags
);
334 return LogOnCreateError(result
);
336 // Set the port to connect to.
337 result
= client_settings_
->put_RDPPort(server_endpoint_
.port());
339 return LogOnCreateError(result
);
341 // Disable audio in the session.
342 // TODO(alexeypa): re-enable audio redirection when http://crbug.com/242312 is
344 result
= client_
->get_SecuredSettings2(secured_settings2
.Receive());
345 if (SUCCEEDED(result
)) {
346 result
= secured_settings2
->put_AudioRedirectionMode(kRdpAudioModeNone
);
348 return LogOnCreateError(result
);
351 result
= client_
->get_SecuredSettings(secured_settings
.Receive());
353 return LogOnCreateError(result
);
355 // Set the terminal ID as the working directory for the initial program. It is
356 // observed that |WorkDir| is used only if an initial program is also
357 // specified, but is still passed to the RDP server and can then be read back
358 // from the session parameters. This makes it possible to use |WorkDir| to
359 // match the RDP connection with the session it is attached to.
361 // This code should be in sync with WtsTerminalMonitor::LookupTerminalId().
362 result
= secured_settings
->put_WorkDir(terminal_id
);
364 return LogOnCreateError(result
);
366 result
= client_
->Connect();
368 return LogOnCreateError(result
);
373 void RdpClientWindow::OnDestroy() {
375 client_settings_
.Release();
378 HRESULT
RdpClientWindow::OnAuthenticationWarningDisplayed() {
379 LOG(WARNING
) << "RDP: authentication warning is about to be shown.";
381 // Hook window activation to cancel any modal UI shown by the RDP control.
382 // This does not affect creation of other instances of the RDP control on this
383 // thread because the RDP control's window is hidden and is not activated.
384 window_activate_hook_
= WindowHook::Create();
388 HRESULT
RdpClientWindow::OnAuthenticationWarningDismissed() {
389 LOG(WARNING
) << "RDP: authentication warning has been dismissed.";
391 window_activate_hook_
= nullptr;
395 HRESULT
RdpClientWindow::OnConnected() {
396 VLOG(1) << "RDP: successfully connected to " << server_endpoint_
.ToString();
402 HRESULT
RdpClientWindow::OnDisconnected(long reason
) {
403 if (reason
== kDisconnectReasonNoInfo
||
404 reason
== kDisconnectReasonLocalNotError
||
405 reason
== kDisconnectReasonRemoteByUser
||
406 reason
== kDisconnectReasonByServer
) {
407 VLOG(1) << "RDP: disconnected from " << server_endpoint_
.ToString()
408 << ", reason=" << reason
;
409 NotifyDisconnected();
413 // Get the extended disconnect reason code.
414 mstsc::ExtendedDisconnectReasonCode extended_code
;
415 HRESULT result
= client_
->get_ExtendedDisconnectReason(&extended_code
);
417 extended_code
= mstsc::exDiscReasonNoInfo
;
419 // Get the error message as well.
420 base::win::ScopedBstr error_message
;
421 base::win::ScopedComPtr
<mstsc::IMsRdpClient5
> client5
;
422 result
= client_
.QueryInterface(client5
.Receive());
423 if (SUCCEEDED(result
)) {
424 result
= client5
->GetErrorDescription(reason
, extended_code
,
425 error_message
.Receive());
427 error_message
.Reset();
430 LOG(ERROR
) << "RDP: disconnected from " << server_endpoint_
.ToString()
431 << ": " << error_message
<< " (reason=" << reason
432 << ", extended_code=" << extended_code
<< ")";
434 NotifyDisconnected();
438 HRESULT
RdpClientWindow::OnFatalError(long error_code
) {
439 LOG(ERROR
) << "RDP: an error occured: error_code="
442 NotifyDisconnected();
446 HRESULT
RdpClientWindow::OnConfirmClose(VARIANT_BOOL
* allow_close
) {
447 *allow_close
= VARIANT_TRUE
;
449 NotifyDisconnected();
453 int RdpClientWindow::LogOnCreateError(HRESULT error
) {
454 LOG(ERROR
) << "RDP: failed to initiate a connection to "
455 << server_endpoint_
.ToString() << ": error="
456 << std::hex
<< error
<< std::dec
;
458 client_settings_
.Release();
462 void RdpClientWindow::NotifyConnected() {
464 event_handler_
->OnConnected();
467 void RdpClientWindow::NotifyDisconnected() {
468 if (event_handler_
) {
469 EventHandler
* event_handler
= event_handler_
;
470 event_handler_
= nullptr;
471 event_handler
->OnDisconnected();
475 scoped_refptr
<RdpClientWindow::WindowHook
>
476 RdpClientWindow::WindowHook::Create() {
477 scoped_refptr
<WindowHook
> window_hook
= g_window_hook
.Pointer()->Get();
479 if (!window_hook
.get())
480 window_hook
= new WindowHook();
485 RdpClientWindow::WindowHook::WindowHook() : hook_(nullptr) {
486 DCHECK(!g_window_hook
.Pointer()->Get());
488 // Install a window hook to be called on window activation.
489 hook_
= SetWindowsHookEx(WH_CBT
,
490 &WindowHook::CloseWindowOnActivation
,
492 GetCurrentThreadId());
493 // Without the hook installed, RdpClientWindow will not be able to cancel
494 // modal UI windows. This will block the UI message loop so it is better to
495 // terminate the process now.
498 // Let CloseWindowOnActivation() to access the hook handle.
499 g_window_hook
.Pointer()->Set(this);
502 RdpClientWindow::WindowHook::~WindowHook() {
503 DCHECK(g_window_hook
.Pointer()->Get() == this);
505 g_window_hook
.Pointer()->Set(nullptr);
507 BOOL result
= UnhookWindowsHookEx(hook_
);
512 LRESULT CALLBACK
RdpClientWindow::WindowHook::CloseWindowOnActivation(
513 int code
, WPARAM wparam
, LPARAM lparam
) {
514 // Get the hook handle.
515 HHOOK hook
= g_window_hook
.Pointer()->Get()->hook_
;
517 if (code
!= HCBT_ACTIVATE
)
518 return CallNextHookEx(hook
, code
, wparam
, lparam
);
520 // Close the window once all pending window messages are processed.
521 HWND window
= reinterpret_cast<HWND
>(wparam
);
522 LOG(WARNING
) << "RDP: closing a window: " << std::hex
<< window
<< std::dec
;
523 ::PostMessage(window
, WM_CLOSE
, 0, 0);
527 } // namespace remoting