use std::slice;
use std::time::Duration;
use chrono::{DateTime, Months, Utc};
use super::fetch::{Archive, Credential, Transfer};
use super::{DatabaseFormat, DatabaseSpec, GeoIpDownloadError, Kind, SECS_PER_DAY};
use crate::Secret;
use crate::geoip::config::{
AutoDownloadConfig, GeoIpProvider, ProviderChoice, ProviderSelection, ProviderTier,
};
const DAY: Duration = Duration::from_secs(24 * 60 * 60);
const TWICE_WEEKLY: Duration = Duration::from_secs(7 * 24 * 60 * 60 / 2);
const MONTHLY: Duration = Duration::from_secs(30 * 24 * 60 * 60);
const MONTH: &str = "{month}";
const EDITION: &str = "{edition}";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UrlStrategy {
Static(&'static str),
MonthlyDated {
template: &'static str,
fallback: bool,
},
Edition {
endpoint: &'static str,
},
}
impl UrlStrategy {
fn resolve(self, edition: &str) -> (String, Option<String>) {
match self {
Self::Static(url) => (url.to_string(), None),
Self::MonthlyDated { template, fallback } => {
let now = Utc::now();
let previous = if fallback {
now.checked_sub_months(Months::new(1))
.map(|month| dated(template, month))
} else {
None
};
(dated(template, now), previous)
}
Self::Edition { endpoint } => (endpoint.replace(EDITION, edition), None),
}
}
}
fn dated(template: &str, month: DateTime<Utc>) -> String {
template.replace(MONTH, &month.format("%Y-%m").to_string())
}
#[derive(Debug, Clone, Copy)]
struct CredentialSlot {
path: &'static str,
read: fn(&AutoDownloadConfig) -> Option<&Secret>,
}
impl CredentialSlot {
fn require(
self,
provider: &'static str,
auto: &AutoDownloadConfig,
) -> Result<Secret, GeoIpDownloadError> {
(self.read)(auto)
.filter(|secret| !secret.is_empty())
.cloned()
.ok_or(GeoIpDownloadError::MissingCredential {
provider,
field: self.path,
})
}
}
#[derive(Debug, Clone, Copy)]
enum CredentialKind {
None,
Basic {
username: CredentialSlot,
password: CredentialSlot,
fields: &'static str,
},
QueryToken {
name: &'static str,
token: CredentialSlot,
},
}
impl CredentialKind {
fn resolve(
self,
provider: &'static str,
auto: &AutoDownloadConfig,
) -> Result<Credential, GeoIpDownloadError> {
Ok(match self {
Self::None => Credential::None,
Self::Basic {
username,
password,
fields,
} => Credential::Basic {
username: username.require(provider, auto)?,
password: password.require(provider, auto)?,
fields,
},
Self::QueryToken { name, token } => Credential::QueryToken {
name,
value: token.require(provider, auto)?,
fields: token.path,
},
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceTerms {
pub name: &'static str,
pub kind: &'static str,
pub terms_url: &'static str,
pub cadence: Duration,
pub min_interval: Option<Duration>,
}
#[derive(Debug)]
pub(super) struct SourceSpec {
provider: GeoIpProvider,
tier: ProviderTier,
kind: Kind,
name: &'static str,
url: UrlStrategy,
credential: CredentialKind,
archive: Archive,
checksum: Option<UrlStrategy>,
cadence: Duration,
min_interval: Option<Duration>,
terms_url: &'static str,
file: &'static str,
format: DatabaseFormat,
}
impl SourceSpec {
fn find(choice: ProviderChoice, kind: Kind) -> Option<&'static Self> {
SOURCES.iter().find(|source| {
source.provider == choice.provider && source.tier == choice.tier && source.kind == kind
})
}
pub(super) fn select(
choice: ProviderChoice,
kind: Kind,
) -> Result<Option<&'static Self>, GeoIpDownloadError> {
if let Some(source) = Self::find(choice, kind) {
return Ok(Some(source));
}
if !SOURCES
.iter()
.any(|source| source.provider == choice.provider)
{
return Ok(None);
}
if !SOURCES
.iter()
.any(|source| source.provider == choice.provider && source.tier == choice.tier)
{
return Err(GeoIpDownloadError::UnsupportedTier {
provider: choice.provider.label().to_string(),
tier: choice.tier.label(),
});
}
Ok(None)
}
pub(super) fn database(&'static self) -> DatabaseSpec {
DatabaseSpec {
format: self.format,
names: slice::from_ref(&self.file),
}
}
fn edition(&'static self) -> &'static str {
self.file.strip_suffix(".mmdb").unwrap_or(self.file)
}
pub(super) fn transfer(
&'static self,
auto: &AutoDownloadConfig,
) -> Result<Transfer, GeoIpDownloadError> {
let edition = self.edition();
let (url, fallback_url) = self.url.resolve(edition);
Ok(Transfer {
url,
fallback_url,
checksum_url: self.checksum.map(|checksum| checksum.resolve(edition).0),
dest: auto.data_dir.join(self.file),
archive: self.archive,
format: self.format,
credential: self.credential.resolve(self.provider.label(), auto)?,
})
}
pub(super) fn staleness_window(&self, auto: &AutoDownloadConfig) -> Duration {
let ceiling = Duration::from_secs(u64::from(auto.max_age_days) * SECS_PER_DAY);
self.cadence
.min(ceiling)
.max(self.min_interval.unwrap_or(Duration::ZERO))
}
fn terms(&'static self) -> SourceTerms {
SourceTerms {
name: self.name,
kind: self.kind.label(),
terms_url: self.terms_url,
cadence: self.cadence,
min_interval: self.min_interval,
}
}
}
const DB_IP_LITE_TERMS: &str = "https://db-ip.com/db/lite.php";
const MAXMIND_GEOLITE2_TERMS: &str = "https://www.maxmind.com/en/geolite/eula";
const MAXMIND_GEOIP2_TERMS: &str = "https://www.maxmind.com/en/end-user-license-agreement";
const IPINFO_LITE_TERMS: &str = "https://ipinfo.io/developers/ipinfo-lite-database";
const SAPICS_TERMS: &str = "https://github.com/sapics/ip-location-db";
const MAXMIND_ENDPOINT: &str =
"https://download.maxmind.com/geoip/databases/{edition}/download?suffix=tar.gz";
const MAXMIND_CHECKSUM_ENDPOINT: &str =
"https://download.maxmind.com/geoip/databases/{edition}/download?suffix=tar.gz.sha256";
static SOURCES: &[SourceSpec] = &[
SourceSpec {
provider: GeoIpProvider::DbIp,
tier: ProviderTier::Free,
kind: Kind::City,
name: "DB-IP Lite City",
url: UrlStrategy::MonthlyDated {
template: "https://download.db-ip.com/free/dbip-city-lite-{month}.mmdb.gz",
fallback: true,
},
credential: CredentialKind::None,
archive: Archive::Gzip,
checksum: None,
cadence: MONTHLY,
min_interval: None,
terms_url: DB_IP_LITE_TERMS,
file: "dbip-city-lite.mmdb",
format: DatabaseFormat::Mmdb,
},
SourceSpec {
provider: GeoIpProvider::DbIp,
tier: ProviderTier::Free,
kind: Kind::Asn,
name: "DB-IP Lite ASN",
url: UrlStrategy::MonthlyDated {
template: "https://download.db-ip.com/free/dbip-asn-lite-{month}.mmdb.gz",
fallback: true,
},
credential: CredentialKind::None,
archive: Archive::Gzip,
checksum: None,
cadence: MONTHLY,
min_interval: None,
terms_url: DB_IP_LITE_TERMS,
file: "dbip-asn-lite.mmdb",
format: DatabaseFormat::Mmdb,
},
SourceSpec {
provider: GeoIpProvider::MaxMind,
tier: ProviderTier::Free,
kind: Kind::City,
name: "MaxMind GeoLite2 City",
url: UrlStrategy::Edition {
endpoint: MAXMIND_ENDPOINT,
},
credential: MAXMIND_CREDENTIAL,
archive: Archive::TarGz {
member: "GeoLite2-City.mmdb",
},
checksum: Some(UrlStrategy::Edition {
endpoint: MAXMIND_CHECKSUM_ENDPOINT,
}),
cadence: TWICE_WEEKLY,
min_interval: Some(DAY),
terms_url: MAXMIND_GEOLITE2_TERMS,
file: "GeoLite2-City.mmdb",
format: DatabaseFormat::Mmdb,
},
SourceSpec {
provider: GeoIpProvider::MaxMind,
tier: ProviderTier::Free,
kind: Kind::Asn,
name: "MaxMind GeoLite2 ASN",
url: UrlStrategy::Edition {
endpoint: MAXMIND_ENDPOINT,
},
credential: MAXMIND_CREDENTIAL,
archive: Archive::TarGz {
member: "GeoLite2-ASN.mmdb",
},
checksum: Some(UrlStrategy::Edition {
endpoint: MAXMIND_CHECKSUM_ENDPOINT,
}),
cadence: TWICE_WEEKLY,
min_interval: Some(DAY),
terms_url: MAXMIND_GEOLITE2_TERMS,
file: "GeoLite2-ASN.mmdb",
format: DatabaseFormat::Mmdb,
},
SourceSpec {
provider: GeoIpProvider::MaxMind,
tier: ProviderTier::Paid,
kind: Kind::City,
name: "MaxMind GeoIP2 City",
url: UrlStrategy::Edition {
endpoint: MAXMIND_ENDPOINT,
},
credential: MAXMIND_CREDENTIAL,
archive: Archive::TarGz {
member: "GeoIP2-City.mmdb",
},
checksum: Some(UrlStrategy::Edition {
endpoint: MAXMIND_CHECKSUM_ENDPOINT,
}),
cadence: TWICE_WEEKLY,
min_interval: Some(DAY),
terms_url: MAXMIND_GEOIP2_TERMS,
file: "GeoIP2-City.mmdb",
format: DatabaseFormat::Mmdb,
},
SourceSpec {
provider: GeoIpProvider::MaxMind,
tier: ProviderTier::Paid,
kind: Kind::Asn,
name: "MaxMind GeoIP2 ISP",
url: UrlStrategy::Edition {
endpoint: MAXMIND_ENDPOINT,
},
credential: MAXMIND_CREDENTIAL,
archive: Archive::TarGz {
member: "GeoIP2-ISP.mmdb",
},
checksum: Some(UrlStrategy::Edition {
endpoint: MAXMIND_CHECKSUM_ENDPOINT,
}),
cadence: TWICE_WEEKLY,
min_interval: Some(DAY),
terms_url: MAXMIND_GEOIP2_TERMS,
file: "GeoIP2-ISP.mmdb",
format: DatabaseFormat::Mmdb,
},
SourceSpec {
provider: GeoIpProvider::IpInfo,
tier: ProviderTier::Free,
kind: Kind::City,
name: "IPinfo Lite",
url: UrlStrategy::Static("https://ipinfo.io/data/ipinfo_lite.mmdb"),
credential: CredentialKind::QueryToken {
name: "token",
token: CredentialSlot {
path: "auto_download.ipinfo_token",
read: |auto| auto.ipinfo_token.as_ref(),
},
},
archive: Archive::Raw,
checksum: None,
cadence: DAY,
min_interval: Some(DAY),
terms_url: IPINFO_LITE_TERMS,
file: "ipinfo-lite.mmdb",
format: DatabaseFormat::Mmdb,
},
SourceSpec {
provider: GeoIpProvider::SapicsOriginAsn,
tier: ProviderTier::Free,
kind: Kind::Asn,
name: "sapics origin-asn",
url: UrlStrategy::Static(
"https://github.com/sapics/ip-location-db/releases/download/latest/origin-asn.mmdb",
),
credential: CredentialKind::None,
archive: Archive::Raw,
checksum: Some(UrlStrategy::Static(
"https://github.com/sapics/ip-location-db/releases/download/checksum/origin-asn.mmdb.sha256",
)),
cadence: DAY,
min_interval: None,
terms_url: SAPICS_TERMS,
file: "origin-asn.mmdb",
format: DatabaseFormat::Mmdb,
},
SourceSpec {
provider: GeoIpProvider::SapicsIpToAsn,
tier: ProviderTier::Free,
kind: Kind::Asn,
name: "sapics iptoasn-asn",
url: UrlStrategy::Static(
"https://github.com/sapics/ip-location-db/releases/download/latest/iptoasn-asn.mmdb",
),
credential: CredentialKind::None,
archive: Archive::Raw,
checksum: Some(UrlStrategy::Static(
"https://github.com/sapics/ip-location-db/releases/download/checksum/iptoasn-asn.mmdb.sha256",
)),
cadence: DAY,
min_interval: None,
terms_url: SAPICS_TERMS,
file: "iptoasn-asn.mmdb",
format: DatabaseFormat::Mmdb,
},
];
const MAXMIND_CREDENTIAL: CredentialKind = CredentialKind::Basic {
username: CredentialSlot {
path: "auto_download.maxmind_account_id",
read: |auto| auto.maxmind_account_id.as_ref(),
},
password: CredentialSlot {
path: "auto_download.maxmind_license_key",
read: |auto| auto.maxmind_license_key.as_ref(),
},
fields: "auto_download.maxmind_account_id and auto_download.maxmind_license_key",
};
#[must_use]
pub fn source_terms(selection: ProviderSelection) -> Vec<SourceTerms> {
[Kind::City, Kind::Asn]
.into_iter()
.filter_map(|kind| SourceSpec::find(kind.choice(selection), kind))
.map(SourceSpec::terms)
.collect()
}