1use std::{
18 net::{IpAddr, SocketAddr},
19 str::FromStr,
20};
21
22use http::{HeaderMap, HeaderName};
23
24use crate::Error;
25
26macro_rules! client_ip_headers {
27 (
28 $(
29 $(#[$docs:meta])*
30 ($variant:ident, $name:literal, $extractor:path);
31 )+
32 ) => {
33 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
38 pub enum ClientIpHeader {
39 $(
40 $(#[$docs])*
41 $variant,
42 )+
43 }
44
45 impl FromStr for ClientIpHeader {
46 type Err = Error;
47
48 fn from_str(source: &str) -> Result<Self, Self::Err> {
49 let name = HeaderName::from_bytes(source.as_bytes())
50 .map_err(|_| Error::unsupported_header_name(source))?;
51
52 match name.as_str() {
53 $(
54 $name => Ok(Self::$variant),
55 )+
56 _ => Err(Error::unsupported_header_name(source)),
57 }
58 }
59 }
60
61 impl ClientIpHeader {
62 fn extract(self, headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
63 match self {
64 $(
65 Self::$variant => ($extractor)(headers),
66 )+
67 }
68 }
69 }
70 };
71}
72
73client_ip_headers! {
74 (
76 CfConnectingIp,
77 "cf-connecting-ip",
78 crate::client_ip_headers::extract_header_cf_connecting_ip
79 );
80 (XRealIp, "x-real-ip", crate::client_ip_headers::extract_header_x_real_ip);
82 (Forwarded, "forwarded", crate::forwarded::extract_rightmost_forwarded);
84 (
86 XForwardedFor,
87 "x-forwarded-for",
88 crate::x_forwarded::extract_rightmost_x_forwarded_for
89 );
90 (
92 CloudFrontViewerAddress,
93 "cloudfront-viewer-address",
94 crate::client_ip_headers::extract_header_cloudfront_viewer_address
95 );
96 (
98 FlyClientIp,
99 "fly-client-ip",
100 crate::client_ip_headers::extract_header_fly_client_ip
101 );
102 (
104 TrueClientIp,
105 "true-client-ip",
106 crate::client_ip_headers::extract_header_true_client_ip
107 );
108 (
110 XEnvoyExternalAddress,
111 "x-envoy-external-address",
112 crate::client_ip_headers::extract_header_x_envoy_external_address
113 );
114}
115
116impl TryFrom<&str> for ClientIpHeader {
117 type Error = Error;
118
119 fn try_from(source: &str) -> Result<Self, Self::Error> {
120 source.parse()
121 }
122}
123
124pub const CLIENT_IP_HEADERS: &[ClientIpHeader] = &[
131 ClientIpHeader::Forwarded,
132 ClientIpHeader::XForwardedFor,
133 ClientIpHeader::XRealIp,
134 ClientIpHeader::CfConnectingIp,
135];
136
137pub const fn extract_peer_ip(peer: SocketAddr) -> IpAddr {
143 extract_peer_address(peer).ip()
144}
145
146pub const fn extract_peer_address(peer: SocketAddr) -> SocketAddr {
152 peer
153}
154
155#[cfg(feature = "axum")]
162pub fn extract_axum_peer_address<B>(request: &http::Request<B>) -> Option<SocketAddr> {
163 request
164 .extensions()
165 .get::<axum::extract::ConnectInfo<SocketAddr>>()
166 .map(|info| info.0)
167}
168
169#[cfg(feature = "axum")]
177pub fn extract_axum_peer_ip<B>(request: &http::Request<B>) -> Option<IpAddr> {
178 extract_axum_peer_address(request).map(|peer| peer.ip())
179}
180
181pub fn extract_client_ip(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
192 extract_client_ip_with_headers(headers, CLIENT_IP_HEADERS)
193}
194
195pub fn extract_client_ip_with_headers(
205 headers: &HeaderMap,
206 sources: &[ClientIpHeader],
207) -> Result<Option<IpAddr>, Error> {
208 for source in sources {
209 if let Some(ip) = source.extract(headers)? {
210 return Ok(Some(ip));
211 }
212 }
213
214 Ok(None)
215}
216
217#[cfg(test)]
218mod tests {
219 use http::HeaderMap;
220
221 use crate::{forwarded::FORWARDED, x_forwarded::X_FORWARDED_FOR};
222
223 use super::*;
224 #[test]
225 fn peer_functions_preserve_the_transport_address() {
226 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
227 assert_eq!(extract_peer_address(peer), peer);
228 assert_eq!(
229 extract_peer_ip(peer),
230 "203.0.113.8".parse::<IpAddr>().unwrap()
231 );
232 }
233
234 #[cfg(feature = "axum")]
235 #[test]
236 fn axum_peer_address_reads_connect_info_extension() {
237 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
238 let mut request = http::Request::new(());
239 request
240 .extensions_mut()
241 .insert(axum::extract::ConnectInfo(peer));
242
243 assert_eq!(extract_axum_peer_address(&request), Some(peer));
244 }
245
246 #[cfg(feature = "axum")]
247 #[test]
248 fn axum_peer_address_returns_none_without_connect_info() {
249 let request = http::Request::new(());
250
251 assert_eq!(extract_axum_peer_address(&request), None);
252 }
253
254 #[cfg(feature = "axum")]
255 #[test]
256 fn axum_peer_ip_reads_connect_info_extension() {
257 let peer: SocketAddr = "203.0.113.8:443".parse().unwrap();
258 let mut request = http::Request::new(());
259 request
260 .extensions_mut()
261 .insert(axum::extract::ConnectInfo(peer));
262
263 assert_eq!(extract_axum_peer_ip(&request), Some(peer.ip()));
264 }
265
266 #[cfg(feature = "axum")]
267 #[test]
268 fn axum_peer_ip_returns_none_without_connect_info() {
269 let request = http::Request::new(());
270
271 assert_eq!(extract_axum_peer_ip(&request), None);
272 }
273
274 #[test]
275 fn default_order_is_stable_and_first_present_header_wins() {
276 assert_eq!(
277 CLIENT_IP_HEADERS,
278 &[
279 ClientIpHeader::Forwarded,
280 ClientIpHeader::XForwardedFor,
281 ClientIpHeader::XRealIp,
282 ClientIpHeader::CfConnectingIp,
283 ]
284 );
285
286 let mut headers = HeaderMap::new();
287 headers.insert("cf-connecting-ip", "192.0.2.1".parse().unwrap());
288 headers.insert("x-real-ip", "192.0.2.2".parse().unwrap());
289 headers.insert(&FORWARDED, "for=192.0.2.3".parse().unwrap());
290 headers.insert(&X_FORWARDED_FOR, "192.0.2.4".parse().unwrap());
291
292 assert_eq!(
293 extract_client_ip(&headers).unwrap(),
294 Some("192.0.2.3".parse().unwrap())
295 );
296 }
297
298 #[test]
299 fn default_order_falls_through_only_when_a_header_is_absent() {
300 let mut headers = HeaderMap::new();
301 headers.insert(&FORWARDED, "for=192.0.2.3".parse().unwrap());
302 headers.insert(&X_FORWARDED_FOR, "192.0.2.4".parse().unwrap());
303
304 assert_eq!(
305 extract_client_ip(&headers).unwrap(),
306 Some("192.0.2.3".parse().unwrap())
307 );
308
309 headers.insert("x-real-ip", "not-an-ip".parse().unwrap());
310 assert_eq!(
311 extract_client_ip(&headers).unwrap(),
312 Some("192.0.2.3".parse().unwrap())
313 );
314
315 headers.insert(&FORWARDED, "for=unknown".parse().unwrap());
316 assert!(matches!(
317 extract_client_ip(&headers),
318 Err(Error::InvalidHeader { .. })
319 ));
320 }
321
322 #[test]
323 fn chain_headers_return_the_rightmost_address() {
324 let mut headers = HeaderMap::new();
325 headers.insert(
326 &FORWARDED,
327 "for=192.0.2.1, for=198.51.100.2".parse().unwrap(),
328 );
329 assert_eq!(
330 extract_client_ip_with_headers(&headers, &[ClientIpHeader::Forwarded]).unwrap(),
331 Some("198.51.100.2".parse().unwrap())
332 );
333
334 headers.remove(&FORWARDED);
335 headers.insert(&X_FORWARDED_FOR, "192.0.2.1, 198.51.100.3".parse().unwrap());
336 assert_eq!(
337 extract_client_ip_with_headers(&headers, &[ClientIpHeader::XForwardedFor]).unwrap(),
338 Some("198.51.100.3".parse().unwrap())
339 );
340 }
341
342 #[test]
343 fn custom_order_changes_precedence() {
344 let mut headers = HeaderMap::new();
345 headers.insert("cf-connecting-ip", "192.0.2.1".parse().unwrap());
346 headers.insert("x-real-ip", "192.0.2.2".parse().unwrap());
347 let custom_headers = [ClientIpHeader::XRealIp, ClientIpHeader::CfConnectingIp];
348
349 assert_eq!(
350 extract_client_ip_with_headers(&headers, &custom_headers).unwrap(),
351 Some("192.0.2.2".parse().unwrap())
352 );
353 assert_eq!(extract_client_ip_with_headers(&headers, &[]).unwrap(), None);
354 }
355
356 #[test]
357 fn custom_order_supports_every_documented_single_value_header() {
358 for (source, header) in [
359 (ClientIpHeader::CfConnectingIp, "cf-connecting-ip"),
360 (ClientIpHeader::XRealIp, "x-real-ip"),
361 (ClientIpHeader::FlyClientIp, "fly-client-ip"),
362 (ClientIpHeader::TrueClientIp, "true-client-ip"),
363 (
364 ClientIpHeader::XEnvoyExternalAddress,
365 "x-envoy-external-address",
366 ),
367 ] {
368 let mut headers = HeaderMap::new();
369 headers.insert(header, "192.0.2.10".parse().unwrap());
370
371 assert_eq!(
372 extract_client_ip_with_headers(&headers, &[source]).unwrap(),
373 Some("192.0.2.10".parse().unwrap()),
374 "source {header}",
375 );
376 }
377
378 let mut headers = HeaderMap::new();
379 headers.insert(
380 "cloudfront-viewer-address",
381 "192.0.2.10:443".parse().unwrap(),
382 );
383 assert_eq!(
384 extract_client_ip_with_headers(&headers, &[ClientIpHeader::CloudFrontViewerAddress],)
385 .unwrap(),
386 Some("192.0.2.10".parse().unwrap())
387 );
388 }
389
390 #[test]
391 fn parses_supported_header_names() {
392 for (name, expected) in [
393 ("cf-connecting-ip", ClientIpHeader::CfConnectingIp),
394 ("X-Real-IP", ClientIpHeader::XRealIp),
395 ("forwarded", ClientIpHeader::Forwarded),
396 ("x-forwarded-for", ClientIpHeader::XForwardedFor),
397 (
398 "cloudfront-viewer-address",
399 ClientIpHeader::CloudFrontViewerAddress,
400 ),
401 ("fly-client-ip", ClientIpHeader::FlyClientIp),
402 ("true-client-ip", ClientIpHeader::TrueClientIp),
403 (
404 "x-envoy-external-address",
405 ClientIpHeader::XEnvoyExternalAddress,
406 ),
407 ] {
408 assert_eq!(ClientIpHeader::try_from(name).unwrap(), expected);
409 assert_eq!(name.parse::<ClientIpHeader>().unwrap(), expected);
410 }
411
412 for header in ["forwarded-for", "not a header"] {
413 let error = header.parse::<ClientIpHeader>().unwrap_err();
414 assert!(matches!(error, Error::UnsupportedHeaderName { .. }));
415 assert!(error.to_string().contains(header));
416 }
417 }
418
419 #[test]
420 fn malformed_selected_source_does_not_fall_through() {
421 let mut headers = HeaderMap::new();
422 headers.insert(&FORWARDED, "for=unknown".parse().unwrap());
423 headers.insert(&X_FORWARDED_FOR, "192.0.2.4".parse().unwrap());
424
425 assert!(matches!(
426 extract_client_ip_with_headers(
427 &headers,
428 &[ClientIpHeader::Forwarded, ClientIpHeader::XForwardedFor,],
429 ),
430 Err(Error::InvalidHeader { .. })
431 ));
432 }
433}