diff --git a/scripts/windows/crossdesk.manifest b/scripts/windows/crossdesk.manifest index 28530aa..a66a661 100644 --- a/scripts/windows/crossdesk.manifest +++ b/scripts/windows/crossdesk.manifest @@ -1,17 +1,13 @@ - - CrossDesk Application - @@ -20,24 +16,17 @@ - - - true/pm - - PerMonitorV2 + true/pm + PerMonitorV2 - - - - - \ No newline at end of file + diff --git a/scripts/windows/crossdesk.rc b/scripts/windows/crossdesk.rc index a67ef6c..2783240 100644 --- a/scripts/windows/crossdesk.rc +++ b/scripts/windows/crossdesk.rc @@ -1,2 +1,11 @@ // Application icon resource; load by the resource name IDI_ICON1. IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico" + +#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1 +#define RT_MANIFEST 24 + +#ifdef CROSSDESK_DEBUG +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest" +#else +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest" +#endif diff --git a/scripts/windows/crossdesk_debug.manifest b/scripts/windows/crossdesk_debug.manifest new file mode 100644 index 0000000..9a7a610 --- /dev/null +++ b/scripts/windows/crossdesk_debug.manifest @@ -0,0 +1,32 @@ + + + + + + CrossDesk Application + + + + + + + + + + + + true/pm + PerMonitorV2 + + + + + + + + + + diff --git a/src/device_controller/mouse/windows/mouse_controller.cpp b/src/device_controller/mouse/windows/mouse_controller.cpp index 570976f..b066171 100644 --- a/src/device_controller/mouse/windows/mouse_controller.cpp +++ b/src/device_controller/mouse/windows/mouse_controller.cpp @@ -1,5 +1,7 @@ #include "mouse_controller.h" +#include + #include "rd_log.h" namespace crossdesk { @@ -18,7 +20,14 @@ int MouseController::Destroy() { return 0; } int MouseController::SendMouseCommand(RemoteAction remote_action, int display_index) { - INPUT ip; + if (display_index < 0 || + display_index >= static_cast(display_info_list_.size())) { + LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}", + display_index, display_info_list_.size()); + return -1; + } + + INPUT ip = {0}; if (remote_action.type == ControlType::mouse) { ip.type = INPUT_MOUSE; @@ -63,13 +72,25 @@ int MouseController::SendMouseCommand(RemoteAction remote_action, ip.mi.time = 0; - SetCursorPos(ip.mi.dx, ip.mi.dy); + if (!SetCursorPos(ip.mi.dx, ip.mi.dy)) { + LOG_WARN("SetCursorPos failed for mouse x={}, y={}, flag={}, err={}", + ip.mi.dx, ip.mi.dy, static_cast(remote_action.m.flag), + GetLastError()); + return -1; + } if (ip.mi.dwFlags != MOUSEEVENTF_MOVE) { - SendInput(1, &ip, sizeof(INPUT)); + UINT sent = SendInput(1, &ip, sizeof(INPUT)); + if (sent != 1) { + LOG_WARN( + "SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}", + ip.mi.dx, ip.mi.dy, remote_action.m.s, + static_cast(remote_action.m.flag), GetLastError()); + return -1; + } } } return 0; } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/service/windows/service_host.cpp b/src/service/windows/service_host.cpp index 1c8af77..fc18652 100644 --- a/src/service/windows/service_host.cpp +++ b/src/service/windows/service_host.cpp @@ -46,6 +46,13 @@ struct InputDesktopInfo { std::string name; }; +struct SecureDesktopMouseRequest { + int x = 0; + int y = 0; + int wheel = 0; + int flag = 0; +}; + struct ScopedEnvironmentBlock { ~ScopedEnvironmentBlock() { if (environment != nullptr) { @@ -339,6 +346,14 @@ std::string BuildSecureInputHelperKeyboardCommand(int key_code, bool is_down, return stream.str(); } +std::string BuildSecureInputHelperMouseCommand(int x, int y, int wheel, + int flag) { + std::ostringstream stream; + stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":" + << wheel << ":" << flag; + return stream.str(); +} + bool ParseSecureDesktopKeyboardIpcCommand(const std::string& command, int* key_code_out, bool* is_down_out, uint32_t* scan_code_out, @@ -412,6 +427,57 @@ bool ParseSecureDesktopKeyboardIpcCommand(const std::string& command, return false; } +bool ParseSecureDesktopMouseIpcCommand(const std::string& command, + SecureDesktopMouseRequest* request_out) { + if (request_out == nullptr) { + return false; + } + + if (command.rfind(kSecureDesktopMouseIpcCommandPrefix, 0) != 0) { + return false; + } + + const size_t x_begin = sizeof(kSecureDesktopMouseIpcCommandPrefix) - 1; + size_t separator = command.find(':', x_begin); + if (separator == std::string::npos) { + return false; + } + + try { + request_out->x = std::stoi(command.substr(x_begin, separator - x_begin)); + } catch (...) { + return false; + } + + const size_t y_begin = separator + 1; + separator = command.find(':', y_begin); + if (separator == std::string::npos) { + return false; + } + + try { + request_out->y = std::stoi(command.substr(y_begin, separator - y_begin)); + } catch (...) { + return false; + } + + const size_t wheel_begin = separator + 1; + separator = command.find(':', wheel_begin); + if (separator == std::string::npos) { + return false; + } + + try { + request_out->wheel = + std::stoi(command.substr(wheel_begin, separator - wheel_begin)); + request_out->flag = std::stoi(command.substr(separator + 1)); + } catch (...) { + return false; + } + + return true; +} + bool CreateSessionSystemToken(DWORD session_id, HANDLE* token_out, DWORD* error_code_out = nullptr) { if (token_out == nullptr) { @@ -1759,7 +1825,13 @@ std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command) { if (ParseSecureDesktopKeyboardIpcCommand(normalized, &key_code, &is_down, &scan_code, &extended)) { return SendSecureDesktopKeyboardInput(key_code, is_down, scan_code, - extended); + extended); + } + SecureDesktopMouseRequest mouse_request; + if (ParseSecureDesktopMouseIpcCommand(normalized, &mouse_request)) { + return SendSecureDesktopMouseInput(mouse_request.x, mouse_request.y, + mouse_request.wheel, + mouse_request.flag); } return BuildErrorJson("unknown_command"); } @@ -2014,6 +2086,45 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput( 1000); } +std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y, + int wheel, + int flag) { + RefreshSessionState(); + ReapSecureInputHelper(); + EnsureSessionHelper(); + + DWORD target_session_id = 0xFFFFFFFF; + bool helper_running = false; + bool can_inject = false; + { + std::lock_guard lock(state_mutex_); + target_session_id = active_session_id_; + helper_running = secure_input_helper_running_ && + secure_input_helper_session_id_ == target_session_id; + can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked(); + } + + if (target_session_id == 0xFFFFFFFF) { + return BuildErrorJson("no_active_console_session"); + } + if (!can_inject) { + return BuildErrorJson("secure_input_not_active"); + } + + if (!helper_running) { + StopSecureInputHelper(); + if (!LaunchSecureInputHelper(target_session_id)) { + std::lock_guard lock(state_mutex_); + return BuildErrorJson(secure_input_helper_last_error_.c_str(), + secure_input_helper_last_error_code_); + } + } + + return QueryNamedPipeMessage( + GetCrossDeskSecureInputHelperPipeName(target_session_id), + BuildSecureInputHelperMouseCommand(x, y, wheel, flag), 1000); +} + bool InstallCrossDeskService(const std::wstring& binary_path) { SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); if (manager == nullptr) { @@ -2238,4 +2349,4 @@ std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel, BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms); } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/service/windows/service_host.h b/src/service/windows/service_host.h index d0dba77..efa9baf 100644 --- a/src/service/windows/service_host.h +++ b/src/service/windows/service_host.h @@ -64,6 +64,7 @@ class CrossDeskServiceHost { std::string SendSecureDesktopKeyboardInput(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); static void WINAPI ServiceMain(DWORD argc, LPWSTR* argv); static BOOL WINAPI ConsoleControlHandler(DWORD control_type); @@ -150,4 +151,4 @@ std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel, } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/tests/windows_manifest_resource_test.cpp b/tests/windows_manifest_resource_test.cpp new file mode 100644 index 0000000..f63e68b --- /dev/null +++ b/tests/windows_manifest_resource_test.cpp @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace { + +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) { + return {}; + } + + std::ostringstream stream; + stream << file.rdbuf(); + return stream.str(); +} + +std::filesystem::path FindRepoRoot() { + std::filesystem::path current = std::filesystem::current_path(); + while (!current.empty()) { + if (std::filesystem::exists(current / "xmake.lua") && + std::filesystem::exists(current / "scripts/windows/crossdesk.rc")) { + return current; + } + current = current.parent_path(); + } + return {}; +} + +bool ExpectContains(const char* name, const std::string& value, + const std::string& expected) { + if (value.find(expected) != std::string::npos) { + return true; + } + + std::cerr << name << " missing expected text: " << expected << "\n"; + return false; +} + +bool ExpectNotContains(const char* name, const std::string& value, + const std::string& unexpected) { + if (value.find(unexpected) == std::string::npos) { + return true; + } + + std::cerr << name << " contains unexpected text: " << unexpected << "\n"; + return false; +} + +#ifdef _WIN32 +bool ExpectActivationContext(const std::filesystem::path& manifest_path) { + ACTCTXW context = {}; + context.cbSize = sizeof(context); + std::wstring source = manifest_path.wstring(); + context.lpSource = source.c_str(); + + HANDLE activation_context = CreateActCtxW(&context); + if (activation_context == INVALID_HANDLE_VALUE) { + std::cerr << "CreateActCtxW failed for " << manifest_path.string() + << ", error=" << GetLastError() << "\n"; + return false; + } + + ReleaseActCtx(activation_context); + return true; +} +#endif + +} // namespace + +int main() { + const std::filesystem::path repo_root = FindRepoRoot(); + if (repo_root.empty()) { + std::cerr << "failed to locate repository root\n"; + return 1; + } + + const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc"); + const std::string manifest = + ReadFile(repo_root / "scripts/windows/crossdesk.manifest"); + const std::string debug_manifest = + ReadFile(repo_root / "scripts/windows/crossdesk_debug.manifest"); + + bool ok = true; + ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest"); + ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest"); + ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG"); + ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST"); + ok &= ExpectContains("crossdesk.manifest", manifest, + "level=\"requireAdministrator\""); + ok &= ExpectContains("crossdesk.manifest", manifest, + "http://schemas.microsoft.com/SMI/2005/WindowsSettings"); + ok &= ExpectContains("crossdesk.manifest", manifest, + "http://schemas.microsoft.com/SMI/2016/WindowsSettings"); + ok &= ExpectNotContains("crossdesk.manifest", manifest, + "processorArchitecture=\"*\""); + ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest, + "level=\"asInvoker\""); + ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest, + "http://schemas.microsoft.com/SMI/2005/WindowsSettings"); + ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest, + "http://schemas.microsoft.com/SMI/2016/WindowsSettings"); + ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest, + "processorArchitecture=\"*\""); +#ifdef _WIN32 + ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest"); + ok &= ExpectActivationContext( + repo_root / "scripts/windows/crossdesk_debug.manifest"); +#endif + return ok ? 0 : 1; +} diff --git a/tests/windows_mouse_controller_safety_test.cpp b/tests/windows_mouse_controller_safety_test.cpp new file mode 100644 index 0000000..6a9e9f5 --- /dev/null +++ b/tests/windows_mouse_controller_safety_test.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include + +namespace { + +std::filesystem::path FindRepoRoot() { + std::filesystem::path current = std::filesystem::current_path(); + while (!current.empty()) { + if (std::filesystem::exists(current / "xmake.lua") && + std::filesystem::exists( + current / "src/device_controller/mouse/windows/mouse_controller.cpp")) { + return current; + } + current = current.parent_path(); + } + return {}; +} + +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) { + return {}; + } + + std::ostringstream stream; + stream << file.rdbuf(); + return stream.str(); +} + +bool ExpectContains(const char* name, const std::string& value, + const std::string& expected) { + if (value.find(expected) != std::string::npos) { + return true; + } + + std::cerr << name << " missing expected text: " << expected << "\n"; + return false; +} + +} // namespace + +int main() { + const std::filesystem::path repo_root = FindRepoRoot(); + if (repo_root.empty()) { + std::cerr << "failed to locate repository root\n"; + return 1; + } + + const std::string mouse_controller = ReadFile( + repo_root / "src/device_controller/mouse/windows/mouse_controller.cpp"); + + bool ok = true; + ok &= ExpectContains("mouse_controller.cpp", mouse_controller, + "INPUT ip = {0};"); + ok &= ExpectContains("mouse_controller.cpp", mouse_controller, + "SetCursorPos failed"); + ok &= ExpectContains("mouse_controller.cpp", mouse_controller, + "SendInput failed for mouse"); + return ok ? 0 : 1; +} diff --git a/tests/windows_service_mouse_ipc_test.cpp b/tests/windows_service_mouse_ipc_test.cpp new file mode 100644 index 0000000..52e6333 --- /dev/null +++ b/tests/windows_service_mouse_ipc_test.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include + +namespace { + +std::filesystem::path FindRepoRoot() { + std::filesystem::path current = std::filesystem::current_path(); + while (!current.empty()) { + if (std::filesystem::exists(current / "xmake.lua") && + std::filesystem::exists(current / "src/service/windows/service_host.cpp")) { + return current; + } + current = current.parent_path(); + } + return {}; +} + +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) { + return {}; + } + + std::ostringstream stream; + stream << file.rdbuf(); + return stream.str(); +} + +bool ExpectContains(const char* name, const std::string& value, + const std::string& expected) { + if (value.find(expected) != std::string::npos) { + return true; + } + + std::cerr << name << " missing expected text: " << expected << "\n"; + return false; +} + +} // namespace + +int main() { + const std::filesystem::path repo_root = FindRepoRoot(); + if (repo_root.empty()) { + std::cerr << "failed to locate repository root\n"; + return 1; + } + + const std::string service_host = + ReadFile(repo_root / "src/service/windows/service_host.cpp"); + + bool ok = true; + ok &= ExpectContains("service_host.cpp", service_host, + "ParseSecureDesktopMouseIpcCommand"); + ok &= ExpectContains("service_host.cpp", service_host, + "BuildSecureInputHelperMouseCommand"); + ok &= ExpectContains("service_host.cpp", service_host, + "return SendSecureDesktopMouseInput"); + return ok ? 0 : 1; +} diff --git a/xmake/targets.lua b/xmake/targets.lua index d30d687..5cdd9af 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -39,6 +39,21 @@ function setup_targets() add_includedirs("src/device_controller") add_files("tests/macos_keyboard_modifier_state_test.cpp") + target("windows_manifest_resource_test") + set_kind("binary") + set_default(false) + add_files("tests/windows_manifest_resource_test.cpp") + + target("windows_service_mouse_ipc_test") + set_kind("binary") + set_default(false) + add_files("tests/windows_service_mouse_ipc_test.cpp") + + target("windows_mouse_controller_safety_test") + set_kind("binary") + set_default(false) + add_files("tests/windows_mouse_controller_safety_test.cpp") + target("screen_capturer") set_kind("object") add_deps("rd_log", "common")