Skip to main content

ntex/web/
info.rs

1use std::{borrow::ToOwned, cell::Ref};
2
3use super::config::WebAppConfig;
4use crate::http::{RequestHead, header, header::HeaderName};
5
6const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
7const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
8const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
9
10/// `HttpRequest` connection information
11#[derive(Debug, Clone, Default)]
12pub struct ConnectionInfo {
13    scheme: String,
14    host: String,
15    remote: Option<String>,
16    peer: Option<String>,
17}
18
19/// Returns `host` if it is a valid `host[:port]`.
20fn valid_host(host: &str) -> Option<&str> {
21    urly::Authority::new(host)
22        .is_ok_and(|a| a.userinfo().is_none() && !a.host().is_empty())
23        .then_some(host)
24}
25
26impl ConnectionInfo {
27    /// Create *`ConnectionInfo`* instance for a request.
28    pub fn get<'a>(req: &'a RequestHead, cfg: &'a WebAppConfig) -> Ref<'a, Self> {
29        if !req.extensions().contains::<ConnectionInfo>() {
30            req.extensions_mut().insert(ConnectionInfo::new(req, cfg));
31        }
32        Ref::map(req.extensions(), |e| e.get().unwrap())
33    }
34
35    fn new(req: &RequestHead, cfg: &WebAppConfig) -> ConnectionInfo {
36        let mut host = None;
37        let mut scheme = None;
38        let mut remote = None;
39        let mut peer = None;
40
41        // load forwarded header
42        for hdr in req.headers.get_all(&header::FORWARDED) {
43            if let Ok(val) = hdr.to_str() {
44                for pair in val.split(';') {
45                    for el in pair.split(',') {
46                        let mut items = el.trim().splitn(2, '=');
47                        if let Some(name) = items.next()
48                            && let Some(val) = items.next()
49                        {
50                            match &name.to_lowercase() as &str {
51                                "for" if remote.is_none() => {
52                                    remote = Some(val.trim());
53                                }
54                                "proto" if scheme.is_none() => {
55                                    scheme = Some(val.trim());
56                                }
57                                "host" if host.is_none() => {
58                                    host = Some(val.trim());
59                                }
60                                _ => (),
61                            }
62                        }
63                    }
64                }
65            }
66        }
67
68        // scheme
69        if scheme.is_none() {
70            if let Some(h) = req.headers.get(&X_FORWARDED_PROTO)
71                && let Ok(h) = h.to_str()
72            {
73                scheme = h.split(',').next().map(str::trim);
74            }
75            if scheme.is_none() {
76                scheme = req.uri.scheme_str();
77                if scheme.is_none() && cfg.secure() {
78                    scheme = Some("https");
79                }
80            }
81        }
82
83        // host, invalid values are skipped
84        host = host
85            .and_then(valid_host)
86            .or_else(|| {
87                let h = req.headers.get(&X_FORWARDED_HOST)?.to_str().ok()?;
88                valid_host(h.split(',').next()?.trim())
89            })
90            // the authority of an absolute-form target takes precedence over
91            // `Host`, see RFC 9112 section 3.2.2
92            .or_else(|| Some(req.uri.authority()?.host_port()).filter(|h| !h.is_empty()))
93            .or_else(|| valid_host(req.headers.get(&header::HOST)?.to_str().ok()?))
94            .or_else(|| Some(cfg.host()));
95
96        // remote addr
97        if remote.is_none() {
98            if let Some(h) = req.headers.get(&X_FORWARDED_FOR)
99                && let Ok(h) = h.to_str()
100            {
101                remote = h.split(',').next().map(str::trim);
102            }
103            if remote.is_none() {
104                // get peeraddr from socketaddr
105                peer = req.peer_addr().map(|addr| format!("{addr}"));
106            }
107        }
108
109        ConnectionInfo {
110            peer,
111            scheme: scheme.unwrap_or("http").to_owned(),
112            host: host.unwrap_or("localhost").to_owned(),
113            remote: remote.map(ToOwned::to_owned),
114        }
115    }
116
117    /// Scheme of the request.
118    ///
119    /// Scheme is resolved through the following headers, in this order:
120    ///
121    /// - Forwarded
122    /// - X-Forwarded-Proto
123    /// - Uri
124    #[inline]
125    pub fn scheme(&self) -> &str {
126        &self.scheme
127    }
128
129    /// Hostname of the request.
130    ///
131    /// Hostname is resolved through the following headers, in this order:
132    ///
133    /// - Forwarded
134    /// - X-Forwarded-Host
135    /// - Uri
136    /// - Host
137    /// - Server hostname
138    ///
139    /// Header values that are not a valid `host[:port]` are skipped.
140    pub fn host(&self) -> &str {
141        &self.host
142    }
143
144    /// Remote socket addr of client initiated HTTP request.
145    ///
146    /// The addr is resolved through the following headers, in this order:
147    ///
148    /// - Forwarded
149    /// - X-Forwarded-For
150    /// - peer name of opened socket
151    ///
152    /// # Security
153    /// Do not use this function for security purposes, unless you can ensure the Forwarded and
154    /// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket
155    /// address explicitly, use
156    /// [`HttpRequest::peer_addr()`](crate::web::HttpRequest::peer_addr) instead.
157    #[inline]
158    pub fn remote(&self) -> Option<&str> {
159        if let Some(ref r) = self.remote {
160            Some(r)
161        } else if let Some(ref peer) = self.peer {
162            Some(peer)
163        } else {
164            None
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::web::test::TestRequest;
173
174    #[test]
175    fn test_forwarded() {
176        let req = TestRequest::default().to_http_request();
177        let info = req.connection_info();
178        assert_eq!(info.scheme(), "http");
179        assert_eq!(info.host(), "localhost:8080");
180
181        let req = TestRequest::default()
182            .header(
183                header::FORWARDED,
184                "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org",
185            )
186            .to_http_request();
187
188        let info = req.connection_info();
189        assert_eq!(info.scheme(), "https");
190        assert_eq!(info.host(), "rust-lang.org");
191        assert_eq!(info.remote(), Some("192.0.2.60"));
192
193        let req = TestRequest::default()
194            .header(header::HOST, "rust-lang.org")
195            .to_http_request();
196
197        let info = req.connection_info();
198        assert_eq!(info.scheme(), "http");
199        assert_eq!(info.host(), "rust-lang.org");
200        assert_eq!(info.remote(), None);
201
202        let req = TestRequest::default()
203            .header(X_FORWARDED_FOR, "192.0.2.60")
204            .to_http_request();
205        let info = req.connection_info();
206        assert_eq!(info.remote(), Some("192.0.2.60"));
207
208        let req = TestRequest::default()
209            .header(X_FORWARDED_HOST, "192.0.2.60")
210            .to_http_request();
211        let info = req.connection_info();
212        assert_eq!(info.host(), "192.0.2.60");
213        assert_eq!(info.remote(), None);
214
215        let req = TestRequest::default()
216            .header(X_FORWARDED_PROTO, "https")
217            .to_http_request();
218        let info = req.connection_info();
219        assert_eq!(info.scheme(), "https");
220    }
221
222    #[test]
223    fn test_forwarded_ignored_items() {
224        let req = TestRequest::default()
225            .header(
226                header::FORWARDED,
227                "for=192.0.2.60, for=192.0.2.61; by=203.0.113.43; host=a.org; host=b.org; proto=https; proto=http; unknown",
228            )
229            .to_http_request();
230        let info = req.connection_info();
231        assert_eq!(info.remote(), Some("192.0.2.60"));
232        assert_eq!(info.host(), "a.org");
233        assert_eq!(info.scheme(), "https");
234    }
235
236    #[test]
237    fn test_host_sources() {
238        // absolute-form target takes precedence over `Host`, userinfo is dropped
239        let req = TestRequest::with_uri("http://u:[email protected]:8080/p")
240            .header(header::HOST, "b.org")
241            .to_http_request();
242        assert_eq!(req.connection_info().host(), "a.org:8080");
243
244        // invalid values are skipped
245        let req = TestRequest::default()
246            .header(header::FORWARDED, "host=a/b")
247            .header(X_FORWARDED_HOST, "evil/x#, c.org")
248            .header(header::HOST, "b.org:81")
249            .to_http_request();
250        assert_eq!(req.connection_info().host(), "b.org:81");
251
252        let req = TestRequest::default()
253            .header(X_FORWARDED_HOST, "[email protected]")
254            .header(header::HOST, "")
255            .to_http_request();
256        assert_eq!(req.connection_info().host(), "localhost:8080");
257    }
258
259    #[test]
260    fn test_secure_config() {
261        let req = TestRequest::default().to_http_request();
262        let info = ConnectionInfo::new(req.head(), &WebAppConfig::new().set_secure());
263        assert_eq!(info.scheme(), "https");
264
265        let info = ConnectionInfo::new(req.head(), &WebAppConfig::new());
266        assert_eq!(info.scheme(), "http");
267    }
268
269    #[crate::rt_test]
270    async fn test_peer_addr() {
271        let req = TestRequest::default()
272            .peer_addr("192.0.2.1:8080".parse().unwrap())
273            .to_http_request();
274        let info = req.connection_info();
275        assert_eq!(info.remote(), Some("192.0.2.1:8080"));
276
277        let req = TestRequest::default()
278            .peer_addr("192.0.2.1:8080".parse().unwrap())
279            .header(X_FORWARDED_FOR, "192.0.2.60")
280            .to_http_request();
281        let info = req.connection_info();
282        assert_eq!(info.remote(), Some("192.0.2.60"));
283    }
284}