100% open source

This commit is contained in:
rustdesk 2022-05-12 17:35:25 +08:00
parent 9098619162
commit c1bad84a86
58 changed files with 8397 additions and 3292 deletions

1359
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,9 +5,19 @@ authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
description = "A remote control software."
default-run = "rustdesk"
[lib]
name = "librustdesk"
crate-type = ["cdylib", "staticlib", "rlib"]
[[bin]]
name = "lic"
path = "src/lic_main.rs"
[features]
inline = []
hbbs = []
cli = []
use_samplerate = ["samplerate"]
use_rubato = ["rubato"]
@ -40,13 +50,16 @@ dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpol
rubato = { version = "0.12", optional = true }
samplerate = { version = "0.2", optional = true }
async-trait = "0.1"
uuid = { version = "1.0.0", features = ["v4"] }
uuid = { version = "1.0", features = ["v4"] }
clap = "3.0"
rpassword = "6.0"
base64 = "0.13"
sysinfo = "0.23"
num_cpus = "1.13"
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.11", features = ["json"] }
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
cpal = "0.13.5"
@ -54,14 +67,19 @@ cpal = "0.13.5"
machine-uid = "0.2"
mac_address = "1.1"
sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" }
sys-locale = "0.2"
enigo = { path = "libs/enigo" }
clipboard = { path = "libs/clipboard" }
rdev = { git = "https://github.com/open-trade/rdev" }
ctrlc = "3.2"
arboard = "2.0"
clipboard-master = "3.1"
#rdev = { path = "../rdev" }
rdev = { git = "https://github.com/open-trade/rdev" }
#minreq = { version = "2.4", features = ["punycode", "https-native"] }
[target.'cfg(target_os = "windows")'.dependencies]
systray = { git = "https://github.com/liyue201/systray-rs" }
#systray = { git = "https://github.com/open-trade/systray-rs" }
trayicon = { version = "0.1", features = ["winit"] }
# > 0.25 not work with trayicon
winit = "0.25"
winapi = { version = "0.3", features = ["winuser"] }
winreg = "0.10"
windows-service = "0.4"
@ -79,15 +97,17 @@ tray-item = "0.7" # looks better than trayicon
psimple = { package = "libpulse-simple-binding", version = "2.25" }
pulse = { package = "libpulse-binding", version = "2.26" }
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
async-process = "1.3"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11"
jni = "0.19.0"
[workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display"]
[package.metadata.winres]
LegalCopyright = "Copyright © 2020"
LegalCopyright = "Copyright © 2022 Purslane, Inc."
# this FileDescription overrides package.description
FileDescription = "RustDesk"
@ -106,7 +126,7 @@ hound = "3.4"
name = "RustDesk"
identifier = "com.carriez.rustdesk"
icon = ["32x32.png", "128x128.png", "128x128@2x.png"]
deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio"]
deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio", "python3-pip", "curl"]
osx_minimum_system_version = "10.14"
resources = ["mac-tray.png"]
@ -114,7 +134,7 @@ resources = ["mac-tray.png"]
#!!! rembember call "strip target/release/rustdesk"
# which reduce binary size a lot
[profile.release]
#lto = true
#codegen-units = 1
#panic = 'abort'
lto = true
codegen-units = 1
panic = 'abort'
#opt-level = 'z' # only have smaller size after strip

1
libs/enigo/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

View File

@ -1,7 +1,7 @@
[package]
name = "hbb_common"
version = "0.1.0"
authors = ["rustdesk<info@rustdesk.com>"]
authors = ["open-trade <info@opentradesolutions.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -35,7 +35,7 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
mac_address = "1.1"
[features]
quic = ["quinn"]
quic = []
[build-dependencies]
protobuf-codegen-pure = "3.0.0-alpha.2"

View File

@ -72,6 +72,7 @@ message PeerInfo {
int32 current_display = 5;
bool sas_enabled = 6;
string version = 7;
int32 conn_id = 8;
}
message LoginResponse {

View File

@ -18,7 +18,9 @@ message RegisterPeerResponse { bool request_pk = 2; }
message PunchHoleRequest {
string id = 1;
NatType nat_type = 2;
string licence_key = 3;
ConnType conn_type = 4;
string token = 5;
}
message PunchHole {
@ -55,6 +57,7 @@ message RegisterPk {
string id = 1;
bytes uuid = 2;
bytes pk = 3;
string old_id = 4;
}
message RegisterPkResponse {
@ -99,7 +102,9 @@ message RequestRelay {
bytes socket_addr = 3;
string relay_server = 4;
bool secure = 5;
string licence_key = 6;
ConnType conn_type = 7;
string token = 8;
}
message RelayResponse {

View File

@ -12,7 +12,6 @@ use std::{
time::SystemTime,
};
pub const APP_NAME: &str = "RustDesk";
pub const RENDEZVOUS_TIMEOUT: u64 = 12_000;
pub const CONNECT_TIMEOUT: u64 = 18_000;
pub const REG_INTERVAL: i64 = 12_000;
@ -26,7 +25,9 @@ pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACA
pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAA7VBMVEUAAAAAcf8Acf8Acf8Adf8Acf8Acf8AcP8Acv8AcP8Acf8Acf8Acf8Acv8Acf8Acf8Ab/8AcP8Acf8Acf8Acf/////7/f8Dc/8TfP/1+f/n8v9Hmf/u9v+Uw//Q5f9hp/8Yfv8Qev8Ld/+52P+z1f+s0f81j/8wjP8Hdf/3+/8mh/8fg//x9//h7//H4P9xsP9rrf9oq/8rif/r9P/D3v+92/+Duv9bpP/d7f/U5/9NnP8/lP8jhP/L4v/B3P+OwP9+t/95tf9Rn/8bgf/Z6v+Zx/90sv9lqf85kf+hy/9UoP+Wxf+kzP+dyP+Lvv/H4q8IAAAAFHRSTlMA+u6bB6x5XR4V0+S4i4k5N+a81W8MiAQAAAVcSURBVHjazdvpWtpAGIbhgEutdW3fL2GHsMsiq4KI+66t5384XahF/GbizJAy3j/1Ah5CJhNCxpm1vbryLRrBfxKJrq+sbjtSa5u7WIDdzTVH5PNSBAsSWfrsMJ+iWKDoJ2fW8hIWbGl55vW/YuE2XhUsb8CCr9OCJVix9G//gyWf/o6/KCyJfrbwAfAPYS0CayK/j4mbsGjrV8AXWLTrONuwasdZhVWrzgqsWnG+wap1Jwqrok4EVkUcmKhdVvBaOVnzYEY/oJpMD4mo6ONF/ZSIUsX2FZjQA7xRqUET+y/v2W/Sy59u62DCDMgdJmhqgIk7eqWQBBNWwPhmj147w8QTzTjKVsGEEBBLuzSrhIkivTF8DD/Aa6forQNMHBD/VyXkgHGfuBN5ALln1TADOnESyGCiT8L/1kILqD6Q0BEm9kkofhdSwNUJiV1jQvZ/SnthBNSaJJGZbgGJUnX+gEqCZPpsJ2T2Y/MGVBrE8eOAvCA/X8A4QXLnmEhTgIPqPAG5IQU4fhmkFOT7HAFenwIU8Jd/TUEODQIUtu1eOj/dUD9cknOTpgEDkup3YrOfVStDUomcWcBVisTiNxVw3TPpgCl4RgFFybZ/9iHmn8uS2yYBA8m7qUEu9oOEejH9gHxC+PazCHbcFM8K+gGHJNAs4z2xgnAkVHQDcnG1IzvnCSfvom7AM3EZ9voah4+KXoAvGFJHMSgqEfegF3BBTKoOVfkMMXFfJ8AT7MuXUDeOE9PWCUiKBpKOlmAP1gngH2LChw7vhJgr9YD8Hnt0BxrE27CtHnDJR4AHTX1+KFAP4Ef0LHTxN9HwlAMSbAjmoavKZ8ayakDXYAhwN3wzqgZk2UPvwRjshmeqATeCT09f3mWnEqoBGf4NxAB/moRqADuOtmDiid6KqQVcsQeOYOKW3uqqBRwL5nITj/yrlFpAVrDpTJT5llQLaLMHwshY7UDgvD+VujDC96WWWsBtSAE5FnChFnAeUkDMdAvw88EqTNT5SYXpTlgPaRQM1AIGorkolNnoUS1gJHigCX48SaoF3Asuspg4Mz0U8+FTgIkCG01V09kwBQP8xG5ofD5AXeirkPEJSUlwSVIfP5ykVQNaggvz+k7prTvVgDKF8BnUXP4kqgEe/257E8Ig7EE1gA8g2stBTz7FLxqrB3SIeYaeQ2IG6gE5l2+Cmt5MGOfP4KsGiH8DOYWOoujnDY2ALHF3810goZFOQDVBTFx9Uj7eI6bp6QTgnLjeGGq6KeJuoRUQixN3pDYWyz1Rva8XIL5UPFQZCsmG3gV7R+dieS+Jd3iHLglce7oBuCOhp3zwHLxPQpfQDvBOSKjZqUIml3ZJ6AD6AajFSZJwewWR8ZPsEY26SQDaJOMeZP23w6bTJ6kBjAJQILm9hzqm7otu4G+nhgGxIQUlPLKzL7GhbxqAboMCuN2XXd+lAL0ajAMwclV+FD6jAPEy5ghAlhfwX2FODX445gHKxyN++fs64PUHmDMAbbYN2DlKk2QaScwdgMs4SZxMv4OJJSoIIQBl2Qtk3gk4qiOUANRPJQHB+0A6j5AC4J27QQEZ4eZPAsYBXFk0N/YD7iUrxRBqALxOTzoMC3x8lCFlfkMjuz8iLfk6fzQCQgjg8q3ZEd8RzUVuKelBh96Nzcc3qelL1V+2zfRv1xc56Ino3tpdPT7cd//MspfTrD/7R6p4W4O2qLMObfnyIHvvYcrPtkZjDybW7d/eb32Bg/UlHnYXuXz5CMt8rC90sr7Uy/5iN+vL/ewveLS/5NNKwcbyR1r2a3/h8wdY+v3L2tZC5oUvW2uO1M7qyvp/Xv6/48z4CTxjJEfyjEaMAAAAAElFTkSuQmCC
";
#[cfg(target_os = "macos")]
pub const ORG: &str = "com.carriez";
lazy_static::lazy_static! {
pub static ref ORG: Arc<RwLock<String>> = Arc::new(RwLock::new("com.carriez".to_owned()));
}
type Size = (i32, i32, i32, i32);
@ -35,10 +36,13 @@ lazy_static::lazy_static! {
static ref CONFIG2: Arc<RwLock<Config2>> = Arc::new(RwLock::new(Config2::load()));
static ref LOCAL_CONFIG: Arc<RwLock<LocalConfig>> = Arc::new(RwLock::new(LocalConfig::load()));
pub static ref ONLINE: Arc<Mutex<HashMap<String, i64>>> = Default::default();
pub static ref PROD_RENDEZVOUS_SERVER: Arc<RwLock<String>> = Default::default();
pub static ref APP_NAME: Arc<RwLock<String>> = Arc::new(RwLock::new("RustDesk".to_owned()));
}
#[cfg(any(target_os = "android", target_os = "ios"))]
lazy_static::lazy_static! {
pub static ref APP_DIR: Arc<RwLock<String>> = Default::default();
pub static ref APP_HOME_DIR: Arc<RwLock<String>> = Default::default();
}
const CHARS: &'static [char] = &[
'2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
@ -256,13 +260,13 @@ impl Config {
}
fn file_(suffix: &str) -> PathBuf {
let name = format!("{}{}", APP_NAME, suffix);
let name = format!("{}{}", *APP_NAME.read().unwrap(), suffix);
Self::path(name).with_extension("toml")
}
pub fn get_home() -> PathBuf {
#[cfg(any(target_os = "android", target_os = "ios"))]
return Self::path("");
return Self::path(APP_HOME_DIR.read().unwrap().as_str());
if let Some(path) = dirs_next::home_dir() {
patch(path)
} else if let Ok(path) = std::env::current_dir() {
@ -282,9 +286,9 @@ impl Config {
#[cfg(not(target_os = "macos"))]
let org = "";
#[cfg(target_os = "macos")]
let org = ORG;
let org = ORG.read().unwrap().clone();
// /var/root for root
if let Some(project) = ProjectDirs::from("", org, APP_NAME) {
if let Some(project) = ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) {
let mut path = patch(project.config_dir().to_path_buf());
path.push(p);
return path;
@ -297,14 +301,14 @@ impl Config {
#[cfg(target_os = "macos")]
{
if let Some(path) = dirs_next::home_dir().as_mut() {
path.push(format!("Library/Logs/{}", APP_NAME));
path.push(format!("Library/Logs/{}", *APP_NAME.read().unwrap()));
return path.clone();
}
}
#[cfg(target_os = "linux")]
{
let mut path = Self::get_home();
path.push(format!(".local/share/logs/{}", APP_NAME));
path.push(format!(".local/share/logs/{}", *APP_NAME.read().unwrap()));
std::fs::create_dir_all(&path).ok();
return path;
}
@ -322,12 +326,20 @@ impl Config {
// \\ServerName\pipe\PipeName
// where ServerName is either the name of a remote computer or a period, to specify the local computer.
// https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names
format!("\\\\.\\pipe\\{}\\query{}", APP_NAME, postfix)
format!(
"\\\\.\\pipe\\{}\\query{}",
*APP_NAME.read().unwrap(),
postfix
)
}
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
let mut path: PathBuf = format!("/tmp/{}", APP_NAME).into();
#[cfg(target_os = "android")]
let mut path: PathBuf =
format!("{}/{}", *APP_DIR.read().unwrap(), *APP_NAME.read().unwrap()).into();
#[cfg(not(target_os = "android"))]
let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into();
fs::create_dir(&path).ok();
fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok();
path.push(format!("ipc{}", postfix));
@ -351,7 +363,10 @@ impl Config {
pub fn get_rendezvous_server() -> String {
let mut rendezvous_server = Self::get_option("custom-rendezvous-server");
if rendezvous_server.is_empty() {
rendezvous_server = CONFIG2.write().unwrap().rendezvous_server.clone();
rendezvous_server = PROD_RENDEZVOUS_SERVER.read().unwrap().clone();
}
if rendezvous_server.is_empty() {
rendezvous_server = CONFIG2.read().unwrap().rendezvous_server.clone();
}
if rendezvous_server.is_empty() {
rendezvous_server = Self::get_rendezvous_servers()
@ -370,6 +385,10 @@ impl Config {
if !s.is_empty() {
return vec![s];
}
let s = PROD_RENDEZVOUS_SERVER.read().unwrap().clone();
if !s.is_empty() {
return vec![s];
}
let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL;
if serial_obsolute {
let ss: Vec<String> = Self::get_option("rendezvous-servers")
@ -446,7 +465,13 @@ impl Config {
fn get_auto_id() -> Option<String> {
#[cfg(any(target_os = "android", target_os = "ios"))]
return None;
{
return Some(
rand::thread_rng()
.gen_range(1_000_000_000..2_000_000_000)
.to_string(),
);
}
let mut id = 0u32;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Ok(Some(ma)) = mac_address::get_mac_address() {
@ -531,6 +556,15 @@ impl Config {
id
}
pub fn get_id_or(b: String) -> String {
let a = CONFIG.read().unwrap().id.clone();
if a.is_empty() {
b
} else {
a
}
}
pub fn get_options() -> HashMap<String, String> {
CONFIG2.read().unwrap().options.clone()
}

View File

@ -430,10 +430,11 @@ pub fn new_error<T: std::string::ToString>(id: i32, err: T, file_num: i32) -> Me
}
#[inline]
pub fn new_dir(id: i32, files: Vec<FileEntry>) -> Message {
pub fn new_dir(id: i32, path: String, files: Vec<FileEntry>) -> Message {
let mut resp = FileResponse::new();
resp.set_dir(FileDirectory {
id,
path,
entries: files.into(),
..Default::default()
});

View File

@ -27,7 +27,7 @@ pub use anyhow::{self, bail};
pub use futures_util;
pub mod config;
pub mod fs;
pub use lazy_static;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use mac_address;
pub use rand;
pub use regex;
@ -35,6 +35,7 @@ pub use sodiumoxide;
pub use tokio_socks;
pub use tokio_socks::IntoTargetAddr;
pub use tokio_socks::TargetAddr;
pub use lazy_static;
#[cfg(feature = "quic")]
pub type Stream = quic::Connection;
@ -179,6 +180,12 @@ where
Ok(io::BufReader::new(file).lines())
}
pub fn is_valid_custom_id(id: &str) -> bool {
regex::Regex::new(r"^[a-zA-Z]\w{5,15}$")
.unwrap()
.is_match(id)
}
pub fn get_version_number(v: &str) -> i64 {
let mut n = 0;
for x in v.split(".") {

View File

@ -14,7 +14,7 @@ pub enum FramedSocket {
ProxySocks(Socks5UdpFramed),
}
fn new_socket(addr: SocketAddr, reuse: bool) -> Result<Socket, std::io::Error> {
fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result<Socket, std::io::Error> {
let socket = match addr {
SocketAddr::V4(..) => Socket::new(Domain::ipv4(), Type::dgram(), None),
SocketAddr::V6(..) => Socket::new(Domain::ipv6(), Type::dgram(), None),
@ -27,6 +27,14 @@ fn new_socket(addr: SocketAddr, reuse: bool) -> Result<Socket, std::io::Error> {
socket.set_reuse_port(true)?;
socket.set_reuse_address(true)?;
}
if buf_size > 0 {
socket.set_recv_buffer_size(buf_size).ok();
}
log::info!(
"Receive buf size of udp {}: {:?}",
addr,
socket.recv_buffer_size()
);
socket.bind(&addr.into())?;
Ok(socket)
}
@ -40,7 +48,7 @@ impl FramedSocket {
#[allow(clippy::never_loop)]
pub async fn new_reuse<T: std::net::ToSocketAddrs>(addr: T) -> ResultType<Self> {
for addr in addr.to_socket_addrs()? {
let socket = new_socket(addr, true)?.into_udp_socket();
let socket = new_socket(addr, true, 0)?.into_udp_socket();
return Ok(Self::Direct(UdpFramed::new(
UdpSocket::from_std(socket)?,
BytesCodec::new(),
@ -49,6 +57,19 @@ impl FramedSocket {
bail!("could not resolve to any address");
}
pub async fn new_with_buf_size<T: std::net::ToSocketAddrs>(
addr: T,
buf_size: usize,
) -> ResultType<Self> {
for addr in addr.to_socket_addrs()? {
return Ok(Self::Direct(UdpFramed::new(
UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?,
BytesCodec::new(),
)));
}
bail!("could not resolve to any address");
}
pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>(
proxy: P,
local: T,

View File

@ -23,7 +23,13 @@ version = "0.3"
default-features = true
features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser"]
[dev-dependencies]
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.10"
jni = "0.19"
lazy_static = "1.4"
log = "0.4"
[target.'cfg(not(target_os = "android"))'.dev-dependencies]
repng = "0.2"
docopt = "1.1"
webm = "1.0"
@ -33,7 +39,6 @@ quest = "0.3"
[build-dependencies]
target_build_utils = "0.3"
bindgen = "0.59"
vcpkg = "0.2"
[target.'cfg(target_os = "linux")'.dependencies]
dbus = { version = "0.9", optional = true }

View File

@ -4,35 +4,40 @@ use std::{
};
fn find_package(name: &str) -> Vec<PathBuf> {
let library = vcpkg::find_package(name).expect("Failed to find package");
println!("cargo:info={}", library.vcpkg_triplet); //TODO
let lib_name = name.trim_start_matches("lib").to_string();
println!("{}", format!("cargo:rustc-link-lib=static={}", lib_name));
match (
library.link_paths.as_slice(),
library.include_paths.as_slice(),
) {
([link_search, ..], [include, ..]) => {
println!(
"{}",
format!("cargo:rustc-link-search={}", link_search.display())
);
println!("{}", format!("cargo:include={}", include.display()));
}
_ => {
panic!(
"{}",
if library.link_paths.is_empty() {
"link path not found"
} else {
"include path not found"
}
)
}
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
let mut path: PathBuf = vcpkg_root.into();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
if target_arch == "x86_64" {
target_arch = "x64".to_owned();
} else if target_arch == "aarch64" {
target_arch = "arm64".to_owned();
}
library.include_paths
let mut target = if target_os == "macos" {
"x64-osx".to_owned()
} else if target_os == "windows" {
"x64-windows-static".to_owned()
} else {
format!("{}-{}", target_arch, target_os)
};
if target_arch == "x86" {
target = target.replace("x64", "x86");
}
println!("cargo:info={}", target);
path.push("installed");
path.push(target);
let lib = name.trim_start_matches("lib").to_string();
println!("{}", format!("cargo:rustc-link-lib=static={}", lib));
println!(
"{}",
format!(
"cargo:rustc-link-search={}",
path.join("lib").to_str().unwrap()
)
);
let include = path.join("include");
println!("{}", format!("cargo:include={}", include.to_str().unwrap()));
vec![include]
}
fn generate_bindings(
@ -94,8 +99,10 @@ fn main() {
// there is problem with cfg(target_os) in build.rs, so use our workaround
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
if target_os == "android" || target_os == "ios" {
if target_os == "ios" {
// nothing
} else if target_os == "android" {
println!("cargo:rustc-cfg=android");
} else if cfg!(windows) {
// The first choice is Windows because DXGI is amazing.
println!("cargo:rustc-cfg=dxgi");

View File

@ -0,0 +1,235 @@
use jni::objects::JByteBuffer;
use jni::objects::JString;
use jni::objects::JValue;
use jni::sys::jboolean;
use jni::JNIEnv;
use jni::{
objects::{GlobalRef, JClass, JObject},
JavaVM,
};
use jni::errors::{Error as JniError, Result as JniResult};
use lazy_static::lazy_static;
use std::ops::Not;
use std::sync::atomic::{AtomicPtr, Ordering::SeqCst};
use std::sync::{Mutex, RwLock};
use std::time::{Duration, Instant};
lazy_static! {
static ref JVM: RwLock<Option<JavaVM>> = RwLock::new(None);
static ref MAIN_SERVICE_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None); // MainService -> video service / audio service / info
static ref INPUT_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None);
static ref VIDEO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT));
static ref AUDIO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT));
}
const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100);
const MAX_AUDIO_FRAME_TIMEOUT: Duration = Duration::from_millis(1000);
struct FrameRaw {
name: &'static str,
ptr: AtomicPtr<u8>,
len: usize,
last_update: Instant,
timeout: Duration,
enable: bool,
}
impl FrameRaw {
fn new(name: &'static str, timeout: Duration) -> Self {
FrameRaw {
name,
ptr: AtomicPtr::default(),
len: 0,
last_update: Instant::now(),
timeout,
enable: false,
}
}
fn set_enable(&mut self, value: bool) {
self.enable = value;
}
fn update(&mut self, data: &mut [u8]) {
if self.enable.not() {
return;
}
self.len = data.len();
self.ptr.store(data.as_mut_ptr(), SeqCst);
self.last_update = Instant::now();
}
// take inner data as slice
// release when success
fn take<'a>(&mut self) -> Option<&'a [u8]> {
if self.enable.not() {
return None;
}
let ptr = self.ptr.load(SeqCst);
if ptr.is_null() || self.len == 0 {
None
} else {
if self.last_update.elapsed() > self.timeout {
log::trace!("Failed to take {} raw,timeout!", self.name);
return None;
}
let slice = unsafe { std::slice::from_raw_parts(ptr, self.len) };
self.release();
Some(slice)
}
}
fn release(&mut self) {
self.len = 0;
self.ptr.store(std::ptr::null_mut(), SeqCst);
}
}
pub fn get_video_raw<'a>() -> Option<&'a [u8]> {
VIDEO_RAW.lock().ok()?.take()
}
pub fn get_audio_raw<'a>() -> Option<&'a [u8]> {
AUDIO_RAW.lock().ok()?.take()
}
#[no_mangle]
pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onVideoFrameUpdate(
env: JNIEnv,
_class: JClass,
buffer: JObject,
) {
let jb = JByteBuffer::from(buffer);
let slice = env.get_direct_buffer_address(jb).unwrap();
VIDEO_RAW.lock().unwrap().update(slice);
}
#[no_mangle]
pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onAudioFrameUpdate(
env: JNIEnv,
_class: JClass,
buffer: JObject,
) {
let jb = JByteBuffer::from(buffer);
let slice = env.get_direct_buffer_address(jb).unwrap();
AUDIO_RAW.lock().unwrap().update(slice);
}
#[no_mangle]
pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_setFrameRawEnable(
env: JNIEnv,
_class: JClass,
name: JString,
value: jboolean,
) {
if let Ok(name) = env.get_string(name) {
let name: String = name.into();
let value = value.eq(&1);
if name.eq("video") {
VIDEO_RAW.lock().unwrap().set_enable(value);
} else if name.eq("audio") {
AUDIO_RAW.lock().unwrap().set_enable(value);
}
};
}
#[no_mangle]
pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_init(
env: JNIEnv,
_class: JClass,
ctx: JObject,
) {
log::debug!("MainService init from java");
let jvm = env.get_java_vm().unwrap();
*JVM.write().unwrap() = Some(jvm);
let context = env.new_global_ref(ctx).unwrap();
*MAIN_SERVICE_CTX.write().unwrap() = Some(context);
}
#[no_mangle]
pub extern "system" fn Java_com_carriez_flutter_1hbb_InputService_init(
env: JNIEnv,
_class: JClass,
ctx: JObject,
) {
log::debug!("InputService init from java");
let jvm = env.get_java_vm().unwrap();
*JVM.write().unwrap() = Some(jvm);
let context = env.new_global_ref(ctx).unwrap();
*INPUT_CTX.write().unwrap() = Some(context);
}
pub fn call_input_service_mouse_input(mask: i32, x: i32, y: i32) -> JniResult<()> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
INPUT_CTX.read().unwrap().as_ref(),
) {
let env = jvm.attach_current_thread_as_daemon()?;
env.call_method(
ctx,
"rustMouseInput",
"(III)V",
&[JValue::Int(mask), JValue::Int(x), JValue::Int(y)],
)?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));
}
}
pub fn call_main_service_get_by_name(name: &str) -> JniResult<String> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let env = jvm.attach_current_thread_as_daemon()?;
let name = env.new_string(name)?;
let res = env
.call_method(
ctx,
"rustGetByName",
"(Ljava/lang/String;)Ljava/lang/String;",
&[JValue::Object(name.into())],
)?
.l()?;
let res = env.get_string(res.into())?;
let res = res.to_string_lossy().to_string();
return Ok(res);
} else {
return Err(JniError::ThrowFailed(-1));
}
}
pub fn call_main_service_set_by_name(
name: &str,
arg1: Option<&str>,
arg2: Option<&str>,
) -> JniResult<()> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let env = jvm.attach_current_thread_as_daemon()?;
let name = env.new_string(name)?;
let arg1 = env.new_string(arg1.unwrap_or(""))?;
let arg2 = env.new_string(arg2.unwrap_or(""))?;
env.call_method(
ctx,
"rustSetByName",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
&[
JValue::Object(name.into()),
JValue::Object(arg1.into()),
JValue::Object(arg2.into()),
],
)?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));
}
}

View File

@ -0,0 +1,6 @@
pub mod ffi;
use std::sync::RwLock;
pub use ffi::*;
use lazy_static::lazy_static;

View File

@ -0,0 +1,129 @@
use crate::android::ffi::*;
use crate::rgba_to_i420;
use lazy_static::lazy_static;
use std::io;
use std::sync::Mutex;
lazy_static! {
static ref SCREEN_SIZE: Mutex<(u16, u16)> = Mutex::new((0, 0));
}
pub struct Capturer {
display: Display,
bgra: Vec<u8>,
saved_raw_data: Vec<u128>, // for faster compare and copy
}
impl Capturer {
pub fn new(display: Display, _yuv: bool) -> io::Result<Capturer> {
Ok(Capturer {
display,
bgra: Vec::new(),
saved_raw_data: Vec::new(),
})
}
pub fn width(&self) -> usize {
self.display.width() as usize
}
pub fn height(&self) -> usize {
self.display.height() as usize
}
pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result<Frame<'a>> {
if let Some(buf) = get_video_raw() {
crate::would_block_if_equal(&mut self.saved_raw_data, buf)?;
rgba_to_i420(self.width(), self.height(), buf, &mut self.bgra);
Ok(Frame::RAW(&self.bgra))
} else {
return Err(io::ErrorKind::WouldBlock.into());
}
}
}
pub enum Frame<'a> {
RAW(&'a [u8]),
VP9(&'a [u8]),
Empty,
}
pub struct Display {
default: bool,
rect: Rect,
}
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
struct Rect {
pub x: i16,
pub y: i16,
pub w: u16,
pub h: u16,
}
impl Display {
pub fn primary() -> io::Result<Display> {
let mut size = SCREEN_SIZE.lock().unwrap();
if size.0 == 0 || size.1 == 0 {
let (w, h) = get_size().unwrap_or((0, 0));
size.0 = w;
size.1 = h;
}
Ok(Display {
default: true,
rect: Rect {
x: 0,
y: 0,
w: size.0,
h: size.1,
},
})
}
pub fn all() -> io::Result<Vec<Display>> {
Ok(vec![Display::primary()?])
}
pub fn width(&self) -> usize {
self.rect.w as usize
}
pub fn height(&self) -> usize {
self.rect.h as usize
}
pub fn origin(&self) -> (i32, i32) {
let r = self.rect;
(r.x as _, r.y as _)
}
pub fn is_online(&self) -> bool {
true
}
pub fn is_primary(&self) -> bool {
self.default
}
pub fn name(&self) -> String {
"Android".into()
}
pub fn refresh_size() {
let mut size = SCREEN_SIZE.lock().unwrap();
let (w, h) = get_size().unwrap_or((0, 0));
size.0 = w;
size.1 = h;
}
}
fn get_size() -> Option<(u16, u16)> {
let res = call_main_service_get_by_name("screen_size").ok()?;
if res.len() > 0 {
let mut sp = res.split(":");
let w = sp.next()?.parse::<u16>().ok()?;
let h = sp.next()?.parse::<u16>().ok()?;
return Some((w, h));
}
None
}

View File

@ -19,7 +19,10 @@ cfg_if! {
} else if #[cfg(dxgi)] {
mod dxgi;
pub use self::dxgi::*;
} else {
} else if #[cfg(android)] {
mod android;
pub use self::android::*;
}else {
//TODO: Fallback implementation.
}
}
@ -42,4 +45,4 @@ pub fn would_block_if_equal(old: &mut Vec<u128>, b: &[u8]) -> std::io::Result<()
old.resize(b.len(), 0);
old.copy_from_slice(b);
Ok(())
}
}

View File

@ -20,4 +20,7 @@ pub mod wayland;
#[cfg(dxgi)]
pub mod dxgi;
#[cfg(android)]
pub mod android;
mod common;

View File

@ -79,14 +79,20 @@ impl Interface for Session {
}
#[tokio::main(flavor = "current_thread")]
pub async fn start_one_port_forward(id: String, port: i32, remote_host: String, remote_port: i32) {
pub async fn start_one_port_forward(
id: String,
port: i32,
remote_host: String,
remote_port: i32,
key: String,
) {
crate::common::test_rendezvous_server();
crate::common::test_nat_type();
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
let handler = Session::new(&id, sender);
handler.lc.write().unwrap().port_forward = (remote_host, remote_port);
if let Err(err) =
crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver).await
crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver, &key).await
{
log::error!("Failed to listen on {}: {}", port, err);
}

View File

@ -29,7 +29,8 @@ use std::{
sync::{mpsc, Arc, RwLock},
};
use uuid::Uuid;
pub mod file_trait;
pub use file_trait::FileManager;
pub const SEC30: Duration = Duration::from_secs(30);
pub struct Client;
@ -101,8 +102,13 @@ impl Drop for OboePlayer {
}
impl Client {
pub async fn start(peer: &str, conn_type: ConnType) -> ResultType<(Stream, bool)> {
match Self::_start(peer, conn_type).await {
pub async fn start(
peer: &str,
key: &str,
token: &str,
conn_type: ConnType,
) -> ResultType<(Stream, bool)> {
match Self::_start(peer, key, token, conn_type).await {
Err(err) => {
let err_str = err.to_string();
if err_str.starts_with("Failed") {
@ -115,7 +121,12 @@ impl Client {
}
}
async fn _start(peer: &str, conn_type: ConnType) -> ResultType<(Stream, bool)> {
async fn _start(
peer: &str,
key: &str,
token: &str,
conn_type: ConnType,
) -> ResultType<(Stream, bool)> {
// to-do: remember the port for each peer, so that we can retry easier
let any_addr = Config::get_any_listen_addr();
if crate::is_ip(peer) {
@ -150,7 +161,9 @@ impl Client {
let nat_type = NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT);
msg_out.set_punch_hole_request(PunchHoleRequest {
id: peer.to_owned(),
token: token.to_owned(),
nat_type: nat_type.into(),
licence_key: key.to_owned(),
conn_type: conn_type.into(),
..Default::default()
});
@ -195,9 +208,9 @@ impl Client {
);
signed_id_pk = rr.get_pk().into();
let mut conn =
Self::create_relay(peer, rr.uuid, rr.relay_server, conn_type)
Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type)
.await?;
Self::secure_connection(peer, signed_id_pk, &mut conn).await?;
Self::secure_connection(peer, signed_id_pk, key, &mut conn).await?;
return Ok((conn, false));
}
_ => {
@ -235,6 +248,8 @@ impl Client {
peer_nat_type,
my_nat_type,
is_local,
key,
token,
conn_type,
)
.await
@ -251,6 +266,8 @@ impl Client {
peer_nat_type: NatType,
my_nat_type: i32,
is_local: bool,
key: &str,
token: &str,
conn_type: ConnType,
) -> ResultType<(Stream, bool)> {
let direct_failures = PeerConfig::load(peer_id).direct_failures;
@ -297,6 +314,8 @@ impl Client {
relay_server.to_owned(),
rendezvous_server,
!signed_id_pk.is_empty(),
key,
token,
conn_type,
)
.await;
@ -318,12 +337,21 @@ impl Client {
}
let mut conn = conn?;
log::info!("{:?} used to establish connection", start.elapsed());
Self::secure_connection(peer_id, signed_id_pk, &mut conn).await?;
Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await?;
Ok((conn, direct))
}
async fn secure_connection(peer_id: &str, signed_id_pk: Vec<u8>, conn: &mut Stream) -> ResultType<()> {
let rs_pk = get_rs_pk("OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=");
async fn secure_connection(
peer_id: &str,
signed_id_pk: Vec<u8>,
key: &str,
conn: &mut Stream,
) -> ResultType<()> {
let rs_pk = get_rs_pk(if key.is_empty() {
"OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="
} else {
key
});
let mut sign_pk = None;
if !signed_id_pk.is_empty() && rs_pk.is_some() {
if let Ok((id, pk)) = decode_id_pk(&signed_id_pk, &rs_pk.unwrap()) {
@ -395,6 +423,8 @@ impl Client {
relay_server: String,
rendezvous_server: &str,
secure: bool,
key: &str,
token: &str,
conn_type: ConnType,
) -> ResultType<Stream> {
let any_addr = Config::get_any_listen_addr();
@ -419,6 +449,7 @@ impl Client {
);
msg_out.set_request_relay(RequestRelay {
id: peer.to_owned(),
token: token.to_owned(),
uuid: uuid.clone(),
relay_server: relay_server.clone(),
secure,
@ -440,13 +471,14 @@ impl Client {
if !succeed {
bail!("Timeout");
}
Self::create_relay(peer, uuid, relay_server, conn_type).await
Self::create_relay(peer, uuid, relay_server, key, conn_type).await
}
async fn create_relay(
peer: &str,
uuid: String,
relay_server: String,
key: &str,
conn_type: ConnType,
) -> ResultType<Stream> {
let mut conn = socket_client::connect_tcp(
@ -458,6 +490,7 @@ impl Client {
.with_context(|| "Failed to connect to relay server")?;
let mut msg_out = RendezvousMessage::new();
msg_out.set_request_relay(RequestRelay {
licence_key: key.to_owned(),
id: peer.to_owned(),
uuid,
conn_type: conn_type.into(),
@ -498,11 +531,10 @@ impl AudioHandler {
if !spec.is_valid() {
bail!("Invalid audio format");
}
use hbb_common::config::APP_NAME;
self.simple = Some(Simple::new(
None, // Use the default server
APP_NAME, // Our applications name
&crate::get_app_name(), // Our applications name
Direction::Playback, // We want a playback stream
None, // Use the default device
"playback", // Description of our stream
@ -693,7 +725,7 @@ impl VideoHandler {
#[derive(Default)]
pub struct LoginConfigHandler {
id: String,
is_file_transfer: bool,
pub is_file_transfer: bool,
is_port_forward: bool,
hash: Hash,
password: Vec<u8>, // remember password for reconnect
@ -701,6 +733,7 @@ pub struct LoginConfigHandler {
config: PeerConfig,
pub port_forward: (String, i32),
pub version: i64,
pub conn_id: i32,
}
impl Deref for LoginConfigHandler {
@ -726,6 +759,17 @@ impl LoginConfigHandler {
self.config = config;
}
pub fn should_auto_login(&self) -> String {
let l = self.lock_after_session_end;
let a = !self.get_option("auto-login").is_empty();
let p = self.get_option("os-password");
if !p.is_empty() && l && a {
p
} else {
"".to_owned()
}
}
fn load_config(&self) -> PeerConfig {
load_config(&self.id)
}
@ -1002,11 +1046,12 @@ impl LoginConfigHandler {
log::debug!("remove password of {}", self.id);
}
}
self.conn_id = pi.conn_id;
// no matter if change, for update file time
self.save_config(config);
}
fn get_remote_dir(&self) -> String {
pub fn get_remote_dir(&self) -> String {
serde_json::from_str::<HashMap<String, String>>(&self.get_option("remote_dir"))
.unwrap_or_default()
.remove(&self.info.username)
@ -1027,7 +1072,7 @@ impl LoginConfigHandler {
fn create_login_msg(&self, password: Vec<u8>) -> Message {
#[cfg(any(target_os = "android", target_os = "ios"))]
let my_id = crate::common::MOBILE_INFO1.lock().unwrap().clone();
let my_id = Config::get_id_or(crate::common::MOBILE_INFO1.lock().unwrap().clone());
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let my_id = Config::get_id();
let mut lr = LoginRequest {
@ -1127,6 +1172,83 @@ pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) {
}
}
// mask = buttons << 3 | type
// type, 1: down, 2: up, 3: wheel
// buttons, 1: left, 2: right, 4: middle
#[inline]
pub fn send_mouse(
mask: i32,
x: i32,
y: i32,
alt: bool,
ctrl: bool,
shift: bool,
command: bool,
interface: &impl Interface,
) {
let mut msg_out = Message::new();
let mut mouse_event = MouseEvent {
mask,
x,
y,
..Default::default()
};
if alt {
mouse_event.modifiers.push(ControlKey::Alt.into());
}
if shift {
mouse_event.modifiers.push(ControlKey::Shift.into());
}
if ctrl {
mouse_event.modifiers.push(ControlKey::Control.into());
}
if command {
mouse_event.modifiers.push(ControlKey::Meta.into());
}
msg_out.set_mouse_event(mouse_event);
interface.send(Data::Message(msg_out));
}
fn activate_os(interface: &impl Interface) {
send_mouse(0, 0, 0, false, false, false, false, interface);
std::thread::sleep(Duration::from_millis(50));
send_mouse(0, 3, 3, false, false, false, false, interface);
std::thread::sleep(Duration::from_millis(50));
send_mouse(1 | 1 << 3, 0, 0, false, false, false, false, interface);
send_mouse(2 | 1 << 3, 0, 0, false, false, false, false, interface);
/*
let mut key_event = KeyEvent::new();
// do not use Esc, which has problem with Linux
key_event.set_control_key(ControlKey::RightArrow);
key_event.press = true;
let mut msg_out = Message::new();
msg_out.set_key_event(key_event.clone());
interface.send(Data::Message(msg_out.clone()));
*/
}
pub fn input_os_password(p: String, activate: bool, interface: impl Interface) {
std::thread::spawn(move || {
_input_os_password(p, activate, interface);
});
}
fn _input_os_password(p: String, activate: bool, interface: impl Interface) {
if activate {
activate_os(&interface);
std::thread::sleep(Duration::from_millis(1200));
}
let mut key_event = KeyEvent::new();
key_event.press = true;
let mut msg_out = Message::new();
key_event.set_seq(p);
msg_out.set_key_event(key_event.clone());
interface.send(Data::Message(msg_out.clone()));
key_event.set_control_key(ControlKey::Return);
msg_out.set_key_event(key_event);
interface.send(Data::Message(msg_out));
}
pub async fn handle_hash(
lc: Arc<RwLock<LoginConfigHandler>>,
hash: Hash,
@ -1175,6 +1297,7 @@ pub async fn handle_login_from_ui(
#[async_trait]
pub trait Interface: Send + Clone + 'static + Sized {
fn send(&self, data: Data);
fn msgbox(&self, msgtype: &str, title: &str, text: &str);
fn handle_login_error(&mut self, err: &str) -> bool;
fn handle_peer_info(&mut self, pi: PeerInfo);

88
src/client/file_trait.rs Normal file
View File

@ -0,0 +1,88 @@
use super::{Data, Interface};
use hbb_common::{
fs,
message_proto::*,
};
pub trait FileManager: Interface {
fn get_home_dir(&self) -> String{
fs::get_home_as_string()
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn read_dir(&self,path: String, include_hidden: bool) -> sciter::Value {
match fs::read_dir(&fs::get_path(&path), include_hidden) {
Err(_) => sciter::Value::null(),
Ok(fd) => {
use crate::ui::remote::make_fd;
let mut m = make_fd(0, &fd.entries.to_vec(), false);
m.set_item("path", path);
m
}
}
}
#[cfg(any(target_os = "android", target_os = "ios"))]
fn read_dir(&self,path: &str, include_hidden: bool) -> String {
use crate::mobile::make_fd_to_json;
match fs::read_dir(&fs::get_path(path), include_hidden){
Ok(fd) => make_fd_to_json(fd),
Err(_)=>"".into()
}
}
fn cancel_job(&mut self, id: i32) {
self.send(Data::CancelJob(id));
}
fn read_remote_dir(&self, path: String, include_hidden: bool) {
let mut msg_out = Message::new();
let mut file_action = FileAction::new();
file_action.set_read_dir(ReadDir {
path,
include_hidden,
..Default::default()
});
msg_out.set_file_action(file_action);
self.send(Data::Message(msg_out));
}
fn remove_file(&mut self, id: i32, path: String, file_num: i32, is_remote: bool) {
self.send(Data::RemoveFile((id, path, file_num, is_remote)));
}
fn remove_dir_all(&mut self, id: i32, path: String, is_remote: bool) {
self.send(Data::RemoveDirAll((id, path, is_remote)));
}
fn confirm_delete_files(&mut self, id: i32, file_num: i32) {
self.send(Data::ConfirmDeleteFiles((id, file_num)));
}
fn set_no_confirm(&mut self, id: i32) {
self.send(Data::SetNoConfirm(id));
}
fn remove_dir(&mut self, id: i32, path: String, is_remote: bool) {
if is_remote {
self.send(Data::RemoveDir((id, path)));
} else {
fs::remove_all_empty_dir(&fs::get_path(&path)).ok();
}
}
fn create_dir(&mut self, id: i32, path: String, is_remote: bool) {
self.send(Data::CreateDir((id, path, is_remote)));
}
fn send_files(
&mut self,
id: i32,
path: String,
to: String,
include_hidden: bool,
is_remote: bool,
) {
self.send(Data::SendFiles((id, path, to, include_hidden, is_remote)));
}
}

View File

@ -1,9 +1,10 @@
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use arboard::Clipboard as ClipboardContext;
use hbb_common::{
allow_err,
anyhow::bail,
compress::{compress as compress_func, decompress},
config::{Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT},
config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT},
get_version_number, log,
message_proto::*,
protobuf::Message as _,
@ -54,6 +55,7 @@ pub fn create_clipboard_msg(content: String) -> Message {
msg
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn check_clipboard(
ctx: &mut ClipboardContext,
old: Option<&Arc<Mutex<String>>>,
@ -73,6 +75,7 @@ pub fn check_clipboard(
None
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc<Mutex<String>>>) {
let content = if clipboard.compress {
decompress(&clipboard.content)
@ -80,15 +83,16 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc<Mutex<String>>>)
clipboard.content
};
if let Ok(content) = String::from_utf8(content) {
if content.is_empty() {
// ctx.set_text may crash if content is empty
return;
}
match ClipboardContext::new() {
Ok(mut ctx) => {
let side = if old.is_none() { "host" } else { "client" };
let old = if let Some(old) = old { old } else { &CONTENT };
*old.lock().unwrap() = content.clone();
if !content.is_empty() {
// empty content make ctx.set_text crash
allow_err!(ctx.set_text(content));
}
allow_err!(ctx.set_text(content));
log::debug!("{} updated on {}", CLIPBOARD_NAME, side);
}
Err(err) => {
@ -234,7 +238,10 @@ pub fn test_nat_type() {
#[tokio::main(flavor = "current_thread")]
async fn test_nat_type_() -> ResultType<bool> {
log::info!("Testing nat ...");
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let is_direct = crate::ipc::get_socks_async(1_000).await.is_none(); // sync socks BTW
#[cfg(any(target_os = "android", target_os = "ios"))]
let is_direct = Config::get_socks().is_none(); // sync socks BTW
if !is_direct {
Config::set_nat_type(NatType::SYMMETRIC as _);
return Ok(true);
@ -451,12 +458,21 @@ async fn _check_software_update() -> hbb_common::ResultType<()> {
Ok(())
}
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
pub fn get_icon() -> String {
hbb_common::config::ICON.to_owned()
}
pub fn get_app_name() -> String {
hbb_common::config::APP_NAME.read().unwrap().clone()
}
#[cfg(target_os = "macos")]
pub fn get_full_name() -> String {
format!(
"{}.{}",
hbb_common::config::ORG,
hbb_common::config::APP_NAME,
hbb_common::config::ORG.read().unwrap(),
hbb_common::config::APP_NAME.read().unwrap(),
)
}
@ -466,7 +482,115 @@ pub fn is_ip(id: &str) -> bool {
.is_match(id)
}
#[inline]
pub fn get_app_name() -> &'static str {
hbb_common::config::APP_NAME
pub fn is_setup(name: &str) -> bool {
name.to_lowercase().ends_with("putes.exe") || name.to_lowercase().ends_with("安装.exe")
}
pub fn get_uuid() -> Vec<u8> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Ok(id) = machine_uid::get() {
return id.into();
}
Config::get_key_pair().1
}
pub fn get_custom_rendezvous_server(custom: String) -> String {
if !custom.is_empty() {
return custom;
}
#[cfg(windows)]
if let Some(lic) = crate::platform::windows::get_license() {
if !lic.host.is_empty() {
return lic.host.clone();
}
}
if !config::PROD_RENDEZVOUS_SERVER.read().unwrap().is_empty() {
return config::PROD_RENDEZVOUS_SERVER.read().unwrap().clone();
}
"".to_owned()
}
pub fn get_api_server(api: String, custom: String) -> String {
if !api.is_empty() {
return api.to_owned();
}
#[cfg(windows)]
if let Some(lic) = crate::platform::windows::get_license() {
if !lic.api.is_empty() {
return lic.api.clone();
}
}
let s = get_custom_rendezvous_server(custom);
if !s.is_empty() {
if s.contains(':') {
let tmp: Vec<&str> = s.split(":").collect();
if tmp.len() == 2 {
let port: u16 = tmp[1].parse().unwrap_or(0);
if port > 2 {
return format!("http://{}:{}", tmp[0], port - 2);
}
}
} else {
return format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2);
}
}
"https://admin.rustdesk.com".to_owned()
}
pub fn get_audit_server(api: String, custom: String) -> String {
let url = get_api_server(api, custom);
if url.is_empty() || url.contains("rustdesk.com") {
return "".to_owned();
}
format!("{}/api/audit", url)
}
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
#[cfg(not(target_os = "linux"))]
{
let mut req = reqwest::Client::new().post(url);
if !header.is_empty() {
let tmp: Vec<&str> = header.split(": ").collect();
if tmp.len() == 2 {
req = req.header(tmp[0], tmp[1]);
}
}
req = req.header("Content-Type", "application/json");
let to = std::time::Duration::from_secs(12);
Ok(req.body(body).timeout(to).send().await?.text().await?)
}
#[cfg(target_os = "linux")]
{
let mut data = vec![
"curl",
"-sS",
"-X",
"POST",
&url,
"-H",
"Content-Type: application/json",
"-d",
&body,
"--connect-timeout",
"12",
];
if !header.is_empty() {
data.push("-H");
data.push(header);
}
let output = async_process::Command::new("curl")
.args(&data)
.output()
.await?;
let res = String::from_utf8_lossy(&output.stdout).to_string();
if !res.is_empty() {
return Ok(res);
}
bail!(String::from_utf8_lossy(&output.stderr).to_string());
}
}
#[tokio::main(flavor = "current_thread")]
pub async fn post_request_sync(url: String, body: String, header: &str) -> ResultType<String> {
post_request(url, body, header).await
}

View File

@ -1,4 +1,5 @@
use crate::rendezvous_mediator::RendezvousMediator;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use clipboard::ClipbaordFile;
use hbb_common::{
allow_err, bail, bytes,
@ -15,7 +16,7 @@ use parity_tokio_ipc::{
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
use std::{collections::HashMap, sync::atomic::Ordering};
#[cfg(not(windows))]
use std::{fs::File, io::prelude::*};
@ -84,6 +85,8 @@ pub enum Data {
enabled: bool,
},
SystemInfo(Option<String>),
ClickTime(i64),
MouseMoveTime(i64),
Authorize,
Close,
SAS,
@ -201,8 +204,17 @@ async fn handle(data: Data, stream: &mut Connection) {
);
allow_err!(stream.send(&Data::SystemInfo(Some(info))).await);
}
Data::ClickTime(_) => {
let t = crate::server::CLICK_TIME.load(Ordering::SeqCst);
allow_err!(stream.send(&Data::ClickTime(t)).await);
}
Data::MouseMoveTime(_) => {
let t = crate::server::MOUSE_MOVE_TIME.load(Ordering::SeqCst);
allow_err!(stream.send(&Data::MouseMoveTime(t)).await);
}
Data::Close => {
log::info!("Receive close message");
#[cfg(not(target_os = "android"))]
crate::server::input_service::fix_key_down_timeout_at_exit();
std::process::exit(0);
}
@ -442,13 +454,17 @@ async fn get_config_async(name: &str, ms_timeout: u64) -> ResultType<Option<Stri
return Ok(None);
}
#[tokio::main(flavor = "current_thread")]
async fn set_config(name: &str, value: String) -> ResultType<()> {
pub async fn set_config_async(name: &str, value: String) -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send_config(name, value).await?;
Ok(())
}
#[tokio::main(flavor = "current_thread")]
pub async fn set_config(name: &str, value: String) -> ResultType<()> {
set_config_async(name, value).await
}
pub fn set_password(v: String) -> ResultType<()> {
Config::set_password(&v);
set_config("password", v)
@ -498,13 +514,17 @@ async fn get_options_(ms_timeout: u64) -> ResultType<HashMap<String, String>> {
}
}
#[tokio::main(flavor = "current_thread")]
pub async fn get_options() -> HashMap<String, String> {
pub async fn get_options_async() -> HashMap<String, String> {
get_options_(1000).await.unwrap_or(Config::get_options())
}
pub fn get_option(key: &str) -> String {
if let Some(v) = get_options().get(key) {
#[tokio::main(flavor = "current_thread")]
pub async fn get_options() -> HashMap<String, String> {
get_options_async().await
}
pub async fn get_option_async(key: &str) -> String {
if let Some(v) = get_options_async().await.get(key) {
v.clone()
} else {
"".to_owned()
@ -550,6 +570,13 @@ pub async fn get_nat_type(ms_timeout: u64) -> i32 {
.unwrap_or(Config::get_nat_type())
}
pub async fn get_rendezvous_servers(ms_timeout: u64) -> Vec<String> {
if let Ok(Some(v)) = get_config_async("rendezvous_servers", ms_timeout).await {
return v.split(',').map(|x| x.to_owned()).collect();
}
return Config::get_rendezvous_servers();
}
#[inline]
async fn get_socks_(ms_timeout: u64) -> ResultType<Option<config::Socks5Server>> {
let mut c = connect(ms_timeout, "").await?;
@ -584,63 +611,3 @@ pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> {
.await?;
Ok(())
}
/*
static mut SHARED_MEMORY: *mut i64 = std::ptr::null_mut();
pub fn initialize_shared_memory(create: bool) {
let mut shmem_flink = "shared-memory".to_owned();
if cfg!(windows) {
let df = "C:\\ProgramData";
let df = if std::path::Path::new(df).exists() {
df.to_owned()
} else {
std::env::var("TEMP").unwrap_or("C:\\Windows\\TEMP".to_owned())
};
let df = format!("{}\\{}", df, *hbb_common::config::APP_NAME.read().unwrap());
std::fs::create_dir(&df).ok();
shmem_flink = format!("{}\\{}", df, shmem_flink);
} else {
shmem_flink = Config::ipc_path("").replace("ipc", "") + &shmem_flink;
}
use shared_memory::*;
let shmem = if create {
match ShmemConf::new()
.force_create_flink()
.size(16)
.flink(&shmem_flink)
.create()
{
Err(ShmemError::LinkExists) => ShmemConf::new().flink(&shmem_flink).open(),
Ok(m) => Ok(m),
Err(e) => Err(e),
}
} else {
ShmemConf::new().flink(&shmem_flink).open()
};
if create {
set_all_perm(&shmem_flink);
}
match shmem {
Ok(shmem) => unsafe {
SHARED_MEMORY = shmem.as_ptr() as *mut i64;
std::mem::forget(shmem);
},
Err(err) => {
log::error!(
"Unable to create or open shmem flink {} : {}",
shmem_flink,
err
);
}
}
}
fn set_all_perm(p: &str) {
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(p, std::fs::Permissions::from_mode(0o0777)).ok();
}
}
*/

View File

@ -1,4 +1,3 @@
use hbb_common::{config::LocalConfig, log};
use std::ops::Deref;
mod cn;
@ -15,12 +14,17 @@ mod id;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String {
let locale = sys_locale::get_locale().unwrap_or_default().to_lowercase();
log::trace!("The current locale is {}", locale);
translate_locale(name, &locale)
}
pub fn translate_locale(name: String, locale: &str) -> String {
let mut lang = LocalConfig::get_option("lang");
let mut lang = hbb_common::config::LocalConfig::get_option("lang").to_lowercase();
if lang.is_empty() {
// zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android
if locale.starts_with("zh") && (locale.ends_with("CN") || locale.ends_with("SG") || locale.ends_with("Hans")) {
lang = "cn".to_owned();
}
}
if lang.is_empty() {
lang = locale
.split("-")
@ -38,10 +42,10 @@ pub fn translate_locale(name: String, locale: &str) -> String {
"de" => de::T.deref(),
"ru" => ru::T.deref(),
"eo" => eo::T.deref(),
"id" => id::T.deref(),
"ptbr" => ptbr::T.deref(),
"br" => ptbr::T.deref(),
"pt" => ptbr::T.deref(),
"id" => id::T.deref(),
_ => en::T.deref(),
};
if let Some(v) = m.get(&name as &str) {

View File

@ -265,7 +265,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_version_audio_tip", ""),
("android_start_service_tip", ""),
("Account", ""),
("Quit", ""),
("Help", ""),
].iter().cloned().collect();
}

View File

@ -1,33 +1,39 @@
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub mod platform;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mod server;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use self::server::*;
mod client;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mod rendezvous_mediator;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use self::rendezvous_mediator::*;
pub mod common;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub mod ipc;
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
pub mod ui;
mod version;
pub use version::*;
#[cfg(any(target_os = "android", target_os = "ios"))]
pub mod mobile;
#[cfg(any(target_os = "android", target_os = "ios"))]
pub mod mobile_ffi;
use common::*;
#[cfg(feature = "cli")]
pub mod cli;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mod port_forward;
mod lang;
#[cfg(windows)]
pub mod clipboard_file;
#[cfg(not(any(target_os = "ios")))]
pub mod platform;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service};
#[cfg(not(any(target_os = "ios")))]
mod server;
#[cfg(not(any(target_os = "ios")))]
pub use self::server::*;
mod client;
#[cfg(not(any(target_os = "ios")))]
mod rendezvous_mediator;
#[cfg(not(any(target_os = "ios")))]
pub use self::rendezvous_mediator::*;
pub mod common;
#[cfg(not(any( target_os = "ios")))]
pub mod ipc;
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
pub mod ui;
mod version;
pub use version::*;
#[cfg(any(target_os = "android", target_os = "ios"))]
pub mod mobile;
#[cfg(any(target_os = "android", target_os = "ios"))]
pub mod mobile_ffi;
use common::*;
#[cfg(feature = "cli")]
pub mod cli;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mod port_forward;
#[cfg(all(windows, feature = "hbbs"))]
mod hbbs;
#[cfg(windows)]
mod license;
#[cfg(windows)]
mod tray;
mod lang;
#[cfg(windows)]
pub mod clipboard_file;

46
src/lic_main.rs Normal file
View File

@ -0,0 +1,46 @@
mod license;
use hbb_common::{sodiumoxide::crypto::sign, ResultType};
use license::*;
fn gen_license(lic: &License) -> ResultType<String> {
let tmp = serde_json::to_vec::<License>(lic)?;
const SK: &[u8; 64] = &[
139, 164, 88, 86, 6, 123, 221, 248, 96, 36, 106, 207, 99, 124, 27, 196, 5, 159, 58, 253,
238, 94, 3, 184, 237, 236, 122, 59, 205, 95, 6, 189, 88, 168, 68, 104, 60, 5, 163, 198,
165, 38, 12, 85, 114, 203, 96, 163, 70, 48, 0, 131, 57, 12, 46, 129, 83, 17, 84, 193, 119,
197, 130, 103,
];
let sk = sign::SecretKey(*SK);
let tmp = base64::encode_config(sign::sign(&tmp, &sk), base64::URL_SAFE_NO_PAD);
let tmp: String = tmp.chars().rev().collect();
Ok(tmp)
}
fn main() {
let mut args = Vec::new();
let mut i = 0;
for arg in std::env::args() {
if i > 0 {
args.push(arg);
}
i += 1;
}
let api = if args.len() < 3 {
"".to_owned()
} else {
args[2].clone()
};
if args.len() == 3 {
println!(
"{:?}",
gen_license(&License {
key: args[0].clone(),
host: args[1].clone(),
api,
})
);
}
if args.len() == 1 {
println!("{:?}", get_license_from_string(&args[0]));
}
}

30
src/license.rs Normal file
View File

@ -0,0 +1,30 @@
use hbb_common::{bail, sodiumoxide::crypto::sign, ResultType};
use serde_derive::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)]
pub struct License {
#[serde(default)]
pub key: String,
#[serde(default)]
pub host: String,
#[serde(default)]
pub api: String,
}
pub fn get_license_from_string(s: &str) -> ResultType<License> {
let tmp: String = s.chars().rev().collect();
const PK: &[u8; 32] = &[
88, 168, 68, 104, 60, 5, 163, 198, 165, 38, 12, 85, 114, 203, 96, 163, 70, 48, 0, 131, 57,
12, 46, 129, 83, 17, 84, 193, 119, 197, 130, 103,
];
let pk = sign::PublicKey(*PK);
let data = base64::decode_config(tmp, base64::URL_SAFE_NO_PAD)?;
if let Ok(lic) = serde_json::from_slice::<License>(&data) {
return Ok(lic);
}
if let Ok(data) = sign::verify(&data, &pk) {
Ok(serde_json::from_slice::<License>(&data)?)
} else {
bail!("sign:verify failed");
}
}

View File

@ -3,7 +3,7 @@
//#![windows_subsystem = "windows"]
use hbb_common::log;
use rustdesk::*;
use librustdesk::*;
#[cfg(any(target_os = "android", target_os = "ios"))]
fn main() {
@ -11,24 +11,40 @@ fn main() {
common::test_nat_type();
#[cfg(target_os = "android")]
crate::common::check_software_update();
mobile::Session::start("");
}
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
fn main() {
// https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write
let mut _async_logger_holder: Option<flexi_logger::LoggerHandle> = None;
let mut args: Vec<String> = std::env::args().skip(1).collect();
let mut args = Vec::new();
let mut i = 0;
let mut is_setup = false;
for arg in std::env::args() {
if i == 0 && common::is_setup(&arg) {
is_setup = true;
} else if i > 0 {
args.push(arg);
}
i += 1;
}
if is_setup {
if args.is_empty() {
args.push("--install".to_owned());
} else if args[0] == "--noinstall" {
args.clear();
}
}
if args.len() > 0 && args[0] == "--version" {
println!("{}", crate::VERSION);
return;
}
#[cfg(not(feature = "inline"))]
#[cfg(feature = "inline")]
{
use hbb_common::env_logger::*;
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
}
#[cfg(feature = "inline")]
#[cfg(not(feature = "inline"))]
{
let mut path = hbb_common::config::Config::log_path();
if args.len() > 0 && args[0].starts_with("--") {
@ -53,7 +69,7 @@ fn main() {
}
}
if args.is_empty() {
std::thread::spawn(move || start_server(false, false));
std::thread::spawn(move || start_server(false));
} else {
#[cfg(windows)]
{
@ -62,12 +78,31 @@ fn main() {
log::error!("Failed to uninstall: {}", err);
}
return;
} else if args[0] == "--after-install" {
if let Err(err) = platform::run_after_install() {
log::error!("Failed to after-install: {}", err);
}
return;
} else if args[0] == "--before-uninstall" {
if let Err(err) = platform::run_before_uninstall() {
log::error!("Failed to before-uninstall: {}", err);
}
return;
} else if args[0] == "--update" {
hbb_common::allow_err!(platform::update_me());
return;
} else if args[0] == "--reinstall" {
hbb_common::allow_err!(platform::uninstall_me());
hbb_common::allow_err!(platform::install_me("desktopicon startmenu",));
hbb_common::allow_err!(platform::install_me(
"desktopicon startmenu",
"".to_owned()
));
return;
} else if args[0] == "--silent-install" {
hbb_common::allow_err!(platform::install_me(
"desktopicon startmenu",
"".to_owned()
));
return;
}
}
@ -86,12 +121,12 @@ fn main() {
log::info!("start --server");
#[cfg(not(target_os = "macos"))]
{
start_server(true, true);
start_server(true);
return;
}
#[cfg(target_os = "macos")]
{
std::thread::spawn(move || start_server(true, true));
std::thread::spawn(move || start_server(true));
}
} else if args[0] == "--import-config" {
if args.len() == 2 {
@ -138,6 +173,7 @@ fn main() {
use clap::App;
let args = format!(
"-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]'
-k, --key=[KEY] ''
-s, --server... 'Start server'",
);
let matches = App::new("rustdesk")
@ -172,6 +208,7 @@ fn main() {
if options.len() > 3 {
remote_host = options[3].clone();
}
cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port);
let key = matches.value_of("key").unwrap_or("").to_owned();
cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key);
}
}

1306
src/mobile.rs Normal file

File diff suppressed because it is too large Load Diff

561
src/mobile_ffi.rs Normal file
View File

@ -0,0 +1,561 @@
use crate::client::file_trait::FileManager;
#[cfg(target_os = "android")]
use crate::mobile::connection_manager::{self, get_clients_length, get_clients_state};
use crate::mobile::{make_fd_to_json, Session};
use hbb_common::{
config::{self, Config, PeerConfig, ONLINE, LocalConfig},
fs, log,
};
use serde_json::{Number, Value};
use std::{
collections::HashMap,
ffi::{CStr, CString},
os::raw::c_char,
};
fn initialize(app_dir: &str) {
*config::APP_DIR.write().unwrap() = app_dir.to_owned();
#[cfg(target_os = "android")]
{
android_logger::init_once(
android_logger::Config::default()
.with_min_level(log::Level::Debug) // limit log level
.with_tag("ffi"), // logs will show under mytag tag
);
}
#[cfg(target_os = "ios")]
{
use hbb_common::env_logger::*;
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug"));
}
crate::common::test_rendezvous_server();
crate::common::test_nat_type();
#[cfg(target_os = "android")]
crate::common::check_software_update();
}
#[no_mangle]
unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *const c_char {
let mut res = "".to_owned();
let arg: &CStr = CStr::from_ptr(arg);
let name: &CStr = CStr::from_ptr(name);
if let Ok(name) = name.to_str() {
match name {
"peers" => {
if !config::APP_DIR.read().unwrap().is_empty() {
let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers()
.drain(..)
.map(|(id, _, p)| (id, p.info))
.collect();
res = serde_json::ser::to_string(&peers).unwrap_or("".to_owned());
}
}
"remote_id" => {
if !config::APP_DIR.read().unwrap().is_empty() {
res = LocalConfig::get_remote_id();
}
}
"remember" => {
res = Session::get_remember().to_string();
}
"event" => {
if let Some(e) = Session::pop_event() {
res = e;
}
}
"toggle_option" => {
if let Ok(arg) = arg.to_str() {
if let Some(v) = Session::get_toggle_option(arg) {
res = v.to_string();
}
}
}
"test_if_valid_server" => {
if let Ok(arg) = arg.to_str() {
res = hbb_common::socket_client::test_if_valid_server(arg);
}
}
"option" => {
if let Ok(arg) = arg.to_str() {
res = Config::get_option(arg);
}
}
"image_quality" => {
res = Session::get_image_quality();
}
"software_update_url" => {
res = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone()
}
"translate" => {
if let Ok(arg) = arg.to_str() {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(arg) {
if let Some(locale) = m.get("locale") {
if let Some(text) = m.get("text") {
res = crate::client::translate_locale(text.to_owned(), locale);
}
}
}
}
}
"peer_option" => {
if let Ok(arg) = arg.to_str() {
res = Session::get_option(arg);
}
}
"server_id" => {
res = Config::get_id();
}
"server_password" => {
res = Config::get_password();
}
"connect_statue" => {
res = ONLINE
.lock()
.unwrap()
.values()
.max()
.unwrap_or(&0)
.clone()
.to_string();
}
// File Action
"get_home_dir" => {
res = fs::get_home_as_string();
}
"read_local_dir_sync" => {
if let Ok(value) = arg.to_str() {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let (Some(path), Some(show_hidden)) =
(m.get("path"), m.get("show_hidden"))
{
if let Ok(fd) =
fs::read_dir(&fs::get_path(path), show_hidden.eq("true"))
{
res = make_fd_to_json(fd);
}
}
}
}
}
// Server Side
#[cfg(target_os = "android")]
"clients_state" => {
res = get_clients_state();
}
#[cfg(target_os = "android")]
"check_clients_length" => {
if let Ok(value) = arg.to_str() {
if value.parse::<usize>().unwrap_or(usize::MAX) != get_clients_length() {
res = get_clients_state()
}
}
}
"uuid" => {
res = base64::encode(crate::get_uuid());
}
_ => {
log::error!("Unknown name of get_by_name: {}", name);
}
}
}
CString::from_vec_unchecked(res.into_bytes()).into_raw()
}
#[no_mangle]
unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) {
let value: &CStr = CStr::from_ptr(value);
if let Ok(value) = value.to_str() {
let name: &CStr = CStr::from_ptr(name);
if let Ok(name) = name.to_str() {
match name {
"init" => {
initialize(value);
}
"info1" => {
*crate::common::MOBILE_INFO1.lock().unwrap() = value.to_owned();
}
"info2" => {
*crate::common::MOBILE_INFO2.lock().unwrap() = value.to_owned();
}
"connect" => {
Session::start(value, false);
}
"connect_file_transfer" => {
Session::start(value, true);
}
"login" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let Some(password) = m.get("password") {
if let Some(remember) = m.get("remember") {
Session::login(password, remember == "true");
}
}
}
}
"close" => {
Session::close();
}
"refresh" => {
Session::refresh();
}
"reconnect" => {
Session::reconnect();
}
"toggle_option" => {
Session::toggle_option(value);
}
"image_quality" => {
Session::set_image_quality(value);
}
"lock_screen" => {
Session::lock_screen();
}
"ctrl_alt_del" => {
Session::ctrl_alt_del();
}
"switch_display" => {
if let Ok(v) = value.parse::<i32>() {
Session::switch_display(v);
}
}
"remove" => {
PeerConfig::remove(value);
}
"input_key" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
let alt = m.get("alt").is_some();
let ctrl = m.get("ctrl").is_some();
let shift = m.get("shift").is_some();
let command = m.get("command").is_some();
let down = m.get("down").is_some();
let press = m.get("press").is_some();
if let Some(name) = m.get("name") {
Session::input_key(name, down, press, alt, ctrl, shift, command);
}
}
}
"input_string" => {
Session::input_string(value);
}
"chat_client_mode" => {
Session::send_chat(value.to_owned());
}
"send_mouse" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
let alt = m.get("alt").is_some();
let ctrl = m.get("ctrl").is_some();
let shift = m.get("shift").is_some();
let command = m.get("command").is_some();
let x = m
.get("x")
.map(|x| x.parse::<i32>().unwrap_or(0))
.unwrap_or(0);
let y = m
.get("y")
.map(|x| x.parse::<i32>().unwrap_or(0))
.unwrap_or(0);
let mut mask = 0;
if let Some(_type) = m.get("type") {
mask = match _type.as_str() {
"down" => 1,
"up" => 2,
"wheel" => 3,
_ => 0,
};
}
if let Some(buttons) = m.get("buttons") {
mask |= match buttons.as_str() {
"left" => 1,
"right" => 2,
"wheel" => 4,
_ => 0,
} << 3;
}
Session::send_mouse(mask, x, y, alt, ctrl, shift, command);
}
}
"option" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let Some(name) = m.get("name") {
if let Some(value) = m.get("value") {
Config::set_option(name.to_owned(), value.to_owned());
if name == "custom-rendezvous-server" {
#[cfg(target_os = "android")]
crate::rendezvous_mediator::RendezvousMediator::restart();
crate::common::test_rendezvous_server();
}
}
}
}
}
"peer_option" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let Some(name) = m.get("name") {
if let Some(value) = m.get("value") {
Session::set_option(name.to_owned(), value.to_owned());
}
}
}
}
"input_os_password" => {
Session::input_os_password(value.to_owned(), true);
}
// File Action
"read_remote_dir" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let (Some(path), Some(show_hidden), Some(session)) = (
m.get("path"),
m.get("show_hidden"),
Session::get().read().unwrap().as_ref(),
) {
session.read_remote_dir(path.to_owned(), show_hidden.eq("true"));
}
}
}
"send_files" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let (
Some(id),
Some(path),
Some(to),
Some(show_hidden),
Some(is_remote),
) = (
m.get("id"),
m.get("path"),
m.get("to"),
m.get("show_hidden"),
m.get("is_remote"),
) {
Session::send_files(
id.parse().unwrap_or(0),
path.to_owned(),
to.to_owned(),
show_hidden.eq("true"),
is_remote.eq("true"),
);
}
}
}
"remove_file" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let (
Some(id),
Some(path),
Some(file_num),
Some(is_remote),
Some(session),
) = (
m.get("id"),
m.get("path"),
m.get("file_num"),
m.get("is_remote"),
Session::get().write().unwrap().as_mut(),
) {
session.remove_file(
id.parse().unwrap_or(0),
path.to_owned(),
file_num.parse().unwrap_or(0),
is_remote.eq("true"),
);
}
}
}
"read_dir_recursive" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let (Some(id), Some(path), Some(is_remote), Some(session)) = (
m.get("id"),
m.get("path"),
m.get("is_remote"),
Session::get().write().unwrap().as_mut(),
) {
session.remove_dir_all(
id.parse().unwrap_or(0),
path.to_owned(),
is_remote.eq("true"),
);
}
}
}
"remove_all_empty_dirs" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let (Some(id), Some(path), Some(is_remote), Some(session)) = (
m.get("id"),
m.get("path"),
m.get("is_remote"),
Session::get().write().unwrap().as_mut(),
) {
session.remove_dir(
id.parse().unwrap_or(0),
path.to_owned(),
is_remote.eq("true"),
);
}
}
}
"cancel_job" => {
if let (Ok(id), Some(session)) =
(value.parse(), Session::get().write().unwrap().as_mut())
{
session.cancel_job(id);
}
}
"create_dir" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(value) {
if let (Some(id), Some(path), Some(is_remote), Some(session)) = (
m.get("id"),
m.get("path"),
m.get("is_remote"),
Session::get().write().unwrap().as_mut(),
) {
session.create_dir(
id.parse().unwrap_or(0),
path.to_owned(),
is_remote.eq("true"),
);
}
}
}
// Server Side
"ensure_init_event_queue" => {
Session::ensure_init_event_queue();
}
"update_password" => {
if value.is_empty() {
Config::set_password(&Config::get_auto_password());
} else {
Config::set_password(value);
}
}
#[cfg(target_os = "android")]
"chat_server_mode" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, Value>>(value) {
if let (Some(Value::Number(id)), Some(Value::String(text))) =
(m.get("id"), m.get("text"))
{
let id = id.as_i64().unwrap_or(0);
connection_manager::send_chat(id as i32, text.to_owned());
}
}
}
"home_dir" => {
*config::APP_HOME_DIR.write().unwrap() = value.to_owned();
}
#[cfg(target_os = "android")]
"login_res" => {
if let Ok(m) = serde_json::from_str::<HashMap<String, Value>>(value) {
if let (Some(Value::Number(id)), Some(Value::Bool(res))) =
(m.get("id"), m.get("res"))
{
let id = id.as_i64().unwrap_or(0);
connection_manager::on_login_res(id as i32, *res);
}
}
}
#[cfg(target_os = "android")]
"stop_service" => {
Config::set_option("stop-service".into(), "Y".into());
crate::rendezvous_mediator::RendezvousMediator::restart();
}
#[cfg(target_os = "android")]
"start_service" => {
Config::set_option("stop-service".into(), "".into());
crate::rendezvous_mediator::RendezvousMediator::restart();
}
#[cfg(target_os = "android")]
"close_conn" => {
if let Ok(id) = value.parse::<i32>() {
connection_manager::close_conn(id);
};
}
_ => {
log::error!("Unknown name of set_by_name: {}", name);
}
}
}
}
}
#[repr(C)]
struct RgbaFrame {
len: u32,
data: *mut u8,
}
#[no_mangle]
unsafe extern "C" fn get_rgba() -> *mut RgbaFrame {
if let Some(mut vec) = Session::rgba() {
if vec.is_empty() {
return std::ptr::null_mut();
}
assert!(vec.len() == vec.capacity());
vec.shrink_to_fit();
let data = vec.as_mut_ptr();
let len = vec.len();
std::mem::forget(vec);
Box::into_raw(Box::new(RgbaFrame {
len: len as _,
data,
}))
} else {
std::ptr::null_mut()
}
}
#[no_mangle]
extern "C" fn free_rgba(f: *mut RgbaFrame) {
if f.is_null() {
return;
}
unsafe {
let len = (*f).len as usize;
drop(Vec::from_raw_parts((*f).data, len, len));
Box::from_raw(f);
}
}
#[cfg(target_os = "android")]
pub mod server_side {
use hbb_common::{config::Config, log};
use jni::{
objects::{JClass, JString},
sys::jstring,
JNIEnv,
};
use crate::start_server;
#[no_mangle]
pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startServer(
env: JNIEnv,
_class: JClass,
) {
log::debug!("startServer from java");
std::thread::spawn(move || start_server(true));
}
#[no_mangle]
pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_translateLocale(
env: JNIEnv,
_class: JClass,
locale: JString,
input: JString,
) -> jstring {
let res = if let (Ok(input), Ok(locale)) = (env.get_string(input), env.get_string(locale)) {
let input: String = input.into();
let locale: String = locale.into();
crate::client::translate_locale(input, &locale)
} else {
"".into()
};
return env.new_string(res).unwrap_or(input).into_inner();
}
#[no_mangle]
pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_refreshScreen(
_env: JNIEnv,
_class: JClass,
) {
crate::server::video_service::refresh()
}
}

View File

@ -536,7 +536,7 @@ pub fn run_as_user(arg: &str) -> ResultType<Option<std::process::Child>> {
// -E required for opensuse
let task = std::process::Command::new("sudo")
.args(vec![
"-E",
"-E",
&format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str,
"-u",
&get_active_username(),
@ -587,7 +587,10 @@ pub fn get_pa_sources() -> Vec<(String, String)> {
}
pub fn lock_screen() {
std::process::Command::new("xdg-screensaver").arg("lock").spawn().ok();
std::process::Command::new("xdg-screensaver")
.arg("lock")
.spawn()
.ok();
}
pub fn toggle_blank_screen(_v: bool) {
@ -604,11 +607,7 @@ pub fn is_installed() -> bool {
fn run_cmds(cmds: String) -> ResultType<Option<String>> {
let mut tmp = std::env::temp_dir();
tmp.push(format!(
"{}_{}",
hbb_common::config::APP_NAME,
crate::get_time()
));
tmp.push(format!("{}_{}", crate::get_app_name(), crate::get_time()));
let mut file = std::fs::File::create(&tmp)?;
file.write_all(cmds.as_bytes())?;
file.sync_all()?;

View File

@ -1,58 +1,76 @@
#[cfg(target_os = "linux")]
pub use linux::*;
#[cfg(target_os = "macos")]
pub use macos::*;
#[cfg(windows)]
pub use windows::*;
#[cfg(windows)]
pub mod windows;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "linux")]
pub mod linux;
use hbb_common::{message_proto::CursorData, ResultType};
#[cfg(not(target_os = "macos"))]
const SERVICE_INTERVAL: u64 = 300;
pub fn is_xfce() -> bool {
#[cfg(target_os = "linux")]
{
return std::env::var_os("XDG_CURRENT_DESKTOP") == Some(std::ffi::OsString::from("XFCE"));
}
#[cfg(not(target_os = "linux"))]
{
return false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cursor_data() {
for _ in 0..30 {
if let Some(hc) = get_cursor().unwrap() {
let cd = get_cursor_data(hc).unwrap();
repng::encode(
std::fs::File::create("cursor.png").unwrap(),
cd.width as _,
cd.height as _,
&cd.colors[..],
)
.unwrap();
}
#[cfg(target_os = "macos")]
macos::is_process_trusted(false);
}
}
#[test]
fn test_get_cursor_pos() {
for _ in 0..30 {
assert!(!get_cursor_pos().is_none());
}
}
}
#[cfg(target_os = "linux")]
pub use linux::*;
#[cfg(target_os = "macos")]
pub use macos::*;
#[cfg(windows)]
pub use windows::*;
#[cfg(windows)]
pub mod windows;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "linux")]
pub mod linux;
use hbb_common::{message_proto::CursorData, ResultType};
#[cfg(not(target_os = "macos"))]
const SERVICE_INTERVAL: u64 = 300;
pub fn get_license_key() -> String {
#[cfg(windows)]
if let Some(lic) = windows::get_license() {
return lic.key;
}
Default::default()
}
pub fn is_xfce() -> bool {
#[cfg(target_os = "linux")]
{
return std::env::var_os("XDG_CURRENT_DESKTOP") == Some(std::ffi::OsString::from("XFCE"));
}
#[cfg(not(target_os = "linux"))]
{
return false;
}
}
// Android
#[cfg(target_os = "android")]
pub fn get_active_username() -> String {
// TODO
"android".into()
}
#[cfg(target_os = "android")]
pub const PA_SAMPLE_RATE: u32 = 48000;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cursor_data() {
for _ in 0..30 {
if let Some(hc) = get_cursor().unwrap() {
let cd = get_cursor_data(hc).unwrap();
repng::encode(
std::fs::File::create("cursor.png").unwrap(),
cd.width as _,
cd.height as _,
&cd.colors[..],
)
.unwrap();
}
#[cfg(target_os = "macos")]
macos::is_process_trusted(false);
}
}
#[test]
fn test_get_cursor_pos() {
for _ in 0..30 {
assert!(!get_cursor_pos().is_none());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,26 @@ use hbb_common::{
};
fn run_rdp(port: u16) {
std::process::Command::new("cmdkey")
.arg("/delete:localhost")
.output()
.ok();
let username = std::env::var("rdp_username").unwrap_or_default();
let password = std::env::var("rdp_password").unwrap_or_default();
if !username.is_empty() || !password.is_empty() {
let mut args = vec!["/generic:localhost".to_owned()];
if !username.is_empty() {
args.push(format!("/user:{}", username));
}
if !password.is_empty() {
args.push(format!("/pass:{}", password));
}
println!("{:?}", args);
std::process::Command::new("cmdkey")
.args(&args)
.output()
.ok();
}
std::process::Command::new("mstsc")
.arg(format!("/v:localhost:{}", port))
.spawn()
@ -25,6 +45,8 @@ pub async fn listen(
port: i32,
interface: impl Interface,
ui_receiver: mpsc::UnboundedReceiver<Data>,
key: &str,
token: &str,
) -> ResultType<()> {
let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?;
let addr = listener.local_addr()?;
@ -40,7 +62,7 @@ pub async fn listen(
log::info!("new connection from {:?}", addr);
let id = id.clone();
let mut forward = Framed::new(forward, BytesCodec::new());
match connect_and_login(&id, &mut ui_receiver, interface.clone(), &mut forward, is_rdp).await {
match connect_and_login(&id, &mut ui_receiver, interface.clone(), &mut forward, key, token, is_rdp).await {
Ok(Some(stream)) => {
let interface = interface.clone();
tokio::spawn(async move {
@ -77,6 +99,8 @@ async fn connect_and_login(
ui_receiver: &mut mpsc::UnboundedReceiver<Data>,
interface: impl Interface,
forward: &mut Framed<TcpStream, BytesCodec>,
key: &str,
token: &str,
is_rdp: bool,
) -> ResultType<Option<Stream>> {
let conn_type = if is_rdp {
@ -84,7 +108,7 @@ async fn connect_and_login(
} else {
ConnType::PORT_FORWARD
};
let (mut stream, _) = Client::start(id, conn_type).await?;
let (mut stream, _) = Client::start(id, key, token, conn_type).await?;
let mut interface = interface;
let mut buffer = Vec::new();
loop {

View File

@ -58,6 +58,7 @@ impl RendezvousMediator {
tokio::spawn(async move {
direct_server(server_cloned).await;
});
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if crate::platform::is_installed() {
std::thread::spawn(move || {
allow_err!(lan_discovery());
@ -385,12 +386,7 @@ impl RendezvousMediator {
async fn register_pk(&mut self, socket: &mut FramedSocket) -> ResultType<()> {
let mut msg_out = Message::new();
let pk = Config::get_key_pair().1;
let uuid = if let Ok(id) = machine_uid::get() {
log::info!("machine uid: {}", id);
id.into()
} else {
pk.clone()
};
let uuid = crate::get_uuid();
let id = Config::get_id();
self.last_id_pk_registry = id.clone();
msg_out.set_register_pk(RegisterPk {
@ -548,11 +544,14 @@ pub fn get_broadcast_port() -> u16 {
}
pub fn get_mac() -> String {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Ok(Some(mac)) = mac_address::get_mac_address() {
mac.to_string()
} else {
"".to_owned()
}
#[cfg(any(target_os = "android", target_os = "ios"))]
"".to_owned()
}
fn lan_discovery() -> ResultType<()> {

View File

@ -1,383 +1,413 @@
use crate::ipc::Data;
pub use connection::*;
use hbb_common::{
allow_err,
anyhow::{anyhow, Context},
bail,
config::{Config, Config2, CONNECT_TIMEOUT, RELAY_PORT},
log,
message_proto::*,
protobuf::{Message as _, ProtobufEnum},
rendezvous_proto::*,
sleep, socket_client,
sodiumoxide::crypto::{box_, secretbox, sign},
timeout, tokio, ResultType, Stream,
};
use service::{GenericService, Service, ServiceTmpl, Subscriber};
use std::{
collections::HashMap,
net::SocketAddr,
sync::{Arc, Mutex, RwLock, Weak},
time::Duration,
};
pub mod audio_service;
mod clipboard_service;
mod connection;
pub mod input_service;
mod service;
mod video_service;
use hbb_common::tcp::new_listener;
pub type Childs = Arc<Mutex<Vec<std::process::Child>>>;
type ConnMap = HashMap<i32, ConnInner>;
lazy_static::lazy_static! {
pub static ref CHILD_PROCESS: Childs = Default::default();
}
pub struct Server {
connections: ConnMap,
services: HashMap<&'static str, Box<dyn Service>>,
id_count: i32,
}
pub type ServerPtr = Arc<RwLock<Server>>;
pub type ServerPtrWeak = Weak<RwLock<Server>>;
pub fn new() -> ServerPtr {
let mut server = Server {
connections: HashMap::new(),
services: HashMap::new(),
id_count: 0,
};
server.add_service(Box::new(audio_service::new()));
server.add_service(Box::new(video_service::new()));
server.add_service(Box::new(clipboard_service::new()));
server.add_service(Box::new(input_service::new_cursor()));
server.add_service(Box::new(input_service::new_pos()));
Arc::new(RwLock::new(server))
}
async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> ResultType<()> {
let local_addr = socket.local_addr();
drop(socket);
// even we drop socket, below still may fail if not use reuse_addr,
// there is TIME_WAIT before socket really released, so sometimes we
// see “Only one usage of each socket address is normally permitted” on windows sometimes,
let listener = new_listener(local_addr, true).await?;
log::info!("Server listening on: {}", &listener.local_addr()?);
if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? {
stream.set_nodelay(true).ok();
let stream_addr = stream.local_addr()?;
create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?;
}
Ok(())
}
pub async fn create_tcp_connection(
server: ServerPtr,
stream: Stream,
addr: SocketAddr,
secure: bool,
) -> ResultType<()> {
let mut stream = stream;
let id = {
let mut w = server.write().unwrap();
w.id_count += 1;
w.id_count
};
let (sk, pk) = Config::get_key_pair();
if secure && pk.len() == sign::PUBLICKEYBYTES && sk.len() == sign::SECRETKEYBYTES {
let mut sk_ = [0u8; sign::SECRETKEYBYTES];
sk_[..].copy_from_slice(&sk);
let sk = sign::SecretKey(sk_);
let mut msg_out = Message::new();
let (our_pk_b, our_sk_b) = box_::gen_keypair();
msg_out.set_signed_id(SignedId {
id: sign::sign(
&IdPk {
id: Config::get_id(),
pk: our_pk_b.0.to_vec(),
..Default::default()
}
.write_to_bytes()
.unwrap_or_default(),
&sk,
),
..Default::default()
});
timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??;
match timeout(CONNECT_TIMEOUT, stream.next()).await? {
Some(res) => {
let bytes = res?;
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
if let Some(message::Union::public_key(pk)) = msg_in.union {
if pk.asymmetric_value.len() == box_::PUBLICKEYBYTES {
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
let mut pk_ = [0u8; box_::PUBLICKEYBYTES];
pk_[..].copy_from_slice(&pk.asymmetric_value);
let their_pk_b = box_::PublicKey(pk_);
let symmetric_key =
box_::open(&pk.symmetric_value, &nonce, &their_pk_b, &our_sk_b)
.map_err(|_| {
anyhow!("Handshake failed: box decryption failure")
})?;
if symmetric_key.len() != secretbox::KEYBYTES {
bail!("Handshake failed: invalid secret key length from peer");
}
let mut key = [0u8; secretbox::KEYBYTES];
key[..].copy_from_slice(&symmetric_key);
stream.set_key(secretbox::Key(key));
} else if pk.asymmetric_value.is_empty() {
Config::set_key_confirmed(false);
log::info!("Force to update pk");
} else {
bail!("Handshake failed: invalid public sign key length from peer");
}
} else {
log::error!("Handshake failed: invalid message type");
}
} else {
bail!("Handshake failed: invalid message format");
}
}
None => {
bail!("Failed to receive public key");
}
}
}
Connection::start(addr, stream, id, Arc::downgrade(&server)).await;
Ok(())
}
pub async fn accept_connection(
server: ServerPtr,
socket: Stream,
peer_addr: SocketAddr,
secure: bool,
) {
if let Err(err) = accept_connection_(server, socket, secure).await {
log::error!("Failed to accept connection from {}: {}", peer_addr, err);
}
}
pub async fn create_relay_connection(
server: ServerPtr,
relay_server: String,
uuid: String,
peer_addr: SocketAddr,
secure: bool,
) {
if let Err(err) =
create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure).await
{
log::error!(
"Failed to create relay connection for {} with uuid {}: {}",
peer_addr,
uuid,
err
);
}
}
async fn create_relay_connection_(
server: ServerPtr,
relay_server: String,
uuid: String,
peer_addr: SocketAddr,
secure: bool,
) -> ResultType<()> {
let mut stream = socket_client::connect_tcp(
crate::check_port(relay_server, RELAY_PORT),
Config::get_any_listen_addr(),
CONNECT_TIMEOUT,
)
.await?;
let mut msg_out = RendezvousMessage::new();
msg_out.set_request_relay(RequestRelay {
uuid,
..Default::default()
});
stream.send(&msg_out).await?;
create_tcp_connection(server, stream, peer_addr, secure).await?;
Ok(())
}
impl Server {
pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) {
for s in self.services.values() {
if !noperms.contains(&s.name()) {
s.on_subscribe(conn.clone());
}
}
self.connections.insert(conn.id(), conn);
}
pub fn remove_connection(&mut self, conn: &ConnInner) {
for s in self.services.values() {
s.on_unsubscribe(conn.id());
}
self.connections.remove(&conn.id());
}
fn add_service(&mut self, service: Box<dyn Service>) {
let name = service.name();
self.services.insert(name, service);
}
pub fn subscribe(&mut self, name: &str, conn: ConnInner, sub: bool) {
if let Some(s) = self.services.get(&name) {
if s.is_subed(conn.id()) == sub {
return;
}
if sub {
s.on_subscribe(conn.clone());
} else {
s.on_unsubscribe(conn.id());
}
}
}
}
impl Drop for Server {
fn drop(&mut self) {
for s in self.services.values() {
s.join();
}
}
}
pub fn check_zombie() {
std::thread::spawn(|| loop {
let mut lock = CHILD_PROCESS.lock().unwrap();
let mut i = 0;
while i != lock.len() {
let c = &mut (*lock)[i];
if let Ok(Some(_)) = c.try_wait() {
lock.remove(i);
} else {
i += 1;
}
}
drop(lock);
std::thread::sleep(Duration::from_millis(100));
});
}
#[tokio::main]
pub async fn start_server(is_server: bool, _tray: bool) {
#[cfg(target_os = "linux")]
{
log::info!("DISPLAY={:?}", std::env::var("DISPLAY"));
log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY"));
}
if is_server {
std::thread::spawn(move || {
if let Err(err) = crate::ipc::start("") {
log::error!("Failed to start ipc: {}", err);
std::process::exit(-1);
}
});
input_service::fix_key_down_timeout_loop();
#[cfg(target_os = "macos")]
tokio::spawn(async { sync_and_watch_config_dir().await });
crate::RendezvousMediator::start_all().await;
} else {
match crate::ipc::connect(1000, "").await {
Ok(mut conn) => {
if conn.send(&Data::SyncConfig(None)).await.is_ok() {
if let Ok(Some(data)) = conn.next_timeout(1000).await {
match data {
Data::SyncConfig(Some((config, config2))) => {
if Config::set(config) {
log::info!("config synced");
}
if Config2::set(config2) {
log::info!("config2 synced");
}
}
_ => {}
}
}
}
}
Err(err) => {
log::info!("server not started (will try to start): {}", err);
std::thread::spawn(|| start_server(true, false));
}
}
}
}
#[cfg(target_os = "macos")]
async fn sync_and_watch_config_dir() {
if crate::platform::is_root() {
return;
}
let mut cfg0 = (Config::get(), Config2::get());
let mut synced = false;
let tries =
if std::env::args().len() == 2 && std::env::args().nth(1) == Some("--server".to_owned()) {
30
} else {
3
};
log::debug!("#tries of ipc service connection: {}", tries);
for i in 1..=tries {
sleep(i as f32 * 0.3).await;
match crate::ipc::connect(1000, "_service").await {
Ok(mut conn) => {
if !synced {
if conn.send(&Data::SyncConfig(None)).await.is_ok() {
if let Ok(Some(data)) = conn.next_timeout(1000).await {
match data {
Data::SyncConfig(Some((config, config2))) => {
let _chk = crate::ipc::CheckIfRestart::new();
if cfg0.0 != config {
cfg0.0 = config.clone();
Config::set(config);
log::info!("sync config from root");
}
if cfg0.1 != config2 {
cfg0.1 = config2.clone();
Config2::set(config2);
log::info!("sync config2 from root");
}
synced = true;
}
_ => {}
};
};
}
}
loop {
sleep(0.3).await;
let cfg = (Config::get(), Config2::get());
if cfg != cfg0 {
log::info!("config updated, sync to root");
match conn.send(&Data::SyncConfig(Some(cfg.clone()))).await {
Err(e) => {
log::error!("sync config to root failed: {}", e);
break;
}
_ => {
cfg0 = cfg;
conn.next_timeout(1000).await.ok();
}
}
}
}
}
Err(_) => {
log::info!("#{} try: failed to connect to ipc_service", i);
}
}
}
log::error!("skipped config sync");
}
use crate::ipc::Data;
pub use connection::*;
use hbb_common::{
allow_err,
anyhow::{anyhow, Context},
bail,
config::{Config, Config2, CONNECT_TIMEOUT, RELAY_PORT},
log,
message_proto::*,
protobuf::{Message as _, ProtobufEnum},
rendezvous_proto::*,
socket_client,
sodiumoxide::crypto::{box_, secretbox, sign},
timeout, tokio, ResultType, Stream,
};
use service::{GenericService, Service, ServiceTmpl, Subscriber};
use std::{
collections::HashMap,
net::SocketAddr,
sync::{Arc, Mutex, RwLock, Weak},
time::Duration,
};
pub mod audio_service;
cfg_if::cfg_if! {
if #[cfg(not(any(target_os = "android", target_os = "ios")))] {
mod clipboard_service;
pub mod input_service;
} else {
mod clipboard_service {
pub const NAME: &'static str = "";
}
pub mod input_service {
pub const NAME_CURSOR: &'static str = "";
pub const NAME_POS: &'static str = "";
}
}
}
mod connection;
mod service;
pub mod video_service;
use hbb_common::tcp::new_listener;
pub type Childs = Arc<Mutex<Vec<std::process::Child>>>;
type ConnMap = HashMap<i32, ConnInner>;
lazy_static::lazy_static! {
pub static ref CHILD_PROCESS: Childs = Default::default();
}
pub struct Server {
connections: ConnMap,
services: HashMap<&'static str, Box<dyn Service>>,
id_count: i32,
}
pub type ServerPtr = Arc<RwLock<Server>>;
pub type ServerPtrWeak = Weak<RwLock<Server>>;
pub fn new() -> ServerPtr {
let mut server = Server {
connections: HashMap::new(),
services: HashMap::new(),
id_count: 0,
};
server.add_service(Box::new(audio_service::new()));
server.add_service(Box::new(video_service::new()));
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
server.add_service(Box::new(clipboard_service::new()));
server.add_service(Box::new(input_service::new_cursor()));
server.add_service(Box::new(input_service::new_pos()));
}
Arc::new(RwLock::new(server))
}
async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> ResultType<()> {
let local_addr = socket.local_addr();
drop(socket);
// even we drop socket, below still may fail if not use reuse_addr,
// there is TIME_WAIT before socket really released, so sometimes we
// see “Only one usage of each socket address is normally permitted” on windows sometimes,
let listener = new_listener(local_addr, true).await?;
log::info!("Server listening on: {}", &listener.local_addr()?);
if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? {
stream.set_nodelay(true).ok();
let stream_addr = stream.local_addr()?;
create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?;
}
Ok(())
}
pub async fn create_tcp_connection(
server: ServerPtr,
stream: Stream,
addr: SocketAddr,
secure: bool,
) -> ResultType<()> {
let mut stream = stream;
let id = {
let mut w = server.write().unwrap();
w.id_count += 1;
w.id_count
};
let (sk, pk) = Config::get_key_pair();
if secure && pk.len() == sign::PUBLICKEYBYTES && sk.len() == sign::SECRETKEYBYTES {
let mut sk_ = [0u8; sign::SECRETKEYBYTES];
sk_[..].copy_from_slice(&sk);
let sk = sign::SecretKey(sk_);
let mut msg_out = Message::new();
let (our_pk_b, our_sk_b) = box_::gen_keypair();
msg_out.set_signed_id(SignedId {
id: sign::sign(
&IdPk {
id: Config::get_id(),
pk: our_pk_b.0.to_vec(),
..Default::default()
}
.write_to_bytes()
.unwrap_or_default(),
&sk,
),
..Default::default()
});
timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??;
match timeout(CONNECT_TIMEOUT, stream.next()).await? {
Some(res) => {
let bytes = res?;
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
if let Some(message::Union::public_key(pk)) = msg_in.union {
if pk.asymmetric_value.len() == box_::PUBLICKEYBYTES {
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
let mut pk_ = [0u8; box_::PUBLICKEYBYTES];
pk_[..].copy_from_slice(&pk.asymmetric_value);
let their_pk_b = box_::PublicKey(pk_);
let symmetric_key =
box_::open(&pk.symmetric_value, &nonce, &their_pk_b, &our_sk_b)
.map_err(|_| {
anyhow!("Handshake failed: box decryption failure")
})?;
if symmetric_key.len() != secretbox::KEYBYTES {
bail!("Handshake failed: invalid secret key length from peer");
}
let mut key = [0u8; secretbox::KEYBYTES];
key[..].copy_from_slice(&symmetric_key);
stream.set_key(secretbox::Key(key));
} else if pk.asymmetric_value.is_empty() {
Config::set_key_confirmed(false);
log::info!("Force to update pk");
} else {
bail!("Handshake failed: invalid public sign key length from peer");
}
} else {
log::error!("Handshake failed: invalid message type");
}
} else {
bail!("Handshake failed: invalid message format");
}
}
None => {
bail!("Failed to receive public key");
}
}
}
Connection::start(addr, stream, id, Arc::downgrade(&server)).await;
Ok(())
}
pub async fn accept_connection(
server: ServerPtr,
socket: Stream,
peer_addr: SocketAddr,
secure: bool,
) {
if let Err(err) = accept_connection_(server, socket, secure).await {
log::error!("Failed to accept connection from {}: {}", peer_addr, err);
}
}
pub async fn create_relay_connection(
server: ServerPtr,
relay_server: String,
uuid: String,
peer_addr: SocketAddr,
secure: bool,
) {
if let Err(err) =
create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure).await
{
log::error!(
"Failed to create relay connection for {} with uuid {}: {}",
peer_addr,
uuid,
err
);
}
}
async fn create_relay_connection_(
server: ServerPtr,
relay_server: String,
uuid: String,
peer_addr: SocketAddr,
secure: bool,
) -> ResultType<()> {
let mut stream = socket_client::connect_tcp(
crate::check_port(relay_server, RELAY_PORT),
Config::get_any_listen_addr(),
CONNECT_TIMEOUT,
)
.await?;
let mut msg_out = RendezvousMessage::new();
let mut licence_key = Config::get_option("key");
if licence_key.is_empty() {
licence_key = crate::platform::get_license_key();
}
msg_out.set_request_relay(RequestRelay {
licence_key,
uuid,
..Default::default()
});
stream.send(&msg_out).await?;
create_tcp_connection(server, stream, peer_addr, secure).await?;
Ok(())
}
impl Server {
pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) {
for s in self.services.values() {
if !noperms.contains(&s.name()) {
s.on_subscribe(conn.clone());
}
}
self.connections.insert(conn.id(), conn);
}
pub fn remove_connection(&mut self, conn: &ConnInner) {
for s in self.services.values() {
s.on_unsubscribe(conn.id());
}
self.connections.remove(&conn.id());
}
fn add_service(&mut self, service: Box<dyn Service>) {
let name = service.name();
self.services.insert(name, service);
}
pub fn subscribe(&mut self, name: &str, conn: ConnInner, sub: bool) {
if let Some(s) = self.services.get(&name) {
if s.is_subed(conn.id()) == sub {
return;
}
if sub {
s.on_subscribe(conn.clone());
} else {
s.on_unsubscribe(conn.id());
}
}
}
}
impl Drop for Server {
fn drop(&mut self) {
for s in self.services.values() {
s.join();
}
}
}
pub fn check_zombie() {
std::thread::spawn(|| loop {
let mut lock = CHILD_PROCESS.lock().unwrap();
let mut i = 0;
while i != lock.len() {
let c = &mut (*lock)[i];
if let Ok(Some(_)) = c.try_wait() {
lock.remove(i);
} else {
i += 1;
}
}
drop(lock);
std::thread::sleep(Duration::from_millis(100));
});
}
#[cfg(any(target_os = "android", target_os = "ios"))]
#[tokio::main]
pub async fn start_server(is_server: bool) {
crate::RendezvousMediator::start_all().await;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tokio::main]
pub async fn start_server(is_server: bool) {
#[cfg(target_os = "linux")]
{
log::info!("DISPLAY={:?}", std::env::var("DISPLAY"));
log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY"));
}
if is_server {
std::thread::spawn(move || {
if let Err(err) = crate::ipc::start("") {
log::error!("Failed to start ipc: {}", err);
std::process::exit(-1);
}
});
#[cfg(windows)]
crate::platform::windows::bootstrap();
input_service::fix_key_down_timeout_loop();
#[cfg(target_os = "macos")]
tokio::spawn(async { sync_and_watch_config_dir().await });
crate::RendezvousMediator::start_all().await;
} else {
match crate::ipc::connect(1000, "").await {
Ok(mut conn) => {
if conn.send(&Data::SyncConfig(None)).await.is_ok() {
if let Ok(Some(data)) = conn.next_timeout(1000).await {
match data {
Data::SyncConfig(Some((config, config2))) => {
if Config::set(config) {
log::info!("config synced");
}
if Config2::set(config2) {
log::info!("config2 synced");
}
}
_ => {}
}
}
}
}
Err(err) => {
log::info!("server not started (will try to start): {}", err);
std::thread::spawn(|| start_server(true));
}
}
}
}
#[cfg(target_os = "macos")]
async fn sync_and_watch_config_dir() {
if crate::platform::is_root() {
return;
}
let mut cfg0 = (Config::get(), Config2::get());
let mut synced = false;
let tries =
if std::env::args().len() == 2 && std::env::args().nth(1) == Some("--server".to_owned()) {
30
} else {
3
};
log::debug!("#tries of ipc service connection: {}", tries);
use hbb_common::sleep;
for i in 1..=tries {
sleep(i as f32 * 0.3).await;
match crate::ipc::connect(1000, "_service").await {
Ok(mut conn) => {
if !synced {
if conn.send(&Data::SyncConfig(None)).await.is_ok() {
if let Ok(Some(data)) = conn.next_timeout(1000).await {
match data {
Data::SyncConfig(Some((config, config2))) => {
let _chk = crate::ipc::CheckIfRestart::new();
if cfg0.0 != config {
cfg0.0 = config.clone();
Config::set(config);
log::info!("sync config from root");
}
if cfg0.1 != config2 {
cfg0.1 = config2.clone();
Config2::set(config2);
log::info!("sync config2 from root");
}
synced = true;
}
_ => {}
};
};
}
}
loop {
sleep(0.3).await;
let cfg = (Config::get(), Config2::get());
if cfg != cfg0 {
log::info!("config updated, sync to root");
match conn.send(&Data::SyncConfig(Some(cfg.clone()))).await {
Err(e) => {
log::error!("sync config to root failed: {}", e);
break;
}
_ => {
cfg0 = cfg;
conn.next_timeout(1000).await.ok();
}
}
}
}
}
Err(_) => {
log::info!("#{} try: failed to connect to ipc_service", i);
}
}
}
log::error!("skipped config sync");
}

View File

@ -1,381 +1,426 @@
// both soundio and cpal use wasapi on windows and coreaudio on mac, they do not support loopback.
// libpulseaudio support loopback because pulseaudio is a standalone audio service with some
// configuration, but need to install the library and start the service on OS, not a good choice.
// windows: https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording
// mac: https://github.com/mattingalls/Soundflower
// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nn-audioclient-iaudioclient
// https://github.com/ExistentialAudio/BlackHole
// if pactl not work, please run
// sudo apt-get --purge --reinstall install pulseaudio
// https://askubuntu.com/questions/403416/how-to-listen-live-sounds-from-input-from-external-sound-card
// https://wiki.debian.org/audio-loopback
// https://github.com/krruzic/pulsectl
use super::*;
use magnum_opus::{Application::*, Channels::*, Encoder};
use std::sync::atomic::{AtomicBool, Ordering};
pub const NAME: &'static str = "audio";
pub const AUDIO_DATA_SIZE_U8: usize = 960 * 4; // 10ms in 48000 stereo
static RESTARTING: AtomicBool = AtomicBool::new(false);
#[cfg(not(target_os = "linux"))]
pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true);
sp.repeat::<cpal_impl::State, _>(33, cpal_impl::run);
sp
}
#[cfg(target_os = "linux")]
pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true);
sp.run(pa_impl::run);
sp
}
pub fn restart() {
log::info!("restart the audio service, freezing now...");
if RESTARTING.load(Ordering::SeqCst) {
return;
}
RESTARTING.store(true, Ordering::SeqCst);
}
#[cfg(target_os = "linux")]
mod pa_impl {
use super::*;
#[tokio::main(flavor = "current_thread")]
pub async fn run(sp: GenericService) -> ResultType<()> {
hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc
RESTARTING.store(false, Ordering::SeqCst);
let mut stream = crate::ipc::connect(1000, "_pa").await?;
unsafe {
AUDIO_ZERO_COUNT = 0;
}
let mut encoder = Encoder::new(crate::platform::linux::PA_SAMPLE_RATE, Stereo, LowDelay)?;
allow_err!(
stream
.send(&crate::ipc::Data::Config((
"audio-input".to_owned(),
Some(Config::get_option("audio-input"))
)))
.await
);
let zero_audio_frame: Vec<f32> = vec![0.; AUDIO_DATA_SIZE_U8 / 4];
while sp.ok() && !RESTARTING.load(Ordering::SeqCst) {
sp.snapshot(|sps| {
sps.send(create_format_msg(crate::platform::linux::PA_SAMPLE_RATE, 2));
Ok(())
})?;
if let Ok(data) = stream.next_raw().await {
if data.len() == 0 {
send_f32(&zero_audio_frame, &mut encoder, &sp);
continue;
}
if data.len() != AUDIO_DATA_SIZE_U8 {
continue;
}
let data = unsafe {
std::slice::from_raw_parts::<f32>(data.as_ptr() as _, data.len() / 4)
};
send_f32(data, &mut encoder, &sp);
}
}
Ok(())
}
}
#[cfg(not(target_os = "linux"))]
mod cpal_impl {
use super::*;
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
Device, Host, SupportedStreamConfig,
};
lazy_static::lazy_static! {
static ref HOST: Host = cpal::default_host();
}
#[derive(Default)]
pub struct State {
stream: Option<(Box<dyn StreamTrait>, Arc<Message>)>,
}
impl super::service::Reset for State {
fn reset(&mut self) {
self.stream.take();
}
}
pub fn run(sp: GenericService, state: &mut State) -> ResultType<()> {
sp.snapshot(|sps| {
match &state.stream {
None => {
state.stream = Some(play(&sp)?);
}
_ => {}
}
if let Some((_, format)) = &state.stream {
sps.send_shared(format.clone());
}
Ok(())
})?;
Ok(())
}
fn send(
data: &[f32],
sample_rate0: u32,
sample_rate: u32,
channels: u16,
encoder: &mut Encoder,
sp: &GenericService,
) {
let buffer;
let data = if sample_rate0 != sample_rate {
buffer = crate::common::resample_channels(data, sample_rate0, sample_rate, channels);
&buffer
} else {
data
};
send_f32(data, encoder, sp);
}
#[cfg(windows)]
fn get_device() -> ResultType<(Device, SupportedStreamConfig)> {
let audio_input = Config::get_option("audio-input");
if !audio_input.is_empty() {
return get_audio_input(&audio_input);
}
let device = HOST
.default_output_device()
.with_context(|| "Failed to get default output device for loopback")?;
log::info!(
"Default output device: {}",
device.name().unwrap_or("".to_owned())
);
let format = device
.default_output_config()
.map_err(|e| anyhow!(e))
.with_context(|| "Failed to get default output format")?;
log::info!("Default output format: {:?}", format);
Ok((device, format))
}
#[cfg(not(windows))]
fn get_device() -> ResultType<(Device, SupportedStreamConfig)> {
let audio_input = Config::get_option("audio-input");
get_audio_input(&audio_input)
}
fn get_audio_input(audio_input: &str) -> ResultType<(Device, SupportedStreamConfig)> {
let mut device = None;
if !audio_input.is_empty() {
for d in HOST
.devices()
.with_context(|| "Failed to get audio devices")?
{
if d.name().unwrap_or("".to_owned()) == audio_input {
device = Some(d);
break;
}
}
}
if device.is_none() {
device = Some(
HOST.default_input_device()
.with_context(|| "Failed to get default input device for loopback")?,
);
}
let device = device.unwrap();
log::info!("Input device: {}", device.name().unwrap_or("".to_owned()));
let format = device
.default_input_config()
.map_err(|e| anyhow!(e))
.with_context(|| "Failed to get default input format")?;
log::info!("Default input format: {:?}", format);
Ok((device, format))
}
fn play(sp: &GenericService) -> ResultType<(Box<dyn StreamTrait>, Arc<Message>)> {
let (device, config) = get_device()?;
let sp = sp.clone();
let err_fn = move |err| {
// too many UnknownErrno, will improve later
log::trace!("an error occurred on stream: {}", err);
};
// Sample rate must be one of 8000, 12000, 16000, 24000, or 48000.
let sample_rate_0 = config.sample_rate().0;
let sample_rate = if sample_rate_0 < 12000 {
8000
} else if sample_rate_0 < 16000 {
12000
} else if sample_rate_0 < 24000 {
16000
} else if sample_rate_0 < 48000 {
24000
} else {
48000
};
log::debug!("Audio sample rate : {}", sample_rate);
unsafe {
AUDIO_ZERO_COUNT = 0;
}
let mut encoder = Encoder::new(
sample_rate,
if config.channels() > 1 { Stereo } else { Mono },
LowDelay,
)?;
let channels = config.channels();
let stream = match config.sample_format() {
cpal::SampleFormat::F32 => device.build_input_stream(
&config.into(),
move |data, _: &_| {
send(
data,
sample_rate_0,
sample_rate,
channels,
&mut encoder,
&sp,
);
},
err_fn,
)?,
cpal::SampleFormat::I16 => device.build_input_stream(
&config.into(),
move |data: &[i16], _: &_| {
let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect();
send(
&buffer,
sample_rate_0,
sample_rate,
channels,
&mut encoder,
&sp,
);
},
err_fn,
)?,
cpal::SampleFormat::U16 => device.build_input_stream(
&config.into(),
move |data: &[u16], _: &_| {
let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect();
send(
&buffer,
sample_rate_0,
sample_rate,
channels,
&mut encoder,
&sp,
);
},
err_fn,
)?,
};
stream.play()?;
Ok((
Box::new(stream),
Arc::new(create_format_msg(sample_rate, channels)),
))
}
}
fn create_format_msg(sample_rate: u32, channels: u16) -> Message {
let format = AudioFormat {
sample_rate,
channels: channels as _,
..Default::default()
};
let mut misc = Misc::new();
misc.set_audio_format(format);
let mut msg = Message::new();
msg.set_misc(misc);
msg
}
// use AUDIO_ZERO_COUNT for the Noise(Zero) Gate Attack Time
// every audio data length is set to 480
// MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows)
const MAX_AUDIO_ZERO_COUNT: u16 = 800;
static mut AUDIO_ZERO_COUNT: u16 = 0;
fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
if data.iter().filter(|x| **x != 0.).next().is_some() {
unsafe {
AUDIO_ZERO_COUNT = 0;
}
} else {
unsafe {
if AUDIO_ZERO_COUNT > MAX_AUDIO_ZERO_COUNT {
if AUDIO_ZERO_COUNT == MAX_AUDIO_ZERO_COUNT + 1 {
log::debug!("Audio Zero Gate Attack");
AUDIO_ZERO_COUNT += 1;
}
return;
}
AUDIO_ZERO_COUNT += 1;
}
}
match encoder.encode_vec_float(data, data.len() * 6) {
Ok(data) => {
let mut msg_out = Message::new();
msg_out.set_audio_frame(AudioFrame {
data,
..Default::default()
});
sp.send(msg_out);
}
Err(_) => {}
}
}
#[cfg(test)]
mod tests {
#[cfg(target_os = "linux")]
#[test]
fn test_pulse() {
let spec = pulse::sample::Spec {
format: pulse::sample::SAMPLE_FLOAT32NE,
channels: 2,
rate: 24000,
};
let hspec = hound::WavSpec {
channels: spec.channels as _,
sample_rate: spec.rate as _,
bits_per_sample: (4 * 8) as _,
sample_format: hound::SampleFormat::Float,
};
const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav");
let mut writer =
hound::WavWriter::create(PATH, hspec).expect("Could not create hsound writer");
let device = crate::platform::linux::get_pa_monitor();
let s = psimple::Simple::new(
None, // Use the default server
"Test", // Our applications name
pulse::stream::Direction::Record, // We want a record stream
Some(&device), // Use the default device
"Test", // Description of our stream
&spec, // Our sample format
None, // Use default channel map
None, // Use default buffering attributes
)
.expect("Could not create simple pulse");
let mut out: Vec<u8> = Vec::with_capacity(1024);
unsafe {
out.set_len(out.capacity());
}
for _ in 0..600 {
s.read(&mut out).expect("Could not read pcm");
let out2 =
unsafe { std::slice::from_raw_parts::<f32>(out.as_ptr() as _, out.len() / 4) };
for v in out2 {
writer.write_sample(*v).ok();
}
}
println!("{:?} {}", device, out.len());
writer.finalize().expect("Could not finalize writer");
}
}
// both soundio and cpal use wasapi on windows and coreaudio on mac, they do not support loopback.
// libpulseaudio support loopback because pulseaudio is a standalone audio service with some
// configuration, but need to install the library and start the service on OS, not a good choice.
// windows: https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording
// mac: https://github.com/mattingalls/Soundflower
// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nn-audioclient-iaudioclient
// https://github.com/ExistentialAudio/BlackHole
// if pactl not work, please run
// sudo apt-get --purge --reinstall install pulseaudio
// https://askubuntu.com/questions/403416/how-to-listen-live-sounds-from-input-from-external-sound-card
// https://wiki.debian.org/audio-loopback
// https://github.com/krruzic/pulsectl
use super::*;
use magnum_opus::{Application::*, Channels::*, Encoder};
use std::sync::atomic::{AtomicBool, Ordering};
pub const NAME: &'static str = "audio";
pub const AUDIO_DATA_SIZE_U8: usize = 960 * 4; // 10ms in 48000 stereo
static RESTARTING: AtomicBool = AtomicBool::new(false);
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true);
sp.repeat::<cpal_impl::State, _>(33, cpal_impl::run);
sp
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true);
sp.run(pa_impl::run);
sp
}
pub fn restart() {
log::info!("restart the audio service, freezing now...");
if RESTARTING.load(Ordering::SeqCst) {
return;
}
RESTARTING.store(true, Ordering::SeqCst);
}
#[cfg(any(target_os = "linux", target_os = "android"))]
mod pa_impl {
use super::*;
#[tokio::main(flavor = "current_thread")]
pub async fn run(sp: GenericService) -> ResultType<()> {
hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc
RESTARTING.store(false, Ordering::SeqCst);
#[cfg(target_os = "linux")]
let mut stream = crate::ipc::connect(1000, "_pa").await?;
unsafe {
AUDIO_ZERO_COUNT = 0;
}
let mut encoder = Encoder::new(crate::platform::PA_SAMPLE_RATE, Stereo, LowDelay)?;
#[cfg(target_os = "linux")]
allow_err!(
stream
.send(&crate::ipc::Data::Config((
"audio-input".to_owned(),
Some(Config::get_option("audio-input"))
)))
.await
);
let zero_audio_frame: Vec<f32> = vec![0.; AUDIO_DATA_SIZE_U8 / 4];
while sp.ok() && !RESTARTING.load(Ordering::SeqCst) {
sp.snapshot(|sps| {
sps.send(create_format_msg(crate::platform::PA_SAMPLE_RATE, 2));
Ok(())
})?;
#[cfg(target_os = "linux")]
if let Ok(data) = stream.next_raw().await {
if data.len() == 0 {
send_f32(&zero_audio_frame, &mut encoder, &sp);
continue;
}
if data.len() != AUDIO_DATA_SIZE_U8 {
continue;
}
let data = unsafe {
std::slice::from_raw_parts::<f32>(data.as_ptr() as _, data.len() / 4)
};
send_f32(data, &mut encoder, &sp);
}
#[cfg(target_os = "android")]
if let Some(data) = scrap::android::ffi::get_audio_raw() {
let data = unsafe {
std::slice::from_raw_parts::<f32>(data.as_ptr() as _, data.len() / 4)
};
send_f32(data, &mut encoder, &sp);
} else {
hbb_common::sleep(0.1).await;
}
}
Ok(())
}
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
mod cpal_impl {
use super::*;
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
Device, Host, SupportedStreamConfig,
};
lazy_static::lazy_static! {
static ref HOST: Host = cpal::default_host();
}
#[derive(Default)]
pub struct State {
stream: Option<(Box<dyn StreamTrait>, Arc<Message>)>,
}
impl super::service::Reset for State {
fn reset(&mut self) {
self.stream.take();
}
}
pub fn run(sp: GenericService, state: &mut State) -> ResultType<()> {
sp.snapshot(|sps| {
match &state.stream {
None => {
state.stream = Some(play(&sp)?);
}
_ => {}
}
if let Some((_, format)) = &state.stream {
sps.send_shared(format.clone());
}
Ok(())
})?;
Ok(())
}
fn send(
data: &[f32],
sample_rate0: u32,
sample_rate: u32,
channels: u16,
encoder: &mut Encoder,
sp: &GenericService,
) {
let buffer;
let data = if sample_rate0 != sample_rate {
buffer = crate::common::resample_channels(data, sample_rate0, sample_rate, channels);
&buffer
} else {
data
};
send_f32(data, encoder, sp);
}
#[cfg(windows)]
fn get_device() -> ResultType<(Device, SupportedStreamConfig)> {
let audio_input = Config::get_option("audio-input");
if !audio_input.is_empty() {
return get_audio_input(&audio_input);
}
let device = HOST
.default_output_device()
.with_context(|| "Failed to get default output device for loopback")?;
log::info!(
"Default output device: {}",
device.name().unwrap_or("".to_owned())
);
let format = device
.default_output_config()
.map_err(|e| anyhow!(e))
.with_context(|| "Failed to get default output format")?;
log::info!("Default output format: {:?}", format);
Ok((device, format))
}
#[cfg(not(windows))]
fn get_device() -> ResultType<(Device, SupportedStreamConfig)> {
let audio_input = Config::get_option("audio-input");
get_audio_input(&audio_input)
}
fn get_audio_input(audio_input: &str) -> ResultType<(Device, SupportedStreamConfig)> {
let mut device = None;
if !audio_input.is_empty() {
for d in HOST
.devices()
.with_context(|| "Failed to get audio devices")?
{
if d.name().unwrap_or("".to_owned()) == audio_input {
device = Some(d);
break;
}
}
}
if device.is_none() {
device = Some(
HOST.default_input_device()
.with_context(|| "Failed to get default input device for loopback")?,
);
}
let device = device.unwrap();
log::info!("Input device: {}", device.name().unwrap_or("".to_owned()));
let format = device
.default_input_config()
.map_err(|e| anyhow!(e))
.with_context(|| "Failed to get default input format")?;
log::info!("Default input format: {:?}", format);
Ok((device, format))
}
fn play(sp: &GenericService) -> ResultType<(Box<dyn StreamTrait>, Arc<Message>)> {
let (device, config) = get_device()?;
let sp = sp.clone();
let err_fn = move |err| {
// too many UnknownErrno, will improve later
log::trace!("an error occurred on stream: {}", err);
};
// Sample rate must be one of 8000, 12000, 16000, 24000, or 48000.
let sample_rate_0 = config.sample_rate().0;
let sample_rate = if sample_rate_0 < 12000 {
8000
} else if sample_rate_0 < 16000 {
12000
} else if sample_rate_0 < 24000 {
16000
} else if sample_rate_0 < 48000 {
24000
} else {
48000
};
log::debug!("Audio sample rate : {}", sample_rate);
unsafe {
AUDIO_ZERO_COUNT = 0;
}
let mut encoder = Encoder::new(
sample_rate,
if config.channels() > 1 { Stereo } else { Mono },
LowDelay,
)?;
let channels = config.channels();
let stream = match config.sample_format() {
cpal::SampleFormat::F32 => device.build_input_stream(
&config.into(),
move |data, _: &_| {
send(
data,
sample_rate_0,
sample_rate,
channels,
&mut encoder,
&sp,
);
},
err_fn,
)?,
cpal::SampleFormat::I16 => device.build_input_stream(
&config.into(),
move |data: &[i16], _: &_| {
let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect();
send(
&buffer,
sample_rate_0,
sample_rate,
channels,
&mut encoder,
&sp,
);
},
err_fn,
)?,
cpal::SampleFormat::U16 => device.build_input_stream(
&config.into(),
move |data: &[u16], _: &_| {
let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect();
send(
&buffer,
sample_rate_0,
sample_rate,
channels,
&mut encoder,
&sp,
);
},
err_fn,
)?,
};
stream.play()?;
Ok((
Box::new(stream),
Arc::new(create_format_msg(sample_rate, channels)),
))
}
}
fn create_format_msg(sample_rate: u32, channels: u16) -> Message {
let format = AudioFormat {
sample_rate,
channels: channels as _,
..Default::default()
};
let mut misc = Misc::new();
misc.set_audio_format(format);
let mut msg = Message::new();
msg.set_misc(misc);
msg
}
// use AUDIO_ZERO_COUNT for the Noise(Zero) Gate Attack Time
// every audio data length is set to 480
// MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows)
const MAX_AUDIO_ZERO_COUNT: u16 = 800;
static mut AUDIO_ZERO_COUNT: u16 = 0;
fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
if data.iter().filter(|x| **x != 0.).next().is_some() {
unsafe {
AUDIO_ZERO_COUNT = 0;
}
} else {
unsafe {
if AUDIO_ZERO_COUNT > MAX_AUDIO_ZERO_COUNT {
if AUDIO_ZERO_COUNT == MAX_AUDIO_ZERO_COUNT + 1 {
log::debug!("Audio Zero Gate Attack");
AUDIO_ZERO_COUNT += 1;
}
return;
}
AUDIO_ZERO_COUNT += 1;
}
}
#[cfg(target_os = "android")]
{
// the permitted opus data size are 120, 240, 480, 960, 1920, and 2880
// if data size is bigger than BATCH_SIZE, AND is an integer multiple of BATCH_SIZE
// then upload in batches
const BATCH_SIZE: usize = 960;
let input_size = data.len();
if input_size > BATCH_SIZE && input_size % BATCH_SIZE == 0 {
let n = input_size / BATCH_SIZE;
for i in 0..n {
match encoder
.encode_vec_float(&data[i * BATCH_SIZE..(i + 1) * BATCH_SIZE], BATCH_SIZE)
{
Ok(data) => {
let mut msg_out = Message::new();
msg_out.set_audio_frame(AudioFrame {
data,
..Default::default()
});
sp.send(msg_out);
}
Err(_) => {}
}
}
} else {
log::debug!("invalid audio data size:{} ", input_size);
return;
}
}
#[cfg(not(target_os = "android"))]
match encoder.encode_vec_float(data, data.len() * 6) {
Ok(data) => {
let mut msg_out = Message::new();
msg_out.set_audio_frame(AudioFrame {
data,
..Default::default()
});
sp.send(msg_out);
}
Err(_) => {}
}
}
#[cfg(test)]
mod tests {
#[cfg(target_os = "linux")]
#[test]
fn test_pulse() {
use libpulse_binding as pulse;
use libpulse_simple_binding as psimple;
let spec = pulse::sample::Spec {
format: pulse::sample::SAMPLE_FLOAT32NE,
channels: 2,
rate: 24000,
};
let hspec = hound::WavSpec {
channels: spec.channels as _,
sample_rate: spec.rate as _,
bits_per_sample: (4 * 8) as _,
sample_format: hound::SampleFormat::Float,
};
const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav");
let mut writer =
hound::WavWriter::create(PATH, hspec).expect("Could not create hsound writer");
let device = crate::platform::linux::get_pa_monitor();
let s = psimple::Simple::new(
None, // Use the default server
"Test", // Our applications name
pulse::stream::Direction::Record, // We want a record stream
Some(&device), // Use the default device
"Test", // Description of our stream
&spec, // Our sample format
None, // Use default channel map
None, // Use default buffering attributes
)
.expect("Could not create simple pulse");
let mut out: Vec<u8> = Vec::with_capacity(1024);
unsafe {
out.set_len(out.capacity());
}
for _ in 0..600 {
s.read(&mut out).expect("Could not read pcm");
let out2 =
unsafe { std::slice::from_raw_parts::<f32>(out.as_ptr() as _, out.len() / 4) };
for v in out2 {
writer.write_sample(*v).ok();
}
}
println!("{:?} {}", device, out.len());
writer.finalize().expect("Could not finalize writer");
}
}

View File

@ -3,119 +3,49 @@ pub use crate::common::{
check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME,
CONTENT,
};
use clipboard_master::{CallbackResult, ClipboardHandler, Master};
use hbb_common::{anyhow, ResultType};
use std::{
io, sync,
sync::{
atomic::{AtomicBool, Ordering},
mpsc::SyncSender,
},
time::Duration,
};
struct State {
ctx: Option<ClipboardContext>,
}
impl Default for State {
fn default() -> Self {
let ctx = match ClipboardContext::new() {
Ok(ctx) => Some(ctx),
Err(err) => {
log::error!("Failed to start {}: {}", NAME, err);
None
}
};
Self { ctx }
}
}
impl super::service::Reset for State {
fn reset(&mut self) {
*CONTENT.lock().unwrap() = Default::default();
}
}
pub fn new() -> GenericService {
let sp = GenericService::new(NAME, true);
sp.run::<_>(listen::run);
sp.repeat::<State, _>(INTERVAL, run);
sp
}
mod listen {
use super::*;
static RUNNING: AtomicBool = AtomicBool::new(true);
static WAIT: Duration = Duration::from_millis(33);
struct ClipHandle {
tx: SyncSender<()>,
}
impl ClipboardHandler for ClipHandle {
fn on_clipboard_change(&mut self) -> CallbackResult {
if !RUNNING.load(Ordering::SeqCst) {
return CallbackResult::Stop;
}
let _ = self.tx.send(());
CallbackResult::Next
fn run(sp: GenericService, state: &mut State) -> ResultType<()> {
if let Some(ctx) = state.ctx.as_mut() {
if let Some(msg) = check_clipboard(ctx, None) {
sp.send(msg);
}
fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult {
if !RUNNING.load(Ordering::SeqCst) {
CallbackResult::Stop
} else {
CallbackResult::StopWithError(error)
sp.snapshot(|sps| {
let txt = crate::CONTENT.lock().unwrap().clone();
if !txt.is_empty() {
let msg_out = crate::create_clipboard_msg(txt);
sps.send_shared(Arc::new(msg_out));
}
}
}
#[tokio::main]
pub async fn run(sp: GenericService) -> ResultType<()> {
let mut ctx = match ClipboardContext::new() {
Ok(ctx) => ctx,
Err(err) => {
log::error!("Failed to start {}: {}", NAME, err);
return Err(anyhow::Error::from(err));
}
};
if !RUNNING.load(Ordering::SeqCst) {
RUNNING.store(true, Ordering::SeqCst);
}
let (tx, rx) = sync::mpsc::sync_channel(12);
let listener = tokio::spawn(async {
log::info!("Clipboard listener running!");
let _ = Master::new(ClipHandle { tx }).run();
});
check_clipboard(&mut ctx, None); // initialize CONTENT for snapshot
while sp.ok() {
let mut update = None;
sp.snapshot(|sps| {
if sps.has_subscribes() {
update = check_clipboard(&mut ctx, None);
}
// if there is update, msg will be later together,
// otherwise it will be only sent to new subscriber,
// but old subscribers ignored
if update.is_none() {
let txt = crate::CONTENT.lock().unwrap().clone();
if !txt.is_empty() {
let msg_out = crate::create_clipboard_msg(txt);
sps.send_shared(Arc::new(msg_out));
}
}
Ok(())
})?;
if let Some(msg) = update {
sp.send(msg);
}
if let Ok(_) = rx.recv_timeout(WAIT) {
if let Some(msg) = check_clipboard(&mut ctx, None) {
sp.send(msg);
}
}
}
RUNNING.store(false, Ordering::SeqCst);
trigger(&mut ctx);
let _ = listener.await;
log::info!("Clipboard listener stopped!");
*CONTENT.lock().unwrap() = Default::default();
Ok(())
}
fn trigger(ctx: &mut ClipboardContext) {
let mut old_text = "".to_owned();
let _ = match ctx.get_text() {
Ok(text) => {
old_text = text;
}
Err(_) => {}
};
ctx.set_text(old_text).ok();
Ok(())
})?;
}
Ok(())
}

View File

@ -1,7 +1,11 @@
use super::{input_service::*, *};
#[cfg(windows)]
use crate::clipboard_file::*;
use crate::{common::update_clipboard, ipc};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::common::update_clipboard;
use crate::ipc;
#[cfg(any(target_os = "android", target_os = "ios"))]
use crate::{common::MOBILE_INFO2, mobile::connection_manager::start_channel};
use hbb_common::{
config::Config,
fs,
@ -15,14 +19,22 @@ use hbb_common::{
},
tokio_util::codec::{BytesCodec, Framed},
};
#[cfg(any(target_os = "android", target_os = "ios"))]
use scrap::android::call_input_service_mouse_input;
use serde_json::{json, value::Value};
use sha2::{Digest, Sha256};
use std::sync::mpsc as std_mpsc;
use std::sync::{
atomic::{AtomicI64, Ordering},
mpsc as std_mpsc,
};
pub type Sender = mpsc::UnboundedSender<(Instant, Arc<Message>)>;
lazy_static::lazy_static! {
static ref LOGIN_FAILURES: Arc::<Mutex<HashMap<String, (i32, i32, i32)>>> = Default::default();
}
pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0);
pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0);
#[derive(Clone, Default)]
pub struct ConnInner {
@ -67,6 +79,8 @@ pub struct Connection {
enable_file_transfer: bool, // by peer
tx_input: std_mpsc::Sender<MessageInput>, // handle input messages
video_ack_required: bool,
peer_info: (String, String),
api_server: String,
}
impl Subscriber for ConnInner {
@ -151,12 +165,18 @@ impl Connection {
disable_clipboard: false,
tx_input,
video_ack_required: false,
peer_info: Default::default(),
api_server: "".to_owned(),
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
tokio::spawn(async move {
if let Err(err) = start_ipc(rx_to_cm, tx_from_cm).await {
log::error!("ipc to connection manager exit: {}", err);
}
});
#[cfg(target_os = "android")]
start_channel(rx_to_cm, tx_from_cm);
if !conn.on_open(addr).await {
return;
}
@ -184,6 +204,7 @@ impl Connection {
},
);
#[cfg(not(any(target_os = "android", target_os = "ios")))]
std::thread::spawn(move || Self::handle_input(rx_input, tx_cloned));
loop {
@ -252,9 +273,9 @@ impl Connection {
ipc::Data::RawMessage(bytes) => {
allow_err!(conn.stream.send_raw(bytes).await);
}
#[cfg(windows)]
ipc::Data::ClipbaordFile(_clip) => {
if conn.file_transfer_enabled() {
#[cfg(windows)]
allow_err!(conn.stream.send(&clip_2_msg(_clip)).await);
}
}
@ -291,6 +312,7 @@ impl Connection {
} else {
conn.timer = time::interval_at(Instant::now() + SEC30, SEC30);
}
conn.post_audit(json!({})); // heartbeat
},
Some((instant, value)) = rx_video.recv() => {
if !conn.video_ack_required {
@ -345,9 +367,13 @@ impl Connection {
conn.on_close(&err.to_string(), false);
}
conn.post_audit(json!({
"action": "close",
}));
log::info!("#{} connection loop exited", id);
}
#[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();
@ -398,6 +424,7 @@ impl Connection {
}
},
Err(err) => {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if block_input_mode {
let _ = crate::platform::block_input(true);
}
@ -410,6 +437,7 @@ 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 {
@ -443,7 +471,7 @@ impl Connection {
rx_from_cm: &mut mpsc::UnboundedReceiver<Data>,
) -> ResultType<()> {
let mut last_recv_time = Instant::now();
if let Some(forward) = self.port_forward_socket.as_mut() {
if let Some(mut forward) = self.port_forward_socket.take() {
log::info!("Running port forwarding loop");
self.stream.set_raw();
loop {
@ -476,6 +504,7 @@ impl Connection {
if last_recv_time.elapsed() >= H1 {
bail!("Timeout");
}
self.post_audit(json!({})); // heartbeat
}
}
}
@ -523,25 +552,78 @@ impl Connection {
let mut msg_out = Message::new();
msg_out.set_hash(self.hash.clone());
self.send(msg_out).await;
self.get_api_server();
self.post_audit(json!({
"ip": addr.ip(),
"action": "new",
}));
true
}
fn get_api_server(&mut self) {
self.api_server = crate::get_audit_server(
Config::get_option("api-server"),
Config::get_option("custom-rendezvous-server"),
);
}
fn post_audit(&self, v: Value) {
if self.api_server.is_empty() {
return;
}
let url = self.api_server.clone();
let mut v = v;
v["id"] = json!(Config::get_id());
v["uuid"] = json!(base64::encode(crate::get_uuid()));
v["Id"] = json!(self.inner.id);
tokio::spawn(async move {
allow_err!(Self::post_audit_async(url, v).await);
});
}
#[inline]
async fn post_audit_async(url: String, v: Value) -> ResultType<()> {
crate::post_request(url, v.to_string(), "").await?;
Ok(())
}
async fn send_logon_response(&mut self) {
if self.authorized {
return;
}
let conn_type = if self.file_transfer.is_some() {
1
} else if self.port_forward_socket.is_some() {
2
} else {
0
};
self.post_audit(json!({"peer": self.peer_info, "Type": conn_type}));
#[allow(unused_mut)]
let mut username = crate::platform::get_active_username();
let mut res = LoginResponse::new();
let mut pi = PeerInfo {
username: username.clone(),
conn_id: self.inner.id,
version: crate::VERSION.to_owned(),
..Default::default()
};
#[cfg(not(target_os = "android"))]
{
pi.hostname = whoami::hostname();
pi.platform = whoami::platform().to_string();
}
#[cfg(target_os = "android")]
{
pi.hostname = MOBILE_INFO2.lock().unwrap().clone();
pi.platform = "Android".into();
}
if self.port_forward_socket.is_some() {
let mut msg_out = Message::new();
res.set_peer_info(PeerInfo {
hostname: whoami::hostname(),
username,
platform: whoami::platform().to_string(),
version: crate::VERSION.to_owned(),
..Default::default()
});
res.set_peer_info(pi);
msg_out.set_login_response(res);
self.send(msg_out).await;
return;
@ -566,20 +648,15 @@ impl Connection {
if crate::platform::is_root() {
sas_enabled = true;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if self.file_transfer.is_some() {
if crate::platform::is_prelogin() || self.tx_to_cm.send(ipc::Data::Test).is_err() {
username = "".to_owned();
}
}
self.authorized = true;
let mut pi = PeerInfo {
hostname: whoami::hostname(),
username,
platform: whoami::platform().to_string(),
version: crate::VERSION.to_owned(),
sas_enabled,
..Default::default()
};
pi.username = username;
pi.sas_enabled = sas_enabled;
let mut sub_service = false;
if self.file_transfer.is_some() {
res.set_peer_info(pi);
@ -641,7 +718,8 @@ impl Connection {
self.file && self.enable_file_transfer
}
async fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) {
fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) {
self.peer_info = (peer_id.clone(), name.clone());
self.send_to_cm(ipc::Data::Login {
id: self.inner.id(),
is_file_transfer: self.file_transfer.is_some(),
@ -753,7 +831,7 @@ impl Connection {
if !crate::is_ip(&lr.username) && lr.username != Config::get_id() {
self.send_login_error("Offline").await;
} else if lr.password.is_empty() {
self.try_start_cm(lr.my_id, lr.my_name, false).await;
self.try_start_cm(lr.my_id, lr.my_name, false);
} else {
let mut hasher = Sha256::new();
hasher.update(&Config::get_password());
@ -787,13 +865,13 @@ impl Connection {
.unwrap()
.insert(self.ip.clone(), failure);
self.send_login_error("Wrong Password").await;
self.try_start_cm(lr.my_id, lr.my_name, false).await;
self.try_start_cm(lr.my_id, lr.my_name, false);
} else {
if failure.0 != 0 {
LOGIN_FAILURES.lock().unwrap().remove(&self.ip);
}
self.try_start_cm(lr.my_id, lr.my_name, true);
self.send_logon_response().await;
self.try_start_cm(lr.my_id, lr.my_name, true).await;
if self.port_forward_socket.is_some() {
return false;
}
@ -814,12 +892,26 @@ impl Connection {
} else if self.authorized {
match msg.union {
Some(message::Union::mouse_event(me)) => {
#[cfg(any(target_os = "android", target_os = "ios"))]
if let Err(e) = call_input_service_mouse_input(me.mask, me.x, me.y) {
log::debug!("call_input_service_mouse_input fail:{}", e);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if self.keyboard {
if is_left_up(&me) {
CLICK_TIME.store(crate::get_time(), Ordering::SeqCst);
} else {
MOUSE_MOVE_TIME.store(crate::get_time(), Ordering::SeqCst);
}
self.input_mouse(me, self.inner.id());
}
}
Some(message::Union::key_event(me)) => {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if self.keyboard {
if is_enter(&me) {
CLICK_TIME.store(crate::get_time(), Ordering::SeqCst);
}
// handle all down as press
// fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which
// make sure all key are released
@ -843,7 +935,9 @@ impl Connection {
}
}
}
Some(message::Union::clipboard(cb)) => {
Some(message::Union::clipboard(cb)) =>
{
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if self.clipboard {
update_clipboard(cb, None);
}
@ -868,18 +962,21 @@ impl Connection {
self.send(fs::new_error(f.id, err, -1)).await;
}
Ok(files) => {
self.send(fs::new_dir(f.id, files)).await;
self.send(fs::new_dir(f.id, f.path, files)).await;
}
}
}
Some(file_action::Union::send(s)) => {
let id = s.id;
match fs::TransferJob::new_read(id, s.path, s.include_hidden) {
let path = s.path;
match fs::TransferJob::new_read(id, path.clone(), s.include_hidden)
{
Err(err) => {
self.send(fs::new_error(id, err, 0)).await;
}
Ok(job) => {
self.send(fs::new_dir(id, job.files().to_vec())).await;
self.send(fs::new_dir(id, path, job.files().to_vec()))
.await;
self.read_jobs.push(job);
self.timer = time::interval(MILLI1);
}
@ -1020,7 +1117,9 @@ impl Connection {
if let Ok(q) = o.enable_file_transfer.enum_value() {
if q != BoolOption::NotSet {
self.enable_file_transfer = q == BoolOption::Yes;
self.send_to_cm(ipc::Data::ClipboardFileEnabled(self.file_transfer_enabled()));
self.send_to_cm(ipc::Data::ClipboardFileEnabled(
self.file_transfer_enabled(),
));
}
}
if let Ok(q) = o.disable_clipboard.enum_value() {
@ -1071,6 +1170,7 @@ impl Connection {
}
log::info!("#{} Connection closed: {}", self.inner.id(), reason);
if lock && self.lock_after_session_end && self.keyboard {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
lock_screen();
}
self.tx_to_cm.send(ipc::Data::Close).ok();
@ -1091,6 +1191,7 @@ impl Connection {
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
async fn start_ipc(
mut rx_to_cm: mpsc::UnboundedReceiver<ipc::Data>,
tx_from_cm: mpsc::UnboundedSender<ipc::Data>,
@ -1148,7 +1249,16 @@ async fn start_ipc(
return Err(err.into());
}
Ok(Some(data)) => {
tx_from_cm.send(data)?;
match data {
ipc::Data::ClickTime(_)=> {
let ct = CLICK_TIME.load(Ordering::SeqCst);
let data = ipc::Data::ClickTime(ct);
stream.send(&data).await?;
}
_ => {
tx_from_cm.send(data)?;
}
}
}
_ => {}
}

View File

@ -1,275 +1,275 @@
use super::*;
use std::{
collections::HashSet,
thread::{self, JoinHandle},
time,
};
pub trait Service: Send + Sync {
fn name(&self) -> &'static str;
fn on_subscribe(&self, sub: ConnInner);
fn on_unsubscribe(&self, id: i32);
fn is_subed(&self, id: i32) -> bool;
fn join(&self);
}
pub trait Subscriber: Default + Send + Sync + 'static {
fn id(&self) -> i32;
fn send(&mut self, msg: Arc<Message>);
}
#[derive(Default)]
pub struct ServiceInner<T: Subscriber + From<ConnInner>> {
name: &'static str,
handle: Option<JoinHandle<()>>,
subscribes: HashMap<i32, T>,
new_subscribes: HashMap<i32, T>,
active: bool,
need_snapshot: bool,
}
pub trait Reset {
fn reset(&mut self);
}
pub struct ServiceTmpl<T: Subscriber + From<ConnInner>>(Arc<RwLock<ServiceInner<T>>>);
pub struct ServiceSwap<T: Subscriber + From<ConnInner>>(ServiceTmpl<T>);
pub type GenericService = ServiceTmpl<ConnInner>;
pub const HIBERNATE_TIMEOUT: u64 = 30;
pub const MAX_ERROR_TIMEOUT: u64 = 1_000;
impl<T: Subscriber + From<ConnInner>> ServiceInner<T> {
fn send_new_subscribes(&mut self, msg: Arc<Message>) {
for s in self.new_subscribes.values_mut() {
s.send(msg.clone());
}
}
fn swap_new_subscribes(&mut self) {
for (_, s) in self.new_subscribes.drain() {
self.subscribes.insert(s.id(), s);
}
assert!(self.new_subscribes.is_empty());
}
#[inline]
fn has_subscribes(&self) -> bool {
self.subscribes.len() > 0 || self.new_subscribes.len() > 0
}
}
impl<T: Subscriber + From<ConnInner>> Service for ServiceTmpl<T> {
#[inline]
fn name(&self) -> &'static str {
self.0.read().unwrap().name
}
fn is_subed(&self, id: i32) -> bool {
self.0.read().unwrap().subscribes.get(&id).is_some()
}
fn on_subscribe(&self, sub: ConnInner) {
let mut lock = self.0.write().unwrap();
if lock.subscribes.get(&sub.id()).is_some() {
return;
}
if lock.need_snapshot {
lock.new_subscribes.insert(sub.id(), sub.into());
} else {
lock.subscribes.insert(sub.id(), sub.into());
}
}
fn on_unsubscribe(&self, id: i32) {
let mut lock = self.0.write().unwrap();
if let None = lock.subscribes.remove(&id) {
lock.new_subscribes.remove(&id);
}
}
fn join(&self) {
self.0.write().unwrap().active = false;
self.0.write().unwrap().handle.take().map(JoinHandle::join);
}
}
impl<T: Subscriber + From<ConnInner>> Clone for ServiceTmpl<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
pub fn new(name: &'static str, need_snapshot: bool) -> Self {
Self(Arc::new(RwLock::new(ServiceInner::<T> {
name,
active: true,
need_snapshot,
..Default::default()
})))
}
#[inline]
pub fn has_subscribes(&self) -> bool {
self.0.read().unwrap().has_subscribes()
}
#[inline]
pub fn ok(&self) -> bool {
let lock = self.0.read().unwrap();
lock.active && lock.has_subscribes()
}
pub fn snapshot<F>(&self, callback: F) -> ResultType<()>
where
F: FnMut(ServiceSwap<T>) -> ResultType<()>,
{
if self.0.read().unwrap().new_subscribes.len() > 0 {
log::info!("Call snapshot of {} service", self.name());
let mut callback = callback;
callback(ServiceSwap::<T>(self.clone()))?;
}
Ok(())
}
#[inline]
pub fn send(&self, msg: Message) {
self.send_shared(Arc::new(msg));
}
pub fn send_to(&self, msg: Message, id: i32) {
if let Some(s) = self.0.write().unwrap().subscribes.get_mut(&id) {
s.send(Arc::new(msg));
}
}
pub fn send_shared(&self, msg: Arc<Message>) {
let mut lock = self.0.write().unwrap();
for s in lock.subscribes.values_mut() {
s.send(msg.clone());
}
}
pub fn send_video_frame(&self, msg: Message) -> HashSet<i32> {
self.send_video_frame_shared(Arc::new(msg))
}
pub fn send_video_frame_shared(&self, msg: Arc<Message>) -> HashSet<i32> {
let mut conn_ids = HashSet::new();
let mut lock = self.0.write().unwrap();
for s in lock.subscribes.values_mut() {
s.send(msg.clone());
conn_ids.insert(s.id());
}
conn_ids
}
pub fn send_without(&self, msg: Message, sub: i32) {
let mut lock = self.0.write().unwrap();
let msg = Arc::new(msg);
for s in lock.subscribes.values_mut() {
if sub != s.id() {
s.send(msg.clone());
}
}
}
pub fn repeat<S, F>(&self, interval_ms: u64, callback: F)
where
F: 'static + FnMut(Self, &mut S) -> ResultType<()> + Send,
S: 'static + Default + Reset,
{
let interval = time::Duration::from_millis(interval_ms);
let mut callback = callback;
let sp = self.clone();
let thread = thread::spawn(move || {
let mut state = S::default();
let mut may_reset = false;
while sp.active() {
let now = time::Instant::now();
if sp.has_subscribes() {
if let Err(err) = callback(sp.clone(), &mut state) {
log::error!("Error of {} service: {}", sp.name(), err);
thread::sleep(time::Duration::from_millis(MAX_ERROR_TIMEOUT));
#[cfg(windows)]
crate::platform::windows::try_change_desktop();
}
if !may_reset {
may_reset = true;
}
} else if may_reset {
state.reset();
may_reset = false;
}
let elapsed = now.elapsed();
if elapsed < interval {
thread::sleep(interval - elapsed);
}
}
});
self.0.write().unwrap().handle = Some(thread);
}
pub fn run<F>(&self, callback: F)
where
F: 'static + FnMut(Self) -> ResultType<()> + Send,
{
let sp = self.clone();
let mut callback = callback;
let thread = thread::spawn(move || {
let mut error_timeout = HIBERNATE_TIMEOUT;
while sp.active() {
if sp.has_subscribes() {
log::debug!("Enter {} service inner loop", sp.name());
let tm = time::Instant::now();
if let Err(err) = callback(sp.clone()) {
log::error!("Error of {} service: {}", sp.name(), err);
if tm.elapsed() > time::Duration::from_millis(MAX_ERROR_TIMEOUT) {
error_timeout = HIBERNATE_TIMEOUT;
} else {
error_timeout *= 2;
}
if error_timeout > MAX_ERROR_TIMEOUT {
error_timeout = MAX_ERROR_TIMEOUT;
}
thread::sleep(time::Duration::from_millis(error_timeout));
#[cfg(windows)]
crate::platform::windows::try_change_desktop();
} else {
log::debug!("Exit {} service inner loop", sp.name());
}
}
thread::sleep(time::Duration::from_millis(HIBERNATE_TIMEOUT));
}
});
self.0.write().unwrap().handle = Some(thread);
}
#[inline]
pub fn active(&self) -> bool {
self.0.read().unwrap().active
}
}
impl<T: Subscriber + From<ConnInner>> ServiceSwap<T> {
#[inline]
pub fn send(&self, msg: Message) {
self.send_shared(Arc::new(msg));
}
#[inline]
pub fn send_shared(&self, msg: Arc<Message>) {
(self.0).0.write().unwrap().send_new_subscribes(msg);
}
#[inline]
pub fn has_subscribes(&self) -> bool {
(self.0).0.read().unwrap().subscribes.len() > 0
}
}
impl<T: Subscriber + From<ConnInner>> Drop for ServiceSwap<T> {
fn drop(&mut self) {
(self.0).0.write().unwrap().swap_new_subscribes();
}
}
use super::*;
use std::{
collections::HashSet,
thread::{self, JoinHandle},
time,
};
pub trait Service: Send + Sync {
fn name(&self) -> &'static str;
fn on_subscribe(&self, sub: ConnInner);
fn on_unsubscribe(&self, id: i32);
fn is_subed(&self, id: i32) -> bool;
fn join(&self);
}
pub trait Subscriber: Default + Send + Sync + 'static {
fn id(&self) -> i32;
fn send(&mut self, msg: Arc<Message>);
}
#[derive(Default)]
pub struct ServiceInner<T: Subscriber + From<ConnInner>> {
name: &'static str,
handle: Option<JoinHandle<()>>,
subscribes: HashMap<i32, T>,
new_subscribes: HashMap<i32, T>,
active: bool,
need_snapshot: bool,
}
pub trait Reset {
fn reset(&mut self);
}
pub struct ServiceTmpl<T: Subscriber + From<ConnInner>>(Arc<RwLock<ServiceInner<T>>>);
pub struct ServiceSwap<T: Subscriber + From<ConnInner>>(ServiceTmpl<T>);
pub type GenericService = ServiceTmpl<ConnInner>;
pub const HIBERNATE_TIMEOUT: u64 = 30;
pub const MAX_ERROR_TIMEOUT: u64 = 1_000;
impl<T: Subscriber + From<ConnInner>> ServiceInner<T> {
fn send_new_subscribes(&mut self, msg: Arc<Message>) {
for s in self.new_subscribes.values_mut() {
s.send(msg.clone());
}
}
fn swap_new_subscribes(&mut self) {
for (_, s) in self.new_subscribes.drain() {
self.subscribes.insert(s.id(), s);
}
assert!(self.new_subscribes.is_empty());
}
#[inline]
fn has_subscribes(&self) -> bool {
self.subscribes.len() > 0 || self.new_subscribes.len() > 0
}
}
impl<T: Subscriber + From<ConnInner>> Service for ServiceTmpl<T> {
#[inline]
fn name(&self) -> &'static str {
self.0.read().unwrap().name
}
fn is_subed(&self, id: i32) -> bool {
self.0.read().unwrap().subscribes.get(&id).is_some()
}
fn on_subscribe(&self, sub: ConnInner) {
let mut lock = self.0.write().unwrap();
if lock.subscribes.get(&sub.id()).is_some() {
return;
}
if lock.need_snapshot {
lock.new_subscribes.insert(sub.id(), sub.into());
} else {
lock.subscribes.insert(sub.id(), sub.into());
}
}
fn on_unsubscribe(&self, id: i32) {
let mut lock = self.0.write().unwrap();
if let None = lock.subscribes.remove(&id) {
lock.new_subscribes.remove(&id);
}
}
fn join(&self) {
self.0.write().unwrap().active = false;
self.0.write().unwrap().handle.take().map(JoinHandle::join);
}
}
impl<T: Subscriber + From<ConnInner>> Clone for ServiceTmpl<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
pub fn new(name: &'static str, need_snapshot: bool) -> Self {
Self(Arc::new(RwLock::new(ServiceInner::<T> {
name,
active: true,
need_snapshot,
..Default::default()
})))
}
#[inline]
pub fn has_subscribes(&self) -> bool {
self.0.read().unwrap().has_subscribes()
}
#[inline]
pub fn ok(&self) -> bool {
let lock = self.0.read().unwrap();
lock.active && lock.has_subscribes()
}
pub fn snapshot<F>(&self, callback: F) -> ResultType<()>
where
F: FnMut(ServiceSwap<T>) -> ResultType<()>,
{
if self.0.read().unwrap().new_subscribes.len() > 0 {
log::info!("Call snapshot of {} service", self.name());
let mut callback = callback;
callback(ServiceSwap::<T>(self.clone()))?;
}
Ok(())
}
#[inline]
pub fn send(&self, msg: Message) {
self.send_shared(Arc::new(msg));
}
pub fn send_to(&self, msg: Message, id: i32) {
if let Some(s) = self.0.write().unwrap().subscribes.get_mut(&id) {
s.send(Arc::new(msg));
}
}
pub fn send_shared(&self, msg: Arc<Message>) {
let mut lock = self.0.write().unwrap();
for s in lock.subscribes.values_mut() {
s.send(msg.clone());
}
}
pub fn send_video_frame(&self, msg: Message) -> HashSet<i32> {
self.send_video_frame_shared(Arc::new(msg))
}
pub fn send_video_frame_shared(&self, msg: Arc<Message>) -> HashSet<i32> {
let mut conn_ids = HashSet::new();
let mut lock = self.0.write().unwrap();
for s in lock.subscribes.values_mut() {
s.send(msg.clone());
conn_ids.insert(s.id());
}
conn_ids
}
pub fn send_without(&self, msg: Message, sub: i32) {
let mut lock = self.0.write().unwrap();
let msg = Arc::new(msg);
for s in lock.subscribes.values_mut() {
if sub != s.id() {
s.send(msg.clone());
}
}
}
pub fn repeat<S, F>(&self, interval_ms: u64, callback: F)
where
F: 'static + FnMut(Self, &mut S) -> ResultType<()> + Send,
S: 'static + Default + Reset,
{
let interval = time::Duration::from_millis(interval_ms);
let mut callback = callback;
let sp = self.clone();
let thread = thread::spawn(move || {
let mut state = S::default();
let mut may_reset = false;
while sp.active() {
let now = time::Instant::now();
if sp.has_subscribes() {
if let Err(err) = callback(sp.clone(), &mut state) {
log::error!("Error of {} service: {}", sp.name(), err);
thread::sleep(time::Duration::from_millis(MAX_ERROR_TIMEOUT));
#[cfg(windows)]
crate::platform::windows::try_change_desktop();
}
if !may_reset {
may_reset = true;
}
} else if may_reset {
state.reset();
may_reset = false;
}
let elapsed = now.elapsed();
if elapsed < interval {
thread::sleep(interval - elapsed);
}
}
});
self.0.write().unwrap().handle = Some(thread);
}
pub fn run<F>(&self, callback: F)
where
F: 'static + FnMut(Self) -> ResultType<()> + Send,
{
let sp = self.clone();
let mut callback = callback;
let thread = thread::spawn(move || {
let mut error_timeout = HIBERNATE_TIMEOUT;
while sp.active() {
if sp.has_subscribes() {
log::debug!("Enter {} service inner loop", sp.name());
let tm = time::Instant::now();
if let Err(err) = callback(sp.clone()) {
log::error!("Error of {} service: {}", sp.name(), err);
if tm.elapsed() > time::Duration::from_millis(MAX_ERROR_TIMEOUT) {
error_timeout = HIBERNATE_TIMEOUT;
} else {
error_timeout *= 2;
}
if error_timeout > MAX_ERROR_TIMEOUT {
error_timeout = MAX_ERROR_TIMEOUT;
}
thread::sleep(time::Duration::from_millis(error_timeout));
#[cfg(windows)]
crate::platform::windows::try_change_desktop();
} else {
log::debug!("Exit {} service inner loop", sp.name());
}
}
thread::sleep(time::Duration::from_millis(HIBERNATE_TIMEOUT));
}
});
self.0.write().unwrap().handle = Some(thread);
}
#[inline]
pub fn active(&self) -> bool {
self.0.read().unwrap().active
}
}
impl<T: Subscriber + From<ConnInner>> ServiceSwap<T> {
#[inline]
pub fn send(&self, msg: Message) {
self.send_shared(Arc::new(msg));
}
#[inline]
pub fn send_shared(&self, msg: Arc<Message>) {
(self.0).0.write().unwrap().send_new_subscribes(msg);
}
#[inline]
pub fn has_subscribes(&self) -> bool {
(self.0).0.read().unwrap().subscribes.len() > 0
}
}
impl<T: Subscriber + From<ConnInner>> Drop for ServiceSwap<T> {
fn drop(&mut self) {
(self.0).0.write().unwrap().swap_new_subscribes();
}
}

View File

@ -32,7 +32,6 @@ use std::{
io::ErrorKind::WouldBlock,
time::{self, Duration, Instant},
};
use virtual_display;
pub const NAME: &'static str = "video";
@ -133,7 +132,7 @@ fn check_display_changed(
last_width: usize,
last_hegiht: usize,
) -> bool {
let displays = match try_get_displays() {
let displays = match Display::all() {
Ok(d) => d,
_ => return false,
};
@ -158,30 +157,20 @@ fn check_display_changed(
}
fn run(sp: GenericService) -> ResultType<()> {
let num_displays = Display::all()?.len();
if num_displays == 0 {
// Device may sometimes be uninstalled by user in "Device Manager" Window.
// Closing device will clear the instance data.
virtual_display::close_device();
} else if num_displays > 1 {
// Try close device, if display device changed.
if virtual_display::is_device_created() {
virtual_display::close_device();
}
}
let fps = 30;
let wait = 1000 / fps;
let spf = time::Duration::from_secs_f32(1. / (fps as f32));
let (ndisplay, current, display) = get_current_display()?;
let (origin, width, height) = (display.origin(), display.width(), display.height());
log::debug!(
"#displays={}, current={}, origin: {:?}, width={}, height={}",
"#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}",
ndisplay,
current,
&origin,
width,
height
height,
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")?;
@ -260,7 +249,31 @@ fn run(sp: GenericService) -> ResultType<()> {
frame_controller.reset();
match c.frame(wait as _) {
#[cfg(any(target_os = "android", target_os = "ios"))]
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;
match frame {
scrap::Frame::VP9(data) => {
let send_conn_ids = handle_one_frame_encoded(&sp, data, ms)?;
frame_controller.set_send(now, send_conn_ids);
}
scrap::Frame::RAW(data) => {
if (data.len() != 0) {
let send_conn_ids = handle_one_frame(&sp, data, ms, &mut vpx)?;
frame_controller.set_send(now, send_conn_ids);
}
}
_ => {}
};
Ok(())
}
Err(err) => Err(err),
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
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;
@ -270,8 +283,14 @@ fn run(sp: GenericService) -> ResultType<()> {
{
try_gdi = 0;
}
Ok(())
}
Err(ref e) if e.kind() == WouldBlock => {
Err(err) => Err(err),
};
match res {
Err(ref e) if e.kind() == WouldBlock =>
{
#[cfg(windows)]
if try_gdi > 0 && !c.is_gdi() {
if try_gdi > 3 {
@ -298,6 +317,7 @@ fn run(sp: GenericService) -> ResultType<()> {
return Err(err.into());
}
_ => {}
}
// i love 3, 6, 8
@ -310,7 +330,6 @@ fn run(sp: GenericService) -> ResultType<()> {
std::thread::sleep(spf - elapsed);
}
}
Ok(())
}
@ -370,8 +389,33 @@ fn handle_one_frame(
Ok(send_conn_ids)
}
#[inline]
#[cfg(any(target_os = "android", target_os = "ios"))]
pub fn handle_one_frame_encoded(
sp: &GenericService,
frame: &[u8],
ms: i64,
) -> ResultType<HashSet<i32>> {
sp.snapshot(|sps| {
// so that new sub and old sub share the same encoder after switch
if sps.has_subscribes() {
bail!("SWITCH");
}
Ok(())
})?;
let mut send_conn_ids: HashSet<i32> = Default::default();
let vp9_frame = VP9 {
data: frame.to_vec(),
key: true,
pts: ms,
..Default::default()
};
send_conn_ids = sp.send_video_frame(create_msg(vec![vp9_frame]));
Ok(send_conn_ids)
}
fn get_display_num() -> usize {
if let Ok(d) = try_get_displays() {
if let Ok(d) = Display::all() {
d.len()
} else {
0
@ -385,7 +429,7 @@ pub fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> {
}
let mut displays = Vec::new();
let mut primary = 0;
for (i, d) in try_get_displays()?.iter().enumerate() {
for (i, d) in Display::all()?.iter().enumerate() {
if d.is_primary() {
primary = i;
}
@ -416,11 +460,13 @@ pub fn switch_display(i: i32) {
}
pub fn refresh() {
#[cfg(target_os = "android")]
Display::refresh_size();
*SWITCH.lock().unwrap() = true;
}
fn get_primary() -> usize {
if let Ok(all) = try_get_displays() {
if let Ok(all) = Display::all() {
for (i, d) in all.iter().enumerate() {
if d.is_primary() {
return i;
@ -434,42 +480,12 @@ pub fn switch_to_primary() {
switch_display(get_primary() as _);
}
fn try_get_displays() -> ResultType<Vec<Display>> {
let mut displays = Display::all()?;
if displays.len() == 0 {
log::debug!("no displays, create virtual display");
// Try plugin monitor
if !virtual_display::is_device_created() {
if let Err(e) = virtual_display::create_device() {
log::debug!("Create device failed {}", e);
}
}
if virtual_display::is_device_created() {
if let Err(e) = virtual_display::plug_in_monitor() {
log::debug!("Plug in monitor failed {}", e);
} else {
if let Err(e) = virtual_display::update_monitor_modes() {
log::debug!("Update monitor modes failed {}", e);
}
}
}
displays = Display::all()?;
} else if displays.len() > 1 {
// If more than one displays exists, close RustDeskVirtualDisplay
if virtual_display::is_device_created() {
virtual_display::close_device()
}
}
Ok(displays)
}
fn get_current_display() -> ResultType<(usize, usize, Display)> {
let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize;
let mut displays = try_get_displays()?;
let mut displays = Display::all()?;
if displays.len() == 0 {
bail!("No displays");
}
let n = displays.len();
if current >= n {
current = 0;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

77
src/tray.rs Normal file
View File

@ -0,0 +1,77 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use trayicon::{MenuBuilder, TrayIconBuilder};
use winit::{
event::Event,
event_loop::{ControlFlow, EventLoop},
};
#[derive(Clone, Eq, PartialEq, Debug)]
enum Events {
DoubleClickTrayIcon,
StopService,
StartService,
}
pub fn start_tray(options: Arc<Mutex<HashMap<String, String>>>) {
let event_loop = EventLoop::<Events>::with_user_event();
let proxy = event_loop.create_proxy();
let icon = include_bytes!("./tray-icon.ico");
let mut tray_icon = TrayIconBuilder::new()
.sender_winit(proxy)
.icon_from_buffer(icon)
.tooltip("RustDesk")
.on_double_click(Events::DoubleClickTrayIcon)
.build()
.unwrap();
let old_state = Arc::new(Mutex::new(0));
event_loop.run(move |event, _, control_flow| {
if options.lock().unwrap().get("ipc-closed").is_some() {
*control_flow = ControlFlow::Exit;
return;
} else {
*control_flow = ControlFlow::Wait;
}
let stopped = if let Some(v) = options.lock().unwrap().get("stop-service") {
!v.is_empty()
} else {
false
};
let stopped = if stopped { 2 } else { 1 };
let old = *old_state.lock().unwrap();
if stopped != old {
hbb_common::log::info!("State changed");
let mut m = MenuBuilder::new();
if stopped == 2 {
m = m.item(
&crate::client::translate("Start service".to_owned()),
Events::StartService,
);
} else {
m = m.item(
&crate::client::translate("Stop service".to_owned()),
Events::StopService,
);
}
tray_icon.set_menu(&m).ok();
*old_state.lock().unwrap() = stopped;
}
match event {
Event::UserEvent(e) => match e {
Events::DoubleClickTrayIcon => {
crate::run_me(Vec::<&str>::new()).ok();
}
Events::StopService => {
crate::ipc::set_option("stop-service", "Y");
}
Events::StartService => {
crate::ipc::set_option("stop-service", "");
}
},
_ => (),
}
});
}

400
src/ui.rs
View File

@ -3,14 +3,19 @@ mod cm;
mod inline;
#[cfg(target_os = "macos")]
mod macos;
mod remote;
pub mod remote;
use crate::common::SOFTWARE_UPDATE_URL;
use crate::ipc;
use hbb_common::{
allow_err,
config::{self, Config, LocalConfig, PeerConfig, APP_NAME, ICON},
log, sleep,
tokio::{self, time},
config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT},
futures::future::join_all,
log,
protobuf::Message as _,
rendezvous_proto::*,
sleep,
tcp::FramedStream,
tokio::{self, sync::mpsc, time},
};
use sciter::Value;
use std::{
@ -19,20 +24,23 @@ use std::{
process::Child,
sync::{Arc, Mutex},
};
use virtual_display;
type Message = RendezvousMessage;
pub type Childs = Arc<Mutex<(bool, HashMap<(String, String), Child>)>>;
type Status = (i32, bool, i64, String);
lazy_static::lazy_static! {
// stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/
static ref STUPID_VALUES: Mutex<Vec<Arc<Vec<Value>>>> = Default::default();
}
#[derive(Default)]
struct UI(
Childs,
Arc<Mutex<(i32, bool)>>,
Arc<Mutex<Status>>,
Arc<Mutex<HashMap<String, String>>>,
Arc<Mutex<String>>,
mpsc::UnboundedSender<ipc::Data>,
);
struct UIHostHandler;
@ -53,20 +61,14 @@ pub fn start(args: &mut [String]) {
allow_err!(sciter::set_options(sciter::RuntimeOptions::GfxLayer(
sciter::GFX_LAYER::WARP
)));
#[cfg(all(windows, not(feature = "inline")))]
unsafe {
winapi::um::shellscalingapi::SetProcessDpiAwareness(2);
}
#[cfg(windows)]
if args.len() > 0 && args[0] == "--tray" {
let mut res;
// while switching from prelogin to user screen, start_tray may fails,
// so we try more times
loop {
res = start_tray();
if res.is_ok() {
log::info!("tray started with username {}", crate::username());
break;
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
allow_err!(res);
let options = check_connect_status(false).1;
crate::tray::start_tray(options);
return;
}
use sciter::SCRIPT_RUNTIME_FEATURES::*;
@ -114,6 +116,11 @@ pub fn start(args: &mut [String]) {
|| args[0] == "--rdp")
&& args.len() > 1
{
#[cfg(windows)]
{
let hw = frame.get_host().get_hwnd();
crate::platform::windows::enable_lowlevel_keyboard(hw as _);
}
let mut iter = args.iter();
let cmd = iter.next().unwrap().clone();
let id = iter.next().unwrap().clone();
@ -151,54 +158,10 @@ pub fn start(args: &mut [String]) {
frame.run_app();
}
#[cfg(windows)]
fn start_tray() -> hbb_common::ResultType<()> {
let mut app = systray::Application::new()?;
let icon = include_bytes!("./tray-icon.ico");
app.set_icon_from_buffer(icon, 32, 32).unwrap();
app.add_menu_item("Open Window", |_| {
crate::run_me(Vec::<&str>::new()).ok();
Ok::<_, systray::Error>(())
})?;
let options = check_connect_status(false).1;
let idx_stopped = Arc::new(Mutex::new((0, 0)));
app.set_timer(std::time::Duration::from_millis(1000), move |app| {
let stopped = if let Some(v) = options.lock().unwrap().get("stop-service") {
!v.is_empty()
} else {
false
};
let stopped = if stopped { 2 } else { 1 };
let mut old = *idx_stopped.lock().unwrap();
if stopped != old.1 {
if old.0 > 0 {
app.remove_menu_item(old.0)
}
if stopped == 1 {
old.0 = app.add_menu_item("Stop Service", |_| {
ipc::set_option("stop-service", "Y");
Ok::<_, systray::Error>(())
})?;
} else {
old.0 = app.add_menu_item("Start Service", |_| {
ipc::set_option("stop-service", "");
Ok::<_, systray::Error>(())
})?;
}
old.1 = stopped;
*idx_stopped.lock().unwrap() = old;
}
Ok::<_, systray::Error>(())
})?;
allow_err!(app.wait_for_message());
Ok(())
}
impl UI {
fn new(childs: Childs) -> Self {
let res = check_connect_status(true);
Self(childs, res.0, res.1)
Self(childs, res.0, res.1, Default::default(), res.2)
}
fn recent_sessions_updated(&mut self) -> bool {
@ -211,7 +174,7 @@ impl UI {
}
}
fn get_id(&mut self) -> String {
fn get_id(&self) -> String {
ipc::get_id()
}
@ -239,10 +202,10 @@ impl UI {
allow_err!(crate::run_me(vec!["--install"]));
}
fn install_me(&mut self, _options: String) {
fn install_me(&mut self, _options: String, _path: String) {
#[cfg(windows)]
std::thread::spawn(move || {
allow_err!(crate::platform::windows::install_me(&_options));
allow_err!(crate::platform::windows::install_me(&_options, _path));
std::process::exit(0);
});
}
@ -273,8 +236,45 @@ impl UI {
}
}
fn run_without_install(&self) {
crate::run_me(vec!["--noinstall"]).ok();
std::process::exit(0);
}
fn show_run_without_install(&self) -> bool {
let mut it = std::env::args();
if let Some(tmp) = it.next() {
if crate::is_setup(&tmp) {
return it.next() == None;
}
}
false
}
fn has_rendezvous_service(&self) -> bool {
#[cfg(all(windows, feature = "hbbs"))]
return crate::platform::is_win_server()
&& crate::platform::windows::get_license().is_some();
return false;
}
fn get_license(&self) -> String {
#[cfg(windows)]
if let Some(lic) = crate::platform::windows::get_license() {
return format!(
"<br /> Key: {} <br /> Host: {} Api: {}",
lic.key, lic.host, lic.api
);
}
Default::default()
}
fn get_option(&self, key: String) -> String {
if let Some(v) = self.2.lock().unwrap().get(&key) {
self.get_option_(&key)
}
fn get_option_(&self, key: &str) -> String {
if let Some(v) = self.2.lock().unwrap().get(key) {
v.to_owned()
} else {
"".to_owned()
@ -314,6 +314,10 @@ impl UI {
c.store(&id);
}
fn using_public_server(&self) -> bool {
crate::get_custom_rendezvous_server(self.get_option_("custom-rendezvous-server")).is_empty()
}
fn get_options(&self) -> Value {
let mut m = Value::map();
for (k, v) in self.2.lock().unwrap().iter() {
@ -380,22 +384,6 @@ impl UI {
ipc::set_options(options.clone()).ok();
}
// TODO: ui prompt
fn install_virtual_display(&self) {
let mut reboot_required = false;
match virtual_display::install_update_driver(&mut reboot_required) {
Ok(_) => {
log::info!(
"Virtual Display: install virtual display done, reboot is {} required",
if reboot_required { "" } else { "not" }
);
}
Err(e) => {
log::error!("Virtual Display: install virtual display failed {}", e);
}
}
}
fn install_path(&mut self) -> String {
#[cfg(windows)]
return crate::platform::windows::get_install_info().1;
@ -426,10 +414,29 @@ impl UI {
.ok();
}
fn is_installed(&mut self) -> bool {
fn is_installed(&self) -> bool {
crate::platform::is_installed()
}
fn is_rdp_service_open(&self) -> bool {
#[cfg(windows)]
return self.is_installed() && crate::platform::windows::is_rdp_service_open();
#[cfg(not(windows))]
return false;
}
fn is_share_rdp(&self) -> bool {
#[cfg(windows)]
return crate::platform::windows::is_share_rdp();
#[cfg(not(windows))]
return false;
}
fn set_share_rdp(&self, _enable: bool) {
#[cfg(windows)]
crate::platform::windows::set_share_rdp(_enable);
}
fn is_installed_lower_version(&self) -> bool {
#[cfg(not(windows))]
return false;
@ -457,11 +464,20 @@ impl UI {
v
}
fn get_mouse_time(&self) -> f64 {
self.1.lock().unwrap().2 as _
}
fn check_mouse_time(&self) {
allow_err!(self.4.send(ipc::Data::MouseMoveTime(0)));
}
fn get_connect_status(&mut self) -> Value {
let mut v = Value::array(0);
let x = *self.1.lock().unwrap();
let x = self.1.lock().unwrap().clone();
v.push(x.0);
v.push(x.1);
v.push(x.3);
v
}
@ -508,7 +524,7 @@ impl UI {
}
fn get_icon(&mut self) -> String {
ICON.to_owned()
crate::get_icon()
}
fn remove_peer(&mut self, id: String) {
@ -572,7 +588,12 @@ impl UI {
return "".to_owned();
}
if dtype != "x11" {
return format!("Unsupported display server type {}, x11 expected!", dtype);
return format!(
"{} {}, {}",
self.t("Unsupported display server ".to_owned()),
dtype,
self.t("x11 expected".to_owned()),
);
}
}
return "".to_owned();
@ -587,7 +608,7 @@ impl UI {
fn fix_login_wayland(&mut self) {
#[cfg(target_os = "linux")]
return crate::platform::linux::fix_login_wayland();
crate::platform::linux::fix_login_wayland();
}
fn current_is_wayland(&mut self) -> bool {
@ -617,7 +638,7 @@ impl UI {
}
fn get_app_name(&self) -> String {
APP_NAME.to_owned()
crate::get_app_name()
}
fn get_software_ext(&self) -> String {
@ -638,7 +659,7 @@ impl UI {
.split("/")
.last()
.map(|x| x.to_owned())
.unwrap_or(APP_NAME.to_owned());
.unwrap_or(crate::get_app_name());
p.push(name);
format!("{}.{}", p.to_string_lossy(), self.get_software_ext())
}
@ -658,6 +679,10 @@ impl UI {
config::LanPeers::load().peers
}
fn get_uuid(&self) -> String {
base64::encode(crate::get_uuid())
}
fn open_url(&self, url: String) {
#[cfg(windows)]
let p = "explorer";
@ -672,6 +697,34 @@ impl UI {
allow_err!(std::process::Command::new(p).arg(url).spawn());
}
fn change_id(&self, id: String) {
let status = self.3.clone();
*status.lock().unwrap() = " ".to_owned();
let old_id = self.get_id();
std::thread::spawn(move || {
*status.lock().unwrap() = change_id(id, old_id).to_owned();
});
}
fn post_request(&self, url: String, body: String, header: String) {
let status = self.3.clone();
*status.lock().unwrap() = " ".to_owned();
std::thread::spawn(move || {
*status.lock().unwrap() = match crate::post_request_sync(url, body, &header) {
Err(err) => err.to_string(),
Ok(text) => text,
};
});
}
fn is_ok_change_id(&self) -> bool {
machine_uid::get().is_ok()
}
fn get_async_job_status(&self) -> String {
self.3.clone().lock().unwrap().clone()
}
fn t(&self, name: String) -> String {
crate::client::translate(name)
}
@ -679,12 +732,21 @@ impl UI {
fn is_xfce(&self) -> bool {
crate::platform::is_xfce()
}
fn get_api_server(&self) -> String {
crate::get_api_server(
self.get_option_("api-server"),
self.get_option_("custom-rendezvous-server"),
)
}
}
impl sciter::EventHandler for UI {
sciter::dispatch_script_call! {
fn t(String);
fn get_api_server();
fn is_xfce();
fn using_public_server();
fn get_id();
fn get_password();
fn update_password(String);
@ -695,16 +757,21 @@ impl sciter::EventHandler for UI {
fn new_remote(String, bool);
fn remove_peer(String);
fn get_connect_status();
fn get_mouse_time();
fn check_mouse_time();
fn get_recent_sessions();
fn get_peer(String);
fn get_fav();
fn store_fav(Value);
fn recent_sessions_updated();
fn get_icon();
fn install_me(String);
fn install_me(String, String);
fn is_installed();
fn set_socks(String, String, String);
fn get_socks();
fn is_rdp_service_open();
fn is_share_rdp();
fn set_share_rdp(bool);
fn is_installed_lower_version();
fn install_path();
fn goto_install();
@ -724,22 +791,30 @@ impl sciter::EventHandler for UI {
fn peer_has_password(String);
fn forget_password(String);
fn set_peer_option(String, String, String);
fn has_rendezvous_service();
fn get_license();
fn test_if_valid_server(String);
fn get_sound_inputs();
fn set_options(Value);
fn set_option(String, String);
fn install_virtual_display();
fn get_software_update_url();
fn get_new_version();
fn get_version();
fn update_me(String);
fn show_run_without_install();
fn run_without_install();
fn get_app_name();
fn get_software_store_path();
fn get_software_ext();
fn open_url(String);
fn change_id(String);
fn get_async_job_status();
fn post_request(String, String, String);
fn is_ok_change_id();
fn create_shortcut(String);
fn discover();
fn get_lan_peers();
fn get_uuid();
}
}
@ -771,15 +846,19 @@ pub fn check_zombie(childs: Childs) {
}
}
// notice: avoiding create ipc connection repeatedly,
// notice: avoiding create ipc connecton repeatly,
// because windows named pipe has serious memory leak issue.
#[tokio::main(flavor = "current_thread")]
async fn check_connect_status_(
reconnect: bool,
status: Arc<Mutex<(i32, bool)>>,
status: Arc<Mutex<Status>>,
options: Arc<Mutex<HashMap<String, String>>>,
rx: mpsc::UnboundedReceiver<ipc::Data>,
) {
let mut key_confirmed = false;
let mut rx = rx;
let mut mouse_time = 0;
let mut id = "".to_owned();
loop {
if let Ok(mut c) = ipc::connect(1000, "").await {
let mut timer = time::interval(time::Duration::from_secs(1));
@ -791,30 +870,47 @@ async fn check_connect_status_(
log::error!("ipc connection closed: {}", err);
break;
}
Ok(Some(ipc::Data::MouseMoveTime(v))) => {
mouse_time = v;
status.lock().unwrap().2 = v;
}
Ok(Some(ipc::Data::Options(Some(v)))) => {
*options.lock().unwrap() = v
}
Ok(Some(ipc::Data::Config((name, Some(value))))) => {
if name == "id" {
id = value;
}
}
Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => {
if x > 0 {
x = 1
}
key_confirmed = c;
*status.lock().unwrap() = (x as _, key_confirmed);
*status.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone());
}
_ => {}
}
}
Some(data) = rx.recv() => {
allow_err!(c.send(&data).await);
}
_ = timer.tick() => {
c.send(&ipc::Data::OnlineStatus(None)).await.ok();
c.send(&ipc::Data::Options(None)).await.ok();
c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok();
}
}
}
}
if !reconnect {
std::process::exit(0);
options
.lock()
.unwrap()
.insert("ipc-closed".to_owned(), "Y".to_owned());
break;
}
*status.lock().unwrap() = (-1, key_confirmed);
*status.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone());
sleep(1.).await;
}
}
@ -847,13 +943,113 @@ fn get_sound_inputs() -> Vec<String> {
fn check_connect_status(
reconnect: bool,
) -> (Arc<Mutex<(i32, bool)>>, Arc<Mutex<HashMap<String, String>>>) {
let status = Arc::new(Mutex::new((0, false)));
) -> (
Arc<Mutex<Status>>,
Arc<Mutex<HashMap<String, String>>>,
mpsc::UnboundedSender<ipc::Data>,
) {
let status = Arc::new(Mutex::new((0, false, 0, "".to_owned())));
let options = Arc::new(Mutex::new(Config::get_options()));
let cloned = status.clone();
let cloned_options = options.clone();
std::thread::spawn(move || check_connect_status_(reconnect, cloned, cloned_options));
(status, options)
let (tx, rx) = mpsc::unbounded_channel::<ipc::Data>();
std::thread::spawn(move || check_connect_status_(reconnect, cloned, cloned_options, rx));
(status, options, tx)
}
const INVALID_FORMAT: &'static str = "Invalid format";
const UNKNOWN_ERROR: &'static str = "Unknown error";
#[tokio::main(flavor = "current_thread")]
async fn change_id(id: String, old_id: String) -> &'static str {
if !hbb_common::is_valid_custom_id(&id) {
return INVALID_FORMAT;
}
let uuid = machine_uid::get().unwrap_or("".to_owned());
if uuid.is_empty() {
return UNKNOWN_ERROR;
}
let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await;
let mut futs = Vec::new();
let err: Arc<Mutex<&str>> = Default::default();
for rendezvous_server in rendezvous_servers {
let err = err.clone();
let id = id.to_owned();
let uuid = uuid.clone();
let old_id = old_id.clone();
futs.push(tokio::spawn(async move {
let tmp = check_id(rendezvous_server, old_id, id, uuid).await;
if !tmp.is_empty() {
*err.lock().unwrap() = tmp;
}
}));
}
join_all(futs).await;
let err = *err.lock().unwrap();
if err.is_empty() {
crate::ipc::set_config_async("id", id.to_owned()).await.ok();
}
err
}
async fn check_id(
rendezvous_server: String,
old_id: String,
id: String,
uuid: String,
) -> &'static str {
let any_addr = Config::get_any_listen_addr();
if let Ok(mut socket) = FramedStream::new(
crate::check_port(rendezvous_server, RENDEZVOUS_PORT),
any_addr,
RENDEZVOUS_TIMEOUT,
)
.await
{
let mut msg_out = Message::new();
msg_out.set_register_pk(RegisterPk {
old_id,
id,
uuid: uuid.into(),
..Default::default()
});
let mut ok = false;
if socket.send(&msg_out).await.is_ok() {
if let Some(Ok(bytes)) = socket.next_timeout(3_000).await {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
match msg_in.union {
Some(rendezvous_message::Union::register_pk_response(rpr)) => {
match rpr.result.enum_value_or_default() {
register_pk_response::Result::OK => {
ok = true;
}
register_pk_response::Result::ID_EXISTS => {
return "Not available";
}
register_pk_response::Result::TOO_FREQUENT => {
return "Too frequent";
}
register_pk_response::Result::NOT_SUPPORT => {
return "This function is turned off by the server";
}
register_pk_response::Result::INVALID_ID_FORMAT => {
return INVALID_FORMAT;
}
_ => {}
}
}
_ => {}
}
}
}
}
if !ok {
return UNKNOWN_ERROR;
}
} else {
return "Failed to connect to rendezvous server";
}
""
}
// sacrifice some memory

View File

@ -1,3 +1,210 @@
var selectTags = [];
var ab = { tags: [], peers: [] };
var abLoading;
var abError;
var current_menu_peer_id = '';
var current_menu_tag = '';
class AddressBook: Reactor.Component
{
this var style;
this var selectedTags = function() {
var tags = handler.get_local_option("selected-tags");
if (tags) return tags.split(",");
return [];
}();
function render() {
if (!handler.get_local_option("access_token")) {
return <div style="margin: *"><div #login-link .link style="margin: *; width: 100px; text-align: center; font-size: 1.2em;">{translate("Login")}</div></div>;
}
if (abLoading) {
return <div style="margin: *"><progress style="color: #0071ff" /></div>;
} else if (abError) {
return <div style="margin: *; text-align: center;">{abError}
<div #retry .link style="margin-left: 1em;">{translate("Retry")}</div>
</div>;
}
var peers = this.getPeers();
var me = this;
return <div .app #ab style="size:*">
<popup>
<menu.context #ab-context>
<li #add-id>{translate('Add ID')}</li>
<li #add-tag>{translate('Add Tag')}</li>
<li #unselect-tags>{translate('Unselect all tags')}</li>
</menu>
<menu.context #tag-context>
<li #remove-tag>{translate('Remove')}</li>
</menu>
</popup>
<div .left-pane>
<div style="padding: 0; padding-bottom: 1em" #tags-label>{translate('Tags')}{svg_menu}</div>
<div #tags>
{ab.tags.map(function(t) {
return <span class={me.selectedTags.indexOf(t) >= 0 ? "active" : "inactive"}>{t}</span>;
})}
</div>
</div>
<div .right-pane>
<div .right-content style="padding-top:0; padding-right: 0;">
<SessionList sessions={peers} type="ab" />
</div>
</div>
</div>;
}
event mouseup $(#tags span) (evt, me) {
if(evt.propButton) {
current_menu_tag = me.text;
me.popup($(#tag-context));
return true;
}
}
event click $(#retry) (_, __) {
refreshCurrentUser();
}
event click $(#login-link) (_, __) {
login();
}
event click $(#tags-label svg#menu) (_, me) {
me.popup($(#ab-context));
}
event click $(#add-id) (_, __) {
var me = this;
msgbox("custom-add-id", translate("Add ID"), <div .form>
<div>{translate("whitelist_sep")}</div>
<textarea .outline-focus spellcheck="false" name="text" style="overflow: scroll-indicator; width:*; height: 160px; font-size: 1.2em; padding: 0.5em;"></textarea>
</div>, function(res=null) {
if (!res) return;
var value = (res.text || "").trim();
var values = value.split(/[\s,;\n]+/g);
if (values.length == 0) return;
for (var v in values) {
var found;
for (var i = 0; i < ab.peers.length; ++i) {
if (ab.peers[i].id == v) {
found = true;
break;
}
}
if (found) continue;
ab.peers.push({ id: v });
}
updateAb();
me.update();
}, 300);
}
event click $(#add-tag) (_, __) {
var me = this;
msgbox("custom-add-tag", translate("Add Tag"), <div .form>
<div>{translate("whitelist_sep")}</div>
<textarea .outline-focus spellcheck="false" name="text" style="overflow: scroll-indicator; width:*; height: 160px; font-size: 1.2em; padding: 0.5em;"></textarea>
</div>, function(res=null) {
if (!res) return;
var value = (res.text || "").trim();
var values = value.split(/[\s,;\n]+/g);
if (values.length == 0) return;
for (var v in values) {
if (ab.tags.indexOf(v) < 0) {
ab.tags.push(v);
}
}
updateAb();
me.update();
}, 300);
}
event click $(#remove-tag) (_, me) {
var tag = current_menu_tag;
var i = ab.tags.indexOf(tag);
ab.tags.splice(i, 1);
for (var p in ab.peers) {
if (p.tags) {
i = p.tags.indexOf(tag);
if (i >= 0) p.tags.splice(i, 1);
}
}
updateAb();
this.update();
}
event click $(#unselect-tags) (_, me) {
this.selectedTags = [];
handler.set_local_option("selected-tags", "");
this.update();
}
event click $(#tags span) (_, me) {
me.attributes.toggleClass('active');
if (me.attributes.hasClass('active')) {
this.selectedTags.push(me.text);
} else {
this.selectedTags.splice(this.selectedTags.indexOf(me.text), 1);
}
handler.set_local_option("selected-tags", this.selectedTags.join(','));
this.update();
}
function getPeers() {
var tags = [];
for (var t in this.selectedTags) {
if (ab.tags.indexOf(t) >= 0) tags.push(t);
}
if (tags.length != this.selectedTags.length) {
this.selectedTags = tags;
handler.set_local_option("selected-tags", tags.join(","));
stdout.println("updated selected tags");
}
if (tags.length == 0) return ab.peers;
var peers = [];
if (tags.length > 0) {
for (var p in ab.peers) {
for (var t in (p.tags || [])) {
if (tags.indexOf(t) >= 0) {
peers.push(p);
break;
}
}
}
} else {
peers = ab.peers;
}
return peers;
}
}
class SelectTags: Reactor.Component {
function this(params) {
selectTags = this;
this.tags = params.tags;
}
function render() {
var me = this;
return <div #tags>
{ab.tags.map(function(t) {
return <span class={me.tags.indexOf(t) >= 0 ? "active" : "inactive"}>{t}</span>;
})}
</div>;
}
event click $(#tags span) (_, me) {
me.attributes.toggleClass('active');
var i = this.tags.indexOf(me.text);
if (i < 0) {
this.tags.push(me.text);
} else {
this.tags.splice(i, 1);
}
}
}
var svg_tile = <svg #session-tile viewBox="0 0 158.6 158.6"><path style="stroke-width:.309756" d="M5.4 157.7c-1-.3-2-1-3.2-2.1-2.8-2.8-2.6-1-2.5-32 0-26.7 0-27 .7-28.3a9.3 9.3 0 0 1 4-4.2c1.2-.6 2.3-.6 29-.7 27.5 0 27.6 0 29.1.6.8.4 2 1.2 2.7 2 2.4 2.5 2.3.7 2.2 31.6-.1 26.5-.1 27.6-.7 28.8a9.3 9.3 0 0 1-4.2 4c-1.4.6-1.6.6-28.5.7a235 235 0 0 1-28.6-.4zm91 0a8.5 8.5 0 0 1-5.7-5.4c-.2-.7-.3-8.3-.3-28.3V96.7l.7-1.6a8.9 8.9 0 0 1 4.6-4.3c1.2-.4 3.8-.5 28.9-.4 26.6.1 27.6.1 28.8.7 1.6.8 3.2 2.5 4 4.2.7 1.4.7 1.6.7 28.3.1 31 .3 29.2-2.5 32-2.8 2.7-1 2.6-31.4 2.6-21.4 0-26.8-.1-27.9-.5zM5.3 67a8.7 8.7 0 0 1-4-3C-.5 61.6-.5 62.3-.5 33.6-.4 3.2-.5 5 2.2 2.2 5-.6 3.2-.4 34.2-.3c26.7 0 27 0 28.3.7 1.7.8 3.4 2.4 4.2 4 .6 1.2.6 2.2.7 28.8 0 25.1 0 27.7-.4 29a9 9 0 0 1-4.3 4.5l-1.6.7H33.7c-20.2 0-27.7-.1-28.4-.4Zm89.8-.3a9 9 0 0 1-4.3-4.6c-.5-1.2-.5-3.8-.5-28.9.1-26.6.2-27.6.7-28.8a9.3 9.3 0 0 1 4.2-4c1.4-.7 1.6-.7 28.3-.7 31-.1 29.2-.3 32 2.5 2.8 2.8 2.6 1 2.5 32 0 26.7 0 26.9-.7 28.3a9.3 9.3 0 0 1-4 4.2c-1.2.5-2.2.6-29 .6l-27.7.1z" transform="translate(.4 .4)"/></svg>;
var svg_list = <svg #session-list viewBox="0 0 246.8 185.8"><path style="stroke-width:.482473" d="M-69.2 102.7A16.5 16.5 0 0 1-67 70.4c7.3-1 15 4 17.3 11 1 3 1 8 0 10.8a16.7 16.7 0 0 1-19.5 10.5zm53-3.4a12.3 12.3 0 0 1-7-16.8c1.3-3 3.1-4.7 6-6 2.2-1 2.8-1 87.2-1 92.4 0 87-.2 90.6 2.6.9.7 2.2 2.4 3 3.7 1.2 2.2 1.4 3.1 1.4 6 0 4.8-2.3 8.6-6.8 11l-1.9 1-85.2.1c-71.9 0-85.5 0-87.3-.6zm-53.5-73c-4.7-1.5-8.6-5-10.6-9.1-1.8-4-1.8-9.8 0-13.7 1.6-3.3 4.4-6.2 7.8-8 2.2-1.2 3-1.3 7.1-1.3 4 0 5 .1 7.3 1.3a16.6 16.6 0 0 1 0 29.6c-2 1-3.4 1.4-6.5 1.5-2.2 0-4.5 0-5.1-.3zm52.3-4.8c-2.4-1.1-5.3-4-6.2-6.5-1-2.4-1-7.3.1-9.7.5-1.1 1.8-2.8 2.8-3.8 3.7-3.5-4-3.2 91-3.2h85.5l2.5 1.1a12 12 0 0 1 0 21.8l-2.5 1.2H70.2c-82.5 0-85.7 0-87.6-1zm-52.1-71.6a18 18 0 0 1-10-7.7 17 17 0 0 1-.7-15c2.3-5 5.8-7.9 11.4-9.3 9-2.3 18.3 4 19.8 13.4a16.4 16.4 0 0 1-15.2 19c-2.1.1-4.1 0-5.3-.4zm52.1-5.9c-1.3-.6-3-1.7-3.7-2.5-4.7-5-4.2-13.7 1-18 3.7-3.1-1.8-3 91.5-2.8l84.9.1 2 1a12 12 0 0 1 6.7 11c0 3-.2 3.9-1.4 6-.8 1.4-2.1 3-3 3.8-3.7 2.7 1.8 2.6-90.6 2.6h-85l-2.4-1.2z" transform="translate(81.7 82.6)"/></svg>;
var search_icon = <svg viewBox="0 0 655.278 655.024"><g transform="translate(-24.497 -195.01)"><path d="m649.96 847.92c-2.9592-1.3629-27.183-24.243-63.36-59.846-32.213-31.702-70.814-69.663-85.78-84.357l-27.21-26.717-4.7897 3.5287c-66.337 48.872-145.32 66.878-224.31 51.138-72.966-14.539-136.58-58.184-178.47-122.44-15.945-24.462-30.723-61.471-36.413-91.191-8.9404-46.696-6.2422-90.39 8.3388-135.04 13.39-41.003 34.756-75.42 66.479-107.09 74.506-74.377 183.71-99.89 284.22-66.397 62.352 20.777 117.67 65.579 150.79 122.12 38.716 66.101 46.59 147.55 21.43 221.66-9.9038 29.171-29.788 63.725-49.916 86.743l-7.0583 8.0717 3.0992 2.919c1.7046 1.6054 40.675 39.928 86.602 85.161 89.007 87.664 87.558 86.034 85.619 96.293-1.2888 6.8209-5.2313 12.041-11.321 14.989-6.7901 3.287-11.55 3.4093-17.952 0.46117zm-316.64-154.63c32.373-5.0481 61.075-15.115 86.553-30.358 47.942-28.683 83.505-72.09 100.89-123.14 35.043-102.91-6.4362-214.07-100.89-270.37-52.514-31.302-117.76-40.564-178.06-25.277-81.183 20.579-145.19 82.918-166.86 162.52-5.5757 20.478-7.445 35.423-7.445 59.52s1.8693 39.042 7.445 59.52c21.409 78.63 85.366 141.52 164.81 162.05 29.22 7.5511 66.493 9.756 93.564 5.5347z" stroke-width="1.28"/></g></svg>;
@ -14,7 +221,6 @@ function getSessionsStyle(type) {
}
var searchPatterns = {};
var current_menu_peer_id = '';
class SearchBar: Reactor.Component {
this var type = "";
@ -67,7 +273,11 @@ class SessionStyle: Reactor.Component {
var option = getSessionsStyleOption(this.type);
var sessionsStyle = getSessionsStyle(this.type);
handler.set_local_option(option, sessionsStyle == "tile" ? "list" : "tile");
app.multipleSessions.update();
if (is_linux) {
app.multipleSessions.stupidUpdate();
} else {
app.multipleSessions.update();
}
}
}
@ -106,6 +316,7 @@ class SessionList: Reactor.Component {
<li #connect>{translate('Connect')}</li>
<li #transfer>{translate('Transfer File')}</li>
<li #tunnel>{translate('TCP Tunneling')}</li>
{false && !handler.using_public_server() && <li #force-always-relay><span>{svg_checkmark}</span>{translate('Always connect via relay')}</li>}
<li #rdp>RDP<EditRdpPort /></li>
<div .separator />
<li #rename>{translate('Rename')}</li>
@ -114,6 +325,7 @@ class SessionList: Reactor.Component {
<li #forget-password>{translate('Unremember Password')}</li>
{(!this.type || this.type == "fav") && <li #add-fav>{translate('Add to Favorites')}</li>}
{(!this.type || this.type == "fav") && <li #remove-fav>{translate('Remove from Favorites')}</li>}
{this.type == "ab" && <li #edit-tag>{translate('Edit Tag')}</li>}
</menu>
</popup>
{sessions}
@ -179,6 +391,12 @@ class SessionList: Reactor.Component {
// https://sciter.com/forums/topic/replacecustomize-context-menu/
var menu = this.$(menu#remote-context);
current_menu_peer_id = id;
var el = this.$(li#force-always-relay);
if (el) {
var force = handler.get_peer_option(id, "force-always-relay");
el.attributes.toggleClass("selected", force == "Y");
el.attributes.toggleClass("line-through", force != "Y");
}
var conn = this.$(menu #connect);
if (conn) {
var alias = me.parent.parent.$(#alias);
@ -202,7 +420,16 @@ class SessionList: Reactor.Component {
} else if (action == "transfer") {
createNewConnect(id, "file-transfer");
} else if (action == "remove") {
if (!this.type) {
if (this.type == "ab") {
for (var i = 0; i < ab.peers.length; ++i) {
if (ab.peers[i].id == id) {
ab.peers.splice(i, 1);
app.update();
updateAb();
break;
}
}
} else {
handler.remove_peer(id);
app.update();
}
@ -211,6 +438,10 @@ class SessionList: Reactor.Component {
} else if (action == "shortcut") {
handler.create_shortcut(id);
} else if (action == "rdp") {
if (is_edit_rdp_port) {
is_edit_rdp_port = false;
return;
}
createNewConnect(id, "rdp");
} else if (action == "add-fav") {
var favs = handler.get_fav();
@ -230,6 +461,15 @@ class SessionList: Reactor.Component {
createNewConnect(id, "port-forward");
} else if (action == "rename") {
var old_name = handler.get_peer_option(id, "alias");
var abPeer;
if (this.type == "ab") {
for (var v in ab.peers) {
if (v.id == id) {
abPeer = v;
old_name = v.alias || "";
}
}
}
msgbox("custom-rename", "Rename", "<div .form> \
<div><input name='name' .outline-focus style='width: *; height: 23px', value='" + old_name + "' /></div> \
</div> \
@ -237,10 +477,30 @@ class SessionList: Reactor.Component {
if (!res) return;
var name = (res.name || "").trim();
if (name != old_name) {
if (abPeer) {
abPeer.alias = name;
updateAb();
}
handler.set_peer_option(id, "alias", name);
}
app.update();
});
} else if (action == "force-always-relay") {
var force = handler.get_peer_option(id, "force-always-relay");
handler.set_peer_option(id, "force-always-relay", force == "Y" ? "" : "Y");
} else if (action == "edit-tag") {
var peer;
for (var v in ab.peers) {
if (v.id == id) {
peer = v;
}
}
if (!peer) return;
msgbox("custom-edit-tag", "Edit Tag", <SelectTags tags={peer.tags || []} />, function(res=null) {
if (!res) return;
peer.tags = selectTags.tags;
updateAb();
}, 260, 500, 0, "size: *; margin: 2em 0;");
}
}
}
@ -267,6 +527,7 @@ class MultipleSessions: Reactor.Component {
<span class={!type ? 'active' : 'inactive'}>{translate('Recent Sessions')}</span>
<span #fav class={type == "fav" ? 'active' : 'inactive'}>{translate('Favorites')}</span>
{handler.is_installed() && <span #lan class={type == "lan" ? 'active' : 'inactive'}>{translate('Discovered')}</span>}
<span #ab class={type == "ab" ? 'active' : 'inactive'}>{translate('Address Book')}</span>
</div>
{!this.hidden && <SearchBar type={type} />}
{!this.hidden && <SessionStyle type={type} />}
@ -274,6 +535,7 @@ class MultipleSessions: Reactor.Component {
{!this.hidden &&
((type == "fav" && <Favorites />) ||
(type == "lan" && handler.is_installed() && <LanPeers />) ||
(type == "ab" && <AddressBook />) ||
<SessionList sessions={handler.get_recent_sessions()} />)}
</div>;
}
@ -349,3 +611,146 @@ class LanPeers: Reactor.Component {
}
view.on("size", function() { if (app && app.multipleSessions) app.multipleSessions.onSize(); });
/*
{
peers: [{id: "abcd", username: "", hostname: "", platform: "", alias: "", tags: ["", "", ...]}, ...],
tags: [],
}
*/
function handleAbError(err) {
abLoading = false;
err = translate(err);
stderr.println(err);
abError = err;
app.update();
}
function getAb() {
abLoading = true;
abError = "";
app.update();
httpRequest(handler.get_api_server() + "/api/ab/get", #post, {}, function(data) {
if (data) {
if (data.error) {
handleAbError(data.error);
return;
}
var tm = data.updated_at;
ab = JSON.parse(data.data);
if (!ab.tags) ab.tags = [];
if (!ab.peers) ab.peers = [];
}
abLoading = false;
app.update();
}, function(err, status) {
handleAbError(err);
}, getHttpHeaders());
}
function updateAb() {
httpRequest(handler.get_api_server() + "/api/ab", #post, { data: JSON.stringify(ab) }, function(data) {
}, function(err, status) {
}, getHttpHeaders());
}
function resetAb() {
ab = { tags: [], peers: [] };
app.update();
}
function updateAbPeer() {
if (ab.peers.length == 0) return;
// to-do: inefficient
var sessions = handler.get_recent_sessions();
if (sessions.length == 0) return;
var s = sessions[0];
var id = s[0] || "";
var p;
for (var tmp in ab.peers) {
if (tmp.id == id) p = tmp;
}
if (!p) return;
var username = s[1] || "";
var hostname = s[2] || "";
var platform = s[3] || "";
var alias = s[4] || "";
var updated;
if (username != (p.username || "")) {
p.username = username;
updated = true;
}
if (hostname != (p.hostname || "")) {
p.hostname = hostname;
updated = true;
}
if (platform != (p.platform || "")) {
p.platform = platform;
updated = true;
}
if (alias != (p.alias || "")) {
if (alias) {
p.alias = alias;
} else if (p.alias) {
handler.set_peer_option(id, "alias", p.alias);
}
updated = true;
}
if (updated) {
updateAb();
stdout.println("Ab peer updated");
}
}
var is_edit_rdp_port;
class EditRdpPort: Reactor.Component {
function render() {
return <span style="margin-left: 12px; padding: 0 6px; display: inline-block;" .link>{svg_edit}</span>;
}
function onMouse(evt) {
if (evt.type == Event.MOUSE_DOWN) {
is_edit_rdp_port = true;
editRdpPort();
}
}
}
function editRdpPort() {
var id = current_menu_peer_id;
var p0 = handler.get_peer_option(id, "rdp_port");
var port = p0 ? <input|text name='port' value={p0} /> :
<input|text name='port' novalue={3389} />;
var name0 = handler.get_peer_option(id, "rdp_username");
var pass0 = handler.get_peer_option(id, "rdp_password");
msgbox("custom-rdp-port", 'RDP ' + translate('Settings'), <div .form .set-password>
<div><span>{translate('Port')}:</span>{port}</div>
<div><span>{translate('Username')}:</span><input|text name="username" value={name0} /></div>
<div><span>{translate('Password')}:</span><PasswordComponent value={pass0} /></div>
</div>, function(res=null) {
if (!res) return;
var p = (res.port || '').trim();
if (p != p0) {
if (!p) p = '0';
p = p.toNumber();
if (p < 0 || p != p.toInteger()) {
return translate("Invalid port");
}
if (p == 0) p = "";
else p = p.toInteger() + '';
handler.set_peer_option(id, "rdp_port", p);
}
var name = (res.username || '').trim();
if (name != name0) {
handler.set_peer_option(id, "rdp_username", name);
}
var pass = (res.password || '').trim();
if (pass != pass0) {
handler.set_peer_option(id, "rdp_password", pass);
}
}, 240);
}

View File

@ -5,7 +5,7 @@ use clipboard::{
};
use hbb_common::{
allow_err,
config::{Config, ICON},
config::Config,
fs, log,
message_proto::*,
protobuf::Message as _,
@ -21,6 +21,7 @@ use std::{
pub struct ConnectionManagerInner {
root: Option<Element>,
senders: HashMap<i32, mpsc::UnboundedSender<Data>>,
click_time: i64,
}
#[derive(Clone)]
@ -41,6 +42,7 @@ impl ConnectionManager {
let inner = ConnectionManagerInner {
root: None,
senders: HashMap::new(),
click_time: Default::default(),
};
let cm = Self(Arc::new(RwLock::new(inner)));
let cloned = cm.clone();
@ -49,7 +51,18 @@ impl ConnectionManager {
}
fn get_icon(&mut self) -> String {
ICON.to_owned()
crate::get_icon()
}
fn check_click_time(&mut self, id: i32) {
let lock = self.read().unwrap();
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::ClickTime(0)));
}
}
fn get_click_time(&self) -> f64 {
self.read().unwrap().click_time as _
}
#[inline]
@ -112,6 +125,9 @@ impl ConnectionManager {
Data::ChatMessage { text } => {
self.call("newMessage", &make_args!(id, text));
}
Data::ClickTime(ms) => {
self.write().unwrap().click_time = ms;
}
Data::FS(v) => match v {
ipc::FS::ReadDir {
dir,
@ -330,6 +346,8 @@ impl sciter::EventHandler for ConnectionManager {
sciter::dispatch_script_call! {
fn t(String);
fn check_click_time(i32);
fn get_click_time();
fn get_icon();
fn close(i32);
fn authorize(i32);
@ -421,7 +439,6 @@ async fn start_ipc(cm: ConnectionManager) {
#[tokio::main(flavor = "current_thread")]
async fn start_pa() {
use crate::audio_service::AUDIO_DATA_SIZE_U8;
use hbb_common::config::APP_NAME;
match new_listener("_pa").await {
Ok(mut incoming) => {
@ -448,14 +465,14 @@ async fn start_pa() {
let spec = pulse::sample::Spec {
format: pulse::sample::Format::F32le,
channels: 2,
rate: crate::platform::linux::PA_SAMPLE_RATE,
rate: crate::platform::PA_SAMPLE_RATE,
};
log::info!("pa monitor: {:?}", device);
// systemctl --user status pulseaudio.service
let mut buf: Vec<u8> = vec![0; AUDIO_DATA_SIZE_U8];
match psimple::Simple::new(
None, // Use the default server
APP_NAME, // Our applications name
&crate::get_app_name(), // Our applications name
pulse::stream::Direction::Record, // We want a record stream
Some(&device), // Use the default device
"record", // Description of our stream

View File

@ -226,7 +226,13 @@ event click $(div.chaticon) {
}
function checkClickTime(callback) {
callback();
var click_callback_time = getTime();
handler.check_click_time(body.cid);
self.timer(120ms, function() {
var d = click_callback_time - handler.get_click_time();
if (d > 120)
callback();
});
}
function adaptSizeForChat() {
@ -234,10 +240,10 @@ function adaptSizeForChat() {
display: show_chat ? "block" : "none",
};
var (x, y, w, h) = view.box(#rectw, #border, #screen);
if (show_chat && w < 600) {
view.move(x - (600 - w), y, 600, h);
} else if (!show_chat && w > 450) {
view.move(x + (w - 300), y, 300, h);
if (show_chat && w < scaleIt(600)) {
view.move(x - (scaleIt(600) - w), y, scaleIt(600), h);
} else if (!show_chat && w > scaleIt(450)) {
view.move(x + (w - scaleIt(300)), y, scaleIt(300), h);
}
}
@ -327,8 +333,8 @@ view << event statechange {
function self.ready() {
adjustBorder();
var (sw, sh) = view.screenBox(#workarea, #dimension);
var w = 300;
var h = 400;
var w = scaleIt(300);
var h = scaleIt(400);
view.move(sw - w, 0, w, h);
}
@ -372,7 +378,7 @@ function self.closing() {
function adjustHeader() {
var hw = $(header).box(#width);
var hw = $(header).box(#width) / scaleFactor;
var tabswrapper = $(div.tabs-wrapper);
var tabs = $(div.tabs);
var arrows = $(div.tab-arrows);
@ -380,7 +386,7 @@ function adjustHeader() {
var n = connections.length;
var wtab = 80;
var max = hw - 98;
var need_width = n * wtab + 2; // include border of active tab
var need_width = n * wtab + scaleIt(2); // include border of active tab
if (need_width < max) {
arrows.style.set {
display: "none",

View File

@ -324,6 +324,33 @@ menu li.line-through {
color: red;
}
#tags {
border: color(border) 1px solid;
size: *;
padding: 0.5em;
overflow-y: scroll-indicator;
border-spacing: 0.5em;
flow: horizontal-flow;
}
#tags span {
display: inline-block;
border: color(border) 1px solid;
border-radius: 6px;
padding: 3px 0.5em;
word-wrap: normal;
}
#tags span.active {
background: color(button);
border-color: color(button);
color: white;
}
#tags span:hover {
border-color: color(hover-border);
}
div#msgbox .msgbox-icon svg {
size: 80px;
background: white;

View File

@ -16,6 +16,19 @@ function isEnterKey(evt) {
(is_linux && evt.keyCode == 65421));
}
function getScaleFactor() {
if (!is_win) return 1;
return self.toPixels(10000dip) / 10000.;
}
var scaleFactor = getScaleFactor();
view << event resolutionchange {
scaleFactor = getScaleFactor();
}
function scaleIt(x) {
return (x * scaleFactor).toInteger();
}
stdout.println("scaleFactor", scaleFactor);
function translate(name) {
try {
return handler.t(name);
@ -226,6 +239,8 @@ function msgbox(type, title, content, callback=null, height=180, width=500, hasR
}
var remember = false;
try { remember = handler.get_remember(); } catch(e) {}
var auto_login = false;
try { auto_login = handler.get_option("auto-login") != ''; } catch(e) {}
width += is_xfce ? 50 : 0;
height += is_xfce ? 50 : 0;
@ -248,7 +263,7 @@ function msgbox(type, title, content, callback=null, height=180, width=500, hasR
} else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) {
callback = function() { view.close(); }
}
$(#msgbox).content(<MsgboxComponent width={width} height={height} type={type} title={title} content={content} remember={remember} callback={callback} contentStyle={contentStyle} hasRetry={hasRetry} />);
$(#msgbox).content(<MsgboxComponent width={width} height={height} auto_login={auto_login} type={type} title={title} content={content} remember={remember} callback={callback} contentStyle={contentStyle} hasRetry={hasRetry} />);
}
function connecting() {
@ -361,10 +376,32 @@ class PasswordComponent: Reactor.Component {
}
}
// type: #post, #get, #delete, #put
function httpRequest(url, type, params, _onSuccess, _onError, headers="") {
if (type != #post) {
stderr.println("only post ok");
}
handler.post_request(url, JSON.stringify(params), headers);
function check_status() {
var status = handler.get_async_job_status();
if (status == " ") self.timer(0.1s, check_status);
else {
try {
var data = JSON.parse(status);
_onSuccess(data);
} catch (e) {
_onError(status, 0);
}
}
}
check_status();
}
function isReasonableSize(r) {
var x = r[0];
var y = r[1];
return !(x < -3200 || x > 3200 || y < -3200 || y > 3200);
var n = scaleIt(3200);
return !(x < -n || x > n || y < -n || y > n);
}
function awake() {

View File

@ -60,7 +60,39 @@ function stateChanged() {
var header;
var old_window_state = View.WINDOW_SHOWN;
var is_edit_os_password;
class EditOsPassword: Reactor.Component {
function render() {
return <span style="margin-left: 12px; padding: 0 6px; display: inline-block;" .link>{svg_edit}</span>;
}
function onMouse(evt) {
if (evt.type == Event.MOUSE_DOWN) {
is_edit_os_password = true;
editOSPassword();
}
}
}
function editOSPassword(login=false) {
var p0 = handler.get_option('os-password');
msgbox("custom-os-password", 'OS Password', p0, function(res=null) {
if (!res) return;
var a0 = handler.get_option('auto-login') != '';
var p = (res.password || '').trim();
var a = res.auto_login || false;
if (p == p0 && a == a0) return;
if (p != p0) handler.set_option('os-password', p);
if (a != a0) handler.set_option('auto-login', a ? 'Y' : '');
if (p && login) {
handler.input_os_password(p, true);
}
});
}
class Header: Reactor.Component {
this var conn_note = "";
function this() {
header = this;
}
@ -138,8 +170,10 @@ class Header: Reactor.Component {
function renderActionPop() {
return <popup>
<menu.context #action-options>
{keyboard_enabled ? <li #os-password>{translate('OS Password')}<EditOsPassword /></li> : ""}
<li #transfer-file>{translate('Transfer File')}</li>
<li #tunnel>{translate('TCP Tunneling')}</li>
{handler.get_audit_server() && <li #note>{translate('Note')}</li>}
<div .separator />
{keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ? <li #ctrl-alt-del>{translate('Insert')} Ctrl + Alt + Del</li> : ""}
{keyboard_enabled ? <li #lock-screen>{translate('Insert Lock')}</li> : ""}
@ -224,11 +258,33 @@ class Header: Reactor.Component {
event click $(#transfer-file) {
handler.transfer_file();
}
event click $(#os-password) (evt) {
if (is_edit_os_password) {
is_edit_os_password = false;
return;
}
var p = handler.get_option('os-password');
if (!p) editOSPassword(true);
else handler.input_os_password(p, true);
}
event click $(#tunnel) {
handler.tunnel();
}
event click $(#note) {
var self = this;
msgbox("custom", "Note", <div .form>
<textarea .outline-focus spellcheck="false" name="text" novalue="input note here" style="overflow: scroll-indicator; width:*; height: 140px; font-size: 1.2em; padding: 0.5em;">{self.conn_note}</textarea>
</div>, function(res=null) {
if (!res) return;
if (!res.text) return;
self.conn_note = res.text;
handler.send_note(res.text);
}, 280);
}
event click $(#ctrl-alt-del) {
handler.ctrl_alt_del();
}
@ -355,7 +411,7 @@ function updateWindowToolbarPosition() {
var el = $(div.window-toolbar);
var w1 = el.box(#width, #border);
var w2 = $(header).box(#width, #border);
var x = (w2 - w1) / 2;
var x = (w2 - w1) / 2 / scaleFactor;
el.style.set {
left: x + "px",
display: "block",
@ -391,10 +447,10 @@ function startChat() {
}
var icon = handler.get_icon();
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
var w = 300;
var h = 400;
var w = scaleIt(300);
var h = scaleIt(400);
var x = (sx + sw - w) / 2;
var y = sy + 80;
var y = sy + scaleIt(80);
var params = {
type: View.FRAME_WINDOW,
x: x,

View File

@ -34,6 +34,20 @@ body {
border-right: color(border) 1px solid;
}
#ab .left-pane {
background: white;
border-radius: 1em;
padding: 1em;
}
#ab .right-pane {
background: none;
}
#ab .right-content {
overflow: unset;
}
.left-pane > div:nth-child(1) {
border-spacing: 1em;
padding: 20px;
@ -358,15 +372,19 @@ div.trust-me > div:nth-child(5) {
text-align: center;
}
div#myid {
div#myid, div#tags-label {
position: relative;
}
div#myid svg#menu {
div#myid svg#menu, div#tags-label svg#menu {
position: absolute;
right: -1em;
}
div#tags-label svg#menu:hover {
background-color: #ddd;
}
div.remote-session svg#menu {
position: absolute;
right: 0;

View File

@ -1,13 +1,16 @@
if (is_osx) view.windowBlurbehind = #light;
stdout.println("current platform:", OS);
stdout.println("is_xfce: ", is_xfce);
// html min-width, min-height not working on mac, below works for all
view.windowMinSize = (500, 300);
view.windowMinSize = (scaleIt(500), scaleIt(300));
var app;
var tmp = handler.get_connect_status();
var connect_status = tmp[0];
var service_stopped = handler.get_option("stop-service") == "Y";
var rendezvous_service_stopped = false;
var using_public_server = handler.using_public_server();
var software_update_url = "";
var key_confirmed = tmp[1];
var system_error = "";
@ -42,12 +45,17 @@ class ConnectStatus: Reactor.Component {
} else if (connect_status == 0) {
return translate('connecting_status');
}
return translate("Ready");
if (!handler.using_public_server()) return translate('Ready');
return <span>{translate("Ready")}, <span .link #setup-server>{translate("setup_server_tip")}</span></span>;
}
event click $(#start-service) () {
handler.set_option("stop-service", "");
}
event click $(#setup-server) () {
handler.open_url("https://rustdesk.com/blog/id-relay-set/");
}
}
function createNewConnect(id, type) {
@ -62,6 +70,19 @@ function createNewConnect(id, type) {
handler.new_remote(id, type);
}
class ShareRdp: Reactor.Component {
function render() {
var rdp_shared_string = translate("Enable RDP session sharing");
var cls = handler.is_share_rdp() ? "selected" : "line-through";
return <li class={cls}><span>{svg_checkmark}</span>{rdp_shared_string}</li>;
}
function onClick() {
handler.set_share_rdp(!handler.is_share_rdp());
this.update();
}
}
var direct_server;
class DirectServer: Reactor.Component {
function this() {
@ -144,6 +165,13 @@ class AudioInputs: Reactor.Component {
}
}
function getUserName() {
try {
return JSON.parse(handler.get_local_option("user_info")).name;
} catch(e) {}
return '';
}
class MyIdMenu: Reactor.Component {
function this() {
myIdMenu = this;
@ -152,11 +180,12 @@ class MyIdMenu: Reactor.Component {
function render() {
return <div #myid>
{this.renderPop()}
{translate("ID")}{svg_menu}
ID{svg_menu}
</div>;
}
function renderPop() {
var username = handler.get_local_option("access_token") ? getUserName() : '';
return <popup>
<menu.context #config-options>
<li #enable-keyboard><span>{svg_checkmark}</span>{translate('Enable Keyboard/Mouse')}</li>
@ -164,16 +193,24 @@ class MyIdMenu: Reactor.Component {
<li #enable-file-transfer><span>{svg_checkmark}</span>{translate('Enable File Transfer')}</li>
<li #enable-tunnel><span>{svg_checkmark}</span>{translate('Enable TCP Tunneling')}</li>
<AudioInputs />
<li #allow-remote-config-modification><span>{svg_checkmark}</span>{translate('Enable remote configuration modification')}</li>
<div .separator />
<li #custom-server>{translate('ID/Relay Server')}</li>
<li #whitelist title={translate('whitelist_tip')}>{translate('IP Whitelisting')}</li>
<li #socks5-server>{translate('Socks5 Proxy')}</li>
{is_win ? <li #install-virtual-display>Install virtual display</li> : ""}
<div .separator />
<li #stop-service><span>{svg_checkmark}</span>{translate("Enable Service")}</li>
<li #stop-service class={service_stopped ? "line-through" : "selected"}><span>{svg_checkmark}</span>{translate("Enable Service")}</li>
{handler.is_rdp_service_open() ? <ShareRdp /> : ""}
<DirectServer />
{false && handler.using_public_server() && <li #allow-always-relay><span>{svg_checkmark}</span>{translate('Always connected via relay')}</li>}
{handler.has_rendezvous_service() ? <li #stop-rendezvous-service>{translate(rendezvous_service_stopped ? "Start ID/relay service" : "Stop ID/relay service")}</li> : ""}
{handler.is_ok_change_id() ? <div .separator /> : ""}
{username ?
<li #logout>{translate('Logout')} ({username})</li> :
<li #login>{translate('Login')}</li>}
{handler.is_ok_change_id() && key_confirmed ? <li #change-id>{translate('Change ID')}</li> : ""}
<div .separator />
<li #about>{translate('About')} {" "} {handler.get_app_name()}</li>
<li #about>{translate('About')} {" "}{handler.get_app_name()}</li>
</menu>
</popup>;
}
@ -190,15 +227,25 @@ class MyIdMenu: Reactor.Component {
this.$(svg#menu).popup(menu);
}
event click $(li#login) () {
login();
}
event click $(li#logout) () {
logout();
}
function toggleMenuState() {
for (var el in $$(menu#config-options>li)) {
if (el.id && el.id.indexOf("enable-") == 0) {
var enabled = handler.get_option(el.id) != "N";
el.attributes.toggleClass("selected", enabled);
el.attributes.toggleClass("line-through", !enabled);
} else if (el.id && el.id === "stop-service") {
el.attributes.toggleClass("selected", !service_stopped);
el.attributes.toggleClass("line-through", service_stopped);
}
if (el.id && el.id.indexOf("allow-") == 0) {
var enabled = handler.get_option(el.id) == "Y";
el.attributes.toggleClass("selected", enabled);
el.attributes.toggleClass("line-through", !enabled);
}
}
}
@ -207,9 +254,10 @@ class MyIdMenu: Reactor.Component {
var name = handler.get_app_name();
msgbox("custom-nocancel-nook-hasclose", "About " + name, "<div style='line-height: 2em'> \
<div>Version: " + handler.get_version() + " \
<div .link .custom-event url='http://rustdesk.com/privacy'>Privacy Statement</div> \
<div .link .custom-event url='http://rustdesk.com'>Website</div> \
<div .link .custom-event url='https://rustdesk.com/privacy'>Privacy Statement</div> \
<div .link .custom-event url='https://rustdesk.com'>Website</div> \
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright &copy; 2022 Purslane Ltd.\
<br />" + handler.get_license() + " \
<p style='font-weight: bold'>Made with heart in this chaotic world!</p>\
</div>\
</div>", function(el) {
@ -223,11 +271,14 @@ class MyIdMenu: Reactor.Component {
if (me.id && me.id.indexOf("enable-") == 0) {
handler.set_option(me.id, handler.get_option(me.id) == "N" ? "" : "N");
}
if (me.id && me.id.indexOf("allow-") == 0) {
handler.set_option(me.id, handler.get_option(me.id) == "Y" ? "" : "Y");
}
if (me.id == "whitelist") {
var old_value = handler.get_option("whitelist").split(",").join("\n");
msgbox("custom-whitelist", translate("IP Whitelisting"), "<div .form> \
<div>" + translate("whitelist_sep") + "</div> \
<textarea spellcheck=\"false\" name=\"text\" novalue=\"0.0.0.0\" style=\"overflow: scroll-indicator; width:*; height: 140px; font-size: 1.2em; padding: 0.5em;\">" + old_value + "</textarea>\
<textarea .outline-focus spellcheck=\"false\" name=\"text\" novalue=\"0.0.0.0\" style=\"overflow: scroll-indicator; width:*; height: 140px; font-size: 1.2em; padding: 0.5em;\">" + old_value + "</textarea>\
</div> \
", function(res=null) {
if (!res) return;
@ -248,16 +299,22 @@ class MyIdMenu: Reactor.Component {
} else if (me.id == "custom-server") {
var configOptions = handler.get_options();
var old_relay = configOptions["relay-server"] || "";
var old_api = configOptions["api-server"] || "";
var old_id = configOptions["custom-rendezvous-server"] || "";
var old_key = configOptions["key"] || "";
msgbox("custom-server", "ID/Relay Server", "<div .form .set-password> \
<div><span>" + translate("ID Server") + ": </span><input .outline-focus name='id' value='" + old_id + "' /></div> \
<div><span>" + translate("Relay Server") + ": </span><input name='relay' value='" + old_relay + "' /></div> \
<div><span>" + translate("API Server") + ": </span><input name='api' value='" + old_api + "' /></div> \
<div><span>" + translate("Key") + ": </span><input name='key' value='" + old_key + "' /></div> \
</div> \
", function(res=null) {
if (!res) return;
var id = (res.id || "").trim();
var relay = (res.relay || "").trim();
if (id == old_id && relay == old_relay) return;
var api = (res.api || "").trim().toLowerCase();
var key = (res.key || "").trim();
if (id == old_id && relay == old_relay && key == old_key && api == old_api) return;
if (id) {
var err = handler.test_if_valid_server(id);
if (err) return translate("ID Server") + ": " + err;
@ -266,10 +323,17 @@ class MyIdMenu: Reactor.Component {
var err = handler.test_if_valid_server(relay);
if (err) return translate("Relay Server") + ": " + err;
}
if (api) {
if (0 != api.indexOf("https://") && 0 != api.indexOf("http://")) {
return translate("API Server") + ": " + translate("invalid_http");
}
}
configOptions["custom-rendezvous-server"] = id;
configOptions["relay-server"] = relay;
configOptions["api-server"] = api;
configOptions["key"] = key;
handler.set_options(configOptions);
}, 240);
}, 260);
} else if (me.id == "socks5-server") {
var socks5 = handler.get_socks() || {};
var old_proxy = socks5[0] || "";
@ -292,10 +356,33 @@ class MyIdMenu: Reactor.Component {
}
handler.set_socks(proxy, username, password);
}, 240);
} else if (me.id == "install-virtual-display") {
handler.install_virtual_display();
} else if (me.id == "stop-service") {
handler.set_option("stop-service", service_stopped ? "" : "Y");
} else if (me.id == "stop-rendezvous-service") {
handler.set_option("stop-rendezvous-service", rendezvous_service_stopped ? "" : "Y");
} else if (me.id == "change-id") {
msgbox("custom-id", translate("Change ID"), "<div .form> \
<div>" + translate('id_change_tip') + " </div> \
<div><span style='width: 100px; display:inline-block'>ID: </span><input .outline-focus style='width: 250px' name='id' /></div> \
</div> \
", function(res=null, show_progress) {
if (!res) return;
show_progress();
var id = (res.id || "").trim();
if (!id) return;
if (id == my_id) return;
handler.change_id(id);
function check_status() {
var status = handler.get_async_job_status();
if (status == " ") self.timer(0.1s, check_status);
else {
if (status) show_progress(false, translate(status));
else show_progress(-1);
}
}
check_status();
return " ";
});
} else if (me.id == "about") {
this.showAbout()
}
@ -387,6 +474,7 @@ class App: Reactor.Component
</div>
<ConnectStatus @{this.connect_status} />
</div>
<div #overlay style="position: absolute;size:*;background:black;opacity:0.5;display:none" />
<div #msgbox />
</div>;
}
@ -418,48 +506,28 @@ class InstallMe: Reactor.Component {
}
}
const http = function() {
function makeRequest(httpverb) {
return function( params ) {
params.type = httpverb;
view.request(params);
};
}
function download(from, to, args..)
{
var rqp = { type:#get, url: from, toFile: to };
var fn = 0;
var on = 0;
for( var p in args )
if( p instanceof Function )
{
switch(++fn) {
case 1: rqp.success = p; break;
case 2: rqp.error = p; break;
case 3: rqp.progress = p; break;
}
} else if( p instanceof Object )
{
switch(++on) {
case 1: rqp.params = p; break;
case 2: rqp.headers = p; break;
}
function download(from, to, args..) {
var rqp = { type:#get, url: from, toFile: to };
var fn = 0;
var on = 0;
for( var p in args ) {
if( p instanceof Function ) {
switch(++fn) {
case 1: rqp.success = p; break;
case 2: rqp.error = p; break;
case 3: rqp.progress = p; break;
}
} else if( p instanceof Object ) {
switch(++on) {
case 1: rqp.params = p; break;
case 2: rqp.headers = p; break;
}
}
view.request(rqp);
}
return {
get: makeRequest(#get),
post: makeRequest(#post),
put: makeRequest(#put),
del: makeRequest(#delete),
download: download
};
}();
}
view.request(rqp);
}
// current running version is higher than installed
class UpgradeMe: Reactor.Component {
function render() {
var update_or_download = is_osx ? "download" : "update";
@ -509,7 +577,7 @@ class UpdateMe: Reactor.Component {
el.content("Downloading %" + (loaded * 100 / total));
};
stdout.println("Downloading " + url + " to " + path);
http.download(
download(
url,
self.url(path),
onsuccess, onerror, onprogress);
@ -778,7 +846,6 @@ event keydown (evt) {
$(body).content(<App />);
function self.closing() {
// return false; // can prevent window close
var (x, y, w, h) = view.box(#rectw, #border, #screen);
handler.closing(x, y, w, h);
return true;
@ -787,13 +854,19 @@ function self.closing() {
function self.ready() {
var r = handler.get_size();
if (isReasonableSize(r) && r[2] > 0) {
view.move(r[0], r[1], r[2], r[3]);
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
if (r[2] >= sw && r[3] >= sh) {
self.timer(1ms, function() { view.windowState = View.WINDOW_MAXIMIZED; });
} else {
view.move(r[0], r[1], r[2], r[3]);
}
} else {
centerize(800, 600);
centerize(scaleIt(800), scaleIt(600));
}
if (!handler.get_remote_id()) {
view.focus = $(#remote_id);
}
refreshCurrentUser();
}
function showAbout() {
@ -801,6 +874,7 @@ function showAbout() {
}
function showSettings() {
if ($(#overlay).style#display == 'block') return;
myIdMenu.showSettingMenu();
}
@ -811,6 +885,16 @@ function checkConnectStatus() {
service_stopped = tmp;
app.update();
}
tmp = !!handler.get_option("stop-rendezvous-service");
if (tmp != rendezvous_service_stopped) {
rendezvous_service_stopped = tmp;
myIdMenu.update();
}
tmp = handler.using_public_server();
if (tmp != using_public_server) {
using_public_server = tmp;
app.connect_status.update();
}
tmp = handler.get_connect_status();
if (tmp[0] != connect_status) {
connect_status = tmp[0];
@ -836,10 +920,126 @@ function checkConnectStatus() {
}
if (handler.recent_sessions_updated()) {
stdout.println("recent sessions updated");
updateAbPeer();
app.update();
}
checkConnectStatus();
});
check_if_overlay();
checkConnectStatus();
});
}
var enter = false;
function self.onMouse(evt) {
switch(evt.type) {
case Event.MOUSE_ENTER:
enter = true;
check_if_overlay();
break;
case Event.MOUSE_LEAVE:
$(#overlay).style#display = 'none';
enter = false;
break;
}
}
function check_if_overlay() {
if (!handler.get_option('allow-remote-config-modification')) {
var time0 = getTime();
handler.check_mouse_time();
self.timer(120ms, function() {
if (!enter) return;
var d = time0 - handler.get_mouse_time();
if (d < 120) $(#overlay).style#display = 'block';
});
}
}
checkConnectStatus();
function login() {
var name0 = getUserName();
var pass0 = '';
msgbox("custom-login", translate('Login'), <div .form .set-password>
<div><span>{translate('Username')}:</span><input|text name="username" value={name0} .outline-focus /></div>
<div><span>{translate('Password')}:</span><PasswordComponent value={pass0} /></div>
</div>, function(res=null, show_progress) {
if (!res) return;
show_progress();
var name = (res.username || '').trim();
if (!name) {
show_progress(false, translate("Username missed"));
return " ";
}
var pass = (res.password || '').trim();
if (!pass) {
show_progress(false, translate("Password missed"));
return " ";
}
abLoading = true;
var url = handler.get_api_server();
httpRequest(url + "/api/login", #post, {username: name, password: pass, id: my_id, uuid: handler.get_uuid()}, function(data) {
if (data.error) {
abLoading = false;
var err = translate(data.error);
show_progress(false, err);
return;
}
handler.set_local_option("access_token", data.access_token);
handler.set_local_option("user_info", JSON.stringify(data.user));
show_progress(-1);
myIdMenu.update();
getAb();
}, function(err, status) {
abLoading = false;
err = translate(err);
if (url.indexOf('rustdesk') < 0) err = url + ', ' + err;
show_progress(false, err);
});
return " ";
});
}
function reset_token() {
handler.set_local_option("access_token", "");
handler.set_local_option("user_info", "");
handler.set_local_option("selected-tags", "");
myIdMenu.update();
resetAb();
if (abComponent) {
abComponent.update();
}
}
function logout() {
var url = handler.get_api_server();
httpRequest(url + "/api/logout", #post, {id: my_id, uuid: handler.get_uuid()}, function(data) {
}, function(err, status) {
msgbox("custom-error", translate('Error'), err);
}, getHttpHeaders());
reset_token();
}
function refreshCurrentUser() {
if (!handler.get_local_option("access_token")) return;
abLoading = true;
abError = "";
app.update();
httpRequest(handler.get_api_server() + "/api/currentUser", #post, {id: my_id, uuid: handler.get_uuid()}, function(data) {
if (data.error) {
handleAbError(data.error);
return;
}
handler.set_local_option("user_info", JSON.stringify(data));
myIdMenu.update();
getAb();
}, function(err, status) {
if (status == 401 || status == 400) {
reset_token();
}
handleAbError(err);
}, getHttpHeaders());
}
function getHttpHeaders() {
return "Authorization: Bearer " + handler.get_local_option("access_token");
}

View File

@ -5,7 +5,7 @@
div.content {
size: *;
background: white;
padding:2em 10em;
padding:2em 8em;
border-spacing: 1em;
}
input {

View File

@ -1,12 +1,16 @@
function self.ready() {
centerize(800, 600);
centerize(scaleIt(800), scaleIt(600));
}
var install_path = "";
class Install: Reactor.Component {
function render() {
return <div .content>
<div style="font-size: 2em;">{translate('Installation')}</div>
<div style="margin: 2em 0;">{translate('Installation Path')} {": "}<input|text disabled value={view.install_path()} /></div>
<div style="margin: 2em 0;">{translate('Installation Path')} {": "}<input|text disabled value={view.install_path()} #path_input />
<button .button .outline #path style="margin-left: 1em">{translate('Change Path')}</button>
</div>
<div><button|checkbox #startmenu checked>{translate('Create start menu shortcuts')}</button></div>
<div><button|checkbox #desktopicon checked>{translate('Create desktop icon')}</button></div>
<div #aggrement .link style="margin-top: 2em;">{translate('End-user license agreement')}</div>
@ -16,6 +20,9 @@ class Install: Reactor.Component {
<progress style={"color:" + color} style="display: none" />
<button .button id="cancel" .outline style="margin-right: 2em;">{translate('Cancel')}</button>
<button .button id="submit">{translate('Accept and Install')}</button>
{handler.show_run_without_install() && <button .button #run-without-install .outline style="margin-left: 2em;">
{translate('Run without install')}
</button>}
</div>
</div>;
}
@ -24,6 +31,21 @@ class Install: Reactor.Component {
view.close();
}
event click $(#run-without-install) {
handler.run_without_install();
}
event click $(#path) {
install_path = view.selectFolder() || "";
if (install_path) {
install_path = install_path.urlUnescape();
install_path = install_path.replace("file://", "").replace("/", "\\");
if (install_path[install_path.length - 1] != "\\") install_path += "\\";
install_path += handler.get_app_name();
$(#path_input).value = install_path;
}
}
event click $(#aggrement) {
view.open_url("http://rustdesk.com/privacy");
}
@ -38,7 +60,7 @@ class Install: Reactor.Component {
if ($(#desktopicon).value) {
args += "desktopicon ";
}
view.install_me(args);
view.install_me(args, install_path);
}
}

View File

@ -25,6 +25,7 @@ class MsgboxComponent: Reactor.Component {
this.remember = params.remember;
this.callback = params.callback;
this.hasRetry = params.hasRetry;
this.auto_login = params.auto_login;
this.contentStyle = params.contentStyle;
try { this.content = translate_text(this.content); } catch (e) {}
}
@ -58,11 +59,18 @@ class MsgboxComponent: Reactor.Component {
if (this.type == "input-password") {
return this.getInputPasswordContent();
}
if (this.type == "custom-os-password") {
var ts = this.auto_login ? { checked: true } : {};
return <div .form>
<PasswordComponent value={this.content} />
<div><button|checkbox(auto_login) {ts}>{translate('Auto Login')}</button></div>
</div>;
}
return this.content;
}
function getColor() {
if (this.type == "input-password") {
if (this.type == "input-password" || this.type == "custom-os-password") {
return "#AD448E";
}
if (this.type == "success") {

View File

@ -12,7 +12,7 @@ use clipboard::{
use enigo::{self, Enigo, KeyboardControllable};
use hbb_common::{
allow_err,
config::{self, Config, PeerConfig},
config::{Config, LocalConfig, PeerConfig},
fs, log,
message_proto::{permission_info::Permission, *},
protobuf::Message as _,
@ -88,6 +88,8 @@ impl Deref for Handler {
}
}
impl FileManager for Handler {}
impl sciter::EventHandler for Handler {
fn get_subscription(&mut self) -> Option<EVENT_GROUPS> {
Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT)
@ -155,12 +157,15 @@ impl sciter::EventHandler for Handler {
}
sciter::dispatch_script_call! {
fn get_audit_server();
fn send_note(String);
fn is_xfce();
fn get_id();
fn get_default_pi();
fn get_option(String);
fn t(String);
fn set_option(String, String);
fn input_os_password(String, bool);
fn save_close_state(String, String);
fn is_file_transfer();
fn is_port_forward();
@ -243,6 +248,8 @@ impl Handler {
let mut me = self.clone();
let peer = self.peer_platform();
let is_win = peer == "Windows";
#[cfg(windows)]
crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _);
std::thread::spawn(move || {
// This will block.
std::env::set_var("KEYBOARD_ONLY", "y"); // pass to rdev
@ -276,6 +283,9 @@ impl Handler {
#[cfg(not(windows))]
let ctrl = get_key_state(enigo::Key::Control);
let shift = get_key_state(enigo::Key::Shift);
#[cfg(windows)]
let command = crate::platform::windows::get_win_key_state();
#[cfg(not(windows))]
let command = get_key_state(enigo::Key::Meta);
let control_key = match key {
Key::Alt => Some(ControlKey::Alt),
@ -530,6 +540,27 @@ impl Handler {
crate::client::translate(name)
}
fn get_audit_server(&self) -> String {
if self.lc.read().unwrap().conn_id <= 0
|| LocalConfig::get_option("access_token").is_empty()
{
return "".to_owned();
}
crate::get_audit_server(
Config::get_option("api-server"),
Config::get_option("custom-rendezvous-server"),
)
}
fn send_note(&self, note: String) {
let url = self.get_audit_server();
let id = self.id.clone();
let conn_id = self.lc.read().unwrap().conn_id;
std::thread::spawn(move || {
send_note(url, id, conn_id, note);
});
}
fn is_xfce(&self) -> bool {
crate::platform::is_xfce()
}
@ -659,6 +690,10 @@ impl Handler {
self.lc.write().unwrap().set_option(k, v);
}
fn input_os_password(&mut self, pass: String, activate: bool) {
input_os_password(pass, activate, self.clone());
}
fn save_close_state(&self, k: String, v: String) {
self.write().unwrap().close_state.insert(k, v);
}
@ -671,38 +706,7 @@ impl Handler {
}
fn get_icon(&mut self) -> String {
config::ICON.to_owned()
}
fn get_home_dir(&mut self) -> String {
fs::get_home_as_string()
}
fn read_dir(&mut self, path: String, include_hidden: bool) -> Value {
match fs::read_dir(&fs::get_path(&path), include_hidden) {
Err(_) => Value::null(),
Ok(fd) => {
let mut m = make_fd(0, &fd.entries.to_vec(), false);
m.set_item("path", path);
m
}
}
}
fn cancel_job(&mut self, id: i32) {
self.send(Data::CancelJob(id));
}
fn read_remote_dir(&mut self, path: String, include_hidden: bool) {
let mut msg_out = Message::new();
let mut file_action = FileAction::new();
file_action.set_read_dir(ReadDir {
path,
include_hidden,
..Default::default()
});
msg_out.set_file_action(file_action);
self.send(Data::Message(msg_out));
crate::get_icon()
}
fn send_chat(&mut self, text: String) {
@ -727,45 +731,6 @@ impl Handler {
self.send(Data::Message(msg_out));
}
fn remove_file(&mut self, id: i32, path: String, file_num: i32, is_remote: bool) {
self.send(Data::RemoveFile((id, path, file_num, is_remote)));
}
fn remove_dir_all(&mut self, id: i32, path: String, is_remote: bool) {
self.send(Data::RemoveDirAll((id, path, is_remote)));
}
fn confirm_delete_files(&mut self, id: i32, file_num: i32) {
self.send(Data::ConfirmDeleteFiles((id, file_num)));
}
fn set_no_confirm(&mut self, id: i32) {
self.send(Data::SetNoConfirm(id));
}
fn remove_dir(&mut self, id: i32, path: String, is_remote: bool) {
if is_remote {
self.send(Data::RemoveDir((id, path)));
} else {
fs::remove_all_empty_dir(&fs::get_path(&path)).ok();
}
}
fn create_dir(&mut self, id: i32, path: String, is_remote: bool) {
self.send(Data::CreateDir((id, path, is_remote)));
}
fn send_files(
&mut self,
id: i32,
path: String,
to: String,
include_hidden: bool,
is_remote: bool,
) {
self.send(Data::SendFiles((id, path, to, include_hidden, is_remote)));
}
fn is_file_transfer(&self) -> bool {
self.cmd == "--file-transfer"
}
@ -859,13 +824,6 @@ impl Handler {
fs::get_string(&path)
}
#[inline]
fn send(&mut self, data: Data) {
if let Some(ref sender) = self.read().unwrap().sender {
sender.send(data).ok();
}
}
fn login(&mut self, password: String, remember: bool) {
self.send(Data::Login((password, remember)));
}
@ -875,12 +833,16 @@ impl Handler {
}
fn enter(&mut self) {
#[cfg(windows)]
crate::platform::windows::stop_system_key_propagate(true);
unsafe {
IS_IN = true;
}
}
fn leave(&mut self) {
#[cfg(windows)]
crate::platform::windows::stop_system_key_propagate(false);
unsafe {
IS_IN = false;
}
@ -896,28 +858,17 @@ impl Handler {
shift: bool,
command: bool,
) {
let mut msg_out = Message::new();
let mut mouse_event = MouseEvent {
mask,
x,
y,
..Default::default()
};
if alt {
mouse_event.modifiers.push(ControlKey::Alt.into());
#[allow(unused_mut)]
let mut command = command;
#[cfg(windows)]
{
if !command && crate::platform::windows::get_win_key_state() {
command = true;
}
}
if shift {
mouse_event.modifiers.push(ControlKey::Shift.into());
}
if ctrl {
mouse_event.modifiers.push(ControlKey::Control.into());
}
if command {
mouse_event.modifiers.push(ControlKey::Meta.into());
}
msg_out.set_mouse_event(mouse_event);
self.send(Data::Message(msg_out));
// on macos, ctrl + left = right, up wont emit, so we need to
send_mouse(mask, x, y, alt, ctrl, shift, command, self);
// on macos, ctrl + left button down = right button down, up won't emit, so we need to
// emit up myself if peer is not macos
// to-do: how about ctrl + left from win to macos
if cfg!(target_os = "macos") {
@ -1199,10 +1150,19 @@ async fn start_one_port_forward(
remote_host: String,
remote_port: i32,
receiver: mpsc::UnboundedReceiver<Data>,
key: &str,
token: &str,
) {
handler.lc.write().unwrap().port_forward = (remote_host, remote_port);
if let Err(err) =
crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver).await
if let Err(err) = crate::port_forward::listen(
handler.id.clone(),
port,
handler.clone(),
receiver,
key,
token,
)
.await
{
handler.on_error(&format!("Failed to listen on {}: {}", port, err));
}
@ -1213,9 +1173,28 @@ async fn start_one_port_forward(
async fn io_loop(handler: Handler) {
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
handler.write().unwrap().sender = Some(sender.clone());
let mut options = crate::ipc::get_options_async().await;
let mut key = options.remove("key").unwrap_or("".to_owned());
let token = LocalConfig::get_option("access_token");
if key.is_empty() {
key = crate::platform::get_license_key();
}
if handler.is_port_forward() {
if handler.is_rdp() {
start_one_port_forward(handler, 0, "".to_owned(), 3389, receiver).await;
let port = handler
.get_option("rdp_port".to_owned())
.parse::<i32>()
.unwrap_or(3389);
std::env::set_var(
"rdp_username",
handler.get_option("rdp_username".to_owned()),
);
std::env::set_var(
"rdp_password",
handler.get_option("rdp_password".to_owned()),
);
log::info!("Remote rdp port: {}", port);
start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await;
} else if handler.args.len() == 0 {
let pfs = handler.lc.read().unwrap().port_forwards.clone();
let mut queues = HashMap::<i32, mpsc::UnboundedSender<Data>>::new();
@ -1231,6 +1210,8 @@ async fn io_loop(handler: Handler) {
let (sender, receiver) = mpsc::unbounded_channel::<Data>();
queues.insert(port, sender);
let handler = handler.clone();
let key = key.clone();
let token = token.clone();
tokio::spawn(async move {
start_one_port_forward(
handler,
@ -1238,6 +1219,8 @@ async fn io_loop(handler: Handler) {
remote_host,
remote_port,
receiver,
&key,
&token,
)
.await;
});
@ -1268,7 +1251,16 @@ async fn io_loop(handler: Handler) {
}
let remote_host = handler.args[1].clone();
let remote_port = handler.args[2].parse::<i32>().unwrap_or(0);
start_one_port_forward(handler, port, remote_host, remote_port, receiver).await;
start_one_port_forward(
handler,
port,
remote_host,
remote_port,
receiver,
&key,
&token,
)
.await;
}
return;
}
@ -1296,7 +1288,7 @@ async fn io_loop(handler: Handler) {
#[cfg(windows)]
clipboard_file_context: None,
};
remote.io_loop().await;
remote.io_loop(&key, &token).await;
}
struct RemoveJob {
@ -1339,7 +1331,7 @@ struct Remote {
}
impl Remote {
async fn io_loop(&mut self) {
async fn io_loop(&mut self, key: &str, token: &str) {
let stop_clipboard = self.start_clipboard();
let mut last_recv_time = Instant::now();
let conn_type = if self.handler.is_file_transfer() {
@ -1347,7 +1339,7 @@ impl Remote {
} else {
ConnType::default()
};
match Client::start(&self.handler.id, conn_type).await {
match Client::start(&self.handler.id, key, token, conn_type).await {
Ok((mut peer, direct)) => {
unsafe {
SERVER_KEYBOARD_ENABLED = true;
@ -1934,7 +1926,7 @@ impl Remote {
}
}
fn make_fd(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> Value {
pub fn make_fd(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> Value {
let mut m = Value::map();
m.set_item("id", id);
let mut a = Value::array(0);
@ -1963,6 +1955,12 @@ fn make_fd(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> Value {
#[async_trait]
impl Interface for Handler {
fn send(&self, data: Data) {
if let Some(ref sender) = self.read().unwrap().sender {
sender.send(data).ok();
}
}
fn msgbox(&self, msgtype: &str, title: &str, text: &str) {
let retry = check_if_retry(msgtype, title, text);
self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry));
@ -2019,6 +2017,10 @@ impl Interface for Handler {
);
log::info!("[video] initialized: {:?}", ok);
});
let p = self.lc.read().unwrap().should_auto_login();
if !p.is_empty() {
input_os_password(p, true, self.clone());
}
}
self.lc.write().unwrap().handle_peer_info(username, pi);
self.call("updatePi", &make_args!(pi_sciter));
@ -2031,7 +2033,7 @@ impl Interface for Handler {
{
let mut path = std::env::temp_dir();
path.push(&self.id);
let path = path.with_extension(config::APP_NAME.to_lowercase());
let path = path.with_extension(crate::get_app_name().to_lowercase());
std::fs::File::create(&path).ok();
if let Some(path) = path.to_str() {
crate::platform::windows::add_recent_document(&path);
@ -2058,3 +2060,9 @@ impl Handler {
self.msgbox("error", "Error", err);
}
}
#[tokio::main(flavor = "current_thread")]
async fn send_note(url: String, id: String, conn_id: i32, note: String) {
let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note });
allow_err!(crate::post_request(url, body.to_string(), "").await);
}

View File

@ -22,7 +22,7 @@ handler.setDisplay = function(x, y, w, h) {
}
// in case toolbar not shown correclty
view.windowMinSize = (500, 300);
view.windowMinSize = (scaleIt(500), scaleIt(300));
function adaptDisplay() {
var w = display_width;
@ -43,7 +43,7 @@ function adaptDisplay() {
var (x, y) = view.box(#position, #border, #screen);
// extra for border
var extra = is_win ? 4 : 2;
view.move(x, y, w + extra, h + hh + extra);
view.move(x, y, (w + extra).toInteger(), (h + hh + extra).toInteger());
}
}
}
@ -67,8 +67,8 @@ function adaptDisplay() {
}
}
handler.style.set {
width: w + "px",
height: h + "px",
width: w / scaleFactor + "px",
height: h / scaleFactor + "px",
};
}
@ -389,8 +389,8 @@ handler.setCursorPosition = function(x, y) {
cur_y = y - display_origin_y;
var x = cur_x - cur_hotx;
var y = cur_y - cur_hoty;
x *= display_scale;
y *= display_scale;
x *= display_scale / scaleFactor;
y *= display_scale / scaleFactor;
cursor_img.style.set {
left: x + "px",
top: y + "px",
@ -401,13 +401,8 @@ handler.setCursorPosition = function(x, y) {
}
function self.ready() {
// https://sciter.com/forums/topic/focus_lost-and-focus_got-events/
// not got a good way to detect focus/blur in Sciter
// below not work until I click on toolbar on Mac
self.on("focus", "*", function() { stdout.println(this,"got focus") });
self.on("blur", "*", function() { stdout.println(this,"lost focus") });
var w = 960;
var h = 640;
var w = scaleIt(960);
var h = scaleIt(640);
if (is_file_transfer || is_port_forward) {
var r = handler.get_size();
if (isReasonableSize(r) && r[2] > 0) {

View File

@ -8,6 +8,7 @@
#include <memory>
#include <shlobj.h> // NOLINT(build/include_order)
#include <userenv.h>
#include <versionhelpers.h>
void flog(char const *fmt, ...)
{
@ -66,9 +67,12 @@ BOOL GetSessionUserTokenWin(OUT LPHANDLE lphUserToken, DWORD dwSessionId, BOOL a
return bResult;
}
// START the app as system
extern "C"
{
bool is_windows_server()
{
return IsWindowsServer();
}
HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user)
{
HANDLE hProcess = NULL;
@ -89,7 +93,7 @@ extern "C"
CreateEnvironmentBlock(&lpEnvironment, // Environment block
hToken, // New token
TRUE); // Inheritance
TRUE); // Inheritence
}
if (lpEnvironment)
{
@ -259,13 +263,16 @@ extern "C"
auto n = 4 * 3;
auto p = out - (width + 2) * 4 - 4;
// Outline above...
if (p >= out0 && p + n <= out0_end) memset(p, 0xff, n);
if (p >= out0 && p + n <= out0_end)
memset(p, 0xff, n);
// ...besides...
p = out - 4;
if (p + n <= out0_end) memset(p, 0xff, n);
if (p + n <= out0_end)
memset(p, 0xff, n);
// ...and above
p = out + (width + 2) * 4 - 4;
if (p + n <= out0_end) memset(p, 0xff, n);
if (p + n <= out0_end)
memset(p, 0xff, n);
}
in += 4;
out += 4;
@ -373,20 +380,210 @@ extern "C"
SHAddToRecentDocs(SHARD_PATHW, path);
}
uint32_t get_active_user(PWSTR bufin, uint32_t nin)
{
uint32_t nout = 0;
auto id = WTSGetActiveConsoleSessionId();
PWSTR buf = NULL;
DWORD n = 0;
if (WTSQuerySessionInformationW(NULL, id, WTSUserName, &buf, &n))
{
if (buf) {
nout = min(nin, n);
memcpy(bufin, buf, nout);
WTSFreeMemory(buf);
}
}
return nout;
}
DWORD get_current_session(BOOL include_rdp)
{
auto rdp_or_console = WTSGetActiveConsoleSessionId();
if (!include_rdp)
return rdp_or_console;
PWTS_SESSION_INFOA pInfos;
DWORD count;
auto rdp = "rdp";
auto nrdp = strlen(rdp);
if (WTSEnumerateSessionsA(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &pInfos, &count))
{
for (DWORD i = 0; i < count; i++)
{
auto info = pInfos[i];
if (info.State == WTSActive)
{
if (info.pWinStationName == NULL)
continue;
if (!stricmp(info.pWinStationName, "console"))
{
return info.SessionId;
}
if (!strnicmp(info.pWinStationName, rdp, nrdp))
{
rdp_or_console = info.SessionId;
}
}
}
WTSFreeMemory(pInfos);
}
return rdp_or_console;
}
uint32_t get_active_user(PWSTR bufin, uint32_t nin, BOOL rdp)
{
uint32_t nout = 0;
auto id = get_current_session(rdp);
PWSTR buf = NULL;
DWORD n = 0;
if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, id, WTSUserName, &buf, &n))
{
if (buf)
{
nout = min(nin, n);
memcpy(bufin, buf, nout);
WTSFreeMemory(buf);
}
}
return nout;
}
BOOL has_rdp_service()
{
PWTS_SESSION_INFOA pInfos;
DWORD count;
auto rdp = "rdp";
auto nrdp = strlen(rdp);
auto rdp_or_console = WTSGetActiveConsoleSessionId();
if (WTSEnumerateSessionsA(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &pInfos, &count))
{
for (DWORD i = 0; i < count; i++)
{
auto info = pInfos[i];
if (!strnicmp(info.pWinStationName, rdp, nrdp))
{
return TRUE;
}
}
WTSFreeMemory(pInfos);
}
return FALSE;
}
} // end of extern "C"
// below copied from https://github.com/TigerVNC/tigervnc/blob/master/vncviewer/win32.c
extern "C"
{
static HANDLE thread;
static DWORD thread_id;
static HHOOK hook = 0;
static HWND target_wnd = 0;
static HWND default_hook_wnd = 0;
static bool win_down = false;
static bool stop_system_key_propagate = false;
bool is_win_down()
{
return win_down;
}
#define ARRAY_SIZE(a) (sizeof(a) / sizeof(*a))
static int is_system_hotkey(int vkCode, WPARAM wParam)
{
switch (vkCode)
{
case VK_LWIN:
case VK_RWIN:
win_down = wParam == WM_KEYDOWN;
case VK_SNAPSHOT:
return 1;
case VK_TAB:
if (GetAsyncKeyState(VK_MENU) & 0x8000)
return 1;
case VK_ESCAPE:
if (GetAsyncKeyState(VK_MENU) & 0x8000)
return 1;
if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
return 1;
}
return 0;
}
static LRESULT CALLBACK keyboard_hook(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
KBDLLHOOKSTRUCT *msgInfo = (KBDLLHOOKSTRUCT *)lParam;
// Grabbing everything seems to mess up some keyboard state that
// FLTK relies on, so just grab the keys that we normally cannot.
if (stop_system_key_propagate && is_system_hotkey(msgInfo->vkCode, wParam))
{
PostMessage(target_wnd, wParam, msgInfo->vkCode,
(msgInfo->scanCode & 0xff) << 16 |
(msgInfo->flags & 0xff) << 24);
return 1;
}
}
return CallNextHookEx(hook, nCode, wParam, lParam);
}
static DWORD WINAPI keyboard_thread(LPVOID data)
{
MSG msg;
target_wnd = (HWND)data;
// Make sure a message queue is created
PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE | PM_NOYIELD);
hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_hook, GetModuleHandle(0), 0);
// If something goes wrong then there is not much we can do.
// Just sit around and wait for WM_QUIT...
while (GetMessage(&msg, NULL, 0, 0))
;
if (hook)
UnhookWindowsHookEx(hook);
target_wnd = 0;
return 0;
}
int win32_enable_lowlevel_keyboard(HWND hwnd)
{
if (!default_hook_wnd)
{
default_hook_wnd = hwnd;
}
if (!hwnd)
{
hwnd = default_hook_wnd;
}
// Only one target at a time for now
if (thread != NULL)
{
if (hwnd == target_wnd)
return 0;
return 1;
}
// We create a separate thread as it is crucial that hooks are processed
// in a timely manner.
thread = CreateThread(NULL, 0, keyboard_thread, hwnd, 0, &thread_id);
if (thread == NULL)
return 1;
return 0;
}
void win32_disable_lowlevel_keyboard(HWND hwnd)
{
if (!hwnd)
{
hwnd = default_hook_wnd;
}
if (hwnd != target_wnd)
return;
PostThreadMessage(thread_id, WM_QUIT, 0, 0);
CloseHandle(thread);
thread = NULL;
}
void win_stop_system_key_propagate(bool v)
{
stop_system_key_propagate = v;
}
} // end of extern "C"