mirror of
https://github.com/NetworkCats/rustyip.git
synced 2026-11-15 00:15:02 +00:00
perf: reduce allocations and deserialization in handlers and models
This commit is contained in:
Generated
-48
@@ -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",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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<Vec<u8>>;
|
||||
pub type SharedDb = Arc<ArcSwap<DbReader>>;
|
||||
@@ -23,3 +26,68 @@ pub fn lookup(db: &DbReader, ip: IpAddr) -> Option<IpInfo> {
|
||||
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)
|
||||
}
|
||||
|
||||
+36
-25
@@ -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<str>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@@ -26,7 +27,7 @@ pub struct AppState {
|
||||
struct IndexTemplate {
|
||||
ip: String,
|
||||
query: String,
|
||||
site_domain: String,
|
||||
site_domain: Arc<str>,
|
||||
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<IpQuery>,
|
||||
) -> Result<String, AppError> {
|
||||
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<IpQuery>,
|
||||
) -> Result<String, AppError> {
|
||||
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<IpQuery>,
|
||||
) -> Result<String, AppError> {
|
||||
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<IpQuery>,
|
||||
) -> Result<String, AppError> {
|
||||
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<IpQuery>,
|
||||
) -> Result<String, AppError> {
|
||||
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<IpQuery>,
|
||||
) -> Result<String, AppError> {
|
||||
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<IpQuery>,
|
||||
) -> Result<String, AppError> {
|
||||
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<IpQuery>,
|
||||
) -> Result<String, AppError> {
|
||||
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))
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+42
-36
@@ -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<String>,
|
||||
#[serde(rename = "en", skip_serializing_if = "Option::is_none")]
|
||||
pub english: Option<String>,
|
||||
#[serde(rename = "es", skip_serializing_if = "Option::is_none")]
|
||||
pub spanish: Option<String>,
|
||||
#[serde(rename = "fr", skip_serializing_if = "Option::is_none")]
|
||||
pub french: Option<String>,
|
||||
#[serde(rename = "ja", skip_serializing_if = "Option::is_none")]
|
||||
pub japanese: Option<String>,
|
||||
#[serde(rename = "pt-BR", skip_serializing_if = "Option::is_none")]
|
||||
pub brazilian_portuguese: Option<String>,
|
||||
#[serde(rename = "ru", skip_serializing_if = "Option::is_none")]
|
||||
pub russian: Option<String>,
|
||||
#[serde(rename = "zh-CN", skip_serializing_if = "Option::is_none")]
|
||||
pub simplified_chinese: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CityInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub geoname_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub names: Option<BTreeMap<String, String>>,
|
||||
pub names: Option<Names>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -169,7 +189,7 @@ pub struct ContinentInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub geoname_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub names: Option<BTreeMap<String, String>>,
|
||||
pub names: Option<Names>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -179,7 +199,7 @@ pub struct CountryInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iso_code: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub names: Option<BTreeMap<String, String>>,
|
||||
pub names: Option<Names>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -209,7 +229,7 @@ pub struct SubdivisionInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iso_code: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub names: Option<BTreeMap<String, String>>,
|
||||
pub names: Option<Names>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -235,36 +255,20 @@ pub struct ProxyInfo {
|
||||
|
||||
// --- Conversion from MMDB record to API output ---
|
||||
|
||||
fn convert_names(names: &MmdbNames<'_>) -> Option<BTreeMap<String, String>> {
|
||||
fn convert_names(names: &MmdbNames<'_>) -> Option<Names> {
|
||||
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<BTreeMap<String, String>>) -> String {
|
||||
pub fn get_en_name(names: &Option<Names>) -> &str {
|
||||
names
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("en"))
|
||||
.cloned()
|
||||
.and_then(|n| n.english.as_deref())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user