diff --git a/Cargo.lock b/Cargo.lock index cc4fa6e..3154593 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -582,15 +582,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.29" @@ -679,29 +670,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -846,15 +814,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - [[package]] name = "regex-automata" version = "0.4.14" @@ -997,12 +956,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "serde" version = "1.0.228" @@ -1222,7 +1175,6 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", diff --git a/Cargo.toml b/Cargo.toml index b6a4561..9454831 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] axum = "0.8.8" -tokio = { version = "1.49.0", features = ["full"] } +tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros", "net", "signal", "time", "fs", "io-util"] } askama = "0.15.4" askama_web = { version = "0.15.1", features = ["axum-0.8"] } maxminddb = "0.27.3" diff --git a/src/db.rs b/src/db.rs index f28e5a6..7949a22 100644 --- a/src/db.rs +++ b/src/db.rs @@ -4,8 +4,11 @@ use std::sync::Arc; use arc_swap::ArcSwap; use maxminddb::Reader; +use serde::Deserialize; -use crate::models::{from_mmdb_record, IpInfo, MmdbRecord}; +use crate::models::{ + from_mmdb_record, IpInfo, MmdbAsn, MmdbCity, MmdbCountry, MmdbProxy, MmdbRecord, ProxyInfo, +}; pub type DbReader = Reader>; pub type SharedDb = Arc>; @@ -23,3 +26,68 @@ pub fn lookup(db: &DbReader, ip: IpAddr) -> Option { 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 { + 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 { + 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 { + 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 { + 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 { + let result = db.lookup(ip).ok()?; + let record: CityOnly<'_> = result.decode().ok()??; + record.city.names.english.map(str::to_owned) +} diff --git a/src/handlers.rs b/src/handlers.rs index b46b378..7d2638a 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,4 +1,5 @@ use std::net::IpAddr; +use std::sync::Arc; use askama::Template; use axum::extract::{Query, State}; @@ -18,7 +19,7 @@ pub struct IpQuery { #[derive(Clone)] pub struct AppState { pub db: SharedDb, - pub site_domain: String, + pub site_domain: Arc, } #[derive(Template)] @@ -26,7 +27,7 @@ pub struct AppState { struct IndexTemplate { ip: String, query: String, - site_domain: String, + site_domain: Arc, asn: String, org: String, country: String, @@ -75,21 +76,21 @@ fn format_asn(info: &IpInfo) -> String { .unwrap_or_default() } -fn format_org(info: &IpInfo) -> String { +fn format_org(info: &IpInfo) -> &str { info.asn .as_ref() - .and_then(|a| a.autonomous_system_organization.clone()) + .and_then(|a| a.autonomous_system_organization.as_deref()) .unwrap_or_default() } -fn format_country(info: &IpInfo) -> String { +fn format_country(info: &IpInfo) -> &str { info.country .as_ref() .map(|c| get_en_name(&c.names)) .unwrap_or_default() } -fn format_city(info: &IpInfo) -> String { +fn format_city(info: &IpInfo) -> &str { info.city .as_ref() .map(|c| get_en_name(&c.names)) @@ -125,9 +126,9 @@ pub async fn root( query: query.ip.unwrap_or_default(), site_domain: state.site_domain.clone(), asn: format_asn(&info), - org: format_org(&info), - country: format_country(&info), - city: format_city(&info), + org: format_org(&info).to_owned(), + country: format_country(&info).to_owned(), + city: format_city(&info).to_owned(), is_proxy: info.proxy.is_proxy, is_vpn: info.proxy.is_vpn, is_hosting: info.proxy.is_hosting, @@ -165,8 +166,11 @@ pub async fn asn_handler( Query(query): Query, ) -> Result { let ip = resolve_ip(&headers, &query)?; - let info = lookup_ip(&state.db, ip)?; - Ok(format!("{}\n", format_asn(&info))) + let reader = state.db.load(); + let text = db::lookup_asn_number(&reader, ip) + .map(|n| format!("AS{n}")) + .unwrap_or_default(); + Ok(format!("{text}\n")) } pub async fn org_handler( @@ -175,8 +179,9 @@ pub async fn org_handler( Query(query): Query, ) -> Result { let ip = resolve_ip(&headers, &query)?; - let info = lookup_ip(&state.db, ip)?; - Ok(format!("{}\n", format_org(&info))) + let reader = state.db.load(); + let org = db::lookup_asn_org(&reader, ip).unwrap_or_default(); + Ok(format!("{org}\n")) } pub async fn country_handler( @@ -185,8 +190,9 @@ pub async fn country_handler( Query(query): Query, ) -> Result { let ip = resolve_ip(&headers, &query)?; - let info = lookup_ip(&state.db, ip)?; - Ok(format!("{}\n", format_country(&info))) + let reader = state.db.load(); + let country = db::lookup_country_name(&reader, ip).unwrap_or_default(); + Ok(format!("{country}\n")) } pub async fn city_handler( @@ -195,8 +201,9 @@ pub async fn city_handler( Query(query): Query, ) -> Result { let ip = resolve_ip(&headers, &query)?; - let info = lookup_ip(&state.db, ip)?; - Ok(format!("{}\n", format_city(&info))) + let reader = state.db.load(); + let city = db::lookup_city_name(&reader, ip).unwrap_or_default(); + Ok(format!("{city}\n")) } pub async fn proxy_handler( @@ -205,8 +212,9 @@ pub async fn proxy_handler( Query(query): Query, ) -> Result { let ip = resolve_ip(&headers, &query)?; - let info = lookup_ip(&state.db, ip)?; - Ok(format!("{}\n", info.proxy.is_proxy)) + let reader = state.db.load(); + let proxy = db::lookup_proxy(&reader, ip).ok_or(AppError::DbLookupFailed)?; + Ok(format!("{}\n", proxy.is_proxy)) } pub async fn vpn_handler( @@ -215,8 +223,9 @@ pub async fn vpn_handler( Query(query): Query, ) -> Result { let ip = resolve_ip(&headers, &query)?; - let info = lookup_ip(&state.db, ip)?; - Ok(format!("{}\n", info.proxy.is_vpn)) + let reader = state.db.load(); + let proxy = db::lookup_proxy(&reader, ip).ok_or(AppError::DbLookupFailed)?; + Ok(format!("{}\n", proxy.is_vpn)) } pub async fn hosting_handler( @@ -225,8 +234,9 @@ pub async fn hosting_handler( Query(query): Query, ) -> Result { let ip = resolve_ip(&headers, &query)?; - let info = lookup_ip(&state.db, ip)?; - Ok(format!("{}\n", info.proxy.is_hosting)) + let reader = state.db.load(); + let proxy = db::lookup_proxy(&reader, ip).ok_or(AppError::DbLookupFailed)?; + Ok(format!("{}\n", proxy.is_hosting)) } pub async fn tor_handler( @@ -235,6 +245,7 @@ pub async fn tor_handler( Query(query): Query, ) -> Result { let ip = resolve_ip(&headers, &query)?; - let info = lookup_ip(&state.db, ip)?; - Ok(format!("{}\n", info.proxy.is_tor)) + let reader = state.db.load(); + let proxy = db::lookup_proxy(&reader, ip).ok_or(AppError::DbLookupFailed)?; + Ok(format!("{}\n", proxy.is_tor)) } diff --git a/src/main.rs b/src/main.rs index 7477300..085e748 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ async fn main() { let state = handlers::AppState { db: shared_db, - site_domain: config.site_domain, + site_domain: config.site_domain.into(), }; let app = routes::build_router(state); diff --git a/src/models.rs b/src/models.rs index 8a6fbae..0ea2e1b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::fmt::Write; use std::net::IpAddr; use serde::{Deserialize, Serialize}; @@ -154,12 +154,32 @@ pub struct IpInfo { pub proxy: ProxyInfo, } +#[derive(Debug, Serialize)] +pub struct Names { + #[serde(rename = "de", skip_serializing_if = "Option::is_none")] + pub german: Option, + #[serde(rename = "en", skip_serializing_if = "Option::is_none")] + pub english: Option, + #[serde(rename = "es", skip_serializing_if = "Option::is_none")] + pub spanish: Option, + #[serde(rename = "fr", skip_serializing_if = "Option::is_none")] + pub french: Option, + #[serde(rename = "ja", skip_serializing_if = "Option::is_none")] + pub japanese: Option, + #[serde(rename = "pt-BR", skip_serializing_if = "Option::is_none")] + pub brazilian_portuguese: Option, + #[serde(rename = "ru", skip_serializing_if = "Option::is_none")] + pub russian: Option, + #[serde(rename = "zh-CN", skip_serializing_if = "Option::is_none")] + pub simplified_chinese: Option, +} + #[derive(Debug, Serialize)] pub struct CityInfo { #[serde(skip_serializing_if = "Option::is_none")] pub geoname_id: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub names: Option>, + pub names: Option, } #[derive(Debug, Serialize)] @@ -169,7 +189,7 @@ pub struct ContinentInfo { #[serde(skip_serializing_if = "Option::is_none")] pub geoname_id: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub names: Option>, + pub names: Option, } #[derive(Debug, Serialize)] @@ -179,7 +199,7 @@ pub struct CountryInfo { #[serde(skip_serializing_if = "Option::is_none")] pub iso_code: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub names: Option>, + pub names: Option, } #[derive(Debug, Serialize)] @@ -209,7 +229,7 @@ pub struct SubdivisionInfo { #[serde(skip_serializing_if = "Option::is_none")] pub iso_code: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub names: Option>, + pub names: Option, } #[derive(Debug, Serialize)] @@ -235,36 +255,20 @@ pub struct ProxyInfo { // --- Conversion from MMDB record to API output --- -fn convert_names(names: &MmdbNames<'_>) -> Option> { +fn convert_names(names: &MmdbNames<'_>) -> Option { if names.is_empty() { return None; } - let mut map = BTreeMap::new(); - if let Some(v) = names.german { - map.insert("de".to_string(), v.to_string()); - } - if let Some(v) = names.english { - map.insert("en".to_string(), v.to_string()); - } - if let Some(v) = names.spanish { - map.insert("es".to_string(), v.to_string()); - } - if let Some(v) = names.french { - map.insert("fr".to_string(), v.to_string()); - } - if let Some(v) = names.japanese { - map.insert("ja".to_string(), v.to_string()); - } - if let Some(v) = names.brazilian_portuguese { - map.insert("pt-BR".to_string(), v.to_string()); - } - if let Some(v) = names.russian { - map.insert("ru".to_string(), v.to_string()); - } - if let Some(v) = names.simplified_chinese { - map.insert("zh-CN".to_string(), v.to_string()); - } - Some(map) + Some(Names { + german: names.german.map(str::to_owned), + english: names.english.map(str::to_owned), + spanish: names.spanish.map(str::to_owned), + french: names.french.map(str::to_owned), + japanese: names.japanese.map(str::to_owned), + brazilian_portuguese: names.brazilian_portuguese.map(str::to_owned), + russian: names.russian.map(str::to_owned), + simplified_chinese: names.simplified_chinese.map(str::to_owned), + }) } fn has_city(c: &MmdbCity<'_>) -> bool { @@ -396,8 +400,11 @@ pub fn from_mmdb_record(ip: IpAddr, record: &MmdbRecord<'_>) -> IpInfo { is_anonymous: record.proxy.is_anonymous, }; + let mut ip_str = String::with_capacity(45); + write!(ip_str, "{ip}").expect("IP address formatting cannot fail"); + IpInfo { - ip: ip.to_string(), + ip: ip_str, city, continent, country, @@ -410,10 +417,9 @@ pub fn from_mmdb_record(ip: IpAddr, record: &MmdbRecord<'_>) -> IpInfo { } } -pub fn get_en_name(names: &Option>) -> String { +pub fn get_en_name(names: &Option) -> &str { names .as_ref() - .and_then(|m| m.get("en")) - .cloned() + .and_then(|n| n.english.as_deref()) .unwrap_or_default() } diff --git a/tests/integration.rs b/tests/integration.rs index 9071a77..3cc30fb 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -22,7 +22,7 @@ fn build_test_app() -> axum::Router { let shared_db = db::new_shared(reader); let state = AppState { db: shared_db, - site_domain: "test.example.com".to_string(), + site_domain: "test.example.com".into(), }; build_router(state) }