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