http_extract/
x_forwarded.rs1use std::net::{IpAddr, SocketAddr};
12
13use http::{HeaderMap, HeaderName, Request};
14
15use crate::Error;
16
17pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
19
20pub const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
22
23pub fn extract_header_x_forwarded_for(headers: &HeaderMap) -> Result<Option<Vec<IpAddr>>, Error> {
34 extract_comma_values(headers, &X_FORWARDED_FOR, |value| {
35 parse_ip(value, X_FORWARDED_FOR)
36 })
37}
38
39pub fn extract_request_x_forwarded_for<B>(
45 request: &Request<B>,
46) -> Result<Option<Vec<IpAddr>>, Error> {
47 extract_header_x_forwarded_for(request.headers())
48}
49
50pub fn extract_header_x_forwarded_proto(headers: &HeaderMap) -> Result<Option<Vec<String>>, Error> {
58 extract_comma_values(headers, &X_FORWARDED_PROTO, |value| {
59 if is_scheme(value) {
60 Ok(value.to_ascii_lowercase())
61 } else {
62 Err(Error::invalid_header(X_FORWARDED_PROTO))
63 }
64 })
65}
66
67pub fn extract_request_x_forwarded_proto<B>(
74 request: &Request<B>,
75) -> Result<Option<Vec<String>>, Error> {
76 extract_header_x_forwarded_proto(request.headers())
77}
78
79pub fn extract_rightmost_x_forwarded_for(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
85 Ok(extract_header_x_forwarded_for(headers)?.and_then(|ips| ips.last().copied()))
86}
87
88fn parse_ip(value: &str, name: HeaderName) -> Result<IpAddr, Error> {
89 if let Ok(address) = value.parse() {
90 return Ok(address);
91 }
92 if let Ok(address) = value.parse::<SocketAddr>() {
93 return Ok(address.ip());
94 }
95 if let Some(rest) = value.strip_prefix('[')
96 && let Some((inner, suffix)) = rest.split_once(']')
97 && (suffix.is_empty()
98 || suffix.strip_prefix(':').is_some_and(|port| {
99 !port.is_empty() && port.bytes().all(|byte| byte.is_ascii_digit())
100 }))
101 && let Ok(address) = inner.parse()
102 {
103 return Ok(address);
104 }
105 Err(Error::invalid_header(name))
106}
107
108fn extract_comma_values<T>(
109 headers: &HeaderMap,
110 name: &HeaderName,
111 mut parse: impl FnMut(&str) -> Result<T, Error>,
112) -> Result<Option<Vec<T>>, Error> {
113 let mut output = Vec::new();
114 let mut present = false;
115 for value in headers.get_all(name) {
116 present = true;
117 let value = value
118 .to_str()
119 .map_err(|_| Error::invalid_header(name.clone()))?;
120 for item in value.split(',') {
121 let item = item.trim();
122 if item.is_empty() {
123 return Err(Error::invalid_header(name.clone()));
124 }
125 output.push(parse(item)?);
126 }
127 }
128 Ok(present.then_some(output))
129}
130
131fn is_scheme(value: &str) -> bool {
132 let mut bytes = value.bytes();
133 matches!(bytes.next(), Some(byte) if byte.is_ascii_alphabetic())
134 && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
135}
136
137#[cfg(test)]
138mod tests {
139 use std::net::{IpAddr, Ipv4Addr};
140
141 use http::{HeaderMap, HeaderValue};
142
143 use super::*;
144
145 #[test]
146 fn extracts_x_forwarded_for_across_field_lines() {
147 let mut headers = HeaderMap::new();
148 headers.append(&X_FORWARDED_FOR, "192.0.2.1, 198.51.100.2".parse().unwrap());
149 headers.append(&X_FORWARDED_FOR, "203.0.113.3".parse().unwrap());
150
151 assert_eq!(
152 extract_header_x_forwarded_for(&headers).unwrap().unwrap(),
153 vec![
154 IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
155 IpAddr::V4(Ipv4Addr::new(198, 51, 100, 2)),
156 IpAddr::V4(Ipv4Addr::new(203, 0, 113, 3)),
157 ]
158 );
159 }
160
161 #[test]
162 fn rejects_invalid_x_forwarded_for_values() {
163 let mut headers = HeaderMap::new();
164 headers.insert(&X_FORWARDED_FOR, "[2001:db8::1]junk".parse().unwrap());
165 assert!(matches!(
166 extract_header_x_forwarded_for(&headers),
167 Err(Error::InvalidHeader { .. })
168 ));
169
170 headers.insert(&X_FORWARDED_FOR, HeaderValue::from_bytes(&[0xff]).unwrap());
171 assert!(matches!(
172 extract_header_x_forwarded_for(&headers),
173 Err(Error::InvalidHeader { .. })
174 ));
175 }
176
177 #[test]
178 fn extracts_and_normalizes_x_forwarded_proto() {
179 let mut headers = HeaderMap::new();
180 assert_eq!(extract_header_x_forwarded_proto(&headers).unwrap(), None);
181
182 headers.append(&X_FORWARDED_PROTO, "HTTPS, Web+TLS".parse().unwrap());
183 assert_eq!(
184 extract_header_x_forwarded_proto(&headers).unwrap().unwrap(),
185 vec!["https".to_owned(), "web+tls".to_owned()]
186 );
187
188 headers.insert(&X_FORWARDED_PROTO, "http_2".parse().unwrap());
189 assert!(matches!(
190 extract_header_x_forwarded_proto(&headers),
191 Err(Error::InvalidHeader { .. })
192 ));
193 }
194
195 #[test]
196 fn request_entry_points_delegate_to_headers() {
197 let request = Request::builder()
198 .header(&X_FORWARDED_FOR, "192.0.2.1")
199 .header(&X_FORWARDED_PROTO, "HTTPS")
200 .body(())
201 .unwrap();
202 assert_eq!(
203 extract_request_x_forwarded_for(&request).unwrap(),
204 Some(vec!["192.0.2.1".parse().unwrap()])
205 );
206 assert_eq!(
207 extract_request_x_forwarded_proto(&request).unwrap(),
208 Some(vec!["https".to_owned()])
209 );
210 }
211}