openmoat-proxy 0.21.0

Default-deny egress proxy for OpenMoat: CONNECT with an SNI check, plain HTTP, its own DNS, an audit record per connection, and opt-in TLS termination for brokered secrets.
Documentation
use super::{RULE_REQUEST_RULE, Requests};

const HOST: &str = "api.github.com";

fn gists() -> Requests {
    Requests::default()
        .deny(HOST, &["POST /gists", "POST /gists/**"])
        .unwrap()
}

fn head(line: &str, extra: &str) -> Vec<u8> {
    format!("{line} HTTP/1.1\r\nHost: {HOST}\r\n{extra}Connection: close\r\n\r\n").into_bytes()
}

fn refused(r: &Requests, line: &str, extra: &str) -> String {
    let d = r.check(HOST, &head(line, extra)).unwrap_err();
    assert_eq!(d.rules, [RULE_REQUEST_RULE], "{line}");
    d.reasons.join("; ")
}

#[test]
fn a_denied_route_is_refused_in_every_spelling_and_names_no_path() {
    let r = gists();
    for line in [
        "POST /gists",
        "POST /gists/",
        "POST /gists?per_page=1",
        "POST /GISTS",
        "POST /Gists/",
        "post /gists",
        "Post /gists",
        "POST /gists/abc123/comments",
        "POST /gists/abc123/forks",
        "POST /%67ists",
    ] {
        let why = refused(&r, line, "");
        assert!(why.contains("request rule"), "{line}: {why}");
        assert!(
            !why.contains("abc123") && !why.contains("per_page"),
            "{why}"
        );
    }
}

#[test]
fn a_path_the_server_could_read_as_another_or_a_method_override_is_refused() {
    let r = gists();
    for (line, extra) in [
        ("POST /x/../gists", ""),
        ("POST /x/%2e%2e/gists", ""),
        ("POST /gists%2f", ""),
        ("POST /gists;x", ""),
        ("POST //gists", ""),
        ("GET /gists?_method=POST", ""),
        ("GET /gists", "X-HTTP-Method-Override: POST\r\n"),
    ] {
        refused(&r, line, extra);
    }
}

#[test]
fn everything_else_to_the_host_passes() {
    let r = gists();
    for line in [
        "GET /gists",
        "GET /gists/abc",
        "PATCH /gists/abc",
        "GET /zen",
        "POST /repos/o/r/issues",
        "POST /gistsx",
        "HEAD /",
    ] {
        let why = r.check(HOST, &head(line, "")).unwrap();
        assert!(why.is_some_and(|w| w.contains("no request rule")), "{line}");
    }
    assert!(r.names("API.GitHub.com"));
}

#[test]
fn another_host_is_not_checked() {
    let r = gists();
    assert!(!r.names("github.com"));
    assert_eq!(r.check("github.com", &head("POST /gists", "")), Ok(None));
    assert!(!Requests::default().names(HOST));
}

#[test]
fn an_unreadable_head_is_refused() {
    let d = gists().check(HOST, b"POST /gists").unwrap_err();
    assert_eq!(d.rules, [RULE_REQUEST_RULE]);
}

#[test]
fn bad_rules_and_hosts_are_refused() {
    for (host, rule) in [
        (HOST, "post /gists"),
        (HOST, "POST gists"),
        (HOST, "TRACE /"),
        ("*.github.com", "POST /gists"),
        ("", "POST /gists"),
        ("api.github.com:443", "POST /gists"),
    ] {
        assert!(
            Requests::default().deny(host, &[rule]).is_err(),
            "{host} {rule}"
        );
    }
    let hosts: Vec<_> = Requests::default()
        .deny("API.github.com", &["POST /Gists"])
        .unwrap()
        .hosts()
        .map(str::to_owned)
        .collect();
    assert_eq!(hosts, [HOST]);
}