pub const PAC_PATH: &str = "/proxy.pac";
use crate::proxy::hostname::MAX_HOSTNAME_LEN as MAX_HOSTNAME;
const MAX_LABEL: usize = 63;
pub fn is_valid_tld(tld: &str) -> bool {
if tld.is_empty() || tld.len() > MAX_HOSTNAME - 2 {
return false;
}
tld.split('.').all(|label| {
!label.is_empty()
&& label.len() <= MAX_LABEL
&& !label.starts_with('-')
&& !label.ends_with('-')
&& label
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-')
})
}
pub fn hostname_fits(label: &str, tld: &str) -> bool {
label.len() <= MAX_LABEL && label.len() + 1 + tld.len() <= MAX_HOSTNAME
}
pub fn generate(tld: &str, host: &str, port: u16) -> crate::Result<String> {
if !is_valid_tld(tld) {
miette::bail!(
"proxy.tld {tld:?} is not a valid host name suffix, so no PAC file can be generated"
);
}
let tld = tld.to_ascii_lowercase();
Ok(format!(
"// Generated by pitchfork. Routes *.{tld} through the local proxy.\n\
function FindProxyForURL(url, host) {{\n\
\x20 host = host.toLowerCase();\n\
\x20 if (host === \"{tld}\" || dnsDomainIs(host, \".{tld}\")) {{\n\
\x20 return \"PROXY {host_}:{port}\";\n\
\x20 }}\n\
\x20 return \"DIRECT\";\n\
}}\n",
host_ = host,
))
}
pub fn url(host: &str, port: u16) -> String {
format!("http://{host}:{port}{PAC_PATH}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generates_a_script_that_routes_the_tld_and_nothing_else() {
let pac = generate("localhost", "127.0.0.1", 8443).unwrap();
assert!(pac.contains("function FindProxyForURL(url, host)"));
assert!(pac.contains(r#"host === "localhost""#));
assert!(pac.contains(r#"dnsDomainIs(host, ".localhost")"#));
assert!(pac.contains(r#"return "PROXY 127.0.0.1:8443";"#));
assert!(pac.contains(r#"return "DIRECT";"#));
}
#[test]
fn tld_is_lowercased_and_host_matching_is_case_insensitive() {
let pac = generate("TEST", "127.0.0.1", 80).unwrap();
assert!(pac.contains(r#"dnsDomainIs(host, ".test")"#));
assert!(pac.contains("host = host.toLowerCase();"));
}
#[test]
fn multi_label_tld_is_accepted() {
let pac = generate("dev.internal", "127.0.0.1", 443).unwrap();
assert!(pac.contains(r#"dnsDomainIs(host, ".dev.internal")"#));
}
#[test]
fn invalid_tlds_are_rejected_rather_than_escaped() {
for bad in [
"", "a\"b", "a b", "a;b", ".test", "test.", "a\nb", "a..b", "a/b",
] {
assert!(
generate(bad, "127.0.0.1", 443).is_err(),
"expected {bad:?} to be rejected"
);
}
}
#[test]
fn labels_must_be_real_dns_labels() {
for bad in [
"-test",
"test-",
"a.-b",
"a.b-",
&"x".repeat(64),
&format!("a.{}", "x".repeat(64)),
&"a".repeat(252),
] {
assert!(!is_valid_tld(bad), "expected {bad:?} to be rejected");
}
for good in ["localhost", "test", "dev.internal", "my-tld", "a-b.c-d"] {
assert!(is_valid_tld(good), "expected {good:?} to be allowed");
}
assert!(is_valid_tld(&"x".repeat(63)));
}
#[test]
fn a_name_longer_than_a_hostname_does_not_fit() {
assert!(hostname_fits("api", "localhost"));
assert!(hostname_fits("a", &"x".repeat(251)));
assert!(!hostname_fits("ab", &"x".repeat(251)));
}
#[test]
fn a_label_over_63_bytes_does_not_fit_even_in_a_short_name() {
assert!(hostname_fits(&"a".repeat(63), "localhost"));
assert!(!hostname_fits(&"a".repeat(64), "localhost"));
}
#[test]
fn url_points_at_the_proxy_listener() {
assert_eq!(url("127.0.0.1", 8443), "http://127.0.0.1:8443/proxy.pac");
}
}