Home | History | Annotate | Download | only in win
      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.
     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 
    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 
    121 RdpClientWindow::~RdpClientWindow() {
    122   if (m_hWnd)
    123     DestroyWindow();
    124 
    125   DCHECK(!client_);
    126   DCHECK(!client_settings_);
    127 }
    128 
    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 
    144 void RdpClientWindow::Disconnect() {
    145   if (m_hWnd)
    146     SendMessage(WM_CLOSE);
    147 }
    148 
    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     LOG_GETLASTERROR(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 
    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 
    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       UTF8ToUTF16(server_endpoint_.ToStringWithoutPort()).c_str());
    244   base::win::ScopedBstr terminal_id(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 
    384 void RdpClientWindow::OnDestroy() {
    385   client_.Release();
    386   client_settings_.Release();
    387 }
    388 
    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 
    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 
    406 HRESULT RdpClientWindow::OnConnected() {
    407   VLOG(1) << "RDP: successfully connected to " << server_endpoint_.ToString();
    408 
    409   NotifyConnected();
    410   return S_OK;
    411 }
    412 
    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 
    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 
    457 HRESULT RdpClientWindow::OnConfirmClose(VARIANT_BOOL* allow_close) {
    458   *allow_close = VARIANT_TRUE;
    459 
    460   NotifyDisconnected();
    461   return S_OK;
    462 }
    463 
    464 void RdpClientWindow::NotifyConnected() {
    465   if (event_handler_)
    466     event_handler_->OnConnected();
    467 }
    468 
    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>
    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 
    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 
    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
    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