use axum::http::{HeaderMap, header};
use std::net::{IpAddr, SocketAddr};
use crate::ws::origin_guard;
const PROXY_FORWARD_HEADERS: [&str; 4] =
["x-forwarded-for", "x-forwarded-host", "x-real-ip", "forwarded"];
pub fn is_local_request(headers: &HeaderMap, peer: Option<SocketAddr>) -> bool {
let Some(peer) = peer else { return false };
if !peer.ip().is_loopback() {
return false;
}
let Some(host) = origin_guard::host_from_headers(headers) else { return false };
if !is_loopback_host(host) {
return false;
}
if let Some(origin) = headers.get(header::ORIGIN)
&& !origin_guard::origin_host(origin).is_some_and(is_loopback_host)
{
return false;
}
if PROXY_FORWARD_HEADERS.iter().any(|name| headers.contains_key(*name)) {
return false;
}
true
}
pub fn is_loopback_host(host: &str) -> bool {
let host = host.trim();
if let Ok(ip) = host.parse::<IpAddr>() {
return ip.is_loopback();
}
let stripped = origin_guard::strip_port(host);
if stripped.trim_end_matches('.').eq_ignore_ascii_case("localhost") {
return true;
}
stripped.parse::<IpAddr>().map(|ip| ip.is_loopback()).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::HeaderValue;
fn headers(entries: &[(&str, &str)]) -> HeaderMap {
let mut h = HeaderMap::new();
for (k, v) in entries {
h.insert(k.parse::<header::HeaderName>().unwrap(), HeaderValue::from_str(v).unwrap());
}
h
}
fn peer(ip: &str) -> Option<SocketAddr> {
Some(SocketAddr::new(ip.parse().unwrap(), 40000))
}
fn loopback_headers() -> HeaderMap {
headers(&[("host", "127.0.0.1:18777")])
}
#[test]
fn all_true_conditions_pass() {
assert!(is_local_request(&loopback_headers(), peer("127.0.0.1")));
let h = headers(&[("host", "localhost:18777"), ("origin", "http://localhost:18777")]);
assert!(is_local_request(&h, peer("::1")));
}
#[test]
fn rejects_non_loopback_peer() {
assert!(!is_local_request(&loopback_headers(), peer("192.168.5.216")));
assert!(!is_local_request(&loopback_headers(), peer("10.0.0.7")));
}
#[test]
fn rejects_missing_peer() {
assert!(!is_local_request(&loopback_headers(), None));
}
#[test]
fn rejects_domain_host() {
let h = headers(&[("host", "omniterm.example.com")]);
assert!(!is_local_request(&h, peer("127.0.0.1")));
let h = headers(&[("host", "192.168.5.216:18778")]);
assert!(!is_local_request(&h, peer("127.0.0.1")));
}
#[test]
fn rejects_missing_host() {
assert!(!is_local_request(&HeaderMap::new(), peer("127.0.0.1")));
let mut h = HeaderMap::new();
h.insert(header::HOST, HeaderValue::from_bytes(b"\xff\xfe").unwrap());
assert!(!is_local_request(&h, peer("127.0.0.1")));
}
#[test]
fn rejects_malformed_host() {
let h = headers(&[("host", "http://127.0.0.1")]);
assert!(!is_local_request(&h, peer("127.0.0.1")));
let h = headers(&[("host", "example.com:127.0.0.1")]);
assert!(!is_local_request(&h, peer("127.0.0.1")));
}
#[test]
fn rejects_cross_site_origin() {
let h = headers(&[("host", "127.0.0.1:18777"), ("origin", "https://evil.com")]);
assert!(!is_local_request(&h, peer("127.0.0.1")));
let h = headers(&[("host", "127.0.0.1:18777"), ("origin", "http://192.168.5.216:18777")]);
assert!(!is_local_request(&h, peer("127.0.0.1")));
}
#[test]
fn rejects_null_or_malformed_origin() {
let h = headers(&[("host", "127.0.0.1:18777"), ("origin", "null")]);
assert!(!is_local_request(&h, peer("127.0.0.1")));
for bad in ["127.0.0.1:18777", "http://", "http://:18777"] {
let h = headers(&[("host", "127.0.0.1:18777"), ("origin", bad)]);
assert!(!is_local_request(&h, peer("127.0.0.1")), "origin={bad}");
}
let mut h = HeaderMap::new();
h.insert(header::HOST, HeaderValue::from_static("127.0.0.1:18777"));
h.insert(header::ORIGIN, HeaderValue::from_bytes(b"\xff\xfe").unwrap());
assert!(!is_local_request(&h, peer("127.0.0.1")));
}
#[test]
fn accepts_ipv6_origin_and_host() {
let h = headers(&[("host", "[::1]:18777"), ("origin", "http://[::1]:18777")]);
assert!(is_local_request(&h, peer("::1")));
let h = headers(&[("host", "0:0:0:0:0:0:0:1"), ("origin", "http://[0:0:0:0:0:0:0:1]")]);
assert!(is_local_request(&h, peer("::1")));
}
#[test]
fn rejects_proxy_forward_headers() {
for name in PROXY_FORWARD_HEADERS {
let mut h = loopback_headers();
h.insert(
name.parse::<header::HeaderName>().unwrap(),
HeaderValue::from_static("1.2.3.4"),
);
assert!(!is_local_request(&h, peer("127.0.0.1")), "header={name}");
}
}
#[test]
fn loopback_host_forms() {
for host in [
"localhost",
"LOCALHOST",
"LocalHost",
"localhost.",
"127.0.0.1",
"127.0.0.1:18777",
"127.0.0.2",
"::1",
"[::1]",
"[::1]:18777",
"0:0:0:0:0:0:0:1",
" 127.0.0.1 ", ] {
assert!(is_loopback_host(host), "host={host} must be loopback");
}
}
#[test]
fn non_loopback_host_forms() {
for host in [
"",
"example.com",
"0.0.0.0",
"0.0.0.0:9077",
"::",
"[::]",
"192.168.5.216",
"10.0.0.7:9077",
"term-dev.tokitoken.com",
"127.0.0.1.evil.com",
"localhost.evil.com",
"http://127.0.0.1",
"[::1]@evil.com",
"[::1]:80@evil.com",
] {
assert!(!is_loopback_host(host), "host={host} must NOT be loopback");
}
}
#[test]
fn host_forms_drive_is_local_request() {
let cases: [(&str, Option<SocketAddr>, bool); 10] = [
("localhost", peer("127.0.0.1"), true),
("LOCALHOST", peer("::1"), true),
("localhost.", peer("127.0.0.1"), true),
("127.0.0.1:18777", peer("127.0.0.1"), true),
("127.0.0.2", peer("127.0.0.1"), true),
("[::1]:18777", peer("::1"), true),
("::1", peer("::1"), true),
("0:0:0:0:0:0:0:1", peer("::1"), true),
("example.com", peer("127.0.0.1"), false),
("", peer("127.0.0.1"), false),
];
for (host, peer_addr, expected) in cases {
let h = headers(&[("host", host)]);
assert_eq!(
is_local_request(&h, peer_addr),
expected,
"host={host:?} peer={peer_addr:?}"
);
}
}
#[test]
fn peer_unspecified_is_not_loopback() {
assert!(!is_local_request(&loopback_headers(), peer("0.0.0.0")));
}
#[test]
fn v4_mapped_peer_is_not_loopback_fail_closed() {
assert!(!is_local_request(&loopback_headers(), peer("::ffff:127.0.0.1")));
}
}