aboutsummaryrefslogtreecommitdiffhomepage
path: root/libs/hbb_common
diff options
context:
space:
mode:
Diffstat (limited to 'libs/hbb_common')
-rw-r--r--libs/hbb_common/Cargo.toml7
-rw-r--r--libs/hbb_common/protos/message.proto144
-rw-r--r--libs/hbb_common/src/config.rs498
-rw-r--r--libs/hbb_common/src/fs.rs339
-rw-r--r--libs/hbb_common/src/lib.rs152
-rw-r--r--libs/hbb_common/src/password_security.rs242
-rw-r--r--libs/hbb_common/src/platform/linux.rs157
-rw-r--r--libs/hbb_common/src/platform/mod.rs2
-rw-r--r--libs/hbb_common/src/socket_client.rs194
-rw-r--r--libs/hbb_common/src/tcp.rs144
-rw-r--r--libs/hbb_common/src/udp.rs13
11 files changed, 1660 insertions, 232 deletions
diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml
index 4b28fc1..59f0896 100644
--- a/libs/hbb_common/Cargo.toml
+++ b/libs/hbb_common/Cargo.toml
@@ -11,7 +11,7 @@ protobuf = { version = "3.1", features = ["with-bytes"] }
tokio = { version = "1.20", features = ["full"] }
tokio-util = { version = "0.7", features = ["full"] }
futures = "0.3"
-bytes = "1.2"
+bytes = { version = "1.2", features = ["serde"] }
log = "0.4"
env_logger = "0.9"
socket2 = { version = "0.3", features = ["reuseport"] }
@@ -30,15 +30,18 @@ filetime = "0.2"
sodiumoxide = "0.2"
regex = "1.4"
tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
+chrono = "0.4"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
+machine-uid = "0.2"
[features]
quic = []
+flatpak = []
[build-dependencies]
-protobuf-codegen = "3.1"
+protobuf-codegen = { version = "3.1" }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto
index 15ee971..b127ac3 100644
--- a/libs/hbb_common/protos/message.proto
+++ b/libs/hbb_common/protos/message.proto
@@ -1,13 +1,13 @@
syntax = "proto3";
package hbb;
-message VP9 {
+message EncodedVideoFrame {
bytes data = 1;
bool key = 2;
int64 pts = 3;
}
-message VP9s { repeated VP9 frames = 1; }
+message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; }
message RGB { bool compress = 1; }
@@ -19,9 +19,11 @@ message YUV {
message VideoFrame {
oneof union {
- VP9s vp9s = 6;
+ EncodedVideoFrames vp9s = 6;
RGB rgb = 7;
YUV yuv = 8;
+ EncodedVideoFrames h264s = 10;
+ EncodedVideoFrames h265s = 11;
}
int64 timestamp = 9;
}
@@ -38,6 +40,7 @@ message DisplayInfo {
int32 height = 4;
string name = 5;
bool online = 6;
+ bool cursor_embedded = 7;
}
message PortForward {
@@ -61,10 +64,21 @@ message LoginRequest {
PortForward port_forward = 8;
}
bool video_ack_required = 9;
+ uint64 session_id = 10;
+ string version = 11;
}
message ChatMessage { string text = 1; }
+message Features {
+ bool privacy_mode = 1;
+}
+
+message SupportedEncoding {
+ bool h264 = 1;
+ bool h265 = 2;
+}
+
message PeerInfo {
string username = 1;
string hostname = 2;
@@ -74,6 +88,8 @@ message PeerInfo {
bool sas_enabled = 6;
string version = 7;
int32 conn_id = 8;
+ Features features = 9;
+ SupportedEncoding encoding = 10;
}
message LoginResponse {
@@ -90,6 +106,13 @@ message MouseEvent {
repeated ControlKey modifiers = 4;
}
+enum KeyboardMode{
+ Legacy = 0;
+ Map = 1;
+ Translate = 2;
+ Auto = 3;
+}
+
enum ControlKey {
Unknown = 0;
Alt = 1;
@@ -183,6 +206,7 @@ message KeyEvent {
string seq = 6;
}
repeated ControlKey modifiers = 8;
+ KeyboardMode mode = 9;
}
message CursorData {
@@ -252,6 +276,7 @@ message FileAction {
FileRemoveFile remove_file = 6;
ReadAllFiles all_files = 7;
FileTransferCancel cancel = 8;
+ FileTransferSendConfirmRequest send_confirm = 9;
}
}
@@ -263,14 +288,24 @@ message FileResponse {
FileTransferBlock block = 2;
FileTransferError error = 3;
FileTransferDone done = 4;
+ FileTransferDigest digest = 5;
}
}
+message FileTransferDigest {
+ int32 id = 1;
+ sint32 file_num = 2;
+ uint64 last_modified = 3;
+ uint64 file_size = 4;
+ bool is_upload = 5;
+}
+
message FileTransferBlock {
int32 id = 1;
sint32 file_num = 2;
bytes data = 3;
bool compressed = 4;
+ uint32 blk_id = 5;
}
message FileTransferError {
@@ -283,6 +318,16 @@ message FileTransferSendRequest {
int32 id = 1;
string path = 2;
bool include_hidden = 3;
+ int32 file_num = 4;
+}
+
+message FileTransferSendConfirmRequest {
+ int32 id = 1;
+ sint32 file_num = 2;
+ oneof union {
+ bool skip = 3;
+ uint32 offset_blk = 4;
+ }
}
message FileTransferDone {
@@ -294,6 +339,7 @@ message FileTransferReceiveRequest {
int32 id = 1;
string path = 2; // path written to
repeated FileEntry files = 3;
+ int32 file_num = 4;
}
message FileRemoveDir {
@@ -315,38 +361,31 @@ message FileDirCreate {
// main logic from freeRDP
message CliprdrMonitorReady {
- int32 conn_id = 1;
}
message CliprdrFormat {
- int32 conn_id = 1;
int32 id = 2;
string format = 3;
}
message CliprdrServerFormatList {
- int32 conn_id = 1;
repeated CliprdrFormat formats = 2;
}
message CliprdrServerFormatListResponse {
- int32 conn_id = 1;
int32 msg_flags = 2;
}
message CliprdrServerFormatDataRequest {
- int32 conn_id = 1;
int32 requested_format_id = 2;
}
message CliprdrServerFormatDataResponse {
- int32 conn_id = 1;
int32 msg_flags = 2;
bytes format_data = 3;
}
message CliprdrFileContentsRequest {
- int32 conn_id = 1;
int32 stream_id = 2;
int32 list_index = 3;
int32 dw_flags = 4;
@@ -358,7 +397,6 @@ message CliprdrFileContentsRequest {
}
message CliprdrFileContentsResponse {
- int32 conn_id = 1;
int32 msg_flags = 3;
int32 stream_id = 4;
bytes requested_data = 5;
@@ -382,6 +420,7 @@ message SwitchDisplay {
sint32 y = 3;
int32 width = 4;
int32 height = 5;
+ bool cursor_embedded = 6;
}
message PermissionInfo {
@@ -390,6 +429,8 @@ message PermissionInfo {
Clipboard = 2;
Audio = 3;
File = 4;
+ Restart = 5;
+ Recording = 6;
}
Permission permission = 1;
@@ -403,6 +444,20 @@ enum ImageQuality {
Best = 4;
}
+message VideoCodecState {
+ enum PerferCodec {
+ Auto = 0;
+ VPX = 1;
+ H264 = 2;
+ H265 = 3;
+ }
+
+ int32 score_vpx = 1;
+ int32 score_h264 = 2;
+ int32 score_h265 = 3;
+ PerferCodec perfer = 4;
+}
+
message OptionMessage {
enum BoolOption {
NotSet = 0;
@@ -418,16 +473,15 @@ message OptionMessage {
BoolOption disable_audio = 7;
BoolOption disable_clipboard = 8;
BoolOption enable_file_transfer = 9;
-}
-
-message OptionResponse {
- OptionMessage opt = 1;
- string error = 2;
+ VideoCodecState video_codec_state = 10;
+ int32 custom_fps = 11;
}
message TestDelay {
int64 time = 1;
bool from_client = 2;
+ uint32 last_delay = 3;
+ uint32 target_bitrate = 4;
}
message PublicKey {
@@ -447,6 +501,57 @@ message AudioFrame {
int64 timestamp = 2;
}
+// Notify peer to show message box.
+message MessageBox {
+ // Message type. Refer to flutter/lib/commom.dart/msgBox().
+ string msgtype = 1;
+ string title = 2;
+ // English
+ string text = 3;
+ // If not empty, msgbox provides a button to following the link.
+ // The link here can't be directly http url.
+ // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app).
+ string link = 4;
+}
+
+message BackNotification {
+ // no need to consider block input by someone else
+ enum BlockInputState {
+ BlkStateUnknown = 0;
+ BlkOnSucceeded = 2;
+ BlkOnFailed = 3;
+ BlkOffSucceeded = 4;
+ BlkOffFailed = 5;
+ }
+ enum PrivacyModeState {
+ PrvStateUnknown = 0;
+ // Privacy mode on by someone else
+ PrvOnByOther = 2;
+ // Privacy mode is not supported on the remote side
+ PrvNotSupported = 3;
+ // Privacy mode on by self
+ PrvOnSucceeded = 4;
+ // Privacy mode on by self, but denied
+ PrvOnFailedDenied = 5;
+ // Some plugins are not found
+ PrvOnFailedPlugin = 6;
+ // Privacy mode on by self, but failed
+ PrvOnFailed = 7;
+ // Privacy mode off by self
+ PrvOffSucceeded = 8;
+ // Ctrl + P
+ PrvOffByPeer = 9;
+ // Privacy mode off by self, but failed
+ PrvOffFailed = 10;
+ PrvOffUnknown = 11;
+ }
+
+ oneof union {
+ PrivacyModeState privacy_mode_state = 1;
+ BlockInputState block_input_state = 2;
+ }
+}
+
message Misc {
oneof union {
ChatMessage chat_message = 4;
@@ -456,8 +561,12 @@ message Misc {
AudioFormat audio_format = 8;
string close_reason = 9;
bool refresh_video = 10;
- OptionResponse option_response = 11;
bool video_received = 12;
+ BackNotification back_notification = 13;
+ bool restart_remote_device = 14;
+ bool uac = 15;
+ bool foreground_window_elevated = 16;
+ bool stop_service = 17;
}
}
@@ -481,5 +590,6 @@ message Message {
FileResponse file_response = 18;
Misc misc = 19;
Cliprdr cliprdr = 20;
+ MessageBox message_box = 21;
}
}
diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs
index ea7dd6d..1d427a2 100644
--- a/libs/hbb_common/src/config.rs
+++ b/libs/hbb_common/src/config.rs
@@ -1,22 +1,35 @@
-use crate::log;
-use directories_next::ProjectDirs;
-use rand::Rng;
-use serde_derive::{Deserialize, Serialize};
-use sodiumoxide::crypto::sign;
use std::{
collections::HashMap,
fs,
- net::{IpAddr, Ipv4Addr, SocketAddr},
+ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::{Path, PathBuf},
sync::{Arc, Mutex, RwLock},
time::SystemTime,
};
+use anyhow::Result;
+use rand::Rng;
+use regex::Regex;
+use serde as de;
+use serde_derive::{Deserialize, Serialize};
+use sodiumoxide::base64;
+use sodiumoxide::crypto::sign;
+
+use crate::{
+ log,
+ password_security::{
+ decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original,
+ encrypt_vec_or_original,
+ },
+};
+
pub const RENDEZVOUS_TIMEOUT: u64 = 12_000;
pub const CONNECT_TIMEOUT: u64 = 18_000;
+pub const READ_TIMEOUT: u64 = 30_000;
pub const REG_INTERVAL: i64 = 12_000;
pub const COMPRESS_LEVEL: i32 = 3;
-const SERIAL: i32 = 1;
+const SERIAL: i32 = 3;
+const PASSWORD_ENC_VERSION: &'static str = "00";
// 128x128
#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding
pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII=
@@ -38,12 +51,27 @@ lazy_static::lazy_static! {
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()));
+ static ref KEY_PAIR: Arc<Mutex<Option<(Vec<u8>, Vec<u8>)>>> = Default::default();
+ static ref HW_CODEC_CONFIG: Arc<RwLock<HwCodecConfig>> = Arc::new(RwLock::new(HwCodecConfig::load()));
}
-#[cfg(any(target_os = "android", target_os = "ios"))]
+
lazy_static::lazy_static! {
pub static ref APP_DIR: Arc<RwLock<String>> = Default::default();
+}
+
+#[cfg(any(target_os = "android", target_os = "ios"))]
+lazy_static::lazy_static! {
pub static ref APP_HOME_DIR: Arc<RwLock<String>> = Default::default();
}
+
+// #[cfg(any(target_os = "android", target_os = "ios"))]
+lazy_static::lazy_static! {
+ pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([
+ ("rustdesk docs home", "https://rustdesk.com/docs/en/"),
+ ("rustdesk docs x11-required", "https://rustdesk.com/docs/en/manual/linux/#x11-required"),
+ ]);
+}
+
const CHARS: &'static [char] = &[
'2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
@@ -54,9 +82,30 @@ pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[
"rs-sg.rustdesk.com",
"rs-cn.rustdesk.com",
];
+pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=";
pub const RENDEZVOUS_PORT: i32 = 21116;
pub const RELAY_PORT: i32 = 21117;
+macro_rules! serde_field_string {
+ ($default_func:ident, $de_func:ident, $default_expr:expr) => {
+ fn $default_func() -> String {
+ $default_expr
+ }
+
+ fn $de_func<'de, D>(deserializer: D) -> Result<String, D::Error>
+ where
+ D: de::Deserializer<'de>,
+ {
+ let s: &str = de::Deserialize::deserialize(deserializer)?;
+ Ok(if s.is_empty() {
+ Self::$default_func()
+ } else {
+ s.to_owned()
+ })
+ }
+ };
+}
+
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum NetworkType {
Direct,
@@ -66,13 +115,15 @@ pub enum NetworkType {
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct Config {
#[serde(default)]
- pub id: String,
+ pub id: String, // use
+ #[serde(default)]
+ enc_id: String, // store
#[serde(default)]
password: String,
#[serde(default)]
salt: String,
#[serde(default)]
- pub key_pair: (Vec<u8>, Vec<u8>), // sk, pk
+ key_pair: (Vec<u8>, Vec<u8>), // sk, pk
#[serde(default)]
key_confirmed: bool,
#[serde(default)]
@@ -107,7 +158,7 @@ pub struct Config2 {
pub options: HashMap<String, String>,
}
-#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
pub struct PeerConfig {
#[serde(default)]
pub password: Vec<u8>,
@@ -117,9 +168,20 @@ pub struct PeerConfig {
pub size_ft: Size,
#[serde(default)]
pub size_pf: Size,
- #[serde(default)]
- pub view_style: String, // original (default), scale
- #[serde(default)]
+ #[serde(
+ default = "PeerConfig::default_view_style",
+ deserialize_with = "PeerConfig::deserialize_view_style"
+ )]
+ pub view_style: String,
+ #[serde(
+ default = "PeerConfig::default_scroll_style",
+ deserialize_with = "PeerConfig::deserialize_scroll_style"
+ )]
+ pub scroll_style: String,
+ #[serde(
+ default = "PeerConfig::default_image_quality",
+ deserialize_with = "PeerConfig::deserialize_image_quality"
+ )]
pub image_quality: String,
#[serde(default)]
pub custom_image_quality: Vec<i32>,
@@ -139,12 +201,21 @@ pub struct PeerConfig {
pub disable_clipboard: bool,
#[serde(default)]
pub enable_file_transfer: bool,
-
- // the other scalar value must before this
#[serde(default)]
+ pub show_quality_monitor: bool,
+ #[serde(default)]
+ pub keyboard_mode: String,
+
+ // The other scalar value must before this
+ #[serde(default, deserialize_with = "PeerConfig::deserialize_options")]
pub options: HashMap<String, String>,
+ // Various data for flutter ui
+ #[serde(default)]
+ pub ui_flutter: HashMap<String, String>,
#[serde(default)]
pub info: PeerInfoSerde,
+ #[serde(default)]
+ pub transfer: TransferSerde,
}
#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)]
@@ -157,6 +228,14 @@ pub struct PeerInfoSerde {
pub platform: String,
}
+#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
+pub struct TransferSerde {
+ #[serde(default)]
+ pub write_jobs: Vec<String>,
+ #[serde(default)]
+ pub read_jobs: Vec<String>,
+}
+
fn patch(path: PathBuf) -> PathBuf {
if let Some(_tmp) = path.to_str() {
#[cfg(windows)]
@@ -188,7 +267,17 @@ fn patch(path: PathBuf) -> PathBuf {
impl Config2 {
fn load() -> Config2 {
- Config::load_::<Config2>("2")
+ let mut config = Config::load_::<Config2>("2");
+ if let Some(mut socks) = config.socks {
+ let (password, _, store) =
+ decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION);
+ socks.password = password;
+ config.socks = Some(socks);
+ if store {
+ config.store();
+ }
+ }
+ config
}
pub fn file() -> PathBuf {
@@ -196,7 +285,12 @@ impl Config2 {
}
fn store(&self) {
- Config::store_(self, "2");
+ let mut config = self.clone();
+ if let Some(mut socks) = config.socks {
+ socks.password = encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION);
+ config.socks = Some(socks);
+ }
+ Config::store_(&config, "2");
}
pub fn get() -> Config2 {
@@ -227,6 +321,11 @@ pub fn load_path<T: serde::Serialize + serde::de::DeserializeOwned + Default + s
cfg
}
+#[inline]
+pub fn store_path<T: serde::Serialize>(path: PathBuf, cfg: T) -> crate::ResultType<()> {
+ Ok(confy::store_path(path, cfg)?)
+}
+
impl Config {
fn load_<T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug>(
suffix: &str,
@@ -235,24 +334,68 @@ impl Config {
log::debug!("Configuration path: {}", file.display());
let cfg = load_path(file);
if suffix.is_empty() {
- log::debug!("{:?}", cfg);
+ log::trace!("{:?}", cfg);
}
cfg
}
fn store_<T: serde::Serialize>(config: &T, suffix: &str) {
let file = Self::file_(suffix);
- if let Err(err) = confy::store_path(file, config) {
+ if let Err(err) = store_path(file, config) {
log::error!("Failed to store config: {}", err);
}
}
fn load() -> Config {
- Config::load_::<Config>("")
+ let mut config = Config::load_::<Config>("");
+ let mut store = false;
+ let (password, _, store1) = decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION);
+ config.password = password;
+ store |= store1;
+ let mut id_valid = false;
+ let (id, encrypted, store2) = decrypt_str_or_original(&config.enc_id, PASSWORD_ENC_VERSION);
+ if encrypted {
+ config.id = id;
+ id_valid = true;
+ store |= store2;
+ } else {
+ if crate::get_modified_time(&Self::file_(""))
+ .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation
+ .unwrap_or(crate::get_exe_time())
+ < crate::get_exe_time()
+ {
+ if !config.id.is_empty()
+ && config.enc_id.is_empty()
+ && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1
+ {
+ id_valid = true;
+ store = true;
+ }
+ }
+ }
+ if !id_valid {
+ for _ in 0..3 {
+ if let Some(id) = Config::get_auto_id() {
+ config.id = id;
+ store = true;
+ break;
+ } else {
+ log::error!("Failed to generate new id");
+ }
+ }
+ }
+ if store {
+ config.store();
+ }
+ config
}
fn store(&self) {
- Config::store_(self, "");
+ let mut config = self.clone();
+ config.password = encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION);
+ config.enc_id = encrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION);
+ config.id = "".to_owned();
+ Config::store_(&config, "");
}
pub fn file() -> PathBuf {
@@ -264,15 +407,22 @@ impl Config {
Config::with_extension(Self::path(name))
}
+ pub fn is_empty(&self) -> bool {
+ (self.id.is_empty() && self.enc_id.is_empty()) || self.key_pair.0.is_empty()
+ }
+
pub fn get_home() -> PathBuf {
#[cfg(any(target_os = "android", target_os = "ios"))]
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() {
- path
- } else {
- std::env::temp_dir()
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ {
+ if let Some(path) = dirs_next::home_dir() {
+ patch(path)
+ } else if let Ok(path) = std::env::current_dir() {
+ path
+ } else {
+ std::env::temp_dir()
+ }
}
}
@@ -283,17 +433,22 @@ impl Config {
path.push(p);
return path;
}
- #[cfg(not(target_os = "macos"))]
- let org = "";
- #[cfg(target_os = "macos")]
- let org = ORG.read().unwrap().clone();
- // /var/root for root
- 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;
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ {
+ #[cfg(not(target_os = "macos"))]
+ let org = "";
+ #[cfg(target_os = "macos")]
+ let org = ORG.read().unwrap().clone();
+ // /var/root for root
+ if let Some(project) =
+ directories_next::ProjectDirs::from("", &org, &*APP_NAME.read().unwrap())
+ {
+ let mut path = patch(project.config_dir().to_path_buf());
+ path.push(p);
+ return path;
+ }
+ return "".into();
}
- return "".into();
}
#[allow(unreachable_code)]
@@ -356,8 +511,12 @@ impl Config {
}
#[inline]
- pub fn get_any_listen_addr() -> SocketAddr {
- SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0)
+ pub fn get_any_listen_addr(is_ipv4: bool) -> SocketAddr {
+ if is_ipv4 {
+ SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
+ } else {
+ SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)
+ }
}
pub fn get_rendezvous_server() -> String {
@@ -472,22 +631,25 @@ impl Config {
.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() {
- for x in &ma.bytes()[2..] {
- id = (id << 8) | (*x as u32);
+ {
+ let mut id = 0u32;
+ if let Ok(Some(ma)) = mac_address::get_mac_address() {
+ for x in &ma.bytes()[2..] {
+ id = (id << 8) | (*x as u32);
+ }
+ id = id & 0x1FFFFFFF;
+ Some(id.to_string())
+ } else {
+ None
}
- id = id & 0x1FFFFFFF;
- Some(id.to_string())
- } else {
- None
}
}
- pub fn get_auto_password() -> String {
+ pub fn get_auto_password(length: usize) -> String {
let mut rng = rand::thread_rng();
- (0..6)
+ (0..length)
.map(|_| CHARS[rng.gen::<usize>() % CHARS.len()])
.collect()
}
@@ -525,24 +687,26 @@ impl Config {
config.store();
}
- pub fn set_key_pair(pair: (Vec<u8>, Vec<u8>)) {
- let mut config = CONFIG.write().unwrap();
- if config.key_pair == pair {
- return;
- }
- config.key_pair = pair;
- config.store();
- }
-
pub fn get_key_pair() -> (Vec<u8>, Vec<u8>) {
// lock here to make sure no gen_keypair more than once
- let mut config = CONFIG.write().unwrap();
+ // no use of CONFIG directly here to ensure no recursive calling in Config::load because of password dec which calling this function
+ let mut lock = KEY_PAIR.lock().unwrap();
+ if let Some(p) = lock.as_ref() {
+ return p.clone();
+ }
+ let mut config = Config::load_::<Config>("");
if config.key_pair.0.is_empty() {
let (pk, sk) = sign::gen_keypair();
- config.key_pair = (sk.0.to_vec(), pk.0.into());
- config.store();
+ let key_pair = (sk.0.to_vec(), pk.0.into());
+ config.key_pair = key_pair.clone();
+ std::thread::spawn(|| {
+ let mut config = CONFIG.write().unwrap();
+ config.key_pair = key_pair;
+ config.store();
+ });
}
- config.key_pair.clone()
+ *lock = Some(config.key_pair.clone());
+ return config.key_pair;
}
pub fn get_id() -> String {
@@ -608,7 +772,7 @@ impl Config {
log::info!("id updated from {} to {}", id, new_id);
}
- pub fn set_password(password: &str) {
+ pub fn set_permanent_password(password: &str) {
let mut config = CONFIG.write().unwrap();
if password == config.password {
return;
@@ -617,13 +781,8 @@ impl Config {
config.store();
}
- pub fn get_password() -> String {
- let mut password = CONFIG.read().unwrap().password.clone();
- if password.is_empty() {
- password = Config::get_auto_password();
- Config::set_password(&password);
- }
- password
+ pub fn get_permanent_password() -> String {
+ CONFIG.read().unwrap().password.clone()
}
pub fn set_salt(salt: &str) {
@@ -638,7 +797,7 @@ impl Config {
pub fn get_salt() -> String {
let mut salt = CONFIG.read().unwrap().salt.clone();
if salt.is_empty() {
- salt = Config::get_auto_password();
+ salt = Config::get_auto_password(6);
Config::set_salt(&salt);
}
salt
@@ -693,9 +852,30 @@ const PEERS: &str = "peers";
impl PeerConfig {
pub fn load(id: &str) -> PeerConfig {
- let _ = CONFIG.read().unwrap(); // for lock
+ let _lock = CONFIG.read().unwrap();
match confy::load_path(&Self::path(id)) {
- Ok(config) => config,
+ Ok(config) => {
+ let mut config: PeerConfig = config;
+ let mut store = false;
+ let (password, _, store2) =
+ decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
+ config.password = password;
+ store = store || store2;
+ config.options.get_mut("rdp_password").map(|v| {
+ let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
+ *v = password;
+ store = store || store2;
+ });
+ config.options.get_mut("os-password").map(|v| {
+ let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
+ *v = password;
+ store = store || store2;
+ });
+ if store {
+ config.store(id);
+ }
+ config
+ }
Err(err) => {
log::error!("Failed to load config: {}", err);
Default::default()
@@ -704,8 +884,18 @@ impl PeerConfig {
}
pub fn store(&self, id: &str) {
- let _ = CONFIG.read().unwrap(); // for lock
- if let Err(err) = confy::store_path(Self::path(id), self) {
+ let _lock = CONFIG.read().unwrap();
+ let mut config = self.clone();
+ config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
+ config
+ .options
+ .get_mut("rdp_password")
+ .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION));
+ config
+ .options
+ .get_mut("os-password")
+ .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION));
+ if let Err(err) = store_path(Self::path(id), config) {
log::error!("Failed to store config: {}", err);
}
}
@@ -715,7 +905,17 @@ impl PeerConfig {
}
fn path(id: &str) -> PathBuf {
- let path: PathBuf = [PEERS, id].iter().collect();
+ let id_encoded: String;
+
+ //If the id contains invalid chars, encode it
+ let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*").unwrap();
+ if forbidden_paths.is_match(id) {
+ id_encoded =
+ "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str();
+ } else {
+ id_encoded = id.to_string();
+ }
+ let path: PathBuf = [PEERS, id_encoded.as_str()].iter().collect();
Config::with_extension(Config::path(path))
}
@@ -738,11 +938,22 @@ impl PeerConfig {
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("")
.to_owned();
- let c = PeerConfig::load(&id);
+
+ let id_decoded_string: String;
+ if id.starts_with("base64_") && id.len() != 7 {
+ let id_decoded = base64::decode(&id[7..], base64::Variant::Original)
+ .unwrap_or(Vec::new());
+ id_decoded_string =
+ String::from_utf8_lossy(&id_decoded).as_ref().to_owned();
+ } else {
+ id_decoded_string = id;
+ }
+
+ let c = PeerConfig::load(&id_decoded_string);
if c.info.platform.is_empty() {
fs::remove_file(&p).ok();
}
- (id, t, c)
+ (id_decoded_string, t, c)
})
.filter(|p| !p.2.info.platform.is_empty())
.collect();
@@ -752,6 +963,33 @@ impl PeerConfig {
}
Default::default()
}
+
+ serde_field_string!(
+ default_view_style,
+ deserialize_view_style,
+ "original".to_owned()
+ );
+ serde_field_string!(
+ default_scroll_style,
+ deserialize_scroll_style,
+ "scrollauto".to_owned()
+ );
+ serde_field_string!(
+ default_image_quality,
+ deserialize_image_quality,
+ "balanced".to_owned()
+ );
+
+ fn deserialize_options<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
+ where
+ D: de::Deserializer<'de>,
+ {
+ let mut mp: HashMap<String, String> = de::Deserialize::deserialize(deserializer)?;
+ if !mp.contains_key("codec-preference") {
+ mp.insert("codec-preference".to_owned(), "auto".to_owned());
+ }
+ Ok(mp)
+ }
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
@@ -759,11 +997,16 @@ pub struct LocalConfig {
#[serde(default)]
remote_id: String, // latest used one
#[serde(default)]
+ kb_layout_type: String,
+ #[serde(default)]
size: Size,
#[serde(default)]
pub fav: Vec<String>,
#[serde(default)]
options: HashMap<String, String>,
+ // Various data for flutter ui
+ #[serde(default)]
+ ui_flutter: HashMap<String, String>,
}
impl LocalConfig {
@@ -775,6 +1018,16 @@ impl LocalConfig {
Config::store_(self, "_local");
}
+ pub fn get_kb_layout_type() -> String {
+ LOCAL_CONFIG.read().unwrap().kb_layout_type.clone()
+ }
+
+ pub fn set_kb_layout_type(kb_layout_type: String) {
+ let mut config = LOCAL_CONFIG.write().unwrap();
+ config.kb_layout_type = kb_layout_type;
+ config.store();
+ }
+
pub fn get_size() -> Size {
LOCAL_CONFIG.read().unwrap().size
}
@@ -835,17 +1088,59 @@ impl LocalConfig {
config.store();
}
}
+
+ pub fn get_flutter_config(k: &str) -> String {
+ if let Some(v) = LOCAL_CONFIG.read().unwrap().ui_flutter.get(k) {
+ v.clone()
+ } else {
+ "".to_owned()
+ }
+ }
+
+ pub fn set_flutter_config(k: String, v: String) {
+ let mut config = LOCAL_CONFIG.write().unwrap();
+ let v2 = if v.is_empty() { None } else { Some(&v) };
+ if v2 != config.ui_flutter.get(&k) {
+ if v2.is_none() {
+ config.ui_flutter.remove(&k);
+ } else {
+ config.ui_flutter.insert(k, v);
+ }
+ config.store();
+ }
+ }
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
-pub struct LanPeers {
+pub struct DiscoveryPeer {
+ #[serde(default)]
+ pub id: String,
+ #[serde(default)]
+ pub username: String,
+ #[serde(default)]
+ pub hostname: String,
+ #[serde(default)]
+ pub platform: String,
#[serde(default)]
- pub peers: String,
+ pub online: bool,
+ #[serde(default)]
+ pub ip_mac: HashMap<String, String>,
+}
+
+impl DiscoveryPeer {
+ pub fn is_same_peer(&self, other: &DiscoveryPeer) -> bool {
+ self.id == other.id && self.username == other.username
+ }
+}
+
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct LanPeers {
+ pub peers: Vec<DiscoveryPeer>,
}
impl LanPeers {
pub fn load() -> LanPeers {
- let _ = CONFIG.read().unwrap(); // for lock
+ let _lock = CONFIG.read().unwrap();
match confy::load_path(&Config::file_("_lan_peers")) {
Ok(peers) => peers,
Err(err) => {
@@ -855,9 +1150,11 @@ impl LanPeers {
}
}
- pub fn store(peers: String) {
- let f = LanPeers { peers };
- if let Err(err) = confy::store_path(Config::file_("_lan_peers"), f) {
+ pub fn store(peers: &Vec<DiscoveryPeer>) {
+ let f = LanPeers {
+ peers: peers.clone(),
+ };
+ if let Err(err) = store_path(Config::file_("_lan_peers"), f) {
log::error!("Failed to store lan peers: {}", err);
}
}
@@ -871,9 +1168,40 @@ impl LanPeers {
}
}
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct HwCodecConfig {
+ #[serde(default)]
+ pub options: HashMap<String, String>,
+}
+
+impl HwCodecConfig {
+ pub fn load() -> HwCodecConfig {
+ Config::load_::<HwCodecConfig>("_hwcodec")
+ }
+
+ pub fn store(&self) {
+ Config::store_(self, "_hwcodec");
+ }
+
+ pub fn remove() {
+ std::fs::remove_file(Config::file_("_hwcodec")).ok();
+ }
+
+ /// refresh current global HW_CODEC_CONFIG, usually uesd after HwCodecConfig::remove()
+ pub fn refresh() {
+ *HW_CODEC_CONFIG.write().unwrap() = HwCodecConfig::load();
+ log::debug!("HW_CODEC_CONFIG refreshed successfully");
+ }
+
+ pub fn get() -> HwCodecConfig {
+ return HW_CODEC_CONFIG.read().unwrap().clone();
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
+
#[test]
fn test_serialize() {
let cfg: Config = Default::default();
diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs
index a5bc6ad..570fad8 100644
--- a/libs/hbb_common/src/fs.rs
+++ b/libs/hbb_common/src/fs.rs
@@ -1,13 +1,17 @@
-use crate::{bail, message_proto::*, ResultType};
+#[cfg(windows)]
+use std::os::windows::prelude::*;
use std::path::{Path, PathBuf};
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+use serde_derive::{Deserialize, Serialize};
+use tokio::{fs::File, io::*};
+
+use crate::{bail, get_version_number, message_proto::*, ResultType, Stream};
// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html
use crate::{
compress::{compress, decompress},
config::{Config, COMPRESS_LEVEL},
};
-#[cfg(windows)]
-use std::os::windows::prelude::*;
-use tokio::{fs::File, io::*};
pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType<FileDirectory> {
let mut dir = FileDirectory {
@@ -184,16 +188,63 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<F
read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
}
+#[inline]
+pub fn is_file_exists(file_path: &str) -> bool {
+ return Path::new(file_path).exists();
+}
+
+#[inline]
+pub fn can_enable_overwrite_detection(version: i64) -> bool {
+ version >= get_version_number("1.1.10")
+}
+
#[derive(Default)]
pub struct TransferJob {
- id: i32,
- path: PathBuf,
- files: Vec<FileEntry>,
- file_num: i32,
+ pub id: i32,
+ pub remote: String,
+ pub path: PathBuf,
+ pub show_hidden: bool,
+ pub is_remote: bool,
+ pub is_last_job: bool,
+ pub file_num: i32,
+ pub files: Vec<FileEntry>,
+
file: Option<File>,
total_size: u64,
finished_size: u64,
transferred: u64,
+ enable_overwrite_detection: bool,
+ file_confirmed: bool,
+ // indicating the last file is skipped
+ file_skipped: bool,
+ file_is_waiting: bool,
+ default_overwrite_strategy: Option<bool>,
+}
+
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct TransferJobMeta {
+ #[serde(default)]
+ pub id: i32,
+ #[serde(default)]
+ pub remote: String,
+ #[serde(default)]
+ pub to: String,
+ #[serde(default)]
+ pub show_hidden: bool,
+ #[serde(default)]
+ pub file_num: i32,
+ #[serde(default)]
+ pub is_remote: bool,
+}
+
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct RemoveJobMeta {
+ #[serde(default)]
+ pub path: String,
+ #[serde(default)]
+ pub is_remote: bool,
+ #[serde(default)]
+ pub no_confirm: bool,
}
#[inline]
@@ -219,25 +270,54 @@ fn is_compressed_file(name: &str) -> bool {
}
impl TransferJob {
- pub fn new_write(id: i32, path: String, files: Vec<FileEntry>) -> Self {
+ pub fn new_write(
+ id: i32,
+ remote: String,
+ path: String,
+ file_num: i32,
+ show_hidden: bool,
+ is_remote: bool,
+ files: Vec<FileEntry>,
+ enable_overwrite_detection: bool,
+ ) -> Self {
+ log::info!("new write {}", path);
let total_size = files.iter().map(|x| x.size as u64).sum();
Self {
id,
+ remote,
path: get_path(&path),
+ file_num,
+ show_hidden,
+ is_remote,
files,
total_size,
+ enable_overwrite_detection,
..Default::default()
}
}
- pub fn new_read(id: i32, path: String, include_hidden: bool) -> ResultType<Self> {
- let files = get_recursive_files(&path, include_hidden)?;
+ pub fn new_read(
+ id: i32,
+ remote: String,
+ path: String,
+ file_num: i32,
+ show_hidden: bool,
+ is_remote: bool,
+ enable_overwrite_detection: bool,
+ ) -> ResultType<Self> {
+ log::info!("new read {}", path);
+ let files = get_recursive_files(&path, show_hidden)?;
let total_size = files.iter().map(|x| x.size as u64).sum();
Ok(Self {
id,
+ remote,
path: get_path(&path),
+ file_num,
+ show_hidden,
+ is_remote,
files,
total_size,
+ enable_overwrite_detection,
..Default::default()
})
}
@@ -302,7 +382,7 @@ impl TransferJob {
}
}
- pub async fn write(&mut self, block: FileTransferBlock, raw: Option<&[u8]>) -> ResultType<()> {
+ pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> {
if block.id != self.id {
bail!("Wrong id");
}
@@ -324,25 +404,20 @@ impl TransferJob {
let path = format!("{}.download", get_string(&path));
self.file = Some(File::create(&path).await?);
}
- let data = if let Some(data) = raw {
- data
- } else {
- &block.data
- };
if block.compressed {
- let tmp = decompress(data);
+ let tmp = decompress(&block.data);
self.file.as_mut().unwrap().write_all(&tmp).await?;
self.finished_size += tmp.len() as u64;
} else {
- self.file.as_mut().unwrap().write_all(data).await?;
- self.finished_size += data.len() as u64;
+ self.file.as_mut().unwrap().write_all(&block.data).await?;
+ self.finished_size += block.data.len() as u64;
}
- self.transferred += data.len() as u64;
+ self.transferred += block.data.len() as u64;
Ok(())
}
#[inline]
- fn join(&self, name: &str) -> PathBuf {
+ pub fn join(&self, name: &str) -> PathBuf {
if name.is_empty() {
self.path.clone()
} else {
@@ -350,7 +425,7 @@ impl TransferJob {
}
}
- pub async fn read(&mut self) -> ResultType<Option<FileTransferBlock>> {
+ pub async fn read(&mut self, stream: &mut Stream) -> ResultType<Option<FileTransferBlock>> {
let file_num = self.file_num as usize;
if file_num >= self.files.len() {
self.file.take();
@@ -361,13 +436,26 @@ impl TransferJob {
match File::open(self.join(&name)).await {
Ok(file) => {
self.file = Some(file);
+ self.file_confirmed = false;
+ self.file_is_waiting = false;
}
Err(err) => {
self.file_num += 1;
+ self.file_confirmed = false;
+ self.file_is_waiting = false;
return Err(err.into());
}
}
}
+ if self.enable_overwrite_detection {
+ if !self.file_confirmed() {
+ if !self.file_is_waiting() {
+ self.send_current_digest(stream).await?;
+ self.set_file_is_waiting(true);
+ }
+ return Ok(None);
+ }
+ }
const BUF_SIZE: usize = 128 * 1024;
let mut buf: Vec<u8> = Vec::with_capacity(BUF_SIZE);
unsafe {
@@ -380,6 +468,8 @@ impl TransferJob {
Err(err) => {
self.file_num += 1;
self.file = None;
+ self.file_confirmed = false;
+ self.file_is_waiting = false;
return Err(err.into());
}
Ok(n) => {
@@ -394,6 +484,8 @@ impl TransferJob {
if offset == 0 {
self.file_num += 1;
self.file = None;
+ self.file_confirmed = false;
+ self.file_is_waiting = false;
} else {
self.finished_size += offset as u64;
if !is_compressed_file(name) {
@@ -413,6 +505,139 @@ impl TransferJob {
..Default::default()
}))
}
+
+ async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> {
+ let mut msg = Message::new();
+ let mut resp = FileResponse::new();
+ let meta = self.file.as_ref().unwrap().metadata().await?;
+ let last_modified = meta
+ .modified()?
+ .duration_since(SystemTime::UNIX_EPOCH)?
+ .as_secs();
+ resp.set_digest(FileTransferDigest {
+ id: self.id,
+ file_num: self.file_num,
+ last_modified,
+ file_size: meta.len(),
+ ..Default::default()
+ });
+ msg.set_file_response(resp);
+ stream.send(&msg).await?;
+ log::info!(
+ "id: {}, file_num:{}, digest message is sent. waiting for confirm. msg: {:?}",
+ self.id,
+ self.file_num,
+ msg
+ );
+ Ok(())
+ }
+
+ pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option<bool>) {
+ self.default_overwrite_strategy = overwrite_strategy;
+ }
+
+ pub fn default_overwrite_strategy(&self) -> Option<bool> {
+ self.default_overwrite_strategy
+ }
+
+ pub fn set_file_confirmed(&mut self, file_confirmed: bool) {
+ log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed);
+ self.file_confirmed = file_confirmed;
+ self.file_skipped = false;
+ }
+
+ pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) {
+ self.file_is_waiting = file_is_waiting;
+ }
+
+ #[inline]
+ pub fn file_is_waiting(&self) -> bool {
+ self.file_is_waiting
+ }
+
+ #[inline]
+ pub fn file_confirmed(&self) -> bool {
+ self.file_confirmed
+ }
+
+ /// Indicating whether the last file is skipped
+ #[inline]
+ pub fn file_skipped(&self) -> bool {
+ self.file_skipped
+ }
+
+ /// Indicating whether the whole task is skipped
+ #[inline]
+ pub fn job_skipped(&self) -> bool {
+ self.file_skipped() && self.files.len() == 1
+ }
+
+ /// Check whether the job is completed after `read` returns `None`
+ /// This is a helper function which gives additional lifecycle when the job reads `None`.
+ /// If returns `true`, it means we can delete the job automatically. `False` otherwise.
+ ///
+ /// [`Note`]
+ /// Conditions:
+ /// 1. Files are not waiting for confirmation by peers.
+ #[inline]
+ pub fn job_completed(&self) -> bool {
+ // has no error, Condition 2
+ if !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) {
+ return true;
+ }
+ return false;
+ }
+
+ /// Get job error message, useful for getting status when job had finished
+ pub fn job_error(&self) -> Option<String> {
+ if self.job_skipped() {
+ return Some("skipped".to_string());
+ }
+ None
+ }
+
+ pub fn set_file_skipped(&mut self) -> bool {
+ log::debug!("skip file {} in job {}", self.file_num, self.id);
+ self.file.take();
+ self.set_file_confirmed(false);
+ self.set_file_is_waiting(false);
+ self.file_num += 1;
+ self.file_skipped = true;
+ true
+ }
+
+ pub fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool {
+ if self.file_num() != r.file_num {
+ log::info!("file num truncated, ignoring");
+ } else {
+ match r.union {
+ Some(file_transfer_send_confirm_request::Union::Skip(s)) => {
+ if s {
+ self.set_file_skipped();
+ } else {
+ self.set_file_confirmed(true);
+ }
+ }
+ Some(file_transfer_send_confirm_request::Union::OffsetBlk(_offset)) => {
+ self.set_file_confirmed(true);
+ }
+ _ => {}
+ }
+ }
+ true
+ }
+
+ #[inline]
+ pub fn gen_meta(&self) -> TransferJobMeta {
+ TransferJobMeta {
+ id: self.id,
+ remote: self.remote.to_string(),
+ to: self.path.to_string_lossy().to_string(),
+ file_num: self.file_num,
+ show_hidden: self.show_hidden,
+ is_remote: self.is_remote,
+ }
+ }
}
#[inline]
@@ -453,12 +678,22 @@ pub fn new_block(block: FileTransferBlock) -> Message {
}
#[inline]
-pub fn new_receive(id: i32, path: String, files: Vec<FileEntry>) -> Message {
+pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
+ let mut msg_out = Message::new();
+ let mut action = FileAction::new();
+ action.set_send_confirm(r);
+ msg_out.set_file_action(action);
+ msg_out
+}
+
+#[inline]
+pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec<FileEntry>) -> Message {
let mut action = FileAction::new();
action.set_receive(FileTransferReceiveRequest {
id,
path,
files: files.into(),
+ file_num,
..Default::default()
});
let mut msg_out = Message::new();
@@ -467,12 +702,14 @@ pub fn new_receive(id: i32, path: String, files: Vec<FileEntry>) -> Message {
}
#[inline]
-pub fn new_send(id: i32, path: String, include_hidden: bool) -> Message {
+pub fn new_send(id: i32, path: String, file_num: i32, include_hidden: bool) -> Message {
+ log::info!("new send: {},id : {}", path, id);
let mut action = FileAction::new();
action.set_send(FileTransferSendRequest {
id,
path,
include_hidden,
+ file_num,
..Default::default()
});
let mut msg_out = Message::new();
@@ -509,7 +746,10 @@ pub async fn handle_read_jobs(
) -> ResultType<()> {
let mut finished = Vec::new();
for job in jobs.iter_mut() {
- match job.read().await {
+ if job.is_last_job {
+ continue;
+ }
+ match job.read(stream).await {
Err(err) => {
stream
.send(&new_error(job.id(), err, job.file_num()))
@@ -519,8 +759,19 @@ pub async fn handle_read_jobs(
stream.send(&new_block(block)).await?;
}
Ok(None) => {
- finished.push(job.id());
- stream.send(&new_done(job.id(), job.file_num())).await?;
+ if job.job_completed() {
+ finished.push(job.id());
+ let err = job.job_error();
+ if err.is_some() {
+ stream
+ .send(&new_error(job.id(), err.unwrap(), job.file_num()))
+ .await?;
+ } else {
+ stream.send(&new_done(job.id(), job.file_num())).await?;
+ }
+ } else {
+ // waiting confirmation.
+ }
}
}
}
@@ -565,3 +816,35 @@ pub fn transform_windows_path(entries: &mut Vec<FileEntry>) {
entry.name = entry.name.replace("\\", "/");
}
}
+
+pub enum DigestCheckResult {
+ IsSame,
+ NeedConfirm(FileTransferDigest),
+ NoSuchFile,
+}
+
+#[inline]
+pub fn is_write_need_confirmation(
+ file_path: &str,
+ digest: &FileTransferDigest,
+) -> ResultType<DigestCheckResult> {
+ let path = Path::new(file_path);
+ if path.exists() && path.is_file() {
+ let metadata = std::fs::metadata(path)?;
+ let modified_time = metadata.modified()?;
+ let remote_mt = Duration::from_secs(digest.last_modified);
+ let local_mt = modified_time.duration_since(UNIX_EPOCH)?;
+ if remote_mt == local_mt && digest.file_size == metadata.len() {
+ return Ok(DigestCheckResult::IsSame);
+ }
+ Ok(DigestCheckResult::NeedConfirm(FileTransferDigest {
+ id: digest.id,
+ file_num: digest.file_num,
+ last_modified: local_mt.as_secs(),
+ file_size: metadata.len(),
+ ..Default::default()
+ }))
+ } else {
+ Ok(DigestCheckResult::NoSuchFile)
+ }
+} \ No newline at end of file
diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs
index 5f23e46..85e0100 100644
--- a/libs/hbb_common/src/lib.rs
+++ b/libs/hbb_common/src/lib.rs
@@ -1,15 +1,16 @@
pub mod compress;
-#[path = "./protos/message.rs"]
-pub mod message_proto;
-#[path = "./protos/rendezvous.rs"]
-pub mod rendezvous_proto;
+pub mod platform;
+pub mod protos;
pub use bytes;
+use config::Config;
pub use futures;
pub use protobuf;
+pub use protos::message as message_proto;
+pub use protos::rendezvous as rendezvous_proto;
use std::{
fs::File,
io::{self, BufRead},
- net::{Ipv4Addr, SocketAddr, SocketAddrV4},
+ net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
path::Path,
time::{self, SystemTime, UNIX_EPOCH},
};
@@ -27,6 +28,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;
@@ -35,6 +37,9 @@ pub use sodiumoxide;
pub use tokio_socks;
pub use tokio_socks::IntoTargetAddr;
pub use tokio_socks::TargetAddr;
+pub mod password_security;
+pub use chrono;
+pub use directories_next;
#[cfg(feature = "quic")]
pub type Stream = quic::Connection;
@@ -61,6 +66,21 @@ macro_rules! allow_err {
} else {
}
};
+
+ ($e:expr, $($arg:tt)*) => {
+ if let Err(err) = $e {
+ log::debug!(
+ "{:?}, {}, {}:{}:{}:{}",
+ err,
+ format_args!($($arg)*),
+ module_path!(),
+ file!(),
+ line!(),
+ column!()
+ );
+ } else {
+ }
+ };
}
#[inline]
@@ -97,13 +117,31 @@ impl AddrMangle {
}
bytes[..(16 - n_padding)].to_vec()
}
- _ => {
- panic!("Only support ipv4");
+ SocketAddr::V6(addr_v6) => {
+ let mut x = addr_v6.ip().octets().to_vec();
+ let port: [u8; 2] = addr_v6.port().to_le_bytes();
+ x.push(port[0]);
+ x.push(port[1]);
+ x
}
}
}
pub fn decode(bytes: &[u8]) -> SocketAddr {
+ if bytes.len() > 16 {
+ if bytes.len() != 18 {
+ return Config::get_any_listen_addr(false);
+ }
+ #[allow(invalid_value)]
+ let mut tmp: [u8; 2] = unsafe { std::mem::MaybeUninit::uninit().assume_init() };
+ tmp.copy_from_slice(&bytes[16..]);
+ let port = u16::from_le_bytes(tmp);
+ #[allow(invalid_value)]
+ let mut tmp: [u8; 16] = unsafe { std::mem::MaybeUninit::uninit().assume_init() };
+ tmp.copy_from_slice(&bytes[..16]);
+ let ip = std::net::Ipv6Addr::from(tmp);
+ return SocketAddr::new(IpAddr::V6(ip), port);
+ }
let mut padded = [0u8; 16];
padded[..bytes.len()].copy_from_slice(&bytes);
let number = u128::from_le_bytes(padded);
@@ -156,19 +194,23 @@ pub fn get_version_from_url(url: &str) -> String {
}
pub fn gen_version() {
+ use std::io::prelude::*;
let mut file = File::create("./src/version.rs").unwrap();
for line in read_lines("Cargo.toml").unwrap() {
if let Ok(line) = line {
let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect();
if ab.len() == 2 && ab[0] == "version" {
- use std::io::prelude::*;
- file.write_all(format!("pub const VERSION: &str = {};", ab[1]).as_bytes())
+ file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes())
.ok();
- file.sync_all().ok();
break;
}
}
}
+ // generate build date
+ let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
+ file.write_all(format!("pub const BUILD_DATE: &str = \"{}\";", build_date).as_bytes())
+ .ok();
+ file.sync_all().ok();
}
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
@@ -199,6 +241,40 @@ pub fn get_modified_time(path: &std::path::Path) -> SystemTime {
.unwrap_or(UNIX_EPOCH)
}
+pub fn get_created_time(path: &std::path::Path) -> SystemTime {
+ std::fs::metadata(&path)
+ .map(|m| m.created().unwrap_or(UNIX_EPOCH))
+ .unwrap_or(UNIX_EPOCH)
+}
+
+pub fn get_exe_time() -> SystemTime {
+ std::env::current_exe().map_or(UNIX_EPOCH, |path| {
+ let m = get_modified_time(&path);
+ let c = get_created_time(&path);
+ if m > c {
+ m
+ } else {
+ c
+ }
+ })
+}
+
+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
+}
+
+#[inline]
+pub fn get_time() -> i64 {
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .map(|d| d.as_millis())
+ .unwrap_or(0) as _
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -206,5 +282,61 @@ mod tests {
fn test_mangle() {
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116));
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
+
+ let addr = "[2001:db8::1]:8080".parse::<SocketAddr>().unwrap();
+ assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
+
+ let addr = "[2001:db8:ff::1111]:80".parse::<SocketAddr>().unwrap();
+ assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
+ }
+
+ #[test]
+ fn test_allow_err() {
+ allow_err!(Err("test err") as Result<(), &str>);
+ allow_err!(
+ Err("test err with msg") as Result<(), &str>,
+ "prompt {}",
+ "failed"
+ );
+ }
+}
+
+#[inline]
+pub fn is_ipv4_str(id: &str) -> bool {
+ regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$")
+ .unwrap()
+ .is_match(id)
+}
+
+#[inline]
+pub fn is_ipv6_str(id: &str) -> bool {
+ regex::Regex::new(r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$")
+ .unwrap()
+ .is_match(id)
+}
+
+#[inline]
+pub fn is_ip_str(id: &str) -> bool {
+ is_ipv4_str(id) || is_ipv6_str(id)
+}
+
+#[cfg(test)]
+mod test_lib {
+ use super::*;
+
+ #[test]
+ fn test_ipv6() {
+ assert_eq!(is_ipv6_str("1:2:3"), true);
+ assert_eq!(is_ipv6_str("[ab:2:3]:12"), true);
+ assert_eq!(is_ipv6_str("[ABEF:2a:3]:12"), true);
+ assert_eq!(is_ipv6_str("[ABEG:2a:3]:12"), false);
+ assert_eq!(is_ipv6_str("1[ab:2:3]:12"), false);
+ assert_eq!(is_ipv6_str("1.1.1.1"), false);
+ assert_eq!(is_ip_str("1.1.1.1"), true);
+ assert_eq!(is_ipv6_str("1:2:"), false);
+ assert_eq!(is_ipv6_str("1:2::0"), true);
+ assert_eq!(is_ipv6_str("[1:2::0]:1"), true);
+ assert_eq!(is_ipv6_str("[1:2::0]:"), false);
+ assert_eq!(is_ipv6_str("1:2::0]:1"), false);
}
}
diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs
new file mode 100644
index 0000000..6029069
--- /dev/null
+++ b/libs/hbb_common/src/password_security.rs
@@ -0,0 +1,242 @@
+use crate::config::Config;
+use sodiumoxide::base64;
+use std::sync::{Arc, RwLock};
+
+lazy_static::lazy_static! {
+ pub static ref TEMPORARY_PASSWORD:Arc<RwLock<String>> = Arc::new(RwLock::new(Config::get_auto_password(temporary_password_length())));
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum VerificationMethod {
+ OnlyUseTemporaryPassword,
+ OnlyUsePermanentPassword,
+ UseBothPasswords,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ApproveMode {
+ Both,
+ Password,
+ Click,
+}
+
+// Should only be called in server
+pub fn update_temporary_password() {
+ *TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length());
+}
+
+// Should only be called in server
+pub fn temporary_password() -> String {
+ TEMPORARY_PASSWORD.read().unwrap().clone()
+}
+
+fn verification_method() -> VerificationMethod {
+ let method = Config::get_option("verification-method");
+ if method == "use-temporary-password" {
+ VerificationMethod::OnlyUseTemporaryPassword
+ } else if method == "use-permanent-password" {
+ VerificationMethod::OnlyUsePermanentPassword
+ } else {
+ VerificationMethod::UseBothPasswords // default
+ }
+}
+
+pub fn temporary_password_length() -> usize {
+ let length = Config::get_option("temporary-password-length");
+ if length == "8" {
+ 8
+ } else if length == "10" {
+ 10
+ } else {
+ 6 // default
+ }
+}
+
+pub fn temporary_enabled() -> bool {
+ verification_method() != VerificationMethod::OnlyUsePermanentPassword
+}
+
+pub fn permanent_enabled() -> bool {
+ verification_method() != VerificationMethod::OnlyUseTemporaryPassword
+}
+
+pub fn has_valid_password() -> bool {
+ temporary_enabled() && !temporary_password().is_empty()
+ || permanent_enabled() && !Config::get_permanent_password().is_empty()
+}
+
+pub fn approve_mode() -> ApproveMode {
+ let mode = Config::get_option("approve-mode");
+ if mode == "password" {
+ ApproveMode::Password
+ } else if mode == "click" {
+ ApproveMode::Click
+ } else {
+ ApproveMode::Both
+ }
+}
+
+pub fn hide_cm() -> bool {
+ approve_mode() == ApproveMode::Password
+ && verification_method() == VerificationMethod::OnlyUsePermanentPassword
+ && !Config::get_option("allow-hide-cm").is_empty()
+}
+
+const VERSION_LEN: usize = 2;
+
+pub fn encrypt_str_or_original(s: &str, version: &str) -> String {
+ if decrypt_str_or_original(s, version).1 {
+ log::error!("Duplicate encryption!");
+ return s.to_owned();
+ }
+ if version == "00" {
+ if let Ok(s) = encrypt(s.as_bytes()) {
+ return version.to_owned() + &s;
+ }
+ }
+ s.to_owned()
+}
+
+// String: password
+// bool: whether decryption is successful
+// bool: whether should store to re-encrypt when load
+pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
+ if s.len() > VERSION_LEN {
+ let version = &s[..VERSION_LEN];
+ if version == "00" {
+ if let Ok(v) = decrypt(&s[VERSION_LEN..].as_bytes()) {
+ return (
+ String::from_utf8_lossy(&v).to_string(),
+ true,
+ version != current_version,
+ );
+ }
+ }
+ }
+
+ (s.to_owned(), false, !s.is_empty())
+}
+
+pub fn encrypt_vec_or_original(v: &[u8], version: &str) -> Vec<u8> {
+ if decrypt_vec_or_original(v, version).1 {
+ log::error!("Duplicate encryption!");
+ return v.to_owned();
+ }
+ if version == "00" {
+ if let Ok(s) = encrypt(v) {
+ let mut version = version.to_owned().into_bytes();
+ version.append(&mut s.into_bytes());
+ return version;
+ }
+ }
+ v.to_owned()
+}
+
+// Vec<u8>: password
+// bool: whether decryption is successful
+// bool: whether should store to re-encrypt when load
+pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec<u8>, bool, bool) {
+ if v.len() > VERSION_LEN {
+ let version = String::from_utf8_lossy(&v[..VERSION_LEN]);
+ if version == "00" {
+ if let Ok(v) = decrypt(&v[VERSION_LEN..]) {
+ return (v, true, version != current_version);
+ }
+ }
+ }
+
+ (v.to_owned(), false, !v.is_empty())
+}
+
+fn encrypt(v: &[u8]) -> Result<String, ()> {
+ if v.len() > 0 {
+ symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
+ } else {
+ Err(())
+ }
+}
+
+fn decrypt(v: &[u8]) -> Result<Vec<u8>, ()> {
+ if v.len() > 0 {
+ base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false))
+ } else {
+ Err(())
+ }
+}
+
+fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result<Vec<u8>, ()> {
+ use sodiumoxide::crypto::secretbox;
+ use std::convert::TryInto;
+
+ let mut keybuf = crate::get_uuid();
+ keybuf.resize(secretbox::KEYBYTES, 0);
+ let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?);
+ let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
+
+ if encrypt {
+ Ok(secretbox::seal(data, &nonce, &key))
+ } else {
+ secretbox::open(data, &nonce, &key)
+ }
+}
+
+mod test {
+
+ #[test]
+ fn test() {
+ use super::*;
+
+ let version = "00";
+
+ println!("test str");
+ let data = "Hello World";
+ let encrypted = encrypt_str_or_original(data, version);
+ let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
+ println!("data: {}", data);
+ println!("encrypted: {}", encrypted);
+ println!("decrypted: {}", decrypted);
+ assert_eq!(data, decrypted);
+ assert_eq!(version, &encrypted[..2]);
+ assert_eq!(succ, true);
+ assert_eq!(store, false);
+ let (_, _, store) = decrypt_str_or_original(&encrypted, "99");
+ assert_eq!(store, true);
+ assert_eq!(decrypt_str_or_original(&decrypted, version).1, false);
+ assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted);
+
+ println!("test vec");
+ let data: Vec<u8> = vec![1, 2, 3, 4, 5, 6];
+ let encrypted = encrypt_vec_or_original(&data, version);
+ let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
+ println!("data: {:?}", data);
+ println!("encrypted: {:?}", encrypted);
+ println!("decrypted: {:?}", decrypted);
+ assert_eq!(data, decrypted);
+ assert_eq!(version.as_bytes(), &encrypted[..2]);
+ assert_eq!(store, false);
+ assert_eq!(succ, true);
+ let (_, _, store) = decrypt_vec_or_original(&encrypted, "99");
+ assert_eq!(store, true);
+ assert_eq!(decrypt_vec_or_original(&decrypted, version).1, false);
+ assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted);
+
+ println!("test original");
+ let data = version.to_string() + "Hello World";
+ let (decrypted, succ, store) = decrypt_str_or_original(&data, version);
+ assert_eq!(data, decrypted);
+ assert_eq!(store, true);
+ assert_eq!(succ, false);
+ let verbytes = version.as_bytes();
+ let data: Vec<u8> = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6];
+ let (decrypted, succ, store) = decrypt_vec_or_original(&data, version);
+ assert_eq!(data, decrypted);
+ assert_eq!(store, true);
+ assert_eq!(succ, false);
+ let (_, succ, store) = decrypt_str_or_original("", version);
+ assert_eq!(store, false);
+ assert_eq!(succ, false);
+ let (_, succ, store) = decrypt_vec_or_original(&vec![], version);
+ assert_eq!(store, false);
+ assert_eq!(succ, false);
+ }
+}
diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs
new file mode 100644
index 0000000..4c6375d
--- /dev/null
+++ b/libs/hbb_common/src/platform/linux.rs
@@ -0,0 +1,157 @@
+use crate::ResultType;
+
+lazy_static::lazy_static! {
+ pub static ref DISTRO: Disto = Disto::new();
+}
+
+pub struct Disto {
+ pub name: String,
+ pub version_id: String,
+}
+
+impl Disto {
+ fn new() -> Self {
+ let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned())
+ .unwrap_or_default()
+ .trim()
+ .trim_matches('"')
+ .to_string();
+ let version_id =
+ run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release".to_owned())
+ .unwrap_or_default()
+ .trim()
+ .trim_matches('"')
+ .to_string();
+ Self { name, version_id }
+ }
+}
+
+pub fn get_display_server() -> String {
+ let mut session = get_values_of_seat0([0].to_vec())[0].clone();
+ if session.is_empty() {
+ // loginctl has not given the expected output. try something else.
+ if let Ok(sid) = std::env::var("XDG_SESSION_ID") {
+ // could also execute "cat /proc/self/sessionid"
+ session = sid.to_owned();
+ }
+ if session.is_empty() {
+ session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default();
+ }
+ }
+
+ get_display_server_of_session(&session)
+}
+
+fn get_display_server_of_session(session: &str) -> String {
+ let mut display_server = if let Ok(output) =
+ run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
+ // Check session type of the session
+ {
+ let display_server = String::from_utf8_lossy(&output.stdout)
+ .replace("Type=", "")
+ .trim_end()
+ .into();
+ if display_server == "tty" {
+ // If the type is tty...
+ if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "TTY", session]))
+ // Get the tty number
+ {
+ let tty: String = String::from_utf8_lossy(&output.stdout)
+ .replace("TTY=", "")
+ .trim_end()
+ .into();
+ if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty))
+ // And check if Xorg is running on that tty
+ {
+ if xorg_results.trim_end().to_string() != "" {
+ // If it is, manually return "x11", otherwise return tty
+ return "x11".to_owned();
+ }
+ }
+ }
+ }
+ display_server
+ } else {
+ "".to_owned()
+ };
+ if display_server.is_empty() {
+ // loginctl has not given the expected output. try something else.
+ if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
+ display_server = sestype;
+ }
+ }
+ // If the session is not a tty, then just return the type as usual
+ display_server
+}
+
+pub fn get_values_of_seat0(indices: Vec<usize>) -> Vec<String> {
+ if let Ok(output) = run_loginctl(None) {
+ for line in String::from_utf8_lossy(&output.stdout).lines() {
+ if line.contains("seat0") {
+ if let Some(sid) = line.split_whitespace().nth(0) {
+ if is_active(sid) {
+ return indices
+ .into_iter()
+ .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned())
+ .collect::<Vec<String>>();
+ }
+ }
+ }
+ }
+ }
+
+ // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
+ if let Ok(output) = run_loginctl(None) {
+ for line in String::from_utf8_lossy(&output.stdout).lines() {
+ if let Some(sid) = line.split_whitespace().nth(0) {
+ let d = get_display_server_of_session(sid);
+ if is_active(sid) && d != "tty" {
+ return indices
+ .into_iter()
+ .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned())
+ .collect::<Vec<String>>();
+ }
+ }
+ }
+ }
+
+ return indices
+ .iter()
+ .map(|_x| "".to_owned())
+ .collect::<Vec<String>>();
+}
+
+fn is_active(sid: &str) -> bool {
+ if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) {
+ String::from_utf8_lossy(&output.stdout).contains("active")
+ } else {
+ false
+ }
+}
+
+pub fn run_cmds(cmds: String) -> ResultType<String> {
+ let output = std::process::Command::new("sh")
+ .args(vec!["-c", &cmds])
+ .output()?;
+ Ok(String::from_utf8_lossy(&output.stdout).to_string())
+}
+
+#[cfg(not(feature = "flatpak"))]
+fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
+ let mut cmd = std::process::Command::new("loginctl");
+ if let Some(a) = args {
+ return cmd.args(a).output();
+ }
+ cmd.output()
+}
+
+#[cfg(feature = "flatpak")]
+fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
+ let mut l_args = String::from("loginctl");
+ if let Some(a) = args {
+ l_args = format!("{} {}", l_args, a.join(" "));
+ }
+ std::process::Command::new("flatpak-spawn")
+ .args(vec![String::from("--host"), l_args])
+ .output()
+}
diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs
new file mode 100644
index 0000000..8daba25
--- /dev/null
+++ b/libs/hbb_common/src/platform/mod.rs
@@ -0,0 +1,2 @@
+#[cfg(target_os = "linux")]
+pub mod linux;
diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs
index 72ab73f..b7cb137 100644
--- a/libs/hbb_common/src/socket_client.rs
+++ b/libs/hbb_common/src/socket_client.rs
@@ -9,31 +9,15 @@ use std::net::SocketAddr;
use tokio::net::ToSocketAddrs;
use tokio_socks::{IntoTargetAddr, TargetAddr};
-fn to_socket_addr(host: &str) -> ResultType<SocketAddr> {
- use std::net::ToSocketAddrs;
- host.to_socket_addrs()?
- .filter(|x| x.is_ipv4())
- .next()
- .context("Failed to solve")
-}
-
-pub fn get_target_addr(host: &str) -> ResultType<TargetAddr<'static>> {
- let addr = match Config::get_network_type() {
- NetworkType::Direct => to_socket_addr(&host)?.into_target_addr()?,
- NetworkType::ProxySocks => host.into_target_addr()?,
- }
- .to_owned();
- Ok(addr)
-}
-
pub fn test_if_valid_server(host: &str) -> String {
let mut host = host.to_owned();
if !host.contains(":") {
host = format!("{}:{}", host, 0);
}
+ use std::net::ToSocketAddrs;
match Config::get_network_type() {
- NetworkType::Direct => match to_socket_addr(&host) {
+ NetworkType::Direct => match host.to_socket_addrs() {
Err(err) => err.to_string(),
Ok(_) => "".to_owned(),
},
@@ -44,33 +28,126 @@ pub fn test_if_valid_server(host: &str) -> String {
}
}
-pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>(
+pub trait IsResolvedSocketAddr {
+ fn resolve(&self) -> Option<&SocketAddr>;
+}
+
+impl IsResolvedSocketAddr for SocketAddr {
+ fn resolve(&self) -> Option<&SocketAddr> {
+ Some(&self)
+ }
+}
+
+impl IsResolvedSocketAddr for String {
+ fn resolve(&self) -> Option<&SocketAddr> {
+ None
+ }
+}
+
+impl IsResolvedSocketAddr for &str {
+ fn resolve(&self) -> Option<&SocketAddr> {
+ None
+ }
+}
+
+#[inline]
+pub async fn connect_tcp<
+ 't,
+ T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
+>(
target: T,
- local: SocketAddr,
ms_timeout: u64,
) -> ResultType<FramedStream> {
- let target_addr = target.into_target_addr()?;
+ connect_tcp_local(target, None, ms_timeout).await
+}
+pub async fn connect_tcp_local<
+ 't,
+ T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
+>(
+ target: T,
+ local: Option<SocketAddr>,
+ ms_timeout: u64,
+) -> ResultType<FramedStream> {
if let Some(conf) = Config::get_socks() {
- FramedStream::connect(
+ return FramedStream::connect(
conf.proxy.as_str(),
- target_addr,
+ target,
local,
conf.username.as_str(),
conf.password.as_str(),
ms_timeout,
)
- .await
- } else {
- let addr = std::net::ToSocketAddrs::to_socket_addrs(&target_addr)?
- .filter(|x| x.is_ipv4())
- .next()
- .context("Invalid target addr, no valid ipv4 address can be resolved.")?;
- Ok(FramedStream::new(addr, local, ms_timeout).await?)
+ .await;
}
+ if let Some(target) = target.resolve() {
+ if let Some(local) = local {
+ if local.is_ipv6() && target.is_ipv4() {
+ let target = query_nip_io(&target).await?;
+ return Ok(FramedStream::new(target, Some(local), ms_timeout).await?);
+ }
+ }
+ }
+ Ok(FramedStream::new(target, local, ms_timeout).await?)
}
-pub async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<FramedSocket> {
+#[inline]
+pub fn is_ipv4(target: &TargetAddr<'_>) -> bool {
+ match target {
+ TargetAddr::Ip(addr) => addr.is_ipv4(),
+ _ => true,
+ }
+}
+
+#[inline]
+pub async fn query_nip_io(addr: &SocketAddr) -> ResultType<SocketAddr> {
+ tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port()))
+ .await?
+ .filter(|x| x.is_ipv6())
+ .next()
+ .context("Failed to get ipv6 from nip.io")
+}
+
+#[inline]
+pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String {
+ if !ipv4 && crate::is_ipv4_str(&addr) {
+ if let Some(ip) = addr.split(":").next() {
+ return addr.replace(ip, &format!("{}.nip.io", ip));
+ }
+ }
+ addr
+}
+
+async fn test_target(target: &str) -> ResultType<SocketAddr> {
+ if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await {
+ if let Ok(addr) = s.peer_addr() {
+ return Ok(addr);
+ }
+ }
+ tokio::net::lookup_host(target)
+ .await?
+ .next()
+ .context(format!("Failed to look up host for {}", target))
+}
+
+#[inline]
+pub async fn new_udp_for(
+ target: &str,
+ ms_timeout: u64,
+) -> ResultType<(FramedSocket, TargetAddr<'static>)> {
+ let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() {
+ let addr = test_target(target).await?;
+ (addr.is_ipv4(), addr.into_target_addr()?)
+ } else {
+ (true, target.into_target_addr()?)
+ };
+ Ok((
+ new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?,
+ target.to_owned(),
+ ))
+}
+
+async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<FramedSocket> {
match Config::get_socks() {
None => Ok(FramedSocket::new(local).await?),
Some(conf) => {
@@ -87,9 +164,56 @@ pub async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<
}
}
-pub async fn rebind_udp<T: ToSocketAddrs>(local: T) -> ResultType<Option<FramedSocket>> {
- match Config::get_network_type() {
- NetworkType::Direct => Ok(Some(FramedSocket::new(local).await?)),
- _ => Ok(None),
+pub async fn rebind_udp_for(
+ target: &str,
+) -> ResultType<Option<(FramedSocket, TargetAddr<'static>)>> {
+ if Config::get_network_type() != NetworkType::Direct {
+ return Ok(None);
+ }
+ let addr = test_target(target).await?;
+ let v4 = addr.is_ipv4();
+ Ok(Some((
+ FramedSocket::new(Config::get_any_listen_addr(v4)).await?,
+ addr.into_target_addr()?.to_owned(),
+ )))
+}
+
+#[cfg(test)]
+mod tests {
+ use std::net::ToSocketAddrs;
+
+ use super::*;
+
+ #[test]
+ fn test_nat64() {
+ test_nat64_async();
+ }
+
+ #[tokio::main(flavor = "current_thread")]
+ async fn test_nat64_async() {
+ assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1");
+ assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io");
+ assert_eq!(
+ ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false),
+ "1.1.1.1.nip.io:8080"
+ );
+ assert_eq!(
+ ipv4_to_ipv6("rustdesk.com".to_owned(), false),
+ "rustdesk.com"
+ );
+ if ("rustdesk.com:80")
+ .to_socket_addrs()
+ .unwrap()
+ .next()
+ .unwrap()
+ .is_ipv6()
+ {
+ assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap())
+ .await
+ .unwrap()
+ .is_ipv6());
+ return;
+ }
+ assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err());
}
}
diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs
index 7966920..a1322fc 100644
--- a/libs/hbb_common/src/tcp.rs
+++ b/libs/hbb_common/src/tcp.rs
@@ -5,7 +5,7 @@ use protobuf::Message;
use sodiumoxide::crypto::secretbox::{self, Key, Nonce};
use std::{
io::{self, Error, ErrorKind},
- net::SocketAddr,
+ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
ops::{Deref, DerefMut},
pin::Pin,
task::{Context, Poll},
@@ -73,73 +73,79 @@ fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std:
}
impl FramedStream {
- pub async fn new<T1: ToSocketAddrs, T2: ToSocketAddrs>(
- remote_addr: T1,
- local_addr: T2,
+ pub async fn new<T: ToSocketAddrs + std::fmt::Display>(
+ remote_addr: T,
+ local_addr: Option<SocketAddr>,
ms_timeout: u64,
) -> ResultType<Self> {
- for local_addr in lookup_host(&local_addr).await? {
- for remote_addr in lookup_host(&remote_addr).await? {
- let stream = super::timeout(
- ms_timeout,
- new_socket(local_addr, true)?.connect(remote_addr),
- )
- .await??;
- stream.set_nodelay(true).ok();
- let addr = stream.local_addr()?;
- return Ok(Self(
- Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
- addr,
- None,
- 0,
- ));
+ for remote_addr in lookup_host(&remote_addr).await? {
+ let local = if let Some(addr) = local_addr {
+ addr
+ } else {
+ crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4())
+ };
+ if let Ok(socket) = new_socket(local, true) {
+ if let Ok(Ok(stream)) =
+ super::timeout(ms_timeout, socket.connect(remote_addr)).await
+ {
+ stream.set_nodelay(true).ok();
+ let addr = stream.local_addr()?;
+ return Ok(Self(
+ Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
+ addr,
+ None,
+ 0,
+ ));
+ }
}
}
- bail!("could not resolve to any address");
+ bail!(format!("Failed to connect to {}", remote_addr));
}
- pub async fn connect<'a, 't, P, T1, T2>(
+ pub async fn connect<'a, 't, P, T>(
proxy: P,
- target: T1,
- local: T2,
+ target: T,
+ local_addr: Option<SocketAddr>,
username: &'a str,
password: &'a str,
ms_timeout: u64,
) -> ResultType<Self>
where
P: ToProxyAddrs,
- T1: IntoTargetAddr<'t>,
- T2: ToSocketAddrs,
+ T: IntoTargetAddr<'t>,
{
- if let Some(local) = lookup_host(&local).await?.next() {
- if let Some(proxy) = proxy.to_proxy_addrs().next().await {
- let stream =
- super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy?)).await??;
- stream.set_nodelay(true).ok();
- let stream = if username.trim().is_empty() {
- super::timeout(
- ms_timeout,
- Socks5Stream::connect_with_socket(stream, target),
- )
- .await??
- } else {
- super::timeout(
- ms_timeout,
- Socks5Stream::connect_with_password_and_socket(
- stream, target, username, password,
- ),
- )
- .await??
- };
- let addr = stream.local_addr()?;
- return Ok(Self(
- Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
- addr,
- None,
- 0,
- ));
+ if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await {
+ let local = if let Some(addr) = local_addr {
+ addr
+ } else {
+ crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
+ };
+ let stream =
+ super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??;
+ stream.set_nodelay(true).ok();
+ let stream = if username.trim().is_empty() {
+ super::timeout(
+ ms_timeout,
+ Socks5Stream::connect_with_socket(stream, target),
+ )
+ .await??
+ } else {
+ super::timeout(
+ ms_timeout,
+ Socks5Stream::connect_with_password_and_socket(
+ stream, target, username, password,
+ ),
+ )
+ .await??
};
- };
+ let addr = stream.local_addr()?;
+ return Ok(Self(
+ Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
+ addr,
+ None,
+ 0,
+ ));
+ }
bail!("could not resolve to any address");
}
@@ -252,6 +258,38 @@ pub async fn new_listener<T: ToSocketAddrs>(addr: T, reuse: bool) -> ResultType<
}
}
+pub async fn listen_any(port: u16) -> ResultType<TcpListener> {
+ if let Ok(mut socket) = TcpSocket::new_v6() {
+ #[cfg(unix)]
+ {
+ use std::os::unix::io::{FromRawFd, IntoRawFd};
+ let raw_fd = socket.into_raw_fd();
+ let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) };
+ sock2.set_only_v6(false).ok();
+ socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) };
+ }
+ #[cfg(windows)]
+ {
+ use std::os::windows::prelude::{FromRawSocket, IntoRawSocket};
+ let raw_socket = socket.into_raw_socket();
+ let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) };
+ sock2.set_only_v6(false).ok();
+ socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) };
+ }
+ if socket
+ .bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port))
+ .is_ok()
+ {
+ if let Ok(l) = socket.listen(DEFAULT_BACKLOG) {
+ return Ok(l);
+ }
+ }
+ }
+ let s = TcpSocket::new_v4()?;
+ s.bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?;
+ Ok(s.listen(DEFAULT_BACKLOG)?)
+}
+
impl Unpin for DynTcpStream {}
impl AsyncRead for DynTcpStream {
diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs
index 3532dd1..38121a4 100644
--- a/libs/hbb_common/src/udp.rs
+++ b/libs/hbb_common/src/udp.rs
@@ -49,7 +49,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()?.filter(|x| x.is_ipv4()) {
+ for addr in addr.to_socket_addrs()? {
let socket = new_socket(addr, true, 0)?.into_udp_socket();
return Ok(Self::Direct(UdpFramed::new(
UdpSocket::from_std(socket)?,
@@ -63,7 +63,7 @@ impl FramedSocket {
addr: T,
buf_size: usize,
) -> ResultType<Self> {
- for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) {
+ 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(),
@@ -164,4 +164,13 @@ impl FramedSocket {
None
}
}
+
+ pub fn is_ipv4(&self) -> bool {
+ if let FramedSocket::Direct(x) = self {
+ if let Ok(v) = x.get_ref().local_addr() {
+ return v.is_ipv4();
+ }
+ }
+ true
+ }
}