fix: harden media handoff and live logs

Reject stale extension media cookie headers before yt-dlp metadata work, preserve ordinary capture cookies, and advance the companion extension.

Stream redacted diagnostic logs only while the visible Logs view is active, with bounded batched updates and race-safe snapshot handoff.
This commit is contained in:
NimBold
2026-07-10 19:02:39 +03:30
parent 4f4c655de6
commit 248f3869ad
13 changed files with 348 additions and 114 deletions
+30 -8
View File
@@ -315,10 +315,15 @@ fn normalize_download(payload: ExtensionRequest) -> Option<ExtensionDownload> {
silent: payload.silent,
filename,
headers: payload.headers.filter(|value| !value.trim().is_empty()),
// Keep the exact tab/container cookie context. The Add modal may retry
// public media without this header when an upstream rejects its size,
// but authenticated and container-scoped media must not lose it here.
cookies: payload.cookies.filter(|value| !value.trim().is_empty()),
// Explicit media is resolved by yt-dlp, which must use Firelink's
// configured browser-cookie source. Forwarding a browser's complete
// Cookie header can exceed upstream limits and makes old extension
// builds pay for a doomed metadata request before retrying. Ordinary
// captured downloads still need their exact request cookies.
cookies: (!payload.media)
.then_some(payload.cookies)
.flatten()
.filter(|value| !value.trim().is_empty()),
media: payload.media,
})
}
@@ -496,24 +501,41 @@ mod tests {
}
#[test]
fn explicit_media_preserves_the_extension_cookie_header() {
fn explicit_media_drops_the_extension_cookie_header() {
let download = normalize_download(ExtensionRequest {
urls: vec!["https://www.youtube.com/watch?v=example".to_string()],
referer: None,
silent: false,
filename: None,
headers: Some("User-Agent: Firefox".to_string()),
cookies: Some("large=browser-cookie-header".to_string()),
cookies: Some(format!("large={}", "x".repeat(64 * 1024))),
media: true,
})
.expect("valid media handoff");
assert!(download.media);
assert!(download.cookies.is_none());
assert_eq!(download.headers.as_deref(), Some("User-Agent: Firefox"));
}
#[test]
fn regular_capture_preserves_the_extension_cookie_header() {
let download = normalize_download(ExtensionRequest {
urls: vec!["https://example.com/private.zip".to_string()],
referer: None,
silent: true,
filename: None,
headers: None,
cookies: Some("session=browser-cookie-header".to_string()),
media: false,
})
.expect("valid download handoff");
assert!(!download.media);
assert_eq!(
download.cookies.as_deref(),
Some("large=browser-cookie-header")
Some("session=browser-cookie-header")
);
assert_eq!(download.headers.as_deref(), Some("User-Agent: Firefox"));
}
#[test]
+57 -27
View File
@@ -4542,6 +4542,27 @@ fn redact_log_line(line: &str) -> String {
query.replace_all(&redacted, "$1?[redacted]").into_owned()
}
fn redact_log_line_for_output(line: &str) -> String {
static HOME_PATHS: OnceLock<(String, String)> = OnceLock::new();
let (home, escaped_home) = HOME_PATHS.get_or_init(|| {
let home = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
.unwrap_or_default();
let home = if home.len() > 3 { home } else { String::new() };
let escaped_home = home.replace('\\', "\\\\");
(home, escaped_home)
});
let mut redacted = line.to_string();
if !home.is_empty() {
redacted = redacted.replace(home, "<HOME>");
}
if !escaped_home.is_empty() && escaped_home != home {
redacted = redacted.replace(escaped_home, "<HOME>");
}
redact_log_line(&redacted)
}
fn redact_log_line_for_app(line: &str, app_handle: &tauri::AppHandle) -> String {
use tauri::Manager;
let without_home = app_handle
@@ -4780,8 +4801,9 @@ mod tests {
media_output_template, media_progress_args, media_progress_speed,
normalize_speed_limit_for_aria2,
parse_firelink_deep_link, parse_ffmpeg_version, parse_media_progress_line,
redact_log_line, sanitize_ytdlp_config_value, should_cleanup_media_artifacts_after_failure,
FirelinkDeepLink, MediaProgress, MediaSpeedSampler, MEDIA_PROGRESS_PREFIX,
redact_log_line, redact_log_line_for_output, sanitize_ytdlp_config_value,
should_cleanup_media_artifacts_after_failure, FirelinkDeepLink, MediaProgress,
MediaSpeedSampler, MEDIA_PROGRESS_PREFIX,
};
use serde_json::json;
use std::time::{Duration, Instant};
@@ -4979,6 +5001,15 @@ mod tests {
assert!(redacted.contains("[redacted]"));
}
#[test]
fn redacts_live_log_output_before_webview_delivery() {
let line = "Cookie: session=abc https://example.com/file?signature=secret";
let redacted = redact_log_line_for_output(line);
assert!(!redacted.contains("session=abc"));
assert!(!redacted.contains("signature=secret"));
assert!(redacted.contains("[redacted]"));
}
#[test]
fn collects_primary_url_and_unique_mirrors_in_order() {
let uris = collect_download_uris(
@@ -5459,6 +5490,7 @@ mod tests {
}
static LOG_PAUSED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
static LOG_STREAM_ACTIVE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[tauri::command]
fn toggle_log_pause(pause: bool) {
@@ -5470,6 +5502,11 @@ fn is_log_paused() -> bool {
LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed)
}
#[tauri::command]
fn set_log_stream_active(active: bool) {
LOG_STREAM_ACTIVE.store(active, std::sync::atomic::Ordering::Release);
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
ensure_reqwest_crypto_provider();
@@ -5881,7 +5918,13 @@ pub fn run() {
tauri_plugin_log::Builder::new()
.targets([
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir {
file_name: None,
}),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview)
.filter(|_| {
LOG_STREAM_ACTIVE.load(std::sync::atomic::Ordering::Acquire)
}),
])
.level(if cfg!(debug_assertions) { log::LevelFilter::Debug } else { log::LevelFilter::Info })
.filter(|metadata| {
@@ -5899,29 +5942,15 @@ pub fn run() {
.max_file_size(10_000_000)
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepSome(3))
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
.format({
let home_dir = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();
let home_dir = if home_dir.len() > 3 { home_dir } else { String::new() };
let escaped_home_dir = home_dir.replace("\\", "\\\\");
move |out, message, record| {
let msg = message.to_string();
let redacted = if !home_dir.is_empty() {
let mut s = msg.replace(&home_dir, "<HOME>");
if !escaped_home_dir.is_empty() && escaped_home_dir != home_dir {
s = s.replace(&escaped_home_dir, "<HOME>");
}
s
} else {
msg
};
out.finish(format_args!(
"[{}][{}][{}] {}",
chrono::Local::now().format("%Y-%m-%d][%H:%M:%S"),
record.level(),
record.target(),
redacted
))
}
.format(|out, message, record| {
let redacted = redact_log_line_for_output(&message.to_string());
out.finish(format_args!(
"[{}][{}][{}] {}",
chrono::Local::now().format("%Y-%m-%d][%H:%M:%S"),
record.level(),
record.target(),
redacted
))
})
.build(),
)
@@ -5955,7 +5984,8 @@ pub fn run() {
parity::create_category_directories,
db_save_settings, db_load_settings, db_get_all_downloads, db_replace_downloads,
db_get_all_queues, db_replace_queues,
read_logs, export_logs, toggle_log_pause, is_log_paused, clear_logs
read_logs, export_logs, toggle_log_pause, is_log_paused, clear_logs,
set_log_stream_active
])
.build(tauri::generate_context!())
.expect("error while building tauri application")