Skip to main content

sofie_http/
client.rs

1use std::net::IpAddr;
2
3use http::{HeaderMap, HeaderName};
4use sofie_core::{Error, Result};
5
6const FORWARDED_FOR: &str = "x-forwarded-for";
7const REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
8/// A misnamed header would put a credential in every log line.
9const CREDENTIAL_HEADERS: [&str; 3] = ["authorization", "proxy-authorization", "cookie"];
10
11/// Nothing is trusted by default, so a client cannot name its own address.
12#[derive(Clone, Debug)]
13pub struct TrustedProxies {
14    networks: Vec<Network>,
15    request_id_header: HeaderName,
16}
17
18impl Default for TrustedProxies {
19    fn default() -> Self {
20        Self {
21            networks: Vec::new(),
22            request_id_header: REQUEST_ID,
23        }
24    }
25}
26
27#[derive(Clone, Copy, Debug)]
28struct Network {
29    address: IpAddr,
30    prefix: u8,
31}
32
33impl crate::Serving {
34    /// `sofie::run` reads them from `SOFIE_TRUSTED_PROXIES`.
35    #[must_use]
36    pub fn trusted_proxies(mut self, proxies: TrustedProxies) -> Self {
37        self.proxies = proxies;
38        self
39    }
40}
41
42impl TrustedProxies {
43    pub fn parse(text: &str) -> Result<Self> {
44        let networks = text
45            .split(',')
46            .map(str::trim)
47            .filter(|entry| !entry.is_empty())
48            .map(Network::parse)
49            .collect::<Result<_>>()?;
50        Ok(Self {
51            networks,
52            ..Self::default()
53        })
54    }
55
56    /// The header the proxies name their own request id in; `x-request-id` unless set.
57    pub fn request_id_header(mut self, name: &str) -> Result<Self> {
58        let header = HeaderName::try_from(name.trim()).map_err(|error| {
59            Error::internal(
60                format!("request id header `{name}` is not a header name"),
61                error,
62            )
63        })?;
64        if CREDENTIAL_HEADERS.contains(&header.as_str()) {
65            return Err(Error::internal_message(format!(
66                "request id header `{header}` carries credentials"
67            )));
68        }
69        self.request_id_header = header;
70        Ok(self)
71    }
72
73    fn trusts(&self, address: IpAddr) -> bool {
74        self.networks
75            .iter()
76            .any(|network| network.contains(address))
77    }
78
79    /// `X-Forwarded-For` is read from the right; an unparsable hop ends the walk (it may be forged).
80    pub(crate) fn client_of(&self, peer: IpAddr, headers: &HeaderMap) -> IpAddr {
81        let mut client = peer.to_canonical();
82        if !self.trusts(client) {
83            return client;
84        }
85        for hop in forwarded_hops(headers).rev() {
86            let Some(hop) = parsed_hop(hop) else {
87                return client;
88            };
89            client = hop;
90            if !self.trusts(hop) {
91                return hop;
92            }
93        }
94        client
95    }
96
97    pub(crate) fn is_empty(&self) -> bool {
98        self.networks.is_empty()
99    }
100
101    pub(crate) fn is_trusted(&self, peer: IpAddr) -> bool {
102        self.trusts(peer.to_canonical())
103    }
104
105    /// The header naming the edge's request id comes only from a trusted peer.
106    pub(crate) fn client(
107        &self,
108        peer: IpAddr,
109        headers: &HeaderMap,
110    ) -> (IpAddr, Option<&HeaderName>) {
111        let edge_id_header = self.is_trusted(peer).then_some(&self.request_id_header);
112        (self.client_of(peer, headers), edge_id_header)
113    }
114}
115
116/// Split as bytes, so a byte that is not text, which a client can send, spoils only its own hop.
117fn forwarded_hops(headers: &HeaderMap) -> impl DoubleEndedIterator<Item = &[u8]> {
118    headers
119        .get_all(FORWARDED_FOR)
120        .into_iter()
121        .flat_map(|line| line.as_bytes().split(|byte| *byte == b','))
122}
123
124fn parsed_hop(hop: &[u8]) -> Option<IpAddr> {
125    let hop = std::str::from_utf8(hop).ok()?.trim();
126    hop.parse::<IpAddr>().ok().map(|hop| hop.to_canonical())
127}
128
129impl Network {
130    fn parse(entry: &str) -> Result<Self> {
131        let refused = || {
132            Error::internal_message(format!(
133                "trusted proxy `{entry}` is not an address or a range such as 10.0.0.0/8"
134            ))
135        };
136        let (address, prefix) = entry.split_once('/').unwrap_or((entry, ""));
137        let address: IpAddr = address.parse().map_err(|_| refused())?;
138        let address = address.to_canonical();
139        let bits = bit_length(address);
140        let prefix = match prefix {
141            "" => bits,
142            prefix => prefix.parse().map_err(|_| refused())?,
143        };
144        if prefix > bits {
145            return Err(refused());
146        }
147        Ok(Self { address, prefix })
148    }
149
150    fn contains(self, address: IpAddr) -> bool {
151        match (self.address, address) {
152            (IpAddr::V4(network), IpAddr::V4(address)) => same_prefix(
153                network.to_bits().into(),
154                address.to_bits().into(),
155                32,
156                self.prefix,
157            ),
158            (IpAddr::V6(network), IpAddr::V6(address)) => {
159                same_prefix(network.to_bits(), address.to_bits(), 128, self.prefix)
160            }
161            _ => false,
162        }
163    }
164}
165
166fn bit_length(address: IpAddr) -> u8 {
167    match address {
168        IpAddr::V4(_) => 32,
169        IpAddr::V6(_) => 128,
170    }
171}
172
173fn same_prefix(network: u128, address: u128, bits: u8, prefix: u8) -> bool {
174    let ignored = u32::from(bits - prefix);
175    network.checked_shr(ignored).unwrap_or(0) == address.checked_shr(ignored).unwrap_or(0)
176}
177
178#[cfg(test)]
179mod tests {
180    use http::HeaderValue;
181
182    use super::*;
183
184    fn ip(text: &str) -> IpAddr {
185        text.parse().unwrap()
186    }
187
188    fn forwarded(lines: &[&'static str]) -> HeaderMap {
189        let mut headers = HeaderMap::new();
190        for line in lines {
191            headers.append(FORWARDED_FOR, HeaderValue::from_static(line));
192        }
193        headers
194    }
195
196    fn client(trusted: &str, peer: &str, lines: &[&'static str]) -> IpAddr {
197        TrustedProxies::parse(trusted)
198            .unwrap()
199            .client_of(ip(peer), &forwarded(lines))
200    }
201
202    #[test]
203    fn an_untrusted_peer_is_the_client_whatever_it_forwards() {
204        assert_eq!(
205            client("", "203.0.113.9", &["198.51.100.1"]),
206            ip("203.0.113.9")
207        );
208        assert_eq!(
209            client("10.0.0.0/8", "203.0.113.9", &["198.51.100.1"]),
210            ip("203.0.113.9")
211        );
212    }
213
214    #[test]
215    fn behind_trusted_proxies_the_rightmost_untrusted_hop_is_the_client() {
216        let lines = ["198.51.100.66, 203.0.113.5", "10.1.2.3"];
217
218        assert_eq!(client("10.0.0.0/8", "10.0.0.1", &lines), ip("203.0.113.5"));
219        assert_eq!(
220            client("10.0.0.0/8, 203.0.113.5", "10.0.0.1", &lines),
221            ip("198.51.100.66")
222        );
223    }
224
225    #[test]
226    fn a_hop_that_does_not_parse_ends_the_walk_at_the_last_trusted_one() {
227        assert_eq!(
228            client(
229                "10.0.0.0/8",
230                "10.0.0.1",
231                &["203.0.113.5, unknown, 10.0.0.2"]
232            ),
233            ip("10.0.0.2")
234        );
235        assert_eq!(client("10.0.0.0/8", "10.0.0.1", &[]), ip("10.0.0.1"));
236    }
237
238    #[test]
239    fn a_hop_that_is_not_text_spoils_only_itself() {
240        let mut headers = HeaderMap::new();
241        let line = HeaderValue::from_bytes(b"\xff, 203.0.113.5").unwrap();
242        headers.append(FORWARDED_FOR, line);
243        let proxies = TrustedProxies::parse("10.0.0.0/8").unwrap();
244
245        assert_eq!(
246            proxies.client_of(ip("10.0.0.1"), &headers),
247            ip("203.0.113.5")
248        );
249    }
250
251    #[test]
252    fn ranges_match_by_prefix_in_either_family() {
253        let proxies = TrustedProxies::parse("192.0.2.0/24, 2001:db8::/32, 198.51.100.7").unwrap();
254
255        assert!(proxies.is_trusted(ip("192.0.2.200")));
256        assert!(proxies.is_trusted(ip("::ffff:192.0.2.1")));
257        assert!(proxies.is_trusted(ip("2001:db8:1::1")));
258        assert!(proxies.is_trusted(ip("198.51.100.7")));
259        assert!(!proxies.is_trusted(ip("192.0.3.1")));
260        assert!(!proxies.is_trusted(ip("198.51.100.8")));
261        assert!(
262            TrustedProxies::parse("0.0.0.0/0")
263                .unwrap()
264                .is_trusted(ip("8.8.8.8"))
265        );
266    }
267
268    #[test]
269    fn a_malformed_entry_is_refused_naming_it() {
270        for entry in ["10.0.0.0/33", "proxy.test", "10.0.0.0/x", "::/129"] {
271            let error = TrustedProxies::parse(entry).unwrap_err();
272            assert!(error.to_string().contains(entry), "{error}");
273        }
274    }
275}