wafrift_proxy/lib.rs
1//! wafrift-proxy — forward HTTP proxy with per-host adaptive WAF evasion.
2//!
3//! Slots between any intercepting proxy (Burp / Caido / mitmproxy) and the
4//! upstream target. On every forwarded request the proxy applies the full
5//! wafrift evasion pipeline (encoding + content-type switching + fingerprint
6//! rotation + body padding) and records bypasses to the per-WAF gene bank.
7//!
8//! Key modules:
9//! - [`intercept`] — HTTP CONNECT tunnel handler (MITM TLS interception)
10//! - [`mitm`] — TLS certificate minting + impersonation via BoringSSL (optional)
11//! - [`upstream`] — Upstream forwarding with evasion pipeline applied
12//! - [`upstream_policy`] — Scope / skip rules (`--only-host`, `--skip-path`, …)
13//! - [`rate_limit`] — Token-bucket rate limiter (per upstream host)
14//! - [`tui`] — ratatui live dashboard (Flow / Overview / Hosts tabs)
15//! - [`scope`] — Glob-based scope evaluation
16//! - [`hop_by_hop`] — Hop-by-hop header stripping per RFC 7230 §6.1
17//!
18//! The binary entry point lives in `main.rs`; this lib module exposes
19//! the building blocks downstream consumers (the bench harness,
20//! integration tests, third-party Rust code that embeds the proxy) need.
21
22pub mod hop_by_hop;
23pub mod intercept;
24pub mod mitm;
25pub mod rate_limit;
26pub mod scope;
27pub mod tui;
28pub mod upstream;
29pub mod upstream_policy;
30
31/// Extract the host from a Host header, handling IPv6 bracket notation and bare IPv6 literals.
32///
33/// Returns the host component only (strips `:port`). For malformed input
34/// (e.g. unclosed brackets) returns an empty string so callers can fall
35/// back to a safe default rather than routing to garbage.
36#[allow(clippy::collapsible_if)]
37pub fn extract_host_from_header(s: &str) -> String {
38 let s = s.trim();
39 if s.is_empty() {
40 return String::new();
41 }
42 if s.starts_with('[') {
43 if let Some(end_idx) = s.find(']') {
44 if end_idx > 1 {
45 return s[1..end_idx].to_string();
46 }
47 }
48 // Malformed bracket notation — don't attempt to route it.
49 return String::new();
50 }
51 // Bare IPv6 (no brackets). Avoid `split(':')` which would truncate at the first segment.
52 if s.contains(':') {
53 if let Ok(std::net::IpAddr::V6(ip)) = s.parse() {
54 return ip.to_string();
55 }
56 }
57 s.split(':').next().unwrap_or(s).to_string()
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn extract_bare_ipv6() {
66 assert_eq!(extract_host_from_header("2001:db8::1"), "2001:db8::1");
67 }
68
69 #[test]
70 fn extract_bracketed_v6_with_port() {
71 assert_eq!(extract_host_from_header("[::1]:443"), "::1");
72 }
73
74 #[test]
75 fn extract_hostname_with_port() {
76 assert_eq!(extract_host_from_header("example.com:443"), "example.com");
77 }
78
79 #[test]
80 fn extract_bare_ipv4() {
81 assert_eq!(extract_host_from_header("192.168.1.1"), "192.168.1.1");
82 }
83
84 #[test]
85 fn extract_ipv4_with_port() {
86 assert_eq!(extract_host_from_header("192.168.1.1:8080"), "192.168.1.1");
87 }
88
89 #[test]
90 fn extract_bracketed_v6_no_port() {
91 assert_eq!(extract_host_from_header("[::1]"), "::1");
92 assert_eq!(extract_host_from_header("[2001:db8::1]"), "2001:db8::1");
93 }
94
95 #[test]
96 fn extract_malformed_bracket_returns_empty() {
97 // Unclosed bracket — must not crash or return garbage like "[".
98 assert_eq!(extract_host_from_header("[::1"), "");
99 assert_eq!(extract_host_from_header("["), "");
100 }
101
102 #[test]
103 fn extract_empty_returns_empty() {
104 assert_eq!(extract_host_from_header(""), "");
105 assert_eq!(extract_host_from_header(" "), "");
106 }
107}