use crate::net::HttpClient;
use crate::stealth::StealthProfile;
use deno_core::error::ModuleLoaderError;
use deno_core::{
resolve_import, ModuleLoadOptions, ModuleLoadReferrer, ModuleLoadResponse, ModuleLoader,
ModuleSource, ModuleSourceCode, ModuleSpecifier, ModuleType, ResolutionKind,
};
fn percent_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 3 <= bytes.len() {
if let Ok(b) = u8::from_str_radix(&s[i + 1..i + 3], 16) {
out.push(b);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
pub struct BrowserModuleLoader {
profile: StealthProfile,
client: Option<HttpClient>,
}
impl BrowserModuleLoader {
pub fn new(profile: StealthProfile) -> Self {
let client = HttpClient::shared(&profile).ok();
Self { profile, client }
}
}
impl ModuleLoader for BrowserModuleLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_kind: ResolutionKind,
) -> Result<ModuleSpecifier, ModuleLoaderError> {
resolve_import(specifier, referrer).map_err(|e| ModuleLoaderError::generic(e.to_string()))
}
fn load(
&self,
module_specifier: &ModuleSpecifier,
maybe_referrer: Option<&ModuleLoadReferrer>,
_options: ModuleLoadOptions,
) -> ModuleLoadResponse {
let spec = module_specifier.clone();
let url = module_specifier.to_string();
let profile = self.profile.clone();
let client = self.client.clone();
let referer = maybe_referrer
.map(|r| r.specifier.to_string())
.unwrap_or_else(|| url.clone());
if let Some(rest) = url.strip_prefix("data:") {
use base64::Engine as _;
let (meta, payload) = rest.split_once(',').unwrap_or(("", rest));
let code = if meta.contains(";base64") {
base64::engine::general_purpose::STANDARD
.decode(payload.trim())
.ok()
.map(|b| String::from_utf8_lossy(&b).into_owned())
.unwrap_or_default()
} else {
percent_decode(payload)
};
return ModuleLoadResponse::Sync(Ok(ModuleSource::new(
ModuleType::JavaScript,
ModuleSourceCode::String(code.into()),
&spec,
None,
)));
}
if !(url.starts_with("http://") || url.starts_with("https://")) {
return ModuleLoadResponse::Sync(Ok(ModuleSource::new(
ModuleType::JavaScript,
ModuleSourceCode::String(String::new().into()),
&spec,
None,
)));
}
let fut = async move {
let client = client.ok_or_else(|| {
ModuleLoaderError::generic("module loader: shared HTTP client unavailable")
})?;
let mut hdrs = crate::net::headers::nav_headers_for_url(&profile, &referer, false);
hdrs.push(("referer".to_string(), referer));
hdrs.push(("accept".to_string(), "*/*".to_string()));
hdrs.push(("sec-fetch-dest".to_string(), "script".to_string()));
hdrs.push(("sec-fetch-mode".to_string(), "cors".to_string()));
let resp = client
.get_follow_with_headers(&url, &hdrs, 5)
.await
.map_err(|e| ModuleLoaderError::generic(format!("module fetch {url}: {e}")))?;
if !resp.ok() {
return Err(ModuleLoaderError::generic(format!(
"module fetch {url} -> status {}",
resp.status
)));
}
let code = resp.text();
Ok(ModuleSource::new(
ModuleType::JavaScript,
ModuleSourceCode::String(code.into()),
&spec,
None,
))
};
ModuleLoadResponse::Async(Box::pin(fut))
}
}