fix: proxy all images through reqwest to bypass WebKit TLS (#163)

When image cache is disabled or unavailable, images were returned
as original HTTPS URLs for WebKit to load directly. On AppImage
distros where GnuTLS can't find system CA certs, this fails silently.

Now all images go through reqwest (rustls, bundled CA) and are
written to temp files as file:// URLs. WebKit never needs to
resolve HTTPS for images.

cc
This commit is contained in:
victorrhgap committed 2026-03-24 20:35:32 -06:00
1 parent e6ea2b483c
commit 39e5867a7d
2 files changed
+67 -17

No files matched your search

+64 -16
View File
@@ -13204,6 +13204,47 @@ pub async fn v2_fetch_url_bytes(url: String) -> Result<Vec<u8>, String> {
// ============ Image Cache Commands ============ // ============ Image Cache Commands ============
/// Download an image via reqwest (rustls) and write to a temp file.
/// Returns a file:// URL that WebKit can load without needing system TLS.
/// Used as fallback when the image cache service is unavailable.
async fn download_image_to_temp(url: &str) -> Result<String, String> {
let url_owned = url.to_string();
let bytes = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, String> {
let response = reqwest::blocking::Client::new()
.get(&url_owned)
.timeout(std::time::Duration::from_secs(10))
.send()
.map_err(|e| format!("Failed to download image: {}", e))?;
if !response.status().is_success() {
return Err(format!("HTTP {}", response.status()));
}
response
.bytes()
.map(|b| b.to_vec())
.map_err(|e| format!("Failed to read image bytes: {}", e))
})
.await
.map_err(|e| format!("Task join error: {}", e))??;
// Write to temp dir with a hash-based filename to avoid duplicates
let hash = {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
url.hash(&mut hasher);
hasher.finish()
};
let tmp_dir = std::env::temp_dir().join("qbz-img-proxy");
std::fs::create_dir_all(&tmp_dir)
.map_err(|e| format!("Failed to create temp dir: {}", e))?;
let tmp_path = tmp_dir.join(format!("{:x}.img", hash));
std::fs::write(&tmp_path, &bytes)
.map_err(|e| format!("Failed to write temp image: {}", e))?;
Ok(format!("file://{}", tmp_path.display()))
}
#[tauri::command] #[tauri::command]
pub async fn v2_get_cached_image( pub async fn v2_get_cached_image(
url: String, url: String,
@@ -13223,8 +13264,9 @@ pub async fn v2_get_cached_image(
}; };
if !settings.enabled { if !settings.enabled {
// Cache disabled — return original URL // Cache disabled — still proxy through reqwest so WebKit never
return Ok(url); // needs to resolve HTTPS (fixes AppImage TLS on some distros)
return download_image_to_temp(&url).await;
} }
// Check cache first // Check cache first
@@ -13240,7 +13282,7 @@ pub async fn v2_get_cached_image(
} }
} }
// Download the image (in blocking context since reqwest::blocking) // Download the image via reqwest (uses rustls — own CA bundle)
let url_clone = url.clone(); let url_clone = url.clone();
let bytes = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, String> { let bytes = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, String> {
let response = reqwest::blocking::Client::new() let response = reqwest::blocking::Client::new()
@@ -13262,19 +13304,25 @@ pub async fn v2_get_cached_image(
.map_err(|e| format!("Task join error: {}", e))??; .map_err(|e| format!("Task join error: {}", e))??;
// Store in cache and evict if needed // Store in cache and evict if needed
let max_bytes = (settings.max_size_mb as u64) * 1024 * 1024; let store_result = {
let lock = cache_state let max_bytes = (settings.max_size_mb as u64) * 1024 * 1024;
.service let lock = cache_state
.lock() .service
.map_err(|e| format!("Cache lock error: {}", e))?; .lock()
if let Some(service) = lock.as_ref() { .map_err(|e| format!("Cache lock error: {}", e))?;
let path = service.store(&url, &bytes)?; if let Some(service) = lock.as_ref() {
// Evict LRU entries if over limit let path = service.store(&url, &bytes)?;
let _ = service.evict(max_bytes); let _ = service.evict(max_bytes);
Ok(format!("file://{}", path.display())) Some(format!("file://{}", path.display()))
} else { } else {
// Service not initialized, return original URL None
Ok(url) }
}; // lock dropped here, before any .await
match store_result {
Some(path) => Ok(path),
// Service not initialized — use temp file fallback
None => download_image_to_temp(&url).await,
} }
} }
+3 -1
View File
@@ -49,7 +49,9 @@ export async function getCachedImageUrl(url: string): Promise<string> {
resolvedUrls.set(url, resolved); resolvedUrls.set(url, resolved);
return resolved; return resolved;
} catch { } catch {
// On failure, use original URL // Backend failed to proxy — use original URL as last resort.
// This may fail on AppImage distros with broken GnuTLS but
// is better than showing nothing on distros where WebKit works.
resolvedUrls.set(url, url); resolvedUrls.set(url, url);
return url; return url;
} finally { } finally {