1use crate::device::{parse_device_name, parse_device_type};
2use crate::fingerprint::compute_fingerprint;
3use http::HeaderMap;
4use std::net::IpAddr;
5
6#[derive(Debug, Clone)]
9pub struct SessionMeta {
10 pub ip_address: String,
12 pub user_agent: String,
14 pub device_name: String,
16 pub device_type: String,
18 pub fingerprint: String,
20}
21
22impl SessionMeta {
23 pub fn from_headers(
28 ip_address: String,
29 user_agent: &str,
30 accept_language: &str,
31 accept_encoding: &str,
32 ) -> Self {
33 Self {
34 ip_address,
35 device_name: parse_device_name(user_agent),
36 device_type: parse_device_type(user_agent),
37 fingerprint: compute_fingerprint(user_agent, accept_language, accept_encoding),
38 user_agent: user_agent.to_string(),
39 }
40 }
41}
42
43pub fn header_str<'a>(headers: &'a HeaderMap, name: &str) -> &'a str {
46 headers
47 .get(name)
48 .and_then(|v| v.to_str().ok())
49 .unwrap_or("")
50}
51
52pub fn extract_client_ip(
57 headers: &HeaderMap,
58 trusted_proxies: &[String],
59 connect_ip: Option<IpAddr>,
60) -> String {
61 let parsed_nets: Vec<ipnet::IpNet> = trusted_proxies
62 .iter()
63 .filter_map(|s| s.parse().ok())
64 .collect();
65
66 if let Some(ip) = connect_ip
69 && !parsed_nets.is_empty()
70 && !parsed_nets.iter().any(|net| net.contains(&ip))
71 {
72 return ip.to_string();
73 }
74
75 if let Some(forwarded) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok())
77 && let Some(first) = forwarded.split(',').next()
78 {
79 let candidate = first.trim();
80 if candidate.parse::<IpAddr>().is_ok() {
81 return candidate.to_string();
82 }
83 }
84
85 if let Some(real_ip) = headers.get("x-real-ip").and_then(|v| v.to_str().ok()) {
86 let candidate = real_ip.trim();
87 if candidate.parse::<IpAddr>().is_ok() {
88 return candidate.to_string();
89 }
90 }
91
92 connect_ip
93 .map(|ip| ip.to_string())
94 .unwrap_or_else(|| "unknown".to_string())
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn extract_ip_from_xff() {
103 let mut headers = HeaderMap::new();
104 headers.insert("x-forwarded-for", "1.2.3.4, 5.6.7.8".parse().unwrap());
105 assert_eq!(extract_client_ip(&headers, &[], None), "1.2.3.4");
106 }
107
108 #[test]
109 fn extract_ip_from_x_real_ip() {
110 let mut headers = HeaderMap::new();
111 headers.insert("x-real-ip", "9.8.7.6".parse().unwrap());
112 assert_eq!(extract_client_ip(&headers, &[], None), "9.8.7.6");
113 }
114
115 #[test]
116 fn extract_ip_prefers_xff() {
117 let mut headers = HeaderMap::new();
118 headers.insert("x-forwarded-for", "1.2.3.4".parse().unwrap());
119 headers.insert("x-real-ip", "9.8.7.6".parse().unwrap());
120 assert_eq!(extract_client_ip(&headers, &[], None), "1.2.3.4");
121 }
122
123 #[test]
124 fn extract_ip_falls_back_to_unknown() {
125 let headers = HeaderMap::new();
126 assert_eq!(extract_client_ip(&headers, &[], None), "unknown");
127 }
128
129 #[test]
130 fn extract_ip_falls_back_to_connect_ip() {
131 let headers = HeaderMap::new();
132 let ip: IpAddr = "192.168.1.1".parse().unwrap();
133 assert_eq!(extract_client_ip(&headers, &[], Some(ip)), "192.168.1.1");
134 }
135
136 #[test]
137 fn untrusted_source_ignores_xff() {
138 let mut headers = HeaderMap::new();
139 headers.insert("x-forwarded-for", "1.2.3.4".parse().unwrap());
140 let untrusted: IpAddr = "203.0.113.5".parse().unwrap();
141 let trusted = vec!["10.0.0.0/24".to_string()];
142 assert_eq!(
143 extract_client_ip(&headers, &trusted, Some(untrusted)),
144 "203.0.113.5"
145 );
146 }
147
148 #[test]
149 fn trusted_proxy_uses_xff() {
150 let mut headers = HeaderMap::new();
151 headers.insert("x-forwarded-for", "8.8.8.8".parse().unwrap());
152 let trusted_ip: IpAddr = "10.0.0.1".parse().unwrap();
153 let trusted = vec!["10.0.0.0/24".to_string()];
154 assert_eq!(
155 extract_client_ip(&headers, &trusted, Some(trusted_ip)),
156 "8.8.8.8"
157 );
158 }
159
160 #[test]
161 fn session_meta_from_headers() {
162 let meta = SessionMeta::from_headers(
163 "10.0.0.1".to_string(),
164 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/120.0.0.0",
165 "en-US",
166 "gzip",
167 );
168 assert_eq!(meta.ip_address, "10.0.0.1");
169 assert_eq!(meta.device_name, "Chrome on macOS");
170 assert_eq!(meta.device_type, "desktop");
171 assert_eq!(meta.fingerprint.len(), 64);
172 }
173}