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 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 (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       screen_size_(SkISize::Make(0, 0)),
    118       server_endpoint_(server_endpoint),
    119       terminal_id_(terminal_id) {
    120 }
    121 
    122 RdpClientWindow::~RdpClientWindow() {
    123   if (m_hWnd)
    124     DestroyWindow();
    125 
    126   DCHECK(!client_);
    127   DCHECK(!client_settings_);
    128 }
    129 
    130 bool RdpClientWindow::Connect(const SkISize& screen_size) {
    131   DCHECK(!m_hWnd);
    132 
    133   screen_size_ = screen_size;
    134   RECT rect = { 0, 0, screen_size_.width(), screen_size_.height() };
    135   bool result = Create(NULL, rect, NULL) != NULL;
    136 
    137   // Hide the window since this class is about establishing a connection, not
    138   // about showing a UI to the user.
    139   if (result)
    140     ShowWindow(SW_HIDE);
    141 
    142   return result;
    143 }
    144 
    145 void RdpClientWindow::Disconnect() {
    146   if (m_hWnd)
    147     SendMessage(WM_CLOSE);
    148 }
    149 
    150 void RdpClientWindow::InjectSas() {
    151   if (!m_hWnd)
    152     return;
    153 
    154   // Fins the window handling the keyboard input.
    155   HWND input_window = FindWindowRecursively(m_hWnd, kRdpInputWindowClass);
    156   if (!input_window) {
    157     LOG(ERROR) << "Failed to find the window handling the keyboard input.";
    158     return;
    159   }
    160 
    161   VLOG(3) << "Injecting Ctrl+Alt+End to emulate SAS.";
    162 
    163   BYTE keyboard_state[kKeyboardStateLength];
    164   if (!GetKeyboardState(keyboard_state)) {
    165     LOG_GETLASTERROR(ERROR) << "Failed to get the keyboard state.";
    166     return;
    167   }
    168 
    169   // This code is running in Session 0, so we expect no keys to be pressed.
    170   DCHECK(!(keyboard_state[VK_CONTROL] & kKeyPressedFlag));
    171   DCHECK(!(keyboard_state[VK_MENU] & kKeyPressedFlag));
    172   DCHECK(!(keyboard_state[VK_END] & kKeyPressedFlag));
    173 
    174   // Map virtual key codes to scan codes.
    175   UINT control = MapVirtualKey(VK_CONTROL, MAPVK_VK_TO_VSC);
    176   UINT alt = MapVirtualKey(VK_MENU, MAPVK_VK_TO_VSC);
    177   UINT end = MapVirtualKey(VK_END, MAPVK_VK_TO_VSC) | KF_EXTENDED;
    178   UINT up = KF_UP | KF_REPEAT;
    179 
    180   // Press 'Ctrl'.
    181   keyboard_state[VK_CONTROL] |= kKeyPressedFlag;
    182   keyboard_state[VK_LCONTROL] |= kKeyPressedFlag;
    183   CHECK(SetKeyboardState(keyboard_state));
    184   SendMessage(input_window, WM_KEYDOWN, VK_CONTROL, MAKELPARAM(1, control));
    185 
    186   // Press 'Alt'.
    187   keyboard_state[VK_MENU] |= kKeyPressedFlag;
    188   keyboard_state[VK_LMENU] |= kKeyPressedFlag;
    189   CHECK(SetKeyboardState(keyboard_state));
    190   SendMessage(input_window, WM_KEYDOWN, VK_MENU,
    191               MAKELPARAM(1, alt | KF_ALTDOWN));
    192 
    193   // Press and release 'End'.
    194   SendMessage(input_window, WM_KEYDOWN, VK_END,
    195               MAKELPARAM(1, end | KF_ALTDOWN));
    196   SendMessage(input_window, WM_KEYUP, VK_END,
    197               MAKELPARAM(1, end | up | KF_ALTDOWN));
    198 
    199   // Release 'Alt'.
    200   keyboard_state[VK_MENU] &= ~kKeyPressedFlag;
    201   keyboard_state[VK_LMENU] &= ~kKeyPressedFlag;
    202   CHECK(SetKeyboardState(keyboard_state));
    203   SendMessage(input_window, WM_KEYUP, VK_MENU, MAKELPARAM(1, alt | up));
    204 
    205   // Release 'Ctrl'.
    206   keyboard_state[VK_CONTROL] &= ~kKeyPressedFlag;
    207   keyboard_state[VK_LCONTROL] &= ~kKeyPressedFlag;
    208   CHECK(SetKeyboardState(keyboard_state));
    209   SendMessage(input_window, WM_KEYUP, VK_CONTROL, MAKELPARAM(1, control | up));
    210 }
    211 
    212 void RdpClientWindow::OnClose() {
    213   if (!client_) {
    214     NotifyDisconnected();
    215     return;
    216   }
    217 
    218   // Request a graceful shutdown.
    219   mstsc::ControlCloseStatus close_status;
    220   HRESULT result = client_->RequestClose(&close_status);
    221   if (FAILED(result)) {
    222     LOG(ERROR) << "Failed to request a graceful shutdown of an RDP connection"
    223                << ", result=0x" << std::hex << result << std::dec;
    224     NotifyDisconnected();
    225     return;
    226   }
    227 
    228   if (close_status != mstsc::controlCloseWaitForEvents) {
    229     NotifyDisconnected();
    230     return;
    231   }
    232 
    233   // Expect IMsTscAxEvents::OnConfirmClose() or IMsTscAxEvents::OnDisconnect()
    234   // to be called if mstsc::controlCloseWaitForEvents was returned.
    235 }
    236 
    237 LRESULT RdpClientWindow::OnCreate(CREATESTRUCT* create_struct) {
    238   CAxWindow2 activex_window;
    239   base::win::ScopedComPtr<IUnknown> control;
    240   HRESULT result = E_FAIL;
    241   base::win::ScopedComPtr<mstsc::IMsTscSecuredSettings> secured_settings;
    242   base::win::ScopedComPtr<mstsc::IMsRdpClientSecuredSettings> secured_settings2;
    243   base::win::ScopedBstr server_name(
    244       UTF8ToUTF16(server_endpoint_.ToStringWithoutPort()).c_str());
    245   base::win::ScopedBstr terminal_id(UTF8ToUTF16(terminal_id_).c_str());
    246 
    247   // Create the child window that actually hosts the ActiveX control.
    248   RECT rect = { 0, 0, screen_size_.width(), screen_size_.height() };
    249   activex_window.Create(m_hWnd, rect, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER);
    250   if (activex_window.m_hWnd == NULL) {
    251     result = HRESULT_FROM_WIN32(GetLastError());
    252     goto done;
    253   }
    254 
    255   // Instantiate the RDP ActiveX control.
    256   result = activex_window.CreateControlEx(
    257       OLESTR("MsTscAx.MsTscAx"),
    258       NULL,
    259       NULL,
    260       control.Receive(),
    261       __uuidof(mstsc::IMsTscAxEvents),
    262       reinterpret_cast<IUnknown*>(static_cast<RdpEventsSink*>(this)));
    263   if (FAILED(result))
    264     goto done;
    265 
    266   result = control.QueryInterface(client_.Receive());
    267   if (FAILED(result))
    268     goto done;
    269 
    270   // Use 32-bit color.
    271   result = client_->put_ColorDepth(32);
    272   if (FAILED(result))
    273     goto done;
    274 
    275   // Set dimensions of the remote desktop.
    276   result = client_->put_DesktopWidth(screen_size_.width());
    277   if (FAILED(result))
    278     goto done;
    279   result = client_->put_DesktopHeight(screen_size_.height());
    280   if (FAILED(result))
    281     goto done;
    282 
    283   // Set the server name to connect to.
    284   result = client_->put_Server(server_name);
    285   if (FAILED(result))
    286     goto done;
    287 
    288   // Fetch IMsRdpClientAdvancedSettings interface for the client.
    289   result = client_->get_AdvancedSettings2(client_settings_.Receive());
    290   if (FAILED(result))
    291     goto done;
    292 
    293   // Disable background input mode.
    294   result = client_settings_->put_allowBackgroundInput(0);
    295   if (FAILED(result))
    296     goto done;
    297 
    298   // Do not use bitmap cache.
    299   result = client_settings_->put_BitmapPersistence(0);
    300   if (SUCCEEDED(result))
    301     result = client_settings_->put_CachePersistenceActive(0);
    302   if (FAILED(result))
    303     goto done;
    304 
    305   // Do not use compression.
    306   result = client_settings_->put_Compress(0);
    307   if (FAILED(result))
    308     goto done;
    309 
    310   // Enable the Ctrl+Alt+Del screen.
    311   result = client_settings_->put_DisableCtrlAltDel(0);
    312   if (FAILED(result))
    313     goto done;
    314 
    315   // Disable printer and clipboard redirection.
    316   result = client_settings_->put_DisableRdpdr(FALSE);
    317   if (FAILED(result))
    318     goto done;
    319 
    320   // Do not display the connection bar.
    321   result = client_settings_->put_DisplayConnectionBar(VARIANT_FALSE);
    322   if (FAILED(result))
    323     goto done;
    324 
    325   // Do not grab focus on connect.
    326   result = client_settings_->put_GrabFocusOnConnect(VARIANT_FALSE);
    327   if (FAILED(result))
    328     goto done;
    329 
    330   // Enable enhanced graphics, font smoothing and desktop composition.
    331   const LONG kDesiredFlags = WTS_PERF_ENABLE_ENHANCED_GRAPHICS |
    332                              WTS_PERF_ENABLE_FONT_SMOOTHING |
    333                              WTS_PERF_ENABLE_DESKTOP_COMPOSITION;
    334   result = client_settings_->put_PerformanceFlags(kDesiredFlags);
    335   if (FAILED(result))
    336     goto done;
    337 
    338   // Set the port to connect to.
    339   result = client_settings_->put_RDPPort(server_endpoint_.port());
    340   if (FAILED(result))
    341     goto done;
    342 
    343   // Disable audio in the session.
    344   // TODO(alexeypa): re-enable audio redirection when http://crbug.com/242312 is
    345   // fixed.
    346   result = client_->get_SecuredSettings2(secured_settings2.Receive());
    347   if (SUCCEEDED(result)) {
    348     result = secured_settings2->put_AudioRedirectionMode(kRdpAudioModeNone);
    349     if (FAILED(result))
    350       goto done;
    351   }
    352 
    353   result = client_->get_SecuredSettings(secured_settings.Receive());
    354   if (FAILED(result))
    355     goto done;
    356 
    357   // Set the terminal ID as the working directory for the initial program. It is
    358   // observed that |WorkDir| is used only if an initial program is also
    359   // specified, but is still passed to the RDP server and can then be read back
    360   // from the session parameters. This makes it possible to use |WorkDir| to
    361   // match the RDP connection with the session it is attached to.
    362   //
    363   // This code should be in sync with WtsTerminalMonitor::LookupTerminalId().
    364   result = secured_settings->put_WorkDir(terminal_id);
    365   if (FAILED(result))
    366     goto done;
    367 
    368   result = client_->Connect();
    369   if (FAILED(result))
    370     goto done;
    371 
    372 done:
    373   if (FAILED(result)) {
    374     LOG(ERROR) << "RDP: failed to initiate a connection to "
    375                << server_endpoint_.ToString() << ": error="
    376                << std::hex << result << std::dec;
    377     client_.Release();
    378     client_settings_.Release();
    379     return -1;
    380   }
    381 
    382   return 0;
    383 }
    384 
    385 void RdpClientWindow::OnDestroy() {
    386   client_.Release();
    387   client_settings_.Release();
    388 }
    389 
    390 HRESULT RdpClientWindow::OnAuthenticationWarningDisplayed() {
    391   LOG(WARNING) << "RDP: authentication warning is about to be shown.";
    392 
    393   // Hook window activation to cancel any modal UI shown by the RDP control.
    394   // This does not affect creation of other instances of the RDP control on this
    395   // thread because the RDP control's window is hidden and is not activated.
    396   window_activate_hook_ = WindowHook::Create();
    397   return S_OK;
    398 }
    399 
    400 HRESULT RdpClientWindow::OnAuthenticationWarningDismissed() {
    401   LOG(WARNING) << "RDP: authentication warning has been dismissed.";
    402 
    403   window_activate_hook_ = NULL;
    404   return S_OK;
    405 }
    406 
    407 HRESULT RdpClientWindow::OnConnected() {
    408   VLOG(1) << "RDP: successfully connected to " << server_endpoint_.ToString();
    409 
    410   NotifyConnected();
    411   return S_OK;
    412 }
    413 
    414 HRESULT RdpClientWindow::OnDisconnected(long reason) {
    415   if (reason == kDisconnectReasonNoInfo ||
    416       reason == kDisconnectReasonLocalNotError ||
    417       reason == kDisconnectReasonRemoteByUser ||
    418       reason == kDisconnectReasonByServer) {
    419     VLOG(1) << "RDP: disconnected from " << server_endpoint_.ToString()
    420             << ", reason=" << reason;
    421     NotifyDisconnected();
    422     return S_OK;
    423   }
    424 
    425   // Get the extended disconnect reason code.
    426   mstsc::ExtendedDisconnectReasonCode extended_code;
    427   HRESULT result = client_->get_ExtendedDisconnectReason(&extended_code);
    428   if (FAILED(result))
    429     extended_code = mstsc::exDiscReasonNoInfo;
    430 
    431   // Get the error message as well.
    432   base::win::ScopedBstr error_message;
    433   base::win::ScopedComPtr<mstsc::IMsRdpClient5> client5;
    434   result = client_.QueryInterface(client5.Receive());
    435   if (SUCCEEDED(result)) {
    436     result = client5->GetErrorDescription(reason, extended_code,
    437                                           error_message.Receive());
    438     if (FAILED(result))
    439       error_message.Reset();
    440   }
    441 
    442   LOG(ERROR) << "RDP: disconnected from " << server_endpoint_.ToString()
    443              << ": " << error_message << " (reason=" << reason
    444              << ", extended_code=" << extended_code << ")";
    445 
    446   NotifyDisconnected();
    447   return S_OK;
    448 }
    449 
    450 HRESULT RdpClientWindow::OnFatalError(long error_code) {
    451   LOG(ERROR) << "RDP: an error occured: error_code="
    452              << error_code;
    453 
    454   NotifyDisconnected();
    455   return S_OK;
    456 }
    457 
    458 HRESULT RdpClientWindow::OnConfirmClose(VARIANT_BOOL* allow_close) {
    459   *allow_close = VARIANT_TRUE;
    460 
    461   NotifyDisconnected();
    462   return S_OK;
    463 }
    464 
    465 void RdpClientWindow::NotifyConnected() {
    466   if (event_handler_)
    467     event_handler_->OnConnected();
    468 }
    469 
    470 void RdpClientWindow::NotifyDisconnected() {
    471   if (event_handler_) {
    472     EventHandler* event_handler = event_handler_;
    473     event_handler_ = NULL;
    474     event_handler->OnDisconnected();
    475   }
    476 }
    477 
    478 scoped_refptr<RdpClientWindow::WindowHook>
    479 RdpClientWindow::WindowHook::Create() {
    480   scoped_refptr<WindowHook> window_hook = g_window_hook.Pointer()->Get();
    481 
    482   if (!window_hook)
    483     window_hook = new WindowHook();
    484 
    485   return window_hook;
    486 }
    487 
    488 RdpClientWindow::WindowHook::WindowHook() : hook_(NULL) {
    489   DCHECK(!g_window_hook.Pointer()->Get());
    490 
    491   // Install a window hook to be called on window activation.
    492   hook_ = SetWindowsHookEx(WH_CBT,
    493                            &WindowHook::CloseWindowOnActivation,
    494                            NULL,
    495                            GetCurrentThreadId());
    496   // Without the hook installed, RdpClientWindow will not be able to cancel
    497   // modal UI windows. This will block the UI message loop so it is better to
    498   // terminate the process now.
    499   CHECK(hook_);
    500 
    501   // Let CloseWindowOnActivation() to access the hook handle.
    502   g_window_hook.Pointer()->Set(this);
    503 }
    504 
    505 RdpClientWindow::WindowHook::~WindowHook() {
    506   DCHECK(g_window_hook.Pointer()->Get() == this);
    507 
    508   g_window_hook.Pointer()->Set(NULL);
    509 
    510   BOOL result = UnhookWindowsHookEx(hook_);
    511   DCHECK(result);
    512 }
    513 
    514 // static
    515 LRESULT CALLBACK RdpClientWindow::WindowHook::CloseWindowOnActivation(
    516     int code, WPARAM wparam, LPARAM lparam) {
    517   // Get the hook handle.
    518   HHOOK hook = g_window_hook.Pointer()->Get()->hook_;
    519 
    520   if (code != HCBT_ACTIVATE)
    521     return CallNextHookEx(hook, code, wparam, lparam);
    522 
    523   // Close the window once all pending window messages are processed.
    524   HWND window = reinterpret_cast<HWND>(wparam);
    525   LOG(WARNING) << "RDP: closing a window: " << std::hex << window << std::dec;
    526   ::PostMessage(window, WM_CLOSE, 0, 0);
    527   return 0;
    528 }
    529 
    530 }  // namespace remoting
    531