v0.28.0
This commit is contained in:
1 parent
c24bc4279b
commit
3b3f0db377
84 files changed
+2686
-392
No files matched your search
+93
-30
@@ -1,8 +1,16 @@
|
||||
//! Minimal reader for sapics/ip-location-db user-country CSV files.
|
||||
//! Installer verification pins each downloaded file to the publisher-provided
|
||||
//! SHA-256 checksum; runtime parsing stays dependency-free and bounded.
|
||||
//! Bounded country GeoIP reader for direct CSV pairs and native MaxMind DB files.
|
||||
//! Managed downloads use direct HTTPS sources; publisher checksum URLs are optional.
|
||||
mod mmdb;
|
||||
mod update;
|
||||
mod update_http;
|
||||
pub use update::{
|
||||
UpdateResult, UpdateSettings, DEFAULT_INTERVAL_SECONDS, DEFAULT_IPV4_URL, DEFAULT_IPV6_URL,
|
||||
DEFAULT_MAX_BYTES, DEFAULT_MMDB_URL,
|
||||
};
|
||||
pub(crate) use update::{poll_loop as update_poll_loop, update_now};
|
||||
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader},
|
||||
@@ -25,52 +33,101 @@ struct V6Range {
|
||||
end: u128,
|
||||
country: [u8; 2],
|
||||
}
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DatabaseFormat {
|
||||
CsvPair,
|
||||
Mmdb,
|
||||
}
|
||||
|
||||
impl Default for DatabaseFormat {
|
||||
fn default() -> Self {
|
||||
Self::CsvPair
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Backend {
|
||||
Csv { ipv4: Vec<V4Range>, ipv6: Vec<V6Range> },
|
||||
Mmdb(mmdb::Database),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Database {
|
||||
ipv4: Vec<V4Range>,
|
||||
ipv6: Vec<V6Range>,
|
||||
backend: Backend,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct View {
|
||||
pub loaded: bool,
|
||||
pub ipv4_ranges: usize,
|
||||
pub ipv6_ranges: usize,
|
||||
pub source: &'static str,
|
||||
pub source: String,
|
||||
pub format: &'static str,
|
||||
}
|
||||
impl Database {
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
Self { backend: Backend::Csv { ipv4: Vec::new(), ipv6: Vec::new() } }
|
||||
}
|
||||
pub fn load(directory: &Path) -> Result<Self> {
|
||||
let ipv4 = parse_v4(&directory.join("user-country-ipv4.csv"))?;
|
||||
let ipv6 = parse_v6(&directory.join("user-country-ipv6.csv"))?;
|
||||
ensure!(
|
||||
!ipv4.is_empty() && !ipv6.is_empty(),
|
||||
"GeoIP country database is empty"
|
||||
);
|
||||
Ok(Self { ipv4, ipv6 })
|
||||
if directory.join("country.mmdb").is_file() {
|
||||
Self::load_format(directory, DatabaseFormat::Mmdb)
|
||||
} else {
|
||||
Self::load_format(directory, DatabaseFormat::CsvPair)
|
||||
}
|
||||
}
|
||||
pub fn load_format(directory: &Path, format: DatabaseFormat) -> Result<Self> {
|
||||
match format {
|
||||
DatabaseFormat::CsvPair => {
|
||||
let ipv4 = parse_v4(&directory.join("user-country-ipv4.csv"))?;
|
||||
let ipv6 = parse_v6(&directory.join("user-country-ipv6.csv"))?;
|
||||
ensure!(!ipv4.is_empty() && !ipv6.is_empty(), "GeoIP country database is empty");
|
||||
Ok(Self { backend: Backend::Csv { ipv4, ipv6 } })
|
||||
}
|
||||
DatabaseFormat::Mmdb => Ok(Self {
|
||||
backend: Backend::Mmdb(mmdb::Database::load(&directory.join("country.mmdb"), MAX_FILE_BYTES)?),
|
||||
}),
|
||||
}
|
||||
}
|
||||
pub fn loaded(&self) -> bool {
|
||||
!self.ipv4.is_empty() || !self.ipv6.is_empty()
|
||||
match &self.backend {
|
||||
Backend::Csv { ipv4, ipv6 } => !ipv4.is_empty() || !ipv6.is_empty(),
|
||||
Backend::Mmdb(_) => true,
|
||||
}
|
||||
}
|
||||
pub fn view(&self) -> View {
|
||||
View {
|
||||
loaded: self.loaded(),
|
||||
ipv4_ranges: self.ipv4.len(),
|
||||
ipv6_ranges: self.ipv6.len(),
|
||||
source: "sapics/ip-location-db user-country (PDDL)",
|
||||
match &self.backend {
|
||||
Backend::Csv { ipv4, ipv6 } => View {
|
||||
loaded: self.loaded(),
|
||||
ipv4_ranges: ipv4.len(),
|
||||
ipv6_ranges: ipv6.len(),
|
||||
source: "CSV country ranges".into(),
|
||||
format: "csv_pair",
|
||||
},
|
||||
Backend::Mmdb(database) => View {
|
||||
loaded: true,
|
||||
ipv4_ranges: 0,
|
||||
ipv6_ranges: 0,
|
||||
source: format!("MaxMind DB ({})", database.database_type()),
|
||||
format: "mmdb",
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn country(&self, ip: IpAddr) -> Option<String> {
|
||||
let code = match crate::vhost::canonical_ip(ip) {
|
||||
IpAddr::V4(ip) => find_v4(&self.ipv4, u32::from(ip)),
|
||||
IpAddr::V6(ip) => find_v6(&self.ipv6, u128::from(ip)),
|
||||
}?;
|
||||
Some(String::from_utf8_lossy(&code).into_owned())
|
||||
let ip = crate::vhost::canonical_ip(ip);
|
||||
match &self.backend {
|
||||
Backend::Csv { ipv4, ipv6 } => {
|
||||
let code = match ip {
|
||||
IpAddr::V4(ip) => find_v4(ipv4, u32::from(ip)),
|
||||
IpAddr::V6(ip) => find_v6(ipv6, u128::from(ip)),
|
||||
}?;
|
||||
Some(String::from_utf8_lossy(&code).into_owned())
|
||||
}
|
||||
Backend::Mmdb(database) => database.country(ip),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn country_code(raw: &str) -> Option<[u8; 2]> {
|
||||
pub(super) fn normalize_country_code(raw: &str) -> Option<[u8; 2]> {
|
||||
let raw = raw.trim().trim_matches('"').as_bytes();
|
||||
if raw.len() == 2 && raw.iter().all(|b| b.is_ascii_alphabetic()) {
|
||||
Some([raw[0].to_ascii_uppercase(), raw[1].to_ascii_uppercase()])
|
||||
@@ -129,7 +186,7 @@ fn parse_v4(path: &Path) -> Result<Vec<V4Range>> {
|
||||
);
|
||||
}
|
||||
previous_end = Some(end);
|
||||
if let Some(country) = country_code(country) {
|
||||
if let Some(country) = normalize_country_code(country) {
|
||||
result.push(V4Range {
|
||||
start,
|
||||
end,
|
||||
@@ -168,7 +225,7 @@ fn parse_v6(path: &Path) -> Result<Vec<V6Range>> {
|
||||
);
|
||||
}
|
||||
previous_end = Some(end);
|
||||
if let Some(country) = country_code(country) {
|
||||
if let Some(country) = normalize_country_code(country) {
|
||||
result.push(V6Range {
|
||||
start,
|
||||
end,
|
||||
@@ -200,5 +257,11 @@ pub fn validate_directory(directory: &Path) -> Result<()> {
|
||||
.or_else(|error| bail!("GeoIP country database is invalid: {error:#}"))
|
||||
}
|
||||
|
||||
pub fn validate_directory_format(directory: &Path, format: DatabaseFormat) -> Result<()> {
|
||||
Database::load_format(directory, format)
|
||||
.map(|_| ())
|
||||
.or_else(|error| bail!("GeoIP country database is invalid: {error:#}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
include!("geoip/tests.rs");
|
||||
Reference in new issue
Block a user