Skip to main content

rama_http_headers/forwarded/
x_forwarded_host.rs

1use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
2use rama_core::telemetry::tracing;
3use rama_http_types::header;
4use rama_http_types::{HeaderName, HeaderValue};
5use rama_net::address::Host;
6use rama_net::forwarded::{ForwardedAuthority, ForwardedElement};
7
8/// The X-Forwarded-Host (XFH) header is a de-facto standard header for identifying the
9/// original host requested by the client in the Host HTTP request header.
10///
11/// It is recommended to use the [`Forwarded`](super::Forwarded) header instead if you can.
12///
13/// More info can be found at <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host>.
14///
15/// # Syntax
16///
17/// ```text
18/// X-Forwarded-Host: <host>
19/// ```
20///
21/// # Example values
22///
23/// * `id42.example-cdn.com`
24/// * `id42.example-cdn.com:443`
25/// * `203.0.113.195`
26/// * `203.0.113.195:80`
27/// * `2001:db8:85a3:8d3:1319:8a2e:370:7348`
28/// * `[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080`
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct XForwardedHost(ForwardedAuthority);
31
32impl TypedHeader for XForwardedHost {
33    fn name() -> &'static HeaderName {
34        &header::X_FORWARDED_HOST
35    }
36}
37
38impl HeaderDecode for XForwardedHost {
39    fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
40        Ok(Self(
41            values
42                .next()
43                .and_then(|value| value.to_str().ok().and_then(|s| s.parse().ok()))
44                .ok_or_else(Error::invalid)?,
45        ))
46    }
47}
48
49impl HeaderEncode for XForwardedHost {
50    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
51        let s = self.0.to_string();
52        match HeaderValue::try_from(s) {
53            Ok(value) => values.extend(::std::iter::once(value)),
54            Err(err) => {
55                tracing::debug!("failed to encode x-forwarded-host as header value: {err}")
56            }
57        }
58    }
59}
60
61impl XForwardedHost {
62    #[inline]
63    /// Get a reference to the [`Host`] of this [`XForwardedHost`].
64    #[must_use]
65    pub fn host(&self) -> &Host {
66        &self.0.0.host
67    }
68
69    #[inline]
70    /// Get a copy of the `port` of this [`XForwardedHost`] if it is set.
71    /// Empty (`:` with no digits) maps to `None` along with truly absent.
72    #[must_use]
73    pub fn port(&self) -> Option<u16> {
74        self.0.0.port.as_u16()
75    }
76
77    /// Return a reference to the inner data of this header.
78    #[must_use]
79    pub fn inner(&self) -> &ForwardedAuthority {
80        &self.0
81    }
82
83    /// Consume this header into its inner data.
84    #[must_use]
85    pub fn into_inner(self) -> ForwardedAuthority {
86        self.0
87    }
88}
89
90impl IntoIterator for XForwardedHost {
91    type Item = ForwardedElement;
92    type IntoIter = XForwardedHostIterator;
93
94    fn into_iter(self) -> Self::IntoIter {
95        XForwardedHostIterator(Some(self.0))
96    }
97}
98
99impl super::ForwardHeader for XForwardedHost {
100    fn try_from_forwarded<'a, I>(input: I) -> Option<Self>
101    where
102        I: IntoIterator<Item = &'a ForwardedElement>,
103    {
104        let el = input.into_iter().next()?;
105        let host = el.forwarded_host().cloned()?;
106        Some(Self(host))
107    }
108}
109
110#[derive(Debug, Clone)]
111/// An iterator over the `XForwardedHost` header's elements.
112pub struct XForwardedHostIterator(Option<ForwardedAuthority>);
113
114impl Iterator for XForwardedHostIterator {
115    type Item = ForwardedElement;
116
117    fn next(&mut self) -> Option<Self::Item> {
118        self.0.take().map(ForwardedElement::new_forwarded_host)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    use rama_http_types::HeaderValue;
127
128    macro_rules! test_header {
129        ($name: ident, $input: expr, $expected: expr) => {
130            #[test]
131            fn $name() {
132                assert_eq!(
133                    XForwardedHost::decode(
134                        &mut $input
135                            .into_iter()
136                            .map(|s| HeaderValue::from_bytes(s.as_bytes()).unwrap())
137                            .collect::<Vec<_>>()
138                            .iter()
139                    )
140                    .ok(),
141                    $expected,
142                );
143            }
144        };
145    }
146
147    // Tests from the Docs
148    test_header!(
149        test1,
150        vec!["id42.example-cdn.com"],
151        Some(XForwardedHost("id42.example-cdn.com".parse().unwrap()))
152    );
153    test_header!(
154        test2,
155        // 2nd one gets ignored
156        vec!["id42.example-cdn.com", "example.com"],
157        Some(XForwardedHost("id42.example-cdn.com".parse().unwrap()))
158    );
159    test_header!(
160        test3,
161        vec!["id42.example-cdn.com:443"],
162        Some(XForwardedHost("id42.example-cdn.com:443".parse().unwrap()))
163    );
164    test_header!(
165        test4,
166        vec!["203.0.113.195"],
167        Some(XForwardedHost("203.0.113.195".parse().unwrap()))
168    );
169    test_header!(
170        test5,
171        vec!["203.0.113.195:80"],
172        Some(XForwardedHost("203.0.113.195:80".parse().unwrap()))
173    );
174    test_header!(
175        test6,
176        vec!["2001:db8:85a3:8d3:1319:8a2e:370:7348"],
177        Some(XForwardedHost(
178            "2001:db8:85a3:8d3:1319:8a2e:370:7348".parse().unwrap()
179        ))
180    );
181    test_header!(
182        test7,
183        vec!["[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080"],
184        Some(XForwardedHost(
185            "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080"
186                .parse()
187                .unwrap()
188        ))
189    );
190
191    #[test]
192    fn test_x_forwarded_host_symmetry_encode() {
193        for input in [
194            XForwardedHost("id42.example-cdn.com".parse().unwrap()),
195            XForwardedHost("id42.example-cdn.com:443".parse().unwrap()),
196            XForwardedHost("127.0.0.1".parse().unwrap()),
197        ] {
198            let mut values = Vec::new();
199            input.encode(&mut values);
200            assert_eq!(XForwardedHost::decode(&mut values.iter()).unwrap(), input);
201        }
202    }
203}