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:
2 files changed
+67
-17
No files matched your search
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user