mirror of
https://github.com/NetworkCats/rustyip.git
synced 2026-11-13 16:17:46 +00:00
258 lines
7.5 KiB
Rust
258 lines
7.5 KiB
Rust
use std::net::IpAddr;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
use arc_swap::ArcSwap;
|
|
use maxminddb::Reader;
|
|
use serde::Deserialize;
|
|
|
|
use crate::models::{
|
|
IpInfo, MmdbAsn, MmdbCity, MmdbCountry, MmdbProxy, MmdbRecord, ProxyInfo, from_mmdb_record,
|
|
};
|
|
|
|
pub type DbReader = Reader<Vec<u8>>;
|
|
pub type SharedDb = Arc<ArcSwap<DbReader>>;
|
|
|
|
pub fn load_db(path: &Path) -> Result<DbReader, maxminddb::MaxMindDbError> {
|
|
Reader::open_readfile(path)
|
|
}
|
|
|
|
pub fn new_shared(reader: DbReader) -> SharedDb {
|
|
Arc::new(ArcSwap::from_pointee(reader))
|
|
}
|
|
|
|
pub fn lookup(db: &DbReader, ip: IpAddr) -> Option<IpInfo> {
|
|
let result = db.lookup(ip).ok()?;
|
|
let record: MmdbRecord<'_> = result.decode().ok()??;
|
|
Some(from_mmdb_record(ip, &record))
|
|
}
|
|
|
|
// Lightweight deserialization structs for single-field endpoints.
|
|
// These avoid deserializing the full MMDB record when only one field is needed.
|
|
|
|
#[derive(Deserialize, Default)]
|
|
struct ProxyOnly {
|
|
#[serde(default)]
|
|
proxy: MmdbProxy,
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
struct AsnOnly<'a> {
|
|
#[serde(borrow, default)]
|
|
asn: MmdbAsn<'a>,
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
struct CountryOnly<'a> {
|
|
#[serde(borrow, default)]
|
|
country: MmdbCountry<'a>,
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
struct CityOnly<'a> {
|
|
#[serde(borrow, default)]
|
|
city: MmdbCity<'a>,
|
|
}
|
|
|
|
pub fn lookup_proxy(db: &DbReader, ip: IpAddr) -> Option<ProxyInfo> {
|
|
let result = db.lookup(ip).ok()?;
|
|
let record: ProxyOnly = result.decode().ok()??;
|
|
Some(ProxyInfo {
|
|
is_proxy: record.proxy.is_proxy,
|
|
is_vpn: record.proxy.is_vpn,
|
|
is_tor: record.proxy.is_tor,
|
|
is_hosting: record.proxy.is_hosting,
|
|
is_cdn: record.proxy.is_cdn,
|
|
is_school: record.proxy.is_school,
|
|
is_anonymous: record.proxy.is_anonymous,
|
|
})
|
|
}
|
|
|
|
pub fn lookup_asn_number(db: &DbReader, ip: IpAddr) -> Option<u32> {
|
|
let result = db.lookup(ip).ok()?;
|
|
let record: AsnOnly<'_> = result.decode().ok()??;
|
|
record.asn.autonomous_system_number
|
|
}
|
|
|
|
pub fn lookup_asn_org(db: &DbReader, ip: IpAddr) -> Option<String> {
|
|
let result = db.lookup(ip).ok()?;
|
|
let record: AsnOnly<'_> = result.decode().ok()??;
|
|
record.asn.autonomous_system_organization.map(str::to_owned)
|
|
}
|
|
|
|
pub fn lookup_country_name(db: &DbReader, ip: IpAddr) -> Option<String> {
|
|
let result = db.lookup(ip).ok()?;
|
|
let record: CountryOnly<'_> = result.decode().ok()??;
|
|
record.country.names.english.map(str::to_owned)
|
|
}
|
|
|
|
pub fn lookup_city_name(db: &DbReader, ip: IpAddr) -> Option<String> {
|
|
let result = db.lookup(ip).ok()?;
|
|
let record: CityOnly<'_> = result.decode().ok()??;
|
|
record.city.names.english.map(str::to_owned)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::path::Path;
|
|
|
|
fn test_db_path() -> String {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
format!("{manifest_dir}/data/Merged-IP.mmdb")
|
|
}
|
|
|
|
fn test_reader() -> DbReader {
|
|
load_db(Path::new(&test_db_path())).expect("failed to load test DB")
|
|
}
|
|
|
|
#[test]
|
|
fn load_db_with_valid_path() {
|
|
let reader = load_db(Path::new(&test_db_path()));
|
|
assert!(reader.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn load_db_with_invalid_path() {
|
|
let reader = load_db(Path::new("/nonexistent/path.mmdb"));
|
|
assert!(reader.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn new_shared_creates_valid_shared_db() {
|
|
let reader = test_reader();
|
|
let shared = new_shared(reader);
|
|
let loaded = shared.load();
|
|
// Verify the shared DB is functional by doing a simple lookup
|
|
let ip: IpAddr = "1.1.1.1".parse().unwrap();
|
|
let result = loaded.lookup(ip);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_returns_some_for_known_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "1.1.1.1".parse().unwrap();
|
|
let result = lookup(&reader, ip);
|
|
assert!(result.is_some());
|
|
let info = result.unwrap();
|
|
assert_eq!(info.ip, "1.1.1.1");
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_returns_none_for_unroutable_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "0.0.0.0".parse().unwrap();
|
|
let result = lookup(&reader, ip);
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_proxy_returns_data_for_known_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "45.77.77.77".parse().unwrap();
|
|
let proxy = lookup_proxy(&reader, ip);
|
|
assert!(proxy.is_some());
|
|
let proxy = proxy.unwrap();
|
|
assert!(proxy.is_proxy);
|
|
assert!(proxy.is_hosting);
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_proxy_returns_none_for_unroutable_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "0.0.0.0".parse().unwrap();
|
|
let proxy = lookup_proxy(&reader, ip);
|
|
assert!(proxy.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_asn_number_for_known_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "1.1.1.1".parse().unwrap();
|
|
let asn = lookup_asn_number(&reader, ip);
|
|
assert_eq!(asn, Some(13335));
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_asn_number_returns_none_for_unroutable_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "0.0.0.0".parse().unwrap();
|
|
let asn = lookup_asn_number(&reader, ip);
|
|
assert!(asn.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_asn_org_for_known_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "1.1.1.1".parse().unwrap();
|
|
let org = lookup_asn_org(&reader, ip);
|
|
assert!(org.is_some());
|
|
assert_eq!(org.unwrap(), "Cloudflare, Inc.");
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_asn_org_returns_none_for_unroutable_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "0.0.0.0".parse().unwrap();
|
|
let org = lookup_asn_org(&reader, ip);
|
|
assert!(org.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_country_name_for_known_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "45.77.77.77".parse().unwrap();
|
|
let country = lookup_country_name(&reader, ip);
|
|
assert_eq!(country.as_deref(), Some("United States"));
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_country_name_returns_none_for_unroutable_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "0.0.0.0".parse().unwrap();
|
|
let country = lookup_country_name(&reader, ip);
|
|
assert!(country.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_city_name_for_known_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "45.77.77.77".parse().unwrap();
|
|
let city = lookup_city_name(&reader, ip);
|
|
assert_eq!(city.as_deref(), Some("Piscataway"));
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_city_name_returns_none_for_unroutable_ip() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "0.0.0.0".parse().unwrap();
|
|
let city = lookup_city_name(&reader, ip);
|
|
assert!(city.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_works_with_ipv6() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "2606:4700:4700::1111".parse().unwrap();
|
|
let result = lookup(&reader, ip);
|
|
assert!(result.is_some());
|
|
let info = result.unwrap();
|
|
assert_eq!(info.ip, "2606:4700:4700::1111");
|
|
assert!(info.asn.is_some());
|
|
assert_eq!(
|
|
info.asn.as_ref().unwrap().autonomous_system_number,
|
|
Some(13335)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn partial_lookups_work_with_ipv6() {
|
|
let reader = test_reader();
|
|
let ip: IpAddr = "2606:4700:4700::1111".parse().unwrap();
|
|
assert_eq!(lookup_asn_number(&reader, ip), Some(13335));
|
|
assert!(lookup_asn_org(&reader, ip).is_some());
|
|
assert!(lookup_proxy(&reader, ip).is_some());
|
|
}
|
|
}
|