From d829c7b933298b1d15dd5fbe61e84f38154b0ce9 Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Fri, 4 Sep 2026 11:19:06 +0200 Subject: [PATCH 1/4] Move GeoIP to separate module --- ic-bn-lib/src/geoip.rs | 87 +++++++++++++++++++ ic-bn-lib/src/http/middleware/request_meta.rs | 63 +------------- ic-bn-lib/src/http/middleware/waf.rs | 7 +- ic-bn-lib/src/lib.rs | 1 + 4 files changed, 93 insertions(+), 65 deletions(-) create mode 100644 ic-bn-lib/src/geoip.rs diff --git a/ic-bn-lib/src/geoip.rs b/ic-bn-lib/src/geoip.rs new file mode 100644 index 0000000..4f971ee --- /dev/null +++ b/ic-bn-lib/src/geoip.rs @@ -0,0 +1,87 @@ +use std::{fmt::Display, net::IpAddr, ops::Deref, path::PathBuf}; + +use anyhow::Context; +use arrayvec::ArrayString; +use maxminddb::geoip2; +use serde::{Deserialize, Serialize}; + +use crate::Error; + +/// Two-letter country code. +/// See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] +pub struct CountryCode(pub ArrayString<2>); + +impl Deref for CountryCode { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.as_str() + } +} + +impl Display for CountryCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Looks up the client's country using his IP address +pub struct GeoIp { + db: maxminddb::Reader>, +} + +impl GeoIp { + /// Creates a new GeoIp instance from a provided database + pub fn new(db_path: &PathBuf) -> Result { + Ok(Self { + db: maxminddb::Reader::open_readfile(db_path).context("unable to load GeoIP DB")?, + }) + } + + /// Looks up the country code from an IP + pub fn lookup_country(&self, ip: IpAddr) -> Option { + let country: Option = self.db.lookup(ip).ok()?.decode().ok()?; + // Country code should always fit into 2-letter ArrayString. + // If for whatever reason it does not - return None. + Some(CountryCode(country?.country.iso_code?.try_into().ok()?)) + } +} + +#[cfg(test)] +mod test { + use std::net::Ipv4Addr; + + use super::*; + + // Known entries in the MaxMind test DB + const IP_KNOWN: Ipv4Addr = Ipv4Addr::new(89, 160, 20, 112); + const COUNTRY_KNOWN: &str = "SE"; + const IP_UNKNOWN: Ipv4Addr = Ipv4Addr::new(10, 10, 10, 10); + + fn test_db_path() -> PathBuf { + PathBuf::from(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test-data/geoip-test-db.mmdb" + )) + } + + #[test] + fn lookup_known_ip_returns_country_code() { + let geoip = GeoIp::new(&test_db_path()).unwrap(); + assert_eq!( + geoip + .lookup_country(IpAddr::V4(IP_KNOWN)) + .unwrap() + .0 + .as_str(), + COUNTRY_KNOWN + ); + } + + #[test] + fn lookup_unknown_ip_returns_none() { + let geoip = GeoIp::new(&test_db_path()).unwrap(); + assert!(geoip.lookup_country(IpAddr::V4(IP_UNKNOWN)).is_none()); + } +} diff --git a/ic-bn-lib/src/http/middleware/request_meta.rs b/ic-bn-lib/src/http/middleware/request_meta.rs index 852d775..186b2da 100644 --- a/ic-bn-lib/src/http/middleware/request_meta.rs +++ b/ic-bn-lib/src/http/middleware/request_meta.rs @@ -8,7 +8,6 @@ use std::{ }; use anyhow::Context; -use arrayvec::ArrayString; use axum::{ extract::{Request, State}, middleware::Next, @@ -17,11 +16,11 @@ use axum::{ use bytes::Bytes; use http::{HeaderMap, header::HeaderValue}; use ipnet::IpNet; -use maxminddb::geoip2; use serde::{Deserialize, Serialize}; use crate::{ Error, + geoip::GeoIp, http::{ headers::{X_REAL_IP, X_REQUEST_ID}, server::conn::ConnInfo, @@ -71,47 +70,6 @@ impl Display for RequestId { } } -/// Two-letter country code. -/// See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] -pub struct CountryCode(pub ArrayString<2>); - -impl Deref for CountryCode { - type Target = str; - - fn deref(&self) -> &Self::Target { - self.0.as_str() - } -} - -impl Display for CountryCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Looks up the client's country using his IP address -pub struct GeoIp { - db: maxminddb::Reader>, -} - -impl GeoIp { - /// Creates a new GeoIp instance from a provided database - pub fn new(db_path: &PathBuf) -> Result { - Ok(Self { - db: maxminddb::Reader::open_readfile(db_path).context("unable to load GeoIP DB")?, - }) - } - - /// Looks up the country code from an IP - pub fn lookup(&self, ip: IpAddr) -> Option { - let country: Option = self.db.lookup(ip).ok()?.decode().ok()?; - // Country code should always fit into 2-letter ArrayString. - // If for whatever reason it does not - return None. - Some(CountryCode(country?.country.iso_code?.try_into().ok()?)) - } -} - /// State for [`middleware`] pub struct RequestMetaState { /// Optional GeoIP database @@ -217,7 +175,7 @@ pub async fn middleware( request.extensions_mut().insert(v); // Look up country code if GeoIP is enabled - state.geoip.as_ref().and_then(|x| x.lookup(v.0)) + state.geoip.as_ref().and_then(|x| x.lookup_country(v.0)) }); if let Some(v) = country_code { @@ -265,7 +223,7 @@ mod test { use tower::Service; use super::*; - use crate::{hname, http::server::conn::ConnInfo, hval, network::Addr}; + use crate::{geoip::CountryCode, hname, http::server::conn::ConnInfo, hval, network::Addr}; const X_TEST_REQUEST_ID: &str = "x-test-request-id"; const X_TEST_REMOTE_ADDR: &str = "x-test-remote-addr"; @@ -284,21 +242,6 @@ mod test { )) } - #[test] - fn lookup_known_ip_returns_country_code() { - let geoip = GeoIp::new(&test_db_path()).unwrap(); - assert_eq!( - geoip.lookup(IpAddr::V4(IP_KNOWN)).unwrap().0.as_str(), - COUNTRY_KNOWN - ); - } - - #[test] - fn lookup_unknown_ip_returns_none() { - let geoip = GeoIp::new(&test_db_path()).unwrap(); - assert!(geoip.lookup(IpAddr::V4(IP_UNKNOWN)).is_none()); - } - #[test] fn extract_ip_ignores_header_from_untrusted_source() { let state = RequestMetaState::new(vec![], vec![]); diff --git a/ic-bn-lib/src/http/middleware/waf.rs b/ic-bn-lib/src/http/middleware/waf.rs index 716c824..08e4165 100644 --- a/ic-bn-lib/src/http/middleware/waf.rs +++ b/ic-bn-lib/src/http/middleware/waf.rs @@ -42,11 +42,8 @@ use tracing::warn; use url::Url; use crate::{ - http::{ - Error, - client::Client, - middleware::{RemoteAddr, request_meta::CountryCode}, - }, + geoip::CountryCode, + http::{Error, client::Client, middleware::RemoteAddr}, tasks::Run, }; diff --git a/ic-bn-lib/src/lib.rs b/ic-bn-lib/src/lib.rs index 7294fd2..d96eb05 100644 --- a/ic-bn-lib/src/lib.rs +++ b/ic-bn-lib/src/lib.rs @@ -9,6 +9,7 @@ #[cfg(feature = "custom-domains")] pub mod custom_domains; pub mod dns; +pub mod geoip; pub mod health; pub mod http; pub mod ic; From 30486fbbed5095dcb7f7b3101a615afff17bb55b Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Mon, 7 Sep 2026 14:31:35 +0200 Subject: [PATCH 2/4] Add lookup_city to GeoIP --- Cargo.lock | 11 ++++++++++ Cargo.toml | 2 +- ic-bn-lib/src/geoip.rs | 48 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2bbc6c8..b191314 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3696,7 +3696,9 @@ dependencies = [ "ipnetwork", "log", "memchr", + "memmap2", "serde", + "simdutf8", "thiserror 2.0.20", ] @@ -3706,6 +3708,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + [[package]] name = "merlin" version = "3.0.0" diff --git a/Cargo.toml b/Cargo.toml index 19a2cf7..d631a7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ mail-parser = { version = "0.11.3", features = ["full_encoding"] } mail-send = { version = "0.6.0", default-features = false, features = [ "builder", ] } -maxminddb = "0.29" +maxminddb = { version = "0.29", features = ["mmap", "simdutf8"] } mockall = "0.15" mock-io = { version = "0.3.2", features = ["full"] } moka = { version = "0.12.15", features = ["sync"] } diff --git a/ic-bn-lib/src/geoip.rs b/ic-bn-lib/src/geoip.rs index 4f971ee..b83aa36 100644 --- a/ic-bn-lib/src/geoip.rs +++ b/ic-bn-lib/src/geoip.rs @@ -2,10 +2,13 @@ use std::{fmt::Display, net::IpAddr, ops::Deref, path::PathBuf}; use anyhow::Context; use arrayvec::ArrayString; +use clap::ArgAction::Count; use maxminddb::geoip2; use serde::{Deserialize, Serialize}; -use crate::Error; +use crate::{Error, TruncatesString}; + +const CITY_NAME_MAX_LENGTH: usize = 32; /// Two-letter country code. /// See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 @@ -26,6 +29,19 @@ impl Display for CountryCode { } } +/// Location representation +pub struct Location { + pub lat: f64, + pub lon: f64, +} + +/// GeoIP lookup city representation. +pub struct City { + pub name: Option>, + pub country_code: Option, + pub location: Option, +} + /// Looks up the client's country using his IP address pub struct GeoIp { db: maxminddb::Reader>, @@ -46,6 +62,36 @@ impl GeoIp { // If for whatever reason it does not - return None. Some(CountryCode(country?.country.iso_code?.try_into().ok()?)) } + + /// Looks up the city from an IP + pub fn lookup_city(&self, ip: IpAddr) -> Option { + let city: geoip2::City = self.db.lookup(ip).ok()?.decode().ok()??; + + Some(City { + // Try English, then German, otherwise None + name: city + .city + .names + .english + .or(city.city.names.german) + // SAFETY: truncate_bytes makes the string *no longer* than CITY_NAME_MAX_LENGTH + // so it will always fit into Arraystring + .map(|x| x.truncate_bytes(CITY_NAME_MAX_LENGTH).try_into().unwrap()), + + country_code: city + .country + .iso_code + .and_then(|x| x.try_into().ok()) + .map(CountryCode), + + // Location is Some only when both lat & lon are available + location: city + .location + .latitude + .zip(city.location.longitude) + .map(|(lat, lon)| Location { lat, lon }), + }) + } } #[cfg(test)] From 51b38c7622ab5933e742cf64adffd33f08dfe42b Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Mon, 7 Sep 2026 14:47:29 +0200 Subject: [PATCH 3/4] Add tests --- ic-bn-lib/src/geoip.rs | 256 +++++++++++++++++--- ic-bn-lib/test-data/geoip-city-test-db.mmdb | Bin 0 -> 22569 bytes 2 files changed, 228 insertions(+), 28 deletions(-) create mode 100644 ic-bn-lib/test-data/geoip-city-test-db.mmdb diff --git a/ic-bn-lib/src/geoip.rs b/ic-bn-lib/src/geoip.rs index b83aa36..c1b9c1b 100644 --- a/ic-bn-lib/src/geoip.rs +++ b/ic-bn-lib/src/geoip.rs @@ -2,7 +2,6 @@ use std::{fmt::Display, net::IpAddr, ops::Deref, path::PathBuf}; use anyhow::Context; use arrayvec::ArrayString; -use clap::ArgAction::Count; use maxminddb::geoip2; use serde::{Deserialize, Serialize}; @@ -42,6 +41,35 @@ pub struct City { pub location: Option, } +impl From> for City { + fn from(city: geoip2::City<'_>) -> Self { + Self { + // Try English, then German, otherwise None + name: city + .city + .names + .english + .or(city.city.names.german) + // SAFETY: truncate_bytes makes the string *no longer* than CITY_NAME_MAX_LENGTH + // so it will always fit into Arraystring + .map(|x| x.truncate_bytes(CITY_NAME_MAX_LENGTH).try_into().unwrap()), + + country_code: city + .country + .iso_code + .and_then(|x| x.try_into().ok()) + .map(CountryCode), + + // Location is Some only when both lat & lon are available + location: city + .location + .latitude + .zip(city.location.longitude) + .map(|(lat, lon)| Location { lat, lon }), + } + } +} + /// Looks up the client's country using his IP address pub struct GeoIp { db: maxminddb::Reader>, @@ -66,45 +94,31 @@ impl GeoIp { /// Looks up the city from an IP pub fn lookup_city(&self, ip: IpAddr) -> Option { let city: geoip2::City = self.db.lookup(ip).ok()?.decode().ok()??; - - Some(City { - // Try English, then German, otherwise None - name: city - .city - .names - .english - .or(city.city.names.german) - // SAFETY: truncate_bytes makes the string *no longer* than CITY_NAME_MAX_LENGTH - // so it will always fit into Arraystring - .map(|x| x.truncate_bytes(CITY_NAME_MAX_LENGTH).try_into().unwrap()), - - country_code: city - .country - .iso_code - .and_then(|x| x.try_into().ok()) - .map(CountryCode), - - // Location is Some only when both lat & lon are available - location: city - .location - .latitude - .zip(city.location.longitude) - .map(|(lat, lon)| Location { lat, lon }), - }) + Some(city.into()) } } #[cfg(test)] mod test { - use std::net::Ipv4Addr; + use std::net::{Ipv4Addr, Ipv6Addr}; use super::*; - // Known entries in the MaxMind test DB + // Known entries in the MaxMind test DBs (present in both Country & City DBs) const IP_KNOWN: Ipv4Addr = Ipv4Addr::new(89, 160, 20, 112); const COUNTRY_KNOWN: &str = "SE"; + const CITY_KNOWN: &str = "Linköping"; + const LAT_KNOWN: f64 = 58.4167; + const LON_KNOWN: f64 = 15.6167; const IP_UNKNOWN: Ipv4Addr = Ipv4Addr::new(10, 10, 10, 10); + // City DB: country & location, but no city name + const IP_NO_CITY_NAME: Ipv4Addr = Ipv4Addr::new(149, 101, 100, 1); + // City DB: location only, no country and no city name + const IP_LOCATION_ONLY: Ipv6Addr = Ipv6Addr::new(0x2a02, 0xd500, 0, 0, 0, 0, 0, 1); + // City DB: record exists but is empty + const IP_EMPTY_RECORD: Ipv4Addr = Ipv4Addr::new(2, 3, 3, 1); + /// MaxMind GeoIP2-Country test DB fn test_db_path() -> PathBuf { PathBuf::from(concat!( env!("CARGO_MANIFEST_DIR"), @@ -112,6 +126,44 @@ mod test { )) } + /// MaxMind GeoIP2-City test DB + fn test_city_db_path() -> PathBuf { + PathBuf::from(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test-data/geoip-city-test-db.mmdb" + )) + } + + /// Builds a raw MaxMind city record with only the fields we care about + fn geoip2_city<'a>( + english: Option<&'a str>, + german: Option<&'a str>, + iso_code: Option<&'a str>, + latitude: Option, + longitude: Option, + ) -> geoip2::City<'a> { + geoip2::City { + city: geoip2::city::City { + names: geoip2::Names { + english, + german, + ..Default::default() + }, + ..Default::default() + }, + country: geoip2::city::Country { + iso_code, + ..Default::default() + }, + location: geoip2::city::Location { + latitude, + longitude, + ..Default::default() + }, + ..Default::default() + } + } + #[test] fn lookup_known_ip_returns_country_code() { let geoip = GeoIp::new(&test_db_path()).unwrap(); @@ -130,4 +182,152 @@ mod test { let geoip = GeoIp::new(&test_db_path()).unwrap(); assert!(geoip.lookup_country(IpAddr::V4(IP_UNKNOWN)).is_none()); } + + #[test] + fn lookup_city_known_ip_returns_name_country_and_location() { + let geoip = GeoIp::new(&test_city_db_path()).unwrap(); + let city = geoip.lookup_city(IpAddr::V4(IP_KNOWN)).unwrap(); + + assert_eq!(city.name.unwrap().as_str(), CITY_KNOWN); + assert_eq!(city.country_code.unwrap().0.as_str(), COUNTRY_KNOWN); + + let location = city.location.unwrap(); + assert_eq!(location.lat, LAT_KNOWN); + assert_eq!(location.lon, LON_KNOWN); + } + + #[test] + fn lookup_city_unknown_ip_returns_none() { + let geoip = GeoIp::new(&test_city_db_path()).unwrap(); + assert!(geoip.lookup_city(IpAddr::V4(IP_UNKNOWN)).is_none()); + } + + #[test] + fn lookup_city_without_name_returns_country_and_location() { + let geoip = GeoIp::new(&test_city_db_path()).unwrap(); + let city = geoip.lookup_city(IpAddr::V4(IP_NO_CITY_NAME)).unwrap(); + + assert!(city.name.is_none()); + assert_eq!(city.country_code.unwrap().0.as_str(), "US"); + + let location = city.location.unwrap(); + assert_eq!(location.lat, 37.751); + assert_eq!(location.lon, -97.822); + } + + #[test] + fn lookup_city_ipv6_with_location_only() { + let geoip = GeoIp::new(&test_city_db_path()).unwrap(); + let city = geoip.lookup_city(IpAddr::V6(IP_LOCATION_ONLY)).unwrap(); + + assert!(city.name.is_none()); + assert!(city.country_code.is_none()); + + let location = city.location.unwrap(); + assert_eq!(location.lat, 48.69096); + assert_eq!(location.lon, 9.14062); + } + + #[test] + fn lookup_city_empty_record_returns_city_without_fields() { + let geoip = GeoIp::new(&test_city_db_path()).unwrap(); + let city = geoip.lookup_city(IpAddr::V4(IP_EMPTY_RECORD)).unwrap(); + + assert!(city.name.is_none()); + assert!(city.country_code.is_none()); + assert!(city.location.is_none()); + } + + #[test] + fn lookup_city_with_country_db_returns_country_only() { + let geoip = GeoIp::new(&test_db_path()).unwrap(); + let city = geoip.lookup_city(IpAddr::V4(IP_KNOWN)).unwrap(); + + assert!(city.name.is_none()); + assert_eq!(city.country_code.unwrap().0.as_str(), COUNTRY_KNOWN); + assert!(city.location.is_none()); + } + + #[test] + fn city_from_geoip2_prefers_english_name() { + let city = City::from(geoip2_city( + Some("Singapore"), + Some("Singapur"), + None, + None, + None, + )); + assert_eq!(city.name.unwrap().as_str(), "Singapore"); + } + + #[test] + fn city_from_geoip2_falls_back_to_german_name() { + let city = City::from(geoip2_city(None, Some("Singapur"), None, None, None)); + assert_eq!(city.name.unwrap().as_str(), "Singapur"); + } + + #[test] + fn city_from_geoip2_without_names_has_no_name() { + let city = City::from(geoip2_city(None, None, None, None, None)); + assert!(city.name.is_none()); + } + + #[test] + fn city_from_geoip2_truncates_long_name() { + // 58 ASCII bytes, truncated to exactly CITY_NAME_MAX_LENGTH bytes + let long = "Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch"; + assert!(long.len() > CITY_NAME_MAX_LENGTH); + let city = City::from(geoip2_city(Some(long), None, None, None, None)); + assert_eq!(city.name.unwrap().as_str(), &long[..CITY_NAME_MAX_LENGTH]); + + // 17 two-byte chars = 34 bytes, byte 32 is a char boundary -> 16 chars kept + let name = "ä".repeat(17); + let city = City::from(geoip2_city(Some(&name), None, None, None, None)); + assert_eq!(city.name.unwrap().as_str(), "ä".repeat(16).as_str()); + + // 11 three-byte chars = 33 bytes, byte 32 is mid-char -> 10 chars (30 bytes) kept + let name = "€".repeat(11); + let city = City::from(geoip2_city(Some(&name), None, None, None, None)); + assert_eq!(city.name.unwrap().as_str(), "€".repeat(10).as_str()); + + // Truncation also applies to the German fallback + let city = City::from(geoip2_city(None, Some(long), None, None, None)); + assert_eq!(city.name.unwrap().as_str(), &long[..CITY_NAME_MAX_LENGTH]); + } + + #[test] + fn city_from_geoip2_country_code() { + let city = City::from(geoip2_city(None, None, Some("SE"), None, None)); + assert_eq!(city.country_code.unwrap().0.as_str(), "SE"); + + // ISO code that doesn't fit into two letters is dropped + let city = City::from(geoip2_city(None, None, Some("SWE"), None, None)); + assert!(city.country_code.is_none()); + + let city = City::from(geoip2_city(None, None, None, None, None)); + assert!(city.country_code.is_none()); + } + + #[test] + fn city_from_geoip2_location_requires_both_coordinates() { + let city = City::from(geoip2_city( + None, + None, + None, + Some(LAT_KNOWN), + Some(LON_KNOWN), + )); + let location = city.location.unwrap(); + assert_eq!(location.lat, LAT_KNOWN); + assert_eq!(location.lon, LON_KNOWN); + + let city = City::from(geoip2_city(None, None, None, Some(LAT_KNOWN), None)); + assert!(city.location.is_none()); + + let city = City::from(geoip2_city(None, None, None, None, Some(LON_KNOWN))); + assert!(city.location.is_none()); + + let city = City::from(geoip2_city(None, None, None, None, None)); + assert!(city.location.is_none()); + } } diff --git a/ic-bn-lib/test-data/geoip-city-test-db.mmdb b/ic-bn-lib/test-data/geoip-city-test-db.mmdb new file mode 100644 index 0000000000000000000000000000000000000000..e20846ac1757a3745ae41411757ed3817536e3be GIT binary patch literal 22569 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 Date: Tue, 8 Sep 2026 14:55:57 +0200 Subject: [PATCH 4/4] Change RequestMeta API a bit --- ic-bn-lib/src/http/middleware/request_meta.rs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/ic-bn-lib/src/http/middleware/request_meta.rs b/ic-bn-lib/src/http/middleware/request_meta.rs index 186b2da..ef05b8a 100644 --- a/ic-bn-lib/src/http/middleware/request_meta.rs +++ b/ic-bn-lib/src/http/middleware/request_meta.rs @@ -73,7 +73,7 @@ impl Display for RequestId { /// State for [`middleware`] pub struct RequestMetaState { /// Optional GeoIP database - geoip: Option, + geoip: Option>, /// Trust incoming headers from these subnets for the purpose of IP address extraction. /// If not set - headers will not be used. @@ -94,23 +94,36 @@ impl RequestMetaState { } } - /// Creates a new [`RequestMetaState`] with a GeoIP DB - pub fn new_with_geoip( + /// Creates a new [`RequestMetaState`] with a GeoIP provided + pub const fn new_with_geoip( + trust_ip_from: Vec, + trust_request_id_from: Vec, + geoip: Option>, + ) -> Self { + Self { + geoip, + trust_ip_from, + trust_request_id_from, + } + } + + /// Creates a new [`RequestMetaState`] with a GeoIP DB path + pub fn new_with_geoip_db( trust_ip_from: Vec, trust_request_id_from: Vec, geoip_db_path: Option, ) -> Result { let geoip = if let Some(v) = geoip_db_path { - Some(GeoIp::new(&v).context("unable to init GeoIP")?) + Some(Arc::new(GeoIp::new(&v).context("unable to init GeoIP")?)) } else { None }; - Ok(Self { - geoip, + Ok(Self::new_with_geoip( trust_ip_from, trust_request_id_from, - }) + geoip, + )) } /// Extracts remote IP address from the `x-real-ip` header if remote is trusted & header exists @@ -600,7 +613,8 @@ mod test { #[tokio::test] async fn middleware_geoip_unknown_ip_no_country_code() { - let state = RequestMetaState::new_with_geoip(vec![], vec![], Some(test_db_path())).unwrap(); + let state = + RequestMetaState::new_with_geoip_db(vec![], vec![], Some(test_db_path())).unwrap(); let mut app = app(state); let req = Request::builder().body(Body::empty()).unwrap(); @@ -614,7 +628,8 @@ mod test { #[tokio::test] async fn middleware_geoip_known_ip_attaches_country_code_to_request_and_response() { - let state = RequestMetaState::new_with_geoip(vec![], vec![], Some(test_db_path())).unwrap(); + let state = + RequestMetaState::new_with_geoip_db(vec![], vec![], Some(test_db_path())).unwrap(); let mut app = app(state); let req = Request::builder().body(Body::empty()).unwrap();