use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::LazyLock;
use std::time::Duration;
use ipnet::IpNet;
use url::Host;
pub(crate) mod actions;
pub(crate) mod cli;
pub(crate) mod contract;
pub(crate) mod forms;
pub(crate) mod spawn;
pub(crate) const CLI_SESSION_PREFIX: &str = "mahbot-chrome-";
pub(crate) const DEADLINE_SLACK: Duration = Duration::from_secs(2);
pub(crate) const DEFAULT_OPEN_TIMEOUT: Duration = Duration::from_secs(20);
pub(crate) const CLI_EPHEMERAL_PREFIX: &str = "mahbot-chrome-ephemeral-";
static DENIED_IP_NETS: LazyLock<[IpNet; 4]> = LazyLock::new(|| {
[
IpNet::new(IpAddr::V4(Ipv4Addr::new(169, 254, 0, 0)), 16)
.expect("169.254.0.0/16 is a valid IPv4 net"),
IpNet::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8).expect("0.0.0.0/8 is a valid IPv4 net"),
IpNet::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 128).expect("::1/128 is a valid IPv6 net"),
IpNet::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0, 0)), 96)
.expect("::ffff:0:0/96 is a valid IPv6 net"),
]
});
pub(crate) fn validate_url(url: &str) -> anyhow::Result<()> {
let url = url.trim();
if url.is_empty() {
anyhow::bail!("URL cannot be empty");
}
if url.to_ascii_lowercase().starts_with("file://") {
anyhow::bail!("file:// URLs are not allowed in chrome automation");
}
let parsed = url::Url::parse(url)
.map_err(|_| anyhow::anyhow!("Only http:// and https:// URLs are allowed"))?;
if !matches!(parsed.scheme(), "http" | "https") {
anyhow::bail!("Only http:// and https:// URLs are allowed");
}
let Some(host) = parsed.host() else {
return Ok(());
};
let ip = match host {
Host::Domain(domain) => {
if domain.eq_ignore_ascii_case("metadata.google.internal") {
anyhow::bail!(
"Refused to navigate to {url}: host {host} is a denied link-local/metadata address (SSRF guard)"
);
}
None
}
Host::Ipv4(addr) => Some(IpAddr::V4(addr)),
Host::Ipv6(addr) => Some(IpAddr::V6(addr)),
};
if ip.is_some_and(|ip| DENIED_IP_NETS.iter().any(|net| net.contains(&ip))) {
anyhow::bail!(
"Refused to navigate to {url}: host {host} is a denied link-local/metadata address (SSRF guard)"
);
}
Ok(())
}
pub(crate) fn is_blank_page_url(url: &str) -> bool {
let url = url.trim();
url.is_empty() || url.starts_with("about:blank")
}
pub(crate) fn is_chrome_error_page(url: &str) -> bool {
url.trim().starts_with("chrome-error://")
}
#[must_use]
pub(crate) fn escape_js_single_quoted(s: &str) -> String {
s.replace('\\', "\\\\").replace('\'', "\\'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn url_validation_accepts_real_and_local_targets() {
for url in [
"https://example.com",
"HTTP://EXAMPLE.COM",
"https://EXAMPLE.com/path",
"http://localhost:3000",
"http://127.0.0.1",
"http://192.168.1.1",
"http://10.0.0.5",
"http://0x7f000001/",
] {
assert!(validate_url(url).is_ok(), "expected {url:?} to be accepted");
}
}
#[test]
fn url_validation_rejects_unsafe_targets() {
let err = validate_url("").unwrap_err();
assert!(err.to_string().contains("URL cannot be empty"));
for url in ["ftp://example.com", "example.com"] {
let err = validate_url(url).unwrap_err();
assert!(
err.to_string()
.contains("Only http:// and https:// URLs are allowed"),
"unexpected error for {url:?}: {err}"
);
}
for url in ["FILE:///etc/passwd", "file:///etc/passwd"] {
let err = validate_url(url).unwrap_err();
assert!(
err.to_string().contains("not allowed"),
"unexpected error for {url:?}: {err}"
);
}
for url in [
"http://169.254.169.254/",
"http://0.0.0.0/",
"http://0.1.2.3/",
"http://[::1]/",
"http://[::ffff:169.254.169.254]/",
"http://metadata.google.internal/computeMetadata/v1/",
] {
let err = validate_url(url).unwrap_err();
assert!(
err.to_string().contains("SSRF guard"),
"unexpected error for {url:?}: {err}"
);
}
}
#[test]
fn blank_page_url_predicate() {
assert!(is_blank_page_url("about:blank"));
assert!(is_blank_page_url("about:blank#blocked"));
assert!(is_blank_page_url(""));
assert!(!is_blank_page_url("https://example.com/image.png"));
assert!(!is_blank_page_url("https://example.com/file.pdf"));
assert!(!is_blank_page_url("about:srcdoc"));
}
#[test]
fn chrome_error_page_predicate() {
assert!(is_chrome_error_page("chrome-error://chromewebdata/"));
assert!(!is_chrome_error_page("https://example.com/"));
}
}