rfham_geo/geoip/providers/
mod.rs1use crate::{
12 error::{GeoError, GeoResult},
13 geoip::{Asn, Code, GeoLocation, IpGeoData, Locale, Location, Provider, ProviderDataLicense},
14};
15use lat_long::{Coordinate, Latitude, Longitude};
16use rfham_core::error::CoreError;
17use serde::{Deserialize, Serialize};
18use std::{fmt::Debug, net::IpAddr};
19
20#[derive(Clone, Copy, Debug, Default, PartialEq)]
79pub struct GeoIpLookup();
80
81#[derive(Clone, Copy, Debug, Default, PartialEq)]
82pub struct NoOp();
83
84#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
97struct GeoIpResponse {
98 ip: IpAddr,
99 isp: String,
100 org: String,
101 hostname: String,
102 latitude: Latitude,
103 longitude: Longitude,
104 postal_code: String,
105 city: String,
106 country_code: String,
107 country_name: String,
108 continent_code: String,
109 continent_name: String,
110 region: String,
111 district: String,
112 timezone_name: String,
113 connection_type: String,
114 asn_number: u64,
115 asn_org: String,
116 asn: String,
117 currency_name: String,
118 currency_code: String,
119 language_code: String,
120 language_name: String,
121 success: bool,
122 premium: bool,
123}
124
125const GEOIP_LOOKUP_URL: &str = "https://json.geoiplookup.io/";
130
131impl Provider for GeoIpLookup {
132 fn lookup(&self, address: &std::net::IpAddr) -> GeoResult<Option<IpGeoData>> {
133 let response = reqwest::blocking::get(format!("{GEOIP_LOOKUP_URL}{address}"))?;
134 if response.status().is_success() {
135 let body = response.text()?;
136 let parsed: GeoIpResponse = serde_json::from_str(&body)?;
137 if parsed.success {
138 Ok(Some(parsed.try_into()?))
139 } else {
140 Ok(None)
141 }
142 } else {
143 Err(GeoError::Http(response.status()))
144 }
145 }
146
147 fn license(&self) -> ProviderDataLicense {
148 ProviderDataLicense::Public
149 }
150}
151
152impl TryFrom<GeoIpResponse> for IpGeoData {
155 type Error = CoreError;
156
157 fn try_from(response: GeoIpResponse) -> Result<Self, Self::Error> {
158 let location = Location::new(
159 Code::new(response.continent_code.parse()?, response.continent_name),
160 Code::new(response.country_code.parse()?, response.country_name),
161 );
162 let location = if !response.region.is_empty() {
163 location.with_region(response.region)
164 } else {
165 location
166 };
167 let location = if !response.city.is_empty() {
168 location.with_city(response.city)
169 } else {
170 location
171 };
172 let location = if !response.district.is_empty() {
173 location.with_district(response.district)
174 } else {
175 location
176 };
177 let location = if !response.postal_code.is_empty() {
178 location.with_postal_code(response.postal_code)
179 } else {
180 location
181 };
182 let location = location.with_location(GeoLocation::new(Coordinate::new(
183 response.latitude,
184 response.longitude,
185 )));
186
187 let data = IpGeoData::new(response.ip, location);
188
189 let data = if !response.timezone_name.is_empty()
190 || !response.currency_code.is_empty()
191 || !response.language_code.is_empty()
192 {
193 let locale = Locale::default();
194
195 let locale = if !response.timezone_name.is_empty() {
196 locale.with_timezone(response.timezone_name)
197 } else {
198 locale
199 };
200 let locale = if !response.currency_code.is_empty() {
201 locale.with_currency(Code::new(
202 response.currency_code.parse()?,
203 response.currency_name,
204 ))
205 } else {
206 locale
207 };
208 let locale = if !response.language_code.is_empty() {
209 locale.with_language(Code::new(
210 response.language_code.parse()?,
211 response.language_name,
212 ))
213 } else {
214 locale
215 };
216 data.with_locale(locale)
217 } else {
218 data
219 };
220
221 let data = if response.asn_number != 0 {
222 data.with_asn(Asn::new(
223 response.asn_number,
224 response.asn,
225 response.asn_org,
226 ))
227 } else {
228 data
229 };
230
231 Ok(data)
232 }
233}
234
235impl Provider for NoOp {
238 fn lookup(&self, _: &std::net::IpAddr) -> GeoResult<Option<IpGeoData>> {
239 Ok(None)
240 }
241
242 fn license(&self) -> ProviderDataLicense {
243 ProviderDataLicense::Public
244 }
245}
246
247pub mod local;
256
257#[cfg(test)]
262mod tests {
263 use super::*;
264 use pretty_assertions::assert_eq;
265
266 #[test]
267 fn test_parse_geoip_response() {
268 const RESPONSE: &str = r##"{
269 "ip": "23.64.167.34",
270 "isp": "Akamai Technologies, Inc.",
271 "org": "Akamai Technologies, Inc.",
272 "hostname": "",
273 "latitude": 32.814,
274 "longitude": -96.9489,
275 "postal_code": "",
276 "city": "Irving",
277 "country_code": "US",
278 "country_name": "United States",
279 "continent_code": "NA",
280 "continent_name": "North America",
281 "region": "Texas",
282 "district": "",
283 "timezone_name": "America/Chicago",
284 "connection_type": "Corporate",
285 "asn_number": 16625,
286 "asn_org": "Akamai Technologies, Inc.",
287 "asn": "AS16625 - Akamai Technologies, Inc.",
288 "currency_code": "USD",
289 "currency_name": "United States Dollar",
290 "language_code": "en",
291 "language_name": "English",
292 "success": true,
293 "premium": false
294}"##;
295 let parsed: Result<GeoIpResponse, serde_json::Error> = serde_json::from_str(RESPONSE);
296 assert!(parsed.is_ok());
297 let parsed = parsed.unwrap();
298 assert_eq!("23.64.167.34".to_string(), parsed.ip.to_string());
299 }
300
301 #[test]
302 fn test_geoip_response_to_data() {
303 const RESPONSE: &str = r##"{
304 "ip": "23.64.167.34",
305 "isp": "Akamai Technologies, Inc.",
306 "org": "Akamai Technologies, Inc.",
307 "hostname": "",
308 "latitude": 32.814,
309 "longitude": -96.9489,
310 "postal_code": "",
311 "city": "Irving",
312 "country_code": "US",
313 "country_name": "United States",
314 "continent_code": "NA",
315 "continent_name": "North America",
316 "region": "Texas",
317 "district": "",
318 "timezone_name": "America/Chicago",
319 "connection_type": "Corporate",
320 "asn_number": 16625,
321 "asn_org": "Akamai Technologies, Inc.",
322 "asn": "AS16625 - Akamai Technologies, Inc.",
323 "currency_code": "USD",
324 "currency_name": "United States Dollar",
325 "language_code": "en",
326 "language_name": "English",
327 "success": true,
328 "premium": false
329}"##;
330 let parsed: GeoIpResponse = serde_json::from_str(RESPONSE).unwrap();
331 let data: IpGeoData = parsed.try_into().unwrap();
332 assert_eq!("NA", data.location().continent().code().to_string());
333 assert_eq!("US", data.location().country().code().to_string());
334 assert_eq!(16625, data.asn().unwrap().number());
335 }
336
337 #[test]
338 fn test_noop_provider() {
339 let provider = NoOp::default();
340 let ip_address: IpAddr = "23.64.167.34".parse().unwrap();
341 assert_eq!(None, provider.lookup(&ip_address).unwrap())
342 }
343}