• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 namespace remoting {
19 
20 namespace {
21 
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;
28 
29 // Maximum length of a window class name including the terminating NULL.
30 const 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.
35 const BYTE kKeyPressedFlag = 0x80;
36 
37 const int kKeyboardStateLength = 256;
38 
39 // The RDP control creates 'IHWindowClass' window to handle keyboard input.
40 const wchar_t kRdpInputWindowClass[] = L"IHWindowClass";
41 
42 enum 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.
55 base::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.
FindWindowRecursively(HWND parent,const base::string16 & class_name)62 HWND 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.
94 class 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 
RdpClientWindow(const net::IPEndPoint & server_endpoint,const std::string & terminal_id,EventHandler * event_handler)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) {
119 }
120 
~RdpClientWindow()121 RdpClientWindow::~RdpClientWindow() {
122   if (m_hWnd)
123     DestroyWindow();
124 
125   DCHECK(!client_);
126   DCHECK(!client_settings_);
127 }
128 
Connect(const webrtc::DesktopSize & screen_size)129 bool 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 
Disconnect()144 void RdpClientWindow::Disconnect() {
145   if (m_hWnd)
146     SendMessage(WM_CLOSE);
147 }
148 
InjectSas()149 void 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 
OnClose()211 void 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 
OnCreate(CREATESTRUCT * create_struct)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());
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 
371 done:
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 
OnDestroy()384 void RdpClientWindow::OnDestroy() {
385   client_.Release();
386   client_settings_.Release();
387 }
388 
OnAuthenticationWarningDisplayed()389 HRESULT 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 
OnAuthenticationWarningDismissed()399 HRESULT RdpClientWindow::OnAuthenticationWarningDismissed() {
400   LOG(WARNING) << "RDP: authentication warning has been dismissed.";
401 
402   window_activate_hook_ = NULL;
403   return S_OK;
404 }
405 
OnConnected()406 HRESULT RdpClientWindow::OnConnected() {
407   VLOG(1) << "RDP: successfully connected to " << server_endpoint_.ToString();
408 
409   NotifyConnected();
410   return S_OK;
411 }
412 
OnDisconnected(long reason)413 HRESULT 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 
OnFatalError(long error_code)449 HRESULT 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 
OnConfirmClose(VARIANT_BOOL * allow_close)457 HRESULT RdpClientWindow::OnConfirmClose(VARIANT_BOOL* allow_close) {
458   *allow_close = VARIANT_TRUE;
459 
460   NotifyDisconnected();
461   return S_OK;
462 }
463 
NotifyConnected()464 void RdpClientWindow::NotifyConnected() {
465   if (event_handler_)
466     event_handler_->OnConnected();
467 }
468 
NotifyDisconnected()469 void RdpClientWindow::NotifyDisconnected() {
470   if (event_handler_) {
471     EventHandler* event_handler = event_handler_;
472     event_handler_ = NULL;
473     event_handler->OnDisconnected();
474   }
475 }
476 
477 scoped_refptr<RdpClientWindow::WindowHook>
Create()478 RdpClientWindow::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 
WindowHook()487 RdpClientWindow::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 
~WindowHook()504 RdpClientWindow::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
CloseWindowOnActivation(int code,WPARAM wparam,LPARAM lparam)514 LRESULT 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