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");
8const CREDENTIAL_HEADERS: [&str; 3] = ["authorization", "proxy-authorization", "cookie"];
10
11#[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 #[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 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 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 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
116fn 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}