mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-19 15:46:17 +00:00
fix(settings): harden Torrent network validation
- Align the DHT message-timeout range with bundled Aria2 1.37.0. - Validate and canonicalize Torrent network text settings at the native boundary. - Fence delayed input responses and reject contradictory IPv6 bind state. - Add regression coverage for malformed settings and cross-field races.
This commit is contained in:
+32
-1
@@ -10319,6 +10319,16 @@ fn db_load_settings(
|
||||
.transpose()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn canonicalize_torrent_network_setting(
|
||||
caller: tauri::WebviewWindow,
|
||||
field: String,
|
||||
value: String,
|
||||
) -> Result<String, String> {
|
||||
properties_window::ensure_main_window(&caller)?;
|
||||
crate::settings::canonicalize_torrent_network_setting(&field, &value)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn db_get_all_downloads(
|
||||
caller: tauri::WebviewWindow,
|
||||
@@ -11350,6 +11360,26 @@ mod tests {
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["--dht-message-timeout=10"]
|
||||
);
|
||||
|
||||
let mut maximum = std::process::Command::new("aria2c");
|
||||
apply_aria2_torrent_dht_options(&mut maximum, queue::MAX_TORRENT_DHT_MESSAGE_TIMEOUT);
|
||||
assert_eq!(
|
||||
maximum
|
||||
.get_args()
|
||||
.map(|arg| arg.to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["--dht-message-timeout=60"]
|
||||
);
|
||||
|
||||
let mut out_of_range = std::process::Command::new("aria2c");
|
||||
apply_aria2_torrent_dht_options(&mut out_of_range, 61);
|
||||
assert_eq!(
|
||||
out_of_range
|
||||
.get_args()
|
||||
.map(|arg| arg.to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["--dht-message-timeout=10"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -15451,7 +15481,8 @@ pub fn run() {
|
||||
properties_window::properties_window_registry_remove_for_download,
|
||||
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media, parity::get_supported_media_domains,
|
||||
parity::create_category_directories,
|
||||
db_save_settings, db_load_settings, db_get_all_downloads, db_replace_downloads,
|
||||
db_save_settings, db_load_settings, canonicalize_torrent_network_setting,
|
||||
db_get_all_downloads, db_replace_downloads,
|
||||
db_commit_download_state,
|
||||
clear_torrent_removal_paths, reconcile_torrent_removal_reservations,
|
||||
db_get_all_queues, db_replace_queues,
|
||||
|
||||
@@ -32,7 +32,10 @@ pub const MIN_TORRENT_MAX_OPEN_FILES: u32 = 1;
|
||||
pub const MAX_TORRENT_MAX_OPEN_FILES: u32 = 4_096;
|
||||
pub const DEFAULT_TORRENT_DHT_MESSAGE_TIMEOUT: u32 = 10;
|
||||
pub const MIN_TORRENT_DHT_MESSAGE_TIMEOUT: u32 = 1;
|
||||
pub const MAX_TORRENT_DHT_MESSAGE_TIMEOUT: u32 = 600;
|
||||
// Aria2 1.37.0 rejects values above 60 during option parsing. Keep this
|
||||
// boundary aligned with the bundled engine so a saved setting cannot prevent
|
||||
// the daemon from starting.
|
||||
pub const MAX_TORRENT_DHT_MESSAGE_TIMEOUT: u32 = 60;
|
||||
pub const DEFAULT_TORRENT_MAX_CONCURRENT_SEEDS: u32 = 2;
|
||||
pub const MIN_TORRENT_MAX_CONCURRENT_SEEDS: u32 = 1;
|
||||
pub const MAX_TORRENT_MAX_CONCURRENT_SEEDS: u32 = 64;
|
||||
@@ -8509,8 +8512,9 @@ mod tests {
|
||||
#[test]
|
||||
fn torrent_network_limits_and_web_seed_normalization_are_bounded() {
|
||||
assert_eq!(normalize_torrent_dht_message_timeout(1).unwrap(), 1);
|
||||
assert_eq!(normalize_torrent_dht_message_timeout(600).unwrap(), 600);
|
||||
assert_eq!(normalize_torrent_dht_message_timeout(60).unwrap(), 60);
|
||||
assert!(normalize_torrent_dht_message_timeout(0).is_err());
|
||||
assert!(normalize_torrent_dht_message_timeout(61).is_err());
|
||||
assert_eq!(normalize_torrent_max_concurrent_seeds(2).unwrap(), 2);
|
||||
assert!(normalize_torrent_max_concurrent_seeds(65).is_err());
|
||||
|
||||
|
||||
@@ -233,6 +233,37 @@ pub fn canonicalize_torrent_network_settings(stored: &str) -> Result<String, Str
|
||||
.map_err(|error| format!("failed to encode canonical settings: {error}"))
|
||||
}
|
||||
|
||||
/// Normalize one text setting before the frontend commits it to durable state.
|
||||
/// Keep this on the native boundary so interactive validation and persisted
|
||||
/// settings use exactly the same Aria2-compatible rules.
|
||||
pub fn canonicalize_torrent_network_setting(field: &str, value: &str) -> Result<String, String> {
|
||||
let normalized = match field {
|
||||
"torrentListenPort" => {
|
||||
crate::queue::normalize_torrent_port_spec(Some(value), "TCP listen ports")?
|
||||
}
|
||||
"torrentDhtListenPort" => {
|
||||
crate::queue::normalize_torrent_port_spec(Some(value), "UDP listen ports")?
|
||||
}
|
||||
"torrentExternalIp" => crate::queue::normalize_torrent_external_ip(Some(value))?,
|
||||
"torrentDhtEntryPoint" => {
|
||||
crate::queue::normalize_torrent_dht_entry_point(Some(value), false)?
|
||||
}
|
||||
"torrentDhtEntryPoint6" => {
|
||||
crate::queue::normalize_torrent_dht_entry_point(Some(value), true)?
|
||||
}
|
||||
"torrentDhtListenAddr6" => {
|
||||
crate::queue::normalize_torrent_dht_listen_addr6(Some(value))?
|
||||
}
|
||||
"torrentLpdInterface" => crate::queue::normalize_torrent_lpd_interface(Some(value))?,
|
||||
"torrentPeerIdPrefix" => crate::queue::normalize_torrent_peer_id_prefix(Some(value))?,
|
||||
"torrentPeerAgent" => crate::queue::normalize_torrent_peer_agent(Some(value))?,
|
||||
"torrentBindAddress" => crate::queue::normalize_torrent_bind_address(Some(value))?,
|
||||
"aria2DiskCache" => return crate::queue::normalize_aria2_disk_cache(Some(value)),
|
||||
_ => return Err("unknown Torrent network setting".to_string()),
|
||||
};
|
||||
Ok(normalized.unwrap_or_default())
|
||||
}
|
||||
|
||||
pub fn update_settings_state(
|
||||
app_handle: &AppHandle,
|
||||
update: impl FnOnce(&mut Map<String, Value>),
|
||||
@@ -936,7 +967,8 @@ fn default_settings() -> PersistedSettings {
|
||||
mod tests {
|
||||
use crate::ipc::{FontFamily, WindowControlStyle};
|
||||
use super::{
|
||||
canonicalize_torrent_network_settings, decode_stored_settings, default_settings,
|
||||
canonicalize_torrent_network_setting, canonicalize_torrent_network_settings,
|
||||
decode_stored_settings, default_settings,
|
||||
preserve_portable_pairing_token, preserve_scheduler_runtime_keys,
|
||||
torrent_startup_settings,
|
||||
};
|
||||
@@ -1376,6 +1408,29 @@ mod tests {
|
||||
assert_eq!(canonical["state"]["torrentSeparateSeedSlots"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalizes_individual_torrent_network_inputs_with_shared_rules() {
|
||||
assert_eq!(
|
||||
canonicalize_torrent_network_setting("torrentListenPort", " 6881-6999 ").unwrap(),
|
||||
"6881-6999"
|
||||
);
|
||||
assert_eq!(
|
||||
canonicalize_torrent_network_setting("torrentDhtEntryPoint6", "[2001:db8::1]:6881")
|
||||
.unwrap(),
|
||||
"[2001:db8::1]:6881"
|
||||
);
|
||||
assert_eq!(
|
||||
canonicalize_torrent_network_setting("aria2DiskCache", " 256m ").unwrap(),
|
||||
"256M"
|
||||
);
|
||||
assert_eq!(
|
||||
canonicalize_torrent_network_setting("torrentBindAddress", " ").unwrap(),
|
||||
""
|
||||
);
|
||||
assert!(canonicalize_torrent_network_setting("torrentListenPort", "61").is_err());
|
||||
assert!(canonicalize_torrent_network_setting("unknown", "value").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_ipv6_bind_address_when_transport_is_disabled() {
|
||||
let stored = json!({
|
||||
|
||||
Reference in New Issue
Block a user