keyclaw 0.2.1

Local MITM proxy that keeps secrets out of LLM traffic
Documentation
#[derive(Debug, PartialEq, Eq)]
enum NoProxyEntry {
    Wildcard,
    Host {
        raw: String,
        host: String,
        is_suffix: bool,
        has_port: bool,
    },
}

impl NoProxyEntry {
    fn parse(raw: &str) -> Option<Self> {
        let raw = raw.trim();
        if raw.is_empty() {
            return None;
        }
        if raw == "*" {
            return Some(Self::Wildcard);
        }

        let (host, has_port) = split_host_port(raw);
        let host = host.to_lowercase();
        let is_suffix = host.starts_with('.');
        let host = host.trim_start_matches('.').to_string();
        if host.is_empty() {
            return None;
        }

        Some(Self::Host {
            raw: raw.to_string(),
            host,
            is_suffix,
            has_port,
        })
    }

    fn match_reason(&self, intercepted_host: &str) -> Option<String> {
        match self {
            Self::Wildcard => Some("NO_PROXY=*".to_string()),
            Self::Host {
                raw,
                host,
                is_suffix,
                has_port,
            } => {
                let exact = intercepted_host == host;
                let suffix = intercepted_host.ends_with(&format!(".{host}"));
                if !(exact || *is_suffix && suffix) {
                    return None;
                }

                if *is_suffix || *has_port {
                    Some(format!(
                        "NO_PROXY includes {raw} (matches {intercepted_host})"
                    ))
                } else {
                    Some(format!("NO_PROXY includes {raw}"))
                }
            }
        }
    }
}

pub(crate) fn launcher_bypass_risk(no_proxy: &str, hosts: &[String]) -> Option<String> {
    let entries = no_proxy
        .split(',')
        .filter_map(NoProxyEntry::parse)
        .collect::<Vec<_>>();

    for host in hosts
        .iter()
        .filter_map(|host| normalize_no_proxy_host(host))
    {
        for entry in &entries {
            if let Some(reason) = entry.match_reason(&host) {
                return Some(reason);
            }
        }
    }

    None
}

fn normalize_no_proxy_host(host: &str) -> Option<String> {
    let host = host.trim();
    if host.is_empty() {
        return None;
    }
    if host.contains('*') || host.contains('?') {
        return None;
    }
    Some(split_host_port(host).0.to_lowercase())
}

fn split_host_port(value: &str) -> (&str, bool) {
    let value = value.trim();
    if value.matches(':').count() == 1 {
        if let Some((host, port)) = value.rsplit_once(':') {
            if !host.is_empty() && port.chars().all(|ch| ch.is_ascii_digit()) {
                return (host, true);
            }
        }
    }
    (value, false)
}