mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-28 19:47:26 +00:00
fix: resolve P2 and P3 audit findings
This commit is contained in:
+113
-19
@@ -283,18 +283,54 @@ async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_safe_path(path: &std::path::Path) -> bool {
|
||||||
|
!path.components().any(|c| matches!(c, std::path::Component::ParentDir))
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn open_file(app: tauri::AppHandle, path: String) -> Result<(), String> {
|
async fn open_file(app: tauri::AppHandle, path: String) -> Result<(), String> {
|
||||||
println!("open_file called for path: {}", path);
|
println!("open_file called for path: {}", path);
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
app.opener().open_path(&path, None::<String>).map_err(|e| format!("Failed to open file: {}", e))
|
|
||||||
|
let mut resolved_dest = std::path::PathBuf::from(&path);
|
||||||
|
if path.starts_with("~/") {
|
||||||
|
if let Ok(home) = app.path().home_dir() {
|
||||||
|
resolved_dest = home.join(&path[2..]);
|
||||||
|
}
|
||||||
|
} else if path == "~" {
|
||||||
|
if let Ok(home) = app.path().home_dir() {
|
||||||
|
resolved_dest = home;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_safe_path(&resolved_dest) {
|
||||||
|
return Err("Path traversal blocked".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.opener().open_path(resolved_dest.to_string_lossy().as_ref(), None::<String>).map_err(|e| format!("Failed to open file: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn show_in_folder(app: tauri::AppHandle, path: String) -> Result<(), String> {
|
async fn show_in_folder(app: tauri::AppHandle, path: String) -> Result<(), String> {
|
||||||
println!("show_in_folder called for path: {}", path);
|
println!("show_in_folder called for path: {}", path);
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
app.opener().reveal_item_in_dir(&path).map_err(|e| format!("Failed to reveal in folder: {}", e))
|
|
||||||
|
let mut resolved_dest = std::path::PathBuf::from(&path);
|
||||||
|
if path.starts_with("~/") {
|
||||||
|
if let Ok(home) = app.path().home_dir() {
|
||||||
|
resolved_dest = home.join(&path[2..]);
|
||||||
|
}
|
||||||
|
} else if path == "~" {
|
||||||
|
if let Ok(home) = app.path().home_dir() {
|
||||||
|
resolved_dest = home;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_safe_path(&resolved_dest) {
|
||||||
|
return Err("Path traversal blocked".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.opener().reveal_item_in_dir(resolved_dest.to_string_lossy().as_ref()).map_err(|e| format!("Failed to reveal in folder: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
@@ -538,8 +574,54 @@ async fn start_download(
|
|||||||
resolved_dest = home;
|
resolved_dest = home;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = connections;
|
|
||||||
let _ = checksum;
|
if !is_safe_path(&resolved_dest) {
|
||||||
|
return Err(AppError::Internal("Path traversal blocked".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if connections.unwrap_or(1) > 1 || checksum.is_some() {
|
||||||
|
println!("Routing multi-part/checksum download to aria2: {}", id);
|
||||||
|
let mut options = serde_json::Map::new();
|
||||||
|
options.insert("dir".to_string(), serde_json::json!(resolved_dest.to_string_lossy().to_string()));
|
||||||
|
options.insert("out".to_string(), serde_json::json!(filename));
|
||||||
|
if let Some(conn) = connections {
|
||||||
|
options.insert("split".to_string(), serde_json::json!(conn.to_string()));
|
||||||
|
options.insert("max-connection-per-server".to_string(), serde_json::json!(conn.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(speed) = speed_limit {
|
||||||
|
options.insert("max-download-limit".to_string(), serde_json::json!(speed));
|
||||||
|
}
|
||||||
|
if let Some(user) = username {
|
||||||
|
options.insert("http-user".to_string(), serde_json::json!(user));
|
||||||
|
}
|
||||||
|
if let Some(pass) = password {
|
||||||
|
options.insert("http-passwd".to_string(), serde_json::json!(pass));
|
||||||
|
}
|
||||||
|
if let Some(chk) = checksum {
|
||||||
|
options.insert("checksum".to_string(), serde_json::json!(chk));
|
||||||
|
}
|
||||||
|
if let Some(ua) = user_agent {
|
||||||
|
options.insert("user-agent".to_string(), serde_json::json!(ua));
|
||||||
|
}
|
||||||
|
if let Some(prox) = proxy {
|
||||||
|
options.insert("all-proxy".to_string(), serde_json::json!(prox));
|
||||||
|
}
|
||||||
|
if let Some(cook) = cookies {
|
||||||
|
options.insert("header".to_string(), serde_json::json!(format!("Cookie: {}", cook)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = serde_json::json!([
|
||||||
|
[url],
|
||||||
|
options
|
||||||
|
]);
|
||||||
|
|
||||||
|
rpc_call(state.aria2_port, &state.aria2_secret, "aria2.addUri", params)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| AppError::Internal(e))?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
state
|
state
|
||||||
.download_coordinator
|
.download_coordinator
|
||||||
.send(download::DownloadCmd::Start(download::DownloadPayload {
|
.send(download::DownloadCmd::Start(download::DownloadPayload {
|
||||||
@@ -927,25 +1009,29 @@ fn perform_system_action(action: crate::ipc::PostQueueAction) -> Result<(), Stri
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn set_concurrent_limit(state: tauri::State<'_, AppState>, limit: usize) -> Result<(), String> {
|
async fn set_concurrent_limit(state: tauri::State<'_, AppState>, limit: usize) -> Result<(), String> {
|
||||||
let _ = rpc_call(
|
rpc_call(
|
||||||
state.aria2_port,
|
state.aria2_port,
|
||||||
&state.aria2_secret,
|
&state.aria2_secret,
|
||||||
"aria2.changeGlobalOption",
|
"aria2.changeGlobalOption",
|
||||||
serde_json::json!([{"max-concurrent-downloads": limit.to_string()}])
|
serde_json::json!([{"max-concurrent-downloads": limit.to_string()}])
|
||||||
).await;
|
).await.map(|_| ()).map_err(|e| {
|
||||||
Ok(())
|
eprintln!("Failed to set concurrent limit: {}", e);
|
||||||
|
e
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn set_global_speed_limit(state: tauri::State<'_, AppState>, limit: Option<String>) -> Result<(), String> {
|
async fn set_global_speed_limit(state: tauri::State<'_, AppState>, limit: Option<String>) -> Result<(), String> {
|
||||||
let limit_str = limit.unwrap_or_else(|| "0".to_string());
|
let limit_str = limit.unwrap_or_else(|| "0".to_string());
|
||||||
let _ = rpc_call(
|
rpc_call(
|
||||||
state.aria2_port,
|
state.aria2_port,
|
||||||
&state.aria2_secret,
|
&state.aria2_secret,
|
||||||
"aria2.changeGlobalOption",
|
"aria2.changeGlobalOption",
|
||||||
serde_json::json!([{"max-overall-download-limit": limit_str}])
|
serde_json::json!([{"max-overall-download-limit": limit_str}])
|
||||||
).await;
|
).await.map(|_| ()).map_err(|e| {
|
||||||
Ok(())
|
eprintln!("Failed to set global speed limit: {}", e);
|
||||||
|
e
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -957,16 +1043,18 @@ fn request_automation_permission() -> Result<(), String> {
|
|||||||
use objc::{msg_send, sel, sel_impl, class};
|
use objc::{msg_send, sel, sel_impl, class};
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let script_str = NSString::alloc(nil).init_str("tell application \"Finder\" to get name");
|
objc::rc::autoreleasepool(|| {
|
||||||
let ns_apple_script: id = msg_send![class!(NSAppleScript), alloc];
|
let script_str = NSString::alloc(nil).init_str("tell application \"Finder\" to get name");
|
||||||
let ns_apple_script: id = msg_send![ns_apple_script, initWithSource: script_str];
|
let ns_apple_script: id = msg_send![class!(NSAppleScript), alloc];
|
||||||
let mut error_dict: id = nil;
|
let ns_apple_script: id = msg_send![ns_apple_script, initWithSource: script_str];
|
||||||
let result: id = msg_send![ns_apple_script, executeAndReturnError: &mut error_dict];
|
let mut error_dict: id = nil;
|
||||||
if result == nil {
|
let result: id = msg_send![ns_apple_script, executeAndReturnError: &mut error_dict];
|
||||||
return Err("Automation permission was not granted".to_string());
|
if result == nil {
|
||||||
}
|
return Err("Automation permission was not granted".to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -1067,6 +1155,9 @@ fn check_file_exists(app_handle: tauri::AppHandle, path: String) -> bool {
|
|||||||
resolved_dest = home;
|
resolved_dest = home;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !is_safe_path(&resolved_dest) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
resolved_dest.exists()
|
resolved_dest.exists()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1083,6 +1174,9 @@ fn delete_file(app_handle: tauri::AppHandle, path: String) -> Result<(), String>
|
|||||||
resolved_dest = home;
|
resolved_dest = home;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !is_safe_path(&resolved_dest) {
|
||||||
|
return Err("Path traversal blocked".to_string());
|
||||||
|
}
|
||||||
if resolved_dest.exists() {
|
if resolved_dest.exists() {
|
||||||
std::fs::remove_file(resolved_dest).map_err(|e| e.to_string())
|
std::fs::remove_file(resolved_dest).map_err(|e| e.to_string())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ pub async fn get_system_proxy() -> Result<Option<String>, String> {
|
|||||||
match sysproxy::Sysproxy::get_system_proxy() {
|
match sysproxy::Sysproxy::get_system_proxy() {
|
||||||
Ok(proxy) => {
|
Ok(proxy) => {
|
||||||
if proxy.enable {
|
if proxy.enable {
|
||||||
// Determine protocol, usually sysproxy returns the host and port
|
let protocol = if proxy.host.contains("://") { "" } else { "http://" };
|
||||||
// We'll default to http:// unless the user has configured something specific
|
Ok(Some(format!("{}{}:{}", protocol, proxy.host, proxy.port)))
|
||||||
Ok(Some(format!("http://{}:{}", proxy.host, proxy.port)))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ export const AddDownloadsModal = () => {
|
|||||||
|
|
||||||
// Metadata parser
|
// Metadata parser
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
const lines = urls.split('\n').map(u => u.trim()).filter(u => u.length > 0);
|
const lines = urls.split('\n').map(u => u.trim()).filter(u => u.length > 0);
|
||||||
|
|
||||||
// Immediately display items in loading state
|
// Immediately display items in loading state
|
||||||
@@ -340,6 +341,7 @@ export const AddDownloadsModal = () => {
|
|||||||
let firstReadyIndex: number | null = null;
|
let firstReadyIndex: number | null = null;
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (!active) break;
|
||||||
const url = lines[i];
|
const url = lines[i];
|
||||||
try {
|
try {
|
||||||
new URL(url);
|
new URL(url);
|
||||||
@@ -408,15 +410,18 @@ export const AddDownloadsModal = () => {
|
|||||||
console.error("Meta fetch failed", e);
|
console.error("Meta fetch failed", e);
|
||||||
updatedItems[i] = { ...updatedItems[i], size: 'Unknown', sizeBytes: 0, status: 'Error' };
|
updatedItems[i] = { ...updatedItems[i], size: 'Unknown', sizeBytes: 0, status: 'Error' };
|
||||||
}
|
}
|
||||||
setParsedItems([...updatedItems]);
|
if (active) setParsedItems([...updatedItems]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstReadyIndex !== null) {
|
if (active && firstReadyIndex !== null) {
|
||||||
setSelectedItemIndex(firstReadyIndex);
|
setSelectedItemIndex(firstReadyIndex);
|
||||||
}
|
}
|
||||||
}, 400);
|
}, 400);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => {
|
||||||
|
active = false;
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
}, [urls, pendingAddFilename]);
|
}, [urls, pendingAddFilename]);
|
||||||
|
|
||||||
if (!isAddModalOpen) return null;
|
if (!isAddModalOpen) return null;
|
||||||
|
|||||||
@@ -245,6 +245,11 @@ export default function SettingsView() {
|
|||||||
type="number" min="1" max="16"
|
type="number" min="1" max="16"
|
||||||
value={settings.perServerConnections}
|
value={settings.perServerConnections}
|
||||||
onChange={(e) => settings.setPerServerConnections(Number(e.target.value))}
|
onChange={(e) => settings.setPerServerConnections(Number(e.target.value))}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const val = Number(e.target.value);
|
||||||
|
if (val < 1) settings.setPerServerConnections(1);
|
||||||
|
if (val > 16) settings.setPerServerConnections(16);
|
||||||
|
}}
|
||||||
className="app-control w-24 text-center"
|
className="app-control w-24 text-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,6 +262,11 @@ export default function SettingsView() {
|
|||||||
type="number" min="1" max="12"
|
type="number" min="1" max="12"
|
||||||
value={settings.maxConcurrentDownloads}
|
value={settings.maxConcurrentDownloads}
|
||||||
onChange={(e) => settings.setMaxConcurrentDownloads(Number(e.target.value))}
|
onChange={(e) => settings.setMaxConcurrentDownloads(Number(e.target.value))}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const val = Number(e.target.value);
|
||||||
|
if (val < 1) settings.setMaxConcurrentDownloads(1);
|
||||||
|
if (val > 12) settings.setMaxConcurrentDownloads(12);
|
||||||
|
}}
|
||||||
className="app-control w-24 text-center"
|
className="app-control w-24 text-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,6 +295,11 @@ export default function SettingsView() {
|
|||||||
type="number" min="0" max="10"
|
type="number" min="0" max="10"
|
||||||
value={settings.maxAutomaticRetries}
|
value={settings.maxAutomaticRetries}
|
||||||
onChange={(e) => settings.setMaxAutomaticRetries(Number(e.target.value))}
|
onChange={(e) => settings.setMaxAutomaticRetries(Number(e.target.value))}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const val = Number(e.target.value);
|
||||||
|
if (val < 0) settings.setMaxAutomaticRetries(0);
|
||||||
|
if (val > 10) settings.setMaxAutomaticRetries(10);
|
||||||
|
}}
|
||||||
className="app-control w-24 text-center"
|
className="app-control w-24 text-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -453,9 +468,14 @@ export default function SettingsView() {
|
|||||||
<div className="mac-settings-row">
|
<div className="mac-settings-row">
|
||||||
<span className="text-[13px] text-text-primary pl-4">Proxy Port</span>
|
<span className="text-[13px] text-text-primary pl-4">Proxy Port</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number" min="1" max="65535"
|
||||||
value={settings.proxyPort}
|
value={settings.proxyPort}
|
||||||
onChange={(e) => settings.setProxyPort(Number(e.target.value))}
|
onChange={(e) => settings.setProxyPort(Number(e.target.value))}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const val = Number(e.target.value);
|
||||||
|
if (val < 1) settings.setProxyPort(1);
|
||||||
|
if (val > 65535) settings.setProxyPort(65535);
|
||||||
|
}}
|
||||||
className="app-control w-24 text-center"
|
className="app-control w-24 text-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useSettingsStore } from './useSettingsStore';
|
|||||||
export type { DownloadCategory } from '../utils/downloads';
|
export type { DownloadCategory } from '../utils/downloads';
|
||||||
|
|
||||||
const getProxyArgs = (settings: ReturnType<typeof useSettingsStore.getState>) => {
|
const getProxyArgs = (settings: ReturnType<typeof useSettingsStore.getState>) => {
|
||||||
|
if (settings.proxyMode === 'system') return 'system';
|
||||||
if (settings.proxyMode === 'custom' && settings.proxyHost) {
|
if (settings.proxyMode === 'custom' && settings.proxyHost) {
|
||||||
return `http://${settings.proxyHost}:${settings.proxyPort}`;
|
return `http://${settings.proxyHost}:${settings.proxyPort}`;
|
||||||
}
|
}
|
||||||
@@ -25,7 +26,8 @@ export const getSiteLogin = (url: string, settings: ReturnType<typeof useSetting
|
|||||||
const suffix = pattern.substring(2);
|
const suffix = pattern.substring(2);
|
||||||
if (host === suffix || host.endsWith('.' + suffix)) return login;
|
if (host === suffix || host.endsWith('.' + suffix)) return login;
|
||||||
} else if (pattern.includes('*')) {
|
} else if (pattern.includes('*')) {
|
||||||
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const regex = new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
|
||||||
if (regex.test(host)) return login;
|
if (regex.test(host)) return login;
|
||||||
} else if (host === pattern) {
|
} else if (host === pattern) {
|
||||||
return login;
|
return login;
|
||||||
@@ -191,9 +193,13 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
redownload: (id) => {
|
redownload: (id) => {
|
||||||
let updatedItem: DownloadItem | null = null;
|
let updatedItem: DownloadItem | null = null;
|
||||||
|
let wasDownloading = false;
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
downloads: state.downloads.map(d => {
|
downloads: state.downloads.map(d => {
|
||||||
if (d.id === id) {
|
if (d.id === id) {
|
||||||
|
if (d.status === 'downloading') {
|
||||||
|
wasDownloading = true;
|
||||||
|
}
|
||||||
const updated: DownloadItem = { ...d, status: 'queued', _dispatched: false, fraction: 0, speed: '-', eta: '-' };
|
const updated: DownloadItem = { ...d, status: 'queued', _dispatched: false, fraction: 0, speed: '-', eta: '-' };
|
||||||
updatedItem = updated;
|
updatedItem = updated;
|
||||||
return updated;
|
return updated;
|
||||||
@@ -201,6 +207,9 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
return d;
|
return d;
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
if (wasDownloading) {
|
||||||
|
invoke('pause_download', { id }).catch(console.error);
|
||||||
|
}
|
||||||
if (updatedItem) {
|
if (updatedItem) {
|
||||||
const toSave = { ...(updatedItem as DownloadItem) };
|
const toSave = { ...(updatedItem as DownloadItem) };
|
||||||
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
|
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ const MEDIA_DOMAINS = [
|
|||||||
'instagram.com',
|
'instagram.com',
|
||||||
'tiktok.com',
|
'tiktok.com',
|
||||||
'reddit.com',
|
'reddit.com',
|
||||||
|
'v.redd.it',
|
||||||
'soundcloud.com',
|
'soundcloud.com',
|
||||||
'facebook.com'
|
'facebook.com',
|
||||||
|
'fb.watch'
|
||||||
];
|
];
|
||||||
|
|
||||||
export const categoryForFileName = (fileName: string): DownloadCategory => {
|
export const categoryForFileName = (fileName: string): DownloadCategory => {
|
||||||
|
|||||||
Reference in New Issue
Block a user