diff --git a/libs/base/protos/message.proto b/libs/base/protos/message.proto index 991e8a31e..146e44036 100644 --- a/libs/base/protos/message.proto +++ b/libs/base/protos/message.proto @@ -1030,5 +1030,7 @@ message Message { TerminalAction terminal_action = 31; TerminalResponse terminal_response = 32; PortForwardChannel port_forward_channel = 33; + // Auto Login: the peer types the password only if it is at its logon or lock screen. + OSLogin os_login = 34; } } diff --git a/src/client.rs b/src/client.rs index d3e66e73c..6d752d498 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4467,6 +4467,32 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { interface.send(Data::Message(msg_out)); } +/// Auto Login. A peer that can check for its logon or lock screen gets the password and types it +/// itself, only if it is there right then. Keys typed from here land in whatever has focus if the +/// screen is not locked by the time they arrive. +pub fn auto_login(p: String, pi: &PeerInfo, interface: impl Interface) { + if !is_peer_handling_os_login(pi) { + input_os_password(p, true, interface); + return; + } + std::thread::spawn(move || { + activate_os(&interface, true); + std::thread::sleep(Duration::from_millis(1200)); + let mut msg_out = Message::new(); + msg_out.set_os_login(OSLogin { + password: p, + ..Default::default() + }); + interface.send(Data::Message(msg_out)); + }); +} + +/// Windows and macOS peers from the 1.5.1 release handle `os_login`. +fn is_peer_handling_os_login(pi: &PeerInfo) -> bool { + (pi.platform == crate::common::PLATFORM_WINDOWS || pi.platform == crate::common::PLATFORM_MACOS) + && hbb_common::get_version_number(&pi.version) >= hbb_common::get_version_number("1.5.1") +} + #[derive(Copy, Clone)] struct LoginErrorMsgBox { msgtype: &'static str, diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 334ae40e2..ddc190ccc 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -827,6 +827,10 @@ pub fn is_locked() -> bool { } } +pub fn is_logon_screen() -> bool { + is_prelogin() || is_locked() +} + pub fn is_root() -> bool { crate::username() == "root" } diff --git a/src/platform/windows.cc b/src/platform/windows.cc index c06b58f3f..03e171838 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -604,14 +604,16 @@ extern "C" return rdp_or_console; } - BOOL is_session_locked(DWORD session_id) + // WTSInfoExLevel1.SessionFlags, or WTS_SESSIONSTATE_UNKNOWN when it cannot be read. + DWORD get_session_flags(DWORD session_id) { + DWORD flags = WTS_SESSIONSTATE_UNKNOWN; + // WTS_CURRENT_SESSION, which would query the caller's session instead. if (session_id == 0xFFFFFFFF) { - return FALSE; + return flags; } PWTSINFOEXW pInfo = NULL; DWORD bytes = 0; - BOOL locked = FALSE; if (WTSQuerySessionInformationW( WTS_CURRENT_SERVER_HANDLE, session_id, @@ -619,13 +621,13 @@ extern "C" (LPWSTR *)&pInfo, &bytes)) { if (pInfo && pInfo->Level == 1) { - locked = (pInfo->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_LOCK); + flags = (DWORD)pInfo->Data.WTSInfoExLevel1.SessionFlags; } if (pInfo) { WTSFreeMemory(pInfo); } } - return locked; + return flags; } uint32_t get_active_user(PWSTR bufin, uint32_t nin, BOOL rdp) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6eb21327f..45fec0c0e 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -559,7 +559,6 @@ const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; extern "C" { fn get_current_session(rdp: BOOL) -> DWORD; - fn is_session_locked(session_id: DWORD) -> BOOL; fn LaunchProcessWin( cmd: *const u16, session_id: DWORD, @@ -1263,7 +1262,7 @@ pub fn is_locked() -> bool { let Some(session_id) = get_current_process_session_id() else { return false; }; - unsafe { is_session_locked(session_id) == TRUE } + is_session_locked_flag(session_id) == Some(true) } #[inline] @@ -1277,6 +1276,47 @@ pub fn is_logon_ui() -> ResultType { .any(|pid| get_session_id_of_process(pid) == Some(current_sid))) } +// Auto-login types the OS password on this, so a state that cannot be read counts as unlocked. +// `is_prelogin()` reads a failed query as no user signed in, and `is_logon_ui()` is left out +// because a LogonUI.exe can outlive the unlock. +pub fn is_logon_screen() -> bool { + let Some(session_id) = get_current_process_session_id() else { + return false; + }; + is_session_prelogin(session_id) == Some(true) + || is_session_locked_flag(session_id) == Some(true) +} + +fn is_session_prelogin(session_id: u32) -> Option { + extern "C" { + fn get_session_user_info(path: *mut u16, n: u32, session_id: u32) -> u32; + } + let mut buff = [0u16; 256]; + // The bytes copied include the terminating null, so 0 means the query failed. + let n = unsafe { get_session_user_info(buff.as_mut_ptr(), buff.len() as _, session_id) }; + if n == 0 { + return None; + } + let username = String::from_utf16_lossy(&buff); + let username = username.trim_end_matches('\0'); + Some(username.is_empty() || username == "SYSTEM") +} + +// Windows 7 and Server 2008 R2 report LOCK and UNLOCK the other way round (see WTSINFOEX_LEVEL1). +fn is_session_locked_flag(session_id: u32) -> Option { + extern "C" { + fn get_session_flags(session_id: DWORD) -> DWORD; + } + const WTS_SESSIONSTATE_LOCK: DWORD = 0; + const WTS_SESSIONSTATE_UNLOCK: DWORD = 1; + let reversed = !base::platform::windows::is_windows_version_or_greater(6, 2, 0, 0, 0); + match unsafe { get_session_flags(session_id) } { + WTS_SESSIONSTATE_LOCK => Some(!reversed), + WTS_SESSIONSTATE_UNLOCK => Some(reversed), + _ => None, + } +} + pub fn is_root() -> bool { // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user unsafe { is_local_system() == TRUE } diff --git a/src/server/connection.rs b/src/server/connection.rs index eddd0c521..193f62686 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2463,6 +2463,41 @@ impl Connection { self.tx_input.send(MessageInput::Key((msg, press))).ok(); } + // Auto Login: the password and Return the client would otherwise type itself, typed only if a + // check right before finds the logon or lock screen. + #[cfg(any(target_os = "windows", target_os = "macos"))] + async fn type_auto_login_password(&self, password: String) { + // A peer can send this at any rate, hence the throttled logs. + let at_logon_screen = tokio::task::spawn_blocking(crate::platform::is_logon_screen) + .await + .unwrap_or_else(|e| { + hbb_common::throttled_log!( + Duration::from_secs(60), + error, + "Failed to check the logon screen: {}", + e + ); + false + }); + if !at_logon_screen { + hbb_common::throttled_log!( + Duration::from_secs(60), + info, + "Auto login dropped, not at the logon or lock screen" + ); + return; + } + let mut key_event = KeyEvent::new(); + key_event.mode = KeyboardMode::Legacy.into(); + key_event.press = true; + key_event.set_seq(password); + self.input_key(key_event.clone(), false); + key_event.set_control_key(ControlKey::Return); + CLICK_TIME.store(get_time(), Ordering::SeqCst); + MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); + self.input_key(key_event, true); + } + fn verify_h1(&self, h1: &[u8]) -> bool { let mut hasher2 = Sha256::new(); hasher2.update(h1); @@ -3355,6 +3390,13 @@ impl Connection { } self.update_auto_disconnect_timer(); } + #[cfg(any(target_os = "windows", target_os = "macos"))] + Some(message::Union::OsLogin(login)) => { + if self.peer_keyboard_enabled() { + self.type_auto_login_password(login.password).await; + } + self.update_auto_disconnect_timer(); + } Some(message::Union::Clipboard(cb)) => { if self.should_handle_text_clipboard_message() && self.clipboard_enabled() { #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -6114,6 +6156,7 @@ impl Connection { Some(message::Union::TerminalAction(_)) => "terminal_action", Some(message::Union::TerminalResponse(_)) => "terminal_response", Some(message::Union::PortForwardChannel(_)) => "port_forward_channel", + Some(message::Union::OsLogin(_)) => "os_login", Some(message::Union::Misc(misc)) => Self::misc_message_family(misc), Some(_) => "message.other", None => "empty", diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f28752e77..abe38272f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1832,7 +1832,7 @@ impl Interface for Session { self.try_change_init_resolution(pi.current_display); let p = self.lc.read().unwrap().should_auto_login(); if !p.is_empty() { - input_os_password(p, true, self.clone()); + crate::client::auto_login(p, &pi, self.clone()); } } let current = &pi.displays[pi.current_display as usize];