Initial commit: RustDesk 1.4.7 macOS desktop port
- Filled empty libs/hbb_common/ submodule (cloned from rustdesk/hbb_common) - Patched Flutter 3.44 / Dart 3.12 compatibility: * flutter/lib/generated_bridge.dart: asTypedList with cast<>, DartPort=Int64 * flutter/lib/common.dart: DialogTheme->DialogThemeData, TabBarTheme->TabBarThemeData * flutter/pubspec.yaml: extended_text 14.0.0->15.0.2, google_fonts override 5.0.0 * flutter/macos/Runner/Configs/Release.xcconfig: EXCLUDED_ARCHS=x86_64 - Build verified: cargo check + cargo build --features flutter + cargo build --release --features flutter - Verified flutter build macos --debug and --release both produce working .app - Verified .dmg installer (27MB arm64) created via hdiutil - Build deps: Xcode 26.5, CocoaPods 1.16.2, Flutter 3.44, VCPKG arm64-osx
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use std::io;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
// Bound speculative allocation from untrusted frame headers.
|
||||
const MAX_PREALLOCATED_PAYLOAD_LEN: usize = 256 * 1024;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BytesCodec {
|
||||
state: DecodeState,
|
||||
raw: bool,
|
||||
max_packet_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum DecodeState {
|
||||
Head,
|
||||
Data(usize),
|
||||
}
|
||||
|
||||
impl Default for BytesCodec {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BytesCodec {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: DecodeState::Head,
|
||||
raw: false,
|
||||
max_packet_length: usize::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_raw(&mut self) {
|
||||
self.raw = true;
|
||||
}
|
||||
|
||||
pub fn set_max_packet_length(&mut self, n: usize) {
|
||||
self.max_packet_length = n;
|
||||
}
|
||||
|
||||
fn decode_head(&mut self, src: &mut BytesMut) -> io::Result<Option<usize>> {
|
||||
if src.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let head_len = ((src[0] & 0x3) + 1) as usize;
|
||||
if src.len() < head_len {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut n = src[0] as usize;
|
||||
if head_len > 1 {
|
||||
n |= (src[1] as usize) << 8;
|
||||
}
|
||||
if head_len > 2 {
|
||||
n |= (src[2] as usize) << 16;
|
||||
}
|
||||
if head_len > 3 {
|
||||
n |= (src[3] as usize) << 24;
|
||||
}
|
||||
n >>= 2;
|
||||
if n > self.max_packet_length {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "Too big packet"));
|
||||
}
|
||||
src.advance(head_len);
|
||||
// Do not reserve the full header-declared length: a peer can advertise a huge
|
||||
// frame and force excessive allocation before sending the payload.
|
||||
src.reserve(
|
||||
n.saturating_sub(src.len())
|
||||
.min(MAX_PREALLOCATED_PAYLOAD_LEN),
|
||||
);
|
||||
Ok(Some(n))
|
||||
}
|
||||
|
||||
fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result<Option<BytesMut>> {
|
||||
if src.len() < n {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(src.split_to(n)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for BytesCodec {
|
||||
type Item = BytesMut;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<BytesMut>, io::Error> {
|
||||
if self.raw {
|
||||
if !src.is_empty() {
|
||||
let len = src.len();
|
||||
return Ok(Some(src.split_to(len)));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
let n = match self.state {
|
||||
DecodeState::Head => match self.decode_head(src)? {
|
||||
Some(n) => {
|
||||
self.state = DecodeState::Data(n);
|
||||
n
|
||||
}
|
||||
None => return Ok(None),
|
||||
},
|
||||
DecodeState::Data(n) => n,
|
||||
};
|
||||
|
||||
match self.decode_data(n, src)? {
|
||||
Some(data) => {
|
||||
self.state = DecodeState::Head;
|
||||
Ok(Some(data))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<Bytes> for BytesCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> {
|
||||
if self.raw {
|
||||
buf.reserve(data.len());
|
||||
buf.put(data);
|
||||
return Ok(());
|
||||
}
|
||||
if data.len() <= 0x3F {
|
||||
buf.put_u8((data.len() << 2) as u8);
|
||||
} else if data.len() <= 0x3FFF {
|
||||
buf.put_u16_le((data.len() << 2) as u16 | 0x1);
|
||||
} else if data.len() <= 0x3FFFFF {
|
||||
let h = (data.len() << 2) as u32 | 0x2;
|
||||
buf.put_u16_le((h & 0xFFFF) as u16);
|
||||
buf.put_u8((h >> 16) as u8);
|
||||
} else if data.len() <= 0x3FFFFFFF {
|
||||
buf.put_u32_le((data.len() << 2) as u32 | 0x3);
|
||||
} else {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow"));
|
||||
}
|
||||
buf.extend(data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_codec1() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3F, 1);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
let buf_saved = buf.clone();
|
||||
assert_eq!(buf.len(), 0x3F + 1);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3F);
|
||||
assert_eq!(res[0], 1);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
let mut codec2 = BytesCodec::new();
|
||||
let mut buf2 = BytesMut::new();
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
buf2.extend(&buf_saved[0..1]);
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
buf2.extend(&buf_saved[1..]);
|
||||
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
|
||||
assert_eq!(res.len(), 0x3F);
|
||||
assert_eq!(res[0], 1);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec2() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
assert!(codec.encode("".into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 1);
|
||||
bytes.resize(0x3F + 1, 2);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 0x3F + 2 + 2);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3F + 1);
|
||||
assert_eq!(res[0], 2);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec3() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3F - 1, 3);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 0x3F + 1 - 1);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3F - 1);
|
||||
assert_eq!(res[0], 3);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_codec4() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3FFF, 4);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 0x3FFF + 2);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3FFF);
|
||||
assert_eq!(res[0], 4);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec5() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3FFFFF, 5);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 0x3FFFFF + 3);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3FFFFF);
|
||||
assert_eq!(res[0], 5);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec6() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3FFFFF + 1, 6);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
let buf_saved = buf.clone();
|
||||
assert_eq!(buf.len(), 0x3FFFFF + 4 + 1);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3FFFFF + 1);
|
||||
assert_eq!(res[0], 6);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
let mut codec2 = BytesCodec::new();
|
||||
let mut buf2 = BytesMut::new();
|
||||
buf2.extend(&buf_saved[0..1]);
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
buf2.extend(&buf_saved[1..6]);
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
buf2.extend(&buf_saved[6..]);
|
||||
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
|
||||
assert_eq!(res.len(), 0x3FFFFF + 1);
|
||||
assert_eq!(res[0], 6);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_large_frame_header_caps_preallocation() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let n = 0x3FFFFFFFusize;
|
||||
const MAX_REASONABLE_CAPACITY: usize = MAX_PREALLOCATED_PAYLOAD_LEN * 4;
|
||||
|
||||
buf.put_u32_le((n << 2) as u32 | 0x3);
|
||||
|
||||
assert!(matches!(codec.decode(&mut buf), Ok(None)));
|
||||
assert!(buf.capacity() <= MAX_REASONABLE_CAPACITY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use std::{cell::RefCell, io};
|
||||
use zstd::bulk::Compressor;
|
||||
|
||||
// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(),
|
||||
// which is currently 22. Levels >= 20
|
||||
// Default level is ZSTD_CLEVEL_DEFAULT==3.
|
||||
// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT
|
||||
thread_local! {
|
||||
static COMPRESSOR: RefCell<io::Result<Compressor<'static>>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL));
|
||||
}
|
||||
|
||||
pub fn compress(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
COMPRESSOR.with(|c| {
|
||||
if let Ok(mut c) = c.try_borrow_mut() {
|
||||
match &mut *c {
|
||||
Ok(c) => match c.compress(data) {
|
||||
Ok(res) => out = res,
|
||||
Err(err) => {
|
||||
crate::log::debug!("Failed to compress: {}", err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
crate::log::debug!("Failed to get compressor: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
pub fn decompress(data: &[u8]) -> Vec<u8> {
|
||||
zstd::decode_all(data).unwrap_or_default()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,412 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
use sodiumoxide::base64;
|
||||
|
||||
use crate::{
|
||||
log,
|
||||
password_security::{decrypt_str_or_original, symmetric_crypt},
|
||||
};
|
||||
|
||||
pub(super) const PASSWORD_ENC_VERSION: &str = "00";
|
||||
pub(super) const PERMANENT_PASSWORD_ENC_VERSION: &str = "01";
|
||||
pub(super) const PERMANENT_PASSWORD_HASH_PREFIX: &str = "00";
|
||||
const HBBS_PRESET_PASSWORD_HASH_PREFIX: &str = "00";
|
||||
pub(super) const PERMANENT_PASSWORD_H1_LEN: usize = 32;
|
||||
pub(super) const DEFAULT_SALT_LEN: usize = 32;
|
||||
pub const ENCRYPT_MAX_LEN: usize = 128; // used for password, pin, etc, not for all
|
||||
const VERSION_LEN: usize = 2;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn is_permanent_password_hashed_storage(v: &str) -> bool {
|
||||
decode_permanent_password_h1_from_hashed_storage(v).is_some()
|
||||
}
|
||||
|
||||
pub fn compute_permanent_password_h1(
|
||||
password: &str,
|
||||
salt: &str,
|
||||
) -> [u8; PERMANENT_PASSWORD_H1_LEN] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password.as_bytes());
|
||||
hasher.update(salt.as_bytes());
|
||||
let out = hasher.finalize();
|
||||
let mut h1 = [0u8; PERMANENT_PASSWORD_H1_LEN];
|
||||
h1.copy_from_slice(&out[..PERMANENT_PASSWORD_H1_LEN]);
|
||||
h1
|
||||
}
|
||||
|
||||
pub(super) fn constant_time_eq_32(a: &[u8; 32], b: &[u8; 32]) -> bool {
|
||||
sodiumoxide::utils::memcmp(a, b)
|
||||
}
|
||||
|
||||
pub(super) fn encode_permanent_password_storage_from_h1(
|
||||
h1: &[u8; PERMANENT_PASSWORD_H1_LEN],
|
||||
) -> String {
|
||||
PERMANENT_PASSWORD_HASH_PREFIX.to_owned() + &base64::encode(h1, base64::Variant::Original)
|
||||
}
|
||||
|
||||
pub(super) fn encode_permanent_password_encrypted_storage_from_h1(
|
||||
h1: &[u8; PERMANENT_PASSWORD_H1_LEN],
|
||||
) -> Option<String> {
|
||||
let hashed_storage = encode_permanent_password_storage_from_h1(h1);
|
||||
encrypt_permanent_password_storage(&hashed_storage)
|
||||
}
|
||||
|
||||
pub(super) fn decode_permanent_password_h1_from_hashed_storage(
|
||||
storage: &str,
|
||||
) -> Option<[u8; PERMANENT_PASSWORD_H1_LEN]> {
|
||||
decode_password_h1_after_prefix(storage, PERMANENT_PASSWORD_HASH_PREFIX)
|
||||
}
|
||||
|
||||
fn decode_password_h1_after_prefix(
|
||||
storage: &str,
|
||||
prefix: &str,
|
||||
) -> Option<[u8; PERMANENT_PASSWORD_H1_LEN]> {
|
||||
let encoded = storage.strip_prefix(prefix)?;
|
||||
|
||||
let v = base64::decode(encoded.as_bytes(), base64::Variant::Original).ok()?;
|
||||
if v.len() != PERMANENT_PASSWORD_H1_LEN {
|
||||
return None;
|
||||
}
|
||||
let mut h1 = [0u8; PERMANENT_PASSWORD_H1_LEN];
|
||||
h1.copy_from_slice(&v[..PERMANENT_PASSWORD_H1_LEN]);
|
||||
Some(h1)
|
||||
}
|
||||
|
||||
fn encrypt_permanent_password_storage(storage: &str) -> Option<String> {
|
||||
if storage.chars().count() > ENCRYPT_MAX_LEN {
|
||||
return None;
|
||||
}
|
||||
let encrypted = symmetric_crypt(storage.as_bytes(), true).ok()?;
|
||||
Some(
|
||||
PERMANENT_PASSWORD_ENC_VERSION.to_owned()
|
||||
+ &base64::encode(encrypted, base64::Variant::Original),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn decrypt_permanent_password_str_or_original(storage: &str) -> (String, bool, bool) {
|
||||
if storage.len() > VERSION_LEN && storage.starts_with(PERMANENT_PASSWORD_ENC_VERSION) {
|
||||
if let Ok(decoded) = base64::decode(
|
||||
&storage.as_bytes()[VERSION_LEN..],
|
||||
base64::Variant::Original,
|
||||
) {
|
||||
if let Ok(v) = symmetric_crypt(&decoded, false) {
|
||||
return (String::from_utf8_lossy(&v).to_string(), true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
(storage.to_owned(), false, !storage.is_empty())
|
||||
}
|
||||
|
||||
pub fn local_permanent_password_storage_is_usable_for_auth(storage: &str, salt: &str) -> bool {
|
||||
if storage.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if decode_permanent_password_h1_from_storage(storage).is_some() {
|
||||
return !salt.is_empty();
|
||||
}
|
||||
if storage.starts_with(PERMANENT_PASSWORD_ENC_VERSION) {
|
||||
let (_, decrypted, _) = decrypt_permanent_password_str_or_original(storage);
|
||||
if decrypted {
|
||||
log::error!("Permanent password storage looks current but cannot be decoded as a hash");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let (_, decrypted, looks_like_plaintext) =
|
||||
decrypt_str_or_original(storage, PASSWORD_ENC_VERSION);
|
||||
if storage.starts_with(PASSWORD_ENC_VERSION) && !decrypted && !looks_like_plaintext {
|
||||
log::error!("Permanent password storage looks encrypted but cannot be decrypted");
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn preset_permanent_password_storage_is_usable_for_auth(storage: &str, salt: &str) -> bool {
|
||||
if storage.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if salt.is_empty() {
|
||||
return true;
|
||||
}
|
||||
decode_preset_password_h1_from_storage(storage).is_some()
|
||||
}
|
||||
|
||||
pub fn decode_preset_password_h1_from_storage(
|
||||
storage: &str,
|
||||
) -> Option<[u8; PERMANENT_PASSWORD_H1_LEN]> {
|
||||
decode_password_h1_after_prefix(storage, HBBS_PRESET_PASSWORD_HASH_PREFIX)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn local_permanent_password_storage_matches_plain(storage: &str, salt: &str, input: &str) -> bool {
|
||||
if storage.is_empty() || input.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if !local_permanent_password_storage_is_usable_for_auth(storage, salt) {
|
||||
return false;
|
||||
}
|
||||
if let Some(stored_h1) = decode_permanent_password_h1_from_storage(storage) {
|
||||
if salt.is_empty() {
|
||||
log::error!("Salt is empty but permanent password storage is hashed");
|
||||
return false;
|
||||
}
|
||||
let h1 = compute_permanent_password_h1(input, salt);
|
||||
return constant_time_eq_32(&h1, &stored_h1);
|
||||
}
|
||||
storage == input
|
||||
}
|
||||
|
||||
pub(super) fn preset_permanent_password_storage_matches_plain(
|
||||
storage: &str,
|
||||
salt: &str,
|
||||
input: &str,
|
||||
) -> bool {
|
||||
if storage.is_empty() || input.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if salt.is_empty() {
|
||||
return storage == input;
|
||||
}
|
||||
let Some(stored_h1) = decode_preset_password_h1_from_storage(storage) else {
|
||||
return false;
|
||||
};
|
||||
let h1 = compute_permanent_password_h1(input, salt);
|
||||
constant_time_eq_32(&h1, &stored_h1)
|
||||
}
|
||||
|
||||
pub fn decode_permanent_password_h1_from_storage(
|
||||
storage: &str,
|
||||
) -> Option<[u8; PERMANENT_PASSWORD_H1_LEN]> {
|
||||
if storage.starts_with(PERMANENT_PASSWORD_ENC_VERSION) {
|
||||
let (hashed_storage, decrypted, _) = decrypt_permanent_password_str_or_original(storage);
|
||||
if !decrypted {
|
||||
return None;
|
||||
}
|
||||
return decode_permanent_password_h1_from_hashed_storage(&hashed_storage);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Salt can be updated only when the password is empty, plaintext, or decryptable
|
||||
// legacy storage. Current-prefixed storage is treated as salt-bound.
|
||||
pub(super) fn password_is_empty_or_not_hashed(permanent_password_storage: &str) -> bool {
|
||||
if permanent_password_storage.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if decode_permanent_password_h1_from_storage(permanent_password_storage).is_some() {
|
||||
return false;
|
||||
}
|
||||
if permanent_password_storage.starts_with(PERMANENT_PASSWORD_ENC_VERSION) {
|
||||
return false;
|
||||
}
|
||||
let (_, decrypted, looks_like_plaintext) =
|
||||
decrypt_str_or_original(permanent_password_storage, PASSWORD_ENC_VERSION);
|
||||
decrypted || looks_like_plaintext
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::password_security::encrypt_str_or_original;
|
||||
|
||||
fn encode_hbbs_preset_password_storage_from_h1(h1: &[u8; PERMANENT_PASSWORD_H1_LEN]) -> String {
|
||||
HBBS_PRESET_PASSWORD_HASH_PREFIX.to_owned() + &base64::encode(h1, base64::Variant::Original)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permanent_password_h1_storage_roundtrip() {
|
||||
let salt = "salt123";
|
||||
let password = "p@ssw0rd";
|
||||
let h1 = compute_permanent_password_h1(password, salt);
|
||||
let stored = encode_permanent_password_storage_from_h1(&h1);
|
||||
assert!(stored.starts_with(PERMANENT_PASSWORD_HASH_PREFIX));
|
||||
assert!(is_permanent_password_hashed_storage(&stored));
|
||||
let decoded = decode_permanent_password_h1_from_hashed_storage(&stored).unwrap();
|
||||
assert_eq!(&decoded[..], &h1[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permanent_password_encrypted_storage_uses_01_outer_and_00_inner() {
|
||||
let h1 = compute_permanent_password_h1("p@ssw0rd", "salt123");
|
||||
let storage = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
|
||||
|
||||
assert!(storage.starts_with(PERMANENT_PASSWORD_ENC_VERSION));
|
||||
assert!(!is_permanent_password_hashed_storage(&storage));
|
||||
|
||||
let (inner, decrypted, should_store) = decrypt_permanent_password_str_or_original(&storage);
|
||||
assert!(decrypted);
|
||||
assert!(!should_store);
|
||||
assert!(inner.starts_with(PERMANENT_PASSWORD_HASH_PREFIX));
|
||||
assert_eq!(
|
||||
decode_permanent_password_h1_from_storage(&storage),
|
||||
Some(h1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_hashed_password_storage_matches_plain_with_salt() {
|
||||
let salt = "salt123";
|
||||
let h1 = compute_permanent_password_h1("p@ssw0rd", salt);
|
||||
let storage = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
|
||||
|
||||
assert!(local_permanent_password_storage_is_usable_for_auth(
|
||||
&storage, salt
|
||||
));
|
||||
assert!(local_permanent_password_storage_matches_plain(
|
||||
&storage, salt, "p@ssw0rd"
|
||||
));
|
||||
assert!(!local_permanent_password_storage_matches_plain(
|
||||
&storage, salt, "wrong"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hbbs_00_hashed_preset_password_storage_is_decoded_for_preset_auth() {
|
||||
let h1 = compute_permanent_password_h1("p@ssw0rd", "salt123");
|
||||
let storage = encode_hbbs_preset_password_storage_from_h1(&h1);
|
||||
|
||||
assert_eq!(decode_preset_password_h1_from_storage(&storage), Some(h1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hbbs_00_hashed_preset_password_storage_matches_plain_with_salt() {
|
||||
let salt = "salt123";
|
||||
let h1 = compute_permanent_password_h1("p@ssw0rd", salt);
|
||||
let storage = encode_hbbs_preset_password_storage_from_h1(&h1);
|
||||
|
||||
assert!(preset_permanent_password_storage_is_usable_for_auth(
|
||||
&storage, salt
|
||||
));
|
||||
assert!(preset_permanent_password_storage_matches_plain(
|
||||
&storage, salt, "p@ssw0rd"
|
||||
));
|
||||
assert!(!preset_permanent_password_storage_matches_plain(
|
||||
&storage, salt, "wrong"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_hash_storage_is_not_accepted_as_preset_storage() {
|
||||
let salt = "salt123";
|
||||
let h1 = compute_permanent_password_h1("p@ssw0rd", salt);
|
||||
let storage = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
|
||||
|
||||
assert!(!preset_permanent_password_storage_is_usable_for_auth(
|
||||
&storage, salt
|
||||
));
|
||||
assert!(!preset_permanent_password_storage_matches_plain(
|
||||
&storage, salt, "p@ssw0rd"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hbbs_00_shaped_preset_password_without_salt_stays_plaintext() {
|
||||
let h1 = compute_permanent_password_h1("p@ssw0rd", "salt123");
|
||||
let storage = encode_hbbs_preset_password_storage_from_h1(&h1);
|
||||
|
||||
assert!(preset_permanent_password_storage_is_usable_for_auth(
|
||||
&storage, ""
|
||||
));
|
||||
assert!(preset_permanent_password_storage_matches_plain(
|
||||
&storage, "", &storage
|
||||
));
|
||||
assert!(!preset_permanent_password_storage_matches_plain(
|
||||
&storage, "", "p@ssw0rd"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hashed_preset_password_storage_without_salt_is_not_usable() {
|
||||
let h1 = compute_permanent_password_h1("p@ssw0rd", "salt123");
|
||||
let storage = encode_permanent_password_storage_from_h1(&h1);
|
||||
|
||||
assert!(!local_permanent_password_storage_is_usable_for_auth(
|
||||
&storage, ""
|
||||
));
|
||||
assert!(!local_permanent_password_storage_matches_plain(
|
||||
&storage, "", "p@ssw0rd"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_plain_preset_password_without_salt_keeps_old_behavior() {
|
||||
let storage = "01not-a-valid-hash";
|
||||
|
||||
assert!(preset_permanent_password_storage_is_usable_for_auth(
|
||||
storage, ""
|
||||
));
|
||||
assert!(preset_permanent_password_storage_matches_plain(
|
||||
storage,
|
||||
"",
|
||||
"01not-a-valid-hash"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_malformed_preset_password_with_salt_is_not_usable_for_auth() {
|
||||
for storage in ["01not-a-valid-hash", "00not-a-valid-hash"] {
|
||||
assert!(!preset_permanent_password_storage_is_usable_for_auth(
|
||||
storage,
|
||||
"preset-salt"
|
||||
));
|
||||
assert!(!preset_permanent_password_storage_matches_plain(
|
||||
storage,
|
||||
"preset-salt",
|
||||
storage
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_current_version_storage_is_not_usable_for_auth() {
|
||||
let encrypted = symmetric_crypt(b"not-a-hash", true).unwrap();
|
||||
let encrypted_non_hash = PERMANENT_PASSWORD_ENC_VERSION.to_owned()
|
||||
+ &base64::encode(encrypted, base64::Variant::Original);
|
||||
|
||||
assert!(!local_permanent_password_storage_is_usable_for_auth(
|
||||
&encrypted_non_hash,
|
||||
"salt123"
|
||||
));
|
||||
assert!(!local_permanent_password_storage_matches_plain(
|
||||
&encrypted_non_hash,
|
||||
"salt123",
|
||||
&encrypted_non_hash
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_plain_preset_password_that_decodes_as_hash_requires_salt() {
|
||||
let h1 = compute_permanent_password_h1("plain-looking-hash", "salt123");
|
||||
let storage = encode_permanent_password_storage_from_h1(&h1);
|
||||
|
||||
assert!(!local_permanent_password_storage_is_usable_for_auth(
|
||||
&storage, ""
|
||||
));
|
||||
assert!(!local_permanent_password_storage_matches_plain(
|
||||
&storage, "", &storage
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_is_empty_or_not_hashed_accepts_plaintext_and_decryptable_legacy_plaintext() {
|
||||
let storage =
|
||||
encrypt_str_or_original("legacy-secret", PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
|
||||
|
||||
assert!(password_is_empty_or_not_hashed("00secret"));
|
||||
assert!(password_is_empty_or_not_hashed(&storage));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_is_empty_or_not_hashed_treats_locked_00_storage_as_hashed() {
|
||||
let invalid_payload = vec![42u8; sodiumoxide::crypto::secretbox::MACBYTES + 1];
|
||||
let locked_storage = PASSWORD_ENC_VERSION.to_owned()
|
||||
+ &base64::encode(invalid_payload, base64::Variant::Original);
|
||||
|
||||
assert!(!password_is_empty_or_not_hashed(&locked_storage));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_is_empty_or_not_hashed_treats_invalid_01_storage_as_hashed() {
|
||||
assert!(!password_is_empty_or_not_hashed("01not-a-valid-hash"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use sha2::digest::Update;
|
||||
use sha2::{Digest, Sha512};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Once;
|
||||
use sysinfo::System;
|
||||
|
||||
const TABLE: [u8; 256] = [
|
||||
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
|
||||
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
|
||||
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
|
||||
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
|
||||
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
|
||||
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
|
||||
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
|
||||
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
|
||||
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
|
||||
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
|
||||
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
|
||||
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
|
||||
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
|
||||
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
|
||||
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16,
|
||||
];
|
||||
|
||||
pub fn expand_key(key: &[u8; 16]) -> Vec<[u8; 16]> {
|
||||
let mut round_keys = Vec::with_capacity(11);
|
||||
let mut expanded_key = Vec::with_capacity(176);
|
||||
expanded_key.extend_from_slice(key);
|
||||
|
||||
for i in 4..44 {
|
||||
let mut temp = [0u8; 4];
|
||||
temp.copy_from_slice(&expanded_key[(i - 1) * 4..i * 4]);
|
||||
|
||||
if i % 4 == 0 {
|
||||
temp.rotate_left(1);
|
||||
for j in 0..4 {
|
||||
temp[j] = TABLE[temp[j] as usize];
|
||||
}
|
||||
temp[0] ^= match i {
|
||||
4 => 0x01,
|
||||
8 => 0x02,
|
||||
12 => 0x04,
|
||||
16 => 0x08,
|
||||
20 => 0x10,
|
||||
24 => 0x20,
|
||||
28 => 0x40,
|
||||
32 => 0x80,
|
||||
36 => 0x1b,
|
||||
40 => 0x36,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
for j in 0..4 {
|
||||
let prev = expanded_key[(i - 4) * 4 + j];
|
||||
expanded_key.push(prev ^ temp[j]);
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in expanded_key.chunks(16) {
|
||||
let mut round_key = [0u8; 16];
|
||||
round_key.copy_from_slice(chunk);
|
||||
round_keys.push(round_key);
|
||||
}
|
||||
|
||||
round_keys
|
||||
}
|
||||
|
||||
fn finalize_block(input: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
|
||||
let round_keys = expand_key(key);
|
||||
let mut state = *input;
|
||||
|
||||
add_round_key(&mut state, &round_keys[0]);
|
||||
|
||||
for round in 1..10 {
|
||||
sub_bytes(&mut state);
|
||||
shift_rows(&mut state);
|
||||
mix_columns(&mut state);
|
||||
add_round_key(&mut state, &round_keys[round]);
|
||||
}
|
||||
|
||||
sub_bytes(&mut state);
|
||||
shift_rows(&mut state);
|
||||
add_round_key(&mut state, &round_keys[10]);
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
fn sub_bytes(state: &mut [u8; 16]) {
|
||||
for byte in state.iter_mut() {
|
||||
*byte = TABLE[*byte as usize];
|
||||
}
|
||||
}
|
||||
|
||||
fn shift_rows(state: &mut [u8; 16]) {
|
||||
let mut temp = *state;
|
||||
temp[1] = state[5];
|
||||
temp[5] = state[9];
|
||||
temp[9] = state[13];
|
||||
temp[13] = state[1];
|
||||
temp[2] = state[10];
|
||||
temp[6] = state[14];
|
||||
temp[10] = state[2];
|
||||
temp[14] = state[6];
|
||||
temp[3] = state[15];
|
||||
temp[7] = state[3];
|
||||
temp[11] = state[7];
|
||||
temp[15] = state[11];
|
||||
*state = temp;
|
||||
}
|
||||
|
||||
pub fn add_round_key(state: &mut [u8; 16], round_key: &[u8; 16]) {
|
||||
for i in 0..16 {
|
||||
state[i] ^= round_key[i];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gf_mul(a: u8, b: u8) -> u8 {
|
||||
let mut p = 0u8;
|
||||
let mut temp = b;
|
||||
let mut a = a;
|
||||
|
||||
while a != 0 {
|
||||
if (a & 1) != 0 {
|
||||
p ^= temp;
|
||||
}
|
||||
let high_bit = temp & 0x80;
|
||||
temp <<= 1;
|
||||
if high_bit != 0 {
|
||||
temp ^= 0x1b;
|
||||
}
|
||||
a >>= 1;
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
fn mix_columns(state: &mut [u8; 16]) {
|
||||
for i in 0..4 {
|
||||
let s0 = state[i * 4];
|
||||
let s1 = state[i * 4 + 1];
|
||||
let s2 = state[i * 4 + 2];
|
||||
let s3 = state[i * 4 + 3];
|
||||
|
||||
state[i * 4] = gf_mul(0x02, s0) ^ gf_mul(0x03, s1) ^ s2 ^ s3;
|
||||
state[i * 4 + 1] = s0 ^ gf_mul(0x02, s1) ^ gf_mul(0x03, s2) ^ s3;
|
||||
state[i * 4 + 2] = s0 ^ s1 ^ gf_mul(0x02, s2) ^ gf_mul(0x03, s3);
|
||||
state[i * 4 + 3] = gf_mul(0x03, s0) ^ s1 ^ s2 ^ gf_mul(0x02, s3);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_system_entropy() -> [u8; 16] {
|
||||
let mut entropy = [0u8; 16];
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
for i in 0..8 {
|
||||
entropy[i] = ((timestamp >> (32 - i)) & 0xFF) as u8;
|
||||
}
|
||||
entropy
|
||||
}
|
||||
|
||||
fn get_key() -> [u8; 16] {
|
||||
let entropy = get_system_entropy();
|
||||
let base = [
|
||||
0x5d, 0x12, 0x3f, 0x4a, 0x7e, 0xc1, 0x89, 0xb3, 0x91, 0xa4, 0x2b, 0x7f, 0x3c, 0xe2, 0x6d,
|
||||
0x15,
|
||||
];
|
||||
let mut key = [0u8; 16];
|
||||
for i in 0..16 {
|
||||
key[i] = base[i] ^ entropy[i];
|
||||
}
|
||||
base
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct FingerprintingInfo {
|
||||
eol: String,
|
||||
endianness: String,
|
||||
brand: String,
|
||||
speed_max: String,
|
||||
cores: String,
|
||||
physical_cores: String,
|
||||
mem_total: String,
|
||||
platform: String,
|
||||
arch: String,
|
||||
id: String,
|
||||
addr: String,
|
||||
}
|
||||
|
||||
static mut FINGERPRINTING_INFO: Option<FingerprintingInfo> = None;
|
||||
static INIT: Once = Once::new();
|
||||
static mut CACHED_FINGERPRINTS: Option<HashMap<String, Vec<u8>>> = None;
|
||||
|
||||
impl FingerprintingInfo {
|
||||
fn new() -> Self {
|
||||
let mut sys = System::new();
|
||||
sys.refresh_cpu();
|
||||
let cpu = sys.cpus().first();
|
||||
let id = {
|
||||
let mut id = crate::config::Config::get_id();
|
||||
id.truncate(16);
|
||||
format!("{:<16}", id)
|
||||
};
|
||||
|
||||
FingerprintingInfo {
|
||||
eol: if cfg!(windows) { "\r\n" } else { "\n" }.to_string(),
|
||||
endianness: if cfg!(target_endian = "big") {
|
||||
"BE"
|
||||
} else {
|
||||
"LE"
|
||||
}
|
||||
.to_string(),
|
||||
brand: cpu.map(|cpu| cpu.brand().to_string()).unwrap_or_default(),
|
||||
speed_max: cpu
|
||||
.map(|cpu| cpu.frequency().to_string())
|
||||
.unwrap_or_default(),
|
||||
cores: sys.cpus().len().to_string(),
|
||||
physical_cores: sys.physical_core_count().unwrap_or(1).to_string(),
|
||||
mem_total: sys.total_memory().to_string(),
|
||||
platform: std::env::consts::OS.to_string(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
id,
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
addr: "0".repeat(16),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
addr: {
|
||||
let mut addr = default_net::get_mac().map(|m| m.addr).unwrap_or_default();
|
||||
if addr.is_empty() {
|
||||
addr = mac_address::get_mac_address()
|
||||
.ok()
|
||||
.and_then(|mac| mac)
|
||||
.map(|mac| mac.to_string())
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
}
|
||||
addr = addr.replace(":", "");
|
||||
format!("{:0<16}", addr)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fingerprinting_info() -> FingerprintingInfo {
|
||||
unsafe {
|
||||
INIT.call_once(|| {
|
||||
FINGERPRINTING_INFO = Some(FingerprintingInfo::new());
|
||||
CACHED_FINGERPRINTS = Some(HashMap::new());
|
||||
});
|
||||
#[allow(static_mut_refs)]
|
||||
FINGERPRINTING_INFO.clone().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fingerprint(only: Option<Vec<String>>, except: Option<Vec<String>>) -> Vec<u8> {
|
||||
let all_parameters = vec![
|
||||
"eol".to_string(),
|
||||
"endianness".to_string(),
|
||||
"brand".to_string(),
|
||||
"speed_max".to_string(),
|
||||
"cores".to_string(),
|
||||
"physical_cores".to_string(),
|
||||
"mem_total".to_string(),
|
||||
"platform".to_string(),
|
||||
"arch".to_string(),
|
||||
"id".to_string(),
|
||||
"addr".to_string(),
|
||||
];
|
||||
|
||||
let parameters = match (only, except) {
|
||||
(Some(only_params), _) => only_params,
|
||||
(None, Some(except_params)) => all_parameters
|
||||
.into_iter()
|
||||
.filter(|param| !except_params.contains(param))
|
||||
.collect(),
|
||||
(None, None) => all_parameters,
|
||||
};
|
||||
|
||||
let cache_key = parameters.join("");
|
||||
|
||||
unsafe {
|
||||
#[allow(static_mut_refs)]
|
||||
if let Some(cache) = &mut CACHED_FINGERPRINTS {
|
||||
if let Some(fingerprint) = cache.get(&cache_key) {
|
||||
return fingerprint.clone();
|
||||
}
|
||||
|
||||
let fingerprint = calculate_fingerprint(¶meters);
|
||||
cache.insert(cache_key, fingerprint.clone());
|
||||
fingerprint
|
||||
} else {
|
||||
calculate_fingerprint(¶meters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Sha512Hasher {
|
||||
sha512: Sha512,
|
||||
key: [u8; 16],
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Sha512Hasher {
|
||||
fn new() -> Self {
|
||||
let key = get_key();
|
||||
Sha512Hasher {
|
||||
sha512: Sha512::new(),
|
||||
key,
|
||||
buffer: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, data: &[u8]) {
|
||||
if data.len() <= 32 {
|
||||
self.buffer.extend_from_slice(data);
|
||||
} else {
|
||||
let split_point = data.len() - 32;
|
||||
Update::update(&mut self.sha512, &data[..split_point]);
|
||||
|
||||
self.buffer.clear();
|
||||
self.buffer.extend_from_slice(&data[split_point..]);
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize(self) -> Vec<u8> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
result.extend(self.sha512.finalize());
|
||||
|
||||
if !self.buffer.is_empty() {
|
||||
let mut first_block = [0u8; 16];
|
||||
let mut second_block = [0u8; 16];
|
||||
if self.buffer.len() >= 32 {
|
||||
let start_first = self.buffer.len() - 32;
|
||||
let start_second = self.buffer.len() - 16;
|
||||
first_block.copy_from_slice(&self.buffer[start_first..start_second]);
|
||||
second_block.copy_from_slice(&self.buffer[start_second..]);
|
||||
} else if self.buffer.len() > 16 {
|
||||
let start_second = self.buffer.len() - 16;
|
||||
first_block[..self.buffer.len() - 16].copy_from_slice(&self.buffer[..start_second]);
|
||||
second_block.copy_from_slice(&self.buffer[start_second..]);
|
||||
} else {
|
||||
first_block[..self.buffer.len()].copy_from_slice(&self.buffer);
|
||||
}
|
||||
let encrypted_first = finalize_block(&first_block, &self.key);
|
||||
let encrypted_second = finalize_block(&second_block, &self.key);
|
||||
result.extend(&encrypted_first);
|
||||
result.extend(&encrypted_second);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_fingerprint(parameters: &[String]) -> Vec<u8> {
|
||||
let info = get_fingerprinting_info();
|
||||
|
||||
let mut hasher = Sha512Hasher::new();
|
||||
|
||||
let fingerprint_string = parameters
|
||||
.iter()
|
||||
.filter_map(|param| match param.as_str() {
|
||||
"eol" => Some(info.eol.as_str()),
|
||||
"endianness" => Some(&info.endianness),
|
||||
"brand" => Some(&info.brand),
|
||||
"speed_max" => Some(&info.speed_max),
|
||||
"cores" => Some(&info.cores),
|
||||
"physical_cores" => Some(&info.physical_cores),
|
||||
"mem_total" => Some(&info.mem_total),
|
||||
"platform" => Some(&info.platform),
|
||||
"arch" => Some(&info.arch),
|
||||
"id" => Some(&info.id),
|
||||
"addr" => Some(&info.addr),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("");
|
||||
hasher.update(fingerprint_string.as_bytes());
|
||||
hasher.finalize()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
use std::{fmt, slice::Iter, str::FromStr};
|
||||
|
||||
use crate::protos::message::KeyboardMode;
|
||||
|
||||
impl fmt::Display for KeyboardMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
KeyboardMode::Legacy => write!(f, "legacy"),
|
||||
KeyboardMode::Map => write!(f, "map"),
|
||||
KeyboardMode::Translate => write!(f, "translate"),
|
||||
KeyboardMode::Auto => write!(f, "auto"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for KeyboardMode {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"legacy" => Ok(KeyboardMode::Legacy),
|
||||
"map" => Ok(KeyboardMode::Map),
|
||||
"translate" => Ok(KeyboardMode::Translate),
|
||||
"auto" => Ok(KeyboardMode::Auto),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardMode {
|
||||
pub fn iter() -> Iter<'static, KeyboardMode> {
|
||||
static KEYBOARD_MODES: [KeyboardMode; 4] = [
|
||||
KeyboardMode::Legacy,
|
||||
KeyboardMode::Map,
|
||||
KeyboardMode::Translate,
|
||||
KeyboardMode::Auto,
|
||||
];
|
||||
KEYBOARD_MODES.iter()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
pub mod compress;
|
||||
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 serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufRead},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
path::Path,
|
||||
time::{self, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
pub use tokio;
|
||||
pub use tokio_util;
|
||||
pub mod proxy;
|
||||
pub mod socket_client;
|
||||
pub mod tcp;
|
||||
pub mod udp;
|
||||
pub use env_logger;
|
||||
pub use log;
|
||||
pub mod bytes_codec;
|
||||
pub use anyhow::{self, bail};
|
||||
pub use futures_util;
|
||||
pub mod config;
|
||||
pub mod fs;
|
||||
pub mod mem;
|
||||
pub use lazy_static;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use mac_address;
|
||||
pub use rand;
|
||||
pub use regex;
|
||||
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;
|
||||
pub use libc;
|
||||
pub mod keyboard;
|
||||
pub use base64;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use dlopen;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use machine_uid;
|
||||
pub use serde_derive;
|
||||
pub use serde_json;
|
||||
pub use sha2;
|
||||
pub use sysinfo;
|
||||
pub use thiserror;
|
||||
pub use toml;
|
||||
pub use uuid;
|
||||
pub mod fingerprint;
|
||||
pub use flexi_logger;
|
||||
pub mod stream;
|
||||
pub mod websocket;
|
||||
#[cfg(feature = "webrtc")]
|
||||
pub mod webrtc;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub use rustls_platform_verifier;
|
||||
pub use stream::Stream;
|
||||
pub use whoami;
|
||||
pub mod tls;
|
||||
pub mod verifier;
|
||||
pub use async_recursion;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use users;
|
||||
pub use libloading;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use x11;
|
||||
|
||||
pub type SessionID = uuid::Uuid;
|
||||
|
||||
#[inline]
|
||||
pub async fn sleep(sec: f32) {
|
||||
tokio::time::sleep(time::Duration::from_secs_f32(sec)).await;
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! allow_err {
|
||||
($e:expr) => {
|
||||
if let Err(err) = $e {
|
||||
log::debug!(
|
||||
"{:?}, {}:{}:{}:{}",
|
||||
err,
|
||||
module_path!(),
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
|
||||
($e:expr, $($arg:tt)*) => {
|
||||
if let Err(err) = $e {
|
||||
log::debug!(
|
||||
"{:?}, {}, {}:{}:{}:{}",
|
||||
err,
|
||||
format_args!($($arg)*),
|
||||
module_path!(),
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn timeout<T: std::future::Future>(ms: u64, future: T) -> tokio::time::Timeout<T> {
|
||||
tokio::time::timeout(std::time::Duration::from_millis(ms), future)
|
||||
}
|
||||
|
||||
pub type ResultType<F, E = anyhow::Error> = anyhow::Result<F, E>;
|
||||
|
||||
/// Certain router and firewalls scan the packet and if they
|
||||
/// find an IP address belonging to their pool that they use to do the NAT mapping/translation, so here we mangle the ip address
|
||||
|
||||
pub struct AddrMangle();
|
||||
|
||||
#[inline]
|
||||
pub fn try_into_v4(addr: SocketAddr) -> SocketAddr {
|
||||
match addr {
|
||||
SocketAddr::V6(v6) if !addr.ip().is_loopback() => {
|
||||
if let Some(v4) = v6.ip().to_ipv4() {
|
||||
SocketAddr::new(IpAddr::V4(v4), addr.port())
|
||||
} else {
|
||||
addr
|
||||
}
|
||||
}
|
||||
_ => addr,
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrMangle {
|
||||
pub fn encode(addr: SocketAddr) -> Vec<u8> {
|
||||
// not work with [:1]:<port>
|
||||
let addr = try_into_v4(addr);
|
||||
match addr {
|
||||
SocketAddr::V4(addr_v4) => {
|
||||
let tm = (SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or(std::time::Duration::ZERO)
|
||||
.as_micros() as u32) as u128;
|
||||
let ip = u32::from_le_bytes(addr_v4.ip().octets()) as u128;
|
||||
let port = addr.port() as u128;
|
||||
let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF));
|
||||
let bytes = v.to_le_bytes();
|
||||
let mut n_padding = 0;
|
||||
for i in bytes.iter().rev() {
|
||||
if i == &0u8 {
|
||||
n_padding += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bytes[..(16 - n_padding)].to_vec()
|
||||
}
|
||||
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 {
|
||||
use std::convert::TryInto;
|
||||
|
||||
if bytes.len() > 16 {
|
||||
if bytes.len() != 18 {
|
||||
return Config::get_any_listen_addr(false);
|
||||
}
|
||||
let tmp: [u8; 2] = bytes[16..].try_into().unwrap_or_default();
|
||||
let port = u16::from_le_bytes(tmp);
|
||||
let tmp: [u8; 16] = bytes[..16].try_into().unwrap_or_default();
|
||||
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);
|
||||
let tm = (number >> 17) & (u32::max_value() as u128);
|
||||
let ip = (((number >> 49) - tm) as u32).to_le_bytes();
|
||||
let port = (number & 0xFFFFFF) - (tm & 0xFFFF);
|
||||
SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]),
|
||||
port as u16,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_version_from_url(url: &str) -> String {
|
||||
let n = url.chars().count();
|
||||
let a = url.chars().rev().position(|x| x == '-');
|
||||
if let Some(a) = a {
|
||||
let b = url.chars().rev().position(|x| x == '.');
|
||||
if let Some(b) = b {
|
||||
if a > b {
|
||||
if url
|
||||
.chars()
|
||||
.skip(n - b)
|
||||
.collect::<String>()
|
||||
.parse::<i32>()
|
||||
.is_ok()
|
||||
{
|
||||
return url.chars().skip(n - a).collect();
|
||||
} else {
|
||||
return url.chars().skip(n - a).take(a - b - 1).collect();
|
||||
}
|
||||
} else {
|
||||
return url.chars().skip(n - a).collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
pub fn gen_version() {
|
||||
println!("cargo:rerun-if-changed=Cargo.toml");
|
||||
use std::io::prelude::*;
|
||||
let mut file = File::create("./src/version.rs").unwrap();
|
||||
for line in read_lines("Cargo.toml").unwrap().flatten() {
|
||||
let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect();
|
||||
if ab.len() == 2 && ab[0] == "version" {
|
||||
file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes())
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// generate build date
|
||||
let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
|
||||
file.write_all(
|
||||
format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(),
|
||||
)
|
||||
.ok();
|
||||
file.sync_all().ok();
|
||||
}
|
||||
|
||||
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(filename)?;
|
||||
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)
|
||||
}
|
||||
|
||||
// Support 1.1.10-1, the number after - is a patch version.
|
||||
pub fn get_version_number(v: &str) -> i64 {
|
||||
let mut versions = v.split('-');
|
||||
|
||||
let mut n = 0;
|
||||
|
||||
// The first part is the version number.
|
||||
// 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10
|
||||
// to leave space for patch version.
|
||||
if let Some(v) = versions.next() {
|
||||
let mut last = 0;
|
||||
for x in v.split('.') {
|
||||
last = x.parse::<i64>().unwrap_or(0);
|
||||
n = n * 1000 + last;
|
||||
}
|
||||
n -= last;
|
||||
n += last * 10;
|
||||
}
|
||||
|
||||
if let Some(v) = versions.next() {
|
||||
n += v.parse::<i64>().unwrap_or(0);
|
||||
}
|
||||
|
||||
// Ignore the rest
|
||||
|
||||
n
|
||||
}
|
||||
|
||||
pub fn get_modified_time(path: &std::path::Path) -> SystemTime {
|
||||
std::fs::metadata(path)
|
||||
.map(|m| m.modified().unwrap_or(UNIX_EPOCH))
|
||||
.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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Known cases where machine_uid::get() may fail:
|
||||
/// - Windows shutdown: "The media is write protected. (os error 19)"
|
||||
/// - macOS (hard to reproduce, reproduced at login screen): "No matching IOPlatformUUID in `ioreg -rd1 -c IOPlatformExpertDevice` command"
|
||||
pub fn get_uuid() -> Vec<u8> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static CACHED_MACHINE_UID: std::sync::OnceLock<Vec<u8>> = std::sync::OnceLock::new();
|
||||
// Throttle only applies to the fallback machine_uid::get() log below, not the Once::call_once retry logs.
|
||||
static LOG_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
// Only macOS needs retry logic here because:
|
||||
// - macOS: in testing, only one failure occurred when reading at 50ms intervals, so retry helps
|
||||
// - Windows: failures during shutdown are persistent, retrying is pointless
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
static INIT: std::sync::Once = std::sync::Once::new();
|
||||
INIT.call_once(|| {
|
||||
// Keep in sync with upstream handling:
|
||||
// https://github.com/rustdesk/rustdesk/blob/85db6779828349b23ca3eba91cc7cd36c5337797/src/common.rs#L822
|
||||
let username = whoami::username().trim_end_matches('\0').to_owned();
|
||||
let max_retries = if username == "root" { 16 } else { 8 };
|
||||
for i in 0..max_retries {
|
||||
match machine_uid::get() {
|
||||
Ok(id) => {
|
||||
let _ = CACHED_MACHINE_UID.set(id.into());
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get machine uid in macOS retry #{i}: {e}");
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(uid) = CACHED_MACHINE_UID.get() {
|
||||
return uid.clone();
|
||||
}
|
||||
|
||||
match machine_uid::get() {
|
||||
Ok(id) => {
|
||||
let uid: Vec<u8> = id.into();
|
||||
let _ = CACHED_MACHINE_UID.set(uid.clone());
|
||||
return uid;
|
||||
}
|
||||
Err(e) => {
|
||||
if LOG_COUNT
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
|
||||
(count < 30).then_some(count + 1)
|
||||
})
|
||||
.is_ok()
|
||||
{
|
||||
log::error!("Failed to get machine uid: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 _
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_ipv4_str(id: &str) -> bool {
|
||||
if let Ok(reg) = regex::Regex::new(
|
||||
r"^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\d+)?$",
|
||||
) {
|
||||
reg.is_match(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_ipv6_str(id: &str) -> bool {
|
||||
if let Ok(reg) = 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+))$",
|
||||
) {
|
||||
reg.is_match(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_ip_str(id: &str) -> bool {
|
||||
is_ipv4_str(id) || is_ipv6_str(id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_domain_port_str(id: &str) -> bool {
|
||||
// modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname.
|
||||
// according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700,
|
||||
// there is no digits in TLD, and length is 2~63.
|
||||
if let Ok(reg) = regex::Regex::new(
|
||||
r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$",
|
||||
) {
|
||||
reg.is_match(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHandle> {
|
||||
static INIT: std::sync::Once = std::sync::Once::new();
|
||||
#[allow(unused_mut)]
|
||||
let mut logger_holder: Option<flexi_logger::LoggerHandle> = None;
|
||||
INIT.call_once(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use env_logger::*;
|
||||
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info,reqwest=warn,rustls=warn,webrtc-sctp=warn,webrtc=warn"));
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
// https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write
|
||||
// though async logger more efficient, but it also causes more problems, disable it for now
|
||||
let mut path = config::Config::log_path();
|
||||
#[cfg(target_os = "android")]
|
||||
if !config::Config::get_home().exists() {
|
||||
return;
|
||||
}
|
||||
if !_name.is_empty() {
|
||||
path.push(_name);
|
||||
}
|
||||
use flexi_logger::*;
|
||||
if let Ok(x) = Logger::try_with_env_or_str("debug,reqwest=warn,rustls=warn,webrtc-sctp=warn,webrtc=warn") {
|
||||
logger_holder = x
|
||||
.log_to_file(FileSpec::default().directory(path))
|
||||
.write_mode(if _is_async {
|
||||
WriteMode::Async
|
||||
} else {
|
||||
WriteMode::Direct
|
||||
})
|
||||
.format(opt_format)
|
||||
.rotate(
|
||||
Criterion::Age(Age::Day),
|
||||
Naming::Timestamps,
|
||||
Cleanup::KeepLogFiles(31),
|
||||
)
|
||||
.start()
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
logger_holder
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct VersionCheckRequest {
|
||||
#[serde(default)]
|
||||
pub os: String,
|
||||
#[serde(default)]
|
||||
pub os_version: String,
|
||||
#[serde(default)]
|
||||
pub arch: String,
|
||||
#[serde(default)]
|
||||
pub device_id: Vec<u8>,
|
||||
#[serde(default)]
|
||||
pub typ: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct VersionCheckResponse {
|
||||
#[serde(default)]
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
pub const VER_TYPE_RUSTDESK_CLIENT: &str = "rustdesk-client";
|
||||
pub const VER_TYPE_RUSTDESK_SERVER: &str = "rustdesk-server";
|
||||
|
||||
pub fn version_check_request(typ: String) -> (VersionCheckRequest, String) {
|
||||
const URL: &str = "https://api.rustdesk.com/version/latest";
|
||||
|
||||
use sysinfo::System;
|
||||
let system = System::new();
|
||||
let os = system.distribution_id();
|
||||
let os_version = system.os_version().unwrap_or_default();
|
||||
let arch = std::env::consts::ARCH.to_string();
|
||||
#[allow(deprecated)]
|
||||
let device_id = fingerprint::get_fingerprint(None, None);
|
||||
(
|
||||
VersionCheckRequest {
|
||||
os,
|
||||
os_version,
|
||||
arch,
|
||||
device_id,
|
||||
typ,
|
||||
},
|
||||
URL.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn time_based_rand() -> u32 {
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
|
||||
let mut x = nanos as u64;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
|
||||
(x % 32768) as u32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipv6() {
|
||||
assert!(is_ipv6_str("1:2:3"));
|
||||
assert!(is_ipv6_str("[ab:2:3]:12"));
|
||||
assert!(is_ipv6_str("[ABEF:2a:3]:12"));
|
||||
assert!(!is_ipv6_str("[ABEG:2a:3]:12"));
|
||||
assert!(!is_ipv6_str("1[ab:2:3]:12"));
|
||||
assert!(!is_ipv6_str("1.1.1.1"));
|
||||
assert!(is_ip_str("1.1.1.1"));
|
||||
assert!(!is_ipv6_str("1:2:"));
|
||||
assert!(is_ipv6_str("1:2::0"));
|
||||
assert!(is_ipv6_str("[1:2::0]:1"));
|
||||
assert!(!is_ipv6_str("[1:2::0]:"));
|
||||
assert!(!is_ipv6_str("1:2::0]:1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipv4() {
|
||||
assert!(is_ipv4_str("1.2.3.4"));
|
||||
assert!(is_ipv4_str("1.2.3.4:90"));
|
||||
assert!(is_ipv4_str("192.168.0.1"));
|
||||
assert!(is_ipv4_str("0.0.0.0"));
|
||||
assert!(is_ipv4_str("255.255.255.255"));
|
||||
assert!(!is_ipv4_str("256.0.0.0"));
|
||||
assert!(!is_ipv4_str("256.256.256.256"));
|
||||
assert!(!is_ipv4_str("1:2:"));
|
||||
assert!(!is_ipv4_str("192.168.0.256"));
|
||||
assert!(!is_ipv4_str("192.168.0.1/24"));
|
||||
assert!(!is_ipv4_str("192.168.0."));
|
||||
assert!(!is_ipv4_str("192.168..1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hostname_port() {
|
||||
assert!(!is_domain_port_str("a:12"));
|
||||
assert!(!is_domain_port_str("a.b.c:12"));
|
||||
assert!(is_domain_port_str("test.com:12"));
|
||||
assert!(is_domain_port_str("test-UPPER.com:12"));
|
||||
assert!(is_domain_port_str("some-other.domain.com:12"));
|
||||
assert!(!is_domain_port_str("under_score:12"));
|
||||
assert!(!is_domain_port_str("a@bc:12"));
|
||||
assert!(!is_domain_port_str("1.1.1.1:12"));
|
||||
assert!(!is_domain_port_str("1.2.3:12"));
|
||||
assert!(!is_domain_port_str("1.2.3.45:12"));
|
||||
assert!(!is_domain_port_str("a.b.c:123456"));
|
||||
assert!(!is_domain_port_str("---:12"));
|
||||
assert!(!is_domain_port_str(".:12"));
|
||||
// todo: should we also check for these edge cases?
|
||||
// out-of-range port
|
||||
assert!(is_domain_port_str("test.com:0"));
|
||||
assert!(is_domain_port_str("test.com:98989"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mangle2() {
|
||||
let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap();
|
||||
let addr_v4 = "127.0.0.1:8080".parse().unwrap();
|
||||
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4);
|
||||
assert_eq!(
|
||||
AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())),
|
||||
addr_v4
|
||||
);
|
||||
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4);
|
||||
let addr_v6 = "[ef::fe]:8080".parse().unwrap();
|
||||
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6);
|
||||
let addr_v6 = "[::1]:8080".parse().unwrap();
|
||||
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_version_number() {
|
||||
assert_eq!(get_version_number("1.1.10"), 1001100);
|
||||
assert_eq!(get_version_number("1.1.10-1"), 1001101);
|
||||
assert_eq!(get_version_number("1.1.11-1"), 1001111);
|
||||
assert_eq!(get_version_number("1.2.3"), 1002030);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/// SAFETY: the returned Vec must not be resized or reserverd
|
||||
pub unsafe fn aligned_u8_vec(cap: usize, align: usize) -> Vec<u8> {
|
||||
use std::alloc::*;
|
||||
|
||||
let layout =
|
||||
Layout::from_size_align(cap, align).expect("invalid aligned value, must be power of 2");
|
||||
unsafe {
|
||||
let ptr = alloc(layout);
|
||||
if ptr.is_null() {
|
||||
panic!("failed to allocate {} bytes", cap);
|
||||
}
|
||||
Vec::from_raw_parts(ptr, 0, cap)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
use crate::config::Config;
|
||||
use sodiumoxide::{base64, crypto::secretbox};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TEMPORARY_PASSWORD:Arc<RwLock<String>> = Arc::new(RwLock::new(get_auto_password()));
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum VerificationMethod {
|
||||
OnlyUseTemporaryPassword,
|
||||
OnlyUsePermanentPassword,
|
||||
UseBothPasswords,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ApproveMode {
|
||||
Both,
|
||||
Password,
|
||||
Click,
|
||||
}
|
||||
|
||||
fn get_auto_password() -> String {
|
||||
let len = temporary_password_length();
|
||||
if Config::get_bool_option(crate::config::keys::OPTION_ALLOW_NUMERNIC_ONE_TIME_PASSWORD) {
|
||||
Config::get_auto_numeric_password(len)
|
||||
} else {
|
||||
Config::get_auto_password(len)
|
||||
}
|
||||
}
|
||||
|
||||
// Should only be called in server
|
||||
pub fn update_temporary_password() {
|
||||
*TEMPORARY_PASSWORD.write().unwrap() = get_auto_password();
|
||||
}
|
||||
|
||||
// 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::has_permanent_password()
|
||||
}
|
||||
|
||||
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
|
||||
&& crate::config::option2bool("allow-hide-cm", &Config::get_option("allow-hide-cm"))
|
||||
}
|
||||
|
||||
const VERSION_LEN: usize = 2;
|
||||
const FORMAT_V1: u8 = 1;
|
||||
|
||||
// Check if data is already encrypted by verifying:
|
||||
// 1) version prefix "00"
|
||||
// 2) valid base64 payload
|
||||
// 3) decoded payload length >= secretbox::MACBYTES
|
||||
//
|
||||
// We intentionally avoid trying to decrypt here because key mismatch would cause
|
||||
// false negatives.
|
||||
// The decoded payload may be either legacy ciphertext or FORMAT_V1 || nonce || ciphertext.
|
||||
// Reference: secretbox::seal returns ciphertext length = plaintext length + MACBYTES
|
||||
// https://github.com/sodiumoxide/sodiumoxide/blob/3057acb1a030ad86ed8892a223d64036ab5e8523/src/crypto/secretbox/xsalsa20poly1305.rs#L67
|
||||
fn is_encrypted(v: &[u8]) -> bool {
|
||||
if v.len() <= VERSION_LEN || !v.starts_with(b"00") {
|
||||
return false;
|
||||
}
|
||||
match base64::decode(&v[VERSION_LEN..], base64::Variant::Original) {
|
||||
Ok(decoded) => decoded.len() >= sodiumoxide::crypto::secretbox::MACBYTES,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String {
|
||||
if is_encrypted(s.as_bytes()) {
|
||||
log::error!("Duplicate encryption!");
|
||||
return s.to_owned();
|
||||
}
|
||||
if s.chars().count() > max_len {
|
||||
return String::default();
|
||||
}
|
||||
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
|
||||
// note: s.len() return length in bytes, s.chars().count() return char count
|
||||
// &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars
|
||||
pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
|
||||
if s.len() > VERSION_LEN {
|
||||
if s.starts_with("00") {
|
||||
if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) {
|
||||
return (
|
||||
String::from_utf8_lossy(&v).to_string(),
|
||||
true,
|
||||
"00" != current_version,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For values that already look encrypted (version prefix + base64), avoid
|
||||
// repeated store on each load when decryption fails.
|
||||
(
|
||||
s.to_owned(),
|
||||
false,
|
||||
!s.is_empty() && !is_encrypted(s.as_bytes()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec<u8> {
|
||||
if is_encrypted(v) {
|
||||
log::error!("Duplicate encryption!");
|
||||
return v.to_owned();
|
||||
}
|
||||
if v.len() > max_len {
|
||||
return vec![];
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For values that already look encrypted (version prefix + base64), avoid
|
||||
// repeated store on each load when decryption fails.
|
||||
(v.to_owned(), false, !v.is_empty() && !is_encrypted(v))
|
||||
}
|
||||
|
||||
fn encrypt(v: &[u8]) -> Result<String, ()> {
|
||||
if !v.is_empty() {
|
||||
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt(v: &[u8]) -> Result<Vec<u8>, ()> {
|
||||
if !v.is_empty() {
|
||||
base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result<Vec<u8>, ()> {
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
use std::convert::TryInto;
|
||||
|
||||
let uuid = crate::get_uuid();
|
||||
let mut keybuf = uuid.clone();
|
||||
keybuf.resize(secretbox::KEYBYTES, 0);
|
||||
let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?);
|
||||
|
||||
if encrypt {
|
||||
let nonce = secretbox::gen_nonce();
|
||||
let encrypted = secretbox::seal(data, &nonce, &key);
|
||||
let mut output = Vec::with_capacity(1 + nonce.0.len() + encrypted.len());
|
||||
output.push(FORMAT_V1);
|
||||
output.extend(nonce.0);
|
||||
output.extend(encrypted);
|
||||
Ok(output)
|
||||
} else {
|
||||
let res = open_secretbox_payload(data, &key);
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if res.is_err() {
|
||||
// Fallback: try pk if uuid decryption failed (in case encryption used pk due to machine_uid failure)
|
||||
if let Some(key_pair) = Config::get_existing_key_pair() {
|
||||
let pk = key_pair.1;
|
||||
if pk != uuid {
|
||||
let mut keybuf = pk;
|
||||
keybuf.resize(secretbox::KEYBYTES, 0);
|
||||
let pk_key = secretbox::Key(keybuf.try_into().map_err(|_| ())?);
|
||||
return open_secretbox_payload(data, &pk_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
fn open_secretbox_payload(data: &[u8], key: &secretbox::Key) -> Result<Vec<u8>, ()> {
|
||||
if data.first() == Some(&FORMAT_V1)
|
||||
&& data.len() >= 1 + secretbox::NONCEBYTES + secretbox::MACBYTES
|
||||
{
|
||||
let mut nonce = [0u8; secretbox::NONCEBYTES];
|
||||
nonce.copy_from_slice(&data[1..1 + secretbox::NONCEBYTES]);
|
||||
let nonce = secretbox::Nonce(nonce);
|
||||
if let Ok(decrypted) = secretbox::open(&data[1 + secretbox::NONCEBYTES..], &nonce, key) {
|
||||
return Ok(decrypted);
|
||||
}
|
||||
}
|
||||
|
||||
let legacy_nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
|
||||
secretbox::open(data, &legacy_nonce, key)
|
||||
}
|
||||
|
||||
mod test {
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use super::*;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::time::Instant;
|
||||
|
||||
let version = "00";
|
||||
let max_len = 128;
|
||||
|
||||
println!("test str");
|
||||
let data = "1ü1111";
|
||||
let encrypted = encrypt_str_or_original(data, version, max_len);
|
||||
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!(succ);
|
||||
assert!(!store);
|
||||
let (_, _, store) = decrypt_str_or_original(&encrypted, "99");
|
||||
assert!(store);
|
||||
assert!(!decrypt_str_or_original(&decrypted, version).1);
|
||||
assert_eq!(
|
||||
encrypt_str_or_original(&encrypted, version, max_len),
|
||||
encrypted
|
||||
);
|
||||
|
||||
println!("test vec");
|
||||
let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
|
||||
let encrypted = encrypt_vec_or_original(&data, version, max_len);
|
||||
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!(!store);
|
||||
assert!(succ);
|
||||
let (_, _, store) = decrypt_vec_or_original(&encrypted, "99");
|
||||
assert!(store);
|
||||
assert!(!decrypt_vec_or_original(&decrypted, version).1);
|
||||
assert_eq!(
|
||||
encrypt_vec_or_original(&encrypted, version, max_len),
|
||||
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!(store);
|
||||
assert!(!succ);
|
||||
let verbytes = version.as_bytes();
|
||||
let data: Vec<u8> = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6];
|
||||
let (decrypted, succ, store) = decrypt_vec_or_original(&data, version);
|
||||
assert_eq!(data, decrypted);
|
||||
assert!(store);
|
||||
assert!(!succ);
|
||||
let (_, succ, store) = decrypt_str_or_original("", version);
|
||||
assert!(!store);
|
||||
assert!(!succ);
|
||||
let (_, succ, store) = decrypt_vec_or_original(&[], version);
|
||||
assert!(!store);
|
||||
assert!(!succ);
|
||||
let data = "1ü1111";
|
||||
assert_eq!(decrypt_str_or_original(data, version).0, data);
|
||||
let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
|
||||
assert_eq!(decrypt_vec_or_original(&data, version).0, data);
|
||||
|
||||
// Base64-shaped "00" prefixed values shorter than MACBYTES are treated
|
||||
// as original/plain values and should be stored.
|
||||
let data = "00YWJjZA==";
|
||||
let (decrypted, succ, store) = decrypt_str_or_original(data, version);
|
||||
assert_eq!(decrypted, data);
|
||||
assert!(!succ);
|
||||
assert!(store);
|
||||
let data = b"00YWJjZA==".to_vec();
|
||||
let (decrypted, succ, store) = decrypt_vec_or_original(&data, version);
|
||||
assert_eq!(decrypted, data);
|
||||
assert!(!succ);
|
||||
assert!(store);
|
||||
|
||||
// When decoded length reaches MACBYTES, it is treated as encrypted-like
|
||||
// and should not trigger repeated store.
|
||||
let exact_mac = vec![0u8; sodiumoxide::crypto::secretbox::MACBYTES];
|
||||
let exact_mac_b64 =
|
||||
sodiumoxide::base64::encode(&exact_mac, sodiumoxide::base64::Variant::Original);
|
||||
let data = format!("00{exact_mac_b64}");
|
||||
let (_, succ, store) = decrypt_str_or_original(&data, version);
|
||||
assert!(!succ);
|
||||
assert!(!store);
|
||||
let data = data.into_bytes();
|
||||
let (_, succ, store) = decrypt_vec_or_original(&data, version);
|
||||
assert!(!succ);
|
||||
assert!(!store);
|
||||
|
||||
println!("test speed");
|
||||
let test_speed = |len: usize, name: &str| {
|
||||
let mut data: Vec<u8> = vec![];
|
||||
let mut rng = thread_rng();
|
||||
for _ in 0..len {
|
||||
data.push(rng.gen_range(0..255));
|
||||
}
|
||||
let start: Instant = Instant::now();
|
||||
let encrypted = encrypt_vec_or_original(&data, version, len);
|
||||
assert_ne!(data, decrypted);
|
||||
let t1 = start.elapsed();
|
||||
let start = Instant::now();
|
||||
let (decrypted, _, _) = decrypt_vec_or_original(&encrypted, version);
|
||||
let t2 = start.elapsed();
|
||||
assert_eq!(data, decrypted);
|
||||
println!("{name}");
|
||||
println!("encrypt:{:?}, decrypt:{:?}", t1, t2);
|
||||
|
||||
let start: Instant = Instant::now();
|
||||
let encrypted = base64::encode(&data, base64::Variant::Original);
|
||||
let t1 = start.elapsed();
|
||||
let start = Instant::now();
|
||||
let decrypted = base64::decode(&encrypted, base64::Variant::Original).unwrap();
|
||||
let t2 = start.elapsed();
|
||||
assert_eq!(data, decrypted);
|
||||
println!("base64, encrypt:{:?}, decrypt:{:?}", t1, t2,);
|
||||
};
|
||||
test_speed(128, "128");
|
||||
test_speed(1024, "1k");
|
||||
test_speed(1024 * 1024, "1M");
|
||||
test_speed(10 * 1024 * 1024, "10M");
|
||||
test_speed(100 * 1024 * 1024, "100M");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_encrypted() {
|
||||
use super::*;
|
||||
use sodiumoxide::base64::{encode, Variant};
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
|
||||
// Empty data should not be considered encrypted
|
||||
assert!(!is_encrypted(b""));
|
||||
assert!(!is_encrypted(b"0"));
|
||||
assert!(!is_encrypted(b"00"));
|
||||
|
||||
// Data without "00" prefix should not be considered encrypted
|
||||
assert!(!is_encrypted(b"01abcd"));
|
||||
assert!(!is_encrypted(b"99abcd"));
|
||||
assert!(!is_encrypted(b"hello world"));
|
||||
|
||||
// Data with "00" prefix but invalid base64 should not be considered encrypted
|
||||
assert!(!is_encrypted(b"00!!!invalid base64!!!"));
|
||||
assert!(!is_encrypted(b"00@#$%"));
|
||||
|
||||
// Data with "00" prefix and valid base64 but shorter than MACBYTES is not encrypted
|
||||
assert!(!is_encrypted(b"00YWJjZA==")); // "abcd" in base64
|
||||
assert!(!is_encrypted(b"00SGVsbG8gV29ybGQ=")); // "Hello World" in base64
|
||||
|
||||
// Data with "00" prefix and valid base64 with decoded len == MACBYTES is considered encrypted
|
||||
let exact_mac = vec![0u8; secretbox::MACBYTES];
|
||||
let exact_mac_b64 = encode(&exact_mac, Variant::Original);
|
||||
let exact_mac_candidate = format!("00{exact_mac_b64}");
|
||||
assert!(is_encrypted(exact_mac_candidate.as_bytes()));
|
||||
|
||||
// Real encrypted data should be detected
|
||||
let version = "00";
|
||||
let max_len = 128;
|
||||
let encrypted_str = encrypt_str_or_original("1", version, max_len);
|
||||
assert!(is_encrypted(encrypted_str.as_bytes()));
|
||||
let encrypted_vec = encrypt_vec_or_original(b"1", version, max_len);
|
||||
assert!(is_encrypted(&encrypted_vec));
|
||||
|
||||
// Original unencrypted data should not be detected as encrypted
|
||||
assert!(!is_encrypted(b"1"));
|
||||
assert!(!is_encrypted("1".as_bytes()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_payload_min_len_macbytes() {
|
||||
use super::*;
|
||||
use sodiumoxide::base64::{decode, Variant};
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
|
||||
let version = "00";
|
||||
let max_len = 128;
|
||||
|
||||
let encrypted_str = encrypt_str_or_original("1", version, max_len);
|
||||
let decoded = decode(&encrypted_str.as_bytes()[VERSION_LEN..], Variant::Original).unwrap();
|
||||
assert!(
|
||||
decoded.len() >= secretbox::MACBYTES,
|
||||
"decoded encrypted payload must be at least MACBYTES"
|
||||
);
|
||||
|
||||
let encrypted_vec = encrypt_vec_or_original(b"1", version, max_len);
|
||||
let decoded = decode(&encrypted_vec[VERSION_LEN..], Variant::Original).unwrap();
|
||||
assert!(
|
||||
decoded.len() >= secretbox::MACBYTES,
|
||||
"decoded encrypted payload must be at least MACBYTES"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption_uses_random_nonce() {
|
||||
use super::*;
|
||||
|
||||
let data = b"test password 123";
|
||||
let encrypted1 = symmetric_crypt(data, true).unwrap();
|
||||
let encrypted2 = symmetric_crypt(data, true).unwrap();
|
||||
|
||||
assert_eq!(encrypted1.first(), Some(&FORMAT_V1));
|
||||
assert_eq!(encrypted2.first(), Some(&FORMAT_V1));
|
||||
assert_eq!(
|
||||
encrypted1.len(),
|
||||
1 + secretbox::NONCEBYTES + data.len() + secretbox::MACBYTES
|
||||
);
|
||||
assert_ne!(encrypted1, encrypted2);
|
||||
assert_eq!(symmetric_crypt(&encrypted1, false).unwrap(), data);
|
||||
assert_eq!(symmetric_crypt(&encrypted2, false).unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_legacy_zero_nonce_payload() {
|
||||
use super::*;
|
||||
use std::convert::TryInto;
|
||||
|
||||
let data = b"test password 123";
|
||||
let uuid = crate::get_uuid();
|
||||
let mut keybuf = uuid.clone();
|
||||
keybuf.resize(secretbox::KEYBYTES, 0);
|
||||
let key = secretbox::Key(keybuf.try_into().unwrap());
|
||||
let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
|
||||
let encrypted = secretbox::seal(data, &nonce, &key);
|
||||
|
||||
assert_eq!(symmetric_crypt(&encrypted, false).unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_legacy_payload_starting_with_v1_marker() {
|
||||
use super::*;
|
||||
use std::convert::TryInto;
|
||||
|
||||
let mut keybuf = crate::get_uuid();
|
||||
keybuf.resize(secretbox::KEYBYTES, 0);
|
||||
let key = secretbox::Key(keybuf.try_into().unwrap());
|
||||
let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
|
||||
|
||||
for i in 0..=u16::MAX {
|
||||
let data = format!("legacy collision payload {i:05}");
|
||||
let encrypted = secretbox::seal(data.as_bytes(), &nonce, &key);
|
||||
if encrypted.first() == Some(&FORMAT_V1) {
|
||||
assert_eq!(symmetric_crypt(&encrypted, false).unwrap(), data.as_bytes());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("failed to find legacy payload starting with FORMAT_V1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_short_v1_payload_returns_error() {
|
||||
use super::*;
|
||||
|
||||
let encrypted = vec![FORMAT_V1];
|
||||
|
||||
assert!(symmetric_crypt(&encrypted, false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_legacy_string_does_not_request_store() {
|
||||
use super::*;
|
||||
use sodiumoxide::base64::{encode, Variant};
|
||||
use std::convert::TryInto;
|
||||
|
||||
let data = "test password 123";
|
||||
let uuid = crate::get_uuid();
|
||||
let mut keybuf = uuid.clone();
|
||||
keybuf.resize(secretbox::KEYBYTES, 0);
|
||||
let key = secretbox::Key(keybuf.try_into().unwrap());
|
||||
let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
|
||||
let encrypted = secretbox::seal(data.as_bytes(), &nonce, &key);
|
||||
let encrypted = "00".to_owned() + &encode(encrypted, Variant::Original);
|
||||
|
||||
let (decrypted, success, store) = decrypt_str_or_original(&encrypted, "00");
|
||||
|
||||
assert_eq!(decrypted, data);
|
||||
assert!(success);
|
||||
assert!(!store);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_legacy_vec_does_not_request_store() {
|
||||
use super::*;
|
||||
use sodiumoxide::base64::{encode, Variant};
|
||||
use std::convert::TryInto;
|
||||
|
||||
let data = b"test password 123";
|
||||
let uuid = crate::get_uuid();
|
||||
let mut keybuf = uuid.clone();
|
||||
keybuf.resize(secretbox::KEYBYTES, 0);
|
||||
let key = secretbox::Key(keybuf.try_into().unwrap());
|
||||
let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
|
||||
let encrypted = secretbox::seal(data, &nonce, &key);
|
||||
let encrypted = ("00".to_owned() + &encode(encrypted, Variant::Original)).into_bytes();
|
||||
|
||||
let (decrypted, success, store) = decrypt_vec_or_original(&encrypted, "00");
|
||||
|
||||
assert_eq!(decrypted, data);
|
||||
assert!(success);
|
||||
assert!(!store);
|
||||
}
|
||||
|
||||
// Test decryption fallback when data was encrypted with key_pair but decryption tries machine_uid first
|
||||
#[test]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn test_decrypt_with_pk_fallback() {
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
use std::convert::TryInto;
|
||||
|
||||
let uuid = crate::get_uuid();
|
||||
let pk = crate::config::Config::get_key_pair().1;
|
||||
|
||||
// Ensure uuid != pk, otherwise fallback branch won't be tested
|
||||
if uuid == pk {
|
||||
eprintln!("skip: uuid == pk, fallback branch won't be tested");
|
||||
return;
|
||||
}
|
||||
|
||||
let data = b"test password 123";
|
||||
let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
|
||||
|
||||
// Encrypt with pk (simulating machine_uid failure during encryption)
|
||||
let mut pk_keybuf = pk;
|
||||
pk_keybuf.resize(secretbox::KEYBYTES, 0);
|
||||
let pk_key = secretbox::Key(pk_keybuf.try_into().unwrap());
|
||||
let encrypted = secretbox::seal(data, &nonce, &pk_key);
|
||||
|
||||
// Decrypt using symmetric_crypt (should fallback to pk since uuid differs)
|
||||
let decrypted = super::symmetric_crypt(&encrypted, false);
|
||||
assert!(
|
||||
decrypted.is_ok(),
|
||||
"Decryption with pk fallback should succeed"
|
||||
);
|
||||
assert_eq!(decrypted.unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn test_decrypt_v1_with_pk_fallback() {
|
||||
use super::*;
|
||||
use sodiumoxide::base64::{encode, Variant};
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
use std::convert::TryInto;
|
||||
|
||||
let uuid = crate::get_uuid();
|
||||
let pk = crate::config::Config::get_key_pair().1;
|
||||
|
||||
if uuid == pk {
|
||||
eprintln!("skip: uuid == pk, fallback branch won't be tested");
|
||||
return;
|
||||
}
|
||||
|
||||
let data = b"test password 123";
|
||||
let nonce = secretbox::gen_nonce();
|
||||
|
||||
let mut pk_keybuf = pk;
|
||||
pk_keybuf.resize(secretbox::KEYBYTES, 0);
|
||||
let pk_key = secretbox::Key(pk_keybuf.try_into().unwrap());
|
||||
let ciphertext = secretbox::seal(data, &nonce, &pk_key);
|
||||
|
||||
let mut encrypted = Vec::with_capacity(1 + secretbox::NONCEBYTES + ciphertext.len());
|
||||
encrypted.push(FORMAT_V1);
|
||||
encrypted.extend(nonce.0);
|
||||
encrypted.extend(ciphertext);
|
||||
|
||||
assert_eq!(super::symmetric_crypt(&encrypted, false).unwrap(), data);
|
||||
|
||||
let encrypted_str = "00".to_owned() + &encode(&encrypted, Variant::Original);
|
||||
let (decrypted, success, store) = decrypt_str_or_original(&encrypted_str, "00");
|
||||
assert_eq!(decrypted.as_bytes(), data);
|
||||
assert!(success);
|
||||
assert!(!store);
|
||||
|
||||
let encrypted_vec = encrypted_str.into_bytes();
|
||||
let (decrypted, success, store) = decrypt_vec_or_original(&encrypted_vec, "00");
|
||||
assert_eq!(decrypted, data);
|
||||
assert!(success);
|
||||
assert!(!store);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
use crate::ResultType;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use users::{get_current_uid, get_user_by_uid, os::unix::UserExt};
|
||||
|
||||
use sctk::{
|
||||
output::OutputData,
|
||||
output::{OutputHandler, OutputState},
|
||||
reexports::client::protocol::wl_output::WlOutput,
|
||||
reexports::client::{globals, Proxy},
|
||||
reexports::client::{Connection, QueueHandle},
|
||||
registry::{ProvidesRegistryState, RegistryState},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref DISTRO: Distro = Distro::new();
|
||||
}
|
||||
|
||||
// to-do: There seems to be some runtime issue that causes the audit logs to be generated.
|
||||
// We may need to fix this and remove this workaround in the future.
|
||||
//
|
||||
// We use the pre-search method to find the command path to avoid the audit logs on some systems.
|
||||
// No idea why the audit logs happen.
|
||||
// Though the audit logs may disappear after rebooting.
|
||||
//
|
||||
// See https://github.com/rustdesk/rustdesk/discussions/11959
|
||||
//
|
||||
// `ausearch -x /usr/share/rustdesk/rustdesk` will return
|
||||
// ...
|
||||
// time->Tue Jun 24 10:40:43 2025
|
||||
// type=PROCTITLE msg=audit(1750776043.446:192757): proctitle=2F7573722F62696E2F727573746465736B002D2D73657276696365
|
||||
// type=PATH msg=audit(1750776043.446:192757): item=0 name="/usr/local/bin/sh" nametype=UNKNOWN cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0
|
||||
// type=CWD msg=audit(1750776043.446:192757): cwd="/"
|
||||
// type=SYSCALL msg=audit(1750776043.446:192757): arch=c000003e syscall=59 success=no exit=-2 a0=7fb7dbd22da0 a1=1d65f2c0 a2=7ffc25193360 a3=7ffc25194ec0 items=1 ppid=172208 pid=267565 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="rustdesk" exe="/usr/share/rustdesk/rustdesk" subj=unconfined key="processos_criados"
|
||||
// ----
|
||||
// time->Tue Jun 24 10:40:43 2025
|
||||
// type=PROCTITLE msg=audit(1750776043.446:192758): proctitle=2F7573722F62696E2F727573746465736B002D2D73657276696365
|
||||
// type=PATH msg=audit(1750776043.446:192758): item=0 name="/usr/sbin/sh" nametype=UNKNOWN cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0
|
||||
// ...
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CMD_LOGINCTL: String = find_cmd_path("loginctl");
|
||||
pub static ref CMD_PS: String = find_cmd_path("ps");
|
||||
pub static ref CMD_SH: String = find_cmd_path("sh");
|
||||
}
|
||||
|
||||
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
|
||||
pub const DISPLAY_SERVER_X11: &str = "x11";
|
||||
pub const DISPLAY_DESKTOP_KDE: &str = "KDE";
|
||||
|
||||
pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP";
|
||||
|
||||
pub struct Distro {
|
||||
pub name: String,
|
||||
pub version_id: String,
|
||||
}
|
||||
|
||||
impl Distro {
|
||||
fn new() -> Self {
|
||||
let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
Self { name, version_id }
|
||||
}
|
||||
}
|
||||
|
||||
fn find_cmd_path(cmd: &'static str) -> String {
|
||||
let test_cmd = format!("/bin/{}", cmd);
|
||||
if std::path::Path::new(&test_cmd).exists() {
|
||||
return test_cmd;
|
||||
}
|
||||
let test_cmd = format!("/usr/bin/{}", cmd);
|
||||
if std::path::Path::new(&test_cmd).exists() {
|
||||
return test_cmd;
|
||||
}
|
||||
if let Ok(output) = Command::new("which").arg(cmd).output() {
|
||||
if output.status.success() {
|
||||
return String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
}
|
||||
}
|
||||
cmd.to_string()
|
||||
}
|
||||
|
||||
// Deprecated. Use `hbb_common::platform::linux::is_kde_session()` instead for now.
|
||||
// Or we need to set the correct environment variable in the server process.
|
||||
#[inline]
|
||||
pub fn is_kde() -> bool {
|
||||
if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) {
|
||||
env == DISPLAY_DESKTOP_KDE
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use `hbb_common::platform::linux::is_kde()` here.
|
||||
// It's not correct in the server process.
|
||||
pub fn is_kde_session() -> bool {
|
||||
std::process::Command::new(CMD_SH.as_str())
|
||||
.arg("-c")
|
||||
.arg("pgrep -f kded[0-9]+")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.output()
|
||||
.map(|o| !o.stdout.is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_gdm_user(username: &str) -> bool {
|
||||
username == "gdm" || username == "sddm"
|
||||
// || username == "lightgdm"
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_desktop_wayland() -> bool {
|
||||
get_display_server() == DISPLAY_SERVER_WAYLAND
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_x11_or_headless() -> bool {
|
||||
!is_desktop_wayland()
|
||||
}
|
||||
|
||||
// -1
|
||||
const INVALID_SESSION: &str = "4294967295";
|
||||
|
||||
pub fn get_display_server() -> String {
|
||||
// Check for forced display server environment variable first
|
||||
if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") {
|
||||
return forced_display;
|
||||
}
|
||||
|
||||
// Check if `loginctl` can be called successfully
|
||||
if run_loginctl(None).is_err() {
|
||||
return DISPLAY_SERVER_X11.to_owned();
|
||||
}
|
||||
|
||||
let mut session = get_values_of_seat0(&[0])[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;
|
||||
}
|
||||
if session.is_empty() {
|
||||
session = run_cmds("cat /proc/self/sessionid").unwrap_or_default();
|
||||
if session == INVALID_SESSION {
|
||||
session = "".to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
if session.is_empty() {
|
||||
std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned())
|
||||
} else {
|
||||
get_display_server_of_session(&session)
|
||||
}
|
||||
}
|
||||
|
||||
pub 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
|
||||
{
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.replace("Type=", "")
|
||||
.trim_end()
|
||||
.into()
|
||||
} else {
|
||||
"".to_owned()
|
||||
};
|
||||
if display_server.is_empty() || display_server == "tty" || display_server == "unspecified" {
|
||||
if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
|
||||
if !sestype.is_empty() {
|
||||
return sestype.to_lowercase();
|
||||
}
|
||||
}
|
||||
display_server = "x11".to_owned();
|
||||
}
|
||||
display_server.to_lowercase()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn line_values(indices: &[usize], line: &str) -> Vec<String> {
|
||||
indices
|
||||
.into_iter()
|
||||
.map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_values_of_seat0(indices: &[usize]) -> Vec<String> {
|
||||
_get_values_of_seat0(indices, true)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec<String> {
|
||||
_get_values_of_seat0(indices, false)
|
||||
}
|
||||
|
||||
// Ignore "3 sessions listed."
|
||||
fn ignore_loginctl_line(line: &str) -> bool {
|
||||
line.contains("sessions") || line.split(" ").count() < 4
|
||||
}
|
||||
|
||||
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
|
||||
if let Ok(output) = run_loginctl(None) {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if ignore_loginctl_line(line) {
|
||||
continue;
|
||||
}
|
||||
if line.contains("seat0") {
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
if is_active(sid) {
|
||||
if ignore_gdm_wayland {
|
||||
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
|
||||
&& get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return line_values(indices, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if ignore_loginctl_line(line) {
|
||||
continue;
|
||||
}
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
if is_active(sid) {
|
||||
let d = get_display_server_of_session(sid);
|
||||
if ignore_gdm_wayland {
|
||||
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
|
||||
&& d == DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if d == "tty" || d == "unspecified" {
|
||||
continue;
|
||||
}
|
||||
return line_values(indices, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line_values(indices, "")
|
||||
}
|
||||
|
||||
pub 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 is_active_and_seat0(sid: &str) -> bool {
|
||||
if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) {
|
||||
String::from_utf8_lossy(&output.stdout).contains("State=active")
|
||||
&& String::from_utf8_lossy(&output.stdout).contains("Seat=seat0")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Check both "Lock" and "Switch user"
|
||||
pub fn is_session_locked(sid: &str) -> bool {
|
||||
if let Ok(output) = run_loginctl(Some(vec!["show-session", sid, "--property=LockedHint"])) {
|
||||
String::from_utf8_lossy(&output.stdout).contains("LockedHint=yes")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// **Note** that the return value here, the last character is '\n'.
|
||||
// Use `run_cmds_trim_newline()` if you want to remove '\n' at the end.
|
||||
pub fn run_cmds(cmds: &str) -> ResultType<String> {
|
||||
let output = std::process::Command::new(CMD_SH.as_str())
|
||||
.args(vec!["-c", cmds])
|
||||
.output()?;
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
pub fn run_cmds_trim_newline(cmds: &str) -> ResultType<String> {
|
||||
let output = std::process::Command::new(CMD_SH.as_str())
|
||||
.args(vec!["-c", cmds])
|
||||
.output()?;
|
||||
let out = String::from_utf8_lossy(&output.stdout);
|
||||
Ok(if out.ends_with('\n') {
|
||||
out[..out.len() - 1].to_string()
|
||||
} else {
|
||||
out.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
|
||||
if std::env::var("FLATPAK_ID").is_ok() {
|
||||
let mut l_args = CMD_LOGINCTL.to_string();
|
||||
if let Some(a) = args.as_ref() {
|
||||
l_args = format!("{} {}", l_args, a.join(" "));
|
||||
}
|
||||
let res = std::process::Command::new("flatpak-spawn")
|
||||
.args(vec![String::from("--host"), l_args])
|
||||
.output();
|
||||
if res.is_ok() {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
let mut cmd = std::process::Command::new(CMD_LOGINCTL.as_str());
|
||||
if let Some(a) = args {
|
||||
return cmd.args(a).output();
|
||||
}
|
||||
cmd.output()
|
||||
}
|
||||
|
||||
/// forever: may not work
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> {
|
||||
let cmds: HashMap<&str, Vec<&str>> = HashMap::from([
|
||||
("notify-send", [title, msg].to_vec()),
|
||||
(
|
||||
"zenity",
|
||||
[
|
||||
"--info",
|
||||
"--timeout",
|
||||
if forever { "0" } else { "3" },
|
||||
"--title",
|
||||
title,
|
||||
"--text",
|
||||
msg,
|
||||
]
|
||||
.to_vec(),
|
||||
),
|
||||
("kdialog", ["--title", title, "--msgbox", msg].to_vec()),
|
||||
(
|
||||
"xmessage",
|
||||
[
|
||||
"-center",
|
||||
"-timeout",
|
||||
if forever { "0" } else { "3" },
|
||||
title,
|
||||
msg,
|
||||
]
|
||||
.to_vec(),
|
||||
),
|
||||
]);
|
||||
for (k, v) in cmds {
|
||||
if Command::new(k).args(v).spawn().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
crate::bail!("failed to post system message");
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WaylandDisplayInfo {
|
||||
pub name: String,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub logical_size: Option<(i32, i32)>,
|
||||
pub refresh_rate: i32,
|
||||
}
|
||||
|
||||
// Retrieves information about all connected displays via the Wayland protocol.
|
||||
pub fn get_wayland_displays() -> ResultType<Vec<WaylandDisplayInfo>> {
|
||||
struct WaylandEnv {
|
||||
registry_state: RegistryState,
|
||||
output_state: OutputState,
|
||||
}
|
||||
|
||||
impl OutputHandler for WaylandEnv {
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
|
||||
fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput) {}
|
||||
fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput) {}
|
||||
fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput) {}
|
||||
}
|
||||
|
||||
impl ProvidesRegistryState for WaylandEnv {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
|
||||
sctk::registry_handlers!();
|
||||
}
|
||||
|
||||
sctk::delegate_output!(WaylandEnv);
|
||||
sctk::delegate_registry!(WaylandEnv);
|
||||
|
||||
let conn = Connection::connect_to_env()?;
|
||||
let (globals, mut event_queue) = globals::registry_queue_init(&conn)?;
|
||||
let queue_handle = event_queue.handle();
|
||||
|
||||
let registry_state = RegistryState::new(&globals);
|
||||
let output_state = OutputState::new(&globals, &queue_handle);
|
||||
|
||||
let mut environment = WaylandEnv {
|
||||
registry_state,
|
||||
output_state,
|
||||
};
|
||||
|
||||
event_queue.roundtrip(&mut environment)?;
|
||||
|
||||
let outputs: Vec<_> = environment.output_state.outputs().collect();
|
||||
let mut display_infos = Vec::new();
|
||||
|
||||
for output in outputs {
|
||||
if let Some(output_data) = output.data::<OutputData>() {
|
||||
output_data.with_output_info(|info| {
|
||||
if let Some(mode) = info.modes.iter().find(|m| m.current) {
|
||||
let (x, y) = info.location;
|
||||
let (width, height) = mode.dimensions;
|
||||
let refresh_rate = mode.refresh_rate;
|
||||
let name = info.name.clone().unwrap_or_default();
|
||||
let logical_size = info.logical_size;
|
||||
display_infos.push(WaylandDisplayInfo {
|
||||
name,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
logical_size,
|
||||
refresh_rate,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(display_infos)
|
||||
}
|
||||
|
||||
/// Escape a string for safe use in shell commands by wrapping in single quotes.
|
||||
///
|
||||
/// This function handles the edge case of single quotes within the string by:
|
||||
/// 1. Ending the current single-quoted section
|
||||
/// 2. Adding an escaped single quote
|
||||
/// 3. Starting a new single-quoted section
|
||||
///
|
||||
/// Example: "it's here" -> "'it'\''s here'"
|
||||
#[inline]
|
||||
pub fn shell_quote(s: &str) -> String {
|
||||
format!("'{}'", s.replace("'", "'\\''"))
|
||||
}
|
||||
|
||||
/// Get the current user's home directory via getpwuid (trusted source).
|
||||
///
|
||||
/// This function uses the system's password database (via `getpwuid`) to retrieve
|
||||
/// the home directory, avoiding the security risk of relying on the `HOME`
|
||||
/// environment variable which can be manipulated by untrusted input.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(PathBuf)` if the home directory was found and exists
|
||||
/// - `None` if the user lookup failed or the directory doesn't exist
|
||||
///
|
||||
/// # Security
|
||||
/// This function is designed to be safe against confused-deputy attacks where
|
||||
/// an attacker might manipulate environment variables to influence privileged
|
||||
/// operations.
|
||||
pub fn get_home_dir_trusted() -> Option<PathBuf> {
|
||||
let uid = get_current_uid();
|
||||
match get_user_by_uid(uid) {
|
||||
Some(user) => {
|
||||
let home = user.home_dir();
|
||||
if Path::is_dir(home) {
|
||||
Some(PathBuf::from(home))
|
||||
} else {
|
||||
log::warn!(
|
||||
"Home directory for uid {} does not exist or is not a directory: {:?}",
|
||||
uid,
|
||||
home
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::warn!("Failed to get user info for uid {}", uid);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_run_cmds_trim_newline() {
|
||||
assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123");
|
||||
assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123");
|
||||
assert_eq!(
|
||||
run_cmds_trim_newline("whoami").unwrap() + "\n",
|
||||
run_cmds("whoami").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
/// Test get_home_dir_trusted: returns valid path and ignores HOME env var
|
||||
#[test]
|
||||
fn test_get_home_dir_trusted() {
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
|
||||
// Set HOME to a fake/malicious path
|
||||
std::env::set_var("HOME", "/tmp/fake_malicious_home");
|
||||
let result = get_home_dir_trusted();
|
||||
|
||||
// Restore original HOME
|
||||
match original_home {
|
||||
Some(home) => std::env::set_var("HOME", home),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
|
||||
// Verify: returns valid path that is NOT the fake HOME
|
||||
if let Some(path) = result {
|
||||
assert!(path.is_absolute(), "Path should be absolute: {:?}", path);
|
||||
assert!(path.is_dir(), "Path should be a directory: {:?}", path);
|
||||
assert_ne!(
|
||||
path.to_string_lossy(),
|
||||
"/tmp/fake_malicious_home",
|
||||
"Should not use HOME env var"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test shell_quote with normal strings
|
||||
#[test]
|
||||
fn test_shell_quote_normal() {
|
||||
assert_eq!(shell_quote("hello"), "'hello'");
|
||||
assert_eq!(shell_quote("/home/user"), "'/home/user'");
|
||||
}
|
||||
|
||||
/// Test shell_quote with spaces
|
||||
#[test]
|
||||
fn test_shell_quote_spaces() {
|
||||
assert_eq!(shell_quote("/home/my user/file"), "'/home/my user/file'");
|
||||
assert_eq!(shell_quote("path with spaces"), "'path with spaces'");
|
||||
}
|
||||
|
||||
/// Test shell_quote with single quotes (the tricky case)
|
||||
#[test]
|
||||
fn test_shell_quote_single_quotes() {
|
||||
assert_eq!(shell_quote("it's"), "'it'\\''s'");
|
||||
assert_eq!(shell_quote("don't stop"), "'don'\\''t stop'");
|
||||
}
|
||||
|
||||
/// Test shell_quote with shell metacharacters
|
||||
#[test]
|
||||
fn test_shell_quote_metacharacters() {
|
||||
// These should all be safely quoted
|
||||
assert_eq!(shell_quote("test;rm -rf /"), "'test;rm -rf /'");
|
||||
assert_eq!(shell_quote("$(whoami)"), "'$(whoami)'");
|
||||
assert_eq!(shell_quote("`id`"), "'`id`'");
|
||||
assert_eq!(shell_quote("a && b"), "'a && b'");
|
||||
assert_eq!(shell_quote("a | b"), "'a | b'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use crate::ResultType;
|
||||
use osascript;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AlertParams {
|
||||
title: String,
|
||||
message: String,
|
||||
alert_type: String,
|
||||
buttons: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AlertResult {
|
||||
#[serde(rename = "buttonReturned")]
|
||||
button: String,
|
||||
}
|
||||
|
||||
/// Firstly run the specified app, then alert a dialog. Return the clicked button value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `app` - The app to execute the script.
|
||||
/// * `alert_type` - Alert type. . informational, warning, critical
|
||||
/// * `title` - The alert title.
|
||||
/// * `message` - The alert message.
|
||||
/// * `buttons` - The buttons to show.
|
||||
pub fn alert(
|
||||
app: String,
|
||||
alert_type: String,
|
||||
title: String,
|
||||
message: String,
|
||||
buttons: Vec<String>,
|
||||
) -> ResultType<String> {
|
||||
let script = osascript::JavaScript::new(&format!(
|
||||
"
|
||||
var App = Application('{}');
|
||||
App.includeStandardAdditions = true;
|
||||
return App.displayAlert($params.title, {{
|
||||
message: $params.message,
|
||||
'as': $params.alert_type,
|
||||
buttons: $params.buttons,
|
||||
}});
|
||||
",
|
||||
app
|
||||
));
|
||||
|
||||
let result: AlertResult = script.execute_with_params(AlertParams {
|
||||
title,
|
||||
message,
|
||||
alert_type,
|
||||
buttons,
|
||||
})?;
|
||||
Ok(result.button)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use crate::{config::Config, log};
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::process::exit;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
static mut GLOBAL_CALLBACK: Option<Box<dyn Fn()>> = None;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
extern "C" fn breakdown_signal_handler(sig: i32) {
|
||||
let mut stack = vec![];
|
||||
backtrace::trace(|frame| {
|
||||
backtrace::resolve_frame(frame, |symbol| {
|
||||
if let Some(name) = symbol.name() {
|
||||
stack.push(name.to_string());
|
||||
}
|
||||
});
|
||||
true // keep going to the next frame
|
||||
});
|
||||
let mut info = String::default();
|
||||
if stack.iter().any(|s| {
|
||||
s.contains(&"nouveau_pushbuf_kick")
|
||||
|| s.to_lowercase().contains("nvidia")
|
||||
|| s.contains("gdk_window_end_draw_frame")
|
||||
|| s.contains("glGetString")
|
||||
}) {
|
||||
Config::set_option("allow-always-software-render".to_string(), "Y".to_string());
|
||||
info = "Always use software rendering will be set.".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
if stack.iter().any(|s| {
|
||||
s.to_lowercase().contains("nvidia")
|
||||
|| s.to_lowercase().contains("amf")
|
||||
|| s.to_lowercase().contains("mfx")
|
||||
|| s.contains("cuProfilerStop")
|
||||
}) {
|
||||
Config::set_option("enable-hwcodec".to_string(), "N".to_string());
|
||||
info = "Perhaps hwcodec causing the crash, disable it first".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
log::error!(
|
||||
"Got signal {} and exit. stack:\n{}",
|
||||
sig,
|
||||
stack.join("\n").to_string()
|
||||
);
|
||||
if !info.is_empty() {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::system_message(
|
||||
"RustDesk",
|
||||
&format!("Got signal {} and exit.{}", sig, info),
|
||||
true,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
unsafe {
|
||||
#[allow(static_mut_refs)]
|
||||
if let Some(callback) = &GLOBAL_CALLBACK {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub fn register_breakdown_handler<T>(callback: T)
|
||||
where
|
||||
T: Fn() + 'static,
|
||||
{
|
||||
unsafe {
|
||||
GLOBAL_CALLBACK = Some(Box::new(callback));
|
||||
libc::signal(libc::SIGSEGV, breakdown_signal_handler as _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
use winapi::{
|
||||
shared::minwindef::{DWORD, FALSE, TRUE},
|
||||
um::{
|
||||
handleapi::CloseHandle,
|
||||
pdh::{
|
||||
PdhAddEnglishCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx,
|
||||
PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE,
|
||||
PDH_HCOUNTER, PDH_HQUERY,
|
||||
},
|
||||
synchapi::{CreateEventA, WaitForSingleObject},
|
||||
sysinfoapi::VerSetConditionMask,
|
||||
winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0},
|
||||
winnt::{
|
||||
HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION,
|
||||
VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CPU_USAGE_ONE_MINUTE: Arc<Mutex<Option<(f64, Instant)>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs
|
||||
#[repr(transparent)]
|
||||
pub struct RAIIHandle(pub HANDLE);
|
||||
|
||||
impl Drop for RAIIHandle {
|
||||
fn drop(&mut self) {
|
||||
// This never gives problem except when running under a debugger.
|
||||
unsafe { CloseHandle(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub(self) struct RAIIPDHQuery(pub PDH_HQUERY);
|
||||
|
||||
impl Drop for RAIIPDHQuery {
|
||||
fn drop(&mut self) {
|
||||
unsafe { PdhCloseQuery(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_cpu_performance_monitor() {
|
||||
// Code from:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex
|
||||
// Why value lower than taskManager:
|
||||
// https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43
|
||||
// Therefore we should compare with Precess Explorer rather than taskManager
|
||||
|
||||
let f = || unsafe {
|
||||
// load avg or cpu usage, test with prime95.
|
||||
// Prefer cpu usage because we can get accurate value from Precess Explorer.
|
||||
// const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0";
|
||||
const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0";
|
||||
const SAMPLE_INTERVAL: DWORD = 2; // 2 second
|
||||
|
||||
let mut ret;
|
||||
let mut query: PDH_HQUERY = std::mem::zeroed();
|
||||
ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query);
|
||||
if ret != 0 {
|
||||
log::error!("PdhOpenQueryA failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
let _query = RAIIPDHQuery(query);
|
||||
let mut counter: PDH_HCOUNTER = std::mem::zeroed();
|
||||
ret = PdhAddEnglishCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter);
|
||||
if ret != 0 {
|
||||
log::error!("PdhAddEnglishCounterA failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
ret = PdhCollectQueryData(query);
|
||||
if ret != 0 {
|
||||
log::error!("PdhCollectQueryData failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
let mut _counter_type: DWORD = 0;
|
||||
let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed();
|
||||
let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _);
|
||||
if event.is_null() {
|
||||
log::error!("CreateEventA failed");
|
||||
return;
|
||||
}
|
||||
let _event: RAIIHandle = RAIIHandle(event);
|
||||
ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event);
|
||||
if ret != 0 {
|
||||
log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut queue: VecDeque<f64> = VecDeque::new();
|
||||
let mut recent_valid: VecDeque<bool> = VecDeque::new();
|
||||
loop {
|
||||
// latest one minute
|
||||
if queue.len() == 31 {
|
||||
queue.pop_front();
|
||||
}
|
||||
if recent_valid.len() == 31 {
|
||||
recent_valid.pop_front();
|
||||
}
|
||||
// allow get value within one minute
|
||||
if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 {
|
||||
let sum: f64 = queue.iter().map(|f| f.to_owned()).sum();
|
||||
let avg = sum / (queue.len() as f64);
|
||||
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now()));
|
||||
} else {
|
||||
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = None;
|
||||
}
|
||||
if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) {
|
||||
recent_valid.push_back(false);
|
||||
continue;
|
||||
}
|
||||
if PdhGetFormattedCounterValue(
|
||||
counter,
|
||||
PDH_FMT_DOUBLE,
|
||||
&mut _counter_type,
|
||||
&mut counter_value,
|
||||
) != 0
|
||||
|| counter_value.CStatus != 0
|
||||
{
|
||||
recent_valid.push_back(false);
|
||||
continue;
|
||||
}
|
||||
queue.push_back(counter_value.u.doubleValue().clone());
|
||||
recent_valid.push_back(true);
|
||||
}
|
||||
};
|
||||
use std::sync::Once;
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
std::thread::spawn(f);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn cpu_uage_one_minute() -> Option<f64> {
|
||||
let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone();
|
||||
if let Some((v, instant)) = v {
|
||||
if instant.elapsed().as_secs() < 30 {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn sync_cpu_usage(cpu_usage: Option<f64>) {
|
||||
let v = match cpu_usage {
|
||||
Some(cpu_usage) => Some((cpu_usage, Instant::now())),
|
||||
None => None,
|
||||
};
|
||||
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = v;
|
||||
log::info!("cpu usage synced: {:?}", cpu_usage);
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
|
||||
// https://github.com/nodejs/node-convergence-archive/blob/e11fe0c2777561827cdb7207d46b0917ef3c42a7/deps/uv/src/win/util.c#L780
|
||||
pub fn is_windows_version_or_greater(
|
||||
os_major: u32,
|
||||
os_minor: u32,
|
||||
build_number: u32,
|
||||
service_pack_major: u32,
|
||||
service_pack_minor: u32,
|
||||
) -> bool {
|
||||
let mut osvi: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() };
|
||||
osvi.dwOSVersionInfoSize = std::mem::size_of::<OSVERSIONINFOEXW>() as DWORD;
|
||||
osvi.dwMajorVersion = os_major as _;
|
||||
osvi.dwMinorVersion = os_minor as _;
|
||||
osvi.dwBuildNumber = build_number as _;
|
||||
osvi.wServicePackMajor = service_pack_major as _;
|
||||
osvi.wServicePackMinor = service_pack_minor as _;
|
||||
|
||||
let result = unsafe {
|
||||
let mut condition_mask = 0;
|
||||
let op = VER_GREATER_EQUAL;
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_MAJORVERSION, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_MINORVERSION, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_BUILDNUMBER, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMAJOR, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMINOR, op);
|
||||
|
||||
VerifyVersionInfoW(
|
||||
&mut osvi as *mut OSVERSIONINFOEXW,
|
||||
VER_MAJORVERSION
|
||||
| VER_MINORVERSION
|
||||
| VER_BUILDNUMBER
|
||||
| VER_SERVICEPACKMAJOR
|
||||
| VER_SERVICEPACKMINOR,
|
||||
condition_mask,
|
||||
)
|
||||
};
|
||||
|
||||
result == TRUE
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
|
||||
@@ -0,0 +1,716 @@
|
||||
use std::{
|
||||
io::Error as IoError,
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
};
|
||||
|
||||
use anyhow::bail;
|
||||
use async_recursion::async_recursion;
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use httparse::{Error as HttpParseError, Response, EMPTY_HEADER};
|
||||
use thiserror::Error as ThisError;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream};
|
||||
use tokio_native_tls::{native_tls, TlsConnector, TlsStream};
|
||||
use tokio_rustls::{client::TlsStream as RustlsTlsStream, TlsConnector as RustlsTlsConnector};
|
||||
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, TargetAddr};
|
||||
use tokio_util::codec::Framed;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
bytes_codec::BytesCodec,
|
||||
config::Socks5Server,
|
||||
tcp::{DynTcpStream, FramedStream},
|
||||
tls::{get_cached_tls_accept_invalid_cert, get_cached_tls_type, upsert_tls_cache, TlsType},
|
||||
ResultType,
|
||||
};
|
||||
|
||||
#[derive(Debug, ThisError)]
|
||||
pub enum ProxyError {
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] IoError),
|
||||
#[error("Target parse error: {0}")]
|
||||
TargetParseError(String),
|
||||
#[error("HTTP parse error: {0}")]
|
||||
HttpParseError(#[from] HttpParseError),
|
||||
#[error("The maximum response header length is exceeded: {0}")]
|
||||
MaximumResponseHeaderLengthExceeded(usize),
|
||||
#[error("The end of file is reached")]
|
||||
EndOfFile,
|
||||
#[error("The url is error: {0}")]
|
||||
UrlBadScheme(String),
|
||||
#[error("The url parse error: {0}")]
|
||||
UrlParseScheme(#[from] url::ParseError),
|
||||
#[error("No HTTP code was found in the response")]
|
||||
NoHttpCode,
|
||||
#[error("The HTTP code is not equal 200: {0}")]
|
||||
HttpCode200(u16),
|
||||
#[error("The proxy address resolution failed: {0}")]
|
||||
AddressResolutionFailed(String),
|
||||
#[error("The native tls error: {0}")]
|
||||
NativeTlsError(#[from] tokio_native_tls::native_tls::Error),
|
||||
}
|
||||
|
||||
const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096;
|
||||
/// The maximum HTTP Headers, which can be parsed.
|
||||
const MAXIMUM_RESPONSE_HEADERS: usize = 16;
|
||||
const DEFINE_TIME_OUT: u64 = 600;
|
||||
|
||||
pub trait IntoUrl {
|
||||
// Besides parsing as a valid `Url`, the `Url` must be a valid
|
||||
// `http::Uri`, in that it makes sense to use in a network request.
|
||||
fn into_url(self) -> Result<Url, ProxyError>;
|
||||
|
||||
fn as_str(&self) -> &str;
|
||||
}
|
||||
|
||||
impl IntoUrl for Url {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
if self.has_host() {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(ProxyError::UrlBadScheme(self.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoUrl for &'a str {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
Url::parse(self)
|
||||
.map_err(ProxyError::UrlParseScheme)?
|
||||
.into_url()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoUrl for &'a String {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
(&**self).into_url()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoUrl for String {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
(&*self).into_url()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Auth {
|
||||
user_name: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
fn get_proxy_authorization(&self) -> String {
|
||||
format!(
|
||||
"Proxy-Authorization: Basic {}\r\n",
|
||||
self.get_basic_authorization()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_basic_authorization(&self) -> String {
|
||||
let authorization = format!("{}:{}", &self.user_name, &self.password);
|
||||
general_purpose::STANDARD.encode(authorization.as_bytes())
|
||||
}
|
||||
|
||||
pub fn username(&self) -> &str {
|
||||
&self.user_name
|
||||
}
|
||||
|
||||
pub fn password(&self) -> &str {
|
||||
&self.password
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ProxyScheme {
|
||||
Http {
|
||||
auth: Option<Auth>,
|
||||
host: String,
|
||||
},
|
||||
Https {
|
||||
auth: Option<Auth>,
|
||||
host: String,
|
||||
},
|
||||
Socks5 {
|
||||
addr: SocketAddr,
|
||||
auth: Option<Auth>,
|
||||
remote_dns: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProxyScheme {
|
||||
pub fn maybe_auth(&self) -> Option<&Auth> {
|
||||
match self {
|
||||
ProxyScheme::Http { auth, .. }
|
||||
| ProxyScheme::Https { auth, .. }
|
||||
| ProxyScheme::Socks5 { auth, .. } => auth.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
fn socks5(addr: SocketAddr) -> Result<Self, ProxyError> {
|
||||
Ok(ProxyScheme::Socks5 {
|
||||
addr,
|
||||
auth: None,
|
||||
remote_dns: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn http(host: &str) -> Result<Self, ProxyError> {
|
||||
Ok(ProxyScheme::Http {
|
||||
auth: None,
|
||||
host: host.to_string(),
|
||||
})
|
||||
}
|
||||
fn https(host: &str) -> Result<Self, ProxyError> {
|
||||
Ok(ProxyScheme::Https {
|
||||
auth: None,
|
||||
host: host.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
|
||||
let auth = Auth {
|
||||
user_name: username.into(),
|
||||
password: password.into(),
|
||||
};
|
||||
match self {
|
||||
ProxyScheme::Http { auth: a, .. } => *a = Some(auth),
|
||||
ProxyScheme::Https { auth: a, .. } => *a = Some(auth),
|
||||
ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(url: Url) -> Result<Self, ProxyError> {
|
||||
use url::Position;
|
||||
|
||||
// Resolve URL to a host and port
|
||||
let to_addr = || {
|
||||
let addrs = url.socket_addrs(|| match url.scheme() {
|
||||
"socks5" => Some(1080),
|
||||
_ => None,
|
||||
})?;
|
||||
addrs
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost))
|
||||
};
|
||||
|
||||
let mut scheme: Self = match url.scheme() {
|
||||
"http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
|
||||
"https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
|
||||
"socks5" => Self::socks5(to_addr()?)?,
|
||||
e => return Err(ProxyError::UrlBadScheme(e.to_string())),
|
||||
};
|
||||
|
||||
if let Some(pwd) = url.password() {
|
||||
let username = url.username();
|
||||
scheme.set_basic_auth(username, pwd);
|
||||
}
|
||||
|
||||
Ok(scheme)
|
||||
}
|
||||
pub async fn socket_addrs(&self) -> Result<SocketAddr, ProxyError> {
|
||||
log::trace!("Resolving socket address");
|
||||
match self {
|
||||
ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await,
|
||||
ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await,
|
||||
ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_host(&self, host: &str, default_port: u16) -> Result<SocketAddr, ProxyError> {
|
||||
let (host_str, port) = match host.split_once(':') {
|
||||
Some((h, p)) => (h, p.parse::<u16>().ok()),
|
||||
None => (host, None),
|
||||
};
|
||||
let addr = (host_str, port.unwrap_or(default_port))
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?;
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
pub fn get_domain(&self) -> Result<String, ProxyError> {
|
||||
match self {
|
||||
ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => {
|
||||
let domain = host
|
||||
.split(':')
|
||||
.next()
|
||||
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?;
|
||||
Ok(domain.to_string())
|
||||
}
|
||||
ProxyScheme::Socks5 { addr, .. } => match addr {
|
||||
SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()),
|
||||
SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn get_host_and_port(&self) -> Result<String, ProxyError> {
|
||||
match self {
|
||||
ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)),
|
||||
ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)),
|
||||
ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)),
|
||||
}
|
||||
}
|
||||
fn append_default_port(&self, host: &str, default_port: u16) -> String {
|
||||
if host.contains(':') {
|
||||
host.to_string()
|
||||
} else {
|
||||
format!("{}:{}", host, default_port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoProxyScheme {
|
||||
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError>;
|
||||
}
|
||||
|
||||
impl<S: IntoUrl> IntoProxyScheme for S {
|
||||
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
|
||||
// validate the URL
|
||||
let url = match self.as_str().into_url() {
|
||||
Ok(ok) => ok,
|
||||
Err(e) => {
|
||||
match e {
|
||||
// If the string does not contain protocol headers, try to parse it using the socks5 protocol
|
||||
ProxyError::UrlParseScheme(_source) => {
|
||||
let try_this = format!("socks5://{}", self.as_str());
|
||||
try_this.into_url()?
|
||||
}
|
||||
_ => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ProxyScheme::parse(url)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoProxyScheme for ProxyScheme {
|
||||
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Proxy {
|
||||
pub intercept: ProxyScheme,
|
||||
ms_timeout: u64,
|
||||
}
|
||||
|
||||
impl Proxy {
|
||||
pub fn new<U: IntoProxyScheme>(proxy_scheme: U, ms_timeout: u64) -> Result<Self, ProxyError> {
|
||||
Ok(Self {
|
||||
intercept: proxy_scheme.into_proxy_scheme()?,
|
||||
ms_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_http_or_https(&self) -> bool {
|
||||
return match self.intercept {
|
||||
ProxyScheme::Socks5 { .. } => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn from_conf(conf: &Socks5Server, ms_timeout: Option<u64>) -> Result<Self, ProxyError> {
|
||||
let mut proxy;
|
||||
match ms_timeout {
|
||||
None => {
|
||||
proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?;
|
||||
}
|
||||
Some(time_out) => {
|
||||
proxy = Self::new(&conf.proxy, time_out)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !conf.password.is_empty() && !conf.username.is_empty() {
|
||||
proxy = proxy.basic_auth(&conf.username, &conf.password);
|
||||
}
|
||||
Ok(proxy)
|
||||
}
|
||||
|
||||
pub async fn proxy_addrs(&self) -> Result<SocketAddr, ProxyError> {
|
||||
self.intercept.socket_addrs().await
|
||||
}
|
||||
|
||||
fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
|
||||
self.intercept.set_basic_auth(username, password);
|
||||
self
|
||||
}
|
||||
|
||||
async fn new_stream(
|
||||
&self,
|
||||
local: SocketAddr,
|
||||
proxy: SocketAddr,
|
||||
) -> ResultType<tokio::net::TcpStream> {
|
||||
let stream = super::timeout(
|
||||
self.ms_timeout,
|
||||
crate::tcp::new_socket(local, true)?.connect(proxy),
|
||||
)
|
||||
.await??;
|
||||
stream.set_nodelay(true).ok();
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub async fn connect<'t, T>(
|
||||
&self,
|
||||
target: T,
|
||||
local_addr: Option<SocketAddr>,
|
||||
) -> ResultType<FramedStream>
|
||||
where
|
||||
T: IntoTargetAddr<'t>,
|
||||
{
|
||||
log::trace!("Connect to proxy server");
|
||||
let proxy = self.proxy_addrs().await?;
|
||||
|
||||
let target_addr = target
|
||||
.into_target_addr()
|
||||
.map_err(|e| ProxyError::TargetParseError(e.to_string()))?;
|
||||
|
||||
let local = if let Some(addr) = local_addr {
|
||||
addr
|
||||
} else {
|
||||
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
|
||||
};
|
||||
|
||||
let stream = self.new_stream(local, proxy).await?;
|
||||
let addr = stream.local_addr()?;
|
||||
|
||||
return match self.intercept {
|
||||
ProxyScheme::Http { .. } => {
|
||||
log::trace!("Connect to remote http proxy server: {}", proxy);
|
||||
let stream =
|
||||
super::timeout(self.ms_timeout, self.http_connect(stream, &target_addr))
|
||||
.await??;
|
||||
Ok(FramedStream(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
))
|
||||
}
|
||||
ProxyScheme::Https { .. } => {
|
||||
log::trace!("Connect to remote https proxy server: {}", proxy);
|
||||
let url = format!("https://{}", self.intercept.get_host_and_port()?);
|
||||
let tls_type = get_cached_tls_type(&url);
|
||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(&url);
|
||||
let stream = match tls_type.unwrap_or(TlsType::Rustls) {
|
||||
TlsType::Rustls => {
|
||||
self.https_connect_rustls_wrap_danger(
|
||||
&url,
|
||||
local,
|
||||
proxy,
|
||||
Some(stream),
|
||||
&target_addr,
|
||||
tls_type.is_some(),
|
||||
danger_accept_invalid_cert,
|
||||
danger_accept_invalid_cert,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
TlsType::NativeTls => {
|
||||
self.https_connect_nativetls_wrap_danger(
|
||||
&url,
|
||||
local,
|
||||
proxy,
|
||||
&target_addr,
|
||||
danger_accept_invalid_cert,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
_ => {
|
||||
// Unreachable
|
||||
crate::bail!("Unreachable, TlsType::Plain in HTTPS proxy");
|
||||
}
|
||||
};
|
||||
Ok(FramedStream(
|
||||
Framed::new(stream, BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
))
|
||||
}
|
||||
ProxyScheme::Socks5 { .. } => {
|
||||
log::trace!("Connect to remote socket5 proxy server: {}", proxy);
|
||||
let stream = if let Some(auth) = self.intercept.maybe_auth() {
|
||||
super::timeout(
|
||||
self.ms_timeout,
|
||||
Socks5Stream::connect_with_password_and_socket(
|
||||
stream,
|
||||
target_addr,
|
||||
&auth.user_name,
|
||||
&auth.password,
|
||||
),
|
||||
)
|
||||
.await??
|
||||
} else {
|
||||
super::timeout(
|
||||
self.ms_timeout,
|
||||
Socks5Stream::connect_with_socket(stream, target_addr),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
Ok(FramedStream(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async fn https_connect_nativetls_wrap_danger<'a>(
|
||||
&self,
|
||||
url: &str,
|
||||
local: SocketAddr,
|
||||
proxy: SocketAddr,
|
||||
target_addr: &TargetAddr<'a>,
|
||||
danger_accept_invalid_cert: Option<bool>,
|
||||
) -> ResultType<DynTcpStream> {
|
||||
let stream = self.new_stream(local, proxy).await?;
|
||||
let s = super::timeout(
|
||||
self.ms_timeout,
|
||||
self.https_connect_nativetls(
|
||||
stream,
|
||||
&target_addr,
|
||||
danger_accept_invalid_cert.unwrap_or(false),
|
||||
),
|
||||
)
|
||||
.await??;
|
||||
upsert_tls_cache(
|
||||
url,
|
||||
TlsType::NativeTls,
|
||||
danger_accept_invalid_cert.unwrap_or(false),
|
||||
);
|
||||
Ok(DynTcpStream(Box::new(s)))
|
||||
}
|
||||
|
||||
pub async fn https_connect_nativetls<'a, Input>(
|
||||
&self,
|
||||
io: Input,
|
||||
target_addr: &TargetAddr<'a>,
|
||||
danger_accept_invalid_cert: bool,
|
||||
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
|
||||
where
|
||||
Input: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let mut tls_connector_builder = native_tls::TlsConnector::builder();
|
||||
if danger_accept_invalid_cert {
|
||||
tls_connector_builder.danger_accept_invalid_certs(true);
|
||||
}
|
||||
let tls_connector = TlsConnector::from(tls_connector_builder.build()?);
|
||||
let stream = tls_connector
|
||||
.connect(&self.intercept.get_domain()?, io)
|
||||
.await?;
|
||||
self.http_connect(stream, target_addr).await
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn https_connect_rustls_wrap_danger<'a>(
|
||||
&self,
|
||||
url: &str,
|
||||
local: SocketAddr,
|
||||
proxy: SocketAddr,
|
||||
stream: Option<tokio::net::TcpStream>,
|
||||
target_addr: &TargetAddr<'a>,
|
||||
is_tls_type_cached: bool,
|
||||
danger_accept_invalid_cert: Option<bool>,
|
||||
origin_danger_accept_invalid_cert: Option<bool>,
|
||||
) -> ResultType<DynTcpStream> {
|
||||
let stream = stream.unwrap_or(self.new_stream(local, proxy).await?);
|
||||
match super::timeout(
|
||||
self.ms_timeout,
|
||||
self.https_connect_rustls(
|
||||
stream,
|
||||
target_addr,
|
||||
danger_accept_invalid_cert.unwrap_or(false),
|
||||
),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok(s) => {
|
||||
upsert_tls_cache(
|
||||
&url,
|
||||
TlsType::Rustls,
|
||||
danger_accept_invalid_cert.unwrap_or(false),
|
||||
);
|
||||
Ok(DynTcpStream(Box::new(s)))
|
||||
}
|
||||
Err(e) => {
|
||||
// NOTE: Maybe it's better to check if the error is related to TLS here. (ProxyError::IoError(e), or ProxyError::NativeTlsError(e))
|
||||
// But we can only get the error when the TLS protocol is TLSv1.1.
|
||||
// The error message of the following is unclear:
|
||||
// https://github.com/rustdesk/rustdesk-server-pro/issues/189#issuecomment-1895701480
|
||||
// So we just try to fallback unconditionally here.
|
||||
//
|
||||
// If the protocol is TLS 1.1, the error is:
|
||||
// 1. "IO Error: received fatal alert: ProtocolVersion"
|
||||
// 2. "IO Error: An existing connection was forcibly closed by the remote host. (os error 10054)" on Windows sometimes.
|
||||
//
|
||||
// If the cert verification fails, the error is:
|
||||
// "IO Error: invalid peer certificate: UnknownIssuer"
|
||||
|
||||
let s = if danger_accept_invalid_cert.is_none() {
|
||||
log::warn!(
|
||||
"Falling back to rustls-tls (accept invalid cert) for HTTPS proxy server."
|
||||
);
|
||||
self.https_connect_rustls_wrap_danger(
|
||||
&url,
|
||||
local,
|
||||
proxy,
|
||||
None,
|
||||
target_addr,
|
||||
is_tls_type_cached,
|
||||
Some(true),
|
||||
origin_danger_accept_invalid_cert,
|
||||
)
|
||||
.await?
|
||||
} else if !is_tls_type_cached {
|
||||
log::warn!("Falling back to native-tls for HTTPS proxy server.");
|
||||
self.https_connect_nativetls_wrap_danger(
|
||||
&url,
|
||||
local,
|
||||
proxy,
|
||||
&target_addr,
|
||||
origin_danger_accept_invalid_cert,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
log::error!(
|
||||
"Failed to connect to HTTPS proxy server with native-tls: {:?}.",
|
||||
e
|
||||
);
|
||||
bail!(e)
|
||||
};
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn https_connect_rustls<'a, Input>(
|
||||
&self,
|
||||
io: Input,
|
||||
target_addr: &TargetAddr<'a>,
|
||||
danger_accept_invalid_cert: bool,
|
||||
) -> Result<BufStream<RustlsTlsStream<Input>>, ProxyError>
|
||||
where
|
||||
Input: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let url_domain = self.intercept.get_domain()?;
|
||||
let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str())
|
||||
.map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))?
|
||||
.to_owned();
|
||||
let client_config = crate::verifier::client_config(danger_accept_invalid_cert)
|
||||
.map_err(|e| ProxyError::IoError(std::io::Error::other(e)))?;
|
||||
let tls_connector = RustlsTlsConnector::from(std::sync::Arc::new(client_config));
|
||||
let stream = tls_connector.connect(domain, io).await?;
|
||||
self.http_connect(stream, target_addr).await
|
||||
}
|
||||
|
||||
pub async fn http_connect<'a, Input>(
|
||||
&self,
|
||||
io: Input,
|
||||
target_addr: &TargetAddr<'a>,
|
||||
) -> Result<BufStream<Input>, ProxyError>
|
||||
where
|
||||
Input: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let mut stream = BufStream::new(io);
|
||||
let (domain, port) = get_domain_and_port(target_addr)?;
|
||||
|
||||
let request = self.make_request(&domain, port);
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
stream.flush().await?;
|
||||
recv_and_check_response(&mut stream).await?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn make_request(&self, host: &str, port: u16) -> String {
|
||||
let mut request = format!(
|
||||
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n",
|
||||
host = host,
|
||||
port = port
|
||||
);
|
||||
|
||||
if let Some(auth) = self.intercept.maybe_auth() {
|
||||
request = format!("{}{}", request, auth.get_proxy_authorization());
|
||||
}
|
||||
|
||||
request.push_str("\r\n");
|
||||
request
|
||||
}
|
||||
}
|
||||
|
||||
fn get_domain_and_port<'a>(target_addr: &TargetAddr<'a>) -> Result<(String, u16), ProxyError> {
|
||||
match target_addr {
|
||||
tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())),
|
||||
tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), *port)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_response<IO>(stream: &mut BufStream<IO>) -> Result<String, ProxyError>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
let mut response = String::new();
|
||||
|
||||
loop {
|
||||
if stream.read_line(&mut response).await? == 0 {
|
||||
return Err(ProxyError::EndOfFile);
|
||||
}
|
||||
|
||||
if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() {
|
||||
return Err(ProxyError::MaximumResponseHeaderLengthExceeded(
|
||||
response.len(),
|
||||
));
|
||||
}
|
||||
|
||||
if response.ends_with("\r\n\r\n") {
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_and_check_response<IO>(stream: &mut BufStream<IO>) -> Result<(), ProxyError>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let response_string = get_response(stream).await?;
|
||||
|
||||
let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS];
|
||||
let mut response = Response::new(&mut response_headers);
|
||||
let response_bytes = response_string.into_bytes();
|
||||
response.parse(&response_bytes)?;
|
||||
|
||||
return match response.code {
|
||||
Some(code) => {
|
||||
if code == 200 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ProxyError::HttpCode200(code))
|
||||
}
|
||||
}
|
||||
None => Err(ProxyError::NoHttpCode),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
#[cfg(feature = "webrtc")]
|
||||
use crate::webrtc::{self, is_webrtc_endpoint};
|
||||
use crate::{
|
||||
config::{Config, NetworkType},
|
||||
tcp::FramedStream,
|
||||
udp::FramedSocket,
|
||||
websocket::{self, check_ws, is_ws_endpoint},
|
||||
ResultType, Stream,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use tokio::net::{ToSocketAddrs, UdpSocket};
|
||||
use tokio_socks::{IntoTargetAddr, TargetAddr};
|
||||
|
||||
#[inline]
|
||||
pub fn check_port<T: std::string::ToString>(host: T, port: i32) -> String {
|
||||
let host = host.to_string();
|
||||
if crate::is_ipv6_str(&host) {
|
||||
if host.starts_with('[') {
|
||||
return host;
|
||||
}
|
||||
return format!("[{host}]:{port}");
|
||||
}
|
||||
if !host.contains(':') {
|
||||
return format!("{host}:{port}");
|
||||
}
|
||||
host
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn increase_port<T: std::string::ToString>(host: T, offset: i32) -> String {
|
||||
let host = host.to_string();
|
||||
if crate::is_ipv6_str(&host) {
|
||||
if host.starts_with('[') {
|
||||
let tmp: Vec<&str> = host.split("]:").collect();
|
||||
if tmp.len() == 2 {
|
||||
let port: i32 = tmp[1].parse().unwrap_or(0);
|
||||
if port > 0 {
|
||||
return format!("{}]:{}", tmp[0], port + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if host.contains(':') {
|
||||
let tmp: Vec<&str> = host.split(':').collect();
|
||||
if tmp.len() == 2 {
|
||||
let port: i32 = tmp[1].parse().unwrap_or(0);
|
||||
if port > 0 {
|
||||
return format!("{}:{}", tmp[0], port + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
host
|
||||
}
|
||||
|
||||
pub fn split_host_port<T: std::string::ToString>(host: T) -> Option<(String, i32)> {
|
||||
let host = host.to_string();
|
||||
if crate::is_ipv6_str(&host) {
|
||||
if host.starts_with('[') {
|
||||
let tmp: Vec<&str> = host.split("]:").collect();
|
||||
if tmp.len() == 2 {
|
||||
let port: i32 = tmp[1].parse().unwrap_or(0);
|
||||
if port > 0 {
|
||||
return Some((format!("{}]", tmp[0]), port));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if host.contains(':') {
|
||||
let tmp: Vec<&str> = host.split(':').collect();
|
||||
if tmp.len() == 2 {
|
||||
let port: i32 = tmp[1].parse().unwrap_or(0);
|
||||
if port > 0 {
|
||||
return Some((tmp[0].to_string(), port));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String {
|
||||
let host = check_port(host, 0);
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() {
|
||||
test_if_valid_server_for_proxy_(&host)
|
||||
} else {
|
||||
match host.to_socket_addrs() {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn test_if_valid_server_for_proxy_(host: &str) -> String {
|
||||
// `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing,
|
||||
// it can be used for both `socks` and `http` proxy.
|
||||
match &host.into_target_addr() {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// This function checks if the target is a websocket endpoint and connects accordingly.
|
||||
#[inline]
|
||||
pub async fn connect_tcp<
|
||||
't,
|
||||
T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
|
||||
>(
|
||||
target: T,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<crate::Stream> {
|
||||
#[cfg(feature = "webrtc")]
|
||||
if is_webrtc_endpoint(&target.to_string()) {
|
||||
return Ok(Stream::WebRTC(
|
||||
webrtc::WebRTCStream::new(&target.to_string(), false, ms_timeout).await?,
|
||||
));
|
||||
}
|
||||
let target_str = check_ws(&target.to_string());
|
||||
if is_ws_endpoint(&target_str) {
|
||||
return Ok(Stream::WebSocket(
|
||||
websocket::WsFramedStream::new(target_str, None, None, ms_timeout).await?,
|
||||
));
|
||||
}
|
||||
connect_tcp_local(target, None, ms_timeout).await
|
||||
}
|
||||
|
||||
// This function connects directly to the target without checking for websocket endpoints.
|
||||
pub async fn connect_tcp_local<
|
||||
't,
|
||||
T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
|
||||
>(
|
||||
target: T,
|
||||
local: Option<SocketAddr>,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Stream> {
|
||||
if let Some(conf) = Config::get_socks() {
|
||||
return Ok(Stream::Tcp(
|
||||
FramedStream::connect(target, local, &conf, ms_timeout).await?,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(target_addr) = target.resolve() {
|
||||
if let Some(local_addr) = local {
|
||||
if local_addr.is_ipv6() && target_addr.is_ipv4() {
|
||||
let resolved_target = query_nip_io(target_addr).await?;
|
||||
return Ok(Stream::Tcp(
|
||||
FramedStream::new(resolved_target, Some(local_addr), ms_timeout).await?,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Stream::Tcp(
|
||||
FramedStream::new(target, local, ms_timeout).await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[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?
|
||||
.find(|x| x.is_ipv6())
|
||||
.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!("{ip}.nip.io"));
|
||||
}
|
||||
}
|
||||
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_direct_udp_for(target: &str) -> ResultType<(Arc<UdpSocket>, SocketAddr)> {
|
||||
let peer_addr = test_target(target).await?;
|
||||
let local_addr = Config::get_any_listen_addr(peer_addr.is_ipv4());
|
||||
let socket = UdpSocket::bind(local_addr).await?;
|
||||
Ok((Arc::new(socket), peer_addr))
|
||||
}
|
||||
|
||||
#[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) => {
|
||||
let socket = FramedSocket::new_proxy(
|
||||
conf.proxy.as_str(),
|
||||
local,
|
||||
conf.username.as_str(),
|
||||
conf.password.as_str(),
|
||||
ms_timeout,
|
||||
)
|
||||
.await?;
|
||||
Ok(socket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_if_valid_server() {
|
||||
assert!(!test_if_valid_server("a", false).is_empty());
|
||||
// on Linux, "1" is resolved to "0.0.0.1"
|
||||
assert!(test_if_valid_server("1.1.1.1", false).is_empty());
|
||||
assert!(test_if_valid_server("1.1.1.1:1", false).is_empty());
|
||||
assert!(test_if_valid_server("microsoft.com", false).is_empty());
|
||||
assert!(test_if_valid_server("microsoft.com:1", false).is_empty());
|
||||
|
||||
// with proxy
|
||||
// `:0` indicates `let host = check_port(host, 0);` is called.
|
||||
assert!(test_if_valid_server_for_proxy_("a:0").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_port() {
|
||||
assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12");
|
||||
assert_eq!(check_port("1:2", 32), "[1:2]:32");
|
||||
assert_eq!(check_port("z1:2", 32), "z1:2");
|
||||
assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32");
|
||||
assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32");
|
||||
assert_eq!(check_port("test.com:32", 0), "test.com:32");
|
||||
assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13");
|
||||
assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13");
|
||||
assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4");
|
||||
assert_eq!(increase_port("test.com", 1), "test.com");
|
||||
assert_eq!(increase_port("test.com:13", 4), "test.com:17");
|
||||
assert_eq!(increase_port("1:13", 4), "1:13");
|
||||
assert_eq!(increase_port("22:1:13", 4), "22:1:13");
|
||||
assert_eq!(increase_port("z1:2", 1), "z1:3");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use crate::{config, tcp, websocket, ResultType};
|
||||
#[cfg(feature = "webrtc")]
|
||||
use crate::webrtc;
|
||||
use sodiumoxide::crypto::secretbox::Key;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
// support Websocket and tcp.
|
||||
pub enum Stream {
|
||||
#[cfg(feature = "webrtc")]
|
||||
WebRTC(webrtc::WebRTCStream),
|
||||
WebSocket(websocket::WsFramedStream),
|
||||
Tcp(tcp::FramedStream),
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
#[inline]
|
||||
pub fn set_send_timeout(&mut self, ms: u64) {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Stream::WebRTC(s) => s.set_send_timeout(ms),
|
||||
Stream::WebSocket(s) => s.set_send_timeout(ms),
|
||||
Stream::Tcp(s) => s.set_send_timeout(ms),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_raw(&mut self) {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Stream::WebRTC(s) => s.set_raw(),
|
||||
Stream::WebSocket(s) => s.set_raw(),
|
||||
Stream::Tcp(s) => s.set_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_bytes(&mut self, bytes: bytes::Bytes) -> ResultType<()> {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Stream::WebRTC(s) => s.send_bytes(bytes).await,
|
||||
Stream::WebSocket(s) => s.send_bytes(bytes).await,
|
||||
Stream::Tcp(s) => s.send_bytes(bytes).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, bytes: Vec<u8>) -> ResultType<()> {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Stream::WebRTC(s) => s.send_raw(bytes).await,
|
||||
Stream::WebSocket(s) => s.send_raw(bytes).await,
|
||||
Stream::Tcp(s) => s.send_raw(bytes).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_key(&mut self, key: Key) {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Stream::WebRTC(s) => s.set_key(key),
|
||||
Stream::WebSocket(s) => s.set_key(key),
|
||||
Stream::Tcp(s) => s.set_key(key),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_secured(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Stream::WebRTC(s) => s.is_secured(),
|
||||
Stream::WebSocket(s) => s.is_secured(),
|
||||
Stream::Tcp(s) => s.is_secured(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
) -> Option<Result<bytes::BytesMut, std::io::Error>> {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Stream::WebRTC(s) => s.next_timeout(timeout).await,
|
||||
Stream::WebSocket(s) => s.next_timeout(timeout).await,
|
||||
Stream::Tcp(s) => s.next_timeout(timeout).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// establish connect from websocket
|
||||
#[inline]
|
||||
pub async fn connect_websocket(
|
||||
url: impl AsRef<str>,
|
||||
local_addr: Option<SocketAddr>,
|
||||
proxy_conf: Option<&config::Socks5Server>,
|
||||
timeout_ms: u64,
|
||||
) -> ResultType<Self> {
|
||||
let ws_stream =
|
||||
websocket::WsFramedStream::new(url, local_addr, proxy_conf, timeout_ms).await?;
|
||||
log::debug!("WebSocket connection established");
|
||||
Ok(Self::WebSocket(ws_stream))
|
||||
}
|
||||
|
||||
/// send message
|
||||
#[inline]
|
||||
pub async fn send(&mut self, msg: &impl protobuf::Message) -> ResultType<()> {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Self::WebRTC(s) => s.send(msg).await,
|
||||
Self::WebSocket(ws) => ws.send(msg).await,
|
||||
Self::Tcp(tcp) => tcp.send(msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// receive message
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Result<bytes::BytesMut, std::io::Error>> {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Self::WebRTC(s) => s.next().await,
|
||||
Self::WebSocket(ws) => ws.next().await,
|
||||
Self::Tcp(tcp) => tcp.next().await,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
match self {
|
||||
#[cfg(feature = "webrtc")]
|
||||
Self::WebRTC(s) => s.local_addr(),
|
||||
Self::WebSocket(ws) => ws.local_addr(),
|
||||
Self::Tcp(tcp) => tcp.local_addr(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from(stream: TcpStream, stream_addr: SocketAddr) -> Self {
|
||||
Self::Tcp(tcp::FramedStream::from(stream, stream_addr))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "webrtc")]
|
||||
pub fn get_webrtc_stream(&self) -> Option<webrtc::WebRTCStream> {
|
||||
match self {
|
||||
Self::WebRTC(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy};
|
||||
use anyhow::Context as AnyhowCtx;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use protobuf::Message;
|
||||
use sodiumoxide::crypto::{
|
||||
box_,
|
||||
secretbox::{self, Key, Nonce},
|
||||
};
|
||||
use std::{
|
||||
io::{self, Error, ErrorKind},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
ops::{Deref, DerefMut},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite, ReadBuf},
|
||||
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
|
||||
};
|
||||
use tokio_socks::IntoTargetAddr;
|
||||
use tokio_util::codec::Framed;
|
||||
|
||||
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
|
||||
pub struct DynTcpStream(pub Box<dyn TcpStreamTrait + Send + Sync>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Encrypt(pub Key, pub u64, pub u64);
|
||||
|
||||
pub struct FramedStream(
|
||||
pub Framed<DynTcpStream, BytesCodec>,
|
||||
pub SocketAddr,
|
||||
pub Option<Encrypt>,
|
||||
pub u64,
|
||||
);
|
||||
|
||||
impl Deref for FramedStream {
|
||||
type Target = Framed<DynTcpStream, BytesCodec>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for FramedStream {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DynTcpStream {
|
||||
type Target = Box<dyn TcpStreamTrait + Send + Sync>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for DynTcpStream {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
|
||||
let socket = match addr {
|
||||
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
|
||||
std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?,
|
||||
};
|
||||
if reuse {
|
||||
// windows has no reuse_port, but its reuse_address
|
||||
// almost equals to unix's reuse_port + reuse_address,
|
||||
// though may introduce nondeterministic behavior
|
||||
// illumos has no support for SO_REUSEPORT
|
||||
#[cfg(all(unix, not(target_os = "illumos")))]
|
||||
socket.set_reuseport(true).ok();
|
||||
socket.set_reuseaddr(true).ok();
|
||||
}
|
||||
socket.bind(addr)?;
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
impl FramedStream {
|
||||
pub async fn new<T: ToSocketAddrs + std::fmt::Display>(
|
||||
remote_addr: T,
|
||||
local_addr: Option<SocketAddr>,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self> {
|
||||
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!(format!("Failed to connect to {remote_addr}"));
|
||||
}
|
||||
|
||||
pub async fn connect<'t, T>(
|
||||
target: T,
|
||||
local_addr: Option<SocketAddr>,
|
||||
proxy_conf: &Socks5Server,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self>
|
||||
where
|
||||
T: IntoTargetAddr<'t>,
|
||||
{
|
||||
let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?;
|
||||
proxy.connect::<T>(target, local_addr).await
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.1
|
||||
}
|
||||
|
||||
pub fn set_send_timeout(&mut self, ms: u64) {
|
||||
self.3 = ms;
|
||||
}
|
||||
|
||||
pub fn from(stream: impl TcpStreamTrait + Send + Sync + 'static, addr: SocketAddr) -> Self {
|
||||
Self(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_raw(&mut self) {
|
||||
self.0.codec_mut().set_raw();
|
||||
self.2 = None;
|
||||
}
|
||||
|
||||
pub fn is_secured(&self) -> bool {
|
||||
self.2.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> {
|
||||
self.send_raw(msg.write_to_bytes()?).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, msg: Vec<u8>) -> ResultType<()> {
|
||||
let mut msg = msg;
|
||||
if let Some(key) = self.2.as_mut() {
|
||||
msg = key.enc(&msg);
|
||||
}
|
||||
self.send_bytes(bytes::Bytes::from(msg)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> {
|
||||
if self.3 > 0 {
|
||||
super::timeout(self.3, self.0.send(bytes)).await??;
|
||||
} else {
|
||||
self.0.send(bytes).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Result<BytesMut, Error>> {
|
||||
let mut res = self.0.next().await;
|
||||
if let Some(Ok(bytes)) = res.as_mut() {
|
||||
if let Some(key) = self.2.as_mut() {
|
||||
if let Err(err) = key.dec(bytes) {
|
||||
return Some(Err(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(&mut self, ms: u64) -> Option<Result<BytesMut, Error>> {
|
||||
if let Ok(res) = super::timeout(ms, self.next()).await {
|
||||
res
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_key(&mut self, key: Key) {
|
||||
self.2 = Some(Encrypt::new(key));
|
||||
}
|
||||
|
||||
fn get_nonce(seqnum: u64) -> Nonce {
|
||||
let mut nonce = Nonce([0u8; secretbox::NONCEBYTES]);
|
||||
nonce.0[..std::mem::size_of_val(&seqnum)].copy_from_slice(&seqnum.to_le_bytes());
|
||||
nonce
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_BACKLOG: u32 = 128;
|
||||
|
||||
pub async fn new_listener<T: ToSocketAddrs>(addr: T, reuse: bool) -> ResultType<TcpListener> {
|
||||
if !reuse {
|
||||
Ok(TcpListener::bind(addr).await?)
|
||||
} else {
|
||||
let addr = lookup_host(&addr)
|
||||
.await?
|
||||
.next()
|
||||
.context("could not resolve to any address")?;
|
||||
new_socket(addr, true)?
|
||||
.listen(DEFAULT_BACKLOG)
|
||||
.map_err(anyhow::Error::msg)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn listen_any(port: u16) -> ResultType<TcpListener> {
|
||||
if let Ok(mut socket) = TcpSocket::new_v6() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// illumos has no support for SO_REUSEPORT
|
||||
#[cfg(not(target_os = "illumos"))]
|
||||
socket.set_reuseport(true).ok();
|
||||
socket.set_reuseaddr(true).ok();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(new_socket(
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port),
|
||||
true,
|
||||
)?
|
||||
.listen(DEFAULT_BACKLOG)?)
|
||||
}
|
||||
|
||||
impl Unpin for DynTcpStream {}
|
||||
|
||||
impl AsyncRead for DynTcpStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for DynTcpStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_flush(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + AsyncWrite + Unpin> TcpStreamTrait for R {}
|
||||
|
||||
impl Encrypt {
|
||||
pub fn new(key: Key) -> Self {
|
||||
Self(key, 0, 0)
|
||||
}
|
||||
|
||||
pub fn dec(&mut self, bytes: &mut BytesMut) -> Result<(), Error> {
|
||||
if bytes.len() <= 1 {
|
||||
return Ok(());
|
||||
}
|
||||
self.2 += 1;
|
||||
let nonce = FramedStream::get_nonce(self.2);
|
||||
match secretbox::open(bytes, &nonce, &self.0) {
|
||||
Ok(res) => {
|
||||
bytes.clear();
|
||||
bytes.put_slice(&res);
|
||||
Ok(())
|
||||
}
|
||||
Err(()) => Err(Error::new(ErrorKind::Other, "decryption error")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enc(&mut self, data: &[u8]) -> Vec<u8> {
|
||||
self.1 += 1;
|
||||
let nonce = FramedStream::get_nonce(self.1);
|
||||
secretbox::seal(&data, &nonce, &self.0)
|
||||
}
|
||||
|
||||
pub fn decode(
|
||||
symmetric_data: &[u8],
|
||||
their_pk_b: &[u8],
|
||||
our_sk_b: &box_::SecretKey,
|
||||
) -> ResultType<Key> {
|
||||
if their_pk_b.len() != box_::PUBLICKEYBYTES {
|
||||
anyhow::bail!("Handshake failed: pk length {}", their_pk_b.len());
|
||||
}
|
||||
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
|
||||
let mut pk_ = [0u8; box_::PUBLICKEYBYTES];
|
||||
pk_[..].copy_from_slice(their_pk_b);
|
||||
let their_pk_b = box_::PublicKey(pk_);
|
||||
let symmetric_key = box_::open(symmetric_data, &nonce, &their_pk_b, &our_sk_b)
|
||||
.map_err(|_| anyhow::anyhow!("Handshake failed: box decryption failure"))?;
|
||||
if symmetric_key.len() != secretbox::KEYBYTES {
|
||||
anyhow::bail!("Handshake failed: invalid secret key length from peer");
|
||||
}
|
||||
let mut key = [0u8; secretbox::KEYBYTES];
|
||||
key[..].copy_from_slice(&symmetric_key);
|
||||
Ok(Key(key))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use std::{collections::HashMap, sync::RwLock};
|
||||
|
||||
use crate::config::allow_insecure_tls_fallback;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TlsType {
|
||||
Plain,
|
||||
NativeTls,
|
||||
Rustls,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref URL_TLS_TYPE: RwLock<HashMap<String, TlsType>> = RwLock::new(HashMap::new());
|
||||
static ref URL_TLS_DANGER_ACCEPT_INVALID_CERTS: RwLock<HashMap<String, bool>> = RwLock::new(HashMap::new());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_plain(url: &str) -> bool {
|
||||
url.starts_with("ws://") || url.starts_with("http://")
|
||||
}
|
||||
|
||||
// Extract domain from URL.
|
||||
// e.g., "https://example.com/path" -> "example.com"
|
||||
// "https://example.com:8080/path" -> "example.com:8080"
|
||||
// See the tests for more examples.
|
||||
#[inline]
|
||||
fn get_domain_and_port_from_url(url: &str) -> &str {
|
||||
// Remove scheme (e.g., http://, https://, ws://, wss://)
|
||||
let scheme_end = url.find("://").map(|pos| pos + 3).unwrap_or(0);
|
||||
let url2 = &url[scheme_end..];
|
||||
// If userinfo is present, domain is after last '@'
|
||||
let after_at = match url2.rfind('@') {
|
||||
Some(pos) => &url2[pos + 1..],
|
||||
None => url2,
|
||||
};
|
||||
// Find the end of domain (before '/' or '?')
|
||||
let domain_end = after_at.find(&['/', '?'][..]).unwrap_or(after_at.len());
|
||||
&after_at[..domain_end]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn upsert_tls_cache(url: &str, tls_type: TlsType, danger_accept_invalid_cert: bool) {
|
||||
if is_plain(url) {
|
||||
return;
|
||||
}
|
||||
|
||||
let domain_port = get_domain_and_port_from_url(url);
|
||||
// Use curly braces to ensure the lock is released immediately.
|
||||
{
|
||||
URL_TLS_TYPE
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(domain_port.to_string(), tls_type);
|
||||
}
|
||||
{
|
||||
URL_TLS_DANGER_ACCEPT_INVALID_CERTS
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(domain_port.to_string(), danger_accept_invalid_cert);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn reset_tls_cache() {
|
||||
// Use curly braces to ensure the lock is released immediately.
|
||||
{
|
||||
URL_TLS_TYPE.write().unwrap().clear();
|
||||
}
|
||||
{
|
||||
URL_TLS_DANGER_ACCEPT_INVALID_CERTS.write().unwrap().clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_cached_tls_type(url: &str) -> Option<TlsType> {
|
||||
if is_plain(url) {
|
||||
return Some(TlsType::Plain);
|
||||
}
|
||||
let domain_port = get_domain_and_port_from_url(url);
|
||||
URL_TLS_TYPE.read().unwrap().get(domain_port).cloned()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_cached_tls_accept_invalid_cert(url: &str) -> Option<bool> {
|
||||
if !allow_insecure_tls_fallback() {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
if is_plain(url) {
|
||||
return Some(false);
|
||||
}
|
||||
let domain_port = get_domain_and_port_from_url(url);
|
||||
URL_TLS_DANGER_ACCEPT_INVALID_CERTS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(domain_port)
|
||||
.cloned()
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_domain_and_port_from_url() {
|
||||
for (url, expected_domain_port) in vec![
|
||||
("http://example.com", "example.com"),
|
||||
("https://example.com", "example.com"),
|
||||
("ws://example.com/path", "example.com"),
|
||||
("wss://example.com:8080/path", "example.com:8080"),
|
||||
("https://user:pass@example.com", "example.com"),
|
||||
("https://example.com?query=param", "example.com"),
|
||||
("https://example.com:8443?query=param", "example.com:8443"),
|
||||
("ftp://example.com/resource", "example.com"), // ftp scheme
|
||||
("example.com/path", "example.com"), // no scheme
|
||||
("example.com:8080/path", "example.com:8080"),
|
||||
] {
|
||||
let domain_port = get_domain_and_port_from_url(url);
|
||||
assert_eq!(domain_port, expected_domain_port);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use crate::ResultType;
|
||||
use anyhow::{anyhow, Context};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use protobuf::Message;
|
||||
use socket2::{Domain, Socket, Type};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket};
|
||||
use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs};
|
||||
use tokio_util::{codec::BytesCodec, udp::UdpFramed};
|
||||
|
||||
pub enum FramedSocket {
|
||||
Direct(UdpFramed<BytesCodec>),
|
||||
ProxySocks(Socks5UdpFramed),
|
||||
}
|
||||
|
||||
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),
|
||||
}?;
|
||||
if reuse {
|
||||
// windows has no reuse_port, but its reuse_address
|
||||
// almost equals to unix's reuse_port + reuse_address,
|
||||
// though may introduce nondeterministic behavior
|
||||
// illumos has no support for SO_REUSEPORT
|
||||
#[cfg(all(unix, not(target_os = "illumos")))]
|
||||
socket.set_reuse_port(true).ok();
|
||||
socket.set_reuse_address(true).ok();
|
||||
}
|
||||
// only nonblocking work with tokio, https://stackoverflow.com/questions/64649405/receiver-on-tokiompscchannel-only-receives-messages-when-buffer-is-full
|
||||
socket.set_nonblocking(true)?;
|
||||
if buf_size > 0 {
|
||||
socket.set_recv_buffer_size(buf_size).ok();
|
||||
}
|
||||
log::debug!(
|
||||
"Receive buf size of udp {}: {:?}",
|
||||
addr,
|
||||
socket.recv_buffer_size()
|
||||
);
|
||||
if addr.is_ipv6() && addr.ip().is_unspecified() && addr.port() > 0 {
|
||||
socket.set_only_v6(false).ok();
|
||||
}
|
||||
socket.bind(&addr.into())?;
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
impl FramedSocket {
|
||||
pub async fn new<T: ToSocketAddrs>(addr: T) -> ResultType<Self> {
|
||||
Self::new_reuse(addr, false, 0).await
|
||||
}
|
||||
|
||||
pub async fn new_reuse<T: ToSocketAddrs>(
|
||||
addr: T,
|
||||
reuse: bool,
|
||||
buf_size: usize,
|
||||
) -> ResultType<Self> {
|
||||
let addr = lookup_host(&addr)
|
||||
.await?
|
||||
.next()
|
||||
.context("could not resolve to any address")?;
|
||||
Ok(Self::Direct(UdpFramed::new(
|
||||
UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?,
|
||||
BytesCodec::new(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>(
|
||||
proxy: P,
|
||||
local: T,
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self> {
|
||||
let framed = if username.trim().is_empty() {
|
||||
super::timeout(ms_timeout, Socks5UdpFramed::connect(proxy, Some(local))).await??
|
||||
} else {
|
||||
super::timeout(
|
||||
ms_timeout,
|
||||
Socks5UdpFramed::connect_with_password(proxy, Some(local), username, password),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
log::trace!(
|
||||
"Socks5 udp connected, local addr: {:?}, target addr: {}",
|
||||
framed.local_addr(),
|
||||
framed.socks_addr()
|
||||
);
|
||||
Ok(Self::ProxySocks(framed))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
msg: &impl Message,
|
||||
addr: impl IntoTargetAddr<'_>,
|
||||
) -> ResultType<()> {
|
||||
let addr = addr.into_target_addr()?.to_owned();
|
||||
let send_data = Bytes::from(msg.write_to_bytes()?);
|
||||
match self {
|
||||
Self::Direct(f) => {
|
||||
if let TargetAddr::Ip(addr) = addr {
|
||||
f.send((send_data, addr)).await?
|
||||
}
|
||||
}
|
||||
Self::ProxySocks(f) => f.send((send_data, addr)).await?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/68733302/1926020
|
||||
#[inline]
|
||||
pub async fn send_raw(
|
||||
&mut self,
|
||||
msg: &'static [u8],
|
||||
addr: impl IntoTargetAddr<'static>,
|
||||
) -> ResultType<()> {
|
||||
let addr = addr.into_target_addr()?.to_owned();
|
||||
|
||||
match self {
|
||||
Self::Direct(f) => {
|
||||
if let TargetAddr::Ip(addr) = addr {
|
||||
f.send((Bytes::from(msg), addr)).await?
|
||||
}
|
||||
}
|
||||
Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
|
||||
match self {
|
||||
Self::Direct(f) => match f.next().await {
|
||||
Some(Ok((data, addr))) => {
|
||||
Some(Ok((data, addr.into_target_addr().ok()?.to_owned())))
|
||||
}
|
||||
Some(Err(e)) => Some(Err(anyhow!(e))),
|
||||
None => None,
|
||||
},
|
||||
Self::ProxySocks(f) => match f.next().await {
|
||||
Some(Ok((data, _))) => Some(Ok((data.data, data.dst_addr))),
|
||||
Some(Err(e)) => Some(Err(anyhow!(e))),
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(
|
||||
&mut self,
|
||||
ms: u64,
|
||||
) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
|
||||
if let Ok(res) =
|
||||
tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await
|
||||
{
|
||||
res
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> Option<SocketAddr> {
|
||||
if let FramedSocket::Direct(x) = self {
|
||||
if let Ok(v) = x.get_ref().local_addr() {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
use crate::ResultType;
|
||||
use rustls_pki_types::{ServerName, UnixTime};
|
||||
use std::sync::Arc;
|
||||
use tokio_rustls::rustls::{self, client::WebPkiServerVerifier, ClientConfig};
|
||||
use tokio_rustls::rustls::{
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
DigitallySignedStruct, Error as TLSError, SignatureScheme,
|
||||
};
|
||||
|
||||
// https://github.com/seanmonstar/reqwest/blob/fd61bc93e6f936454ce0b978c6f282f06eee9287/src/tls.rs#L608
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct NoVerifier;
|
||||
|
||||
impl ServerCertVerifier for NoVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls_pki_types::CertificateDer,
|
||||
_intermediates: &[rustls_pki_types::CertificateDer],
|
||||
_server_name: &ServerName,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, TLSError> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls_pki_types::CertificateDer,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TLSError> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls_pki_types::CertificateDer,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TLSError> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
vec![
|
||||
SignatureScheme::RSA_PKCS1_SHA1,
|
||||
SignatureScheme::ECDSA_SHA1_Legacy,
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
SignatureScheme::ED448,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// A certificate verifier that tries a primary verifier first,
|
||||
/// and falls back to a platform verifier if the primary fails.
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
#[derive(Debug)]
|
||||
struct FallbackPlatformVerifier {
|
||||
primary: Arc<dyn ServerCertVerifier>,
|
||||
fallback: Arc<dyn ServerCertVerifier>,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
impl FallbackPlatformVerifier {
|
||||
fn with_platform_fallback(
|
||||
primary: Arc<dyn ServerCertVerifier>,
|
||||
provider: Arc<rustls::crypto::CryptoProvider>,
|
||||
) -> Result<Self, TLSError> {
|
||||
#[cfg(target_os = "android")]
|
||||
if !crate::config::ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
{
|
||||
return Err(TLSError::General(
|
||||
"rustls-platform-verifier not initialized".to_string(),
|
||||
));
|
||||
}
|
||||
let fallback = Arc::new(rustls_platform_verifier::Verifier::new(provider)?);
|
||||
Ok(Self { primary, fallback })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
impl ServerCertVerifier for FallbackPlatformVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
end_entity: &rustls_pki_types::CertificateDer<'_>,
|
||||
intermediates: &[rustls_pki_types::CertificateDer<'_>],
|
||||
server_name: &ServerName<'_>,
|
||||
ocsp_response: &[u8],
|
||||
now: UnixTime,
|
||||
) -> Result<ServerCertVerified, TLSError> {
|
||||
match self.primary.verify_server_cert(
|
||||
end_entity,
|
||||
intermediates,
|
||||
server_name,
|
||||
ocsp_response,
|
||||
now,
|
||||
) {
|
||||
Ok(verified) => Ok(verified),
|
||||
Err(primary_err) => {
|
||||
match self.fallback.verify_server_cert(
|
||||
end_entity,
|
||||
intermediates,
|
||||
server_name,
|
||||
ocsp_response,
|
||||
now,
|
||||
) {
|
||||
Ok(verified) => Ok(verified),
|
||||
Err(fallback_err) => {
|
||||
log::error!(
|
||||
"Both primary and fallback verifiers failed to verify server certificate, primary error: {:?}, fallback error: {:?}",
|
||||
primary_err,
|
||||
fallback_err
|
||||
);
|
||||
Err(primary_err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &rustls_pki_types::CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TLSError> {
|
||||
// Both WebPkiServerVerifier and rustls_platform_verifier use the same signature verification implementation.
|
||||
// https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L278
|
||||
// https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/crypto/mod.rs#L17
|
||||
// https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/android.rs#L9
|
||||
// https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/apple.rs#L6
|
||||
self.primary.verify_tls12_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &rustls_pki_types::CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TLSError> {
|
||||
// Same implementation as verify_tls12_signature.
|
||||
self.primary.verify_tls13_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
// Both WebPkiServerVerifier and rustls_platform_verifier use the same crypto provider,
|
||||
// so their supported signature schemes are identical.
|
||||
// https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L172C52-L172C85
|
||||
// https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/android.rs#L327
|
||||
// https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/apple.rs#L304
|
||||
self.primary.supported_verify_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
fn webpki_server_verifier(
|
||||
provider: Arc<rustls::crypto::CryptoProvider>,
|
||||
) -> ResultType<Arc<WebPkiServerVerifier>> {
|
||||
// Load root certificates from both bundled webpki_roots and system-native certificate stores.
|
||||
// This approach is consistent with how reqwest and tokio-tungstenite handle root certificates.
|
||||
// https://github.com/snapview/tokio-tungstenite/blob/35d110c24c9d030d1608ec964d70c789dfb27452/src/tls.rs#L95
|
||||
// https://github.com/seanmonstar/reqwest/blob/b126ca49da7897e5d676639cdbf67a0f6838b586/src/async_impl/client.rs#L643
|
||||
let mut root_cert_store = rustls::RootCertStore::empty();
|
||||
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let rustls_native_certs::CertificateResult { certs, errors, .. } =
|
||||
rustls_native_certs::load_native_certs();
|
||||
if !errors.is_empty() {
|
||||
log::warn!("native root CA certificate loading errors: {errors:?}");
|
||||
}
|
||||
root_cert_store.add_parsable_certificates(certs);
|
||||
|
||||
// Build verifier using with_root_certificates behavior (WebPkiServerVerifier without CRLs).
|
||||
// Both reqwest and tokio-tungstenite use this approach.
|
||||
// https://github.com/seanmonstar/reqwest/blob/b126ca49da7897e5d676639cdbf67a0f6838b586/src/async_impl/client.rs#L749
|
||||
// https://github.com/snapview/tokio-tungstenite/blob/35d110c24c9d030d1608ec964d70c789dfb27452/src/tls.rs#L127
|
||||
// https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/client/builder.rs#L47
|
||||
// with_root_certificates creates a WebPkiServerVerifier without revocation checking:
|
||||
// https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L177
|
||||
// https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L168
|
||||
// Since no CRL is provided (as is the case here), we must explicitly set allow_unknown_revocation_status()
|
||||
// to match the behavior of with_root_certificates, which allows unknown revocation status by default.
|
||||
// https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L37
|
||||
// Note: build() only returns an error if the root certificate store is empty, which won't happen here.
|
||||
let verifier = rustls::client::WebPkiServerVerifier::builder_with_provider(
|
||||
Arc::new(root_cert_store),
|
||||
provider.clone(),
|
||||
)
|
||||
.allow_unknown_revocation_status()
|
||||
.build()
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
Ok(verifier)
|
||||
}
|
||||
|
||||
pub fn client_config(danger_accept_invalid_cert: bool) -> ResultType<ClientConfig> {
|
||||
if danger_accept_invalid_cert {
|
||||
client_config_danger()
|
||||
} else {
|
||||
client_config_safe()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_config_safe() -> ResultType<ClientConfig> {
|
||||
// Use the default builder which uses the default protocol versions and crypto provider.
|
||||
// The with_protocol_versions API has been removed in rustls master branch:
|
||||
// https://github.com/rustls/rustls/pull/2599
|
||||
// This approach is consistent with tokio-tungstenite's usage:
|
||||
// https://github.com/snapview/tokio-tungstenite/blob/35d110c24c9d030d1608ec964d70c789dfb27452/src/tls.rs#L126
|
||||
let config_builder = rustls::ClientConfig::builder();
|
||||
let provider = config_builder.crypto_provider().clone();
|
||||
let webpki_verifier = webpki_server_verifier(provider.clone())?;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
match FallbackPlatformVerifier::with_platform_fallback(webpki_verifier.clone(), provider) {
|
||||
Ok(fallback_verifier) => {
|
||||
let config = config_builder
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(fallback_verifier))
|
||||
.with_no_client_auth();
|
||||
Ok(config)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to create fallback verifier: {:?}, use webpki verifier instead",
|
||||
e
|
||||
);
|
||||
let config = config_builder
|
||||
.with_webpki_verifier(webpki_verifier)
|
||||
.with_no_client_auth();
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
let config = config_builder
|
||||
.with_webpki_verifier(webpki_verifier)
|
||||
.with_no_client_auth();
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_config_danger() -> ResultType<ClientConfig> {
|
||||
let config = ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerifier))
|
||||
.with_no_client_auth();
|
||||
Ok(config)
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use webrtc::api::setting_engine::SettingEngine;
|
||||
use webrtc::api::APIBuilder;
|
||||
use webrtc::data_channel::RTCDataChannel;
|
||||
use webrtc::ice::mdns::MulticastDnsMode;
|
||||
use webrtc::ice_transport::ice_server::RTCIceServer;
|
||||
use webrtc::peer_connection::configuration::RTCConfiguration;
|
||||
use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState;
|
||||
use webrtc::peer_connection::policy::ice_transport_policy::RTCIceTransportPolicy;
|
||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||
use webrtc::peer_connection::RTCPeerConnection;
|
||||
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use tokio::sync::watch;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::timeout;
|
||||
use url::Url;
|
||||
|
||||
use crate::config;
|
||||
use crate::protobuf::Message;
|
||||
use crate::sodiumoxide::crypto::secretbox::Key;
|
||||
use crate::ResultType;
|
||||
|
||||
pub struct WebRTCStream {
|
||||
pc: Arc<RTCPeerConnection>,
|
||||
stream: Arc<Mutex<Arc<RTCDataChannel>>>,
|
||||
state_notify: watch::Receiver<bool>,
|
||||
send_timeout: u64,
|
||||
}
|
||||
|
||||
/// Standard maximum message size for WebRTC data channels (RFC 8831, 65535 bytes).
|
||||
/// Most browsers, including Chromium, enforce this protocol limit.
|
||||
const DATA_CHANNEL_BUFFER_SIZE: u16 = u16::MAX;
|
||||
|
||||
// use 3 public STUN servers to find out the NAT type, 2 must be the same address but different ports
|
||||
// https://stackoverflow.com/questions/72805316/determine-nat-mapping-behaviour-using-two-stun-servers
|
||||
// luckily nextcloud supports two ports for STUN
|
||||
// unluckily webrtc-rs does not use the same port to do the STUN request
|
||||
static DEFAULT_ICE_SERVERS: [&str; 3] = [
|
||||
"stun:stun.cloudflare.com:3478",
|
||||
"stun:stun.nextcloud.com:3478",
|
||||
"stun:stun.nextcloud.com:443",
|
||||
];
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref SESSIONS: Arc::<Mutex<HashMap<String, WebRTCStream>>> = Default::default();
|
||||
}
|
||||
|
||||
impl Clone for WebRTCStream {
|
||||
fn clone(&self) -> Self {
|
||||
WebRTCStream {
|
||||
pc: self.pc.clone(),
|
||||
stream: self.stream.clone(),
|
||||
state_notify: self.state_notify.clone(),
|
||||
send_timeout: self.send_timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebRTCStream {
|
||||
#[inline]
|
||||
fn get_remote_offer(endpoint: &str) -> ResultType<String> {
|
||||
// Ensure the endpoint starts with the "webrtc://" prefix
|
||||
if !endpoint.starts_with("webrtc://") {
|
||||
return Err(
|
||||
Error::new(ErrorKind::InvalidInput, "Invalid WebRTC endpoint format").into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Extract the Base64-encoded SDP part
|
||||
let encoded_sdp = &endpoint["webrtc://".len()..];
|
||||
// Decode the Base64 string
|
||||
let decoded_bytes = BASE64_STANDARD
|
||||
.decode(encoded_sdp)
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidInput, "Failed to decode Base64 SDP"))?;
|
||||
Ok(String::from_utf8(decoded_bytes).map_err(|_| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Failed to convert decoded bytes to UTF-8",
|
||||
)
|
||||
})?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sdp_to_endpoint(sdp: &str) -> String {
|
||||
let encoded_sdp = BASE64_STANDARD.encode(sdp);
|
||||
format!("webrtc://{}", encoded_sdp)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_key_for_sdp(sdp: &RTCSessionDescription) -> ResultType<String> {
|
||||
let binding = sdp.unmarshal()?;
|
||||
let Some(fingerprint) = binding.attribute("fingerprint") else {
|
||||
// find fingerprint attribute in media descriptions
|
||||
for media in &binding.media_descriptions {
|
||||
if media.media_name.media != "application" {
|
||||
continue;
|
||||
}
|
||||
if let Some(fp) = media
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|x| x.key == "fingerprint")
|
||||
.and_then(|x| x.value.clone())
|
||||
{
|
||||
return Ok(fp);
|
||||
}
|
||||
}
|
||||
return Err(anyhow::anyhow!("SDP fingerprint attribute not found"));
|
||||
};
|
||||
Ok(fingerprint.to_string())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_key_for_sdp_json(sdp_json: &str) -> ResultType<String> {
|
||||
if sdp_json.is_empty() {
|
||||
return Ok("".to_string());
|
||||
}
|
||||
let sdp = serde_json::from_str::<RTCSessionDescription>(&sdp_json)?;
|
||||
Self::get_key_for_sdp(&sdp)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn get_key_for_peer(pc: &Arc<RTCPeerConnection>, is_local: bool) -> ResultType<String> {
|
||||
let Some(desc) = (match is_local {
|
||||
true => pc.local_description().await,
|
||||
false => pc.remote_description().await,
|
||||
}) else {
|
||||
return Err(anyhow::anyhow!("PeerConnection description is not set"));
|
||||
};
|
||||
Self::get_key_for_sdp(&desc)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_ice_server_from_url(url: &str) -> Option<RTCIceServer> {
|
||||
// standard url format with turn scheme: turn://user:pass@host:port
|
||||
match Url::parse(url) {
|
||||
Ok(u) => {
|
||||
if u.scheme() == "turn"
|
||||
|| u.scheme() == "turns"
|
||||
|| u.scheme() == "stun"
|
||||
|| u.scheme() == "stuns"
|
||||
{
|
||||
Some(RTCIceServer {
|
||||
urls: vec![format!(
|
||||
"{}:{}:{}",
|
||||
u.scheme(),
|
||||
u.host_str().unwrap_or_default(),
|
||||
u.port().unwrap_or(3478)
|
||||
)],
|
||||
username: u.username().to_string(),
|
||||
credential: u.password().unwrap_or_default().to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_ice_servers() -> Vec<RTCIceServer> {
|
||||
let mut ice_servers = Vec::new();
|
||||
let cfg = config::Config::get_option(config::keys::OPTION_ICE_SERVERS);
|
||||
|
||||
let mut has_stun = false;
|
||||
|
||||
for url in cfg.split(',').map(str::trim) {
|
||||
if let Some(ice_server) = Self::get_ice_server_from_url(url) {
|
||||
// Detect STUN in user config
|
||||
if ice_server
|
||||
.urls
|
||||
.iter()
|
||||
.any(|u| u.starts_with("stun:") || u.starts_with("stuns:"))
|
||||
{
|
||||
has_stun = true;
|
||||
}
|
||||
|
||||
ice_servers.push(ice_server);
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no STUN (either TURN-only or empty config) → prepend defaults
|
||||
if !has_stun {
|
||||
ice_servers.insert(
|
||||
0,
|
||||
RTCIceServer {
|
||||
urls: DEFAULT_ICE_SERVERS.iter().map(|s| s.to_string()).collect(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
ice_servers
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
remote_endpoint: &str,
|
||||
force_relay: bool,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self> {
|
||||
log::debug!("New webrtc stream to endpoint: {}", remote_endpoint);
|
||||
let remote_offer = if remote_endpoint.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
Self::get_remote_offer(remote_endpoint)?
|
||||
};
|
||||
|
||||
let mut key = Self::get_key_for_sdp_json(&remote_offer)?;
|
||||
let sessions_lock = SESSIONS.lock().await;
|
||||
if let Some(cached_stream) = sessions_lock.get(&key) {
|
||||
if !key.is_empty() {
|
||||
log::debug!("Start webrtc with cached peer");
|
||||
return Ok(cached_stream.clone());
|
||||
}
|
||||
}
|
||||
drop(sessions_lock);
|
||||
|
||||
let start_local_offer = remote_offer.is_empty();
|
||||
// Create a SettingEngine and enable Detach
|
||||
let mut s = SettingEngine::default();
|
||||
s.detach_data_channels();
|
||||
s.set_ice_multicast_dns_mode(MulticastDnsMode::Disabled);
|
||||
|
||||
// Create the API object
|
||||
let api = APIBuilder::new().with_setting_engine(s).build();
|
||||
|
||||
// Prepare the configuration, get ICE servers from config
|
||||
let config = RTCConfiguration {
|
||||
ice_servers: Self::get_ice_servers(),
|
||||
ice_transport_policy: if force_relay {
|
||||
RTCIceTransportPolicy::Relay
|
||||
} else {
|
||||
RTCIceTransportPolicy::All
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (notify_tx, notify_rx) = watch::channel(false);
|
||||
// Create a new RTCPeerConnection
|
||||
let pc = Arc::new(api.new_peer_connection(config).await?);
|
||||
let bootstrap_dc = if start_local_offer {
|
||||
let dc_open_notify = notify_tx.clone();
|
||||
// Create a data channel with label "bootstrap"
|
||||
let dc = pc.create_data_channel("bootstrap", None).await?;
|
||||
dc.on_open(Box::new(move || {
|
||||
log::debug!("Local data channel bootstrap open.");
|
||||
let _ = dc_open_notify.send(true);
|
||||
Box::pin(async {})
|
||||
}));
|
||||
dc
|
||||
} else {
|
||||
// Wait for the data channel to be created by the remote peer
|
||||
// Here we create a dummy data channel to satisfy the type system
|
||||
Arc::new(RTCDataChannel::default())
|
||||
};
|
||||
|
||||
let stream = Arc::new(Mutex::new(bootstrap_dc));
|
||||
if !start_local_offer {
|
||||
// Register data channel creation handling
|
||||
let dc_open_notify = notify_tx.clone();
|
||||
let stream_for_dc = stream.clone();
|
||||
pc.on_data_channel(Box::new(move |dc: Arc<RTCDataChannel>| {
|
||||
let d_label = dc.label().to_owned();
|
||||
let dc_open_notify2 = dc_open_notify.clone();
|
||||
let stream_for_dc_clone = stream_for_dc.clone();
|
||||
log::debug!("Remote data channel {} ready", d_label);
|
||||
Box::pin(async move {
|
||||
let mut stream_lock = stream_for_dc_clone.lock().await;
|
||||
*stream_lock = dc.clone();
|
||||
drop(stream_lock);
|
||||
dc.on_open(Box::new(move || {
|
||||
let _ = dc_open_notify2.send(true);
|
||||
Box::pin(async {})
|
||||
}));
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
// This will notify you when the peer has connected/disconnected
|
||||
let stream_for_close = stream.clone();
|
||||
let pc_for_close = pc.clone();
|
||||
pc.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| {
|
||||
let stream_for_close2 = stream_for_close.clone();
|
||||
let on_connection_notify = notify_tx.clone();
|
||||
let pc_for_close2 = pc_for_close.clone();
|
||||
Box::pin(async move {
|
||||
log::debug!("WebRTC session peer connection state: {}", s);
|
||||
match s {
|
||||
RTCPeerConnectionState::Disconnected
|
||||
| RTCPeerConnectionState::Failed
|
||||
| RTCPeerConnectionState::Closed => {
|
||||
let _ = on_connection_notify.send(true);
|
||||
log::debug!("WebRTC session closing due to disconnected");
|
||||
let _ = stream_for_close2.lock().await.close().await;
|
||||
log::debug!("WebRTC session stream closed");
|
||||
|
||||
let mut sessions_lock = SESSIONS.lock().await;
|
||||
match Self::get_key_for_peer(&pc_for_close2, start_local_offer).await {
|
||||
Ok(k) => {
|
||||
sessions_lock.remove(&k);
|
||||
log::debug!("WebRTC session removed key: {}", k);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to extract key for peer during session cleanup: {:?}",
|
||||
e
|
||||
);
|
||||
// Fallback: try to remove any session associated with this peer connection
|
||||
let keys_to_remove: Vec<String> = sessions_lock
|
||||
.iter()
|
||||
.filter_map(|(key, session)| {
|
||||
if Arc::ptr_eq(&session.pc, &pc_for_close2) {
|
||||
Some(key.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for k in keys_to_remove {
|
||||
sessions_lock.remove(&k);
|
||||
log::debug!("WebRTC session removed by fallback key: {}", k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
// process offer/answer
|
||||
if start_local_offer {
|
||||
let sdp = pc.create_offer(None).await?;
|
||||
let mut gather_complete = pc.gathering_complete_promise().await;
|
||||
pc.set_local_description(sdp.clone()).await?;
|
||||
let _ = gather_complete.recv().await;
|
||||
|
||||
log::debug!("local offer:\n{}", sdp.sdp);
|
||||
// get local sdp key
|
||||
key = Self::get_key_for_sdp(&sdp)?;
|
||||
log::debug!("Start webrtc with local key: {}", key);
|
||||
} else {
|
||||
let sdp = serde_json::from_str::<RTCSessionDescription>(&remote_offer)?;
|
||||
pc.set_remote_description(sdp.clone()).await?;
|
||||
let answer = pc.create_answer(None).await?;
|
||||
let mut gather_complete = pc.gathering_complete_promise().await;
|
||||
pc.set_local_description(answer).await?;
|
||||
let _ = gather_complete.recv().await;
|
||||
|
||||
log::debug!("remote offer:\n{}", sdp.sdp);
|
||||
// get remote sdp key
|
||||
key = Self::get_key_for_sdp(&sdp)?;
|
||||
log::debug!("Start webrtc with remote key: {}", key);
|
||||
}
|
||||
|
||||
let mut final_lock = SESSIONS.lock().await;
|
||||
if let Some(session) = final_lock.get(&key) {
|
||||
pc.close().await.ok();
|
||||
return Ok(session.clone());
|
||||
}
|
||||
|
||||
let webrtc_stream = Self {
|
||||
pc,
|
||||
stream,
|
||||
state_notify: notify_rx,
|
||||
send_timeout: ms_timeout,
|
||||
};
|
||||
final_lock.insert(key, webrtc_stream.clone());
|
||||
Ok(webrtc_stream)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn get_local_endpoint(&self) -> ResultType<String> {
|
||||
if let Some(local_desc) = self.pc.local_description().await {
|
||||
let sdp = serde_json::to_string(&local_desc)?;
|
||||
let endpoint = Self::sdp_to_endpoint(&sdp);
|
||||
Ok(endpoint)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Local desc is not set"))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn set_remote_endpoint(&self, endpoint: &str) -> ResultType<()> {
|
||||
let offer = Self::get_remote_offer(endpoint)?;
|
||||
log::debug!("WebRTC set remote sdp: {}", offer);
|
||||
let sdp = serde_json::from_str::<RTCSessionDescription>(&offer)?;
|
||||
self.pc.set_remote_description(sdp).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_raw(&mut self) {
|
||||
// not-supported
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_send_timeout(&mut self, ms: u64) {
|
||||
self.send_timeout = ms;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_key(&mut self, _key: Key) {
|
||||
// not-supported
|
||||
// WebRTC uses built-in DTLS encryption for secure communication.
|
||||
// DTLS handles key exchange and encryption automatically, so explicit key management is not required.
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_secured(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> {
|
||||
self.send_raw(msg.write_to_bytes()?).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, msg: Vec<u8>) -> ResultType<()> {
|
||||
self.send_bytes(Bytes::from(msg)).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn wait_for_connect_result(&mut self) {
|
||||
if *self.state_notify.borrow() {
|
||||
return;
|
||||
}
|
||||
let _ = self.state_notify.changed().await;
|
||||
}
|
||||
|
||||
pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> {
|
||||
if self.send_timeout > 0 {
|
||||
match timeout(
|
||||
Duration::from_millis(self.send_timeout),
|
||||
self.wait_for_connect_result(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
self.pc.close().await.ok();
|
||||
return Err(Error::new(
|
||||
ErrorKind::TimedOut,
|
||||
"WebRTC send wait for connect timeout",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.wait_for_connect_result().await;
|
||||
}
|
||||
let stream = self.stream.lock().await.clone();
|
||||
stream.send(&bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Result<BytesMut, Error>> {
|
||||
self.wait_for_connect_result().await;
|
||||
let stream = self.stream.lock().await.clone();
|
||||
|
||||
// TODO reuse buffer?
|
||||
let mut buffer = BytesMut::zeroed(DATA_CHANNEL_BUFFER_SIZE as usize);
|
||||
let dc = stream.detach().await.ok()?;
|
||||
let n = match dc.read(&mut buffer).await {
|
||||
Ok(n) => n,
|
||||
Err(err) => {
|
||||
self.pc.close().await.ok();
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("data channel read error: {}", err),
|
||||
)));
|
||||
}
|
||||
};
|
||||
if n == 0 {
|
||||
self.pc.close().await.ok();
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"data channel read exited with 0 bytes",
|
||||
)));
|
||||
}
|
||||
buffer.truncate(n);
|
||||
Some(Ok(buffer))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(&mut self, ms: u64) -> Option<Result<BytesMut, Error>> {
|
||||
match timeout(Duration::from_millis(ms), self.next()).await {
|
||||
Ok(res) => res,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_webrtc_endpoint(endpoint: &str) -> bool {
|
||||
// use sdp base64 json string as endpoint, or prefix webrtc:
|
||||
endpoint.starts_with("webrtc://")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config;
|
||||
use crate::webrtc::WebRTCStream;
|
||||
use crate::webrtc::DEFAULT_ICE_SERVERS;
|
||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||
|
||||
#[test]
|
||||
fn test_webrtc_ice_url() {
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_server_from_url("turn://example.com:3478")
|
||||
.unwrap_or_default()
|
||||
.urls[0],
|
||||
"turn:example.com:3478"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_server_from_url("turn://example.com")
|
||||
.unwrap_or_default()
|
||||
.urls[0],
|
||||
"turn:example.com:3478"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_server_from_url("turn://123@example.com")
|
||||
.unwrap_or_default()
|
||||
.username,
|
||||
"123"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_server_from_url("turn://123@example.com")
|
||||
.unwrap_or_default()
|
||||
.credential,
|
||||
""
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_server_from_url("turn://123:321@example.com")
|
||||
.unwrap_or_default()
|
||||
.credential,
|
||||
"321"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_server_from_url("stun://example.com:3478")
|
||||
.unwrap_or_default()
|
||||
.urls[0],
|
||||
"stun:example.com:3478"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_server_from_url("http://123:123@example.com:3478"),
|
||||
None
|
||||
);
|
||||
|
||||
config::Config::set_option("ice-servers".to_string(), "".to_string());
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_servers()[0].urls[0],
|
||||
DEFAULT_ICE_SERVERS[0].to_string()
|
||||
);
|
||||
|
||||
config::Config::set_option(
|
||||
"ice-servers".to_string(),
|
||||
",stun://example.com,turn://example.com,sdf".to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_servers()[0].urls[0],
|
||||
"stun:example.com:3478"
|
||||
);
|
||||
assert_eq!(
|
||||
WebRTCStream::get_ice_servers()[1].urls[0],
|
||||
"turn:example.com:3478"
|
||||
);
|
||||
assert_eq!(WebRTCStream::get_ice_servers().len(), 2);
|
||||
config::Config::set_option(
|
||||
"ice-servers".to_string(),
|
||||
"".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webrtc_session_key() {
|
||||
let mut sdp_str = "".to_owned();
|
||||
assert_eq!(
|
||||
WebRTCStream::get_key_for_sdp(
|
||||
&RTCSessionDescription::offer(sdp_str).unwrap_or_default()
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
""
|
||||
);
|
||||
|
||||
sdp_str = "\
|
||||
v=0
|
||||
o=- 7400546379179479477 208696200 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=fingerprint:sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88
|
||||
a=group:BUNDLE 0
|
||||
a=extmap-allow-mixed
|
||||
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
|
||||
c=IN IP4 0.0.0.0
|
||||
a=setup:actpass
|
||||
a=mid:0
|
||||
a=sendrecv
|
||||
a=sctp-port:5000
|
||||
a=ice-ufrag:RMWjjpXfpXbDPdMz
|
||||
a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT".to_owned();
|
||||
assert_eq!(
|
||||
WebRTCStream::get_key_for_sdp(
|
||||
&RTCSessionDescription::offer(sdp_str).unwrap_or_default()
|
||||
).unwrap_or_default(),
|
||||
"sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88"
|
||||
);
|
||||
|
||||
sdp_str = "\
|
||||
v=0
|
||||
o=- 7400546379179479477 208696200 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE 0
|
||||
a=extmap-allow-mixed
|
||||
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
|
||||
c=IN IP4 0.0.0.0
|
||||
a=fingerprint:sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88
|
||||
a=setup:actpass
|
||||
a=mid:0
|
||||
a=sendrecv
|
||||
a=sctp-port:5000
|
||||
a=ice-ufrag:RMWjjpXfpXbDPdMz
|
||||
a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT".to_owned();
|
||||
assert_eq!(
|
||||
WebRTCStream::get_key_for_sdp(
|
||||
&RTCSessionDescription::offer(sdp_str).unwrap_or_default()
|
||||
).unwrap_or_default(),
|
||||
"sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88"
|
||||
);
|
||||
|
||||
sdp_str = "\
|
||||
v=0
|
||||
o=- 7400546379179479477 208696200 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE 0
|
||||
a=extmap-allow-mixed
|
||||
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
|
||||
c=IN IP4 0.0.0.0
|
||||
a=setup:actpass
|
||||
a=mid:0
|
||||
a=sendrecv
|
||||
a=sctp-port:5000
|
||||
a=ice-ufrag:RMWjjpXfpXbDPdMz
|
||||
a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT"
|
||||
.to_owned();
|
||||
assert!(
|
||||
WebRTCStream::get_key_for_sdp(
|
||||
&RTCSessionDescription::offer(sdp_str).unwrap_or_default()
|
||||
)
|
||||
.is_err(),
|
||||
"can not find fingerprint attribute"
|
||||
);
|
||||
|
||||
sdp_str = "\
|
||||
v=0
|
||||
o=- 7400546379179479477 208696200 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE 0
|
||||
a=extmap-allow-mixed
|
||||
m=audio 9 UDP/DTLS/SCTP webrtc-datachannel
|
||||
c=IN IP4 0.0.0.0
|
||||
a=fingerprint:sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88
|
||||
a=setup:actpass
|
||||
a=mid:0
|
||||
a=sendrecv
|
||||
a=sctp-port:5000
|
||||
a=ice-ufrag:RMWjjpXfpXbDPdMz
|
||||
a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT".to_owned();
|
||||
assert!(
|
||||
WebRTCStream::get_key_for_sdp(
|
||||
&RTCSessionDescription::offer(sdp_str).unwrap_or_default()
|
||||
)
|
||||
.is_err(),
|
||||
"can not find datachannel fingerprint attribute"
|
||||
);
|
||||
|
||||
assert!(
|
||||
WebRTCStream::get_key_for_sdp(
|
||||
&RTCSessionDescription::offer("".to_owned()).unwrap_or_default()
|
||||
)
|
||||
.is_err(),
|
||||
"invalid sdp should error"
|
||||
);
|
||||
|
||||
assert!(
|
||||
WebRTCStream::get_key_for_sdp_json("{}").is_err(),
|
||||
"empty sdp json should error"
|
||||
);
|
||||
|
||||
assert!(
|
||||
WebRTCStream::get_key_for_sdp_json("{ss}").is_err(),
|
||||
"invalid sdp json should error"
|
||||
);
|
||||
|
||||
let endpoint = "webrtc://eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoidj0wXHJcbm89LSA0MTA1NDk3NTY2NDgyMTQzODEwIDYwMzk1NzQw\
|
||||
MCBJTiBJUDQgMC4wLjAuMFxyXG5zPS1cclxudD0wIDBcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDYxOjYwOjc0OjQwOjI4OkNFOjBCOjBDOjc1OjRCOj\
|
||||
EwOjlBOkVFOjc3OkY1OjQ0OjU3Ojg0OjUxOkRCOjA0OjkyOjRBOjEwOjFDOjRFOjVGOjdFOkYxOkIzOjcxOjIyXHJcbmE9Z3JvdXA6QlVORExFIDBcclxu\
|
||||
YT1leHRtYXAtYWxsb3ctbWl4ZWRcclxubT1hcHBsaWNhdGlvbiA5IFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDAuMC\
|
||||
4wLjBcclxuYT1zZXR1cDphY3RpdmVcclxuYT1taWQ6MFxyXG5hPXNlbmRyZWN2XHJcbmE9c2N0cC1wb3J0OjUwMDBcclxuYT1pY2UtdWZyYWc6SHlnU1Rr\
|
||||
V2RsRlpHRG1XWlxyXG5hPWljZS1wd2Q6SkJneFZWaGZveVhHdHZha1VWcnBQeHVOSVpMU3llS1pcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDEgdWRwID\
|
||||
IxMzA3MDY0MzEgMTkyLjE2OC4xLjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDIgdWRwIDIxMzA3MDY0MzEgMTkyLjE2OC4x\
|
||||
LjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MTg2MTA0NTE5MCAxIHVkcCAxNjk0NDk4ODE1IDE0LjIxMi42OC4xMiAyNzAwNCB0eXAgc3JmbH\
|
||||
ggcmFkZHIgMC4wLjAuMCBycG9ydCA2NDAwOFxyXG5hPWNhbmRpZGF0ZToxODYxMDQ1MTkwIDIgdWRwIDE2OTQ0OTg4MTUgMTQuMjEyLjY4LjEyIDI3MDA0\
|
||||
IHR5cCBzcmZseCByYWRkciAwLjAuMC4wIHJwb3J0IDY0MDA4XHJcbmE9ZW5kLW9mLWNhbmRpZGF0ZXNcclxuIn0=".to_owned();
|
||||
assert_eq!(
|
||||
WebRTCStream::get_key_for_sdp_json(
|
||||
&WebRTCStream::get_remote_offer(&endpoint).unwrap_or_default()
|
||||
).unwrap_or_default(),
|
||||
"sha-256 61:60:74:40:28:CE:0B:0C:75:4B:10:9A:EE:77:F5:44:57:84:51:DB:04:92:4A:10:1C:4E:5F:7E:F1:B3:71:22"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_webrtc_new_stream() {
|
||||
let mut endpoint = "webrtc://sdfsdf".to_owned();
|
||||
assert!(
|
||||
WebRTCStream::new(&endpoint, false, 10000).await.is_err(),
|
||||
"invalid webrtc endpoint should error"
|
||||
);
|
||||
|
||||
endpoint = "wss://sdfsdf".to_owned();
|
||||
assert!(
|
||||
WebRTCStream::new(&endpoint, false, 10000).await.is_err(),
|
||||
"invalid webrtc endpoint should error"
|
||||
);
|
||||
|
||||
assert!(
|
||||
WebRTCStream::new("", false, 10000).await.is_ok(),
|
||||
"local webrtc endpoint should ok"
|
||||
);
|
||||
|
||||
endpoint = "webrtc://eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoidj0wXHJcbm89LSA0MTA1NDk3NTY2NDgyMTQzODEwIDYwMzk1NzQw\
|
||||
MCBJTiBJUDQgMC4wLjAuMFxyXG5zPS1cclxudD0wIDBcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDYxOjYwOjc0OjQwOjI4OkNFOjBCOjBDOjc1OjRCOj\
|
||||
EwOjlBOkVFOjc3OkY1OjQ0OjU3Ojg0OjUxOkRCOjA0OjkyOjRBOjEwOjFDOjRFOjVGOjdFOkYxOkIzOjcxOjIyXHJcbmE9Z3JvdXA6QlVORExFIDBcclxu\
|
||||
YT1leHRtYXAtYWxsb3ctbWl4ZWRcclxubT1hcHBsaWNhdGlvbiA5IFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDAuMC\
|
||||
4wLjBcclxuYT1zZXR1cDphY3RpdmVcclxuYT1taWQ6MFxyXG5hPXNlbmRyZWN2XHJcbmE9c2N0cC1wb3J0OjUwMDBcclxuYT1pY2UtdWZyYWc6SHlnU1Rr\
|
||||
V2RsRlpHRG1XWlxyXG5hPWljZS1wd2Q6SkJneFZWaGZveVhHdHZha1VWcnBQeHVOSVpMU3llS1pcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDEgdWRwID\
|
||||
IxMzA3MDY0MzEgMTkyLjE2OC4xLjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDIgdWRwIDIxMzA3MDY0MzEgMTkyLjE2OC4x\
|
||||
LjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MTg2MTA0NTE5MCAxIHVkcCAxNjk0NDk4ODE1IDE0LjIxMi42OC4xMiAyNzAwNCB0eXAgc3JmbH\
|
||||
ggcmFkZHIgMC4wLjAuMCBycG9ydCA2NDAwOFxyXG5hPWNhbmRpZGF0ZToxODYxMDQ1MTkwIDIgdWRwIDE2OTQ0OTg4MTUgMTQuMjEyLjY4LjEyIDI3MDA0\
|
||||
IHR5cCBzcmZseCByYWRkciAwLjAuMC4wIHJwb3J0IDY0MDA4XHJcbmE9ZW5kLW9mLWNhbmRpZGF0ZXNcclxuIn0=".to_owned();
|
||||
assert!(
|
||||
WebRTCStream::new(&endpoint, false, 10000).await.is_err(),
|
||||
"connect to an 'answer' webrtc endpoint should error"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
use crate::{
|
||||
config::{
|
||||
keys::OPTION_RELAY_SERVER, use_ws, Config, Socks5Server, RELAY_PORT, RENDEZVOUS_PORT,
|
||||
},
|
||||
protobuf::Message,
|
||||
socket_client::split_host_port,
|
||||
sodiumoxide::crypto::secretbox::Key,
|
||||
tcp::Encrypt,
|
||||
tls::{get_cached_tls_accept_invalid_cert, get_cached_tls_type, upsert_tls_cache, TlsType},
|
||||
ResultType,
|
||||
};
|
||||
use anyhow::bail;
|
||||
use async_recursion::async_recursion;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use std::{
|
||||
io::{Error, ErrorKind},
|
||||
net::SocketAddr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{net::TcpStream, time::timeout};
|
||||
use tokio_native_tls::native_tls::TlsConnector;
|
||||
use tokio_tungstenite::{
|
||||
connect_async_tls_with_config, tungstenite::protocol::Message as WsMessage, Connector,
|
||||
MaybeTlsStream, WebSocketStream,
|
||||
};
|
||||
use tungstenite::client::IntoClientRequest;
|
||||
use tungstenite::protocol::Role;
|
||||
|
||||
pub struct WsFramedStream {
|
||||
stream: WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
addr: SocketAddr,
|
||||
encrypt: Option<Encrypt>,
|
||||
send_timeout: u64,
|
||||
}
|
||||
|
||||
impl WsFramedStream {
|
||||
#[inline]
|
||||
fn get_connector(
|
||||
tls_type: &TlsType,
|
||||
danger_accept_invalid_certs: bool,
|
||||
) -> ResultType<Option<Connector>> {
|
||||
match tls_type {
|
||||
TlsType::Plain => Ok(Some(Connector::Plain)),
|
||||
TlsType::NativeTls => {
|
||||
let connector = TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(danger_accept_invalid_certs)
|
||||
.build()?;
|
||||
Ok(Some(Connector::NativeTls(connector)))
|
||||
}
|
||||
TlsType::Rustls => {
|
||||
let connector = match crate::verifier::client_config(danger_accept_invalid_certs) {
|
||||
Ok(client_config) => Some(Connector::Rustls(Arc::new(client_config))),
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to get client config: {:?}, fallback to default connector",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok(connector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
url: &str,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
||||
// to-do: websocket proxy.
|
||||
|
||||
let tls_type = get_cached_tls_type(url);
|
||||
let is_tls_type_cached = tls_type.is_some();
|
||||
let tls_type = tls_type.unwrap_or(TlsType::Rustls);
|
||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(&url);
|
||||
Self::try_connect(
|
||||
url,
|
||||
ms_timeout,
|
||||
tls_type,
|
||||
is_tls_type_cached,
|
||||
danger_accept_invalid_cert,
|
||||
danger_accept_invalid_cert,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn try_connect(
|
||||
url: &str,
|
||||
ms_timeout: u64,
|
||||
tls_type: TlsType,
|
||||
is_tls_type_cached: bool,
|
||||
danger_accept_invalid_cert: Option<bool>,
|
||||
original_danger_accept_invalid_certs: Option<bool>,
|
||||
) -> ResultType<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
||||
let ws_config = None;
|
||||
let disable_nagle = false;
|
||||
let request = url
|
||||
.into_client_request()
|
||||
.map_err(|e| Error::new(ErrorKind::Other, e))?;
|
||||
let connector =
|
||||
Self::get_connector(&tls_type, danger_accept_invalid_cert.unwrap_or(false))?;
|
||||
match timeout(
|
||||
Duration::from_millis(ms_timeout),
|
||||
connect_async_tls_with_config(request, ws_config, disable_nagle, connector),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok((ws_stream, _)) => {
|
||||
upsert_tls_cache(url, tls_type, danger_accept_invalid_cert.unwrap_or(false));
|
||||
Ok(ws_stream)
|
||||
}
|
||||
Err(e) => match (tls_type, is_tls_type_cached, danger_accept_invalid_cert) {
|
||||
(TlsType::Rustls, _, None) => {
|
||||
log::warn!(
|
||||
"WebSocket connection with rustls-tls failed, try accept invalid certs: {}, {:?}",
|
||||
url,
|
||||
e
|
||||
);
|
||||
Self::try_connect(
|
||||
url,
|
||||
ms_timeout,
|
||||
tls_type,
|
||||
is_tls_type_cached,
|
||||
Some(true),
|
||||
original_danger_accept_invalid_certs,
|
||||
)
|
||||
.await
|
||||
}
|
||||
(TlsType::Rustls, false, Some(_)) => {
|
||||
log::warn!(
|
||||
"WebSocket connection with rustls-tls failed, try native-tls: {}, {:?}",
|
||||
url,
|
||||
e
|
||||
);
|
||||
Self::try_connect(
|
||||
url,
|
||||
ms_timeout,
|
||||
TlsType::NativeTls,
|
||||
is_tls_type_cached,
|
||||
original_danger_accept_invalid_certs,
|
||||
original_danger_accept_invalid_certs,
|
||||
)
|
||||
.await
|
||||
}
|
||||
(TlsType::NativeTls, _, None) => {
|
||||
log::warn!(
|
||||
"WebSocket connection with native-tls failed, try accept invalid certs: {}, {:?}",
|
||||
url,
|
||||
e
|
||||
);
|
||||
Self::try_connect(
|
||||
url,
|
||||
ms_timeout,
|
||||
tls_type,
|
||||
is_tls_type_cached,
|
||||
Some(true),
|
||||
original_danger_accept_invalid_certs,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"WebSocket connection failed with tls_type {:?}: {}, {:?}",
|
||||
tls_type,
|
||||
url,
|
||||
e
|
||||
);
|
||||
if let tungstenite::Error::Http(response) = &e {
|
||||
if response.status().is_redirection() {
|
||||
bail!(
|
||||
"WebSocket connection failed ({}). The server may not support WebSocket.",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
bail!("WebSocket error: {}", e)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new<T: AsRef<str>>(
|
||||
url: T,
|
||||
_local_addr: Option<SocketAddr>,
|
||||
_proxy_conf: Option<&Socks5Server>,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self> {
|
||||
let stream = Self::connect(url.as_ref(), ms_timeout).await?;
|
||||
let addr = match stream.get_ref() {
|
||||
MaybeTlsStream::Plain(tcp) => tcp.peer_addr()?,
|
||||
MaybeTlsStream::NativeTls(tls) => tls.get_ref().get_ref().get_ref().peer_addr()?,
|
||||
MaybeTlsStream::Rustls(tls) => tls.get_ref().0.peer_addr()?,
|
||||
_ => return Err(Error::new(ErrorKind::Other, "Unsupported stream type").into()),
|
||||
};
|
||||
|
||||
let ws = Self {
|
||||
stream,
|
||||
addr,
|
||||
encrypt: None,
|
||||
send_timeout: ms_timeout,
|
||||
};
|
||||
|
||||
Ok(ws)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_raw(&mut self) {
|
||||
self.encrypt = None;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn from_tcp_stream(stream: TcpStream, addr: SocketAddr) -> ResultType<Self> {
|
||||
let ws_stream =
|
||||
WebSocketStream::from_raw_socket(MaybeTlsStream::Plain(stream), Role::Client, None)
|
||||
.await;
|
||||
|
||||
Ok(Self {
|
||||
stream: ws_stream,
|
||||
addr,
|
||||
encrypt: None,
|
||||
send_timeout: 0,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.addr
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_send_timeout(&mut self, ms: u64) {
|
||||
self.send_timeout = ms;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_key(&mut self, key: Key) {
|
||||
self.encrypt = Some(Encrypt::new(key));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_secured(&self) -> bool {
|
||||
self.encrypt.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> {
|
||||
self.send_raw(msg.write_to_bytes()?).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, msg: Vec<u8>) -> ResultType<()> {
|
||||
let mut msg = msg;
|
||||
if let Some(key) = self.encrypt.as_mut() {
|
||||
msg = key.enc(&msg);
|
||||
}
|
||||
self.send_bytes(Bytes::from(msg)).await
|
||||
}
|
||||
|
||||
pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> {
|
||||
let msg = WsMessage::Binary(bytes);
|
||||
if self.send_timeout > 0 {
|
||||
timeout(
|
||||
Duration::from_millis(self.send_timeout),
|
||||
self.stream.send(msg),
|
||||
)
|
||||
.await??
|
||||
} else {
|
||||
self.stream.send(msg).await?
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Result<BytesMut, Error>> {
|
||||
while let Some(msg) = self.stream.next().await {
|
||||
let msg = match msg {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("WebSocket protocol error: {}", e),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
match msg {
|
||||
WsMessage::Binary(data) => {
|
||||
let mut bytes = BytesMut::from(&data[..]);
|
||||
if let Some(key) = self.encrypt.as_mut() {
|
||||
if let Err(err) = key.dec(&mut bytes) {
|
||||
return Some(Err(err));
|
||||
}
|
||||
}
|
||||
return Some(Ok(bytes));
|
||||
}
|
||||
WsMessage::Text(text) => {
|
||||
let bytes = BytesMut::from(text.as_bytes());
|
||||
return Some(Ok(bytes));
|
||||
}
|
||||
WsMessage::Close(_) => {
|
||||
return None;
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(&mut self, ms: u64) -> Option<Result<BytesMut, Error>> {
|
||||
match timeout(Duration::from_millis(ms), self.next()).await {
|
||||
Ok(res) => res,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ws_endpoint(endpoint: &str) -> bool {
|
||||
endpoint.starts_with("ws://") || endpoint.starts_with("wss://")
|
||||
}
|
||||
|
||||
/**
|
||||
* Core function to convert an endpoint to WebSocket format
|
||||
*
|
||||
* Converts between different address formats:
|
||||
* 1. IPv4 address with/without port -> ws://ipv4:port
|
||||
* 2. IPv6 address with/without port -> ws://[ipv6]:port
|
||||
* 3. Domain with/without port -> ws(s)://domain/ws/path
|
||||
*
|
||||
* @param endpoint The endpoint to convert
|
||||
* @return The converted WebSocket endpoint
|
||||
*/
|
||||
pub fn check_ws(endpoint: &str) -> String {
|
||||
if !use_ws() {
|
||||
return endpoint.to_string();
|
||||
}
|
||||
|
||||
if endpoint.is_empty() {
|
||||
return endpoint.to_string();
|
||||
}
|
||||
|
||||
if is_ws_endpoint(endpoint) {
|
||||
return endpoint.to_string();
|
||||
}
|
||||
|
||||
let Some((endpoint_host, endpoint_port)) = split_host_port(endpoint) else {
|
||||
debug_assert!(false, "endpoint doesn't have port");
|
||||
return endpoint.to_string();
|
||||
};
|
||||
|
||||
let custom_rendezvous_server = Config::get_rendezvous_server();
|
||||
let relay_server = Config::get_option(OPTION_RELAY_SERVER);
|
||||
let rendezvous_port = split_host_port(&custom_rendezvous_server)
|
||||
.map(|(_, p)| p)
|
||||
.unwrap_or(RENDEZVOUS_PORT);
|
||||
let relay_port = split_host_port(&relay_server)
|
||||
.map(|(_, p)| p)
|
||||
.unwrap_or(RELAY_PORT);
|
||||
|
||||
let (relay, dst_port) = if endpoint_port == rendezvous_port {
|
||||
// rendezvous
|
||||
(false, endpoint_port + 2)
|
||||
} else if endpoint_port == rendezvous_port - 1 {
|
||||
// online
|
||||
(false, endpoint_port + 3)
|
||||
} else if endpoint_port == relay_port || endpoint_port == rendezvous_port + 1 {
|
||||
// relay
|
||||
// https://github.com/rustdesk/rustdesk/blob/6ffbcd1375771f2482ec4810680623a269be70f1/src/rendezvous_mediator.rs#L615
|
||||
// https://github.com/rustdesk/rustdesk-server/blob/235a3c326ceb665e941edb50ab79faa1208f7507/src/relay_server.rs#L83, based on relay port.
|
||||
(true, endpoint_port + 2)
|
||||
} else {
|
||||
// fallback relay
|
||||
// for controlling side, relay server is passed from the controlled side, not related to local config.
|
||||
(true, endpoint_port + 2)
|
||||
};
|
||||
|
||||
let (address, is_domain) = if crate::is_ip_str(endpoint) {
|
||||
(format!("{}:{}", endpoint_host, dst_port), false)
|
||||
} else {
|
||||
let domain_path = if relay { "/ws/relay" } else { "/ws/id" };
|
||||
(format!("{}{}", endpoint_host, domain_path), true)
|
||||
};
|
||||
let protocol = if is_domain {
|
||||
let api_server = Config::get_option("api-server");
|
||||
if api_server.starts_with("https") {
|
||||
"wss"
|
||||
} else {
|
||||
"ws"
|
||||
}
|
||||
} else {
|
||||
"ws"
|
||||
};
|
||||
format!("{}://{}", protocol, address)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{keys, Config};
|
||||
|
||||
#[test]
|
||||
fn test_check_ws() {
|
||||
// enable websocket
|
||||
Config::set_option(keys::OPTION_ALLOW_WEBSOCKET.to_string(), "Y".to_string());
|
||||
|
||||
// not set custom-rendezvous-server
|
||||
Config::set_option("custom-rendezvous-server".to_string(), "".to_string());
|
||||
Config::set_option("relay-server".to_string(), "".to_string());
|
||||
Config::set_option("api-server".to_string(), "".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
assert_eq!(check_ws("rustdesk.com:21115"), "ws://rustdesk.com/ws/id");
|
||||
assert_eq!(check_ws("rustdesk.com:21116"), "ws://rustdesk.com/ws/id");
|
||||
assert_eq!(check_ws("rustdesk.com:21117"), "ws://rustdesk.com/ws/relay");
|
||||
// set relay-server without port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1".to_string());
|
||||
Config::set_option(
|
||||
"api-server".to_string(),
|
||||
"https://api.rustdesk.com".to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
check_ws("[0:0:0:0:0:0:0:1]:21115"),
|
||||
"ws://[0:0:0:0:0:0:0:1]:21118"
|
||||
);
|
||||
assert_eq!(
|
||||
check_ws("[0:0:0:0:0:0:0:1]:21116"),
|
||||
"ws://[0:0:0:0:0:0:0:1]:21118"
|
||||
);
|
||||
assert_eq!(
|
||||
check_ws("[0:0:0:0:0:0:0:1]:21117"),
|
||||
"ws://[0:0:0:0:0:0:0:1]:21119"
|
||||
);
|
||||
assert_eq!(check_ws("rustdesk.com:21115"), "wss://rustdesk.com/ws/id");
|
||||
assert_eq!(check_ws("rustdesk.com:21116"), "wss://rustdesk.com/ws/id");
|
||||
assert_eq!(
|
||||
check_ws("rustdesk.com:21117"),
|
||||
"wss://rustdesk.com/ws/relay"
|
||||
);
|
||||
// set relay-server with default port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1:21117".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
// set relay-server with custom port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1:34567".to_string());
|
||||
assert_eq!(check_ws("rustdesk.com:21115"), "wss://rustdesk.com/ws/id");
|
||||
assert_eq!(check_ws("rustdesk.com:21116"), "wss://rustdesk.com/ws/id");
|
||||
assert_eq!(
|
||||
check_ws("rustdesk.com:34567"),
|
||||
"wss://rustdesk.com/ws/relay"
|
||||
);
|
||||
|
||||
// set custom-rendezvous-server without port
|
||||
Config::set_option(
|
||||
"custom-rendezvous-server".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
);
|
||||
Config::set_option("relay-server".to_string(), "".to_string());
|
||||
Config::set_option("api-server".to_string(), "".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
// set relay-server without port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
// set relay-server with default port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1:21117".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
// set relay-server with custom port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1:34567".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:34567"), "ws://127.0.0.1:34569");
|
||||
|
||||
// set custom-rendezvous-server without default port
|
||||
Config::set_option(
|
||||
"custom-rendezvous-server".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
);
|
||||
Config::set_option("relay-server".to_string(), "".to_string());
|
||||
Config::set_option("api-server".to_string(), "".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
// set relay-server without port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
// set relay-server with default port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1:21117".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
// set relay-server with custom port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1:34567".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118");
|
||||
assert_eq!(check_ws("127.0.0.1:34567"), "ws://127.0.0.1:34569");
|
||||
|
||||
// set custom-rendezvous-server with custom port
|
||||
Config::set_option(
|
||||
"custom-rendezvous-server".to_string(),
|
||||
"127.0.0.1:23456".to_string(),
|
||||
);
|
||||
Config::set_option("relay-server".to_string(), "".to_string());
|
||||
Config::set_option("api-server".to_string(), "".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:23455"), "ws://127.0.0.1:23458");
|
||||
assert_eq!(check_ws("127.0.0.1:23456"), "ws://127.0.0.1:23458");
|
||||
assert_eq!(check_ws("127.0.0.1:23457"), "ws://127.0.0.1:23459");
|
||||
// set relay-server without port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:23455"), "ws://127.0.0.1:23458");
|
||||
assert_eq!(check_ws("127.0.0.1:23456"), "ws://127.0.0.1:23458");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
// set relay-server with default port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1:21117".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:23455"), "ws://127.0.0.1:23458");
|
||||
assert_eq!(check_ws("127.0.0.1:23456"), "ws://127.0.0.1:23458");
|
||||
assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119");
|
||||
// set relay-server with custom port
|
||||
Config::set_option("relay-server".to_string(), "127.0.0.1:34567".to_string());
|
||||
assert_eq!(check_ws("127.0.0.1:23455"), "ws://127.0.0.1:23458");
|
||||
assert_eq!(check_ws("127.0.0.1:23456"), "ws://127.0.0.1:23458");
|
||||
assert_eq!(check_ws("127.0.0.1:34567"), "ws://127.0.0.1:34569");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user