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(
67 headers: &HeaderMap,
68 trusted_proxies: &[String],
69 connect_ip: Option<IpAddr>,
70) -> String {
71 let parsed_nets: Vec<ipnet::IpNet> = trusted_proxies
72 .iter()
73 .filter_map(|s| s.parse().ok())
74 .collect();
75
76 if let Some(ip) = connect_ip
79 && !parsed_nets.is_empty()
80 && !parsed_nets.iter().any(|net| net.contains(&ip))
81 {
82 return ip.to_string();
83 }
84
85 if let Some(forwarded) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok())
87 && let Some(first) = forwarded.split(',').next()
88 {
89 let candidate = first.trim();
90 if candidate.parse::<IpAddr>().is_ok() {
91 return candidate.to_string();
92 }
93 }
94
95 if let Some(real_ip) = headers.get("x-real-ip").and_then(|v| v.to_str().ok()) {
96 let candidate = real_ip.trim();
97 if candidate.parse::<IpAddr>().is_ok() {
98 return candidate.to_string();
99 }
100 }
101
102 connect_ip
103 .map(|ip| ip.to_string())
104 .unwrap_or_else(|| "unknown".to_string())
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn extract_ip_from_xff() {
113 let mut headers = HeaderMap::new();
114 headers.insert("x-forwarded-for", "1.2.3.4, 5.6.7.8".parse().unwrap());
115 assert_eq!(extract_client_ip(&headers, &[], None), "1.2.3.4");
116 }
117
118 #[test]
119 fn extract_ip_from_x_real_ip() {
120 let mut headers = HeaderMap::new();
121 headers.insert("x-real-ip", "9.8.7.6".parse().unwrap());
122 assert_eq!(extract_client_ip(&headers, &[], None), "9.8.7.6");
123 }
124
125 #[test]
126 fn extract_ip_prefers_xff() {
127 let mut headers = HeaderMap::new();
128 headers.insert("x-forwarded-for", "1.2.3.4".parse().unwrap());
129 headers.insert("x-real-ip", "9.8.7.6".parse().unwrap());
130 assert_eq!(extract_client_ip(&headers, &[], None), "1.2.3.4");
131 }
132
133 #[test]
134 fn extract_ip_falls_back_to_unknown() {
135 let headers = HeaderMap::new();
136 assert_eq!(extract_client_ip(&headers, &[], None), "unknown");
137 }
138
139 #[test]
140 fn extract_ip_falls_back_to_connect_ip() {
141 let headers = HeaderMap::new();
142 let ip: IpAddr = "192.168.1.1".parse().unwrap();
143 assert_eq!(extract_client_ip(&headers, &[], Some(ip)), "192.168.1.1");
144 }
145
146 #[test]
147 fn untrusted_source_ignores_xff() {
148 let mut headers = HeaderMap::new();
149 headers.insert("x-forwarded-for", "1.2.3.4".parse().unwrap());
150 let untrusted: IpAddr = "203.0.113.5".parse().unwrap();
151 let trusted = vec!["10.0.0.0/24".to_string()];
152 assert_eq!(
153 extract_client_ip(&headers, &trusted, Some(untrusted)),
154 "203.0.113.5"
155 );
156 }
157
158 #[test]
159 fn trusted_proxy_uses_xff() {
160 let mut headers = HeaderMap::new();
161 headers.insert("x-forwarded-for", "8.8.8.8".parse().unwrap());
162 let trusted_ip: IpAddr = "10.0.0.1".parse().unwrap();
163 let trusted = vec!["10.0.0.0/24".to_string()];
164 assert_eq!(
165 extract_client_ip(&headers, &trusted, Some(trusted_ip)),
166 "8.8.8.8"
167 );
168 }
169
170 #[test]
171 fn session_meta_from_headers() {
172 let meta = SessionMeta::from_headers(
173 "10.0.0.1".to_string(),
174 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/120.0.0.0",
175 "en-US",
176 "gzip",
177 );
178 assert_eq!(meta.ip_address, "10.0.0.1");
179 assert_eq!(meta.device_name, "Chrome on macOS");
180 assert_eq!(meta.device_type, "desktop");
181 assert_eq!(meta.fingerprint.len(), 64);
182 }
183}