feat: support file:// URL scheme for local HTML testing

Allow loading local HTML/CSS/JS files via the file:// protocol,
enabling Obscura to be used for testing static sites without
running a local HTTP server.

Changes:
- Allow "file" scheme in validate_url() and validate_fetch_url()
- Add fetch_file_url() to read local files and return synthetic
  HTTP responses with appropriate content-type headers
- Skip private IP / localhost checks for file:// URLs

Closes #16
This commit is contained in:
ben1009 committed 2026-04-25 00:18:31 +08:00
1 parent b00a1f20e7
commit 21136ca819
2 files changed
+51 -4

No files matched your search

+45 -2
View File
@@ -66,13 +66,17 @@ pub type ResponseCallback = Arc<dyn Fn(&RequestInfo, &Response) + Send + Sync>;
fn validate_url(url: &Url) -> Result<(), ObscuraNetError> {
let scheme = url.scheme();
if scheme != "http" && scheme != "https" {
if scheme != "http" && scheme != "https" && scheme != "file" {
return Err(ObscuraNetError::Network(format!(
"Forbidden URL scheme '{}' - only http and https are allowed",
"Forbidden URL scheme '{}' - only http, https, and file are allowed",
scheme
)));
}
if scheme == "file" {
return Ok(());
}
if let Some(host) = url.host() {
match host {
url::Host::Ipv4(ip) => {
@@ -115,6 +119,41 @@ fn validate_url(url: &Url) -> Result<(), ObscuraNetError> {
Ok(())
}
async fn fetch_file_url(url: &Url) -> Result<Response, ObscuraNetError> {
let path = url
.to_file_path()
.map_err(|_| ObscuraNetError::Network("Invalid file URL".to_string()))?;
let body = tokio::fs::read(&path)
.await
.map_err(|e| ObscuraNetError::Network(format!("Failed to read file: {}", e)))?;
let mut headers = HashMap::new();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
let ct = match ext.to_lowercase().as_str() {
"html" | "htm" => "text/html",
"css" => "text/css",
"js" | "mjs" => "application/javascript",
"json" => "application/json",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"svg" => "image/svg+xml",
"webp" => "image/webp",
"ico" => "image/x-icon",
_ => "application/octet-stream",
};
headers.insert("content-type".to_string(), ct.to_string());
}
Ok(Response {
url: url.clone(),
status: 200,
headers,
body,
redirected_from: Vec::new(),
})
}
pub struct ObscuraHttpClient {
client: tokio::sync::OnceCell<Client>,
proxy_url: Option<String>,
@@ -190,6 +229,10 @@ impl ObscuraHttpClient {
) -> Result<Response, ObscuraNetError> {
validate_url(url)?;
if url.scheme() == "file" {
return fetch_file_url(url).await;
}
let mut method = initial_method;
let mut body = initial_body;
if self.block_trackers {