cratefield_runtime_native/ports/
mod.rs1mod blob;
7mod clock;
8mod defer;
9mod http;
10mod kv;
11mod rate_limit;
12
13pub use blob::DirBlob;
14pub use clock::TokioClock;
15pub use defer::SpawnDefer;
16pub use http::ReqwestClient;
17pub use kv::{RedisBundle, RedisKv, RedisPortError, redis_from_env};
18pub use rate_limit::RedisRateLimiter;
19
20use axum::http::{HeaderMap, HeaderName};
21use std::net::IpAddr;
22use std::str::FromStr as _;
23
24use cratefield_core::Config;
25
26pub const CLIENT_IP_HEADER: &str = "cf-connecting-ip";
33
34const STRIPPED: [&str; 4] = [
40 "cf-connecting-ip",
41 "x-forwarded-for",
42 "x-real-ip",
43 "forwarded",
44];
45
46#[must_use]
59pub fn trusted_proxy_headers(config: &dyn Config) -> Vec<String> {
60 config
61 .get("TRUSTED_PROXY_HEADERS")
62 .map(|raw| {
63 raw.split(',')
64 .map(str::trim)
65 .filter(|entry| !entry.is_empty())
66 .map(str::to_ascii_lowercase)
67 .filter(|name| HeaderName::from_str(name).is_ok())
68 .collect::<Vec<_>>()
69 })
70 .unwrap_or_default()
71}
72
73#[must_use]
88pub fn client_ip(headers: &HeaderMap, trusted: &[String], peer: Option<IpAddr>) -> Option<IpAddr> {
89 for name in trusted {
90 let parsed = headers
91 .get(name.as_str())
92 .and_then(|value| value.to_str().ok())
93 .and_then(|list| list.split(',').next())
94 .map(str::trim)
95 .and_then(|first| first.parse::<IpAddr>().ok());
96 if let Some(ip) = parsed {
97 return Some(ip);
98 }
99 }
100 peer
101}
102
103pub(crate) fn normalize_headers(headers: &mut HeaderMap, resolved: Option<IpAddr>) {
108 for name in STRIPPED {
109 headers.remove(HeaderName::from_static(name));
110 }
111 if let Some(ip) = resolved
112 && let Ok(value) = axum::http::HeaderValue::from_str(&ip.to_string())
113 {
114 headers.insert(HeaderName::from_static(CLIENT_IP_HEADER), value);
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use axum::http::HeaderValue;
122 use cratefield_core::MapConfig;
123
124 fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
125 use std::str::FromStr as _;
126 let mut map = HeaderMap::new();
127 for (name, value) in pairs {
128 map.insert(
129 HeaderName::from_str(name).expect("test header names are valid"),
130 HeaderValue::from_str(value).expect("valid header value"),
131 );
132 }
133 map
134 }
135
136 #[test]
137 fn trusted_list_defaults_to_empty() {
138 assert!(trusted_proxy_headers(&MapConfig::default()).is_empty());
139 let cfg = MapConfig::from_pairs([("TRUSTED_PROXY_HEADERS", " ")]);
140 assert!(trusted_proxy_headers(&cfg).is_empty());
141 }
142
143 #[test]
144 fn trusted_list_parses_lowercased_and_drops_invalid() {
145 let cfg = MapConfig::from_pairs([(
146 "TRUSTED_PROXY_HEADERS",
147 "X-Forwarded-For, x-real-ip, bad name,,cf-connecting-ip",
148 )]);
149 assert_eq!(
150 trusted_proxy_headers(&cfg),
151 ["x-forwarded-for", "x-real-ip", "cf-connecting-ip"]
152 );
153 }
154
155 #[test]
156 fn unconfigured_trusts_no_header_only_the_peer() {
157 let headers = headers(&[
158 ("x-forwarded-for", "6.6.6.6"),
159 ("cf-connecting-ip", "7.7.7.7"),
160 ("x-real-ip", "8.8.8.8"),
161 ]);
162 let peer: IpAddr = "203.0.113.7".parse().unwrap();
163 assert_eq!(
164 client_ip(&headers, &[], Some(peer)),
165 Some(peer),
166 "with no trusted headers, forwarding headers are decoration"
167 );
168 }
169
170 #[test]
171 fn trusted_header_wins_in_config_order() {
172 let headers = headers(&[
173 ("x-real-ip", "198.51.100.2"),
174 ("x-forwarded-for", "198.51.100.1, 10.0.0.1"),
175 ]);
176 let trusted = vec!["x-forwarded-for".to_owned(), "x-real-ip".to_owned()];
177 let resolved = client_ip(&headers, &trusted, Some("203.0.113.7".parse().unwrap()));
178 assert_eq!(
179 resolved.map(|ip| ip.to_string()).as_deref(),
180 Some("198.51.100.1")
181 );
182 }
183
184 #[test]
185 fn garbage_in_trusted_header_falls_through_to_next_then_peer() {
186 let headers = headers(&[("x-real-ip", "not-an-ip")]);
187 let peer: IpAddr = "203.0.113.7".parse().unwrap();
188 let trusted = vec!["x-real-ip".to_owned()];
189 assert_eq!(client_ip(&headers, &trusted, Some(peer)), Some(peer));
190 }
191
192 #[test]
193 fn normalize_strips_everything_and_sets_the_verdict() {
194 let mut headers = headers(&[
195 ("x-forwarded-for", "6.6.6.6"),
196 ("forwarded", "for=7.7.7.7"),
197 ("x-real-ip", "8.8.8.8"),
198 ("cf-connecting-ip", "9.9.9.9"),
199 ]);
200 normalize_headers(&mut headers, Some("203.0.113.7".parse::<IpAddr>().unwrap()));
201 assert_eq!(headers.get("x-forwarded-for"), None);
203 assert_eq!(headers.get("forwarded"), None);
204 assert_eq!(headers.get("x-real-ip"), None);
205 assert_eq!(
206 headers.get(CLIENT_IP_HEADER).and_then(|v| v.to_str().ok()),
207 Some("203.0.113.7")
208 );
209 assert_eq!(
211 cratefield_core::client_ip(&headers).as_deref(),
212 Some("203.0.113.7")
213 );
214 }
215}