1 Commits
main ... webrtc

Author SHA1 Message Date
RustDesk
5a78ec4230 feat: support WebRTC rendezvous signaling 2026-05-18 18:59:36 +08:00
4 changed files with 286 additions and 968 deletions

View File

@@ -28,6 +28,7 @@ message PunchHoleRequest {
bool force_relay = 8;
int32 upnp_port = 9;
bytes socket_addr_v6 = 10;
string webrtc_sdp_offer = 11;
}
message ControlPermissions {
@@ -58,6 +59,8 @@ message PunchHole {
int32 upnp_port = 6;
bytes socket_addr_v6 = 7;
ControlPermissions control_permissions = 8;
string webrtc_sdp_offer = 9;
reserved 10;
}
message TestNatRequest {
@@ -84,6 +87,7 @@ message PunchHoleSent {
string version = 5;
int32 upnp_port = 6;
bytes socket_addr_v6 = 7;
string webrtc_sdp_answer = 8;
}
message RegisterPk {
@@ -129,6 +133,7 @@ message PunchHoleResponse {
bool is_udp = 9;
int32 upnp_port = 10;
bytes socket_addr_v6 = 11;
string webrtc_sdp_answer = 12;
}
message ConfigUpdate {
@@ -161,6 +166,7 @@ message RelayResponse {
int32 feedback = 9;
bytes socket_addr_v6 = 10;
int32 upnp_port = 11;
string webrtc_sdp_answer = 12;
}
message SoftwareUpdate { string url = 1; }
@@ -231,6 +237,13 @@ message HttpProxyResponse {
string error = 4;
}
message IceCandidate {
string id = 1;
bytes socket_addr = 2;
string session_key = 3;
string candidate = 4;
}
message RendezvousMessage {
oneof union {
RegisterPeer register_peer = 6;
@@ -256,5 +269,6 @@ message RendezvousMessage {
HealthCheck hc = 26;
HttpProxyRequest http_proxy_request = 27;
HttpProxyResponse http_proxy_response = 28;
IceCandidate ice_candidate = 29;
}
}

View File

@@ -9,29 +9,17 @@ use std::{
time::{Duration, Instant, SystemTime},
};
use anyhow::{anyhow, Result};
use anyhow::Result;
use bytes::Bytes;
use rand::Rng;
use regex::Regex;
use serde as de;
use serde_derive::{Deserialize, Serialize};
use serde_json;
use sha2::{Digest, Sha256};
use sodiumoxide::base64;
use sodiumoxide::crypto::sign;
mod permanent_password;
pub use permanent_password::{
compute_permanent_password_h1, decode_permanent_password_h1_from_storage,
decode_preset_password_h1_from_storage, local_permanent_password_storage_is_usable_for_auth,
preset_permanent_password_storage_is_usable_for_auth, ENCRYPT_MAX_LEN,
};
use permanent_password::{
decode_permanent_password_h1_from_hashed_storage, decrypt_permanent_password_str_or_original,
encode_permanent_password_encrypted_storage_from_h1, password_is_empty_or_not_hashed,
preset_permanent_password_storage_matches_plain, DEFAULT_SALT_LEN, PASSWORD_ENC_VERSION,
};
use crate::{
compress::{compress, decompress},
log,
@@ -51,6 +39,57 @@ pub const READ_TIMEOUT: u64 = 18_000;
pub const REG_INTERVAL: i64 = 15_000;
pub const COMPRESS_LEVEL: i32 = 3;
const SERIAL: i32 = 3;
const PASSWORD_ENC_VERSION: &str = "00";
pub const ENCRYPT_MAX_LEN: usize = 128; // used for password, pin, etc, not for all
const PERMANENT_PASSWORD_HASH_PREFIX: &str = "01";
const PERMANENT_PASSWORD_H1_LEN: usize = 32;
const DEFAULT_SALT_LEN: usize = 32;
fn is_permanent_password_hashed_storage(v: &str) -> bool {
decode_permanent_password_h1_from_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
}
fn constant_time_eq_32(a: &[u8; 32], b: &[u8; 32]) -> bool {
sodiumoxide::utils::memcmp(a, b)
}
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 fn decode_permanent_password_h1_from_storage(
storage: &str,
) -> Option<[u8; PERMANENT_PASSWORD_H1_LEN]> {
let encoded = storage.strip_prefix(PERMANENT_PASSWORD_HASH_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)
}
// If password is empty or not hashed storage, it's safe to update salt.
fn password_is_empty_or_not_hashed(permanent_password_storage: &str) -> bool {
permanent_password_storage.is_empty()
|| !is_permanent_password_hashed_storage(permanent_password_storage)
}
#[cfg(target_os = "macos")]
lazy_static::lazy_static! {
@@ -598,9 +637,7 @@ impl Config {
fn load() -> Config {
let mut config = Config::load_::<Config>("");
let mut store = false;
if let Err(err) = Self::validate_or_decrypt_permanent_password_storage(&mut config) {
log::error!("Failed to validate or decrypt permanent password storage: {err}");
}
store |= Self::migrate_permanent_password_to_hashed_storage(&mut config);
let mut id_valid = false;
let (id, encrypted, store2) = decrypt_str_or_original(&config.enc_id, PASSWORD_ENC_VERSION);
if encrypted {
@@ -639,77 +676,44 @@ impl Config {
config
}
fn validate_or_decrypt_permanent_password_storage(config: &mut Config) -> Result<()> {
if config.password.is_empty() {
return Ok(());
fn migrate_permanent_password_to_hashed_storage(config: &mut Config) -> bool {
if config.password.is_empty() || is_permanent_password_hashed_storage(&config.password) {
return false;
}
if config.password.starts_with(PASSWORD_ENC_VERSION) {
let (plain, decrypted, should_store) =
let (plain, decrypted, looks_like_plaintext) =
decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION);
if decrypted {
// `decrypt_str_or_original` returns (value, decrypted_ok, should_store).
// If the value looks like an encrypted payload ("00" + base64 with MAC) but cannot be
// decrypted on this machine, it is most likely copied from another device or corrupted.
// In normal single-machine setups this should be extremely rare, so keep it as-is.
if !decrypted && !looks_like_plaintext {
return false;
}
if config.salt.is_empty() {
config.salt = Config::get_auto_password(DEFAULT_SALT_LEN);
}
if is_permanent_password_hashed_storage(&plain) {
config.password = plain;
return Ok(());
return true;
}
if !should_store {
return Err(anyhow!("Invalid permanent password encrypted hash storage"));
}
return Ok(());
let h1 = compute_permanent_password_h1(&plain, &config.salt);
config.password = encode_permanent_password_storage_from_h1(&h1);
return true;
}
let (decrypted_storage, decrypted, _) =
decrypt_permanent_password_str_or_original(&config.password);
if decrypted {
Self::ensure_permanent_password_hash_salt(config)?;
if decode_permanent_password_h1_from_hashed_storage(&decrypted_storage).is_some() {
return Ok(());
}
return Err(anyhow!("Invalid permanent password encrypted hash storage"));
}
Ok(())
}
fn ensure_permanent_password_hash_salt(config: &Config) -> Result<()> {
if config.salt.is_empty() {
return Err(anyhow!(
"Permanent password hash storage requires a non-empty salt"
));
}
Ok(())
}
fn ensure_permanent_password_salt(config: &mut Config) {
if config.salt.is_empty() {
config.salt = Config::get_auto_password(DEFAULT_SALT_LEN);
}
}
fn prepare_config_for_store(config: &mut Config) {
match Self::validate_or_decrypt_permanent_password_storage(config) {
Ok(_) => {}
Err(err) => {
// This path is for unrecoverable permanent-password storage, such as
// hashed storage without its salt. Keep unrelated config writes working,
// but handle future transient migration errors separately.
log::error!(
"Clearing invalid permanent password storage before storing config: {err}"
);
config.password.clear();
config.salt.clear();
}
}
let h1 = compute_permanent_password_h1(&config.password, &config.salt);
config.password = encode_permanent_password_storage_from_h1(&h1);
true
}
fn store(&self) {
let mut config = self.clone();
Self::prepare_config_for_store(&mut config);
if !config.password.is_empty()
&& decode_permanent_password_h1_from_storage(&config.password).is_none()
{
config.password =
encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
}
Self::migrate_permanent_password_to_hashed_storage(&mut config);
config.enc_id = encrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
config.id = "".to_owned();
Config::store_(&config, "");
@@ -1265,52 +1269,47 @@ impl Config {
log::info!("id updated from {} to {}", id, new_id);
}
/// Sets the local permanent password.
///
/// Returns `true` when the password is accepted or already matches the effective
/// preset password. Returns `false` when changing the password is disabled or
/// the new password cannot be prepared for storage.
pub fn set_permanent_password(password: &str) -> bool {
pub fn set_permanent_password(password: &str) {
if Self::is_disable_change_permanent_password() {
return false;
return;
}
let (preset_storage, preset_salt) = Self::get_preset_password_storage_and_salt();
if preset_permanent_password_storage_matches_plain(&preset_storage, &preset_salt, password)
if HARD_SETTINGS
.read()
.unwrap()
.get("password")
.map_or(false, |v| v == password)
{
if CONFIG.read().unwrap().password.is_empty() {
return true;
return;
}
}
let mut config = CONFIG.write().unwrap();
let stored = if password.is_empty() {
Some(String::new())
String::new()
} else {
Self::compute_permanent_password_storage_for_update(&mut config, password)
};
let Some(stored) = stored else {
log::error!("Failed to compute permanent password storage; refusing update");
return false;
};
if stored == config.password {
return true;
return;
}
config.password = stored;
config.store();
Self::clear_trusted_devices();
true
}
fn compute_permanent_password_storage_for_update(
config: &mut Config,
password: &str,
) -> Option<String> {
) -> String {
// Keep salt stable for user-initiated permanent password updates.
// Salt should only change when service->user sync updates storage and salt as a pair.
Self::ensure_permanent_password_salt(config);
if config.salt.is_empty() {
config.salt = Config::get_auto_password(DEFAULT_SALT_LEN);
}
let h1 = compute_permanent_password_h1(password, &config.salt);
encode_permanent_password_encrypted_storage_from_h1(&h1)
encode_permanent_password_storage_from_h1(&h1)
}
/// Returns the locally persisted permanent password storage and salt (NOT the hard/preset one).
@@ -1329,97 +1328,62 @@ impl Config {
salt: &str,
) -> crate::ResultType<bool> {
let mut config = CONFIG.write().unwrap();
if !Self::apply_permanent_password_storage_for_sync(&mut config, storage, salt)? {
return Ok(false);
}
config.store();
Self::clear_trusted_devices();
Ok(true)
}
fn apply_permanent_password_storage_for_sync(
config: &mut Config,
storage: &str,
salt: &str,
) -> Result<bool> {
if storage.is_empty() {
if config.password.is_empty() && (salt.is_empty() || config.salt == salt) {
return Ok(false);
}
config.password.clear();
if !salt.is_empty() {
config.salt = salt.to_owned();
}
return Ok(true);
}
if salt.is_empty() {
return Err(anyhow!(
"Refusing to persist permanent password storage without salt"
));
}
if decode_permanent_password_h1_from_storage(storage).is_none() {
log::error!("Rejecting non-current permanent password storage sync payload");
return Err(anyhow!("Invalid permanent password storage sync payload"));
}
if config.password == storage && config.salt == salt {
return Ok(false);
}
config.password = storage.to_owned();
config.salt = salt.to_owned();
config.store();
Self::clear_trusted_devices();
Ok(true)
}
/// Returns true if `input` (candidate plaintext) matches the currently effective permanent password.
pub fn matches_permanent_password_plain(input: &str) -> bool {
if input.is_empty() {
return false;
}
let config = CONFIG.read().unwrap();
let storage = config.password.clone();
let salt = config.salt.clone();
drop(config);
if storage.is_empty() {
return HARD_SETTINGS
.read()
.unwrap()
.get("password")
.map_or(false, |v| v == input);
}
if let Some(stored_h1) = decode_permanent_password_h1_from_storage(&storage) {
if salt.is_empty() {
log::error!("Salt is empty but permanent password is hashed");
return false;
}
let h1 = compute_permanent_password_h1(input, &salt);
return constant_time_eq_32(&h1, &stored_h1);
}
log::warn!("Permanent password storage is not hashed; verifying as plaintext");
storage == input
}
pub fn has_permanent_password() -> bool {
let (local_storage, local_salt) = Self::get_local_permanent_password_storage_and_salt();
if !local_storage.is_empty() {
return local_permanent_password_storage_is_usable_for_auth(
&local_storage,
&local_salt,
);
if !CONFIG.read().unwrap().password.is_empty() {
return true;
}
Self::has_usable_preset_password()
}
fn has_usable_preset_password() -> bool {
let (preset_storage, preset_salt) = Self::get_preset_password_storage_and_salt();
preset_permanent_password_storage_is_usable_for_auth(&preset_storage, &preset_salt)
}
pub fn is_using_preset_password() -> bool {
let (local_storage, _) = Self::get_local_permanent_password_storage_and_salt();
local_storage.is_empty() && Self::has_usable_preset_password()
}
pub fn get_preset_password_storage_and_salt() -> (String, String) {
let hard_settings = HARD_SETTINGS.read().unwrap();
let storage = hard_settings.get("password").cloned().unwrap_or_default();
let salt = hard_settings.get("salt").cloned().unwrap_or_default();
(storage, salt)
}
pub fn get_effective_permanent_password_salt() -> String {
let (local_storage, local_salt) = Self::get_local_permanent_password_storage_and_salt();
if !local_storage.is_empty() {
if local_permanent_password_storage_is_usable_for_auth(&local_storage, &local_salt) {
return Self::get_salt();
}
return String::new();
}
let (preset_storage, preset_salt) = Self::get_preset_password_storage_and_salt();
if !preset_salt.is_empty() {
if preset_permanent_password_storage_is_usable_for_auth(&preset_storage, &preset_salt) {
return preset_salt;
}
return String::new();
}
Self::get_salt()
HARD_SETTINGS
.read()
.unwrap()
.get("password")
.map_or(false, |v| !v.is_empty())
}
pub fn has_local_permanent_password() -> bool {
let (local_storage, local_salt) = Self::get_local_permanent_password_storage_and_salt();
local_permanent_password_storage_is_usable_for_auth(&local_storage, &local_salt)
!CONFIG.read().unwrap().password.is_empty()
}
// This shouldn't happen under normal circumstances because the salt
@@ -2888,8 +2852,7 @@ pub mod keys {
pub const OPTION_ENABLE_RECORD_SESSION: &str = "enable-record-session";
pub const OPTION_ENABLE_BLOCK_INPUT: &str = "enable-block-input";
pub const OPTION_ENABLE_PRIVACY_MODE: &str = "enable-privacy-mode";
pub const OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW: &str =
"enable-perm-change-in-accept-window";
pub const OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW: &str = "enable-perm-change-in-accept-window";
pub const OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION: &str = "allow-remote-config-modification";
pub const OPTION_ALLOW_NUMERNIC_ONE_TIME_PASSWORD: &str = "allow-numeric-one-time-password";
pub const OPTION_ENABLE_LAN_DISCOVERY: &str = "enable-lan-discovery";
@@ -3251,44 +3214,7 @@ impl Status {
#[cfg(test)]
mod tests {
use super::{permanent_password::PERMANENT_PASSWORD_ENC_VERSION, *};
static CONFIG_STATE_TEST_LOCK: Mutex<()> = Mutex::new(());
struct ConfigStateTestGuard {
original_config: Config,
original_hard_settings: HashMap<String, String>,
}
impl ConfigStateTestGuard {
fn new(config: Config, hard_settings: HashMap<String, String>) -> Self {
let original_config = Config::get();
let original_hard_settings = HARD_SETTINGS.read().unwrap().clone();
*CONFIG.write().unwrap() = config;
*HARD_SETTINGS.write().unwrap() = hard_settings;
Self {
original_config,
original_hard_settings,
}
}
}
impl Drop for ConfigStateTestGuard {
fn drop(&mut self) {
*CONFIG.write().unwrap() = self.original_config.clone();
*HARD_SETTINGS.write().unwrap() = self.original_hard_settings.clone();
}
}
fn with_config_and_hard_settings<R>(
config: Config,
hard_settings: HashMap<String, String>,
test: impl FnOnce() -> R,
) -> R {
let _guard = CONFIG_STATE_TEST_LOCK.lock().unwrap();
let _state_guard = ConfigStateTestGuard::new(config, hard_settings);
test()
}
use super::*;
#[test]
fn test_serialize() {
@@ -3301,329 +3227,45 @@ mod tests {
}
#[test]
fn test_hbbs_00_hashed_preset_password_storage_matches_plain_with_salt() {
fn test_permanent_password_h1_storage_roundtrip() {
let salt = "salt123";
let h1 = compute_permanent_password_h1("p@ssw0rd", salt);
let storage = "00".to_owned() + &base64::encode(h1, base64::Variant::Original);
let hard_settings = HashMap::from([
("password".to_owned(), storage),
("salt".to_owned(), salt.to_owned()),
]);
with_config_and_hard_settings(Config::default(), hard_settings, || {
assert!(Config::has_permanent_password());
assert!(Config::has_usable_preset_password());
assert!(Config::is_using_preset_password());
assert_eq!(Config::get_effective_permanent_password_salt(), salt);
});
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_storage(&stored).unwrap();
assert_eq!(&decoded[..], &h1[..]);
}
#[test]
fn test_legacy_plain_preset_password_with_00_hash_shape_without_salt_keeps_old_behavior() {
let h1 = compute_permanent_password_h1("p@ssw0rd", "salt123");
let storage = "00".to_owned() + &base64::encode(h1, base64::Variant::Original);
let hard_settings = HashMap::from([("password".to_owned(), storage.clone())]);
let mut config = Config::default();
config.salt = "local1".to_owned();
with_config_and_hard_settings(config, hard_settings, || {
assert!(Config::has_permanent_password());
assert!(Config::has_usable_preset_password());
assert!(Config::is_using_preset_password());
assert_eq!(Config::get_effective_permanent_password_salt(), "local1");
});
}
#[test]
fn test_local_hashed_permanent_password_without_salt_is_not_reported_as_set() {
let h1 = compute_permanent_password_h1("p@ssw0rd", "salt123");
let mut config = Config::default();
config.password = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
with_config_and_hard_settings(config, HashMap::new(), || {
assert!(!Config::has_permanent_password());
assert!(!Config::has_local_permanent_password());
assert!(!Config::is_using_preset_password());
});
}
#[test]
fn test_invalid_local_hashed_password_does_not_generate_effective_salt() {
let h1 = compute_permanent_password_h1("p@ssw0rd", "salt123");
let mut config = Config::default();
config.password = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
with_config_and_hard_settings(config, HashMap::new(), || {
assert_eq!(Config::get_effective_permanent_password_salt(), "");
assert_eq!(
Config::get_local_permanent_password_storage_and_salt().1,
""
);
});
}
#[test]
fn test_legacy_plain_preset_password_uses_local_salt_for_challenge() {
let mut config = Config::default();
config.salt = "local1".to_owned();
let hard_settings = HashMap::from([("password".to_owned(), "legacy-password".to_owned())]);
with_config_and_hard_settings(config, hard_settings, || {
assert_eq!(Config::get_effective_permanent_password_salt(), "local1");
assert!(Config::has_permanent_password());
assert!(Config::is_using_preset_password());
});
}
#[test]
fn test_malformed_preset_password_with_salt_is_not_usable() {
for storage in ["01secret", "00not-a-valid-hash"] {
let hard_settings = HashMap::from([
("password".to_owned(), storage.to_owned()),
("salt".to_owned(), "preset-salt".to_owned()),
]);
with_config_and_hard_settings(Config::default(), hard_settings, || {
assert_eq!(Config::get_effective_permanent_password_salt(), "");
assert_eq!(
Config::get_local_permanent_password_storage_and_salt().1,
""
);
assert!(!Config::has_permanent_password());
assert!(!Config::is_using_preset_password());
});
}
}
#[test]
fn test_validate_or_decrypt_keeps_plaintext_permanent_password_unchanged() {
fn test_migrate_plaintext_permanent_password_to_hashed_storage() {
let mut cfg = Config::default();
cfg.password = "p@ssw0rd".to_owned();
cfg.salt = "".to_owned();
Config::validate_or_decrypt_permanent_password_storage(&mut cfg).unwrap();
assert_eq!(cfg.password, "p@ssw0rd");
assert!(cfg.salt.is_empty());
let changed = Config::migrate_permanent_password_to_hashed_storage(&mut cfg);
assert!(changed);
assert!(is_permanent_password_hashed_storage(&cfg.password));
assert_eq!(cfg.salt.chars().count(), DEFAULT_SALT_LEN);
let stored_h1 = decode_permanent_password_h1_from_storage(&cfg.password).unwrap();
let expected_h1 = compute_permanent_password_h1("p@ssw0rd", &cfg.salt);
assert_eq!(stored_h1, expected_h1);
}
#[test]
fn test_validate_or_decrypt_decrypts_00_permanent_password_without_forcing_store() {
fn test_migrate_plaintext_with_00_prefix_permanent_password_to_hashed_storage() {
let mut cfg = Config::default();
let legacy_storage =
encrypt_str_or_original("legacy-secret", PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
cfg.password = legacy_storage;
cfg.password = "00secret".to_owned();
cfg.salt = "".to_owned();
Config::validate_or_decrypt_permanent_password_storage(&mut cfg).unwrap();
assert_eq!(cfg.password, "legacy-secret");
assert!(cfg.salt.is_empty());
}
let changed = Config::migrate_permanent_password_to_hashed_storage(&mut cfg);
assert!(changed);
assert!(is_permanent_password_hashed_storage(&cfg.password));
assert!(!cfg.salt.is_empty());
#[test]
fn test_validate_or_decrypt_rejects_corrupted_00_permanent_password_storage() {
let legacy_storage =
encrypt_str_or_original("legacy-secret", PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
let mut invalid_payload = base64::decode(
&legacy_storage.as_bytes()[PASSWORD_ENC_VERSION.len()..],
base64::Variant::Original,
)
.unwrap();
*invalid_payload.last_mut().unwrap() ^= 1;
let mut cfg = Config::default();
cfg.password = PASSWORD_ENC_VERSION.to_owned()
+ &base64::encode(invalid_payload, base64::Variant::Original);
cfg.salt = "salt123".to_owned();
assert!(Config::validate_or_decrypt_permanent_password_storage(&mut cfg).is_err());
}
#[test]
fn test_validate_or_decrypt_rejects_encrypted_hashed_permanent_password_without_salt() {
let mut cfg = Config::default();
let h1 = compute_permanent_password_h1("p@ssw0rd", "salt123");
cfg.password = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
let original_password = cfg.password.clone();
assert!(Config::validate_or_decrypt_permanent_password_storage(&mut cfg).is_err());
assert_eq!(cfg.password, original_password);
assert!(cfg.salt.is_empty());
}
#[test]
fn test_set_does_not_validate_or_decrypt_permanent_password_storage_in_memory() {
let mut cfg = Config::default();
let invalid_payload =
crate::password_security::symmetric_crypt(b"not-a-hash", true).unwrap();
let invalid_storage = PERMANENT_PASSWORD_ENC_VERSION.to_owned()
+ &base64::encode(invalid_payload, base64::Variant::Original);
cfg.password = invalid_storage.clone();
cfg.id = "123456789".to_owned();
with_config_and_hard_settings(Config::default(), HashMap::new(), || {
assert!(Config::set(cfg));
let updated = Config::get();
assert_eq!(updated.password, invalid_storage);
assert!(updated.salt.is_empty());
assert_eq!(updated.id, "123456789");
});
}
#[test]
fn test_set_does_not_convert_plaintext_permanent_password_to_storage_format_in_memory() {
let mut cfg = Config::default();
cfg.password = "legacy-secret".to_owned();
cfg.salt = "".to_owned();
with_config_and_hard_settings(Config::default(), HashMap::new(), || {
assert!(Config::set(cfg));
let updated = Config::get();
assert!(!updated.password.starts_with(PASSWORD_ENC_VERSION));
assert_eq!(updated.password, "legacy-secret");
assert!(updated.salt.is_empty());
});
}
#[test]
fn test_set_keeps_plaintext_permanent_password_with_current_prefix_in_memory() {
let mut cfg = Config::default();
cfg.password = "01legacy-secret".to_owned();
cfg.salt = "".to_owned();
with_config_and_hard_settings(Config::default(), HashMap::new(), || {
assert!(Config::set(cfg));
let updated = Config::get();
assert_eq!(updated.password, "01legacy-secret");
assert!(updated.salt.is_empty());
});
}
#[test]
fn test_validate_or_decrypt_keeps_plaintext_permanent_password_with_current_prefix_and_long_base64(
) {
let mut cfg = Config::default();
let plain = "01".to_owned() + &base64::encode([42u8; 24], base64::Variant::Original);
cfg.password = plain.clone();
cfg.salt = "".to_owned();
Config::validate_or_decrypt_permanent_password_storage(&mut cfg).unwrap();
assert_eq!(cfg.password, plain);
assert!(cfg.salt.is_empty());
}
#[test]
fn test_permanent_password_sync_treats_same_encrypted_hash_as_unchanged() {
let mut cfg = Config::default();
cfg.salt = "salt123".to_owned();
let h1 = compute_permanent_password_h1("p@ssw0rd", &cfg.salt);
let encrypted_hash_storage =
encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
cfg.password = encrypted_hash_storage.clone();
Config::validate_or_decrypt_permanent_password_storage(&mut cfg).unwrap();
assert!(!Config::apply_permanent_password_storage_for_sync(
&mut cfg,
&encrypted_hash_storage,
"salt123"
)
.unwrap());
}
#[test]
fn test_permanent_password_sync_stores_incoming_encrypted_hash_when_local_empty() {
let salt = "salt123";
let h1 = compute_permanent_password_h1("p@ssw0rd", salt);
let incoming = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
let mut cfg = Config::default();
assert!(
Config::apply_permanent_password_storage_for_sync(&mut cfg, &incoming, salt).unwrap()
);
assert_eq!(cfg.password, incoming);
assert_eq!(cfg.salt, salt);
}
#[test]
fn test_permanent_password_sync_rejects_non_current_storage_payloads() {
let invalid_payload = vec![42u8; sodiumoxide::crypto::secretbox::MACBYTES + 1];
let invalid_storage = PERMANENT_PASSWORD_ENC_VERSION.to_owned()
+ &base64::encode(invalid_payload, base64::Variant::Original);
let encrypted_legacy_plaintext =
encrypt_str_or_original("legacy-secret", PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
let encrypted = crate::password_security::symmetric_crypt(b"not-a-hash", true).unwrap();
let encrypted_non_hash = PERMANENT_PASSWORD_ENC_VERSION.to_owned()
+ &base64::encode(encrypted, base64::Variant::Original);
for storage in [
"00secret",
&encrypted_legacy_plaintext,
&invalid_storage,
"01invalid",
&encrypted_non_hash,
] {
let mut cfg = Config::default();
assert!(Config::apply_permanent_password_storage_for_sync(
&mut cfg, storage, "salt123"
)
.is_err());
assert!(cfg.password.is_empty());
assert!(cfg.salt.is_empty());
}
let mut cfg = Config::default();
cfg.password = invalid_storage.clone();
cfg.salt = "salt123".to_owned();
assert!(Config::apply_permanent_password_storage_for_sync(
&mut cfg,
&invalid_storage,
"salt123"
)
.is_err());
assert_eq!(cfg.password, invalid_storage);
assert_eq!(cfg.salt, "salt123");
}
#[test]
fn test_permanent_password_sync_rejects_non_empty_storage_without_salt() {
let mut cfg = Config::default();
let h1 = compute_permanent_password_h1("p@ssw0rd", "salt123");
let incoming = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
assert!(
Config::apply_permanent_password_storage_for_sync(&mut cfg, &incoming, "").is_err()
);
assert!(cfg.password.is_empty());
assert!(cfg.salt.is_empty());
}
#[test]
fn test_permanent_password_sync_empty_storage_clears_existing_password() {
let salt = "salt123";
let h1 = compute_permanent_password_h1("p@ssw0rd", salt);
let mut cfg = Config::default();
cfg.password = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
cfg.salt = salt.to_owned();
assert!(Config::apply_permanent_password_storage_for_sync(&mut cfg, "", "").unwrap());
assert!(cfg.password.is_empty());
assert_eq!(cfg.salt, salt);
}
#[test]
fn test_permanent_password_sync_empty_storage_uses_incoming_salt() {
let old_salt = "old-salt";
let h1 = compute_permanent_password_h1("p@ssw0rd", old_salt);
let mut cfg = Config::default();
cfg.password = encode_permanent_password_encrypted_storage_from_h1(&h1).unwrap();
cfg.salt = old_salt.to_owned();
assert!(
Config::apply_permanent_password_storage_for_sync(&mut cfg, "", "new-salt").unwrap()
);
assert!(cfg.password.is_empty());
assert_eq!(cfg.salt, "new-salt");
let stored_h1 = decode_permanent_password_h1_from_storage(&cfg.password).unwrap();
let expected_h1 = compute_permanent_password_h1("00secret", &cfg.salt);
assert_eq!(stored_h1, expected_h1);
}
#[test]

View File

@@ -1,412 +0,0 @@
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"));
}
}

View File

@@ -1,13 +1,14 @@
use std::collections::HashMap;
use std::io::{Error, ErrorKind};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::sync::{Arc, Mutex as StdMutex};
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_candidate::RTCIceCandidateInit;
use webrtc::ice_transport::ice_server::RTCIceServer;
use webrtc::peer_connection::configuration::RTCConfiguration;
use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState;
@@ -18,8 +19,7 @@ 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::sync::{mpsc, watch, Mutex};
use tokio::time::timeout;
use url::Url;
@@ -28,10 +28,19 @@ use crate::protobuf::Message;
use crate::sodiumoxide::crypto::secretbox::Key;
use crate::ResultType;
#[derive(Clone, Debug, PartialEq, Eq)]
enum WebRTCConnectionState {
Pending,
Open,
Closed(String),
}
pub struct WebRTCStream {
pc: Arc<RTCPeerConnection>,
stream: Arc<Mutex<Arc<RTCDataChannel>>>,
state_notify: watch::Receiver<bool>,
state_notify: watch::Receiver<WebRTCConnectionState>,
local_ice_rx: Arc<StdMutex<Option<mpsc::UnboundedReceiver<String>>>>,
session_key: String,
send_timeout: u64,
}
@@ -59,6 +68,8 @@ impl Clone for WebRTCStream {
pc: self.pc.clone(),
stream: self.stream.clone(),
state_notify: self.state_notify.clone(),
local_ice_rx: self.local_ice_rx.clone(),
session_key: self.session_key.clone(),
send_timeout: self.send_timeout,
}
}
@@ -122,7 +133,7 @@ impl WebRTCStream {
if sdp_json.is_empty() {
return Ok("".to_string());
}
let sdp = serde_json::from_str::<RTCSessionDescription>(&sdp_json)?;
let sdp = serde_json::from_str::<RTCSessionDescription>(sdp_json)?;
Self::get_key_for_sdp(&sdp)
}
@@ -243,16 +254,40 @@ impl WebRTCStream {
..Default::default()
};
let (notify_tx, notify_rx) = watch::channel(false);
let (notify_tx, notify_rx) = watch::channel(WebRTCConnectionState::Pending);
let (ice_tx, ice_rx) = mpsc::unbounded_channel::<String>();
// Create a new RTCPeerConnection
let pc = Arc::new(api.new_peer_connection(config).await?);
let local_ice_tx = ice_tx.clone();
pc.on_ice_candidate(Box::new(move |candidate| {
let local_ice_tx = local_ice_tx.clone();
Box::pin(async move {
let Some(candidate) = candidate else {
return;
};
match candidate.to_json() {
Ok(candidate) => match serde_json::to_string(&candidate) {
Ok(candidate_json) => {
let _ = local_ice_tx.send(candidate_json);
}
Err(err) => {
log::warn!("failed to serialize local ICE candidate: {}", err);
}
},
Err(err) => {
log::warn!("failed to convert local ICE candidate to JSON: {}", err);
}
}
})
}));
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);
let _ = dc_open_notify.send(WebRTCConnectionState::Open);
Box::pin(async {})
}));
dc
@@ -277,7 +312,7 @@ impl WebRTCStream {
*stream_lock = dc.clone();
drop(stream_lock);
dc.on_open(Box::new(move || {
let _ = dc_open_notify2.send(true);
let _ = dc_open_notify2.send(WebRTCConnectionState::Open);
Box::pin(async {})
}));
})
@@ -297,7 +332,9 @@ impl WebRTCStream {
RTCPeerConnectionState::Disconnected
| RTCPeerConnectionState::Failed
| RTCPeerConnectionState::Closed => {
let _ = on_connection_notify.send(true);
let _ = on_connection_notify.send(WebRTCConnectionState::Closed(
s.to_string(),
));
log::debug!("WebRTC session closing due to disconnected");
let _ = stream_for_close2.lock().await.close().await;
log::debug!("WebRTC session stream closed");
@@ -339,9 +376,7 @@ impl WebRTCStream {
// 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
@@ -351,9 +386,7 @@ impl WebRTCStream {
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
@@ -371,6 +404,8 @@ impl WebRTCStream {
pc,
stream,
state_notify: notify_rx,
local_ice_rx: Arc::new(StdMutex::new(Some(ice_rx))),
session_key: key.clone(),
send_timeout: ms_timeout,
};
final_lock.insert(key, webrtc_stream.clone());
@@ -397,6 +432,38 @@ impl WebRTCStream {
Ok(())
}
#[inline]
pub fn take_local_ice_rx(&self) -> Option<mpsc::UnboundedReceiver<String>> {
self.local_ice_rx.lock().ok().and_then(|mut rx| rx.take())
}
#[inline]
pub async fn add_remote_ice_candidate(&self, candidate_json: &str) -> ResultType<()> {
if candidate_json.is_empty() {
return Ok(());
}
let candidate = serde_json::from_str::<RTCIceCandidateInit>(candidate_json)?;
self.pc.add_ice_candidate(candidate).await?;
Ok(())
}
#[inline]
pub fn session_key(&self) -> &str {
&self.session_key
}
pub async fn wait_connected(&mut self, ms: u64) -> ResultType<()> {
if ms > 0 {
match timeout(Duration::from_millis(ms), self.wait_for_connect_result()).await {
Ok(result) => result?,
Err(_) => return Err(anyhow::anyhow!("WebRTC wait_connected timeout")),
}
} else {
self.wait_for_connect_result().await?;
}
Ok(())
}
#[inline]
pub fn set_raw(&mut self) {
// not-supported
@@ -435,33 +502,30 @@ impl WebRTCStream {
}
#[inline]
async fn wait_for_connect_result(&mut self) {
if *self.state_notify.borrow() {
return;
async fn wait_for_connect_result(&mut self) -> ResultType<()> {
loop {
match self.state_notify.borrow().clone() {
WebRTCConnectionState::Open => return Ok(()),
WebRTCConnectionState::Closed(reason) => {
return Err(anyhow::anyhow!("WebRTC connection closed: {}", reason));
}
WebRTCConnectionState::Pending => {}
}
self.state_notify.changed().await?;
}
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
if let Err(err) = self.wait_connected(self.send_timeout).await {
self.pc.close().await.ok();
let kind = if err.to_string().contains("deadline")
|| err.to_string().contains("timeout")
{
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;
ErrorKind::TimedOut
} else {
ErrorKind::Other
};
return Err(Error::new(kind, err.to_string()).into());
}
let stream = self.stream.lock().await.clone();
stream.send(&bytes).await?;
@@ -470,7 +534,10 @@ impl WebRTCStream {
#[inline]
pub async fn next(&mut self) -> Option<Result<BytesMut, Error>> {
self.wait_for_connect_result().await;
if let Err(err) = self.wait_for_connect_result().await {
self.pc.close().await.ok();
return Some(Err(Error::new(ErrorKind::Other, err.to_string())));
}
let stream = self.stream.lock().await.clone();
// TODO reuse buffer?
@@ -767,4 +834,11 @@ IHR5cCBzcmZseCByYWRkciAwLjAuMC4wIHJwb3J0IDY0MDA4XHJcbmE9ZW5kLW9mLWNhbmRpZGF0ZXNc
"connect to an 'answer' webrtc endpoint should error"
);
}
#[tokio::test]
async fn test_webrtc_wait_connected_timeout() {
let mut stream = WebRTCStream::new("", false, 100).await.unwrap();
let err = stream.wait_connected(10).await.unwrap_err();
assert!(err.to_string().contains("timeout"));
}
}