//! Keyboard input simulation using Windows SendInput API //! //! Injection is **scan-code based** (`KEYEVENTF_SCANCODE`) rather than virtual-key //! based. Scan codes are layout-independent: the same physical key produces the same //! scan code regardless of the remote keyboard layout, so the remote machine's active //! layout (not the technician's) decides what character a key produces. The viewer //! still carries the virtual-key code for logic that needs it, and we fall back to //! deriving a scan code from the VK when the wire frame did not supply one. use anyhow::Result; #[cfg(windows)] use windows::Win32::UI::Input::KeyboardAndMouse::{ MapVirtualKeyW, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYBD_EVENT_FLAGS, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, KEYEVENTF_UNICODE, MAPVK_VK_TO_VSC_EX, }; /// Keyboard input controller pub struct KeyboardController { /// Tracks which modifier keys this controller currently holds DOWN on the remote. /// Used so a focus-loss / session-end re-sync can release any still-held modifier /// and avoid "stuck" Ctrl/Alt/Shift/Win on the remote desktop. modifiers: ModifierState, } /// Tracks the down/up state of each modifier the agent has injected. #[derive(Default)] struct ModifierState { ctrl: bool, alt: bool, shift: bool, meta: bool, } impl ModifierState { /// Record a modifier transition for `vk_code`. Returns `true` if `vk_code` is a /// modifier key (and the state was updated), `false` otherwise. fn record(&mut self, vk_code: u16, down: bool) -> bool { match vk_code { // VK_CONTROL / VK_LCONTROL / VK_RCONTROL 0x11 | 0xA2 | 0xA3 => { self.ctrl = down; true } // VK_MENU / VK_LMENU / VK_RMENU (Alt) 0x12 | 0xA4 | 0xA5 => { self.alt = down; true } // VK_SHIFT / VK_LSHIFT / VK_RSHIFT 0x10 | 0xA0 | 0xA1 => { self.shift = down; true } // VK_LWIN / VK_RWIN 0x5B | 0x5C => { self.meta = down; true } _ => false, } } /// Return the VK codes of every modifier currently held down, then clear the state. fn drain_held(&mut self) -> Vec { let mut held = Vec::new(); if self.ctrl { held.push(0x11); } if self.alt { held.push(0x12); } if self.shift { held.push(0x10); } if self.meta { held.push(0x5B); } *self = ModifierState::default(); held } } impl KeyboardController { /// Create a new keyboard controller pub fn new() -> Result { Ok(Self { modifiers: ModifierState::default(), }) } /// Press a key down by virtual key code (scan code derived from the VK). #[cfg(windows)] pub fn key_down(&mut self, vk_code: u16) -> Result<()> { self.send_key(vk_code, 0, false, true) } /// Release a key by virtual key code (scan code derived from the VK). #[cfg(windows)] pub fn key_up(&mut self, vk_code: u16) -> Result<()> { self.send_key(vk_code, 0, false, false) } /// Inject a full-fidelity key event. /// /// `scan_code` is the hardware scan code captured by the viewer's low-level hook /// (0 ⇒ derive it from `vk_code`). `is_extended` is the viewer-captured extended-key /// flag (`LLKHF_EXTENDED`); when `false` the agent still derives the flag from the /// VK / scan code so older viewers that don't set it stay correct. #[cfg(windows)] pub fn key_event_full( &mut self, vk_code: u16, scan_code: u16, is_extended: bool, down: bool, ) -> Result<()> { self.send_key(vk_code, scan_code, is_extended, down) } /// Release every modifier this controller currently holds down on the remote. /// /// Called on viewer focus loss and at session end so a Ctrl/Alt/Shift/Win that was /// pressed but whose key-up never arrived (e.g. the technician alt-tabbed away) does /// not stay latched on the remote desktop. #[cfg(windows)] pub fn release_all_modifiers(&mut self) -> Result<()> { for vk in self.modifiers.drain_held() { // Emit the key-up directly; drain_held already cleared the tracked state. if let Err(e) = self.send_key(vk, 0, false, false) { tracing::warn!("Failed to release held modifier vk={:#x}: {}", vk, e); } else { tracing::debug!("Released stuck modifier vk={:#x} on focus loss", vk); } } Ok(()) } /// Send a key event using scan-code injection. #[cfg(windows)] fn send_key( &mut self, vk_code: u16, scan_code: u16, is_extended: bool, down: bool, ) -> Result<()> { // Track modifier state so we can release stuck modifiers later. self.modifiers.record(vk_code, down); // Prefer the viewer-supplied scan code; fall back to deriving one from the VK. // MAPVK_VK_TO_VSC_EX yields a 0xE0-prefixed value for extended keys. let mapped = unsafe { MapVirtualKeyW(vk_code as u32, MAPVK_VK_TO_VSC_EX) as u16 }; let effective_scan = if scan_code != 0 { scan_code } else { mapped }; let mut flags = KEYBD_EVENT_FLAGS::default() | KEYEVENTF_SCANCODE; // Add the extended flag if the viewer flagged it, the VK is inherently // extended, or the mapped scan code carries the 0xE0 extended prefix. if is_extended || Self::is_extended_key(vk_code) || (mapped >> 8) == 0xE0 { flags |= KEYEVENTF_EXTENDEDKEY; } if !down { flags |= KEYEVENTF_KEYUP; } // For scan-code injection the low byte of the scan code is what Windows uses; // the 0xE0 prefix is conveyed via KEYEVENTF_EXTENDEDKEY, not the wScan value. let w_scan = (effective_scan & 0x00FF) as u16; let input = INPUT { r#type: INPUT_KEYBOARD, Anonymous: INPUT_0 { ki: KEYBDINPUT { wVk: windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY(0), wScan: w_scan, dwFlags: flags, time: 0, dwExtraInfo: 0, }, }, }; self.send_input(&[input]) } /// Type a unicode character #[allow(dead_code)] #[cfg(windows)] pub fn type_char(&mut self, ch: char) -> Result<()> { let mut inputs = Vec::new(); let mut buf = [0u16; 2]; let encoded = ch.encode_utf16(&mut buf); // For characters that fit in a single u16 for &code_unit in encoded.iter() { // Key down inputs.push(INPUT { r#type: INPUT_KEYBOARD, Anonymous: INPUT_0 { ki: KEYBDINPUT { wVk: windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY(0), wScan: code_unit, dwFlags: KEYEVENTF_UNICODE, time: 0, dwExtraInfo: 0, }, }, }); // Key up inputs.push(INPUT { r#type: INPUT_KEYBOARD, Anonymous: INPUT_0 { ki: KEYBDINPUT { wVk: windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY(0), wScan: code_unit, dwFlags: KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, time: 0, dwExtraInfo: 0, }, }, }); } self.send_input(&inputs) } /// Type a string of text #[allow(dead_code)] #[cfg(windows)] pub fn type_string(&mut self, text: &str) -> Result<()> { for ch in text.chars() { self.type_char(ch)?; } Ok(()) } /// Send Secure Attention Sequence (Ctrl+Alt+Delete) /// /// Ctrl+Alt+Del is the Secure Attention Sequence and **cannot** be injected via /// `SendInput` — Windows reserves it. It must be raised by `SendSAS`, which only /// works when the caller runs as SYSTEM (or has SeTcbPrivilege) AND the /// `SoftwareSASGeneration` Winlogon policy permits software-generated SAS. The /// managed installer is responsible for installing the SAS helper service (running /// as SYSTEM) and setting that policy. See `set_software_sas_policy` in /// `bin/sas_service.rs` and the `// TODO(installer)` note there. /// /// Tiers, in order: /// 1. The GuruConnect SAS helper service (SYSTEM) via named-pipe IPC — the supported path. /// 2. Direct `sas.dll!SendSAS` — only succeeds if THIS process is already SYSTEM with the policy. /// 3. Fallback key simulation — will NOT reach the secure desktop; logged as a clear failure. #[cfg(windows)] pub fn send_sas(&mut self) -> Result<()> { // Tier 1: Try the SAS service (named pipe IPC to SYSTEM service) match crate::sas_client::request_sas() { Ok(()) => { tracing::info!("SAS sent via GuruConnect SAS Service"); return Ok(()); } Err(e) => { tracing::warn!( "SAS helper service unavailable ({}); trying direct sas.dll", e ); } } // Tier 2: Try using the sas.dll directly (requires SYSTEM + SoftwareSASGeneration) use windows::core::PCWSTR; use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW}; unsafe { let dll_name: Vec = "sas.dll\0".encode_utf16().collect(); let lib = LoadLibraryW(PCWSTR(dll_name.as_ptr())); if let Ok(lib) = lib { let proc_name = b"SendSAS\0"; if let Some(proc) = GetProcAddress(lib, windows::core::PCSTR(proc_name.as_ptr())) { // SendSAS takes a BOOL parameter: FALSE for Ctrl+Alt+Del. // It silently no-ops if the caller lacks privilege / the policy is // unset, so we cannot detect success here — but it is the best // effort short of the SYSTEM helper. let send_sas: extern "system" fn(i32) = std::mem::transmute(proc); send_sas(0); // FALSE = Ctrl+Alt+Del tracing::info!("SAS attempted via direct sas.dll call (effective only if SYSTEM + SoftwareSASGeneration policy set)"); return Ok(()); } } } // Tier 3: SAS could not be delivered through any privileged path. A plain // SendInput of Ctrl+Alt+Del never reaches the secure desktop, so report a // clear, actionable error instead of pretending it worked. let msg = "Ctrl+Alt+Del could not be delivered: the GuruConnect SAS helper \ service is not running and sas.dll!SendSAS is unavailable. Ensure the \ SAS service is installed (runs as SYSTEM) and the SoftwareSASGeneration \ policy is enabled by the installer."; tracing::error!("{}", msg); anyhow::bail!("{}", msg) } /// Check if a virtual key code is an extended key #[cfg(windows)] fn is_extended_key(vk: u16) -> bool { vk_is_extended(vk) } /// Send input events #[cfg(windows)] fn send_input(&self, inputs: &[INPUT]) -> Result<()> { let sent = unsafe { SendInput(inputs, std::mem::size_of::() as i32) }; if sent as usize != inputs.len() { anyhow::bail!("SendInput failed: sent {} of {} inputs", sent, inputs.len()); } Ok(()) } #[cfg(not(windows))] pub fn key_down(&mut self, _vk_code: u16) -> Result<()> { anyhow::bail!("Keyboard input only supported on Windows") } #[cfg(not(windows))] pub fn key_up(&mut self, _vk_code: u16) -> Result<()> { anyhow::bail!("Keyboard input only supported on Windows") } #[cfg(not(windows))] pub fn key_event_full( &mut self, _vk_code: u16, _scan_code: u16, _is_extended: bool, _down: bool, ) -> Result<()> { anyhow::bail!("Keyboard input only supported on Windows") } #[cfg(not(windows))] pub fn release_all_modifiers(&mut self) -> Result<()> { anyhow::bail!("Keyboard input only supported on Windows") } #[cfg(not(windows))] pub fn type_char(&mut self, _ch: char) -> Result<()> { anyhow::bail!("Keyboard input only supported on Windows") } #[cfg(not(windows))] pub fn send_sas(&mut self) -> Result<()> { anyhow::bail!("SAS only supported on Windows") } } /// Common Windows virtual key codes #[allow(dead_code)] pub mod vk { pub const BACK: u16 = 0x08; pub const TAB: u16 = 0x09; pub const RETURN: u16 = 0x0D; pub const SHIFT: u16 = 0x10; pub const CONTROL: u16 = 0x11; pub const MENU: u16 = 0x12; // Alt pub const PAUSE: u16 = 0x13; pub const CAPITAL: u16 = 0x14; // Caps Lock pub const ESCAPE: u16 = 0x1B; pub const SPACE: u16 = 0x20; pub const PRIOR: u16 = 0x21; // Page Up pub const NEXT: u16 = 0x22; // Page Down pub const END: u16 = 0x23; pub const HOME: u16 = 0x24; pub const LEFT: u16 = 0x25; pub const UP: u16 = 0x26; pub const RIGHT: u16 = 0x27; pub const DOWN: u16 = 0x28; pub const INSERT: u16 = 0x2D; pub const DELETE: u16 = 0x2E; // 0-9 keys pub const KEY_0: u16 = 0x30; pub const KEY_9: u16 = 0x39; // A-Z keys pub const KEY_A: u16 = 0x41; pub const KEY_Z: u16 = 0x5A; // Windows keys pub const LWIN: u16 = 0x5B; pub const RWIN: u16 = 0x5C; // Function keys pub const F1: u16 = 0x70; pub const F2: u16 = 0x71; pub const F3: u16 = 0x72; pub const F4: u16 = 0x73; pub const F5: u16 = 0x74; pub const F6: u16 = 0x75; pub const F7: u16 = 0x76; pub const F8: u16 = 0x77; pub const F9: u16 = 0x78; pub const F10: u16 = 0x79; pub const F11: u16 = 0x7A; pub const F12: u16 = 0x7B; // Modifier keys pub const LSHIFT: u16 = 0xA0; pub const RSHIFT: u16 = 0xA1; pub const LCONTROL: u16 = 0xA2; pub const RCONTROL: u16 = 0xA3; pub const LMENU: u16 = 0xA4; // Left Alt pub const RMENU: u16 = 0xA5; // Right Alt } /// Whether a Windows virtual-key code is an "extended" key. /// /// Extended keys must be injected with `KEYEVENTF_EXTENDEDKEY`. This is the /// platform-independent classifier so the determination can be unit-tested off-Windows; /// the `#[cfg(windows)]` injection path delegates here. The viewer-captured /// `LLKHF_EXTENDED` flag is authoritative when present; this is the fallback used when /// the wire frame did not carry it (older viewers / VK-only synthesis). pub fn vk_is_extended(vk: u16) -> bool { matches!( vk, 0x21..=0x28 | // Page Up, Page Down, End, Home, Arrow keys 0x2D | 0x2E | // Insert, Delete 0x5B | 0x5C | // Left/Right Windows keys 0x5D | // Applications key 0x6F | // Numpad Divide 0x90 | // Num Lock 0x91 | // Scroll Lock 0xA3 | // Right Control 0xA5 // Right Alt (AltGr) ) } #[cfg(test)] mod tests { use super::*; #[test] fn extended_keys_are_flagged() { // Arrows / navigation block. for vk in [0x21u16, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28] { assert!(vk_is_extended(vk), "vk={:#x} should be extended", vk); } // Insert / Delete. assert!(vk_is_extended(0x2D)); assert!(vk_is_extended(0x2E)); // Win keys, Apps, NumLock, numpad Divide. assert!(vk_is_extended(0x5B)); assert!(vk_is_extended(0x5C)); assert!(vk_is_extended(0x5D)); assert!(vk_is_extended(0x6F)); assert!(vk_is_extended(0x90)); // Right Ctrl / Right Alt. assert!(vk_is_extended(0xA3)); assert!(vk_is_extended(0xA5)); } #[test] fn non_extended_keys_are_not_flagged() { // Letters, digits, space, enter, left modifiers, numpad digits. for vk in [ 0x41u16, // A 0x5A, // Z 0x30, // 0 0x20, // Space 0x0D, // Enter 0xA0, // Left Shift 0xA2, // Left Control 0xA4, // Left Alt 0x60, // Numpad 0 0x6A, // Numpad Multiply (NOT extended; only Divide is) ] { assert!(!vk_is_extended(vk), "vk={:#x} should NOT be extended", vk); } } #[test] fn modifier_state_records_ctrl_alt_shift_win() { let mut m = ModifierState::default(); // Each of the VK aliases maps to its modifier flag. assert!(m.record(0x11, true)); // VK_CONTROL assert!(m.ctrl); assert!(m.record(0xA4, true)); // VK_LMENU (Alt) assert!(m.alt); assert!(m.record(0xA0, true)); // VK_LSHIFT assert!(m.shift); assert!(m.record(0x5C, true)); // VK_RWIN assert!(m.meta); } #[test] fn modifier_state_ignores_non_modifiers() { let mut m = ModifierState::default(); assert!(!m.record(0x41, true)); // 'A' is not a modifier assert!(!m.ctrl && !m.alt && !m.shift && !m.meta); } #[test] fn modifier_state_tracks_down_then_up() { let mut m = ModifierState::default(); m.record(0x11, true); // Ctrl down assert!(m.ctrl); m.record(0x11, false); // Ctrl up assert!(!m.ctrl); } #[test] fn drain_held_returns_and_clears_held_modifiers() { let mut m = ModifierState::default(); m.record(0xA2, true); // Left Ctrl -> ctrl m.record(0x12, true); // Alt // Shift and Win were never pressed. let mut held = m.drain_held(); held.sort_unstable(); // Canonical VKs returned: Ctrl(0x11), Alt(0x12). assert_eq!(held, vec![0x11u16, 0x12]); // State is cleared after draining. assert!(!m.ctrl && !m.alt && !m.shift && !m.meta); // A second drain yields nothing. assert!(m.drain_held().is_empty()); } #[test] fn drain_held_empty_when_nothing_pressed() { let mut m = ModifierState::default(); assert!(m.drain_held().is_empty()); } }