mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-19 23:52:16 +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!({
|
||||
|
||||
+213
-156
@@ -34,6 +34,7 @@ import {
|
||||
import { usePlatformInfo } from '../utils/platform';
|
||||
import { isTrustedFirelinkReleaseUrl } from '../utils/releaseUrls';
|
||||
import { normalizeCustomProxy } from '../store/useDownloadStore';
|
||||
import { shouldApplyTorrentNetworkInputResult } from '../utils/torrentNetworkInput';
|
||||
import {
|
||||
DEFAULT_TORRENT_DHT_MESSAGE_TIMEOUT,
|
||||
MAX_TORRENT_DHT_MESSAGE_TIMEOUT,
|
||||
@@ -99,6 +100,105 @@ type ManualUpdateStatus =
|
||||
|
||||
type SystemProxyStatus = 'idle' | 'checking' | 'detected' | 'none' | 'error';
|
||||
|
||||
type TorrentNetworkTextField =
|
||||
| 'torrentListenPort'
|
||||
| 'torrentDhtListenPort'
|
||||
| 'torrentExternalIp'
|
||||
| 'torrentDhtEntryPoint'
|
||||
| 'torrentDhtEntryPoint6'
|
||||
| 'torrentDhtListenAddr6'
|
||||
| 'torrentLpdInterface'
|
||||
| 'torrentPeerIdPrefix'
|
||||
| 'torrentPeerAgent'
|
||||
| 'torrentBindAddress'
|
||||
| 'aria2DiskCache';
|
||||
|
||||
const TorrentNetworkTextInput = ({
|
||||
field,
|
||||
value,
|
||||
label,
|
||||
description,
|
||||
placeholder,
|
||||
onCommit,
|
||||
onError,
|
||||
maxLength,
|
||||
className = 'app-control settings-network-input'
|
||||
}: {
|
||||
field: TorrentNetworkTextField;
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
placeholder?: string;
|
||||
onCommit: (value: string) => boolean | void;
|
||||
onError: (error: unknown) => void;
|
||||
maxLength?: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [draft, setDraft] = useState(value);
|
||||
const commitId = useRef(0);
|
||||
const editId = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
editId.current += 1;
|
||||
setDraft(value);
|
||||
}, [value]);
|
||||
|
||||
const commit = async () => {
|
||||
const requestId = ++commitId.current;
|
||||
const editRequestId = editId.current;
|
||||
try {
|
||||
const normalized = await invoke('canonicalize_torrent_network_setting', {
|
||||
field,
|
||||
value: draft
|
||||
});
|
||||
if (!shouldApplyTorrentNetworkInputResult(
|
||||
requestId,
|
||||
commitId.current,
|
||||
editRequestId,
|
||||
editId.current
|
||||
)) return;
|
||||
if (onCommit(normalized) === false) {
|
||||
setDraft(value);
|
||||
onError(new Error('This Torrent network value conflicts with another setting.'));
|
||||
return;
|
||||
}
|
||||
setDraft(normalized);
|
||||
} catch (error) {
|
||||
if (!shouldApplyTorrentNetworkInputResult(
|
||||
requestId,
|
||||
commitId.current,
|
||||
editRequestId,
|
||||
editId.current
|
||||
)) return;
|
||||
setDraft(value);
|
||||
onError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{label}</span>
|
||||
<small>{description}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
dir="ltr"
|
||||
value={draft}
|
||||
onChange={(event) => {
|
||||
editId.current += 1;
|
||||
setDraft(event.target.value);
|
||||
}}
|
||||
onBlur={() => { void commit(); }}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
className={className}
|
||||
aria-label={label}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type NetworkSettingsSection = 'general' | 'discovery' | 'connection' | 'limits' | 'advanced';
|
||||
|
||||
const networkSettingsSections: NetworkSettingsSection[] = [
|
||||
@@ -420,6 +520,15 @@ const engineRunId = useRef(0);
|
||||
|
||||
// Toast notifications
|
||||
const { addToast } = useToast();
|
||||
const showTorrentNetworkInputError = (error: unknown) => {
|
||||
addToast({
|
||||
message: t($ => $.settings.network.torrentNetworkInputInvalid, {
|
||||
detail: error instanceof Error ? error.message : String(error)
|
||||
}),
|
||||
variant: 'error',
|
||||
isActionable: true
|
||||
});
|
||||
};
|
||||
const commitTorrentMaxOpenFiles = (raw: string) => {
|
||||
const next = normalizeTorrentMaxOpenFiles(raw) ?? settings.torrentMaxOpenFiles;
|
||||
const requestId = ++torrentMaxOpenFilesCommitRef.current;
|
||||
@@ -1442,148 +1551,100 @@ runEngineChecks(false);
|
||||
<div id="network-settings-panel-connection" role="tabpanel" aria-labelledby="network-settings-tab-connection" hidden={networkSection !== 'connection'} tabIndex={0}>
|
||||
<h2 className="settings-section-title">{t($ => $.settings.network.torrentNetwork)}</h2>
|
||||
<div className="mac-settings-group">
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentListenPort)}</span>
|
||||
<small>{t($ => $.settings.network.torrentListenPortDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentListenPort}
|
||||
onChange={(event) => settings.setTorrentListenPort(event.target.value)}
|
||||
placeholder="6881-6999"
|
||||
className="app-control settings-port-input text-center"
|
||||
aria-label={t($ => $.settings.network.torrentListenPort)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentBindAddress)}</span>
|
||||
<small>{t($ => $.settings.network.torrentBindAddressDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentBindAddress}
|
||||
onChange={(event) => settings.setTorrentBindAddress(event.target.value)}
|
||||
placeholder="192.0.2.10 or 2001:db8::10"
|
||||
className="app-control settings-network-input"
|
||||
aria-label={t($ => $.settings.network.torrentBindAddress)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentDhtListenPort)}</span>
|
||||
<small>{t($ => $.settings.network.torrentDhtListenPortDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentDhtListenPort}
|
||||
onChange={(event) => settings.setTorrentDhtListenPort(event.target.value)}
|
||||
placeholder="6881-6999"
|
||||
className="app-control settings-port-input text-center"
|
||||
aria-label={t($ => $.settings.network.torrentDhtListenPort)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentExternalIp)}</span>
|
||||
<small>{t($ => $.settings.network.torrentExternalIpDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentExternalIp}
|
||||
onChange={(event) => settings.setTorrentExternalIp(event.target.value)}
|
||||
placeholder={t($ => $.settings.network.torrentExternalIpPlaceholder)}
|
||||
className="app-control settings-network-input"
|
||||
aria-label={t($ => $.settings.network.torrentExternalIp)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentDhtEntryPoint)}</span>
|
||||
<small>{t($ => $.settings.network.torrentDhtEntryPointDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentDhtEntryPoint}
|
||||
onChange={(event) => settings.setTorrentDhtEntryPoint(event.target.value)}
|
||||
placeholder="router.example:6881"
|
||||
className="app-control settings-network-input"
|
||||
aria-label={t($ => $.settings.network.torrentDhtEntryPoint)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentDhtEntryPoint6)}</span>
|
||||
<small>{t($ => $.settings.network.torrentDhtEntryPoint6Description)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentDhtEntryPoint6}
|
||||
onChange={(event) => settings.setTorrentDhtEntryPoint6(event.target.value)}
|
||||
placeholder="[2001:db8::1]:6881"
|
||||
className="app-control settings-network-input"
|
||||
aria-label={t($ => $.settings.network.torrentDhtEntryPoint6)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentDhtListenAddr6)}</span>
|
||||
<small>{t($ => $.settings.network.torrentDhtListenAddr6Description)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentDhtListenAddr6}
|
||||
onChange={(event) => settings.setTorrentDhtListenAddr6(event.target.value)}
|
||||
placeholder="2001:db8::2"
|
||||
className="app-control settings-network-input"
|
||||
aria-label={t($ => $.settings.network.torrentDhtListenAddr6)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentLpdInterface)}</span>
|
||||
<small>{t($ => $.settings.network.torrentLpdInterfaceDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentLpdInterface}
|
||||
onChange={(event) => settings.setTorrentLpdInterface(event.target.value)}
|
||||
placeholder="en0"
|
||||
className="app-control settings-network-input"
|
||||
aria-label={t($ => $.settings.network.torrentLpdInterface)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentPeerIdPrefix)}</span>
|
||||
<small>{t($ => $.settings.network.torrentPeerIdPrefixDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentPeerIdPrefix}
|
||||
onChange={(event) => settings.setTorrentPeerIdPrefix(event.target.value)}
|
||||
placeholder="-FL-1-3-1-"
|
||||
maxLength={20}
|
||||
className="app-control settings-network-input"
|
||||
aria-label={t($ => $.settings.network.torrentPeerIdPrefix)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentPeerAgent)}</span>
|
||||
<small>{t($ => $.settings.network.torrentPeerAgentDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentPeerAgent}
|
||||
onChange={(event) => settings.setTorrentPeerAgent(event.target.value)}
|
||||
placeholder="Firelink/1.3.1"
|
||||
maxLength={128}
|
||||
className="app-control settings-network-input"
|
||||
aria-label={t($ => $.settings.network.torrentPeerAgent)}
|
||||
/>
|
||||
</div>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentListenPort"
|
||||
value={settings.torrentListenPort}
|
||||
label={t($ => $.settings.network.torrentListenPort)}
|
||||
description={t($ => $.settings.network.torrentListenPortDescription)}
|
||||
placeholder="6881-6999"
|
||||
onCommit={settings.setTorrentListenPort}
|
||||
onError={showTorrentNetworkInputError}
|
||||
className="app-control settings-port-input text-center"
|
||||
/>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentBindAddress"
|
||||
value={settings.torrentBindAddress}
|
||||
label={t($ => $.settings.network.torrentBindAddress)}
|
||||
description={t($ => $.settings.network.torrentBindAddressDescription)}
|
||||
placeholder="192.0.2.10 or 2001:db8::10"
|
||||
onCommit={settings.setTorrentBindAddress}
|
||||
onError={showTorrentNetworkInputError}
|
||||
/>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentDhtListenPort"
|
||||
value={settings.torrentDhtListenPort}
|
||||
label={t($ => $.settings.network.torrentDhtListenPort)}
|
||||
description={t($ => $.settings.network.torrentDhtListenPortDescription)}
|
||||
placeholder="6881-6999"
|
||||
onCommit={settings.setTorrentDhtListenPort}
|
||||
onError={showTorrentNetworkInputError}
|
||||
className="app-control settings-port-input text-center"
|
||||
/>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentExternalIp"
|
||||
value={settings.torrentExternalIp}
|
||||
label={t($ => $.settings.network.torrentExternalIp)}
|
||||
description={t($ => $.settings.network.torrentExternalIpDescription)}
|
||||
placeholder={t($ => $.settings.network.torrentExternalIpPlaceholder)}
|
||||
onCommit={settings.setTorrentExternalIp}
|
||||
onError={showTorrentNetworkInputError}
|
||||
/>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentDhtEntryPoint"
|
||||
value={settings.torrentDhtEntryPoint}
|
||||
label={t($ => $.settings.network.torrentDhtEntryPoint)}
|
||||
description={t($ => $.settings.network.torrentDhtEntryPointDescription)}
|
||||
placeholder="router.example:6881"
|
||||
onCommit={settings.setTorrentDhtEntryPoint}
|
||||
onError={showTorrentNetworkInputError}
|
||||
/>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentDhtEntryPoint6"
|
||||
value={settings.torrentDhtEntryPoint6}
|
||||
label={t($ => $.settings.network.torrentDhtEntryPoint6)}
|
||||
description={t($ => $.settings.network.torrentDhtEntryPoint6Description)}
|
||||
placeholder="[2001:db8::1]:6881"
|
||||
onCommit={settings.setTorrentDhtEntryPoint6}
|
||||
onError={showTorrentNetworkInputError}
|
||||
/>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentDhtListenAddr6"
|
||||
value={settings.torrentDhtListenAddr6}
|
||||
label={t($ => $.settings.network.torrentDhtListenAddr6)}
|
||||
description={t($ => $.settings.network.torrentDhtListenAddr6Description)}
|
||||
placeholder="2001:db8::2"
|
||||
onCommit={settings.setTorrentDhtListenAddr6}
|
||||
onError={showTorrentNetworkInputError}
|
||||
/>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentLpdInterface"
|
||||
value={settings.torrentLpdInterface}
|
||||
label={t($ => $.settings.network.torrentLpdInterface)}
|
||||
description={t($ => $.settings.network.torrentLpdInterfaceDescription)}
|
||||
placeholder="en0"
|
||||
onCommit={settings.setTorrentLpdInterface}
|
||||
onError={showTorrentNetworkInputError}
|
||||
/>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentPeerIdPrefix"
|
||||
value={settings.torrentPeerIdPrefix}
|
||||
label={t($ => $.settings.network.torrentPeerIdPrefix)}
|
||||
description={t($ => $.settings.network.torrentPeerIdPrefixDescription)}
|
||||
placeholder="-FL-1-3-1-"
|
||||
maxLength={20}
|
||||
onCommit={settings.setTorrentPeerIdPrefix}
|
||||
onError={showTorrentNetworkInputError}
|
||||
/>
|
||||
<TorrentNetworkTextInput
|
||||
field="torrentPeerAgent"
|
||||
value={settings.torrentPeerAgent}
|
||||
label={t($ => $.settings.network.torrentPeerAgent)}
|
||||
description={t($ => $.settings.network.torrentPeerAgentDescription)}
|
||||
placeholder="Firelink/1.3.1"
|
||||
maxLength={128}
|
||||
onCommit={settings.setTorrentPeerAgent}
|
||||
onError={showTorrentNetworkInputError}
|
||||
/>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentSeparateSeedSlots)}</span>
|
||||
@@ -1639,20 +1700,16 @@ runEngineChecks(false);
|
||||
aria-label={t($ => $.settings.network.torrentMaxOpenFiles)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.aria2DiskCache)}</span>
|
||||
<small>{t($ => $.settings.network.aria2DiskCacheDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.aria2DiskCache}
|
||||
onChange={(event) => settings.setAria2DiskCache(event.target.value)}
|
||||
placeholder="16M"
|
||||
className="app-control settings-network-input text-center"
|
||||
aria-label={t($ => $.settings.network.aria2DiskCache)}
|
||||
/>
|
||||
</div>
|
||||
<TorrentNetworkTextInput
|
||||
field="aria2DiskCache"
|
||||
value={settings.aria2DiskCache}
|
||||
label={t($ => $.settings.network.aria2DiskCache)}
|
||||
description={t($ => $.settings.network.aria2DiskCacheDescription)}
|
||||
placeholder="16M"
|
||||
onCommit={settings.setAria2DiskCache}
|
||||
onError={showTorrentNetworkInputError}
|
||||
className="app-control settings-network-input text-center"
|
||||
/>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentOverallUploadLimit)}</span>
|
||||
|
||||
@@ -1009,6 +1009,7 @@ const common = {
|
||||
aria2DiskCache: 'Aria2 disk cache',
|
||||
aria2DiskCacheDescription: 'Cache size for Aria2, using 0 or a positive value such as 16M. Accepts K/M values up to 1024M and applies after restart.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'Could not apply the Torrent open-file limit: {{detail}}',
|
||||
torrentNetworkInputInvalid: 'Could not apply this Torrent network setting: {{detail}}',
|
||||
torrentOverallUploadLimit: 'Overall Aria2 upload limit',
|
||||
torrentOverallUploadLimitDescription: 'Caps combined Aria2 upload traffic, primarily active Torrent seeding in Firelink. Leave blank for unlimited; the value is applied live and restored when Firelink restarts.',
|
||||
torrentOverallUploadLimitInvalid: 'Enter a valid upload limit, such as 512K or 2M.',
|
||||
|
||||
@@ -1009,6 +1009,7 @@ const fa = {
|
||||
aria2DiskCache: 'کش دیسک Aria2',
|
||||
aria2DiskCacheDescription: 'اندازهٔ کش Aria2؛ صفر یا مقداری مانند 16M وارد کنید. مقادیر K/M تا 1024M پذیرفته میشوند و پس از راهاندازی مجدد اعمال میشوند.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'اعمال محدودیت فایلهای باز تورنت ممکن نشد: {{detail}}',
|
||||
torrentNetworkInputInvalid: 'اعمال این تنظیم شبکه تورنت ممکن نیست: {{detail}}',
|
||||
torrentOverallUploadLimit: 'محدودیت کلی آپلود Aria2',
|
||||
torrentOverallUploadLimitDescription: 'سرعت کلی آپلود Aria2 را محدود میکند؛ در Firelink این مقدار عمدتاً برای سیدینگ تورنتهاست. برای نامحدود بودن خالی بگذارید؛ مقدار جدید زنده اعمال میشود و پس از راهاندازی مجدد Firelink برمیگردد.',
|
||||
torrentOverallUploadLimitInvalid: 'یک محدودیت معتبر مثل 512K یا 2M برای آپلود وارد کنید.',
|
||||
|
||||
@@ -1009,6 +1009,7 @@ const he = {
|
||||
aria2DiskCache: 'מטמון דיסק של Aria2',
|
||||
aria2DiskCacheDescription: 'גודל מטמון Aria2: 0 או ערך כמו 16M. ערכי K/M עד 1024M מתקבלים; חל לאחר הפעלה מחדש.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'לא ניתן להחיל את מגבלת הקבצים הפתוחים של Torrent: {{detail}}',
|
||||
torrentNetworkInputInvalid: 'לא ניתן להחיל את הגדרת רשת הטורנט: {{detail}}',
|
||||
torrentOverallUploadLimit: 'מגבלת העלאה כוללת של Aria2',
|
||||
torrentOverallUploadLimitDescription: 'מגבילה את מהירות ההעלאה המשולבת של Aria2, בעיקר עבור העלאת טורנטים פעילים ב-Firelink. השאר ריק ללא הגבלה; הערך מוחל מיד ומשוחזר לאחר הפעלה מחדש של Firelink.',
|
||||
torrentOverallUploadLimitInvalid: 'הזן מגבלת העלאה תקפה, למשל 512K או 2M.',
|
||||
|
||||
@@ -1009,6 +1009,7 @@ const ru = {
|
||||
aria2DiskCache: 'Дисковый кэш Aria2',
|
||||
aria2DiskCacheDescription: 'Размер кэша Aria2: 0 или значение вроде 16M. Допустимы K/M до 1024M; применяется после перезапуска.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'Не удалось применить лимит открытых файлов Torrent: {{detail}}',
|
||||
torrentNetworkInputInvalid: 'Не удалось применить сетевую настройку Torrent: {{detail}}',
|
||||
torrentOverallUploadLimit: 'Общий лимит отдачи Aria2',
|
||||
torrentOverallUploadLimitDescription: 'Ограничивает суммарную скорость отдачи Aria2; в Firelink это в основном раздача активных торрентов. Оставьте поле пустым для снятия ограничения; значение применяется сразу и восстанавливается после перезапуска Firelink.',
|
||||
torrentOverallUploadLimitInvalid: 'Введите корректный лимит отдачи, например 512K или 2M.',
|
||||
|
||||
@@ -1009,6 +1009,7 @@ const uk = {
|
||||
aria2DiskCache: 'Дисковий кеш Aria2',
|
||||
aria2DiskCacheDescription: 'Розмір кешу Aria2: 0 або значення на кшталт 16M. Допустимі K/M до 1024M; застосовується після перезапуску.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'Не вдалося застосувати обмеження відкритих файлів Torrent: {{detail}}',
|
||||
torrentNetworkInputInvalid: 'Не вдалося застосувати мережеве налаштування Torrent: {{detail}}',
|
||||
torrentOverallUploadLimit: 'Загальне обмеження віддачі Aria2',
|
||||
torrentOverallUploadLimitDescription: 'Обмежує сумарну швидкість віддачі Aria2; у Firelink це переважно роздача активних торрентів. Залиште поле порожнім без обмеження; значення застосовується одразу й відновлюється після перезапуску Firelink.',
|
||||
torrentOverallUploadLimitInvalid: 'Введіть коректне обмеження віддачі, наприклад 512K або 2M.',
|
||||
|
||||
@@ -1009,6 +1009,7 @@ const zhCN = {
|
||||
aria2DiskCache: 'Aria2 磁盘缓存',
|
||||
aria2DiskCacheDescription: 'Aria2 缓存大小:0 或类似 16M 的值。接受最大 1024M 的 K/M 值,重启后生效。',
|
||||
torrentMaxOpenFilesUpdateFailed: '无法应用 Torrent 打开文件数限制:{{detail}}',
|
||||
torrentNetworkInputInvalid: '无法应用此 Torrent 网络设置:{{detail}}',
|
||||
torrentOverallUploadLimit: 'Aria2 总上传限制',
|
||||
torrentOverallUploadLimitDescription: '限制 Aria2 的总上传速度,在 Firelink 中主要用于活动 Torrent 做种。留空表示不限速;新值会立即应用,并在 Firelink 重启后恢复。',
|
||||
torrentOverallUploadLimitInvalid: '请输入有效的上传限制,例如 512K 或 2M。',
|
||||
|
||||
@@ -144,6 +144,10 @@ type CommandMap = {
|
||||
get_supported_media_domains: { args: undefined; result: string[] };
|
||||
db_save_settings: { args: { data: string }; result: void };
|
||||
db_load_settings: { args: undefined; result: string | null };
|
||||
canonicalize_torrent_network_setting: {
|
||||
args: { field: string; value: string };
|
||||
result: string;
|
||||
};
|
||||
db_get_all_downloads: { args: undefined; result: string[] };
|
||||
db_replace_downloads: { args: { data: string }; result: void };
|
||||
db_commit_download_state: {
|
||||
|
||||
@@ -133,6 +133,33 @@ describe('Torrent peer discovery preferences', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('clears an IPv6 bind address when IPv6 transport is disabled', () => {
|
||||
useSettingsStore.setState({
|
||||
torrentIpv6Enabled: true,
|
||||
torrentBindAddress: '2001:db8::10'
|
||||
});
|
||||
|
||||
useSettingsStore.getState().setTorrentIpv6Enabled(false);
|
||||
|
||||
expect(useSettingsStore.getState()).toMatchObject({
|
||||
torrentIpv6Enabled: false,
|
||||
torrentBindAddress: ''
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects an IPv6 bind address entered after IPv6 transport is disabled', () => {
|
||||
useSettingsStore.setState({
|
||||
torrentIpv6Enabled: false,
|
||||
torrentBindAddress: ''
|
||||
});
|
||||
|
||||
expect(useSettingsStore.getState().setTorrentBindAddress('2001:db8::10')).toBe(false);
|
||||
expect(useSettingsStore.getState().torrentBindAddress).toBe('');
|
||||
|
||||
expect(useSettingsStore.getState().setTorrentBindAddress('192.0.2.10')).toBe(true);
|
||||
expect(useSettingsStore.getState().torrentBindAddress).toBe('192.0.2.10');
|
||||
});
|
||||
|
||||
it('matches Aria2 defaults and persists explicit changes', async () => {
|
||||
expect(useSettingsStore.getState().torrentEnableDht).toBe(true);
|
||||
expect(useSettingsStore.getState().torrentEnableDht6).toBe(false);
|
||||
|
||||
@@ -364,7 +364,7 @@ export interface SettingsState {
|
||||
setTorrentLpdInterface: (value: string) => void;
|
||||
setTorrentPeerIdPrefix: (value: string) => void;
|
||||
setTorrentPeerAgent: (value: string) => void;
|
||||
setTorrentBindAddress: (value: string) => void;
|
||||
setTorrentBindAddress: (value: string) => boolean;
|
||||
setAria2DiskCache: (value: string) => void;
|
||||
setCustomUserAgent: (userAgent: string) => void;
|
||||
setAskWhereToSaveEachFile: (ask: boolean) => void;
|
||||
@@ -612,7 +612,17 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
setTorrentLpdInterface: (torrentLpdInterface) => set({ torrentLpdInterface }),
|
||||
setTorrentPeerIdPrefix: (torrentPeerIdPrefix) => set({ torrentPeerIdPrefix }),
|
||||
setTorrentPeerAgent: (torrentPeerAgent) => set({ torrentPeerAgent }),
|
||||
setTorrentBindAddress: (torrentBindAddress) => set({ torrentBindAddress }),
|
||||
setTorrentBindAddress: (torrentBindAddress) => {
|
||||
let accepted = true;
|
||||
set(state => {
|
||||
if (!state.torrentIpv6Enabled && torrentBindAddress.includes(':')) {
|
||||
accepted = false;
|
||||
return state;
|
||||
}
|
||||
return { torrentBindAddress };
|
||||
});
|
||||
return accepted;
|
||||
},
|
||||
setAria2DiskCache: (aria2DiskCache) => set({ aria2DiskCache }),
|
||||
setTorrentMaxOpenFiles: (value) => {
|
||||
const normalized = normalizeTorrentMaxOpenFiles(value);
|
||||
@@ -643,7 +653,15 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
? value
|
||||
: DEFAULT_TORRENT_MAX_CONCURRENT_SEEDS
|
||||
}),
|
||||
setTorrentIpv6Enabled: (torrentIpv6Enabled) => set({ torrentIpv6Enabled }),
|
||||
setTorrentIpv6Enabled: (torrentIpv6Enabled) => set(state => ({
|
||||
torrentIpv6Enabled,
|
||||
// An IPv6 bind address is invalid once IPv6 transport is disabled.
|
||||
// Clear it as part of the same state transition so the next durable
|
||||
// settings save cannot fail on a cross-field contradiction.
|
||||
...(torrentIpv6Enabled || !state.torrentBindAddress.includes(':')
|
||||
? {}
|
||||
: { torrentBindAddress: '' })
|
||||
})),
|
||||
setCustomUserAgent: (customUserAgent) => set({ customUserAgent }),
|
||||
setAskWhereToSaveEachFile: (askWhereToSaveEachFile) => set({ askWhereToSaveEachFile }),
|
||||
setPreventsSleepWhileDownloading: (preventsSleepWhileDownloading) => {
|
||||
|
||||
@@ -81,7 +81,8 @@ export const MIN_TORRENT_MAX_OPEN_FILES = 1;
|
||||
export const MAX_TORRENT_MAX_OPEN_FILES = 4096;
|
||||
export const DEFAULT_TORRENT_DHT_MESSAGE_TIMEOUT = 10;
|
||||
export const MIN_TORRENT_DHT_MESSAGE_TIMEOUT = 1;
|
||||
export const MAX_TORRENT_DHT_MESSAGE_TIMEOUT = 600;
|
||||
// Aria2 1.37.0 accepts DHT message timeouts only from 1 through 60 seconds.
|
||||
export const MAX_TORRENT_DHT_MESSAGE_TIMEOUT = 60;
|
||||
export const DEFAULT_TORRENT_MAX_CONCURRENT_SEEDS = 2;
|
||||
export const MIN_TORRENT_MAX_CONCURRENT_SEEDS = 1;
|
||||
export const MAX_TORRENT_MAX_CONCURRENT_SEEDS = 64;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { shouldApplyTorrentNetworkInputResult } from './torrentNetworkInput';
|
||||
|
||||
describe('Torrent network input request fencing', () => {
|
||||
it('rejects a response from an older request', () => {
|
||||
expect(shouldApplyTorrentNetworkInputResult(1, 2, 4, 4)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a response after the user edits the draft', () => {
|
||||
expect(shouldApplyTorrentNetworkInputResult(2, 2, 3, 4)).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts only the current request for the current draft', () => {
|
||||
expect(shouldApplyTorrentNetworkInputResult(3, 3, 5, 5)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export const shouldApplyTorrentNetworkInputResult = (
|
||||
requestId: number,
|
||||
currentRequestId: number,
|
||||
editRequestId: number,
|
||||
currentEditId: number
|
||||
): boolean => requestId === currentRequestId && editRequestId === currentEditId;
|
||||
Reference in New Issue
Block a user