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#[derive(Debug, Clone, Default)]
12pub struct ConnectionInfo {
13 scheme: String,
14 host: String,
15 remote: Option<String>,
16 peer: Option<String>,
17}
18
19fn 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 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 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 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 = 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 .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 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 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 #[inline]
125 pub fn scheme(&self) -> &str {
126 &self.scheme
127 }
128
129 pub fn host(&self) -> &str {
141 &self.host
142 }
143
144 #[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 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 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}