under/request/remote.rs
1use crate::HttpEntity;
2use std::borrow::Cow;
3use std::net::IpAddr;
4
5/// A type that helps retrieve the remote address of a request.
6///
7/// Because the process is incredibly complex, we can't just have a single
8/// function that returns a single IP address - the _source_ of that IP address
9/// is incredibly important, and we need to know what sources the application
10/// in question trusts.
11///
12/// The basic idea is this: there are multiple potential sources to load an IP
13/// address from. All of these potential sources must be derivable from the
14/// request itself. The application must then specify which sources it trusts
15/// to be able to load an IP address from. Those sources themselves can have
16/// their own configurations that allow them to be more fine-grained. We do
17/// not suggest a default - or, rather, disencourage it. Instead, we make it
18/// easier to specify.
19///
20/// For more information, see <https://adam-p.ca/blog/2022/03/x-forwarded-for/>.
21///
22/// # Examples
23/// ```rust
24/// # use under::*;
25/// # use std::net::IpAddr;
26/// # let mut request = Request::get("/").unwrap().with_local_addr();
27/// request.set_header("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3");
28/// let ip = request.remote_address()
29/// .trust_cloudflare_header()
30/// .trust_forwarded_for(-1)
31/// .trust_peer_address()
32/// .apply();
33/// assert_eq!(ip, Some(IpAddr::from([3, 3, 3, 3])));
34/// ```
35#[derive(Debug, Clone)]
36pub struct RemoteAddress<'r> {
37 /// The request itself. We borrow the request in here so that in the end
38 /// we can just simply have the application pull the IP address from here.
39 request: &'r super::Request,
40 /// The sources that are trusted to load an IP address from.
41 trusted_sources: Vec<RemoteAddressSource>,
42}
43
44impl<'r> RemoteAddress<'r> {
45 pub(crate) fn new(request: &'r super::Request) -> Self {
46 Self {
47 request,
48 trusted_sources: vec![],
49 }
50 }
51}
52
53impl RemoteAddress<'_> {
54 /// Adds a source that loads from the X-Forwarded-For header. The index
55 /// here specifies _which_ entry in the X-Forwarded-For header to use.
56 /// This is useful for load balancing applications that use multiple
57 /// load balancers, or have multiple proxies between the application or
58 /// the user. Ideally, the application would specify the right-most
59 /// specific source, not including the load balancers.
60 ///
61 /// This source correctly parses multiple `X-Forwarded-For` headers,
62 /// implicitly concatenating them.
63 ///
64 /// # Examples
65 /// ```rust
66 /// # use under::*;
67 /// # use std::net::IpAddr;
68 /// # let mut request = Request::get("/").unwrap();
69 /// request.set_header("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3");
70 /// let ip = request.remote_address()
71 /// .trust_forwarded_for(0)
72 /// .apply();
73 /// assert_eq!(ip, Some(IpAddr::from([1, 1, 1, 1])));
74 /// let ip = request.remote_address()
75 /// .trust_forwarded_for(-1)
76 /// .apply();
77 /// assert_eq!(ip, Some(IpAddr::from([3, 3, 3, 3])));
78 /// ```
79 pub fn trust_forwarded_for(&mut self, index: isize) -> &mut Self {
80 self.trusted_sources
81 .push(RemoteAddressSource::XForwardedFor(index));
82 self
83 }
84
85 /// Adds a source that loads from the Forwarded header. The index here
86 /// specifies _which_ entry in the Forwarded header to use. This is
87 /// useful for load balancing applications that use multiple load
88 /// balancers, or have multiple proxies between the application or the
89 /// user. Ideally, the application would specify the right-most
90 /// specific source, not including the load balancers.
91 ///
92 /// This source correctly parses multiple `Forwarded` headers,
93 /// implicitly concatenating them.
94 ///
95 /// # Examples
96 /// ```rust
97 /// # use under::*;
98 /// # use std::net::IpAddr;
99 /// # let mut request = Request::get("/").unwrap();
100 /// request.set_header("Forwarded", "for=1.1.1.1, for=2.2.2.2, for=3.3.3.3");
101 /// let ip = request.remote_address()
102 /// .trust_forwarded(0)
103 /// .apply();
104 /// assert_eq!(ip, Some(IpAddr::from([1, 1, 1, 1])));
105 /// let ip = request.remote_address()
106 /// .trust_forwarded(-1)
107 /// .apply();
108 /// assert_eq!(ip, Some(IpAddr::from([3, 3, 3, 3])));
109 /// ```
110 pub fn trust_forwarded(&mut self, index: isize) -> &mut Self {
111 self.trusted_sources
112 .push(RemoteAddressSource::Forwarded(index));
113 self
114 }
115
116 /// Adds a source that loads from a specific header. This is useful for
117 /// load balancing applications where the load balancer adds a header to
118 /// the request that contains the IP address of the client. Note that,
119 /// however, if the load balancer is not trusted, or that the application
120 /// can ever be accessed without the load balancer, this will not work.
121 /// This source iterates over all possible header values, from top to
122 /// bottom, and returns the first one that parses as an IP address.
123 ///
124 /// # Examples
125 /// ```rust
126 /// # use under::*;
127 /// # use std::net::IpAddr;
128 /// # let mut request = Request::get("/").unwrap();
129 /// request.set_header("X-Real-IP", "1.1.1.1");
130 /// request.add_header("X-Real-IP", "2.2.2.2");
131 /// let ip = request.remote_address().trust_header("X-Real-IP").apply();
132 /// assert_eq!(ip, Some(IpAddr::from([1, 1, 1, 1])));
133 /// ```
134 pub fn trust_header(&mut self, header: impl Into<Cow<'static, str>>) -> &mut Self {
135 self.trusted_sources
136 .push(RemoteAddressSource::Header(header.into()));
137 self
138 }
139
140 /// Adds a source that loads from the header `"CF-Connecting-IP"`. This
141 /// uses the same logic as [`Self::trust_header`] to parse the header.
142 pub fn trust_cloudflare_header(&mut self) -> &mut Self {
143 self.trust_header("CF-Connecting-IP")
144 }
145
146 /// Adds a source that loads from the header `"X-Real-IP"`. This uses
147 /// the same logic as [`Self::trust_header`] to parse the header.
148 pub fn trust_real_ip_header(&mut self) -> &mut Self {
149 self.trust_header("X-Real-IP")
150 }
151
152 /// Adds a source that loads from the header `"True-Client-IP"`. This
153 /// uses the same logic as [`Self::trust_header`] to parse the header.
154 pub fn trust_client_ip_header(&mut self) -> &mut Self {
155 self.trust_header("True-Client-IP")
156 }
157
158 /// Adds a source that loads from the peer address of the TCP connection.
159 /// There generally will always be a peer address, but if the application
160 /// is behind a reverse proxy, this peer address will be the address of
161 /// the reverse proxy, instead of the user's machine. As such, this is
162 /// most likely not what you want.
163 ///
164 /// # Examples
165 /// ```rust
166 /// # use under::*;
167 /// # use std::net::IpAddr;
168 /// # let mut request = Request::get("/").unwrap().with_local_addr();
169 /// let ip = request.remote_address()
170 /// .trust_peer_address()
171 /// .apply();
172 /// assert_eq!(ip, Some(IpAddr::from([127, 0, 0, 1])));
173 /// ```
174 pub fn trust_peer_address(&mut self) -> &mut Self {
175 self.trusted_sources.push(RemoteAddressSource::PeerAddress);
176 self
177 }
178
179 /// Applies the sources to the request, extracting the IP address. Since
180 /// all sources are fallible, this will return `None` if all of the sources
181 /// fail. All sources are evaluated from the first source added to the
182 /// last.
183 ///
184 /// # Examples
185 /// ```rust
186 /// # use under::*;
187 /// # use std::net::IpAddr;
188 /// # let mut request = Request::get("/").unwrap().with_local_addr();
189 /// request.set_header("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3");
190 /// let ip = request.remote_address()
191 /// .trust_cloudflare_header()
192 /// .trust_forwarded_for(-1)
193 /// .trust_peer_address()
194 /// .apply();
195 /// assert_eq!(ip, Some(IpAddr::from([3, 3, 3, 3])));
196 /// ```
197 #[must_use = "you probably don't intend to discard this value"]
198 pub fn apply(&self) -> Option<IpAddr> {
199 for source in &self.trusted_sources {
200 if let Some(ip) = source.apply(self.request) {
201 return Some(ip);
202 }
203 }
204 None
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Hash)]
209enum RemoteAddressSource {
210 /// Loads the X-Forwarded-For header directly from the request. The number
211 /// here indicates which IP address to use; the first one is 0, the second
212 /// is 1, etc.; if the number is negative, then it is the nth address from
213 /// the right; so -1 is the last IP, -2 is the second-to-last, etc.
214 XForwardedFor(isize),
215 /// Loads the Forwarded header directly from the request. The number here
216 /// indicates which IP address to use; the first one is 0, the second is 1,
217 /// etc.; if the number is negative, then it is the nth address from the
218 /// right; so -1 is the last IP, -2 is the second-to-last, etc.
219 ///
220 /// The Forwarded header can contain more information than just an IP
221 /// address, such as a secret provided by a proxy down the line; it is
222 /// possible to include this as a parameter to the header. Unfortunately,
223 /// I do not know of any use cases for this yet, so I have not implemented
224 /// it yet (feel free to open an Issue/PR!).
225 Forwarded(isize),
226 /// Pulls the IP address straight from the specified header. Ideally this
227 /// header would be set by a trusted proxy (overriding any previous
228 /// headers), but if there is no trusted proxy, then it could be set by a
229 /// client.
230 Header(Cow<'static, str>),
231 /// Pulls the IP address straight from the TCP/IP peer. This is the most
232 /// reliable source, but since the application's infrastructure may have
233 /// (reverse) proxies in front of the user, it is not guaranteed to be
234 /// accurate (and, in fact, if there is a trusted proxy, this may return
235 /// the IP of the trusted proxy instead).
236 PeerAddress,
237}
238
239impl RemoteAddressSource {
240 pub fn apply(&self, request: &super::Request) -> Option<IpAddr> {
241 match self {
242 RemoteAddressSource::XForwardedFor(index) => x_forwarded_for_header(request, *index),
243 RemoteAddressSource::Forwarded(index) => forwarded_header(request, *index),
244 RemoteAddressSource::Header(name) => request
245 .header_all(&**name)
246 .into_iter()
247 .filter_map(|v| v.to_str().ok())
248 .find_map(|v| v.parse().ok()),
249 RemoteAddressSource::PeerAddress => request.peer_addr().map(|v| v.ip()),
250 }
251 }
252}
253
254fn x_forwarded_for_header(request: &super::Request, index: isize) -> Option<IpAddr> {
255 let mut ip = request
256 .header_all("X-Forwarded-For")
257 .into_iter()
258 .filter_map(|s| s.to_str().ok())
259 .flat_map(|s| s.split(','))
260 .map(str::trim);
261
262 if index < 0 {
263 #[allow(clippy::cast_sign_loss)]
264 let index = (index.checked_abs()? as usize).checked_sub(1)?;
265 ip.nth_back(index).and_then(|s| s.parse().ok())
266 } else if index >= 0 {
267 #[allow(clippy::cast_sign_loss)]
268 ip.nth(index as usize).and_then(|s| s.parse().ok())
269 } else {
270 None
271 }
272}
273
274lazy_static::lazy_static! {
275 static ref FOR_WORD: regex::Regex = regex::Regex::new(r"(?i)^for$").unwrap();
276 static ref SPECIAL_TOKEN: regex::Regex = regex::Regex::new(r#"^"[(.+)]"$"#).unwrap();
277}
278
279// How is this even more unreliable than x-forwarded-for? If it's not utf-8,
280// or doesn't match key-value parsing pairs, than it'll ignore whole sections.
281// Not sure this is a good thing.
282fn forwarded_header(request: &super::Request, index: isize) -> Option<IpAddr> {
283 fn parse_key_value(s: &str) -> Option<(&str, &str)> {
284 let (key, value) = s.split_once('=')?;
285 Some((key, value))
286 }
287
288 fn parse_ip(s: &str) -> Option<IpAddr> {
289 let s = s.trim();
290 if let Some(cap) = SPECIAL_TOKEN.captures(s) {
291 cap[1].parse().ok()
292 } else {
293 s.parse().ok()
294 }
295 }
296
297 let ip = request
298 .header_all("Forwarded")
299 .into_iter()
300 .filter_map(|s| s.to_str().ok())
301 .flat_map(|s| s.split(','))
302 .map(str::trim)
303 .map(|s| {
304 s.split(';')
305 .filter_map(|s| parse_key_value(s.trim()))
306 .collect::<Vec<_>>()
307 });
308
309 // FOR_WORD is a requirement here because the standard says `for` is
310 // case insensitive. We _could_ try to lowercase it, but...
311 let mut ffor = ip.filter_map(|v| {
312 v.iter()
313 .find(|(k, _)| FOR_WORD.is_match(k))
314 .map(|(_, v)| *v)
315 });
316
317 if index < 0 {
318 #[allow(clippy::cast_sign_loss)]
319 let index = (index.checked_abs()? as usize).checked_sub(1)?;
320 ffor.nth_back(index).and_then(parse_ip)
321 } else if index >= 0 {
322 #[allow(clippy::cast_sign_loss)]
323 ffor.nth(index as usize).and_then(parse_ip)
324 } else {
325 None
326 }
327}