use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DohProvider {
Off,
Cloudflare,
Google,
Quad9,
Custom,
}
impl DohProvider {
pub fn as_str(self) -> &'static str {
match self {
DohProvider::Off => "off",
DohProvider::Cloudflare => "cloudflare",
DohProvider::Google => "google",
DohProvider::Quad9 => "quad9",
DohProvider::Custom => "custom",
}
}
pub fn default_url(self) -> Option<&'static str> {
match self {
DohProvider::Cloudflare => Some("https://cloudflare-dns.com/dns-query"),
DohProvider::Google => Some("https://dns.google/dns-query"),
DohProvider::Quad9 => Some("https://dns.quad9.net/dns-query"),
DohProvider::Off | DohProvider::Custom => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DohConfig {
pub provider: DohProvider,
pub custom_url: Option<Url>,
}
impl Default for DohConfig {
fn default() -> Self {
Self {
provider: DohProvider::Off,
custom_url: None,
}
}
}
impl DohConfig {
pub fn parse(value: &str) -> Result<Self, String> {
let v = value.trim();
if v.is_empty() || v.eq_ignore_ascii_case("off") || v.eq_ignore_ascii_case("system") {
return Ok(Self::default());
}
match v.to_ascii_lowercase().as_str() {
"cloudflare" | "cf" | "1.1.1.1" => Ok(Self {
provider: DohProvider::Cloudflare,
custom_url: None,
}),
"google" | "8.8.8.8" => Ok(Self {
provider: DohProvider::Google,
custom_url: None,
}),
"quad9" | "9.9.9.9" => Ok(Self {
provider: DohProvider::Quad9,
custom_url: None,
}),
_ => {
let u = Url::parse(v)
.map_err(|e| format!("invalid --doh value (not a provider or URL): {e}"))?;
if u.scheme() != "https" {
return Err("--doh custom URL must use https://".into());
}
Ok(Self {
provider: DohProvider::Custom,
custom_url: Some(u),
})
}
}
}
pub fn from_env() -> Self {
match std::env::var("CRAWLEX_DOH") {
Ok(v) => Self::parse(&v).unwrap_or_default(),
Err(_) => Self::default(),
}
}
pub fn is_enabled(&self) -> bool {
!matches!(self.provider, DohProvider::Off)
}
pub fn endpoint_url(&self) -> Option<Url> {
match self.provider {
DohProvider::Off => None,
DohProvider::Custom => self.custom_url.clone(),
other => other.default_url().and_then(|s| Url::parse(s).ok()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_off() {
let c = DohConfig::default();
assert!(!c.is_enabled());
assert!(c.endpoint_url().is_none());
}
#[test]
fn parse_provider_aliases() {
assert_eq!(
DohConfig::parse("cloudflare").unwrap().provider,
DohProvider::Cloudflare
);
assert_eq!(
DohConfig::parse("1.1.1.1").unwrap().provider,
DohProvider::Cloudflare
);
assert_eq!(
DohConfig::parse("Google").unwrap().provider,
DohProvider::Google
);
assert_eq!(
DohConfig::parse("quad9").unwrap().provider,
DohProvider::Quad9
);
assert_eq!(DohConfig::parse("off").unwrap().provider, DohProvider::Off);
assert_eq!(DohConfig::parse("").unwrap().provider, DohProvider::Off);
}
#[test]
fn parse_custom_url() {
let c = DohConfig::parse("https://doh.example.test/dns-query").unwrap();
assert_eq!(c.provider, DohProvider::Custom);
assert_eq!(
c.endpoint_url().unwrap().as_str(),
"https://doh.example.test/dns-query"
);
}
#[test]
fn rejects_non_https_custom() {
let err = DohConfig::parse("http://nope.test/dns-query").unwrap_err();
assert!(err.contains("https"));
}
#[test]
fn endpoint_url_cloudflare_default() {
let c = DohConfig::parse("cloudflare").unwrap();
assert_eq!(
c.endpoint_url().unwrap().as_str(),
"https://cloudflare-dns.com/dns-query"
);
}
}