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.
4
5#include "remoting/host/win/rdp_client_window.h"
6
7#include <wtsdefs.h>
8
9#include <list>
10
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"
17
18namespace remoting {
19
20namespace {
21
22// RDP connection disconnect reasons codes that should not be interpreted as
23// errors.
24const long kDisconnectReasonNoInfo = 0;
25const long kDisconnectReasonLocalNotError = 1;
26const long kDisconnectReasonRemoteByUser = 2;
27const long kDisconnectReasonByServer = 3;
28
29// Maximum length of a window class name including the terminating NULL.
30const int kMaxWindowClassLength = 256;
31
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.
35const BYTE kKeyPressedFlag = 0x80;
36
37const int kKeyboardStateLength = 256;
38
39// The RDP control creates 'IHWindowClass' window to handle keyboard input.
40const wchar_t kRdpInputWindowClass[] = L"IHWindowClass";
41
42enum RdpAudioMode {
43  // Redirect sounds to the client. This is the default value.
44  kRdpAudioModeRedirect = 0,
45
46  // Play sounds at the remote computer. Equivalent to |kRdpAudioModeNone| if
47  // the remote computer is running a server SKU.
48  kRdpAudioModePlayOnServer = 1,
49
50  // Disable sound redirection; do not play sounds at the remote computer.
51  kRdpAudioModeNone = 2
52};
53
54// Points to a per-thread instance of the window activation hook handle.
55base::LazyInstance<base::ThreadLocalPointer<RdpClientWindow::WindowHook> >
56    g_window_hook = LAZY_INSTANCE_INITIALIZER;
57
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 NULL if the child window
61// could not be found.
62HWND FindWindowRecursively(HWND parent, const base::string16& class_name) {
63  std::list<HWND> windows;
64  windows.push_back(parent);
65
66  while (!windows.empty()) {
67    HWND child = FindWindowEx(windows.front(), NULL, NULL, NULL);
68    while (child != NULL) {
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;
74
75      // Remember the window to look through its children.
76      windows.push_back(child);
77
78      // Go to the next child.
79      child = FindWindowEx(windows.front(), child, NULL, NULL);
80    }
81
82    windows.pop_front();
83  }
84
85  return NULL;
86}
87
88}  // namespace
89
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.
94class RdpClientWindow::WindowHook
95    : public base::RefCounted<WindowHook> {
96 public:
97  static scoped_refptr<WindowHook> Create();
98
99 private:
100  friend class base::RefCounted<WindowHook>;
101
102  WindowHook();
103  virtual ~WindowHook();
104
105  static LRESULT CALLBACK CloseWindowOnActivation(
106      int code, WPARAM wparam, LPARAM lparam);
107
108  HHOOK hook_;
109
110  DISALLOW_COPY_AND_ASSIGN(WindowHook);
111};
112
113RdpClientWindow::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) {
119}
120
121RdpClientWindow::~RdpClientWindow() {
122  if (m_hWnd)
123    DestroyWindow();
124
125  DCHECK(!client_);
126  DCHECK(!client_settings_);
127}
128
129bool RdpClientWindow::Connect(const webrtc::DesktopSize& screen_size) {
130  DCHECK(!m_hWnd);
131
132  screen_size_ = screen_size;
133  RECT rect = { 0, 0, screen_size_.width(), screen_size_.height() };
134  bool result = Create(NULL, rect, NULL) != NULL;
135
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);
140
141  return result;
142}
143
144void RdpClientWindow::Disconnect() {
145  if (m_hWnd)
146    SendMessage(WM_CLOSE);
147}
148
149void RdpClientWindow::InjectSas() {
150  if (!m_hWnd)
151    return;
152
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;
158  }
159
160  VLOG(3) << "Injecting Ctrl+Alt+End to emulate SAS.";
161
162  BYTE keyboard_state[kKeyboardStateLength];
163  if (!GetKeyboardState(keyboard_state)) {
164    PLOG(ERROR) << "Failed to get the keyboard state.";
165    return;
166  }
167
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));
172
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;
178
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));
184
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));
191
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));
197
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));
203
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));
209}
210
211void RdpClientWindow::OnClose() {
212  if (!client_) {
213    NotifyDisconnected();
214    return;
215  }
216
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;
225  }
226
227  if (close_status != mstsc::controlCloseWaitForEvents) {
228    NotifyDisconnected();
229    return;
230  }
231
232  // Expect IMsTscAxEvents::OnConfirmClose() or IMsTscAxEvents::OnDisconnect()
233  // to be called if mstsc::controlCloseWaitForEvents was returned.
234}
235
236LRESULT 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());
245
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, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER);
249  if (activex_window.m_hWnd == NULL) {
250    result = HRESULT_FROM_WIN32(GetLastError());
251    goto done;
252  }
253
254  // Instantiate the RDP ActiveX control.
255  result = activex_window.CreateControlEx(
256      OLESTR("MsTscAx.MsTscAx"),
257      NULL,
258      NULL,
259      control.Receive(),
260      __uuidof(mstsc::IMsTscAxEvents),
261      reinterpret_cast<IUnknown*>(static_cast<RdpEventsSink*>(this)));
262  if (FAILED(result))
263    goto done;
264
265  result = control.QueryInterface(client_.Receive());
266  if (FAILED(result))
267    goto done;
268
269  // Use 32-bit color.
270  result = client_->put_ColorDepth(32);
271  if (FAILED(result))
272    goto done;
273
274  // Set dimensions of the remote desktop.
275  result = client_->put_DesktopWidth(screen_size_.width());
276  if (FAILED(result))
277    goto done;
278  result = client_->put_DesktopHeight(screen_size_.height());
279  if (FAILED(result))
280    goto done;
281
282  // Set the server name to connect to.
283  result = client_->put_Server(server_name);
284  if (FAILED(result))
285    goto done;
286
287  // Fetch IMsRdpClientAdvancedSettings interface for the client.
288  result = client_->get_AdvancedSettings2(client_settings_.Receive());
289  if (FAILED(result))
290    goto done;
291
292  // Disable background input mode.
293  result = client_settings_->put_allowBackgroundInput(0);
294  if (FAILED(result))
295    goto done;
296
297  // Do not use bitmap cache.
298  result = client_settings_->put_BitmapPersistence(0);
299  if (SUCCEEDED(result))
300    result = client_settings_->put_CachePersistenceActive(0);
301  if (FAILED(result))
302    goto done;
303
304  // Do not use compression.
305  result = client_settings_->put_Compress(0);
306  if (FAILED(result))
307    goto done;
308
309  // Enable the Ctrl+Alt+Del screen.
310  result = client_settings_->put_DisableCtrlAltDel(0);
311  if (FAILED(result))
312    goto done;
313
314  // Disable printer and clipboard redirection.
315  result = client_settings_->put_DisableRdpdr(FALSE);
316  if (FAILED(result))
317    goto done;
318
319  // Do not display the connection bar.
320  result = client_settings_->put_DisplayConnectionBar(VARIANT_FALSE);
321  if (FAILED(result))
322    goto done;
323
324  // Do not grab focus on connect.
325  result = client_settings_->put_GrabFocusOnConnect(VARIANT_FALSE);
326  if (FAILED(result))
327    goto done;
328
329  // Enable enhanced graphics, font smoothing and desktop composition.
330  const LONG kDesiredFlags = WTS_PERF_ENABLE_ENHANCED_GRAPHICS |
331                             WTS_PERF_ENABLE_FONT_SMOOTHING |
332                             WTS_PERF_ENABLE_DESKTOP_COMPOSITION;
333  result = client_settings_->put_PerformanceFlags(kDesiredFlags);
334  if (FAILED(result))
335    goto done;
336
337  // Set the port to connect to.
338  result = client_settings_->put_RDPPort(server_endpoint_.port());
339  if (FAILED(result))
340    goto done;
341
342  // Disable audio in the session.
343  // TODO(alexeypa): re-enable audio redirection when http://crbug.com/242312 is
344  // fixed.
345  result = client_->get_SecuredSettings2(secured_settings2.Receive());
346  if (SUCCEEDED(result)) {
347    result = secured_settings2->put_AudioRedirectionMode(kRdpAudioModeNone);
348    if (FAILED(result))
349      goto done;
350  }
351
352  result = client_->get_SecuredSettings(secured_settings.Receive());
353  if (FAILED(result))
354    goto done;
355
356  // Set the terminal ID as the working directory for the initial program. It is
357  // observed that |WorkDir| is used only if an initial program is also
358  // specified, but is still passed to the RDP server and can then be read back
359  // from the session parameters. This makes it possible to use |WorkDir| to
360  // match the RDP connection with the session it is attached to.
361  //
362  // This code should be in sync with WtsTerminalMonitor::LookupTerminalId().
363  result = secured_settings->put_WorkDir(terminal_id);
364  if (FAILED(result))
365    goto done;
366
367  result = client_->Connect();
368  if (FAILED(result))
369    goto done;
370
371done:
372  if (FAILED(result)) {
373    LOG(ERROR) << "RDP: failed to initiate a connection to "
374               << server_endpoint_.ToString() << ": error="
375               << std::hex << result << std::dec;
376    client_.Release();
377    client_settings_.Release();
378    return -1;
379  }
380
381  return 0;
382}
383
384void RdpClientWindow::OnDestroy() {
385  client_.Release();
386  client_settings_.Release();
387}
388
389HRESULT RdpClientWindow::OnAuthenticationWarningDisplayed() {
390  LOG(WARNING) << "RDP: authentication warning is about to be shown.";
391
392  // Hook window activation to cancel any modal UI shown by the RDP control.
393  // This does not affect creation of other instances of the RDP control on this
394  // thread because the RDP control's window is hidden and is not activated.
395  window_activate_hook_ = WindowHook::Create();
396  return S_OK;
397}
398
399HRESULT RdpClientWindow::OnAuthenticationWarningDismissed() {
400  LOG(WARNING) << "RDP: authentication warning has been dismissed.";
401
402  window_activate_hook_ = NULL;
403  return S_OK;
404}
405
406HRESULT RdpClientWindow::OnConnected() {
407  VLOG(1) << "RDP: successfully connected to " << server_endpoint_.ToString();
408
409  NotifyConnected();
410  return S_OK;
411}
412
413HRESULT RdpClientWindow::OnDisconnected(long reason) {
414  if (reason == kDisconnectReasonNoInfo ||
415      reason == kDisconnectReasonLocalNotError ||
416      reason == kDisconnectReasonRemoteByUser ||
417      reason == kDisconnectReasonByServer) {
418    VLOG(1) << "RDP: disconnected from " << server_endpoint_.ToString()
419            << ", reason=" << reason;
420    NotifyDisconnected();
421    return S_OK;
422  }
423
424  // Get the extended disconnect reason code.
425  mstsc::ExtendedDisconnectReasonCode extended_code;
426  HRESULT result = client_->get_ExtendedDisconnectReason(&extended_code);
427  if (FAILED(result))
428    extended_code = mstsc::exDiscReasonNoInfo;
429
430  // Get the error message as well.
431  base::win::ScopedBstr error_message;
432  base::win::ScopedComPtr<mstsc::IMsRdpClient5> client5;
433  result = client_.QueryInterface(client5.Receive());
434  if (SUCCEEDED(result)) {
435    result = client5->GetErrorDescription(reason, extended_code,
436                                          error_message.Receive());
437    if (FAILED(result))
438      error_message.Reset();
439  }
440
441  LOG(ERROR) << "RDP: disconnected from " << server_endpoint_.ToString()
442             << ": " << error_message << " (reason=" << reason
443             << ", extended_code=" << extended_code << ")";
444
445  NotifyDisconnected();
446  return S_OK;
447}
448
449HRESULT RdpClientWindow::OnFatalError(long error_code) {
450  LOG(ERROR) << "RDP: an error occured: error_code="
451             << error_code;
452
453  NotifyDisconnected();
454  return S_OK;
455}
456
457HRESULT RdpClientWindow::OnConfirmClose(VARIANT_BOOL* allow_close) {
458  *allow_close = VARIANT_TRUE;
459
460  NotifyDisconnected();
461  return S_OK;
462}
463
464void RdpClientWindow::NotifyConnected() {
465  if (event_handler_)
466    event_handler_->OnConnected();
467}
468
469void RdpClientWindow::NotifyDisconnected() {
470  if (event_handler_) {
471    EventHandler* event_handler = event_handler_;
472    event_handler_ = NULL;
473    event_handler->OnDisconnected();
474  }
475}
476
477scoped_refptr<RdpClientWindow::WindowHook>
478RdpClientWindow::WindowHook::Create() {
479  scoped_refptr<WindowHook> window_hook = g_window_hook.Pointer()->Get();
480
481  if (!window_hook)
482    window_hook = new WindowHook();
483
484  return window_hook;
485}
486
487RdpClientWindow::WindowHook::WindowHook() : hook_(NULL) {
488  DCHECK(!g_window_hook.Pointer()->Get());
489
490  // Install a window hook to be called on window activation.
491  hook_ = SetWindowsHookEx(WH_CBT,
492                           &WindowHook::CloseWindowOnActivation,
493                           NULL,
494                           GetCurrentThreadId());
495  // Without the hook installed, RdpClientWindow will not be able to cancel
496  // modal UI windows. This will block the UI message loop so it is better to
497  // terminate the process now.
498  CHECK(hook_);
499
500  // Let CloseWindowOnActivation() to access the hook handle.
501  g_window_hook.Pointer()->Set(this);
502}
503
504RdpClientWindow::WindowHook::~WindowHook() {
505  DCHECK(g_window_hook.Pointer()->Get() == this);
506
507  g_window_hook.Pointer()->Set(NULL);
508
509  BOOL result = UnhookWindowsHookEx(hook_);
510  DCHECK(result);
511}
512
513// static
514LRESULT CALLBACK RdpClientWindow::WindowHook::CloseWindowOnActivation(
515    int code, WPARAM wparam, LPARAM lparam) {
516  // Get the hook handle.
517  HHOOK hook = g_window_hook.Pointer()->Get()->hook_;
518
519  if (code != HCBT_ACTIVATE)
520    return CallNextHookEx(hook, code, wparam, lparam);
521
522  // Close the window once all pending window messages are processed.
523  HWND window = reinterpret_cast<HWND>(wparam);
524  LOG(WARNING) << "RDP: closing a window: " << std::hex << window << std::dec;
525  ::PostMessage(window, WM_CLOSE, 0, 0);
526  return 0;
527}
528
529}  // namespace remoting
530