http_extract/
forwarded.rs1use std::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 forwarded_for = None;
83 for parameter in split_quoted(element, ';')? {
84 let parameter = trim_ows(parameter);
85 if parameter.is_empty() {
86 return Err(invalid());
87 }
88 let Some((name, value)) = parameter.split_once('=') else {
89 continue;
90 };
91 if !trim_ows(name).eq_ignore_ascii_case("for") {
92 continue;
93 }
94 if forwarded_for.is_some() {
95 return Err(invalid());
96 }
97
98 let (value, quoted) = parse_parameter_value(value)?;
99 forwarded_for = Some(parse_forwarded_node(&value, quoted)?);
100 }
101
102 forwarded_for.ok_or_else(invalid)
103}
104
105fn parse_forwarded_node(value: &str, quoted: bool) -> Result<IpAddr, Error> {
106 if !quoted {
107 return value
108 .parse::<std::net::Ipv4Addr>()
109 .map(IpAddr::V4)
110 .map_err(|_| invalid());
111 }
112
113 if let Some(rest) = value.strip_prefix('[') {
114 let (address, suffix) = rest.split_once(']').ok_or_else(invalid)?;
115 if !suffix.is_empty() {
116 let port = suffix.strip_prefix(':').ok_or_else(invalid)?;
117 validate_node_port(port)?;
118 }
119 return address
120 .parse::<std::net::Ipv6Addr>()
121 .map(IpAddr::V6)
122 .map_err(|_| invalid());
123 }
124
125 let (address, port) = value
126 .split_once(':')
127 .map_or((value, None), |(address, port)| (address, Some(port)));
128 if let Some(port) = port {
129 validate_node_port(port)?;
130 }
131 address
132 .parse::<std::net::Ipv4Addr>()
133 .map(IpAddr::V4)
134 .map_err(|_| invalid())
135}
136
137fn validate_node_port(value: &str) -> Result<(), Error> {
138 let numeric =
139 !value.is_empty() && value.len() <= 5 && value.bytes().all(|byte| byte.is_ascii_digit());
140 let obfuscated =
141 value.len() > 1 && value.starts_with('_') && value.bytes().all(is_obfuscated_byte);
142 if numeric || obfuscated {
143 Ok(())
144 } else {
145 Err(invalid())
146 }
147}
148
149fn split_quoted(value: &str, delimiter: char) -> Result<Vec<&str>, Error> {
150 let mut output = Vec::new();
151 let mut quoted = false;
152 let mut escaped = false;
153 let mut start = 0;
154
155 for (index, character) in value.char_indices() {
156 if escaped {
157 escaped = false;
158 } else if quoted && character == '\\' {
159 escaped = true;
160 } else if character == '"' {
161 quoted = !quoted;
162 } else if !quoted && character == delimiter {
163 output.push(&value[start..index]);
164 start = index + character.len_utf8();
165 }
166 }
167 if quoted || escaped {
168 return Err(invalid());
169 }
170 output.push(&value[start..]);
171 Ok(output)
172}
173
174fn parse_parameter_value(value: &str) -> Result<(String, bool), Error> {
175 let value = trim_ows(value);
176 if !value.starts_with('"') {
177 if value.is_empty() || !value.bytes().all(is_token_byte) {
178 return Err(invalid());
179 }
180 return Ok((value.to_owned(), false));
181 }
182 if value.len() < 2 || !value.ends_with('"') {
183 return Err(invalid());
184 }
185
186 let mut output = String::with_capacity(value.len() - 2);
187 let mut escaped = false;
188 for character in value[1..value.len() - 1].chars() {
189 if escaped {
190 if !is_quoted_pair_character(character) {
191 return Err(invalid());
192 }
193 output.push(character);
194 escaped = false;
195 } else if character == '\\' {
196 escaped = true;
197 } else if is_quoted_text_character(character) {
198 output.push(character);
199 } else {
200 return Err(invalid());
201 }
202 }
203 if escaped {
204 return Err(invalid());
205 }
206 Ok((output, true))
207}
208
209fn trim_ows(value: &str) -> &str {
210 value.trim_matches([' ', '\t'])
211}
212
213const fn is_token_byte(byte: u8) -> bool {
214 byte.is_ascii_alphanumeric()
215 || matches!(
216 byte,
217 b'!' | b'#'
218 | b'$'
219 | b'%'
220 | b'&'
221 | b'\''
222 | b'*'
223 | b'+'
224 | b'-'
225 | b'.'
226 | b'^'
227 | b'_'
228 | b'`'
229 | b'|'
230 | b'~'
231 )
232}
233
234const fn is_obfuscated_byte(byte: u8) -> bool {
235 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')
236}
237
238const fn is_quoted_text_character(character: char) -> bool {
239 matches!(character, '\t' | ' ' | '!' | '#'..='[' | ']'..='~')
240}
241
242const fn is_quoted_pair_character(character: char) -> bool {
243 matches!(character, '\t' | ' '..='~')
244}
245
246const fn invalid() -> Error {
247 Error::invalid_header(FORWARDED)
248}
249
250#[cfg(test)]
251mod tests {
252 use std::net::IpAddr;
253
254 use http::{HeaderMap, HeaderValue};
255
256 use super::*;
257
258 #[test]
259 fn returns_none_when_forwarded_is_absent() {
260 assert_eq!(
261 extract_header_forwarded_for(&HeaderMap::new()).unwrap(),
262 None
263 );
264 }
265
266 #[test]
267 fn extracts_every_for_ip_in_wire_order() {
268 let mut headers = HeaderMap::new();
269 headers.insert(
270 &FORWARDED,
271 "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\""
272 .parse()
273 .unwrap(),
274 );
275
276 assert_eq!(
277 extract_header_forwarded_for(&headers).unwrap().unwrap(),
278 vec![
279 "192.0.2.60".parse::<IpAddr>().unwrap(),
280 "2001:db8:cafe::17".parse::<IpAddr>().unwrap(),
281 "198.51.100.4".parse::<IpAddr>().unwrap(),
282 ]
283 );
284 }
285
286 #[test]
287 fn extracts_for_ip_without_parsing_other_parameters() {
288 let mut headers = HeaderMap::new();
289 headers.insert(
290 &FORWARDED,
291 "for=52.159.243.17;host=example.vercel.app;proto=https;sig=c2lnbmF0dXJlCg==;exp=1786417338"
292 .parse()
293 .unwrap(),
294 );
295
296 assert_eq!(
297 extract_header_forwarded_for(&headers).unwrap().unwrap(),
298 vec!["52.159.243.17".parse::<IpAddr>().unwrap()]
299 );
300 }
301
302 #[test]
303 fn ignores_malformed_parameters_other_than_for() {
304 for parameter in ["broken", "=value", "bad name=value", "proto="] {
305 let mut headers = HeaderMap::new();
306 headers.insert(
307 &FORWARDED,
308 format!("for=192.0.2.1;{parameter}").parse().unwrap(),
309 );
310
311 assert_eq!(
312 extract_header_forwarded_for(&headers).unwrap().unwrap(),
313 vec!["192.0.2.1".parse::<IpAddr>().unwrap()],
314 "unexpectedly rejected {parameter:?}"
315 );
316 }
317 }
318
319 #[test]
320 fn request_entry_point_delegates_to_headers() {
321 let request = Request::builder()
322 .header(&FORWARDED, "for=192.0.2.1")
323 .body(())
324 .unwrap();
325 assert_eq!(
326 extract_request_forwarded_for(&request).unwrap(),
327 Some(vec!["192.0.2.1".parse().unwrap()])
328 );
329 }
330
331 #[test]
332 fn rejects_a_duplicate_or_non_text_field() {
333 let mut headers = HeaderMap::new();
334 headers.append(&FORWARDED, "for=192.0.2.1".parse().unwrap());
335 headers.append(&FORWARDED, "for=198.51.100.2".parse().unwrap());
336 assert!(matches!(
337 extract_header_forwarded_for(&headers),
338 Err(Error::DuplicateHeader { .. })
339 ));
340
341 headers.clear();
342 headers.insert(&FORWARDED, HeaderValue::from_bytes(&[0xff]).unwrap());
343 assert!(matches!(
344 extract_header_forwarded_for(&headers),
345 Err(Error::InvalidHeader { .. })
346 ));
347 }
348
349 #[test]
350 fn rejects_any_element_that_cannot_extend_a_continuous_ip_chain() {
351 for value in [
352 "",
353 "for=192.0.2.1,",
354 "for=192.0.2.1,,for=198.51.100.2",
355 "proto=https",
356 "for=unknown",
357 "for=_hidden",
358 "for=example.com",
359 "for=192.0.2.1:443",
360 "for=\"[not-an-ip]\"",
361 "for=192.0.2.1;for=198.51.100.2",
362 "for=192.0.2.1;proto=\"unterminated",
363 ] {
364 let mut headers = HeaderMap::new();
365 headers.insert(&FORWARDED, value.parse().unwrap());
366 assert!(
367 matches!(
368 extract_header_forwarded_for(&headers),
369 Err(Error::InvalidHeader { .. })
370 ),
371 "unexpectedly accepted {value:?}"
372 );
373 }
374 }
375}