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