privacy_mode: win10 magnifier

Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
fufesou 2022-04-25 12:28:28 +08:00
parent 85cd066cd7
commit c269d1c831
37 changed files with 2163 additions and 119 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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,
})
},
};

View File

@ -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;
}
}

View File

@ -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"

View File

@ -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);
}
}

View File

@ -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);

View File

@ -111,3 +111,32 @@ impl Display {
self.origin() == (0, 0)
}
}
pub struct CapturerMag {
inner: dxgi::mag::CapturerMag,
data: Vec<u8>,
}
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<Self> {
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<bool> {
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<Frame<'a>> {
self.inner.frame(&mut self.data)?;
Ok(Frame(&self.data))
}
}

626
libs/scrap/src/dxgi/mag.rs Normal file
View File

@ -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<u8>)> = 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<unsafe extern "C" fn() -> BOOL>;
pub type MagUninitializeFunc = ::std::option::Option<unsafe extern "C" fn() -> BOOL>;
pub type MagSetWindowSourceFunc =
::std::option::Option<unsafe extern "C" fn(hwnd: HWND, rect: RECT) -> 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<Self> {
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<FARPROC> {
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<u8>,
}
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<Self> {
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::<WNDCLASSEXA>() 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<bool> {
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<u8>) -> 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());
}
}

View File

@ -1,6 +1,7 @@
use std::{io, mem, ptr, slice};
pub mod gdi;
pub use gdi::CapturerGDI;
pub mod mag;
use winapi::{
shared::{

View File

@ -773,6 +773,7 @@ pub struct LoginConfigHandler {
pub port_forward: (String, i32),
pub version: i64,
pub conn_id: i32,
features: Option<Features>,
}
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(),

View File

@ -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<String> {
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
}

View File

@ -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")]

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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<HANDL
let wstr = wstr.as_ptr();
let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE) };
if h.is_null() {
log::error!("Failed to luanch server: {}", get_error());
log::error!("Failed to launch server: {}", get_error());
}
Ok(h)
}
@ -796,6 +798,49 @@ fn get_default_install_path() -> 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
}
}
}

View File

@ -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;

View File

@ -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<MessageInput>, 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<MessageInput>) {
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<Data>,
@ -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<T: std::string::ToString>(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<bool> {
#[cfg(windows)]
{
let plugin_exitst = crate::ui::win_privacy::turn_on_privacy(_conn_id)?;
Ok(plugin_exitst)
}
#[cfg(not(windows))]
{
Ok(true)
}
}
}

View File

@ -148,6 +148,16 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
}
}
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<Message>) {
let mut lock = self.0.write().unwrap();
for s in lock.subscribes.values_mut() {

View File

@ -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<i32> = 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<Instant>) {
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<i32>,
@ -120,6 +144,46 @@ impl VideoFrameController {
}
}
trait TraitCapturer {
fn frame<'a>(&'a mut self, timeout_ms: u32) -> Result<Frame<'a>>;
#[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<Frame<'a>> {
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<Frame<'a>> {
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<Box<dyn TraitCapturer>> {
let use_yuv = true;
#[cfg(not(windows))]
let c: Option<Box<dyn TraitCapturer>> = None;
#[cfg(windows)]
let mut c: Option<Box<dyn TraitCapturer>> = 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<VP9>) -> Message {
let mut msg_out = Message::new();

View File

@ -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;

View File

@ -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::<Data>();
let mut write_jobs: Vec<fs::TransferJob> = 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) => {

View File

@ -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 <div style={style}>
{is_osx || is_xfce ? "" : <span #fullscreen>{svg_fullscreen}</span>}
@ -162,7 +163,7 @@ class Header: Reactor.Component {
{is_win && pi.platform == 'Windows' && file_enabled ? <li #enable-file-transfer .toggle-option><span>{svg_checkmark}</span>{translate('File transfer')}</li> : ""}
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""}
{keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>{translate('Lock after session end')}</li> : ""}
{false && keyboard_enabled && pi.platform == "Windows" ? <li #privacy-mode .toggle-option><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""}
{keyboard_enabled && pi.platform == "Windows" ? <li #privacy-mode><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""}
</menu>
</popup>;
}
@ -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();

View File

@ -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!());

566
src/ui/win_privacy.rs Normal file
View File

@ -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<bool> = Mutex::new(false);
static ref CONN_ID: Mutex<i32> = Mutex::new(0);
static ref CUR_HOOK_THREAD_ID: Mutex<DWORD> = Mutex::new(0);
static ref WND_HANDLERS: Mutex<WindowHandlers> = 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<bool> {
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<PrivacyModeState>) -> 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<u16> = 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<u16> = 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<HWND> {
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<String>) -> 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();
}
}

View File

@ -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;