Skip to main content

rama_http_headers/forwarded/
exotic_forward_ip.rs

1use rama_core::error::BoxErrorExt as _;
2use rama_core::error::ErrorExt;
3use rama_core::error::{BoxError, ErrorContext};
4use rama_http_types::header::{
5    CF_CONNECTING_IP, CLIENT_IP, TRUE_CLIENT_IP, X_CLIENT_IP, X_REAL_IP,
6};
7use rama_http_types::{HeaderName, HeaderValue};
8use rama_macros::paste;
9use rama_net::forwarded::{ForwardedElement, NodeId};
10use std::fmt;
11use std::net::{IpAddr, Ipv6Addr};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14struct ClientAddr {
15    ip: IpAddr,
16    port: Option<u16>,
17}
18
19impl fmt::Display for ClientAddr {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match &self.port {
22            Some(port) => match &self.ip {
23                IpAddr::V6(ip) => write!(f, "[{ip}]:{port}"),
24                IpAddr::V4(ip) => write!(f, "{ip}:{port}"),
25            },
26            None => self.ip.fmt(f),
27        }
28    }
29}
30
31impl std::str::FromStr for ClientAddr {
32    type Err = BoxError;
33
34    fn from_str(s: &str) -> Result<Self, Self::Err> {
35        if let Ok(ip) = s.parse() {
36            // first try host alone, as it is most common,
37            // and also prevents IPv6 to be seen by default with port
38            return Ok(Self { ip, port: None });
39        }
40
41        let (s, port) = try_to_split_num_port_from_str(s);
42        let ip = try_to_parse_str_to_ip(s).context("parse forwarded ip")?;
43
44        match ip {
45            IpAddr::V6(_) if port.is_some() && !s.starts_with('[') => Err(
46                BoxError::from_static_str("missing brackets for IPv6 address with port")
47                    .context_field("ip", ip)
48                    .context_debug_field("port", port),
49            ),
50            _ => Ok(Self { ip, port }),
51        }
52    }
53}
54
55fn try_to_parse_str_to_ip(value: &str) -> Option<IpAddr> {
56    if value.starts_with('[') || value.ends_with(']') {
57        let value = value
58            .strip_prefix('[')
59            .and_then(|value| value.strip_suffix(']'))?;
60        Some(IpAddr::V6(value.parse::<Ipv6Addr>().ok()?))
61    } else {
62        value.parse::<IpAddr>().ok()
63    }
64}
65
66fn try_to_split_num_port_from_str(s: &str) -> (&str, Option<u16>) {
67    if let Some(colon) = s.as_bytes().iter().rposition(|c| *c == b':') {
68        match s[colon + 1..].parse() {
69            Ok(port) => (&s[..colon], Some(port)),
70            Err(_) => (s, None),
71        }
72    } else {
73        (s, None)
74    }
75}
76
77macro_rules! exotic_forward_ip_headers {
78    (
79        $(
80            #[doc = $desc:literal]
81            #[header = $header:ident]
82            $(#[$outer:meta])*
83            pub struct $name:ident;
84        )+
85    ) => {
86        $(
87            #[derive(Debug, Clone, PartialEq, Eq)]
88            #[doc = $desc]
89            $(#[$outer])*
90            pub struct $name(ClientAddr);
91
92            impl $crate::TypedHeader for $name {
93                fn name() -> &'static HeaderName {
94                    &$header
95                }
96            }
97
98            impl $crate::HeaderDecode for $name {
99                fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(
100                    values: &mut I,
101                ) -> Result<Self, crate::Error> {
102                    Ok($name(
103                        values
104                            .next()
105                            .and_then(|value| value.to_str().ok().and_then(|s| s.parse().ok()))
106                            .ok_or_else(crate::Error::invalid)?,
107                    ))
108                }
109            }
110
111            impl $crate::HeaderEncode for $name {
112                fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
113                    let s = self.0.to_string();
114                    values.extend(Some(HeaderValue::try_from(s).unwrap()))
115                }
116            }
117
118            impl super::ForwardHeader for $name {
119                fn try_from_forwarded<'a, I>(input: I) -> Option<Self>
120                where
121                    I: IntoIterator<Item = &'a ForwardedElement>,
122                {
123                    let node = input
124                        .into_iter()
125                        .next()?
126                        .forwarded_for()?;
127                    let ip = node.ip()?;
128                    let port = node.port();
129                    Some($name(ClientAddr { ip, port }))
130                }
131            }
132
133            paste! {
134                impl IntoIterator for $name {
135                    type Item = ForwardedElement;
136                    type IntoIter = [<$name Iterator>];
137
138                    fn into_iter(self) -> Self::IntoIter {
139                        [<$name Iterator>](Some(self.0))
140                    }
141                }
142
143                #[derive(Debug, Clone)]
144                #[doc = concat!("An iterator over the `", stringify!($name), "` header's elements.")]
145                pub struct [<$name Iterator>](Option<ClientAddr>);
146
147                impl Iterator for [<$name Iterator>] {
148                    type Item = ForwardedElement;
149
150                    fn next(&mut self) -> Option<Self::Item> {
151                        self.0.take().map(|addr| {
152                            let node: NodeId = (addr.ip, addr.port).into();
153                            ForwardedElement::new_forwarded_for(node)
154                        })
155                    }
156                }
157            }
158        )+
159    };
160}
161
162exotic_forward_ip_headers! {
163    #[doc = "CF-Connecting-IP provides the client IP address connecting to Cloudflare to the origin web server."]
164    #[header = CF_CONNECTING_IP]
165    pub struct CFConnectingIp;
166
167    #[doc = "True-Client-IP provides the original client IP address to the origin web server (Cloudflare Enterprise)."]
168    #[header = TRUE_CLIENT_IP]
169    pub struct TrueClientIp;
170
171    #[doc = "X-Real-Ip is used by some proxy software to set the real client Ip Address (known to them)."]
172    #[header = X_REAL_IP]
173    pub struct XRealIp;
174
175
176    #[doc = "Client-Ip is used by some proxy software to set the real client Ip Address (known to them)."]
177    #[header = CLIENT_IP]
178    pub struct ClientIp;
179
180    #[doc = "X-Client-Ip is used by some proxy software to set the real client Ip Address (known to them)."]
181    #[header = X_CLIENT_IP]
182    pub struct XClientIp;
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use crate::{HeaderDecode, HeaderEncode};
189
190    macro_rules! test_headers {
191        ($($ty: ident),+ ; $name: ident, $input: expr, $expected: literal) => {
192            #[test]
193            fn $name() {
194                $(
195                    assert_eq!(
196                        $ty::decode(
197                            &mut $input
198                                .into_iter()
199                                .map(|s| HeaderValue::from_bytes(s.as_bytes()).unwrap())
200                                .collect::<Vec<_>>()
201                                .iter()
202                        )
203                        .unwrap(),
204                        $ty($expected.parse().unwrap()),
205                    );
206                )+
207            }
208        };
209    }
210
211    macro_rules! test_header {
212        ($name: ident, $input: expr, $expected: literal) => {
213            test_headers!(CFConnectingIp, TrueClientIp, XRealIp, ClientIp, XClientIp; $name, $input, $expected);
214        };
215    }
216
217    // Tests from the Docs
218    test_header!(test1, vec!["203.0.113.195"], "203.0.113.195");
219    test_header!(test2, vec!["203.0.113.195:80"], "203.0.113.195:80");
220    test_header!(
221        test3,
222        vec!["2001:db8:85a3:8d3:1319:8a2e:370:7348"],
223        "2001:db8:85a3:8d3:1319:8a2e:370:7348"
224    );
225    test_header!(
226        test4,
227        vec!["[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080"],
228        "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080"
229    );
230
231    macro_rules! symmetric_test_header {
232        ($name: ident) => {
233            for input in [
234                $name("127.0.0.1:8080".parse().unwrap()),
235                $name(
236                    "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080"
237                        .parse()
238                        .unwrap(),
239                ),
240                $name("203.0.113.195".parse().unwrap()),
241                $name("203.0.113.195:80".parse().unwrap()),
242            ] {
243                let mut values = Vec::new();
244                input.encode(&mut values);
245                assert_eq!($name::decode(&mut values.iter()).unwrap(), input);
246            }
247        };
248    }
249
250    #[test]
251    fn test_symmetry_encode() {
252        symmetric_test_header!(CFConnectingIp);
253        symmetric_test_header!(TrueClientIp);
254        symmetric_test_header!(XRealIp);
255        symmetric_test_header!(ClientIp);
256        symmetric_test_header!(XClientIp);
257    }
258}