actr_framework/util/
geoip.rs1#[cfg(feature = "geoip")]
36use anyhow::{Context, Result};
37#[cfg(feature = "geoip")]
38use maxminddb::{MaxMindDBError, Reader, geoip2::City};
39#[cfg(feature = "geoip")]
40use std::net::IpAddr;
41#[cfg(feature = "geoip")]
42use std::path::Path;
43#[cfg(feature = "geoip")]
44use tracing::{debug, info, warn};
45
46#[cfg(feature = "geoip")]
50#[derive(Debug)]
51pub struct GeoIpService {
52 reader: Reader<Vec<u8>>,
53}
54
55#[cfg(feature = "geoip")]
56impl GeoIpService {
57 pub fn new<P: AsRef<Path>>(db_path: P) -> Result<Self> {
69 let path = db_path.as_ref();
70
71 if !path.exists() {
73 info!("GeoIP database not found at {:?}", path);
74
75 if let Ok(license_key) = std::env::var("MAXMIND_LICENSE_KEY") {
76 info!("MAXMIND_LICENSE_KEY found, attempting auto-download...");
77 Self::download_database(path, &license_key)?;
78 } else {
79 anyhow::bail!(
80 "GeoIP database not found at {:?}\n\
81 \n\
82 To auto-download:\n\
83 1. Get License Key: https://www.maxmind.com/en/geolite2/signup\n\
84 2. export MAXMIND_LICENSE_KEY=\"your-key\"\n\
85 3. Retry\n\
86 \n\
87 Or manually download:\n\
88 curl -o GeoLite2-City.tar.gz \\\n\
89 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=YOUR_KEY&suffix=tar.gz'\n\
90 tar -xzf GeoLite2-City.tar.gz --strip-components=1 -C {:?}/ '*/GeoLite2-City.mmdb'",
91 path,
92 path.parent().unwrap_or(Path::new("."))
93 );
94 }
95 }
96
97 info!("Loading GeoIP database from: {:?}", path);
98 let reader = Reader::open_readfile(path)
99 .context(format!("Failed to open GeoIP database at {path:?}"))?;
100
101 info!(
102 "GeoIP service initialized (build epoch: {})",
103 reader.metadata.build_epoch
104 );
105 Ok(Self { reader })
106 }
107
108 fn download_database(db_path: &Path, license_key: &str) -> Result<()> {
110 use reqwest::blocking::Client;
111
112 info!("Downloading GeoLite2-City database (~70MB)...");
113
114 let url = format!(
116 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key={}&suffix=tar.gz",
117 license_key
118 );
119
120 let client = Client::builder()
122 .timeout(std::time::Duration::from_secs(300)) .build()?;
124
125 let response = client
126 .get(&url)
127 .send()
128 .context("Failed to download GeoLite2 database")?;
129
130 if !response.status().is_success() {
131 anyhow::bail!(
132 "Download failed with status: {} - Check your MAXMIND_LICENSE_KEY",
133 response.status()
134 );
135 }
136
137 info!("Download complete, extracting...");
138
139 let tar_gz_data = response.bytes()?;
141 let tar_decoder = flate2::read::GzDecoder::new(&tar_gz_data[..]);
142 let mut archive = tar::Archive::new(tar_decoder);
143
144 for entry in archive.entries()? {
146 let mut entry = entry?;
147 let path_in_archive = entry.path()?;
148
149 if path_in_archive.extension() == Some(std::ffi::OsStr::new("mmdb"))
150 && path_in_archive.to_string_lossy().contains("GeoLite2-City")
151 {
152 if let Some(parent) = db_path.parent() {
154 std::fs::create_dir_all(parent)?;
155 }
156
157 let mut output = std::fs::File::create(db_path)?;
159 std::io::copy(&mut entry, &mut output)?;
160
161 let size = std::fs::metadata(db_path)?.len();
162 info!(
163 "GeoIP database downloaded to {:?} ({:.1} MB)",
164 db_path,
165 size as f64 / 1_048_576.0
166 );
167 return Ok(());
168 }
169 }
170
171 anyhow::bail!("GeoLite2-City.mmdb not found in downloaded archive");
172 }
173
174 pub fn lookup(&self, ip: IpAddr) -> Option<(f64, f64)> {
183 match self.reader.lookup::<City>(ip) {
184 Ok(city) => {
185 if let Some(location) = city.location {
186 if let (Some(lat), Some(lon)) = (location.latitude, location.longitude) {
187 debug!("GeoIP lookup: {} -> ({}, {})", ip, lat, lon);
188 return Some((lat, lon));
189 }
190 }
191 debug!("GeoIP lookup: {} found but no coordinates", ip);
192 None
193 }
194 Err(MaxMindDBError::AddressNotFoundError(_)) => {
195 debug!("GeoIP lookup: {} not in database", ip);
196 None
197 }
198 Err(e) => {
199 warn!("GeoIP lookup error for {}: {}", ip, e);
200 None
201 }
202 }
203 }
204
205 pub fn metadata(&self) -> &maxminddb::Metadata {
207 &self.reader.metadata
208 }
209}
210
211#[cfg(not(feature = "geoip"))]
213#[derive(Debug)]
214pub struct GeoIpService;
215
216#[cfg(not(feature = "geoip"))]
217impl GeoIpService {
218 pub fn new<P>(_db_path: P) -> anyhow::Result<Self> {
220 anyhow::bail!("GeoIP feature is not enabled. Rebuild with --features geoip")
221 }
222
223 pub fn lookup(&self, _ip: std::net::IpAddr) -> Option<(f64, f64)> {
225 None
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_geoip_module_compiles() {
235 let _ = std::mem::size_of::<GeoIpService>();
236 }
237
238 #[cfg(feature = "geoip")]
239 #[test]
240 fn test_geoip_lookup_requires_database() {
241 let result = GeoIpService::new("/nonexistent/path.mmdb");
243 assert!(result.is_err());
244 }
245
246 #[cfg(not(feature = "geoip"))]
247 #[test]
248 fn test_geoip_feature_disabled() {
249 let result = GeoIpService::new("/any/path.mmdb");
250 assert!(result.is_err());
251 assert!(
252 result
253 .unwrap_err()
254 .to_string()
255 .contains("GeoIP feature is not enabled")
256 );
257 }
258}