http_extract/
forwarded.rs1use std::{collections::HashSet, net::IpAddr};
21
22use http::{HeaderMap, HeaderName, Request};
23
24use crate::{Error, header::extract_single_header_text};
25
26pub const FORWARDED: HeaderName = HeaderName::from_static("forwarded");
30
31pub fn extract_header_forwarded_for(headers: &HeaderMap) -> Result<Option<Vec<IpAddr>>, Error> {
45 extract_single_header_text(headers, &FORWARDED)?
46 .map(parse_forwarded_for)
47 .transpose()
48}
49
50pub fn extract_request_forwarded_for<B>(
56 request: &Request<B>,
57) -> Result<Option<Vec<IpAddr>>, Error> {
58 extract_header_forwarded_for(request.headers())
59}
60
61pub fn extract_rightmost_forwarded(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
67 Ok(extract_header_forwarded_for(headers)?.and_then(|ips| ips.last().copied()))
68}
69
70fn parse_forwarded_for(value: &str) -> Result<Vec<IpAddr>, Error> {
71 split_quoted(value, ',')?
72 .into_iter()
73 .map(parse_forwarded_element_for)
74 .collect()
75}
76
77fn parse_forwarded_element_for(element: &str) -> Result<IpAddr, Error> {
78 if trim_ows(element).is_empty() {
79 return Err(invalid());
80 }
81
82 let mut parameters = HashSet::new();
83 let mut forwarded_for = None;
84 for parameter in split_quoted(element, ';')? {
85 let parameter = trim_ows(parameter);
86 if parameter.is_empty() {
87 return Err(invalid());
88 }
89 let (name, value) = parameter.split_once('=').ok_or_else(invalid)?;
90 let name = trim_ows(name);
91 if name.is_empty() || !name.bytes().all(is_token_byte) {
92 return Err(invalid());
93 }
94
95 let name = name.to_ascii_lowercase();
96 if !parameters.insert(name.clone()) {
97 return Err(invalid());
98 }
99
100 let (value, quoted) = parse_parameter_value(value)?;
101 if name == "for" {
102 forwarded_for = Some(parse_forwarded_node(&value, quoted)?);
103 }
104 }
105
106 forwarded_for.ok_or_else(invalid)
107}
108
109fn parse_forwarded_node(value: &str, quoted: bool) -> Result<IpAddr, Error> {
110 if !quoted {
111 return value
112 .parse::<std::net::Ipv4Addr>()
113 .map(IpAddr::V4)
114 .map_err(|_| invalid());
115 }
116
117 if let Some(rest) = value.strip_prefix('[') {
118 let (address, suffix) = rest.split_once(']').ok_or_else(invalid)?;
119 if !suffix.is_empty() {
120 let port = suffix.strip_prefix(':').ok_or_else(invalid)?;
121 validate_node_port(port)?;
122 }
123 return address
124 .parse::<std::net::Ipv6Addr>()
125 .map(IpAddr::V6)
126 .map_err(|_| invalid());
127 }
128
129 let (address, port) = value
130 .split_once(':')
131 .map_or((value, None), |(address, port)| (address, Some(port)));
132 if let Some(port) = port {
133 validate_node_port(port)?;
134 }
135 address
136 .parse::<std::net::Ipv4Addr>()
137 .map(IpAddr::V4)
138 .map_err(|_| invalid())
139}
140
141fn validate_node_port(value: &str) -> Result<(), Error> {
142 let numeric =
143 !value.is_empty() && value.len() <= 5 && value.bytes().all(|byte| byte.is_ascii_digit());
144 let obfuscated =
145 value.len() > 1 && value.starts_with('_') && value.bytes().all(is_obfuscated_byte);
146 if numeric || obfuscated {
147 Ok(())
148 } else {
149 Err(invalid())
150 }
151}
152
153fn split_quoted(value: &str, delimiter: char) -> Result<Vec<&str>, Error> {
154 let mut output = Vec::new();
155 let mut quoted = false;
156 let mut escaped = false;
157 let mut start = 0;
158
159 for (index, character) in value.char_indices() {
160 if escaped {
161 escaped = false;
162 } else if quoted && character == '\\' {
163 escaped = true;
164 } else if character == '"' {
165 quoted = !quoted;
166 } else if !quoted && character == delimiter {
167 output.push(&value[start..index]);
168 start = index + character.len_utf8();
169 }
170 }
171 if quoted || escaped {
172 return Err(invalid());
173 }
174 output.push(&value[start..]);
175 Ok(output)
176}
177
178fn parse_parameter_value(value: &str) -> Result<(String, bool), Error> {
179 let value = trim_ows(value);
180 if !value.starts_with('"') {
181 if value.is_empty() || !value.bytes().all(is_token_byte) {
182 return Err(invalid());
183 }
184 return Ok((value.to_owned(), false));
185 }
186 if value.len() < 2 || !value.ends_with('"') {
187 return Err(invalid());
188 }
189
190 let mut output = String::with_capacity(value.len() - 2);
191 let mut escaped = false;
192 for character in value[1..value.len() - 1].chars() {
193 if escaped {
194 if !is_quoted_pair_character(character) {
195 return Err(invalid());
196 }
197 output.push(character);
198 escaped = false;
199 } else if character == '\\' {
200 escaped = true;
201 } else if is_quoted_text_character(character) {
202 output.push(character);
203 } else {
204 return Err(invalid());
205 }
206 }
207 if escaped {
208 return Err(invalid());
209 }
210 Ok((output, true))
211}
212
213fn trim_ows(value: &str) -> &str {
214 value.trim_matches([' ', '\t'])
215}
216
217const fn is_token_byte(byte: u8) -> bool {
218 byte.is_ascii_alphanumeric()
219 || matches!(
220 byte,
221 b'!' | b'#'
222 | b'$'
223 | b'%'
224 | b'&'
225 | b'\''
226 | b'*'
227 | b'+'
228 | b'-'
229 | b'.'
230 | b'^'
231 | b'_'
232 | b'`'
233 | b'|'
234 | b'~'
235 )
236}
237
238const fn is_obfuscated_byte(byte: u8) -> bool {
239 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')
240}
241
242const fn is_quoted_text_character(character: char) -> bool {
243 matches!(character, '\t' | ' ' | '!' | '#'..='[' | ']'..='~')
244}
245
246const fn is_quoted_pair_character(character: char) -> bool {
247 matches!(character, '\t' | ' '..='~')
248}
249
250const fn invalid() -> Error {
251 Error::invalid_header(FORWARDED)
252}
253
254#[cfg(test)]
255mod tests {
256 use std::net::IpAddr;
257
258 use http::{HeaderMap, HeaderValue};
259
260 use super::*;
261
262 #[test]
263 fn returns_none_when_forwarded_is_absent() {
264 assert_eq!(
265 extract_header_forwarded_for(&HeaderMap::new()).unwrap(),
266 None
267 );
268 }
269
270 #[test]
271 fn extracts_every_for_ip_in_wire_order() {
272 let mut headers = HeaderMap::new();
273 headers.insert(
274 &FORWARDED,
275 "for=192.0.2.60;proto=https, For=\"[2001:db8:cafe::17]:4711\";by=_edge, for=\"198.51.100.4:_port\";ext=\"a,b;c\""
276 .parse()
277 .unwrap(),
278 );
279
280 assert_eq!(
281 extract_header_forwarded_for(&headers).unwrap().unwrap(),
282 vec![
283 "192.0.2.60".parse::<IpAddr>().unwrap(),
284 "2001:db8:cafe::17".parse::<IpAddr>().unwrap(),
285 "198.51.100.4".parse::<IpAddr>().unwrap(),
286 ]
287 );
288 }
289
290 #[test]
291 fn request_entry_point_delegates_to_headers() {
292 let request = Request::builder()
293 .header(&FORWARDED, "for=192.0.2.1")
294 .body(())
295 .unwrap();
296 assert_eq!(
297 extract_request_forwarded_for(&request).unwrap(),
298 Some(vec!["192.0.2.1".parse().unwrap()])
299 );
300 }
301
302 #[test]
303 fn rejects_a_duplicate_or_non_text_field() {
304 let mut headers = HeaderMap::new();
305 headers.append(&FORWARDED, "for=192.0.2.1".parse().unwrap());
306 headers.append(&FORWARDED, "for=198.51.100.2".parse().unwrap());
307 assert!(matches!(
308 extract_header_forwarded_for(&headers),
309 Err(Error::DuplicateHeader { .. })
310 ));
311
312 headers.clear();
313 headers.insert(&FORWARDED, HeaderValue::from_bytes(&[0xff]).unwrap());
314 assert!(matches!(
315 extract_header_forwarded_for(&headers),
316 Err(Error::InvalidHeader { .. })
317 ));
318 }
319
320 #[test]
321 fn rejects_any_element_that_cannot_extend_a_continuous_ip_chain() {
322 for value in [
323 "",
324 "for=192.0.2.1,",
325 "for=192.0.2.1,,for=198.51.100.2",
326 "proto=https",
327 "for=unknown",
328 "for=_hidden",
329 "for=example.com",
330 "for=192.0.2.1:443",
331 "for=\"[not-an-ip]\"",
332 "for=192.0.2.1;for=198.51.100.2",
333 "for=192.0.2.1;broken",
334 "for=192.0.2.1;proto=\"unterminated",
335 ] {
336 let mut headers = HeaderMap::new();
337 headers.insert(&FORWARDED, value.parse().unwrap());
338 assert!(
339 matches!(
340 extract_header_forwarded_for(&headers),
341 Err(Error::InvalidHeader { .. })
342 ),
343 "unexpectedly accepted {value:?}"
344 );
345 }
346 }
347}