client_ip/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc = include_str!("../README.md")]
3use std::net::IpAddr;
4
5pub use error::Error;
6use http::{HeaderMap, HeaderName};
7
8type Result<T> = std::result::Result<T, Error>;
9
10/// Extracts client IP from `CF-Connecting-IP` (Cloudflare) header
11pub fn cf_connecting_ip(header_map: &HeaderMap) -> Result<IpAddr> {
12    ip_from_single_header(header_map, &HeaderName::from_static("cf-connecting-ip"))
13}
14
15/// Extracts client IP from `CloudFront-Viewer-Address` (AWS CloudFront) header
16pub fn cloudfront_viewer_address(header_map: &HeaderMap) -> Result<IpAddr> {
17    const HEADER_NAME: HeaderName = HeaderName::from_static("cloudfront-viewer-address");
18
19    fn ip_from_header_value(header_value: &str) -> Result<IpAddr> {
20        // Spec: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html#cloudfront-headers-viewer-location
21        // Note: Both IPv4 and IPv6 addresses (in the specified format) do not contain
22        //       non-ascii characters, so no need to handle percent-encoding.
23        //
24        // CloudFront does not use `[::]:12345` style notation for IPv6 (unfortunately),
25        // otherwise parsing via `SocketAddr` would be possible.
26        header_value
27            .rsplit_once(':')
28            .map(|(ip, _port)| ip)
29            .ok_or_else(|| Error::MalformedHeaderValue {
30                header_name: HEADER_NAME,
31                header_value: header_value.to_owned(),
32            })?
33            .trim()
34            .parse::<IpAddr>()
35            .map_err(|_| Error::MalformedHeaderValue {
36                header_name: HEADER_NAME,
37                header_value: header_value.to_owned(),
38            })
39    }
40
41    let header_value = AsciiHeaderValue::of_last_header(header_map, &HEADER_NAME)?;
42    ip_from_header_value(header_value.0)
43}
44
45/// Extracts client IP from `Fly-Client-IP` (Fly.io) header
46///
47/// When the extractor is run for health check path, provide required
48/// `Fly-Client-IP` header through [`services.http_checks.headers`](https://fly.io/docs/reference/configuration/#services-http_checks)
49/// or [`http_service.checks.headers`](https://fly.io/docs/reference/configuration/#services-http_checks)
50pub fn fly_client_ip(header_map: &HeaderMap) -> Result<IpAddr> {
51    ip_from_single_header(header_map, &HeaderName::from_static("fly-client-ip"))
52}
53
54#[cfg(feature = "forwarded-header")]
55/// Extracts the rightmost IP from `Forwarded` header
56pub fn rightmost_forwarded(header_map: &HeaderMap) -> Result<IpAddr> {
57    const HEADER_NAME: HeaderName = HeaderName::from_static("forwarded");
58
59    fn ip_from_header_value(header_value: &str) -> Result<IpAddr> {
60        use forwarded_header_value::{ForwardedHeaderValue, Identifier};
61
62        let stanza = ForwardedHeaderValue::from_forwarded(header_value)
63            .map_err(|_| Error::MalformedHeaderValue {
64                header_name: HEADER_NAME,
65                header_value: header_value.to_owned(),
66            })?
67            .into_iter()
68            .last()
69            .ok_or_else(|| Error::MalformedHeaderValue {
70                header_name: HEADER_NAME,
71                header_value: header_value.to_owned(),
72            })?;
73
74        let forwarded_for = stanza.forwarded_for.ok_or_else(|| Error::ForwardedNoFor {
75            header_value: header_value.to_owned(),
76        })?;
77
78        match forwarded_for {
79            Identifier::SocketAddr(a) => Ok(a.ip()),
80            Identifier::IpAddr(ip) => Ok(ip),
81            Identifier::String(_) => Err(Error::ForwardedObfuscated {
82                header_value: header_value.to_owned(),
83            }),
84            Identifier::Unknown => Err(Error::ForwardedUnknown {
85                header_value: header_value.to_owned(),
86            }),
87        }
88    }
89
90    let header_value = AsciiHeaderValue::of_last_header(header_map, &HEADER_NAME)?;
91    ip_from_header_value(header_value.0)
92}
93
94/// Extracts the rightmost IP address from the comma-separated list in the value
95/// of the last `X-Forwarded-For` header.
96pub fn rightmost_x_forwarded_for(header_map: &HeaderMap) -> Result<IpAddr> {
97    const HEADER_NAME: HeaderName = HeaderName::from_static("x-forwarded-for");
98
99    fn ip_from_header_value(header_value: &str) -> Result<IpAddr> {
100        header_value
101            .split(',')
102            .next_back()
103            .ok_or_else(|| Error::MalformedHeaderValue {
104                header_name: HEADER_NAME,
105                header_value: header_value.to_owned(),
106            })?
107            .trim()
108            .parse::<IpAddr>()
109            .map_err(|_| Error::MalformedHeaderValue {
110                header_name: HEADER_NAME,
111                header_value: header_value.to_owned(),
112            })
113    }
114
115    let header_value = AsciiHeaderValue::of_last_header(header_map, &HEADER_NAME)?;
116    ip_from_header_value(header_value.0)
117}
118
119/// Extracts client IP from `True-Client-IP` (Akamai, Cloudflare) header
120pub fn true_client_ip(header_map: &HeaderMap) -> Result<IpAddr> {
121    ip_from_single_header(header_map, &HeaderName::from_static("true-client-ip"))
122}
123
124/// Extracts client IP from `X-Real-Ip` (Nginx) header
125pub fn x_real_ip(header_map: &HeaderMap) -> Result<IpAddr> {
126    ip_from_single_header(header_map, &HeaderName::from_static("x-real-ip"))
127}
128
129/// A [`http::HeaderValue`] converted to string and ensured to be valid ASCII
130#[derive(Debug)]
131struct AsciiHeaderValue<'a>(&'a str);
132
133impl<'a> AsciiHeaderValue<'a> {
134    /// Returns value of a header that must occur only once. Multiple
135    /// occurrences of the header are considered a critical proxy configuration
136    /// error.
137    fn of_single_header(header_map: &'a HeaderMap, header_name: &HeaderName) -> Result<Self> {
138        let mut iter = header_map.get_all(header_name).into_iter();
139
140        let Some(header_value) = iter.next() else {
141            return Err(Error::AbsentHeader {
142                header_name: header_name.to_owned(),
143            });
144        };
145
146        if iter.next().is_some() {
147            return Err(Error::SingleHeaderRequired {
148                header_name: header_name.to_owned(),
149            });
150        }
151
152        header_value
153            .to_str()
154            .map_err(|_| Error::NonAsciiHeaderValue {
155                header_name: header_name.to_owned(),
156            })
157            .map(Self)
158    }
159
160    /// Returns a value of the last occurring header.
161    fn of_last_header(header_map: &'a HeaderMap, header_name: &HeaderName) -> Result<Self> {
162        header_map
163            .get_all(header_name)
164            .into_iter()
165            .next_back()
166            .ok_or_else(|| Error::AbsentHeader {
167                header_name: header_name.to_owned(),
168            })?
169            .to_str()
170            .map_err(|_| Error::NonAsciiHeaderValue {
171                header_name: header_name.to_owned(),
172            })
173            .map(Self)
174    }
175
176    /// Tries to parse the whole value as an IP.
177    fn parse_ip(&self, header_name: &HeaderName) -> Result<IpAddr> {
178        self.0
179            .trim()
180            .parse()
181            .map_err(|_| Error::MalformedHeaderValue {
182                header_name: header_name.to_owned(),
183                header_value: self.0.to_owned(),
184            })
185    }
186}
187
188/// Parses an IP from a header that occurs only once. Multiple
189/// occurrences of the header are considered a proxy configuration error.
190fn ip_from_single_header(header_map: &HeaderMap, header_name: &HeaderName) -> Result<IpAddr> {
191    AsciiHeaderValue::of_single_header(header_map, header_name)?.parse_ip(header_name)
192}
193
194mod error {
195    use std::fmt;
196
197    use http::HeaderName;
198
199    /// Errors that can occur during IP extraction
200    #[derive(Debug, PartialEq)]
201    pub enum Error {
202        /// The IP-related header is missing
203        AbsentHeader {
204            /// Header name
205            header_name: HeaderName,
206        },
207        /// Header value contains not only visible ASCII characters
208        NonAsciiHeaderValue {
209            /// Header name
210            header_name: HeaderName,
211        },
212        /// Header value has an unexpected format
213        MalformedHeaderValue {
214            /// Header name
215            header_name: HeaderName,
216            /// Header value
217            header_value: String,
218        },
219        /// Multiple occurrences of a header required to occur only once found
220        ///
221        /// According to the HTTP/1.1 specification (RFC 7230, Section 3.2.2):
222        /// > A sender MUST NOT generate multiple header fields with the same
223        /// > field name in a message unless either the entire field value for
224        /// > that header field is defined as a comma-separated list ...
225        SingleHeaderRequired {
226            /// Header name
227            header_name: HeaderName,
228        },
229        #[cfg(feature = "forwarded-header")]
230        /// Forwarded header doesn't contain `for` directive
231        ForwardedNoFor {
232            /// Header value
233            header_value: String,
234        },
235        #[cfg(feature = "forwarded-header")]
236        /// RFC 7239 allows to [obfuscate IPs](https://www.rfc-editor.org/rfc/rfc7239.html#section-6.3)
237        ForwardedObfuscated {
238            /// Header value
239            header_value: String,
240        },
241        #[cfg(feature = "forwarded-header")]
242        /// RFC 7239 allows [unknown identifiers](https://www.rfc-editor.org/rfc/rfc7239.html#section-6.2)
243        ForwardedUnknown {
244            /// Header value
245            header_value: String,
246        },
247    }
248
249    impl fmt::Display for Error {
250        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251            match self {
252                Self::AbsentHeader { header_name } => {
253                    write!(f, "Missing required header: {header_name}")
254                }
255                Self::NonAsciiHeaderValue { header_name } => write!(
256                    f,
257                    "Header value contains non-ASCII characters: {header_name}",
258                ),
259                Self::MalformedHeaderValue {
260                    header_name,
261                    header_value,
262                } => write!(
263                    f,
264                    "Malformed header value for `{header_name}`: {header_value}",
265                ),
266                Self::SingleHeaderRequired { header_name } => write!(
267                    f,
268                    "Multiple occurrences of the header aren't allowed: {header_name}"
269                ),
270                #[cfg(feature = "forwarded-header")]
271                Self::ForwardedNoFor { header_value } => write!(
272                    f,
273                    "`Forwarded` header missing `for` directive: {header_value}",
274                ),
275                #[cfg(feature = "forwarded-header")]
276                Self::ForwardedObfuscated { header_value } => write!(
277                    f,
278                    "`Forwarded` header contains obfuscated IP: {header_value}",
279                ),
280                #[cfg(feature = "forwarded-header")]
281                Self::ForwardedUnknown { header_value } => write!(
282                    f,
283                    "`Forwarded` header contains unknown identifier: {header_value}",
284                ),
285            }
286        }
287    }
288
289    impl std::error::Error for Error {}
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    const VALID_IPV4: &str = "1.2.3.4";
297    const VALID_IPV6: &str = "1:23:4567:89ab:c:d:e:f";
298
299    fn headers<'a>(items: impl IntoIterator<Item = (&'a str, &'a str)>) -> HeaderMap {
300        HeaderMap::from_iter(
301            items
302                .into_iter()
303                .map(|(name, value)| (name.parse().unwrap(), value.parse().unwrap())),
304        )
305    }
306
307    #[test]
308    fn test_ascii_header_value_of_last_header() {
309        let header_name_str = "my-header";
310        let header_name = HeaderName::from_static(header_name_str);
311
312        assert_eq!(
313            AsciiHeaderValue::of_last_header(&headers([]), &header_name).unwrap_err(),
314            Error::AbsentHeader {
315                header_name: header_name.clone()
316            }
317        );
318
319        assert_eq!(
320            AsciiHeaderValue::of_last_header(&headers([(header_name_str, "ы")]), &header_name)
321                .unwrap_err(),
322            Error::NonAsciiHeaderValue {
323                header_name: header_name.clone()
324            }
325        );
326
327        assert_eq!(
328            AsciiHeaderValue::of_last_header(&headers([(header_name_str, "foo")]), &header_name)
329                .unwrap()
330                .0,
331            "foo",
332            "single valid header"
333        );
334
335        assert_eq!(
336            AsciiHeaderValue::of_last_header(
337                &headers([(header_name_str, "foo"), (header_name_str, "bar")]),
338                &header_name
339            )
340            .unwrap()
341            .0,
342            "bar",
343            "multiple valid headers"
344        );
345    }
346
347    #[test]
348    fn test_ascii_header_value_of_single_header() {
349        let header_name_str = "my-header";
350        let header_name = HeaderName::from_static(header_name_str);
351
352        assert_eq!(
353            AsciiHeaderValue::of_single_header(&headers([]), &header_name).unwrap_err(),
354            Error::AbsentHeader {
355                header_name: header_name.clone()
356            }
357        );
358
359        assert_eq!(
360            AsciiHeaderValue::of_single_header(&headers([(header_name_str, "ы")]), &header_name)
361                .unwrap_err(),
362            Error::NonAsciiHeaderValue {
363                header_name: header_name.clone()
364            }
365        );
366
367        assert_eq!(
368            AsciiHeaderValue::of_single_header(
369                &headers([(header_name_str, "foo"), (header_name_str, "bar")]),
370                &header_name
371            )
372            .unwrap_err(),
373            Error::SingleHeaderRequired {
374                header_name: header_name.clone()
375            }
376        );
377
378        assert_eq!(
379            AsciiHeaderValue::of_single_header(&headers([(header_name_str, "foo")]), &header_name)
380                .unwrap()
381                .0,
382            "foo"
383        );
384    }
385
386    #[test]
387    fn test_cf_connecting_ip() {
388        let header = "cf-connecting-ip";
389
390        assert_eq!(
391            cf_connecting_ip(&headers([])).unwrap_err(),
392            Error::AbsentHeader {
393                header_name: HeaderName::from_static(header)
394            }
395        );
396        assert_eq!(
397            cf_connecting_ip(&headers([(header, "ы")])).unwrap_err(),
398            Error::NonAsciiHeaderValue {
399                header_name: HeaderName::from_static(header)
400            }
401        );
402        assert_eq!(
403            cf_connecting_ip(&headers([(header, "foo")])).unwrap_err(),
404            Error::MalformedHeaderValue {
405                header_name: HeaderName::from_static(header),
406                header_value: "foo".into(),
407            }
408        );
409
410        assert_eq!(
411            cf_connecting_ip(&headers([(header, VALID_IPV4)])).unwrap(),
412            VALID_IPV4.parse::<IpAddr>().unwrap()
413        );
414        assert_eq!(
415            cf_connecting_ip(&headers([(header, VALID_IPV6)])).unwrap(),
416            VALID_IPV6.parse::<IpAddr>().unwrap()
417        );
418    }
419
420    #[test]
421    fn test_cloudfront_viewer_address() {
422        let header = "cloudfront-viewer-address";
423
424        assert_eq!(
425            cloudfront_viewer_address(&headers([])).unwrap_err(),
426            Error::AbsentHeader {
427                header_name: HeaderName::from_static(header)
428            }
429        );
430        assert_eq!(
431            cloudfront_viewer_address(&headers([(header, "ы")])).unwrap_err(),
432            Error::NonAsciiHeaderValue {
433                header_name: HeaderName::from_static(header)
434            }
435        );
436        assert_eq!(
437            cloudfront_viewer_address(&headers([(header, VALID_IPV4)])).unwrap_err(),
438            Error::MalformedHeaderValue {
439                header_name: HeaderName::from_static(header),
440                header_value: VALID_IPV4.into(),
441            }
442        );
443        assert_eq!(
444            cloudfront_viewer_address(&headers([(header, "foo:8000")])).unwrap_err(),
445            Error::MalformedHeaderValue {
446                header_name: HeaderName::from_static(header),
447                header_value: "foo:8000".into(),
448            }
449        );
450
451        let valid_header_value_v4 = format!("{VALID_IPV4}:8000");
452        let valid_header_value_v6 = format!("{VALID_IPV6}:8000");
453        assert_eq!(
454            cloudfront_viewer_address(&headers([(header, valid_header_value_v4.as_ref())]))
455                .unwrap(),
456            VALID_IPV4.parse::<IpAddr>().unwrap()
457        );
458        assert_eq!(
459            cloudfront_viewer_address(&headers([(header, valid_header_value_v6.as_ref())]))
460                .unwrap(),
461            VALID_IPV6.parse::<IpAddr>().unwrap()
462        );
463    }
464
465    #[test]
466    fn test_fly_client_ip() {
467        let header = "fly-client-ip";
468
469        assert_eq!(
470            fly_client_ip(&headers([])).unwrap_err(),
471            Error::AbsentHeader {
472                header_name: HeaderName::from_static(header)
473            }
474        );
475        assert_eq!(
476            fly_client_ip(&headers([(header, "ы")])).unwrap_err(),
477            Error::NonAsciiHeaderValue {
478                header_name: HeaderName::from_static(header)
479            }
480        );
481        assert_eq!(
482            fly_client_ip(&headers([(header, "foo")])).unwrap_err(),
483            Error::MalformedHeaderValue {
484                header_name: HeaderName::from_static(header),
485                header_value: "foo".into(),
486            }
487        );
488
489        assert_eq!(
490            fly_client_ip(&headers([(header, VALID_IPV4)])).unwrap(),
491            VALID_IPV4.parse::<IpAddr>().unwrap()
492        );
493        assert_eq!(
494            fly_client_ip(&headers([(header, VALID_IPV6)])).unwrap(),
495            VALID_IPV6.parse::<IpAddr>().unwrap()
496        );
497    }
498
499    #[cfg(feature = "forwarded-header")]
500    #[test]
501    fn test_rightmost_forwarded() {
502        let header = "forwarded";
503
504        assert_eq!(
505            rightmost_forwarded(&headers([])).unwrap_err(),
506            Error::AbsentHeader {
507                header_name: HeaderName::from_static(header)
508            }
509        );
510        assert_eq!(
511            rightmost_forwarded(&headers([(header, "ы")])).unwrap_err(),
512            Error::NonAsciiHeaderValue {
513                header_name: HeaderName::from_static(header)
514            }
515        );
516        assert_eq!(
517            rightmost_forwarded(&headers([(header, "foo")])).unwrap_err(),
518            Error::MalformedHeaderValue {
519                header_name: HeaderName::from_static(header),
520                header_value: "foo".into(),
521            }
522        );
523        assert_eq!(
524            rightmost_forwarded(&headers([
525                (header, format!("for={VALID_IPV4}").as_ref()),
526                (header, "proto=http"),
527            ]))
528            .unwrap_err(),
529            Error::ForwardedNoFor {
530                header_value: "proto=http".into(),
531            }
532        );
533        assert_eq!(
534            rightmost_forwarded(&headers([(header, "for=unknown")])).unwrap_err(),
535            Error::ForwardedUnknown {
536                header_value: "for=unknown".into(),
537            }
538        );
539        assert_eq!(
540            rightmost_forwarded(&headers([(header, "for=_foo")])).unwrap_err(),
541            Error::ForwardedObfuscated {
542                header_value: "for=_foo".into(),
543            }
544        );
545
546        assert_eq!(
547            rightmost_forwarded(&headers([
548                (header, "proto=http"),
549                (header, format!("for={VALID_IPV4};proto=http").as_ref()),
550            ]))
551            .unwrap(),
552            VALID_IPV4.parse::<IpAddr>().unwrap()
553        );
554        assert_eq!(
555            rightmost_forwarded(&headers([(
556                header,
557                format!("for={VALID_IPV4}:8000").as_ref()
558            ),]))
559            .unwrap(),
560            VALID_IPV4.parse::<IpAddr>().unwrap()
561        );
562
563        assert_eq!(
564            rightmost_forwarded(&headers([(header, format!("for={VALID_IPV6}").as_ref()),]))
565                .unwrap(),
566            VALID_IPV6.parse::<IpAddr>().unwrap()
567        );
568        assert_eq!(
569            rightmost_forwarded(&headers([(
570                header,
571                format!("for=[{VALID_IPV6}]:8000").as_ref()
572            ),]))
573            .unwrap(),
574            VALID_IPV6.parse::<IpAddr>().unwrap()
575        );
576    }
577
578    #[test]
579    fn test_rightmost_x_forwarded_for() {
580        let header = "x-forwarded-for";
581
582        assert_eq!(
583            rightmost_x_forwarded_for(&headers([])).unwrap_err(),
584            Error::AbsentHeader {
585                header_name: HeaderName::from_static(header)
586            }
587        );
588        assert_eq!(
589            rightmost_x_forwarded_for(&headers([(header, "ы")])).unwrap_err(),
590            Error::NonAsciiHeaderValue {
591                header_name: HeaderName::from_static(header)
592            }
593        );
594        assert_eq!(
595            rightmost_x_forwarded_for(&headers([(header, "1.2.3.4,foo")])).unwrap_err(),
596            Error::MalformedHeaderValue {
597                header_name: HeaderName::from_static(header),
598                header_value: "1.2.3.4,foo".into(),
599            }
600        );
601
602        assert_eq!(
603            rightmost_x_forwarded_for(&headers([(header, format!("foo,{VALID_IPV4}").as_ref())]))
604                .unwrap(),
605            VALID_IPV4.parse::<IpAddr>().unwrap()
606        );
607        assert_eq!(
608            rightmost_x_forwarded_for(&headers([(header, VALID_IPV6)])).unwrap(),
609            VALID_IPV6.parse::<IpAddr>().unwrap()
610        );
611    }
612
613    #[test]
614    fn test_true_client_ip() {
615        let header = "true-client-ip";
616
617        assert_eq!(
618            true_client_ip(&headers([])).unwrap_err(),
619            Error::AbsentHeader {
620                header_name: HeaderName::from_static(header)
621            }
622        );
623        assert_eq!(
624            true_client_ip(&headers([(header, "ы")])).unwrap_err(),
625            Error::NonAsciiHeaderValue {
626                header_name: HeaderName::from_static(header)
627            }
628        );
629        assert_eq!(
630            true_client_ip(&headers([(header, "foo")])).unwrap_err(),
631            Error::MalformedHeaderValue {
632                header_name: HeaderName::from_static(header),
633                header_value: "foo".into(),
634            }
635        );
636
637        assert_eq!(
638            true_client_ip(&headers([(header, VALID_IPV4)])).unwrap(),
639            VALID_IPV4.parse::<IpAddr>().unwrap()
640        );
641        assert_eq!(
642            true_client_ip(&headers([(header, VALID_IPV6)])).unwrap(),
643            VALID_IPV6.parse::<IpAddr>().unwrap()
644        );
645    }
646
647    #[test]
648    fn test_x_real_ip() {
649        let header = "x-real-ip";
650
651        assert_eq!(
652            x_real_ip(&headers([])).unwrap_err(),
653            Error::AbsentHeader {
654                header_name: HeaderName::from_static(header)
655            }
656        );
657        assert_eq!(
658            x_real_ip(&headers([(header, "ы")])).unwrap_err(),
659            Error::NonAsciiHeaderValue {
660                header_name: HeaderName::from_static(header)
661            }
662        );
663        assert_eq!(
664            x_real_ip(&headers([(header, "foo")])).unwrap_err(),
665            Error::MalformedHeaderValue {
666                header_name: HeaderName::from_static(header),
667                header_value: "foo".into(),
668            }
669        );
670
671        assert_eq!(
672            x_real_ip(&headers([(header, VALID_IPV4)])).unwrap(),
673            VALID_IPV4.parse::<IpAddr>().unwrap()
674        );
675        assert_eq!(
676            x_real_ip(&headers([(header, VALID_IPV6)])).unwrap(),
677            VALID_IPV6.parse::<IpAddr>().unwrap()
678        );
679    }
680}