Skip to main content

actix_web/
info.rs

1use std::{convert::Infallible, net::SocketAddr};
2
3#[expect(deprecated, reason = "preserve the public ready future type")]
4use actix_utils::future::{ready, Ready};
5use derive_more::{Display, Error};
6
7use crate::{
8    dev::{AppConfig, Payload, RequestHead},
9    http::{
10        header::{self, HeaderName},
11        uri::{Authority, Scheme},
12    },
13    FromRequest, HttpRequest, ResponseError,
14};
15
16static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
17static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
18static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
19
20/// Trim whitespace then any quote marks.
21fn unquote(val: &str) -> &str {
22    val.trim().trim_start_matches('"').trim_end_matches('"')
23}
24
25/// Remove port and IPv6 square brackets from a peer specification.
26fn bare_address(val: &str) -> &str {
27    if val.starts_with('[') {
28        val.split("]:")
29            .next()
30            .map(|s| s.trim_start_matches('[').trim_end_matches(']'))
31            // this indicates that the IPv6 address is malformed so shouldn't
32            // usually happen, but if it does, just return the original input
33            .unwrap_or(val)
34    } else {
35        val.split(':').next().unwrap_or(val)
36    }
37}
38
39/// Extracts and trims first value for given header name.
40fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> {
41    let hdr = req.headers.get(name)?.to_str().ok()?;
42    let val = hdr.split(',').next()?.trim();
43    Some(val)
44}
45
46/// HTTP connection information.
47///
48/// `ConnectionInfo` implements `FromRequest` and can be extracted in handlers.
49///
50/// # Examples
51/// ```
52/// # use actix_web::{HttpResponse, Responder};
53/// use actix_web::dev::ConnectionInfo;
54///
55/// async fn handler(conn: ConnectionInfo) -> impl Responder {
56///     match conn.host() {
57///         "actix.rs" => HttpResponse::Ok().body("Welcome!"),
58///         "admin.actix.rs" => HttpResponse::Ok().body("Admin portal."),
59///         _ => HttpResponse::NotFound().finish()
60///     }
61/// }
62/// # let _svc = actix_web::web::to(handler);
63/// ```
64///
65/// # Implementation Notes
66/// Parses `Forwarded` header information according to [RFC 7239][rfc7239] but does not try to
67/// interpret the values for each property. As such, the getter methods on `ConnectionInfo` return
68/// strings instead of IP addresses or other types to acknowledge that they may be
69/// [obfuscated][rfc7239-63] or [unknown][rfc7239-62].
70///
71/// If the older, related headers are also present (eg. `X-Forwarded-For`), then `Forwarded`
72/// is preferred.
73///
74/// [rfc7239]: https://datatracker.ietf.org/doc/html/rfc7239
75/// [rfc7239-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2
76/// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3
77#[derive(Debug, Clone, Default)]
78pub struct ConnectionInfo {
79    host: String,
80    scheme: String,
81    peer_addr: Option<String>,
82    realip_remote_addr: Option<String>,
83}
84
85impl ConnectionInfo {
86    pub(crate) fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo {
87        let mut host = None;
88        let mut scheme = None;
89        let mut realip_remote_addr = None;
90
91        for (name, val) in req
92            .headers
93            .get_all(&header::FORWARDED)
94            .filter_map(|hdr| hdr.to_str().ok())
95            // "for=1.2.3.4, for=5.6.7.8; scheme=https"
96            .flat_map(|val| val.split(';'))
97            // ["for=1.2.3.4, for=5.6.7.8", " scheme=https"]
98            .flat_map(|vals| vals.split(','))
99            // ["for=1.2.3.4", " for=5.6.7.8", " scheme=https"]
100            .flat_map(|pair| {
101                let mut items = pair.trim().splitn(2, '=');
102                Some((items.next()?, items.next()?))
103            })
104        {
105            // [(name , val      ), ...                                    ]
106            // [("for", "1.2.3.4"), ("for", "5.6.7.8"), ("scheme", "https")]
107
108            // taking the first value for each property is correct because spec states that first
109            // "for" value is client and rest are proxies; multiple values other properties have
110            // no defined semantics
111            //
112            // > In a chain of proxy servers where this is fully utilized, the first
113            // > "for" parameter will disclose the client where the request was first
114            // > made, followed by any subsequent proxy identifiers.
115            // --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2
116
117            match name.trim().to_lowercase().as_str() {
118                "for" => realip_remote_addr.get_or_insert_with(|| bare_address(unquote(val))),
119                "proto" => scheme.get_or_insert_with(|| unquote(val)),
120                "host" => host.get_or_insert_with(|| unquote(val)),
121                "by" => {
122                    // TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1
123                    continue;
124                }
125                _ => continue,
126            };
127        }
128
129        let scheme = scheme
130            .or_else(|| first_header_value(req, &X_FORWARDED_PROTO))
131            .or_else(|| req.uri.scheme().map(Scheme::as_str))
132            .or_else(|| cfg.secure().then_some("https"))
133            .unwrap_or("http")
134            .to_owned();
135
136        let host = host
137            .or_else(|| first_header_value(req, &X_FORWARDED_HOST))
138            .or_else(|| req.headers.get(&header::HOST)?.to_str().ok())
139            .or_else(|| req.uri.authority().map(Authority::as_str))
140            .unwrap_or_else(|| cfg.host())
141            .to_owned();
142
143        let realip_remote_addr = realip_remote_addr
144            .or_else(|| first_header_value(req, &X_FORWARDED_FOR))
145            .map(str::to_owned);
146
147        let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string());
148
149        ConnectionInfo {
150            host,
151            scheme,
152            peer_addr,
153            realip_remote_addr,
154        }
155    }
156
157    /// Real IP (remote address) of client that initiated request.
158    ///
159    /// The address is resolved through the following, in order:
160    /// - `Forwarded` header
161    /// - `X-Forwarded-For` header
162    /// - peer address of opened socket (same as [`peer_addr`](Self::peer_addr))
163    ///
164    /// # Security
165    /// Do not use this function for security purposes unless you can be sure that the `Forwarded`
166    /// and `X-Forwarded-For` headers cannot be spoofed by the client. If you are running without a
167    /// proxy then [obtaining the peer address](Self::peer_addr) would be more appropriate.
168    #[inline]
169    pub fn realip_remote_addr(&self) -> Option<&str> {
170        self.realip_remote_addr
171            .as_deref()
172            .or(self.peer_addr.as_deref())
173    }
174
175    /// Returns serialized IP address of the peer connection.
176    ///
177    /// See [`HttpRequest::peer_addr`] for more details.
178    #[inline]
179    pub fn peer_addr(&self) -> Option<&str> {
180        self.peer_addr.as_deref()
181    }
182
183    /// Hostname of the request.
184    ///
185    /// Hostname is resolved through the following, in order:
186    /// - `Forwarded` header
187    /// - `X-Forwarded-Host` header
188    /// - `Host` header
189    /// - request target / URI
190    /// - configured server hostname
191    #[inline]
192    pub fn host(&self) -> &str {
193        &self.host
194    }
195
196    /// Scheme of the request.
197    ///
198    /// Scheme is resolved through the following, in order:
199    /// - `Forwarded` header
200    /// - `X-Forwarded-Proto` header
201    /// - request target / URI
202    #[inline]
203    pub fn scheme(&self) -> &str {
204        &self.scheme
205    }
206
207    #[doc(hidden)]
208    #[deprecated(since = "4.0.0", note = "Renamed to `peer_addr`.")]
209    pub fn remote_addr(&self) -> Option<&str> {
210        self.peer_addr()
211    }
212}
213
214#[expect(deprecated, reason = "preserve the public ready future type")]
215impl FromRequest for ConnectionInfo {
216    type Error = Infallible;
217    type Future = Ready<Result<Self, Self::Error>>;
218
219    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
220        ready(Ok(req.connection_info().clone()))
221    }
222}
223
224/// Extractor for peer's socket address.
225///
226/// Also see [`HttpRequest::peer_addr`] and [`ConnectionInfo::peer_addr`].
227///
228/// # Examples
229/// ```
230/// # use actix_web::Responder;
231/// use actix_web::dev::PeerAddr;
232///
233/// async fn handler(peer_addr: PeerAddr) -> impl Responder {
234///     let socket_addr = peer_addr.0;
235///     socket_addr.to_string()
236/// }
237/// # let _svc = actix_web::web::to(handler);
238/// ```
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display)]
240#[display("{}", _0)]
241pub struct PeerAddr(pub SocketAddr);
242
243impl PeerAddr {
244    /// Unwrap into inner `SocketAddr` value.
245    pub fn into_inner(self) -> SocketAddr {
246        self.0
247    }
248}
249
250#[derive(Debug, Display, Error)]
251#[non_exhaustive]
252#[display("Missing peer address")]
253pub struct MissingPeerAddr;
254
255impl ResponseError for MissingPeerAddr {}
256
257#[expect(deprecated, reason = "preserve the public ready future type")]
258impl FromRequest for PeerAddr {
259    type Error = MissingPeerAddr;
260    type Future = Ready<Result<Self, Self::Error>>;
261
262    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
263        match req.peer_addr() {
264            Some(addr) => ready(Ok(PeerAddr(addr))),
265            None => {
266                log::error!("Missing peer address.");
267                ready(Err(MissingPeerAddr))
268            }
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::test::TestRequest;
277
278    const X_FORWARDED_FOR: &str = "x-forwarded-for";
279    const X_FORWARDED_HOST: &str = "x-forwarded-host";
280    const X_FORWARDED_PROTO: &str = "x-forwarded-proto";
281
282    #[test]
283    fn info_default() {
284        let req = TestRequest::default().to_http_request();
285        let info = req.connection_info();
286        assert_eq!(info.scheme(), "http");
287        assert_eq!(info.host(), "localhost:8080");
288    }
289
290    #[test]
291    fn host_header() {
292        let req = TestRequest::default()
293            .insert_header((header::HOST, "rust-lang.org"))
294            .to_http_request();
295
296        let info = req.connection_info();
297        assert_eq!(info.scheme(), "http");
298        assert_eq!(info.host(), "rust-lang.org");
299        assert_eq!(info.realip_remote_addr(), None);
300    }
301
302    #[test]
303    fn x_forwarded_for_header() {
304        let req = TestRequest::default()
305            .insert_header((X_FORWARDED_FOR, "192.0.2.60"))
306            .to_http_request();
307        let info = req.connection_info();
308        assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
309    }
310
311    #[test]
312    fn x_forwarded_host_header() {
313        let req = TestRequest::default()
314            .insert_header((X_FORWARDED_HOST, "192.0.2.60"))
315            .to_http_request();
316        let info = req.connection_info();
317        assert_eq!(info.host(), "192.0.2.60");
318        assert_eq!(info.realip_remote_addr(), None);
319    }
320
321    #[test]
322    fn x_forwarded_proto_header() {
323        let req = TestRequest::default()
324            .insert_header((X_FORWARDED_PROTO, "https"))
325            .to_http_request();
326        let info = req.connection_info();
327        assert_eq!(info.scheme(), "https");
328    }
329
330    #[test]
331    fn forwarded_header() {
332        let req = TestRequest::default()
333            .insert_header((
334                header::FORWARDED,
335                "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org",
336            ))
337            .to_http_request();
338
339        let info = req.connection_info();
340        assert_eq!(info.scheme(), "https");
341        assert_eq!(info.host(), "rust-lang.org");
342        assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
343
344        let req = TestRequest::default()
345            .insert_header((
346                header::FORWARDED,
347                "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org",
348            ))
349            .to_http_request();
350
351        let info = req.connection_info();
352        assert_eq!(info.scheme(), "https");
353        assert_eq!(info.host(), "rust-lang.org");
354        assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
355    }
356
357    #[test]
358    fn forwarded_case_sensitivity() {
359        let req = TestRequest::default()
360            .insert_header((header::FORWARDED, "For=192.0.2.60"))
361            .to_http_request();
362        let info = req.connection_info();
363        assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
364    }
365
366    #[test]
367    fn forwarded_weird_whitespace() {
368        let req = TestRequest::default()
369            .insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https"))
370            .to_http_request();
371        let info = req.connection_info();
372        assert_eq!(info.realip_remote_addr(), Some("1.2.3.4"));
373        assert_eq!(info.scheme(), "https");
374
375        let req = TestRequest::default()
376            .insert_header((header::FORWARDED, "  for = 1.2.3.4  "))
377            .to_http_request();
378        let info = req.connection_info();
379        assert_eq!(info.realip_remote_addr(), Some("1.2.3.4"));
380    }
381
382    #[test]
383    fn forwarded_for_quoted() {
384        let req = TestRequest::default()
385            .insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#))
386            .to_http_request();
387        let info = req.connection_info();
388        assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
389    }
390
391    #[test]
392    fn forwarded_for_ipv6() {
393        let req = TestRequest::default()
394            .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]""#))
395            .to_http_request();
396        let info = req.connection_info();
397        assert_eq!(info.realip_remote_addr(), Some("2001:db8:cafe::17"));
398    }
399
400    #[test]
401    fn forwarded_for_ipv6_with_port() {
402        let req = TestRequest::default()
403            .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#))
404            .to_http_request();
405        let info = req.connection_info();
406        assert_eq!(info.realip_remote_addr(), Some("2001:db8:cafe::17"));
407    }
408
409    #[test]
410    fn forwarded_for_multiple() {
411        let req = TestRequest::default()
412            .insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17"))
413            .to_http_request();
414        let info = req.connection_info();
415        // takes the first value
416        assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
417    }
418
419    #[test]
420    fn scheme_from_uri() {
421        let req = TestRequest::get()
422            .uri("https://actix.rs/test")
423            .to_http_request();
424        let info = req.connection_info();
425        assert_eq!(info.scheme(), "https");
426    }
427
428    #[test]
429    fn host_from_uri() {
430        let req = TestRequest::get()
431            .uri("https://actix.rs/test")
432            .to_http_request();
433        let info = req.connection_info();
434        assert_eq!(info.host(), "actix.rs");
435    }
436
437    #[test]
438    fn host_from_server_hostname() {
439        let mut req = TestRequest::get();
440        req.set_server_hostname("actix.rs");
441        let req = req.to_http_request();
442
443        let info = req.connection_info();
444        assert_eq!(info.host(), "actix.rs");
445    }
446
447    #[actix_rt::test]
448    async fn conn_info_extract() {
449        let req = TestRequest::default()
450            .uri("https://actix.rs/test")
451            .to_http_request();
452        let conn_info = ConnectionInfo::extract(&req).await.unwrap();
453        assert_eq!(conn_info.scheme(), "https");
454        assert_eq!(conn_info.host(), "actix.rs");
455    }
456
457    #[actix_rt::test]
458    async fn peer_addr_extract() {
459        let req = TestRequest::default().to_http_request();
460        let res = PeerAddr::extract(&req).await;
461        assert!(res.is_err());
462
463        let addr = "127.0.0.1:8080".parse().unwrap();
464        let req = TestRequest::default().peer_addr(addr).to_http_request();
465        let peer_addr = PeerAddr::extract(&req).await.unwrap();
466        assert_eq!(peer_addr, PeerAddr(addr));
467    }
468
469    #[actix_rt::test]
470    async fn remote_address() {
471        let req = TestRequest::default().to_http_request();
472        let res = ConnectionInfo::extract(&req).await.unwrap();
473        assert!(res.peer_addr().is_none());
474
475        let addr = "127.0.0.1:8080".parse().unwrap();
476        let req = TestRequest::default().peer_addr(addr).to_http_request();
477        let conn_info = ConnectionInfo::extract(&req).await.unwrap();
478        assert_eq!(conn_info.peer_addr().unwrap(), "127.0.0.1");
479    }
480
481    #[actix_rt::test]
482    async fn real_ip_from_socket_addr() {
483        let req = TestRequest::default().to_http_request();
484        let res = ConnectionInfo::extract(&req).await.unwrap();
485        assert!(res.realip_remote_addr().is_none());
486
487        let addr = "127.0.0.1:8080".parse().unwrap();
488        let req = TestRequest::default().peer_addr(addr).to_http_request();
489        let conn_info = ConnectionInfo::extract(&req).await.unwrap();
490        assert_eq!(conn_info.realip_remote_addr().unwrap(), "127.0.0.1");
491    }
492}