Skip to main content

genmeta_proxy/
route.rs

1use dhttp::name::DhttpName as Name;
2use http::{Method, Uri};
3use hyper::body::Incoming;
4
5/// Classification of an incoming proxy request.
6#[derive(Debug)]
7pub enum Route {
8    /// Plain HTTP request to a DHTTP identity domain — forward via DHTTP/3
9    GenmetaPlainHttp {
10        authority: http::uri::Authority,
11        uri: Uri,
12    },
13    /// CONNECT request to a DHTTP identity domain — return 502 (Phase 2: MITM)
14    GenmetaConnect { authority: http::uri::Authority },
15    /// CONNECT request to a non-DHTTP identity domain — standard TCP tunnel
16    TunnelConnect { authority: http::uri::Authority },
17    /// Plain HTTP request to a non-DHTTP identity domain — standard HTTP forward
18    StandardForward { uri: Uri },
19}
20
21/// Routes incoming requests based on the DHTTP identity domain suffix.
22pub struct Router {
23    /// Reserved for future blacklist filtering (Phase 2+)
24    _blacklist: Vec<String>,
25}
26
27impl Default for Router {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl Router {
34    pub fn new() -> Self {
35        Self {
36            _blacklist: Vec::new(),
37        }
38    }
39
40    /// Check if a host (without port) matches any configured suffix.
41    pub fn is_genmeta(&self, host: &str) -> bool {
42        // strip port if present
43        let host = host.split(':').next().unwrap_or(host);
44        host.ends_with('~')
45            || (host.len() >= Name::SUFFIX.len()
46                && host[host.len() - Name::SUFFIX.len()..].eq_ignore_ascii_case(Name::SUFFIX))
47    }
48
49    /// Classify an incoming request into a Route variant.
50    pub fn classify(&self, req: &hyper::Request<Incoming>) -> Route {
51        let method = req.method();
52        let uri = req.uri();
53
54        if method == Method::CONNECT {
55            // CONNECT: authority is in the URI path (host:port)
56            if let Some(authority) = uri.authority() {
57                if self.is_genmeta(authority.host()) {
58                    return Route::GenmetaConnect {
59                        authority: authority.clone(),
60                    };
61                } else {
62                    return Route::TunnelConnect {
63                        authority: authority.clone(),
64                    };
65                }
66            }
67        }
68
69        // Plain HTTP: URI is absolute form (http://host/path)
70        if let Some(authority) = uri.authority()
71            && self.is_genmeta(authority.host())
72        {
73            return Route::GenmetaPlainHttp {
74                authority: authority.clone(),
75                uri: uri.clone(),
76            };
77        }
78
79        Route::StandardForward { uri: uri.clone() }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    fn router() -> Router {
88        Router::new()
89    }
90
91    #[test]
92    fn test_is_genmeta_exact_suffix() {
93        let r = router();
94        assert!(r.is_genmeta("api.dhttp.net"));
95        assert!(r.is_genmeta("API.Dhttp.Net"));
96        assert!(r.is_genmeta("test.dhttp.net"));
97        assert!(r.is_genmeta("a.b.dhttp.net"));
98    }
99
100    #[test]
101    fn test_is_genmeta_non_match() {
102        let r = router();
103        assert!(!r.is_genmeta("example.com"));
104        assert!(!r.is_genmeta("dhttp.net.evil.com"));
105        assert!(!r.is_genmeta("notdhttp.net"));
106    }
107
108    #[test]
109    fn test_is_genmeta_with_port() {
110        let r = router();
111        // is_genmeta takes host (no port), but let's be safe
112        assert!(r.is_genmeta("api.dhttp.net:443"));
113    }
114
115    #[test]
116    fn test_is_genmeta_bare_domain() {
117        // "dhttp.net" without subdomain — matches if suffix is ".dhttp.net"?
118        // Depends on implementation. ".dhttp.net" suffix means subdomains only.
119        // "dhttp.net" does NOT end with ".dhttp.net" — correct behavior.
120        let r = router();
121        assert!(!r.is_genmeta("dhttp.net"));
122    }
123}