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}