Roll src/third_party/WebKit d9c6159:8139f33 (svn 201974:201975)
[chromium-blink-merge.git] / remoting / host / win / rdp_client_window.cc
blob8516195e2d80bf5d172bcda7f077e4f7010d4aa6
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"
7 #include <wtsdefs.h>
9 #include <list>
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"
18 namespace remoting {
20 namespace {
22 // RDP connection disconnect reasons codes that should not be interpreted as
23 // errors.
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
34 // is up.
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";
42 enum RdpAudioMode {
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.
51 kRdpAudioModeNone = 2
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)
73 return child;
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);
82 windows.pop_front();
85 return nullptr;
88 } // namespace
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> {
96 public:
97 static scoped_refptr<WindowHook> Create();
99 private:
100 friend class base::RefCounted<WindowHook>;
102 WindowHook();
103 virtual ~WindowHook();
105 static LRESULT CALLBACK CloseWindowOnActivation(
106 int code, WPARAM wparam, LPARAM lparam);
108 HHOOK hook_;
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() {
122 if (m_hWnd)
123 DestroyWindow();
125 DCHECK(!client_.get());
126 DCHECK(!client_settings_.get());
129 bool RdpClientWindow::Connect(const webrtc::DesktopSize& screen_size) {
130 DCHECK(!m_hWnd);
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.
138 if (result)
139 ShowWindow(SW_HIDE);
141 return result;
144 void RdpClientWindow::Disconnect() {
145 if (m_hWnd)
146 SendMessage(WM_CLOSE);
149 void RdpClientWindow::InjectSas() {
150 if (!m_hWnd)
151 return;
153 // Fins the window handling the keyboard input.
154 HWND input_window = FindWindowRecursively(m_hWnd, kRdpInputWindowClass);
155 if (!input_window) {
156 LOG(ERROR) << "Failed to find the window handling the keyboard input.";
157 return;
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.";
165 return;
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;
179 // Press 'Ctrl'.
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));
185 // Press 'Alt'.
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));
198 // Release 'Alt'.
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));
204 // Release 'Ctrl'.
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();
214 return;
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();
224 return;
227 if (close_status != mstsc::controlCloseWaitForEvents) {
228 NotifyDisconnected();
229 return;
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"),
256 nullptr,
257 nullptr,
258 control.Receive(),
259 __uuidof(mstsc::IMsTscAxEvents),
260 reinterpret_cast<IUnknown*>(static_cast<RdpEventsSink*>(this)));
261 if (FAILED(result))
262 return LogOnCreateError(result);
264 result = control.QueryInterface(client_.Receive());
265 if (FAILED(result))
266 return LogOnCreateError(result);
268 // Use 32-bit color.
269 result = client_->put_ColorDepth(32);
270 if (FAILED(result))
271 return LogOnCreateError(result);
273 // Set dimensions of the remote desktop.
274 result = client_->put_DesktopWidth(screen_size_.width());
275 if (FAILED(result))
276 return LogOnCreateError(result);
277 result = client_->put_DesktopHeight(screen_size_.height());
278 if (FAILED(result))
279 return LogOnCreateError(result);
281 // Set the server name to connect to.
282 result = client_->put_Server(server_name);
283 if (FAILED(result))
284 return LogOnCreateError(result);
286 // Fetch IMsRdpClientAdvancedSettings interface for the client.
287 result = client_->get_AdvancedSettings2(client_settings_.Receive());
288 if (FAILED(result))
289 return LogOnCreateError(result);
291 // Disable background input mode.
292 result = client_settings_->put_allowBackgroundInput(0);
293 if (FAILED(result))
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);
300 if (FAILED(result))
301 return LogOnCreateError(result);
303 // Do not use compression.
304 result = client_settings_->put_Compress(0);
305 if (FAILED(result))
306 return LogOnCreateError(result);
308 // Enable the Ctrl+Alt+Del screen.
309 result = client_settings_->put_DisableCtrlAltDel(0);
310 if (FAILED(result))
311 return LogOnCreateError(result);
313 // Disable printer and clipboard redirection.
314 result = client_settings_->put_DisableRdpdr(FALSE);
315 if (FAILED(result))
316 return LogOnCreateError(result);
318 // Do not display the connection bar.
319 result = client_settings_->put_DisplayConnectionBar(VARIANT_FALSE);
320 if (FAILED(result))
321 return LogOnCreateError(result);
323 // Do not grab focus on connect.
324 result = client_settings_->put_GrabFocusOnConnect(VARIANT_FALSE);
325 if (FAILED(result))
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);
333 if (FAILED(result))
334 return LogOnCreateError(result);
336 // Set the port to connect to.
337 result = client_settings_->put_RDPPort(server_endpoint_.port());
338 if (FAILED(result))
339 return LogOnCreateError(result);
341 // Disable audio in the session.
342 // TODO(alexeypa): re-enable audio redirection when http://crbug.com/242312 is
343 // fixed.
344 result = client_->get_SecuredSettings2(secured_settings2.Receive());
345 if (SUCCEEDED(result)) {
346 result = secured_settings2->put_AudioRedirectionMode(kRdpAudioModeNone);
347 if (FAILED(result))
348 return LogOnCreateError(result);
351 result = client_->get_SecuredSettings(secured_settings.Receive());
352 if (FAILED(result))
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);
363 if (FAILED(result))
364 return LogOnCreateError(result);
366 result = client_->Connect();
367 if (FAILED(result))
368 return LogOnCreateError(result);
370 return 0;
373 void RdpClientWindow::OnDestroy() {
374 client_.Release();
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();
385 return S_OK;
388 HRESULT RdpClientWindow::OnAuthenticationWarningDismissed() {
389 LOG(WARNING) << "RDP: authentication warning has been dismissed.";
391 window_activate_hook_ = nullptr;
392 return S_OK;
395 HRESULT RdpClientWindow::OnConnected() {
396 VLOG(1) << "RDP: successfully connected to " << server_endpoint_.ToString();
398 NotifyConnected();
399 return S_OK;
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();
410 return S_OK;
413 // Get the extended disconnect reason code.
414 mstsc::ExtendedDisconnectReasonCode extended_code;
415 HRESULT result = client_->get_ExtendedDisconnectReason(&extended_code);
416 if (FAILED(result))
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());
426 if (FAILED(result))
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();
435 return S_OK;
438 HRESULT RdpClientWindow::OnFatalError(long error_code) {
439 LOG(ERROR) << "RDP: an error occured: error_code="
440 << error_code;
442 NotifyDisconnected();
443 return S_OK;
446 HRESULT RdpClientWindow::OnConfirmClose(VARIANT_BOOL* allow_close) {
447 *allow_close = VARIANT_TRUE;
449 NotifyDisconnected();
450 return S_OK;
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;
457 client_.Release();
458 client_settings_.Release();
459 return -1;
462 void RdpClientWindow::NotifyConnected() {
463 if (event_handler_)
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();
482 return window_hook;
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,
491 nullptr,
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.
496 CHECK(hook_);
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_);
508 DCHECK(result);
511 // static
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);
524 return 0;
527 } // namespace remoting