use std::sync::Arc; use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use obscura_dom::{parse_html, DomTree}; use obscura_js::runtime::ObscuraJsRuntime; use obscura_net::{ObscuraHttpClient, ObscuraNetError, Response}; use url::Url; use crate::context::BrowserContext; use crate::lifecycle::LifecycleState; fn decode_data_uri(uri: &str) -> Option> { let rest = uri.strip_prefix("data:")?; let comma = rest.find(',')?; let meta = &rest[..comma]; let payload = &rest[comma + 1..]; if meta.split(';').any(|t| t.eq_ignore_ascii_case("base64")) { let cleaned: String = payload.chars().filter(|c| !c.is_whitespace()).collect(); BASE64.decode(cleaned).ok() } else { Some(percent_decode(payload)) } } fn percent_decode(s: &str) -> Vec { let mut out = Vec::with_capacity(s.len()); let b = s.as_bytes(); let mut i = 0; while i < b.len() { if b[i] == b'%' && i + 2 < b.len() { let hi = hex_val(b[i + 1]); let lo = hex_val(b[i + 2]); if let (Some(h), Some(l)) = (hi, lo) { out.push((h << 4) | l); i += 3; continue; } } out.push(b[i]); i += 1; } out } fn hex_val(b: u8) -> Option { match b { b'0'..=b'9' => Some(b - b'0'), b'a'..=b'f' => Some(b - b'a' + 10), b'A'..=b'F' => Some(b - b'A' + 10), _ => None, } } #[cfg(feature = "stealth")] use obscura_net::StealthHttpClient; /// Returns true when a JS-initiated navigation would step from a /// non-file scheme into a file: URL. We treat that move as an SOP /// violation because the existing realm survives the navigation and /// can read the new document's body. fn cross_scheme_to_file(from: &str, to: &str) -> bool { let to_is_file = Url::parse(to) .map(|u| u.scheme().eq_ignore_ascii_case("file")) .unwrap_or(false); if !to_is_file { return false; } Url::parse(from) .map(|u| !u.scheme().eq_ignore_ascii_case("file")) .unwrap_or(true) } /// Sub-resource fetch policy. http(s) is always fine; data: is allowed /// because the bytes are inline in the URI (no network fetch, no SSRF); /// file: is only allowed when the page itself was loaded from file:; /// everything else (javascript:, chrome:, etc) is blocked. /// Real Chrome allows data: subresources by default; Instagram and most /// Meta properties depend on this for their inline bootstrap scripts. fn subresource_allowed(page_url: Option<&Url>, resource: &str) -> bool { let Ok(target) = Url::parse(resource) else { return false }; let scheme = target.scheme().to_ascii_lowercase(); match scheme.as_str() { "http" | "https" | "data" => true, "file" => page_url.map(|u| u.scheme().eq_ignore_ascii_case("file")).unwrap_or(false), _ => false, } } /// Escape a value for safe inclusion inside a JavaScript template /// literal. The previous implementation only escaped `\`, `` ` `` and /// `${`; that left U+2028 / U+2029 (the JS-specific line terminators) /// and other control characters as breakout vectors. Done at the /// callsite means future tweaks come back to one function. fn escape_for_js_template_literal(input: &str) -> String { let mut out = String::with_capacity(input.len()); for ch in input.chars() { match ch { '\\' => out.push_str("\\\\"), '`' => out.push_str("\\`"), '$' => out.push_str("\\$"), '\u{2028}' => out.push_str("\\u2028"), '\u{2029}' => out.push_str("\\u2029"), '\u{0000}' => out.push_str("\\0"), '\r' => out.push_str("\\r"), c if (c as u32) < 0x20 => { out.push_str(&format!("\\u{:04x}", c as u32)); } c => out.push(c), } } out } #[derive(Debug, Clone)] pub struct NetworkEvent { pub request_id: String, pub url: String, pub method: String, pub resource_type: String, pub status: u16, pub headers: std::collections::HashMap, pub response_headers: Arc>, pub body_size: usize, pub timestamp: f64, } pub struct Page { pub id: String, pub frame_id: String, pub url: Option, pub dom: Option, pub js: Option, pub lifecycle: LifecycleState, pub http_client: Arc, pub context: Arc, pub title: String, /// WHATWG canonical name of the current document's character encoding /// (e.g. "UTF-8", "EUC-JP"), detected when the response body is decoded. /// Exposed to JS as `document.characterSet` and used for the URL query /// encoding override on ``/`` hrefs in legacy-charset documents. pub encoding: String, /// Navigation history for Page.getNavigationHistory / navigateToHistoryEntry. /// Entries are URLs in visit order; `history_index` is the current position. /// Pushed on every successful navigation; truncated on goBack -> new nav. pub history: Vec, pub history_index: usize, pub network_events: Vec, network_event_counter: u32, pub intercept_enabled: bool, pub intercept_block_patterns: Vec, intercept_tx: Option>, // Scripts to execute in the page's JS context BEFORE any of the page's // own scripts run — the CDP `Page.addScriptToEvaluateOnNewDocument` // contract. Includes `Runtime.addBinding` shims so puppeteer's // `exposeFunction` bindings exist before inline `