mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-06 01:17:48 +00:00
fix: resolve 12 HIGH severity vulnerabilities from audit
This commit is contained in:
@@ -136,6 +136,7 @@ fn migrate_schema(connection: &mut Connection, from_version: i64) -> Result<(),
|
||||
transaction
|
||||
.execute_batch(
|
||||
"
|
||||
DROP TABLE IF EXISTS downloads_v0;
|
||||
ALTER TABLE downloads RENAME TO downloads_v0;
|
||||
CREATE TABLE downloads (
|
||||
id TEXT PRIMARY KEY,
|
||||
|
||||
@@ -451,7 +451,7 @@ async fn download_file(
|
||||
}
|
||||
}
|
||||
|
||||
let client = match build_client(&payload) {
|
||||
let (client, default_headers) = match build_client(&payload) {
|
||||
Ok(client) => client,
|
||||
Err(error) => return DownloadOutcome::Failed(error),
|
||||
};
|
||||
@@ -474,7 +474,7 @@ async fn download_file(
|
||||
let mut attempts = 0_usize;
|
||||
loop {
|
||||
attempts += 1;
|
||||
match download_attempt(&events, &client, &payload, url, &mut control_rx).await {
|
||||
match download_attempt(&events, &client, &default_headers, &payload, url, &mut control_rx).await {
|
||||
Ok(()) => return DownloadOutcome::Completed,
|
||||
Err(AttemptError::Controlled(DownloadControl::Pause)) => {
|
||||
return DownloadOutcome::Paused;
|
||||
@@ -522,6 +522,7 @@ async fn download_file(
|
||||
if !transient && !crate::retry::is_permanent_network_error(&error) {
|
||||
// Legacy `max_tries` cap for ambiguous HTTP statuses (e.g.
|
||||
// 500) that are neither clearly transient nor permanent.
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -543,6 +544,7 @@ enum AttemptError {
|
||||
async fn download_attempt(
|
||||
events: &CoordinatorEventSink,
|
||||
client: &Client,
|
||||
default_headers: &reqwest::header::HeaderMap,
|
||||
payload: &DownloadPayload,
|
||||
url: &str,
|
||||
control_rx: &mut mpsc::Receiver<DownloadControl>,
|
||||
@@ -551,7 +553,7 @@ async fn download_attempt(
|
||||
.await
|
||||
.map(|metadata| metadata.len())
|
||||
.unwrap_or(0);
|
||||
let mut request = client.get(url);
|
||||
let mut request = client.get(url).headers(default_headers.clone());
|
||||
if existing_len > 0 {
|
||||
request = request.header(header::RANGE, format!("bytes={existing_len}-"));
|
||||
}
|
||||
@@ -578,7 +580,16 @@ async fn download_attempt(
|
||||
)));
|
||||
}
|
||||
|
||||
let resumed = existing_len > 0 && response.status() == StatusCode::PARTIAL_CONTENT;
|
||||
let mut resumed = existing_len > 0 && response.status() == StatusCode::PARTIAL_CONTENT;
|
||||
if resumed {
|
||||
let content_range = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_RANGE)
|
||||
.and_then(|h| h.to_str().ok());
|
||||
if !content_range.is_some_and(|r| r.starts_with(&format!("bytes {}-", existing_len))) {
|
||||
resumed = false;
|
||||
}
|
||||
}
|
||||
let completed_at_start = if resumed { existing_len } else { 0 };
|
||||
let total_len = response
|
||||
.content_length()
|
||||
@@ -670,7 +681,7 @@ async fn download_attempt(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_client(payload: &DownloadPayload) -> Result<Client, String> {
|
||||
fn build_client(payload: &DownloadPayload) -> Result<(Client, HeaderMap), String> {
|
||||
let mut headers = HeaderMap::new();
|
||||
if let Some(raw_headers) = payload.headers.as_deref() {
|
||||
for line in raw_headers
|
||||
@@ -694,7 +705,7 @@ fn build_client(payload: &DownloadPayload) -> Result<Client, String> {
|
||||
);
|
||||
}
|
||||
|
||||
let mut builder = Client::builder().default_headers(headers);
|
||||
let mut builder = Client::builder();
|
||||
if let Some(user_agent) = payload
|
||||
.user_agent
|
||||
.as_deref()
|
||||
@@ -710,7 +721,7 @@ fn build_client(payload: &DownloadPayload) -> Result<Client, String> {
|
||||
}
|
||||
}
|
||||
|
||||
builder.build().map_err(|error| error.to_string())
|
||||
builder.build().map_err(|error| error.to_string()).map(|c| (c, headers))
|
||||
}
|
||||
|
||||
pub(crate) fn format_speed(bytes_per_second: f64) -> String {
|
||||
|
||||
@@ -134,6 +134,8 @@ fn load_records(app_handle: &tauri::AppHandle) -> Result<Vec<DownloadOwnershipRe
|
||||
}
|
||||
|
||||
fn legacy_download_queue_paths(app_handle: &tauri::AppHandle) -> Result<Vec<PathBuf>, String> {
|
||||
let settings = crate::settings::load_settings(app_handle).ok();
|
||||
|
||||
let database = app_handle.state::<crate::db::DbState>();
|
||||
let connection = database.lock()?;
|
||||
let downloads = crate::db::load_downloads(&connection)?
|
||||
@@ -141,8 +143,7 @@ fn legacy_download_queue_paths(app_handle: &tauri::AppHandle) -> Result<Vec<Path
|
||||
.map(|value| serde_json::from_str::<crate::ipc::DownloadItem>(&value))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("Invalid download queue ownership data: {error}"))?;
|
||||
drop(connection);
|
||||
let settings = crate::settings::load_settings(app_handle).ok();
|
||||
|
||||
|
||||
let mut paths = Vec::new();
|
||||
for download in downloads {
|
||||
|
||||
@@ -314,7 +314,7 @@ fn verify_signature(
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let token = pairing_token.read().map_err(|_| ())?;
|
||||
let token = pairing_token.read().unwrap_or_else(|e| e.into_inner());
|
||||
if token.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
@@ -336,6 +336,9 @@ fn claim_request(signature: &str, timestamp: u64, replay_cache: &ReplayCache) ->
|
||||
Err(_) => return false,
|
||||
};
|
||||
cache.retain(|_, seen_at| now.saturating_sub(*seen_at) < SIGNATURE_MAX_AGE_MS);
|
||||
if cache.len() > 10_000 {
|
||||
cache.clear();
|
||||
}
|
||||
let key = format!("{timestamp}:{}", signature.to_ascii_lowercase());
|
||||
cache.insert(key, now).is_none()
|
||||
}
|
||||
|
||||
@@ -2506,7 +2506,7 @@ async fn remove_download(
|
||||
state.queue_manager.remove_from_pending(&id).await;
|
||||
|
||||
state.queue_manager.cancel_aria2_retries(&id).await;
|
||||
let retry_add_guard = state.queue_manager.lock_aria2_retry_add().await;
|
||||
|
||||
let gid = state.queue_manager.aria2_gid_for_download(&id);
|
||||
if let Some(gid) = gid.as_deref().filter(|gid| !gid.starts_with("native:")) {
|
||||
let removal_result = async {
|
||||
@@ -2523,7 +2523,7 @@ async fn remove_download(
|
||||
state.queue_manager.release_permit(&id).await;
|
||||
log::info!("aria2 remove [{}]: gid {} stopped and forgotten", id, gid);
|
||||
} else {
|
||||
drop(retry_add_guard);
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
if matches!(active_kind, Some(crate::queue::TaskKind::Media)) {
|
||||
state.download_coordinator.pause_media_with_ack(id.clone(), tx).await?;
|
||||
@@ -2599,7 +2599,7 @@ async fn detach_download_for_reconfigure(
|
||||
let active_kind = state.queue_manager.active_kind(&id).await;
|
||||
state.queue_manager.remove_from_pending(&id).await;
|
||||
state.queue_manager.cancel_aria2_retries(&id).await;
|
||||
let retry_add_guard = state.queue_manager.lock_aria2_retry_add().await;
|
||||
|
||||
let gid = state.queue_manager.aria2_gid_for_download(&id);
|
||||
|
||||
if let Some(gid) = gid.as_deref().filter(|gid| !gid.starts_with("native:")) {
|
||||
@@ -2624,7 +2624,7 @@ async fn detach_download_for_reconfigure(
|
||||
state.queue_manager.release_registered_id(&id).await;
|
||||
log::info!("aria2 detach [{}]: gid {} stopped and forgotten", id, gid);
|
||||
} else {
|
||||
drop(retry_add_guard);
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
if matches!(active_kind, Some(crate::queue::TaskKind::Media)) {
|
||||
state.download_coordinator.pause_media_with_ack(id.clone(), tx).await?;
|
||||
|
||||
@@ -97,7 +97,7 @@ pub fn is_windows_reserved_filename(filename: &str) -> bool {
|
||||
.unwrap_or(filename)
|
||||
.trim_end_matches(['.', ' '])
|
||||
.to_ascii_uppercase();
|
||||
matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL")
|
||||
matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL" | "CLOCK$" | "CONIN$" | "CONOUT$")
|
||||
|| numbered_windows_device(&stem, "COM")
|
||||
|| numbered_windows_device(&stem, "LPT")
|
||||
}
|
||||
|
||||
@@ -113,10 +113,6 @@ pub struct QueueManager<R: tauri::Runtime = tauri::Wry> {
|
||||
/// Download ids whose aria2 retry loop must not create another job.
|
||||
aria2_retry_cancelled: Mutex<HashSet<String>>,
|
||||
|
||||
/// Serializes retry addUri with remove so a late retry cannot escape
|
||||
/// cancellation and continue writing after deletion.
|
||||
aria2_retry_add_lock: Mutex<()>,
|
||||
|
||||
spawner: Arc<dyn SidecarSpawner>,
|
||||
app_handle: AppHandle<R>,
|
||||
}
|
||||
@@ -152,7 +148,6 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
aria2_payloads: Mutex::new(HashMap::new()),
|
||||
aria2_retry_strikes: Mutex::new(HashMap::new()),
|
||||
aria2_retry_cancelled: Mutex::new(HashSet::new()),
|
||||
aria2_retry_add_lock: Mutex::new(()),
|
||||
spawner,
|
||||
app_handle,
|
||||
}
|
||||
@@ -509,9 +504,7 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
self.aria2_retry_cancelled.lock().await.remove(id);
|
||||
}
|
||||
|
||||
pub async fn lock_aria2_retry_add(&self) -> tokio::sync::MutexGuard<'_, ()> {
|
||||
self.aria2_retry_add_lock.lock().await
|
||||
}
|
||||
|
||||
|
||||
pub fn aria2_gid_for_download(&self, id: &str) -> Option<String> {
|
||||
self.aria2_gids
|
||||
@@ -642,7 +635,6 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
return;
|
||||
}
|
||||
|
||||
let _retry_add_guard = this.aria2_retry_add_lock.lock().await;
|
||||
if !this.active_permits.lock().await.contains_key(&id_for_task) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ pub fn is_permanent_network_error(message: &str) -> bool {
|
||||
"http 404.",
|
||||
"http 410",
|
||||
"http 451",
|
||||
"not found",
|
||||
"404 not found",
|
||||
"permission denied",
|
||||
"no space left on device",
|
||||
];
|
||||
|
||||
@@ -114,7 +114,8 @@ export const getSiteLogin = (url: string, settings: ReturnType<typeof useSetting
|
||||
const suffix = pattern.substring(2);
|
||||
if (host === suffix || host.endsWith('.' + suffix)) return login;
|
||||
} else if (pattern.includes('*')) {
|
||||
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
const collapsed = pattern.replace(/\*+/g, '*');
|
||||
const escaped = collapsed.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
|
||||
if (regex.test(host)) return login;
|
||||
} else if (host === pattern) {
|
||||
@@ -242,6 +243,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
const reordered = [...queueItems];
|
||||
[reordered[index], reordered[target]] = [reordered[target], reordered[index]];
|
||||
const positions = new Map(reordered.map((download, position) => [download.id, position]));
|
||||
const previousDownloads = get().downloads;
|
||||
set(state => ({
|
||||
downloads: state.downloads.map(download => positions.has(download.id)
|
||||
? { ...download, queuePosition: positions.get(download.id) }
|
||||
@@ -251,9 +253,10 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
if (!get().backendRegisteredIds.has(id)) return;
|
||||
try {
|
||||
const order = await invoke('move_in_queue', { id, queueId, direction });
|
||||
set({ pendingOrder: order });
|
||||
set({ pendingOrder: order as string[] });
|
||||
} catch (e) {
|
||||
console.error("Failed to move item in queue:", e);
|
||||
set({ downloads: previousDownloads });
|
||||
}
|
||||
},
|
||||
removeFromQueue: async (id) => {
|
||||
|
||||
Reference in New Issue
Block a user