Skip to main content

ssh_browser/origin/
pac.rs

1//! The PAC script that makes `http://<alias>.<suffix>/` reach this daemon.
2//!
3//! A PAC routes by hostname and never resolves it, so the suffix does not have to
4//! be a real TLD and no DNS server or hosts-file entry is needed. That is why
5//! this works where mkcert-style setups reach for dnsmasq. It also leaves the
6//! address bar alone, unlike a declarativeNetRequest redirect, which rewrites the
7//! URL to 127.0.0.1 and loses the origin the user asked for.
8
9use anyhow::{Result, ensure};
10
11/// Build the script. The suffix is validated rather than escaped: it lands inside
12/// a JavaScript string literal, and a label is the only shape that cannot break
13/// out of one.
14pub fn script(suffix: &str, port: u16) -> Result<String> {
15    ensure!(
16        is_suffix(suffix),
17        "suffix {suffix:?} must be lowercase letters, digits, hyphens and dots"
18    );
19    Ok(format!(
20        "function FindProxyForURL(url, host) {{\n  \
21         if (dnsDomainIs(host, \".{suffix}\") || host === \"{suffix}\") {{\n    \
22         return \"PROXY 127.0.0.1:{port}\";\n  \
23         }}\n  \
24         return \"DIRECT\";\n\
25         }}\n"
26    ))
27}
28
29/// The shape a suffix has to have.
30///
31/// Crate-visible so that `Origin::bind` holds a suffix to the same rule the PAC does.
32/// Validating it in only one of the two places meant `serve --suffix ""` started
33/// happily and then served a PAC route nothing could ever match.
34pub(crate) fn is_suffix(s: &str) -> bool {
35    !s.is_empty()
36        && !s.starts_with(['-', '.'])
37        && !s.ends_with(['-', '.'])
38        && s.bytes()
39            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'.')
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn routes_the_suffix_and_nothing_else() {
48        let s = script("ssh-browser", 7391).unwrap();
49        assert!(s.contains("dnsDomainIs(host, \".ssh-browser\")"));
50        assert!(s.contains("PROXY 127.0.0.1:7391"));
51        assert!(s.contains("return \"DIRECT\""));
52    }
53
54    /// The suffix is configurable, so it is attacker-adjacent input as far as the
55    /// generated script is concerned.
56    #[test]
57    fn a_suffix_that_could_break_out_of_the_string_is_refused() {
58        assert!(script("a\" + evil + \"b", 7391).is_err());
59        assert!(script("a\nb", 7391).is_err());
60        assert!(script("", 7391).is_err());
61        assert!(script("UPPER", 7391).is_err());
62    }
63
64    #[test]
65    fn a_custom_suffix_works() {
66        let s = script("internal.example", 9000).unwrap();
67        assert!(s.contains("dnsDomainIs(host, \".internal.example\")"));
68        assert!(s.contains("PROXY 127.0.0.1:9000"));
69    }
70}