diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index 893f5918c..10cde9cbe 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -63,6 +63,8 @@ extern crate objc; mod win; #[cfg(target_os = "windows")] pub use win::Enigo; +#[cfg(target_os = "windows")] +pub use win::ENIGO_INPUT_EXTRA_VALUE; #[cfg(target_os = "macos")] mod macos; diff --git a/libs/enigo/src/win/mod.rs b/libs/enigo/src/win/mod.rs index 024d7a3fd..62cdbd6e6 100644 --- a/libs/enigo/src/win/mod.rs +++ b/libs/enigo/src/win/mod.rs @@ -2,3 +2,4 @@ mod win_impl; pub mod keycodes; pub use self::win_impl::Enigo; +pub use self::win_impl::ENIGO_INPUT_EXTRA_VALUE; diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index c1fc32131..152c65b6a 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -1,7 +1,7 @@ use winapi; use self::winapi::ctypes::c_int; -use self::winapi::shared::{minwindef::*, windef::*}; +use self::winapi::shared::{basetsd::ULONG_PTR, minwindef::*, windef::*}; use self::winapi::um::winbase::*; use self::winapi::um::winuser::*; @@ -18,6 +18,9 @@ extern "system" { pub struct Enigo; static mut LAYOUT: HKL = std::ptr::null_mut(); +/// The dwExtraInfo value in keyboard and mouse structure that used in SendInput() +pub const ENIGO_INPUT_EXTRA_VALUE: ULONG_PTR = 100; + fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD { let mut input = INPUT { type_: INPUT_MOUSE, @@ -28,7 +31,7 @@ fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD { mouseData: data, dwFlags: flags, time: 0, - dwExtraInfo: 0, + dwExtraInfo: ENIGO_INPUT_EXTRA_VALUE, }) }, }; @@ -56,7 +59,7 @@ fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { wScan: scan, dwFlags: flags, time: 0, - dwExtraInfo: 0, + dwExtraInfo: ENIGO_INPUT_EXTRA_VALUE, }) }, }; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index c296e6a7f..048301d7e 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -65,6 +65,10 @@ message LoginRequest { message ChatMessage { string text = 1; } +message Features { + bool privacy_mode = 1; +} + message PeerInfo { string username = 1; string hostname = 2; @@ -74,6 +78,7 @@ message PeerInfo { bool sas_enabled = 6; string version = 7; int32 conn_id = 8; + Features features = 9; } message LoginResponse { @@ -442,11 +447,6 @@ message OptionMessage { BoolOption enable_file_transfer = 9; } -message OptionResponse { - OptionMessage opt = 1; - string error = 2; -} - message TestDelay { int64 time = 1; bool from_client = 2; @@ -469,6 +469,44 @@ message AudioFrame { int64 timestamp = 2; } +message BackNotification { + // no need to consider block input by someone else + enum BlockInputState { + StateUnknown = 1; + OnSucceeded = 2; + OnFailed = 3; + OffSucceeded = 4; + OffFailed = 5; + } + enum PrivacyModeState { + StateUnknown = 1; + // Privacy mode on by someone else + OnByOther = 2; + // Privacy mode is not supported on the remote side + NotSupported = 3; + // Privacy mode on by self + OnSucceeded = 4; + // Privacy mode on by self, but denied + OnFailedDenied = 5; + // Some plugins are not found + OnFailedPlugin = 6; + // Privacy mode on by self, but failed + OnFailed = 7; + // Privacy mode off by self + OffSucceeded = 8; + // Ctrl + P + OffByPeer = 9; + // Privacy mode off by self, but failed + OffFailed = 10; + OffUnknown = 11; + } + + oneof union { + PrivacyModeState privacy_mode_state = 1; + BlockInputState block_input_state = 2; + } +} + message Misc { oneof union { ChatMessage chat_message = 4; @@ -478,8 +516,8 @@ message Misc { AudioFormat audio_format = 8; string close_reason = 9; bool refresh_video = 10; - OptionResponse option_response = 11; bool video_received = 12; + BackNotification back_notification = 13; } } diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 00c4509ab..424189785 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -17,11 +17,12 @@ block = "0.1" cfg-if = "1.0" libc = "0.2" num_cpus = "1.13" +lazy_static = "1.4" [dependencies.winapi] version = "0.3" default-features = true -features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser"] +features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser", "winerror", "errhandlingapi", "libloaderapi"] [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.10" diff --git a/libs/scrap/examples/capture_mag.rs b/libs/scrap/examples/capture_mag.rs new file mode 100644 index 000000000..3e15b4e69 --- /dev/null +++ b/libs/scrap/examples/capture_mag.rs @@ -0,0 +1,105 @@ +extern crate repng; +extern crate scrap; + +use std::fs::File; + +#[cfg(windows)] +use scrap::CapturerMag; +use scrap::{i420_to_rgb, Display}; + +fn main() { + let n = Display::all().unwrap().len(); + for i in 0..n { + #[cfg(windows)] + record(i); + } +} + +fn get_display(i: usize) -> Display { + Display::all().unwrap().remove(i) +} + +#[cfg(windows)] +fn record(i: usize) { + for d in Display::all().unwrap() { + println!("{:?} {} {}", d.origin(), d.width(), d.height()); + } + + let display = get_display(i); + let (w, h) = (display.width(), display.height()); + + { + let mut capture_mag = + CapturerMag::new(display.origin(), display.width(), display.height(), false) + .expect("Couldn't begin capture."); + let wnd_cls = ""; + let wnd_name = "RustDeskPrivacyWindow"; + if false == capture_mag.exclude(wnd_cls, wnd_name).unwrap() { + println!("No window found for cls {} name {}", wnd_cls, wnd_name); + } else { + println!("Filter window for cls {} name {}", wnd_cls, wnd_name); + } + + let frame = capture_mag.frame(0).unwrap(); + println!("Capture data len: {}, Saving...", frame.len()); + + let mut bitflipped = Vec::with_capacity(w * h * 4); + let stride = frame.len() / h; + + for y in 0..h { + for x in 0..w { + let i = stride * y + 4 * x; + bitflipped.extend_from_slice(&[frame[i + 2], frame[i + 1], frame[i], 255]); + } + } + // Save the image. + let name = format!("capture_mag_{}_1.png", i); + repng::encode( + File::create(name.clone()).unwrap(), + w as u32, + h as u32, + &bitflipped, + ) + .unwrap(); + println!("Image saved to `{}`.", name); + } + + { + let mut capture_mag = + CapturerMag::new(display.origin(), display.width(), display.height(), true) + .expect("Couldn't begin capture."); + let wnd_cls = ""; + let wnd_title = "RustDeskPrivacyWindow"; + if false == capture_mag.exclude(wnd_cls, wnd_title).unwrap() { + println!("No window found for cls {} title {}", wnd_cls, wnd_title); + } else { + println!("Filter window for cls {} title {}", wnd_cls, wnd_title); + } + + let buffer = capture_mag.frame(0).unwrap(); + println!("Capture data len: {}, Saving...", buffer.len()); + + let mut frame = Default::default(); + i420_to_rgb(w, h, &buffer, &mut frame); + + let mut bitflipped = Vec::with_capacity(w * h * 4); + let stride = frame.len() / h; + + for y in 0..h { + for x in 0..w { + let i = stride * y + 3 * x; + bitflipped.extend_from_slice(&[frame[i], frame[i + 1], frame[i + 2], 255]); + } + } + let name = format!("capture_mag_{}_2.png", i); + repng::encode( + File::create(name.clone()).unwrap(), + w as u32, + h as u32, + &bitflipped, + ) + .unwrap(); + + println!("Image saved to `{}`.", name); + } +} diff --git a/libs/scrap/examples/screenshot.rs b/libs/scrap/examples/screenshot.rs index e2da3b3d8..b52ea11f7 100644 --- a/libs/scrap/examples/screenshot.rs +++ b/libs/scrap/examples/screenshot.rs @@ -46,8 +46,7 @@ fn record(i: usize) { } } }; - - println!("Captured! Saving..."); + println!("Captured data len: {}, Saving...", buffer.len()); // Flip the BGRA image into a RGBA image. @@ -96,8 +95,7 @@ fn record(i: usize) { } } }; - - println!("Captured! Saving..."); + println!("Captured data len: {}, Saving...", buffer.len()); let mut frame = Default::default(); i420_to_rgb(w, h, &buffer, &mut frame); diff --git a/libs/scrap/src/common/dxgi.rs b/libs/scrap/src/common/dxgi.rs index 4683ad5f2..c0b4130bb 100644 --- a/libs/scrap/src/common/dxgi.rs +++ b/libs/scrap/src/common/dxgi.rs @@ -111,3 +111,32 @@ impl Display { self.origin() == (0, 0) } } + +pub struct CapturerMag { + inner: dxgi::mag::CapturerMag, + data: Vec, +} + +impl CapturerMag { + pub fn is_supported() -> bool { + dxgi::mag::CapturerMag::is_supported() + } + + pub fn new(origin: (i32, i32), width: usize, height: usize, use_yuv: bool) -> io::Result { + Ok(CapturerMag { + inner: dxgi::mag::CapturerMag::new(origin, width, height, use_yuv)?, + data: Vec::new(), + }) + } + pub fn exclude(&mut self, cls: &str, name: &str) -> io::Result { + self.inner.exclude(cls, name) + } + // ((x, y), w, h) + pub fn get_rect(&self) -> ((i32, i32), usize, usize) { + self.inner.get_rect() + } + pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> { + self.inner.frame(&mut self.data)?; + Ok(Frame(&self.data)) + } +} diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs new file mode 100644 index 000000000..b7cfe9088 --- /dev/null +++ b/libs/scrap/src/dxgi/mag.rs @@ -0,0 +1,626 @@ +// logic from webrtc -- https://github.com/shiguredo/libwebrtc/blob/main/modules/desktop_capture/win/screen_capturer_win_magnifier.cc +use lazy_static; +use std::{ + ffi::CString, + io::{Error, ErrorKind, Result}, + mem::size_of, + sync::Mutex, +}; +use winapi::{ + shared::{ + basetsd::SIZE_T, + guiddef::{IsEqualGUID, GUID}, + minwindef::{BOOL, DWORD, FALSE, FARPROC, HINSTANCE, HMODULE, HRGN, TRUE, UINT}, + ntdef::{LONG, NULL}, + windef::{HWND, RECT}, + winerror::ERROR_CLASS_ALREADY_EXISTS, + }, + um::{ + errhandlingapi::GetLastError, + libloaderapi::{FreeLibrary, GetModuleHandleExA, GetProcAddress, LoadLibraryExA}, + winuser::*, + }, +}; + +pub const MW_FILTERMODE_EXCLUDE: u32 = 0; +pub const MW_FILTERMODE_INCLUDE: u32 = 1; +pub const GET_MODULE_HANDLE_EX_FLAG_PIN: u32 = 1; +pub const GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT: u32 = 2; +pub const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS: u32 = 4; +pub const LOAD_LIBRARY_AS_DATAFILE: u32 = 2; +pub const LOAD_WITH_ALTERED_SEARCH_PATH: u32 = 8; +pub const LOAD_IGNORE_CODE_AUTHZ_LEVEL: u32 = 16; +pub const LOAD_LIBRARY_AS_IMAGE_RESOURCE: u32 = 32; +pub const LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE: u32 = 64; +pub const LOAD_LIBRARY_REQUIRE_SIGNED_TARGET: u32 = 128; +pub const LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR: u32 = 256; +pub const LOAD_LIBRARY_SEARCH_APPLICATION_DIR: u32 = 512; +pub const LOAD_LIBRARY_SEARCH_USER_DIRS: u32 = 1024; +pub const LOAD_LIBRARY_SEARCH_SYSTEM32: u32 = 2048; +pub const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: u32 = 4096; +pub const LOAD_LIBRARY_SAFE_CURRENT_DIRS: u32 = 8192; +pub const LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER: u32 = 16384; +pub const LOAD_LIBRARY_OS_INTEGRITY_CONTINUITY: u32 = 32768; + +extern "C" { + pub static GUID_WICPixelFormat32bppRGBA: GUID; +} + +lazy_static::lazy_static! { + static ref MAG_BUFFER: Mutex<(bool, Vec)> = Default::default(); +} + +pub type REFWICPixelFormatGUID = *const GUID; +pub type WICPixelFormatGUID = GUID; + +#[allow(non_snake_case)] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct tagMAGIMAGEHEADER { + pub width: UINT, + pub height: UINT, + pub format: WICPixelFormatGUID, + pub stride: UINT, + pub offset: UINT, + pub cbSize: SIZE_T, +} +pub type MAGIMAGEHEADER = tagMAGIMAGEHEADER; +pub type PMAGIMAGEHEADER = *mut tagMAGIMAGEHEADER; + +// Function types +pub type MagImageScalingCallback = ::std::option::Option< + unsafe extern "C" fn( + hwnd: HWND, + srcdata: *mut ::std::os::raw::c_void, + srcheader: MAGIMAGEHEADER, + destdata: *mut ::std::os::raw::c_void, + destheader: MAGIMAGEHEADER, + unclipped: RECT, + clipped: RECT, + dirty: HRGN, + ) -> BOOL, +>; + +extern "C" { + pub fn MagShowSystemCursor(fShowCursor: BOOL) -> BOOL; +} +pub type MagInitializeFunc = ::std::option::Option BOOL>; +pub type MagUninitializeFunc = ::std::option::Option BOOL>; +pub type MagSetWindowSourceFunc = + ::std::option::Option BOOL>; +pub type MagSetWindowFilterListFunc = ::std::option::Option< + unsafe extern "C" fn( + hwnd: HWND, + dwFilterMode: DWORD, + count: ::std::os::raw::c_int, + pHWND: *mut HWND, + ) -> BOOL, +>; +pub type MagSetImageScalingCallbackFunc = ::std::option::Option< + unsafe extern "C" fn(hwnd: HWND, callback: MagImageScalingCallback) -> BOOL, +>; + +#[repr(C)] +#[derive(Debug, Clone)] +struct MagInterface { + init_succeeded: bool, + lib_handle: HINSTANCE, + pub mag_initialize_func: MagInitializeFunc, + pub mag_uninitialize_func: MagUninitializeFunc, + pub set_window_source_func: MagSetWindowSourceFunc, + pub set_window_filter_list_func: MagSetWindowFilterListFunc, + pub set_image_scaling_callback_func: MagSetImageScalingCallbackFunc, +} + +// NOTE: MagInitialize and MagUninitialize should not be called in global init and uninit. +// If so, strange errors occur. +impl MagInterface { + fn new() -> Result { + let mut s = MagInterface { + init_succeeded: false, + lib_handle: NULL as _, + mag_initialize_func: None, + mag_uninitialize_func: None, + set_window_source_func: None, + set_window_filter_list_func: None, + set_image_scaling_callback_func: None, + }; + s.init_succeeded = false; + unsafe { + if GetSystemMetrics(SM_CMONITORS) != 1 { + // Do not try to use the magnifier in multi-screen setup (where the API + // crashes sometimes). + return Err(Error::new( + ErrorKind::Other, + "Magnifier capturer cannot work on multi-screen system.", + )); + } + + // load lib + let lib_file_name = "Magnification.dll"; + let lib_file_name_c = CString::new(lib_file_name).unwrap(); + s.lib_handle = LoadLibraryExA( + lib_file_name_c.as_ptr() as _, + NULL, + LOAD_WITH_ALTERED_SEARCH_PATH, + ); + if s.lib_handle.is_null() { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to LoadLibraryExA {}, error: {}", + lib_file_name, + GetLastError() + ), + )); + }; + + // load functions + s.mag_initialize_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagInitialize", + )?)); + s.mag_uninitialize_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagUninitialize", + )?)); + s.set_window_source_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagSetWindowSource", + )?)); + s.set_window_filter_list_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagSetWindowFilterList", + )?)); + s.set_image_scaling_callback_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagSetImageScalingCallback", + )?)); + + // MagInitialize + if let Some(init_func) = s.mag_initialize_func { + if FALSE == init_func() { + return Err(Error::new( + ErrorKind::Other, + format!("Failed to MagInitialize, error: {}", GetLastError()), + )); + } else { + s.init_succeeded = true; + } + } else { + return Err(Error::new( + ErrorKind::Other, + "Unreachable, mag_initialize_func should not be none", + )); + } + } + Ok(s) + } + + unsafe fn load_func(lib_module: HMODULE, func_name: &str) -> Result { + let func_name_c = CString::new(func_name).unwrap(); + let func = GetProcAddress(lib_module, func_name_c.as_ptr() as _); + if func.is_null() { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to GetProcAddress {}, error: {}", + func_name, + GetLastError() + ), + )); + } + Ok(func) + } + + pub(super) fn uninit(&mut self) { + if self.init_succeeded { + if let Some(uninit_func) = self.mag_uninitialize_func { + unsafe { + if FALSE == uninit_func() { + println!("Failed MagUninitialize {}", GetLastError()) + } + } + } + if !self.lib_handle.is_null() { + unsafe { + if FALSE == FreeLibrary(self.lib_handle) { + println!("Failed FreeLibrary {}", GetLastError()) + } + } + self.lib_handle = NULL as _; + } + } + self.init_succeeded = false; + } +} + +impl Drop for MagInterface { + fn drop(&mut self) { + self.uninit(); + } +} + +pub struct CapturerMag { + mag_interface: MagInterface, + host_window: HWND, + magnifier_window: HWND, + + magnifier_host_class: CString, + host_window_name: CString, + magnifier_window_class: CString, + magnifier_window_name: CString, + + rect: RECT, + width: usize, + height: usize, + + use_yuv: bool, + data: Vec, +} + +impl Drop for CapturerMag { + fn drop(&mut self) { + self.destroy_windows(); + self.mag_interface.uninit(); + } +} + +impl CapturerMag { + pub(crate) fn is_supported() -> bool { + MagInterface::new().is_ok() + } + + pub(crate) fn new( + origin: (i32, i32), + width: usize, + height: usize, + use_yuv: bool, + ) -> Result { + let mut s = Self { + mag_interface: MagInterface::new()?, + host_window: 0 as _, + magnifier_window: 0 as _, + magnifier_host_class: CString::new("ScreenCapturerWinMagnifierHost")?, + host_window_name: CString::new("MagnifierHost")?, + magnifier_window_class: CString::new("Magnifier")?, + magnifier_window_name: CString::new("MagnifierWindow")?, + rect: RECT { + left: origin.0 as _, + top: origin.1 as _, + right: origin.0 + width as LONG, + bottom: origin.1 + height as LONG, + }, + width, + height, + use_yuv, + data: Vec::new(), + }; + + unsafe { + let mut instance = 0 as HMODULE; + if 0 == GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + DefWindowProcA as _, + &mut instance as _, + ) { + return Err(Error::new( + ErrorKind::Other, + format!("Failed to GetModuleHandleExA, error: {}", GetLastError()), + )); + } + + // Register the host window class. See the MSDN documentation of the + // Magnification API for more infomation. + let wcex = WNDCLASSEXA { + cbSize: size_of::() as _, + style: 0, + lpfnWndProc: Some(DefWindowProcA), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: instance, + hIcon: 0 as _, + hCursor: LoadCursorA(NULL as _, IDC_ARROW as _), + hbrBackground: 0 as _, + lpszClassName: s.magnifier_host_class.as_ptr() as _, + lpszMenuName: 0 as _, + hIconSm: 0 as _, + }; + + // Ignore the error which may happen when the class is already registered. + if 0 == RegisterClassExA(&wcex) { + let code = GetLastError(); + if code != ERROR_CLASS_ALREADY_EXISTS { + return Err(Error::new( + ErrorKind::Other, + format!("Failed to RegisterClassExA, error: {}", code), + )); + } + } + + // Create the host window. + s.host_window = CreateWindowExA( + WS_EX_LAYERED, + s.magnifier_host_class.as_ptr(), + s.host_window_name.as_ptr(), + WS_POPUP, + 0, + 0, + 0, + 0, + NULL as _, + NULL as _, + instance, + NULL, + ); + if s.host_window.is_null() { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to CreateWindowExA host_window, error: {}", + GetLastError() + ), + )); + } + + // Create the magnifier control. + s.magnifier_window = CreateWindowExA( + 0, + s.magnifier_window_class.as_ptr(), + s.magnifier_window_name.as_ptr(), + WS_CHILD | WS_VISIBLE, + 0, + 0, + 0, + 0, + s.host_window, + NULL as _, + instance, + NULL, + ); + if s.magnifier_window.is_null() { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed CreateWindowA magnifier_window, error: {}", + GetLastError() + ), + )); + } + + // Hide the host window. + let _ = ShowWindow(s.host_window, SW_HIDE); + + // Set the scaling callback to receive captured image. + if let Some(set_callback_func) = s.mag_interface.set_image_scaling_callback_func { + if FALSE + == set_callback_func( + s.magnifier_window, + Some(Self::on_gag_image_scaling_callback), + ) + { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to MagSetImageScalingCallback, error: {}", + GetLastError() + ), + )); + } + } else { + return Err(Error::new( + ErrorKind::Other, + "Unreachable, set_image_scaling_callback_func should not be none", + )); + } + } + + Ok(s) + } + + pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result { + let name_c = CString::new(name).unwrap(); + unsafe { + let mut hwnd = if cls.len() == 0 { + FindWindowExA(NULL as _, NULL as _, NULL as _, name_c.as_ptr()) + } else { + let cls_c = CString::new(cls).unwrap(); + FindWindowExA(NULL as _, NULL as _, cls_c.as_ptr(), name_c.as_ptr()) + }; + + if hwnd.is_null() { + return Ok(false); + } + + if let Some(set_window_filter_list_func) = + self.mag_interface.set_window_filter_list_func + { + if FALSE + == set_window_filter_list_func( + self.magnifier_window, + MW_FILTERMODE_EXCLUDE, + 1, + &mut hwnd, + ) + { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed MagSetWindowFilterList for cls {} name {}, err: {}", + cls, + name, + GetLastError() + ), + )); + } + } else { + return Err(Error::new( + ErrorKind::Other, + "Unreachable, MagSetWindowFilterList should not be none", + )); + } + } + + Ok(true) + } + + pub(crate) fn get_rect(&self) -> ((i32, i32), usize, usize) { + ( + (self.rect.left as _, self.rect.top as _), + self.width as _, + self.height as _, + ) + } + + fn clear_data() { + let mut lock = MAG_BUFFER.lock().unwrap(); + lock.0 = false; + lock.1.clear(); + } + + pub(crate) fn frame(&mut self, data: &mut Vec) -> Result<()> { + Self::clear_data(); + + unsafe { + let x = GetSystemMetrics(SM_XVIRTUALSCREEN); + let y = GetSystemMetrics(SM_YVIRTUALSCREEN); + let w = GetSystemMetrics(SM_CXVIRTUALSCREEN); + let h = GetSystemMetrics(SM_CYVIRTUALSCREEN); + + self.rect = RECT { + left: x as _, + top: y as _, + right: (x + w) as _, + bottom: (y + h) as _, + }; + + if FALSE + == SetWindowPos( + self.magnifier_window, + HWND_TOP, + self.rect.left, + self.rect.top, + self.rect.right, + self.rect.bottom, + 0, + ) + { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed SetWindowPos (x, y, w , h) - ({}, {}, {}, {}), error {}", + self.rect.left, + self.rect.top, + self.rect.right, + self.rect.bottom, + GetLastError() + ), + )); + } + + // on_gag_image_scaling_callback will be called and fill in the + // frame before set_window_source_func_ returns. + if let Some(set_window_source_func) = self.mag_interface.set_window_source_func { + if FALSE == set_window_source_func(self.magnifier_window, self.rect) { + return Err(Error::new( + ErrorKind::Other, + format!("Failed to MagSetWindowSource, error: {}", GetLastError()), + )); + } + } else { + return Err(Error::new( + ErrorKind::Other, + "Unreachable, set_window_source_func should not be none", + )); + } + } + + let mut lock = MAG_BUFFER.lock().unwrap(); + if !lock.0 { + return Err(Error::new( + ErrorKind::Other, + "No data captured by magnifier", + )); + } + + if self.use_yuv { + self.data.resize(lock.1.len(), 0); + unsafe { + std::ptr::copy_nonoverlapping(&mut lock.1[0], &mut self.data[0], self.data.len()); + } + crate::common::bgra_to_i420( + self.width as usize, + self.height as usize, + &self.data, + data, + ); + } else { + data.resize(lock.1.len(), 0); + unsafe { + std::ptr::copy_nonoverlapping(&mut lock.1[0], &mut data[0], data.len()); + } + } + + Ok(()) + } + + fn destroy_windows(&mut self) { + if !self.magnifier_window.is_null() { + unsafe { + if FALSE == DestroyWindow(self.magnifier_window) { + // + println!("Failed DestroyWindow magnifier window {}", GetLastError()) + } + } + } + self.magnifier_window = NULL as _; + + if !self.host_window.is_null() { + unsafe { + if FALSE == DestroyWindow(self.host_window) { + // + println!("Failed DestroyWindow host window {}", GetLastError()) + } + } + } + self.host_window = NULL as _; + } + + unsafe extern "C" fn on_gag_image_scaling_callback( + _hwnd: HWND, + srcdata: *mut ::std::os::raw::c_void, + srcheader: MAGIMAGEHEADER, + _destdata: *mut ::std::os::raw::c_void, + _destheader: MAGIMAGEHEADER, + _unclipped: RECT, + _clipped: RECT, + _dirty: HRGN, + ) -> BOOL { + Self::clear_data(); + + if !IsEqualGUID(&srcheader.format, &GUID_WICPixelFormat32bppRGBA) { + // log warning? + return FALSE; + } + let mut lock = MAG_BUFFER.lock().unwrap(); + lock.1.resize(srcheader.cbSize, 0); + std::ptr::copy_nonoverlapping(srcdata as _, &mut lock.1[0], srcheader.cbSize); + lock.0 = true; + TRUE + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test() { + let mut capture_mag = CapturerMag::new((0, 0), 1920, 1080, false).unwrap(); + capture_mag.exclude("", "RustDeskPrivacyWindow").unwrap(); + std::thread::sleep(std::time::Duration::from_millis(1000 * 10)); + let mut data = Vec::new(); + capture_mag.frame(&mut data).unwrap(); + println!("capture data len: {}", data.len()); + } +} \ No newline at end of file diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 0d343962d..46692535d 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -1,6 +1,7 @@ use std::{io, mem, ptr, slice}; pub mod gdi; pub use gdi::CapturerGDI; +pub mod mag; use winapi::{ shared::{ diff --git a/src/client.rs b/src/client.rs index be2b788ab..fed83cece 100644 --- a/src/client.rs +++ b/src/client.rs @@ -773,6 +773,7 @@ pub struct LoginConfigHandler { pub port_forward: (String, i32), pub version: i64, pub conn_id: i32, + features: Option, } impl Deref for LoginConfigHandler { @@ -866,11 +867,11 @@ impl LoginConfigHandler { }) .into(); } else if name == "privacy-mode" { - config.privacy_mode = !config.privacy_mode; + // try toggle privacy mode option.privacy_mode = (if config.privacy_mode { - BoolOption::Yes - } else { BoolOption::No + } else { + BoolOption::Yes }) .into(); } else if name == "enable-file-transfer" { @@ -992,6 +993,14 @@ impl LoginConfigHandler { } } + pub fn is_privacy_mode_supported(&self) -> bool { + if let Some(features) = &self.features { + features.privacy_mode + } else { + false + } + } + pub fn refresh() -> Message { let mut misc = Misc::new(); misc.set_refresh_video(true); @@ -1064,6 +1073,7 @@ impl LoginConfigHandler { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); } + self.features = pi.features.into_option(); let serde = PeerInfoSerde { username, hostname: pi.hostname.clone(), diff --git a/src/common.rs b/src/common.rs index 2a865afbb..dac98fb76 100644 --- a/src/common.rs +++ b/src/common.rs @@ -594,3 +594,14 @@ pub async fn post_request(url: String, body: String, header: &str) -> ResultType pub async fn post_request_sync(url: String, body: String, header: &str) -> ResultType { post_request(url, body, header).await } + +#[inline] +pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Message { + let mut misc = Misc::new(); + let mut back_notification = BackNotification::new(); + back_notification.set_privacy_mode_state(state); + misc.set_back_notification(back_notification); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out +} diff --git a/src/ipc.rs b/src/ipc.rs index 2388a7d9c..5eabbab66 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -20,6 +20,16 @@ use std::{collections::HashMap, sync::atomic::Ordering}; #[cfg(not(windows))] use std::{fs::File, io::prelude::*}; +// State with timestamp, because std::time::Instant cannot be serialized +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +#[serde(tag = "t", content = "c")] +pub enum PrivacyModeState { + OffSucceeded, + OffFailed, + OffByPeer, + OffUnknown, +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum FS { @@ -116,6 +126,7 @@ pub enum Data { #[cfg(not(any(target_os = "android", target_os = "ios")))] ClipbaordFile(ClipbaordFile), ClipboardFileEnabled(bool), + PrivacyModeState((i32, PrivacyModeState)), } #[tokio::main(flavor = "current_thread")] diff --git a/src/lang/cn.rs b/src/lang/cn.rs index b2a3dc9a4..5c83fffc8 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -265,10 +265,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓10或更高。"), ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), ("Account", "账号"), - ("Quit", "退出"), ("Overwrite", "覆盖"), ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), + ("Quit", "退出"), ("doc_mac_permission", "https://rustdesk.com/docs/zh-cn/manual/mac/#启用权限"), ("Help", "帮助"), + ("Failed", "失败"), + ("Succeeded", "成功"), + ("Someone turns on privacy mode, exit", "其他用户使用隐私模式,退出"), + ("Unsupported", "不支持"), + ("Peer denied", "被控端拒绝"), + ("Please install plugins", "请安装插件"), + ("Peer exit", "被控端退出"), + ("Failed to turn off", "退出失败"), + ("Turned off", "退出"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ab8cbebfa..b369e2dd4 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -270,5 +270,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Quit", "Ukončit"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Nápověda"), + ("Failed", "Nepodařilo se"), + ("Succeeded", "Uspěl"), + ("Someone turns on privacy mode, exit", "Někdo zapne režim soukromí, ukončete ho"), + ("Unsupported", "Nepodporováno"), + ("Peer denied", "Peer popřel"), + ("Please install plugins", "Nainstalujte si prosím pluginy"), + ("Peer exit", "Peer exit"), + ("Failed to turn off", "Nepodařilo se vypnout"), + ("Turned off", "Vypnutý"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index d05ad481b..752cb0df1 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -265,8 +265,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "Die aktuelle Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher."), ("android_start_service_tip", "Tippen Sie auf [Dienst starten] oder ÖFFNEN Sie die Berechtigung [Bildschirmaufnahme], um den Bildschirmfreigabedienst zu starten."), ("Account", "Konto"), - ("Quit", "Ausgang"), + ("Overwrite", "Überschreiben"), + ("This file exists, skip or overwrite this file?", "Diese Datei existiert, diese Datei überspringen oder überschreiben?"), + ("Quit", "Aufhören"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Hilfe"), + ("Failed", "Gescheitert"), + ("Succeeded", "Erfolgreich"), + ("Someone turns on privacy mode, exit", "Jemand aktiviert den Datenschutzmodus, beenden"), + ("Unsupported", "Nicht unterstützt"), + ("Peer denied", "Peer verweigert"), + ("Please install plugins", "Bitte installieren Sie Plugins"), + ("Peer exit", "Peer-Ausgang"), + ("Failed to turn off", "Ausschalten fehlgeschlagen"), + ("Turned off", "Ausgeschaltet"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0edd74e25..2f12d63b9 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -265,8 +265,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", ""), ("android_start_service_tip", ""), ("Account", ""), + ("Overwrite", ""), + ("This file exists, skip or overwrite this file?", ""), ("Quit", ""), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", ""), + ("Failed", ""), + ("Succeeded", ""), + ("Someone turns on privacy mode, exit", ""), + ("Unsupported", ""), + ("Peer denied", ""), + ("Please install plugins", ""), + ("Peer exit", ""), + ("Failed to turn off", ""), + ("Turned off", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 8f6c3f83b..8f1ca3356 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -265,8 +265,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "La version actuelle d'Android ne prend pas en charge la capture audio, veuillez passer à Android 10 ou supérieur."), ("android_start_service_tip", "Appuyez sur [Démarrer le service] ou sur l'autorisation OUVRIR [Capture d'écran] pour démarrer le service de partage d'écran."), ("Account", "Compte"), + ("Overwrite", "Écraser"), + ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), ("Quit", "Quitter"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Aider"), + ("Failed", "échouer"), + ("Succeeded", "Succès"), + ("Someone turns on privacy mode, exit", "Quelqu'un active le mode de confidentialité, quittez"), + ("Unsupported", "Non pris en charge"), + ("Peer denied", "Pair refusé"), + ("Please install plugins", "Veuillez installer les plugins"), + ("Peer exit", "Sortie des pairs"), + ("Failed to turn off", "Échec de la désactivation"), + ("Turned off", "Éteindre"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 5b8475995..78c18250e 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -265,8 +265,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "Versi Android saat ini tidak mendukung pengambilan audio, harap tingkatkan ke Android 10 atau lebih tinggi."), ("android_start_service_tip", "Ketuk izin [Mulai Layanan] atau BUKA [Tangkapan Layar] untuk memulai layanan berbagi layar."), ("Account", "Akun"), + ("Overwrite", "Timpa"), + ("This file exists, skip or overwrite this file?", ""), ("Quit", "Keluar"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Bantuan"), + ("Failed", "Gagal"), + ("Succeeded", "Berhasil"), + ("Someone turns on privacy mode, exit", "Seseorang mengaktifkan mode privasi, keluar"), + ("Unsupported", "Tidak didukung"), + ("Peer denied", "Rekan ditolak"), + ("Please install plugins", "Silakan instal plugin"), + ("Peer exit", "keluar rekan"), + ("Failed to turn off", "Gagal mematikan"), + ("Turned off", "Matikan"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index c85542413..853f44de0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -265,8 +265,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "L'attuale versione di Android non supporta l'acquisizione audio, esegui l'upgrade ad Android 10 o versioni successive."), ("android_start_service_tip", "Toccare [Avvia servizio] o APRI l'autorizzazione [Cattura schermo] per avviare il servizio di condivisione dello schermo."), ("Account", "Account"), + ("Overwrite", "Sovrascrivi"), + ("This file exists, skip or overwrite this file?", "Questo file esiste, saltare o sovrascrivere questo file?"), ("Quit", "Esci"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Aiuto"), + ("Failed", "Fallito"), + ("Succeeded", "Successo"), + ("Someone turns on privacy mode, exit", "Qualcuno attiva la modalità privacy, esci"), + ("Unsupported", "Non supportato"), + ("Peer denied", "Pari negato"), + ("Please install plugins", "Si prega di installare i plugin"), + ("Peer exit", "Uscita tra pari"), + ("Failed to turn off", "Impossibile spegnere"), + ("Turned off", "Spegni"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index d4a6824bf..2370cf35d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -265,8 +265,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor atualize para o Android 10 ou maior."), ("android_start_service_tip", "Toque [Iniciar Serviço] ou abra a permissão [Captura de Tela] para iniciar o serviço de compartilhamento de tela."), ("Account", "Conta"), + ("Overwrite", "Substituir"), + ("This file exists, skip or overwrite this file?", "Este arquivo existe, pular ou substituir este arquivo?"), ("Quit", "Saída"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Ajuda"), + ("Failed", "Falhou"), + ("Succeeded", "Conseguiu"), + ("Someone turns on privacy mode, exit", "Alguém liga o modo de privacidade, saia"), + ("Unsupported", "Sem suporte"), + ("Peer denied", "Par negado"), + ("Please install plugins", "Por favor instale plugins"), + ("Peer exit", "Saída de pares"), + ("Failed to turn off", "Falha ao desligar"), + ("Turned off", "Desligado"), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8cc699895..31317f9c3 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -270,5 +270,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This file exists, skip or overwrite this file?", "Этот файл существует, пропустить или перезаписать этот файл?"), ("doc_mac_permission", "https://rustdesk.com/docs/ru/manual/mac/#включение-разрешений"), ("Help", "Помощь"), + ("Failed", "Неуспешный"), + ("Succeeded", "Успешно"), + ("Someone turns on privacy mode, exit", "Кто-то включает режим конфиденциальности, выйдите"), + ("Unsupported", "Не поддерживается"), + ("Peer denied", "Отказано в пире"), + ("Please install plugins", "Пожалуйста, установите плагины"), + ("Peer exit", "Одноранговый выход"), + ("Failed to turn off", "Не удалось отключить"), + ("Turned off", "Выключен"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7a3af5bcd..81885f176 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -270,5 +270,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Quit", "Ukončiť"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Nápoveda"), + ("Failed", "Nepodarilo sa"), + ("Succeeded", "Podarilo sa"), + ("Someone turns on privacy mode, exit", "Niekto zapne režim súkromia, ukončite ho"), + ("Unsupported", "Nepodporované"), + ("Peer denied", "Peer poprel"), + ("Please install plugins", "Nainštalujte si prosím pluginy"), + ("Peer exit", "Peer exit"), + ("Failed to turn off", "Nepodarilo sa vypnúť"), + ("Turned off", "Vypnutý"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index cc56af085..c9551a0bf 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -270,5 +270,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Quit", ""), ("doc_mac_permission", ""), ("Help", ""), + ("Failed", ""), + ("Succeeded", ""), + ("Someone turns on privacy mode, exit", ""), + ("Unsupported", ""), + ("Peer denied", ""), + ("Please install plugins", ""), + ("Peer exit", ""), + ("Failed to turn off", ""), + ("Turned off", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index d29fe9d50..173438b75 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -265,8 +265,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "Mevcut Android sürümü ses yakalamayı desteklemiyor, lütfen Android 10 veya sonraki bir sürüme yükseltin."), ("android_start_service_tip", "Ekran paylaşım hizmetini başlatmak için [Hizmeti Başlat] veya AÇ [Ekran Yakalama] iznine dokunun."), ("Account", "Hesap"), + ("Overwrite", "üzerine yaz"), + ("This file exists, skip or overwrite this file?", "Bu dosya var, bu dosya atlansın veya üzerine yazılsın mı?"), ("Quit", "Çıkış"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Yardım"), + ("Failed", "Arızalı"), + ("Succeeded", "başarılı"), + ("Someone turns on privacy mode, exit", "Birisi gizlilik modunu açar, çık"), + ("Unsupported", "desteklenmiyor"), + ("Peer denied", "akran reddedildi"), + ("Please install plugins", "Lütfen eklentileri yükleyin"), + ("Peer exit", "akran çıkışı"), + ("Failed to turn off", "kapatılamadı"), + ("Turned off", "Kapalı"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 3316e89c7..26ceeb014 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -270,5 +270,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要跳過或是覆寫此檔案嗎?"), ("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"), ("Help", "幫助"), + ("Account", "帳戶"), + ("Failed", "失敗"), + ("Succeeded", "成功"), + ("Someone turns on privacy mode, exit", "其他用戶開啟隱私模式,退出"), + ("Unsupported", "不支持"), + ("Peer denied", "被控端拒絕"), + ("Please install plugins", "請安裝插件"), + ("Peer exit", "被控端退出"), + ("Failed to turn off", "退出失敗"), + ("Turned off", "退出"), ].iter().cloned().collect(); } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index bcb9a8545..6f45ba8ff 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -9,7 +9,8 @@ use hbb_common::{ use std::io::prelude::*; use std::{ ffi::OsString, - io, mem, + fs, io, mem, + path::Path, sync::{Arc, Mutex}, time::{Duration, Instant}, }; @@ -404,6 +405,7 @@ extern "C" { fn has_rdp_service() -> BOOL; fn get_current_session(rdp: BOOL) -> DWORD; fn LaunchProcessWin(cmd: *const u16, session_id: DWORD, as_user: BOOL) -> HANDLE; + fn GetSessionUserTokenWin(lphUserToken: LPHANDLE, dwSessionId: DWORD, as_user: BOOL) -> BOOL; fn selectInputDesktop() -> BOOL; fn inputDesktopSelected() -> BOOL; fn is_windows_server() -> BOOL; @@ -558,7 +560,7 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType String { format!("{}\\{}", pf, crate::get_app_name()) } +pub fn check_update_broker_process() -> ResultType<()> { + // let (_, path, _, _) = get_install_info(); + let process_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE; + let origin_process_exe = crate::ui::win_privacy::ORIGIN_PROCESS_EXE; + + let exe_file = std::env::current_exe()?; + if exe_file.parent().is_none() { + bail!("Cannot get parent of current exe file"); + } + let cur_dir = exe_file.parent().unwrap(); + let cur_exe = cur_dir.join(process_exe); + + let ori_modified = fs::metadata(origin_process_exe)?.modified()?; + if let Ok(metadata) = fs::metadata(&cur_exe) { + if let Ok(cur_modified) = metadata.modified() { + if cur_modified == ori_modified { + return Ok(()); + } else { + log::info!( + "broker process updated, modify time from {:?} to {:?}", + cur_modified, + ori_modified + ); + } + } + } + + // Force update broker exe if failed to check modified time. + let cmds = format!( + " + chcp 65001 + taskkill /F /IM {broker_exe} + copy /Y \"{origin_process_exe}\" \"{cur_exe}\" + ", + broker_exe = process_exe, + origin_process_exe = origin_process_exe, + cur_exe = cur_exe.to_string_lossy().to_string(), + ); + run_cmds(cmds, false)?; + + Ok(()) +} + fn get_install_info_with_subkey(subkey: String) -> (String, String, String, String) { let mut path = get_reg_of(&subkey, "InstallLocation"); if path.is_empty() { @@ -817,6 +862,7 @@ pub fn update_me() -> ResultType<()> { " chcp 65001 sc stop {app_name} + taskkill /F /IM {broker_exe} taskkill /F /IM {app_name}.exe copy /Y \"{src_exe}\" \"{exe}\" sc start {app_name} @@ -824,6 +870,7 @@ pub fn update_me() -> ResultType<()> { ", src_exe = src_exe, exe = exe, + broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, app_name = crate::get_app_name(), lic = register_licence(), ); @@ -975,6 +1022,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" chcp 65001 md \"{path}\" copy /Y \"{src_exe}\" \"{exe}\" +copy /Y \"{ORIGIN_PROCESS_EXE}\" \"{path}\\{broker_exe}\" reg add {subkey} /f reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\" reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\" @@ -1010,6 +1058,8 @@ sc delete {app_name} path=path, src_exe=std::env::current_exe()?.to_str().unwrap_or(""), exe=exe, + ORIGIN_PROCESS_EXE = crate::ui::win_privacy::ORIGIN_PROCESS_EXE, + broker_exe=crate::ui::win_privacy::INJECTED_PROCESS_EXE, subkey=subkey, app_name=crate::get_app_name(), version=crate::VERSION, @@ -1051,11 +1101,13 @@ fn get_before_uninstall() -> String { chcp 65001 sc stop {app_name} sc delete {app_name} + taskkill /F /IM {broker_exe} taskkill /F /IM {app_name}.exe reg delete HKEY_CLASSES_ROOT\\.{ext} /f netsh advfirewall firewall delete rule name=\"{app_name} Service\" ", app_name = app_name, + broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, ext = ext ) } @@ -1325,3 +1377,20 @@ pub fn quit_gui() { std::process::exit(0); // unsafe { PostQuitMessage(0) }; // some how not work } + +pub fn get_user_token(session_id: u32, as_user: bool) -> HANDLE { + let mut token = NULL as HANDLE; + unsafe { + if FALSE + == GetSessionUserTokenWin( + &mut token as _, + session_id, + if as_user { TRUE } else { FALSE }, + ) + { + NULL as _ + } else { + token + } + } +} diff --git a/src/server.rs b/src/server.rs index f4758e3fb..b41fbddf3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -91,6 +91,15 @@ async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> Ok(()) } +async fn check_privacy_mode_on(stream: &mut Stream) -> ResultType<()> { + if video_service::get_privacy_mode_conn_id() > 0 { + let msg_out = + crate::common::make_privacy_mode_msg(back_notification::PrivacyModeState::OnByOther); + timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??; + } + Ok(()) +} + pub async fn create_tcp_connection( server: ServerPtr, stream: Stream, @@ -98,6 +107,8 @@ pub async fn create_tcp_connection( secure: bool, ) -> ResultType<()> { let mut stream = stream; + check_privacy_mode_on(&mut stream).await?; + let id = { let mut w = server.write().unwrap(); w.id_count += 1; diff --git a/src/server/connection.rs b/src/server/connection.rs index 3a026d924..c2a10fa6b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -50,8 +50,6 @@ enum MessageInput { Key((KeyEvent, bool)), BlockOn, BlockOff, - PrivacyOn, - PrivacyOff, } pub struct Connection { @@ -74,7 +72,6 @@ pub struct Connection { image_quality: i32, lock_after_session_end: bool, show_remote_cursor: bool, // by peer - privacy_mode: bool, ip: String, disable_clipboard: bool, // by peer disable_audio: bool, // by peer @@ -160,7 +157,6 @@ impl Connection { image_quality: ImageQuality::Balanced.value(), lock_after_session_end: false, show_remote_cursor: false, - privacy_mode: false, ip: "".to_owned(), disable_audio: false, enable_file_transfer: false, @@ -281,6 +277,34 @@ impl Connection { allow_err!(conn.stream.send(&clip_2_msg(_clip)).await); } } + ipc::Data::PrivacyModeState((_, state)) => { + let msg_out = match state { + ipc::PrivacyModeState::OffSucceeded => { + video_service::set_privacy_mode_conn_id(0); + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OffSucceeded, + ) + } + ipc::PrivacyModeState::OffFailed => { + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OffFailed, + ) + } + ipc::PrivacyModeState::OffByPeer => { + video_service::set_privacy_mode_conn_id(0); + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OffByPeer, + ) + } + ipc::PrivacyModeState::OffUnknown => { + video_service::set_privacy_mode_conn_id(0); + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OffUnknown, + ) + } + }; + conn.send(msg_out).await; + } _ => {} } }, @@ -362,9 +386,13 @@ impl Connection { } } + if video_service::get_privacy_mode_conn_id() == id { + video_service::set_privacy_mode_conn_id(0); + let _ = privacy_mode::turn_off_privacy(id).await; + } video_service::notify_video_frame_feched(id, None); - super::video_service::update_test_latency(id, 0); - super::video_service::update_image_quality(id, None); + video_service::update_test_latency(id, 0); + video_service::update_image_quality(id, None); if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await { conn.on_close(&err.to_string(), false); } @@ -378,9 +406,6 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] fn handle_input(receiver: std_mpsc::Receiver, tx: Sender) { let mut block_input_mode = false; - let (tx_blank, rx_blank) = std_mpsc::channel(); - - std::thread::spawn(|| Self::handle_blank(rx_blank)); loop { match receiver.recv_timeout(std::time::Duration::from_millis(500)) { @@ -402,28 +427,22 @@ impl Connection { if crate::platform::block_input(true) { block_input_mode = true; } else { - Self::send_option_error(&tx, "Failed to turn on block input mode"); + Self::send_block_input_error( + &tx, + back_notification::BlockInputState::OnFailed, + ); } } MessageInput::BlockOff => { if crate::platform::block_input(false) { block_input_mode = false; } else { - Self::send_option_error(&tx, "Failed to turn off block input mode"); + Self::send_block_input_error( + &tx, + back_notification::BlockInputState::OffFailed, + ); } } - MessageInput::PrivacyOn => { - if crate::platform::block_input(true) { - block_input_mode = true; - } - tx_blank.send(MessageInput::PrivacyOn).ok(); - } - MessageInput::PrivacyOff => { - if crate::platform::block_input(false) { - block_input_mode = false; - } - tx_blank.send(MessageInput::PrivacyOff).ok(); - } }, Err(err) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -439,35 +458,6 @@ impl Connection { log::info!("Input thread exited"); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn handle_blank(receiver: std_mpsc::Receiver) { - let mut last_privacy = false; - loop { - match receiver.recv_timeout(std::time::Duration::from_millis(500)) { - Ok(v) => match v { - MessageInput::PrivacyOn => { - crate::platform::toggle_blank_screen(true); - last_privacy = true; - } - MessageInput::PrivacyOff => { - crate::platform::toggle_blank_screen(false); - last_privacy = false; - } - _ => break, - }, - Err(err) => { - if last_privacy { - crate::platform::toggle_blank_screen(true); - } - if std_mpsc::RecvTimeoutError::Disconnected == err { - break; - } - } - } - } - log::info!("Blank thread exited"); - } - async fn try_port_forward_loop( &mut self, rx_from_cm: &mut mpsc::UnboundedReceiver, @@ -657,8 +647,19 @@ impl Connection { } } self.authorized = true; - pi.username = username; - pi.sas_enabled = sas_enabled; + + let mut pi = PeerInfo { + hostname: whoami::hostname(), + username, + platform: whoami::platform().to_string(), + version: crate::VERSION.to_owned(), + sas_enabled, + features: Some(Features { + privacy_mode: video_service::is_privacy_mode_supported(), + ..Default::default() + }).into(), + ..Default::default() + }; let mut sub_service = false; if self.file_transfer.is_some() { res.set_peer_info(pi); @@ -755,13 +756,13 @@ impl Connection { self.send(msg_out).await; } - fn send_option_error(s: &Sender, err: T) { - let mut msg_out = Message::new(); - let mut res = OptionResponse::new(); + #[inline] + pub fn send_block_input_error(s: &Sender, state: back_notification::BlockInputState) { let mut misc = Misc::new(); - res.error = err.to_string(); - - misc.set_option_response(res); + let mut back_notification = BackNotification::new(); + back_notification.set_block_input_state(state); + misc.set_back_notification(back_notification); + let mut msg_out = Message::new(); msg_out.set_misc(misc); s.send((Instant::now(), Arc::new(msg_out))).ok(); } @@ -1162,12 +1163,44 @@ impl Connection { if self.keyboard { match q { BoolOption::Yes => { - self.privacy_mode = true; - self.tx_input.send(MessageInput::PrivacyOn).ok(); + let msg_out = if !video_service::is_privacy_mode_supported() { + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::NotSupported, + ) + } else { + video_service::set_privacy_mode_conn_id(0); + match privacy_mode::turn_on_privacy(self.inner.id) { + Ok(true) => { + video_service::set_privacy_mode_conn_id(self.inner.id); + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OnSucceeded, + ) + } + Ok(false) => { + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OnFailedPlugin, + ) + } + Err(e) => { + log::error!("Failed to turn on privacy mode. {}", e); + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OnFailed, + ) + } + } + }; + self.send(msg_out).await; } BoolOption::No => { - self.privacy_mode = false; - self.tx_input.send(MessageInput::PrivacyOff).ok(); + let msg_out = if !video_service::is_privacy_mode_supported() { + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::NotSupported, + ) + } else { + video_service::set_privacy_mode_conn_id(0); + privacy_mode::turn_off_privacy(self.inner.id).await + }; + self.send(msg_out).await; } _ => {} } @@ -1318,3 +1351,43 @@ fn try_activate_screen() { mouse_move_relative(6, 6); }); } + +mod privacy_mode { + use super::*; + + pub(super) async fn turn_off_privacy(_conn_id: i32) -> Message { + #[cfg(windows)] + { + use crate::ui::win_privacy::*; + + let res = turn_off_privacy(_conn_id, None); + match res { + Ok(_) => crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OffSucceeded, + ), + Err(e) => { + log::error!("Failed to turn off privacy mode{}", e); + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OffFailed, + ) + } + } + } + #[cfg(not(windows))] + { + crate::common::make_privacy_mode_msg(back_notification::PrivacyModeState::OffFailed) + } + } + + pub(super) fn turn_on_privacy(_conn_id: i32) -> ResultType { + #[cfg(windows)] + { + let plugin_exitst = crate::ui::win_privacy::turn_on_privacy(_conn_id)?; + Ok(plugin_exitst) + } + #[cfg(not(windows))] + { + Ok(true) + } + } +} diff --git a/src/server/service.rs b/src/server/service.rs index 576c93262..9cc1e860c 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -148,6 +148,16 @@ impl> ServiceTmpl { } } + pub fn send_to_others(&self, msg: Message, id: i32) { + let msg = Arc::new(msg); + let mut lock = self.0.write().unwrap(); + for (sid, s) in lock.subscribes.iter_mut() { + if *sid != id { + s.send(msg.clone()); + } + } + } + pub fn send_shared(&self, msg: Arc) { let mut lock = self.0.write().unwrap(); for s in lock.subscribes.values_mut() { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 17b545426..c143b680a 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -26,10 +26,10 @@ use hbb_common::tokio::{ Mutex as TokioMutex, }, }; -use scrap::{Capturer, Config, Display, EncodeFrame, Encoder, VideoCodecId, STRIDE_ALIGN}; +use scrap::{Capturer, Config, Display, EncodeFrame, Encoder, Frame, VideoCodecId, STRIDE_ALIGN}; use std::{ collections::HashSet, - io::ErrorKind::WouldBlock, + io::{ErrorKind::WouldBlock, Result}, time::{self, Duration, Instant}, }; @@ -45,12 +45,36 @@ lazy_static::lazy_static! { let (tx, rx) = unbounded_channel(); (tx, Arc::new(TokioMutex::new(rx))) }; + static ref PRIVACY_MODE_CONN_ID: Mutex = Mutex::new(0); + static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported(); +} + +fn is_capturer_mag_supported() -> bool { + #[cfg(windows)] + return scrap::CapturerMag::is_supported(); + #[cfg(not(windows))] + false } pub fn notify_video_frame_feched(conn_id: i32, frame_tm: Option) { FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap() } +pub fn set_privacy_mode_conn_id(conn_id: i32) { + *PRIVACY_MODE_CONN_ID.lock().unwrap() = conn_id +} + +pub fn get_privacy_mode_conn_id() -> i32 { + *PRIVACY_MODE_CONN_ID.lock().unwrap() +} + +pub fn is_privacy_mode_supported() -> bool { + #[cfg(windows)] + return *IS_CAPTURER_MAGNIFIER_SUPPORTED; + #[cfg(not(windows))] + return false; +} + struct VideoFrameController { cur: Instant, send_conn_ids: HashSet, @@ -120,6 +144,46 @@ impl VideoFrameController { } } +trait TraitCapturer { + fn frame<'a>(&'a mut self, timeout_ms: u32) -> Result>; + + #[cfg(windows)] + fn is_gdi(&self) -> bool; + #[cfg(windows)] + fn set_gdi(&mut self) -> bool; +} + +impl TraitCapturer for Capturer { + fn frame<'a>(&'a mut self, timeout_ms: u32) -> Result> { + self.frame(timeout_ms) + } + + #[cfg(windows)] + fn is_gdi(&self) -> bool { + self.is_gdi() + } + + #[cfg(windows)] + fn set_gdi(&mut self) -> bool { + self.set_gdi() + } +} + +#[cfg(windows)] +impl TraitCapturer for scrap::CapturerMag { + fn frame<'a>(&'a mut self, _timeout_ms: u32) -> Result> { + self.frame(_timeout_ms) + } + + fn is_gdi(&self) -> bool { + false + } + + fn set_gdi(&mut self) -> bool { + false + } +} + pub fn new() -> GenericService { let sp = GenericService::new(NAME, true); sp.run(run); @@ -156,6 +220,76 @@ fn check_display_changed( return false; } +// Capturer object is expensive, avoiding to create it frequently. +fn create_capturer(privacy_mode_id: i32, display: Display) -> ResultType> { + let use_yuv = true; + + #[cfg(not(windows))] + let c: Option> = None; + #[cfg(windows)] + let mut c: Option> = None; + if privacy_mode_id > 0 { + #[cfg(windows)] + { + use crate::ui::win_privacy::*; + + match scrap::CapturerMag::new( + display.origin(), + display.width(), + display.height(), + use_yuv, + ) { + Ok(mut c1) => { + let mut ok = false; + let check_begin = Instant::now(); + while check_begin.elapsed().as_secs() < 5 { + match c1.exclude("", PRIVACY_WINDOW_NAME) { + Ok(false) => { + ok = false; + std::thread::sleep(std::time::Duration::from_millis(500)); + } + Err(e) => { + bail!( + "Failed to exclude privacy window {} - {}, err: {}", + "", + PRIVACY_WINDOW_NAME, + e + ); + } + _ => { + ok = true; + break; + } + } + } + if !ok { + bail!( + "Failed to exclude privacy window {} - {} ", + "", + PRIVACY_WINDOW_NAME + ); + } + c = Some(Box::new(c1)); + } + Err(e) => { + bail!(format!("Failed to create magnifier capture {}", e)); + } + } + } + } + + let c = match c { + Some(c1) => c1, + None => { + let c1 = + Capturer::new(display, use_yuv).with_context(|| "Failed to create capturer")?; + Box::new(c1) + } + }; + + Ok(c) +} + fn run(sp: GenericService) -> ResultType<()> { let fps = 30; let wait = 1000 / fps; @@ -172,8 +306,9 @@ fn run(sp: GenericService) -> ResultType<()> { num_cpus::get_physical(), num_cpus::get(), ); - // Capturer object is expensive, avoiding to create it frequently. - let mut c = Capturer::new(display, true).with_context(|| "Failed to create capturer")?; + + let privacy_mode_id = *PRIVACY_MODE_CONN_ID.lock().unwrap(); + let mut c = create_capturer(privacy_mode_id, display)?; let q = get_image_quality(); let (bitrate, rc_min_quantizer, rc_max_quantizer, speed) = get_quality(width, height, q); @@ -227,6 +362,7 @@ fn run(sp: GenericService) -> ResultType<()> { *SWITCH.lock().unwrap() = true; bail!("SWITCH"); } + check_privacy_mode_changed(&sp, privacy_mode_id)?; if get_image_quality() != q { bail!("SWITCH"); } @@ -250,7 +386,7 @@ fn run(sp: GenericService) -> ResultType<()> { frame_controller.reset(); #[cfg(any(target_os = "android", target_os = "ios"))] - let res = match c.frame(wait as _) { + let res = match (*c).frame(wait as _) { Ok(frame) => { let time = now - start; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; @@ -273,7 +409,7 @@ fn run(sp: GenericService) -> ResultType<()> { }; #[cfg(not(any(target_os = "android", target_os = "ios")))] - let res = match c.frame(wait as _) { + let res = match (*c).frame(wait as _) { Ok(frame) => { let time = now - start; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; @@ -333,6 +469,21 @@ fn run(sp: GenericService) -> ResultType<()> { Ok(()) } +#[inline] +fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> ResultType<()> { + let privacy_mode_id_2 = *PRIVACY_MODE_CONN_ID.lock().unwrap(); + if privacy_mode_id != privacy_mode_id_2 { + if privacy_mode_id_2 != 0 { + let msg_out = crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::OnByOther, + ); + sp.send_to_others(msg_out, privacy_mode_id_2); + } + bail!("SWITCH"); + } + Ok(()) +} + #[inline] fn create_msg(vp9s: Vec) -> Message { let mut msg_out = Message::new(); diff --git a/src/ui.rs b/src/ui.rs index 5e133ea79..0e83415aa 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,6 +3,8 @@ mod cm; mod inline; #[cfg(target_os = "macos")] mod macos; +#[cfg(target_os = "windows")] +pub mod win_privacy; pub mod remote; use crate::common::SOFTWARE_UPDATE_URL; use crate::ipc; diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 91ea8e513..90d066d62 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -399,6 +399,13 @@ impl ConnectionManager { } } + fn send_data(&self, id: i32, data: Data) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(data)); + } + } + fn authorize(&self, id: i32) { let lock = self.read().unwrap(); if let Some(s) = lock.senders.get(&id) { @@ -442,6 +449,21 @@ async fn start_ipc(cm: ConnectionManager) { #[cfg(windows)] std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); + #[cfg(windows)] + std::thread::spawn(move || { + log::info!("try create privacy mode window"); + #[cfg(windows)] + { + if let Err(e) = crate::platform::windows::check_update_broker_process() { + log::warn!( + "Failed to check update broker process. Privacy mode may not work properly. {}", + e + ); + } + } + allow_err!(crate::ui::win_privacy::start()); + }); + match new_listener("_cm").await { Ok(mut incoming) => { while let Some(result) = incoming.next().await { @@ -452,6 +474,8 @@ async fn start_ipc(cm: ConnectionManager) { let cm = cm.clone(); let tx_file = tx_file.clone(); tokio::spawn(async move { + // for tmp use, without real conn id + let conn_id_tmp = -1; let mut conn_id: i32 = 0; let (tx, mut rx) = mpsc::unbounded_channel::(); let mut write_jobs: Vec = Vec::new(); @@ -476,6 +500,10 @@ async fn start_ipc(cm: ConnectionManager) { log::info!("cm ipc connection closed from connection request"); break; } + Data::PrivacyModeState((id, _)) => { + conn_id = conn_id_tmp; + cm.send_data(id, data) + } _ => { cm.handle_data(conn_id, data, &tx_file, &mut write_jobs, &mut stream).await; } @@ -491,7 +519,9 @@ async fn start_ipc(cm: ConnectionManager) { } } } - cm.remove_connection(conn_id); + if conn_id != conn_id_tmp { + cm.remove_connection(conn_id); + } }); } Err(err) => { diff --git a/src/ui/header.tis b/src/ui/header.tis index 2520549b7..19db4b08f 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -126,6 +126,7 @@ class Header: Reactor.Component { updateWindowToolbarPosition(); var style = "flow:horizontal;"; if (is_osx) style += "margin:*"; + self.timer(1ms, updatePrivacyMode); self.timer(1ms, toggleMenuState); return
{is_osx || is_xfce ? "" : {svg_fullscreen}} @@ -162,7 +163,7 @@ class Header: Reactor.Component { {is_win && pi.platform == 'Windows' && file_enabled ?
  • {svg_checkmark}{translate('File transfer')}
  • : ""} {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} - {false && keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} ; } @@ -312,6 +313,8 @@ class Header: Reactor.Component { event click $(menu#display-options>li) (_, me) { if (me.id == "custom") { handle_custom_image_quality(); + } else if (me.id == "privacy-mode") { + togglePrivacyMode(me.id); } else if (me.attributes.hasClass("toggle-option")) { handler.toggle_option(me.id); toggleMenuState(); @@ -354,17 +357,11 @@ function toggleMenuState() { for (var el in $$(menu#display-options>li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } - for (var id in ["show-remote-cursor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "privacy-mode"]) { + for (var id in ["show-remote-cursor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end"]) { var el = self.select('#' + id); if (el) { var value = handler.get_toggle_option(id); el.attributes.toggleClass("selected", value); - if (id == "privacy-mode") { - var el = $(li#block-input); - if (el) { - el.state.disabled = value; - } - } } } } @@ -400,6 +397,46 @@ handler.updatePi = function(v) { } } +function updatePrivacyMode() { + var el = $(li#privacy-mode); + if (el) { + var supported = handler.is_privacy_mode_supported(); + if (!supported) { + // el.attributes.toggleClass("line-through", true); + el.style["display"]="none"; + } else { + var value = handler.get_toggle_option("privacy-mode"); + el.attributes.toggleClass("selected", value); + var el = $(li#block-input); + if (el) { + el.state.disabled = value; + } + } + } +} +handler.updatePrivacyMode = updatePrivacyMode; + +function togglePrivacyMode(privacy_id) { + var supported = handler.is_privacy_mode_supported(); + if (!supported) { + msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), function() { }); + } else { + handler.toggle_option(privacy_id); + } +} + +handler.updateBlockInputState = function(input_blocked) { + if (!input_blocked) { + handler.toggle_option("block-input"); + input_blocked = true; + $(#block-input).text = translate("Unblock user input"); + } else { + handler.toggle_option("unblock-input"); + input_blocked = false; + $(#block-input).text = translate("Block user input"); + } +} + handler.switchDisplay = function(i) { pi.current_display = i; header.update(); diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 80d85acd7..1529253d7 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -226,6 +226,7 @@ impl sciter::EventHandler for Handler { fn save_custom_image_quality(i32, i32); fn refresh_video(); fn get_toggle_option(String); + fn is_privacy_mode_supported(); fn toggle_option(String); fn get_remember(); fn peer_platform(); @@ -496,7 +497,7 @@ impl Handler { } #[inline] - fn save_config(&self, config: PeerConfig) { + pub(super) fn save_config(&self, config: PeerConfig) { self.lc.write().unwrap().save_config(config); } @@ -505,7 +506,7 @@ impl Handler { } #[inline] - fn load_config(&self) -> PeerConfig { + pub(super) fn load_config(&self) -> PeerConfig { load_config(&self.id) } @@ -523,6 +524,10 @@ impl Handler { self.lc.read().unwrap().get_toggle_option(&name) } + fn is_privacy_mode_supported(&self) -> bool { + self.lc.read().unwrap().is_privacy_mode_supported() + } + fn refresh_video(&mut self) { self.send(Data::Message(LoginConfigHandler::refresh())); } @@ -2217,9 +2222,10 @@ impl Remote { self.handler.msgbox("error", "Connection Error", &c); return false; } - Some(misc::Union::option_response(resp)) => { - self.handler - .msgbox("custom-error", "Option Error", &resp.error); + Some(misc::Union::back_notification(notification)) => { + if !self.handle_back_notification(notification).await { + return false; + } } _ => {} }, @@ -2245,6 +2251,123 @@ impl Remote { true } + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { + match notification.union { + Some(back_notification::Union::block_input_state(state)) => { + self.handle_back_msg_block_input( + state.enum_value_or(back_notification::BlockInputState::StateUnknown), + ) + .await; + } + Some(back_notification::Union::privacy_mode_state(state)) => { + if !self + .handle_back_msg_privacy_mode( + state.enum_value_or(back_notification::PrivacyModeState::StateUnknown), + ) + .await + { + return false; + } + } + _ => {} + } + true + } + + #[inline(always)] + fn update_block_input_state(&mut self, on: bool) { + self.handler.call("updateBlockInputState", &make_args!(on)); + } + + async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { + match state { + back_notification::BlockInputState::OnSucceeded => { + self.update_block_input_state(true); + } + back_notification::BlockInputState::OnFailed => { + self.handler + .msgbox("custom-error", "Block user input", "Failed"); + self.update_block_input_state(false); + } + back_notification::BlockInputState::OffSucceeded => { + self.update_block_input_state(false); + } + back_notification::BlockInputState::OffFailed => { + self.handler + .msgbox("custom-error", "Unblock user input", "Failed"); + } + _ => {} + } + } + + #[inline(always)] + fn update_privacy_mode(&mut self, on: bool) { + let mut config = self.handler.load_config(); + config.privacy_mode = on; + self.handler.save_config(config); + + self.handler.call("updatePrivacyMode", &[]); + } + + async fn handle_back_msg_privacy_mode( + &mut self, + state: back_notification::PrivacyModeState, + ) -> bool { + match state { + back_notification::PrivacyModeState::OnByOther => { + self.handler.msgbox( + "error", + "Connecting...", + "Someone turns on privacy mode, exit", + ); + return false; + } + back_notification::PrivacyModeState::NotSupported => { + self.handler + .msgbox("custom-error", "Privacy mode", "Unsupported"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::OnSucceeded => { + self.update_privacy_mode(true); + } + back_notification::PrivacyModeState::OnFailedDenied => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer denied"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::OnFailedPlugin => { + self.handler + .msgbox("custom-error", "Privacy mode", "Please install plugins"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::OnFailed => { + self.handler + .msgbox("custom-error", "Privacy mode", "Failed"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::OffSucceeded => { + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::OffByPeer => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer exit"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::OffFailed => { + self.handler + .msgbox("custom-error", "Privacy mode", "Failed to turn off"); + } + back_notification::PrivacyModeState::OffUnknown => { + self.handler + .msgbox("custom-error", "Privacy mode", "Turned off"); + // log::error!("Privacy mode is turned off with unknown reason"); + self.update_privacy_mode(false); + } + _ => {} + } + true + } + fn check_clipboard_file_context(&mut self) { #[cfg(windows)] { @@ -2333,6 +2456,7 @@ impl Interface for Handler { } else if !self.is_port_forward() { if pi.displays.is_empty() { self.lc.write().unwrap().handle_peer_info(username, pi); + self.call("updatePrivacyMode", &[]); self.msgbox("error", "Remote Error", "No Display"); return; } @@ -2371,6 +2495,7 @@ impl Interface for Handler { } } self.lc.write().unwrap().handle_peer_info(username, pi); + self.call("updatePrivacyMode", &[]); self.call("updatePi", &make_args!(pi_sciter)); if self.is_file_transfer() { self.call2("closeSuccess", &make_args!()); diff --git a/src/ui/win_privacy.rs b/src/ui/win_privacy.rs new file mode 100644 index 000000000..71daaf0f0 --- /dev/null +++ b/src/ui/win_privacy.rs @@ -0,0 +1,566 @@ +use crate::{ + ipc::{connect, Data, PrivacyModeState}, + platform::windows::get_user_token, +}; +use hbb_common::{allow_err, bail, lazy_static, log, tokio, ResultType}; +use std::{ + ffi::CString, + sync::Mutex, + time::{Duration, Instant}, +}; +use winapi::{ + ctypes::c_int, + shared::{ + minwindef::{DWORD, FALSE, HMODULE, LOBYTE, LPARAM, LRESULT, UINT, WPARAM}, + ntdef::{HANDLE, NULL}, + windef::{HHOOK, HWND, POINT}, + }, + um::{ + errhandlingapi::GetLastError, + handleapi::CloseHandle, + libloaderapi::{GetModuleHandleA, GetModuleHandleExA, GetProcAddress}, + memoryapi::{VirtualAllocEx, WriteProcessMemory}, + processthreadsapi::{ + CreateProcessAsUserW, GetCurrentThreadId, QueueUserAPC, ResumeThread, + PROCESS_INFORMATION, STARTUPINFOW, + }, + winbase::{WTSGetActiveConsoleSessionId, CREATE_SUSPENDED, DETACHED_PROCESS}, + winnt::{MEM_COMMIT, PAGE_READWRITE}, + winuser::*, + }, +}; + +pub const ORIGIN_PROCESS_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe"; +pub const INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe"; +pub const PRIVACY_WINDOW_NAME: &'static str = "RustDeskPrivacyWindow"; + +pub const GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT: u32 = 2; +pub const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS: u32 = 4; + +const WM_USER_EXIT_HOOK: u32 = WM_USER + 1; + +lazy_static::lazy_static! { + static ref DLL_FOUND: Mutex = Mutex::new(false); + static ref CONN_ID: Mutex = Mutex::new(0); + static ref CUR_HOOK_THREAD_ID: Mutex = Mutex::new(0); + static ref WND_HANDLERS: Mutex = Mutex::new(WindowHandlers{hthread: 0, hprocess: 0}); +} + +struct WindowHandlers { + hthread: u64, + hprocess: u64, +} + +impl Drop for WindowHandlers { + fn drop(&mut self) { + unsafe { + if self.hthread != 0 { + CloseHandle(self.hthread as _); + } + self.hthread = 0; + if self.hprocess != 0 { + CloseHandle(self.hprocess as _); + } + self.hprocess = 0; + } + } +} + +pub fn turn_on_privacy(conn_id: i32) -> ResultType { + let exe_file = std::env::current_exe()?; + if let Some(cur_dir) = exe_file.parent() { + if !cur_dir.join("WindowInjection.dll").exists() { + return Ok(false); + } + } else { + bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ); + } + + if !*DLL_FOUND.lock().unwrap() { + log::info!("turn_on_privacy, dll not found when started, try start"); + start()?; + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + + let pre_conn_id = *CONN_ID.lock().unwrap(); + if pre_conn_id == conn_id { + return Ok(true); + } + if pre_conn_id != 0 { + bail!("Privacy occupied by another one"); + } + + let hwnd = wait_find_privacy_hwnd(0)?; + if hwnd.is_null() { + bail!("No privacy window created"); + } + privacy_hook::hook()?; + unsafe { + ShowWindow(hwnd as _, SW_SHOW); + } + *CONN_ID.lock().unwrap() = conn_id; + Ok(true) +} + +pub fn turn_off_privacy(conn_id: i32, state: Option) -> ResultType<()> { + let pre_conn_id = *CONN_ID.lock().unwrap(); + if pre_conn_id != 0 && conn_id != 0 && pre_conn_id != conn_id { + bail!("Failed to turn off privacy mode that belongs to someone else") + } + + privacy_hook::unhook()?; + + unsafe { + let hwnd = wait_find_privacy_hwnd(0)?; + if !hwnd.is_null() { + ShowWindow(hwnd, SW_HIDE); + } + } + + if pre_conn_id != 0 { + if let Some(state) = state { + allow_err!(set_privacy_mode_state(pre_conn_id, state, 1_000)); + } + *CONN_ID.lock().unwrap() = 0; + } + + Ok(()) +} + +pub fn start() -> ResultType<()> { + let mut wnd_handlers = WND_HANDLERS.lock().unwrap(); + if wnd_handlers.hprocess != 0 { + return Ok(()); + } + + let exe_file = std::env::current_exe()?; + if exe_file.parent().is_none() { + bail!("Cannot get parent of current exe file"); + } + let cur_dir = exe_file.parent().unwrap(); + + let dll_file = cur_dir.join("WindowInjection.dll"); + if !dll_file.exists() { + bail!( + "Failed to find required file {}", + dll_file.to_string_lossy().as_ref() + ); + } + + *DLL_FOUND.lock().unwrap() = true; + + let hwnd = wait_find_privacy_hwnd(1_000)?; + if !hwnd.is_null() { + log::info!("Privacy window is already created"); + return Ok(()); + } + + // let cmdline = cur_dir.join("MiniBroker.exe").to_string_lossy().to_string(); + let cmdline = cur_dir + .join(INJECTED_PROCESS_EXE) + .to_string_lossy() + .to_string(); + + unsafe { + let cmd_utf16: Vec = cmdline.encode_utf16().chain(Some(0).into_iter()).collect(); + + let mut start_info = STARTUPINFOW { + cb: 0, + lpReserved: NULL as _, + lpDesktop: NULL as _, + lpTitle: NULL as _, + dwX: 0, + dwY: 0, + dwXSize: 0, + dwYSize: 0, + dwXCountChars: 0, + dwYCountChars: 0, + dwFillAttribute: 0, + dwFlags: 0, + wShowWindow: 0, + cbReserved2: 0, + lpReserved2: NULL as _, + hStdInput: NULL as _, + hStdOutput: NULL as _, + hStdError: NULL as _, + }; + let mut proc_info = PROCESS_INFORMATION { + hProcess: NULL as _, + hThread: NULL as _, + dwProcessId: 0, + dwThreadId: 0, + }; + + let session_id = WTSGetActiveConsoleSessionId(); + let token = get_user_token(session_id, true); + if token.is_null() { + bail!("Failed to get token of current user"); + } + + let create_res = CreateProcessAsUserW( + token, + NULL as _, + cmd_utf16.as_ptr() as _, + NULL as _, + NULL as _, + FALSE, + CREATE_SUSPENDED | DETACHED_PROCESS, + NULL, + NULL as _, + &mut start_info, + &mut proc_info, + ); + CloseHandle(token); + if 0 == create_res { + bail!( + "Failed to create privacy window process {}, code {}", + cmdline, + GetLastError() + ); + }; + + inject_dll( + proc_info.hProcess, + proc_info.hThread, + dll_file.to_string_lossy().as_ref(), + )?; + + if 0xffffffff == ResumeThread(proc_info.hThread) { + // CloseHandle + CloseHandle(proc_info.hThread); + CloseHandle(proc_info.hProcess); + + bail!( + "Failed to create privacy window process, {}", + GetLastError() + ); + } + + wnd_handlers.hthread = proc_info.hThread as _; + wnd_handlers.hprocess = proc_info.hProcess as _; + + let hwnd = wait_find_privacy_hwnd(1_000)?; + if hwnd.is_null() { + bail!("Failed to get hwnd after started"); + } + } + + Ok(()) +} + +unsafe fn inject_dll<'a>(hproc: HANDLE, hthread: HANDLE, dll_file: &'a str) -> ResultType<()> { + let dll_file_utf16: Vec = dll_file.encode_utf16().chain(Some(0).into_iter()).collect(); + + let buf = VirtualAllocEx( + hproc, + NULL as _, + dll_file_utf16.len() * 2, + MEM_COMMIT, + PAGE_READWRITE, + ); + if buf.is_null() { + bail!("Failed VirtualAllocEx"); + } + + let mut written: usize = 0; + if 0 == WriteProcessMemory( + hproc, + buf, + dll_file_utf16.as_ptr() as _, + dll_file_utf16.len() * 2, + &mut written, + ) { + bail!("Failed WriteProcessMemory"); + } + + let kernel32_modulename = CString::new("kernel32")?; + let hmodule = GetModuleHandleA(kernel32_modulename.as_ptr() as _); + if hmodule.is_null() { + bail!("Failed GetModuleHandleA"); + } + + let load_librarya_name = CString::new("LoadLibraryW")?; + let load_librarya = GetProcAddress(hmodule, load_librarya_name.as_ptr() as _); + if load_librarya.is_null() { + bail!("Failed GetProcAddress of LoadLibraryW"); + } + + if 0 == QueueUserAPC(Some(std::mem::transmute(load_librarya)), hthread, buf as _) { + bail!("Failed QueueUserAPC"); + } + + Ok(()) +} + +fn wait_find_privacy_hwnd(msecs: u128) -> ResultType { + let tm_begin = Instant::now(); + let wndname = CString::new(PRIVACY_WINDOW_NAME)?; + loop { + unsafe { + let hwnd = FindWindowA(NULL as _, wndname.as_ptr() as _); + if !hwnd.is_null() { + return Ok(hwnd); + } + } + + if msecs == 0 || tm_begin.elapsed().as_millis() > msecs { + return Ok(NULL as _); + } + + std::thread::sleep(Duration::from_millis(100)); + } +} + +#[tokio::main(flavor = "current_thread")] +async fn set_privacy_mode_state( + conn_id: i32, + state: PrivacyModeState, + ms_timeout: u64, +) -> ResultType<()> { + println!("set_privacy_mode_state begin"); + let mut c = connect(ms_timeout, "_cm").await?; + println!("set_privacy_mode_state connect done"); + c.send(&Data::PrivacyModeState((conn_id, state))).await +} + +pub(super) mod privacy_hook { + use super::*; + use std::sync::mpsc::{channel, Sender}; + + fn do_hook(tx: Sender) -> ResultType<(HHOOK, HHOOK)> { + let invalid_ret = (0 as HHOOK, 0 as HHOOK); + + let mut cur_hook_thread_id = CUR_HOOK_THREAD_ID.lock().unwrap(); + if *cur_hook_thread_id != 0 { + // unreachable! + tx.send("Already hooked".to_owned())?; + return Ok(invalid_ret); + } + + unsafe { + let mut hm_keyboard = 0 as HMODULE; + if 0 == GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + DefWindowProcA as _, + &mut hm_keyboard as _, + ) { + tx.send(format!( + "Failed to GetModuleHandleExA, error: {}", + GetLastError() + ))?; + return Ok(invalid_ret); + } + let mut hm_mouse = 0 as HMODULE; + if 0 == GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + DefWindowProcA as _, + &mut hm_mouse as _, + ) { + tx.send(format!( + "Failed to GetModuleHandleExA, error: {}", + GetLastError() + ))?; + return Ok(invalid_ret); + } + + let hook_keyboard = SetWindowsHookExA( + WH_KEYBOARD_LL, + Some(privacy_mode_hook_keyboard), + hm_keyboard, + 0, + ); + if hook_keyboard.is_null() { + tx.send(format!(" SetWindowsHookExA keyboard {}", GetLastError()))?; + return Ok(invalid_ret); + } + + let hook_mouse = + SetWindowsHookExA(WH_MOUSE_LL, Some(privacy_mode_hook_mouse), hm_mouse, 0); + if hook_mouse.is_null() { + if FALSE == UnhookWindowsHookEx(hook_keyboard) { + // Fatal error + log::error!(" UnhookWindowsHookEx keyboard {}", GetLastError()); + } + tx.send(format!(" SetWindowsHookExA mouse {}", GetLastError()))?; + return Ok(invalid_ret); + } + + *cur_hook_thread_id = GetCurrentThreadId(); + tx.send("".to_owned())?; + return Ok((hook_keyboard, hook_mouse)); + } + } + + pub fn hook() -> ResultType<()> { + let (tx, rx) = channel(); + std::thread::spawn(move || { + let hook_keyboard; + let hook_mouse; + unsafe { + match do_hook(tx.clone()) { + Ok(hooks) => { + hook_keyboard = hooks.0; + hook_mouse = hooks.1; + } + Err(e) => { + // Fatal error + tx.send(format!("Unexpected err when hook {}", e)).unwrap(); + return; + } + } + if hook_keyboard.is_null() { + return; + } + + let mut msg = MSG { + hwnd: NULL as _, + message: 0 as _, + wParam: 0 as _, + lParam: 0 as _, + time: 0 as _, + pt: POINT { + x: 0 as _, + y: 0 as _, + }, + }; + while FALSE != GetMessageA(&mut msg, NULL as _, 0, 0) { + if msg.message == WM_USER_EXIT_HOOK { + break; + } + + TranslateMessage(&msg); + DispatchMessageA(&msg); + } + + if FALSE == UnhookWindowsHookEx(hook_keyboard as _) { + // Fatal error + log::error!("Failed UnhookWindowsHookEx keyboard {}", GetLastError()); + } + + if FALSE == UnhookWindowsHookEx(hook_mouse as _) { + // Fatal error + log::error!("Failed UnhookWindowsHookEx mouse {}", GetLastError()); + } + + *CUR_HOOK_THREAD_ID.lock().unwrap() = 0; + } + }); + + match rx.recv() { + Ok(msg) => { + if msg == "" { + Ok(()) + } else { + bail!(msg) + } + } + Err(e) => { + bail!("Failed to wait hook result {}", e) + } + } + } + + pub fn unhook() -> ResultType<()> { + unsafe { + let cur_hook_thread_id = CUR_HOOK_THREAD_ID.lock().unwrap(); + if *cur_hook_thread_id != 0 { + if FALSE == PostThreadMessageA(*cur_hook_thread_id, WM_USER_EXIT_HOOK, 0, 0) { + bail!("Failed to post message to exit hook, {}", GetLastError()); + } + } + } + Ok(()) + } + + #[no_mangle] + pub extern "system" fn privacy_mode_hook_keyboard( + code: c_int, + w_param: WPARAM, + l_param: LPARAM, + ) -> LRESULT { + if code < 0 { + unsafe { + return CallNextHookEx(NULL as _, code, w_param, l_param); + } + } + + let ks = l_param as PKBDLLHOOKSTRUCT; + let w_param2 = w_param as UINT; + + unsafe { + if (*ks).dwExtraInfo != enigo::ENIGO_INPUT_EXTRA_VALUE { + // Disable alt key. Alt + Tab will switch windows. + if (*ks).flags & LLKHF_ALTDOWN == LLKHF_ALTDOWN { + return 1; + } + + match w_param2 { + WM_KEYDOWN => { + // Disable all keys other than P and Ctrl. + if ![80, 162, 163].contains(&(*ks).vkCode) { + return 1; + } + + // NOTE: GetKeyboardState may not work well... + + // Check if Ctrl + P is pressed + let cltr_down = (GetKeyState(VK_CONTROL) as u16) & (0x8000 as u16) > 0; + let key = LOBYTE((*ks).vkCode as _); + if cltr_down && (key == 'p' as u8 || key == 'P' as u8) { + // Ctrl + P is pressed, turn off privacy mode + if let Err(e) = + turn_off_privacy(0, Some(crate::ipc::PrivacyModeState::OffByPeer)) + { + log::error!("Failed to off_privacy {}", e); + } + } + } + WM_KEYUP => { + log::trace!("WM_KEYUP {}", (*ks).vkCode); + } + _ => { + log::trace!("KEYBOARD OTHER {} {}", w_param2, (*ks).vkCode); + } + } + } + } + unsafe { CallNextHookEx(NULL as _, code, w_param, l_param) } + } + + #[no_mangle] + pub extern "system" fn privacy_mode_hook_mouse( + code: c_int, + w_param: WPARAM, + l_param: LPARAM, + ) -> LRESULT { + if code < 0 { + unsafe { + return CallNextHookEx(NULL as _, code, w_param, l_param); + } + } + + let ms = l_param as PMOUSEHOOKSTRUCT; + unsafe { + if (*ms).dwExtraInfo != enigo::ENIGO_INPUT_EXTRA_VALUE { + return 1; + } + } + unsafe { CallNextHookEx(NULL as _, code, w_param, l_param) } + } +} + +mod test { + #[test] + fn privacy_hook() { + //use super::*; + + // privacy_hook::hook().unwrap(); + // std::thread::sleep(std::time::Duration::from_millis(50)); + // privacy_hook::unhook().unwrap(); + } +} diff --git a/src/windows.cc b/src/windows.cc index 81df780a2..dd3fa2e9e 100644 --- a/src/windows.cc +++ b/src/windows.cc @@ -53,26 +53,28 @@ DWORD GetLogonPid(DWORD dwSessionId, BOOL as_user) return dwLogonPid; } -// if should try WTSQueryUserToken? -// https://stackoverflow.com/questions/7285666/example-code-a-service-calls-createprocessasuser-i-want-the-process-to-run-in -BOOL GetSessionUserTokenWin(OUT LPHANDLE lphUserToken, DWORD dwSessionId, BOOL as_user) -{ - BOOL bResult = FALSE; - DWORD Id = GetLogonPid(dwSessionId, as_user); - if (HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Id)) - { - bResult = OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, lphUserToken); - CloseHandle(hProcess); - } - return bResult; -} - +// START the app as system extern "C" { + // if should try WTSQueryUserToken? + // https://stackoverflow.com/questions/7285666/example-code-a-service-calls-createprocessasuser-i-want-the-process-to-run-in + BOOL GetSessionUserTokenWin(OUT LPHANDLE lphUserToken, DWORD dwSessionId, BOOL as_user) + { + BOOL bResult = FALSE; + DWORD Id = GetLogonPid(dwSessionId, as_user); + if (HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Id)) + { + bResult = OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, lphUserToken); + CloseHandle(hProcess); + } + return bResult; + } + bool is_windows_server() { return IsWindowsServer(); } + HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user) { HANDLE hProcess = NULL;