[fix] correct Windows remote control session routing and key mapping

This commit is contained in:
dijunkun
2026-09-19 03:59:35 +08:00
parent 000b2e44e3
commit 4797af9260
13 changed files with 334 additions and 41 deletions
@@ -8,6 +8,7 @@
#define _INTERACTIVE_DESKTOP_H_
#include <Windows.h>
#include <WtsApi32.h>
#include <string>
@@ -33,6 +34,52 @@ inline std::wstring GetDesktopNameW(HDESK desktop) {
return name;
}
inline bool IsDesktopReceivingInput(HDESK desktop) {
BOOL receiving_input = FALSE;
return desktop != nullptr &&
GetUserObjectInformationW(desktop, UOI_IO, &receiving_input,
sizeof(receiving_input), nullptr) &&
receiving_input != FALSE;
}
inline bool IsCurrentProcessSession(DWORD session_id) {
DWORD process_session_id = 0xFFFFFFFF;
return session_id != 0xFFFFFFFF &&
ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id) &&
session_id == process_session_id;
}
inline bool IsCurrentSessionUnlocked() {
DWORD session_id = 0xFFFFFFFF;
if (!ProcessIdToSessionId(GetCurrentProcessId(), &session_id)) return false;
PWTSINFOEXW info = nullptr;
DWORD bytes = 0;
if (!WTSQuerySessionInformationW(
WTS_CURRENT_SERVER_HANDLE, session_id, WTSSessionInfoEx,
reinterpret_cast<LPWSTR*>(&info), &bytes)) {
return false;
}
const bool unlocked =
info != nullptr && bytes >= sizeof(WTSINFOEXW) && info->Level == 1 &&
info->Data.WTSInfoExLevel1.SessionState == WTSActive &&
info->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_UNLOCK;
if (info != nullptr) WTSFreeMemory(info);
return unlocked;
}
inline bool IsCurrentSessionUserDesktopActive() {
// LockApp can be visible on Default while the session is still locked.
if (!IsCurrentSessionUnlocked()) return false;
HDESK desktop = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
if (desktop == nullptr) return false;
// OpenInputDesktop also succeeds in disconnected RDP sessions. The name
// alone does not tell us whether this desktop can currently receive input.
const bool active = IsDesktopReceivingInput(desktop) &&
_wcsicmp(GetDesktopNameW(desktop).c_str(), L"Default") == 0;
CloseDesktop(desktop);
return active;
}
// Use only on worker threads without windows or hooks. Restore the original
// desktop before closing the handle: Windows cannot close a desktop in use.
class ScopedInteractiveDesktop {
@@ -88,6 +135,10 @@ class ScopedInteractiveDesktop {
return !name_.empty() && _wcsicmp(name_.c_str(), input_name.c_str()) == 0;
}
bool IsReceivingInput() const {
return IsDesktopReceivingInput(desktop_);
}
const std::wstring& name() const { return name_; }
private:
@@ -1313,8 +1313,14 @@ void CrossDeskServiceHost::IpcServerLoop() {
DWORD bytes_read = 0;
if (ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr) &&
bytes_read > 0) {
std::string response =
HandleIpcCommand(std::string(buffer, buffer + bytes_read));
ULONG client_session_id = 0xFFFFFFFF;
// Obtain identity from Windows, never from caller-supplied JSON. A GUI
// left in an RDP session must not control a different console session.
const std::string response =
GetNamedPipeClientSessionId(pipe, &client_session_id)
? HandleIpcCommand(std::string(buffer, buffer + bytes_read),
client_session_id)
: BuildErrorJson("client_session_query_failed", GetLastError());
DWORD bytes_written = 0;
WriteFile(pipe, response.data(), static_cast<DWORD>(response.size()),
&bytes_written, nullptr);
@@ -1329,7 +1335,13 @@ void CrossDeskServiceHost::IpcServerLoop() {
void CrossDeskServiceHost::RefreshSessionState() {
std::lock_guard<std::mutex> lock(state_mutex_);
const DWORD previous_session_id = active_session_id_;
active_session_id_ = WTSGetActiveConsoleSessionId();
if (active_session_id_ != previous_session_id) {
ResetSessionHelperReportedStateLocked("session_changed", 0);
sas_secure_desktop_until_tick_ = 0;
sas_secure_desktop_seen_ = false;
}
DWORD process_session_id = 0xFFFFFFFF;
if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) {
process_session_id_ = process_session_id;
@@ -1931,6 +1943,14 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
}
std::lock_guard<std::mutex> lock(state_mutex_);
if (target_session_id != active_session_id_ ||
target_session_id != session_helper_session_id_ ||
json.value("session_id", static_cast<DWORD>(0xFFFFFFFF)) !=
target_session_id ||
json.value("process_id", 0u) != session_helper_process_id_) {
ResetSessionHelperReportedStateLocked("helper_session_mismatch", 0);
return;
}
session_helper_status_ok_ = true;
session_helper_status_error_.clear();
session_helper_status_error_code_ = 0;
@@ -1969,7 +1989,13 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
std::lock_guard<std::mutex> lock(state_mutex_);
last_session_event_type_ = event_type;
last_session_event_session_id_ = session_id;
const DWORD previous_session_id = active_session_id_;
active_session_id_ = WTSGetActiveConsoleSessionId();
if (active_session_id_ != previous_session_id) {
ResetSessionHelperReportedStateLocked("session_changed", 0);
sas_secure_desktop_until_tick_ = 0;
sas_secure_desktop_seen_ = false;
}
DWORD process_session_id = 0xFFFFFFFF;
if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) {
process_session_id_ = process_session_id;
@@ -1988,10 +2014,11 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
prelogin_ = !username_available || username.empty();
if (!QuerySessionLockState(active_session_id_, &session_locked_)) {
if (event_type == WTS_SESSION_LOCK) {
if (session_id == active_session_id_ && event_type == WTS_SESSION_LOCK) {
session_locked_ = true;
} else if (event_type == WTS_SESSION_UNLOCK ||
event_type == WTS_SESSION_LOGON) {
} else if (session_id == active_session_id_ &&
(event_type == WTS_SESSION_UNLOCK ||
event_type == WTS_SESSION_LOGON)) {
session_locked_ = false;
} else if (logon_ui_visible_ || secure_desktop_active_) {
session_locked_ = !prelogin_;
@@ -2007,16 +2034,17 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
}
}
std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command) {
std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command,
DWORD client_session_id) {
std::string normalized = ToLower(Trim(command));
if (normalized == "ping") {
return "{\"ok\":true,\"reply\":\"pong\"}";
}
if (normalized == "status") {
return BuildStatusResponse();
return BuildStatusResponse(client_session_id);
}
if (normalized == "sas") {
return SendSecureAttentionSequence();
return SendSecureAttentionSequence(client_session_id);
}
int key_code = 0;
bool is_down = false;
@@ -2024,22 +2052,29 @@ std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command) {
bool extended = false;
if (ParseSecureDesktopKeyboardIpcCommand(normalized, &key_code, &is_down,
&scan_code, &extended)) {
return SendSecureDesktopKeyboardInput(key_code, is_down, scan_code,
extended);
return SendSecureDesktopKeyboardInput(client_session_id, key_code, is_down,
scan_code, extended);
}
SecureDesktopMouseRequest mouse_request;
if (ParseSecureDesktopMouseIpcCommand(normalized, &mouse_request)) {
return SendSecureDesktopMouseInput(mouse_request.x, mouse_request.y,
mouse_request.wheel,
mouse_request.flag);
return SendSecureDesktopMouseInput(client_session_id, mouse_request.x,
mouse_request.y, mouse_request.wheel,
mouse_request.flag);
}
return BuildErrorJson("unknown_command");
}
std::string CrossDeskServiceHost::BuildStatusResponse() {
std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
ReapSecureInputHelper();
ReapSessionHelper();
RefreshSessionState();
{
std::lock_guard<std::mutex> lock(state_mutex_);
if (client_session_id == 0xFFFFFFFF ||
active_session_id_ != client_session_id) {
return BuildErrorJson("service_session_mismatch");
}
}
EnsureSessionHelper();
RefreshSessionHelperReportedState();
bool keep_secure_input_helper = false;
@@ -2077,6 +2112,9 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
}
std::lock_guard<std::mutex> lock(state_mutex_);
if (active_session_id_ != client_session_id) {
return BuildErrorJson("service_session_mismatch");
}
std::wstring username;
GetSessionUserName(active_session_id_, &username);
std::string username_utf8 = EscapeJsonString(WideToUtf8(username));
@@ -2263,8 +2301,13 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
return stream.str();
}
std::string CrossDeskServiceHost::SendSecureAttentionSequence() {
std::string CrossDeskServiceHost::SendSecureAttentionSequence(
DWORD client_session_id) {
RefreshSessionState();
if (client_session_id == 0xFFFFFFFF ||
client_session_id != WTSGetActiveConsoleSessionId()) {
return BuildErrorJson("service_session_mismatch");
}
LOG_INFO("Received SAS request for session_id={}", active_session_id_);
SasResult result = SendSasNow();
{
@@ -2286,7 +2329,8 @@ std::string CrossDeskServiceHost::SendSecureAttentionSequence() {
}
std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
int key_code, bool is_down, uint32_t scan_code, bool extended) {
DWORD client_session_id, int key_code, bool is_down,
uint32_t scan_code, bool extended) {
RefreshSessionState();
ReapSecureInputHelper();
EnsureSessionHelper();
@@ -2314,6 +2358,9 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
if (target_session_id == 0xFFFFFFFF) {
return BuildErrorJson("no_active_console_session");
}
if (target_session_id != client_session_id) {
return BuildErrorJson("service_session_mismatch");
}
if (!can_inject) {
return BuildErrorJson("secure_input_not_active");
}
@@ -2336,9 +2383,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
1000);
}
std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
int wheel,
int flag) {
std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(
DWORD client_session_id, int x, int y, int wheel, int flag) {
RefreshSessionState();
ReapSecureInputHelper();
EnsureSessionHelper();
@@ -2366,6 +2412,9 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
if (target_session_id == 0xFFFFFFFF) {
return BuildErrorJson("no_active_console_session");
}
if (target_session_id != client_session_id) {
return BuildErrorJson("service_session_mismatch");
}
if (!can_inject) {
return BuildErrorJson("secure_input_not_active");
}
@@ -65,13 +65,16 @@ class CrossDeskServiceHost {
const std::string& interactive_stage) const;
void RefreshSessionHelperReportedState();
void RecordSessionEvent(DWORD event_type, DWORD session_id);
std::string HandleIpcCommand(const std::string& command);
std::string BuildStatusResponse();
std::string SendSecureAttentionSequence();
std::string SendSecureDesktopKeyboardInput(int key_code, bool is_down,
std::string HandleIpcCommand(const std::string& command,
DWORD client_session_id);
std::string BuildStatusResponse(DWORD client_session_id);
std::string SendSecureAttentionSequence(DWORD client_session_id);
std::string SendSecureDesktopKeyboardInput(DWORD client_session_id,
int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false);
std::string SendSecureDesktopMouseInput(int x, int y, int wheel, int flag);
std::string SendSecureDesktopMouseInput(DWORD client_session_id,
int x, int y, int wheel, int flag);
static void WINAPI ServiceMain(DWORD argc, LPWSTR* argv);
static BOOL WINAPI ConsoleControlHandler(DWORD control_type);
@@ -28,6 +28,7 @@
#include "windows_cursor_state.h"
#include "cursor_draw.h"
#include "windows_input_marker.h"
#include "windows_key_metadata.h"
namespace {
@@ -637,6 +638,13 @@ bool EnsureThreadInteractiveDesktopForStage(
desktop_name = L"Winlogon";
}
if (desktop->Bind(desktop_name)) {
if (!desktop->IsReceivingInput()) {
if (switch_details != nullptr) {
switch_details->error_code = ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION;
}
SetLastError(ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION);
return false;
}
if (switch_details != nullptr) {
switch_details->target_desktop = WideToUtf8(desktop->name());
switch_details->current_desktop = switch_details->target_desktop;
@@ -695,6 +703,9 @@ InputInjectionResult InjectKeyboardInput(
input.ki.dwExtraInfo = static_cast<ULONG_PTR>(
crossdesk::kInjectedKeyboardInputMarker);
if (scan_code == 0) {
crossdesk::LookupWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
}
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
const UINT resolved_scan_code =
scan_code != 0