use crate::error;
use serde::Deserialize;
pub type Result<T, E = error::Error> = std::result::Result<T, E>;
const GOOGLEDNS_BASE_URL: &str = "https://dns.google";
#[derive(Debug, Clone, Deserialize)]
pub struct DnsQuestion {
pub name: String,
pub r#type: u32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DnsAnswer {
pub r#type: u32,
#[serde(rename = "TTL")]
pub ttl: u32,
pub data: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Dns {
#[serde(rename = "Status")]
pub status: u32,
#[serde(rename = "TC")]
pub tc: bool,
#[serde(rename = "AD")]
pub ad: bool,
#[serde(rename = "CD")]
pub cd: bool,
#[serde(rename = "Question")]
pub question: Vec<DnsQuestion>,
#[serde(rename = "Answer")]
pub answer: Option<Vec<DnsAnswer>>,
#[serde(rename = "Comment")]
pub comment: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DoH<'a> {
base_url: &'a str,
pub name: String,
pub r#type: Option<u32>,
pub cd: Option<bool>,
pub ct: Option<String>,
pub r#do: Option<bool>,
pub edns_client_subnet: Option<String>,
}
impl<'a> DoH<'a> {
pub fn new(name: String) -> Self {
DoH {
base_url: GOOGLEDNS_BASE_URL,
name,
r#type: None,
cd: None,
ct: None,
r#do: None,
edns_client_subnet: None,
}
}
pub fn set_base_url(mut self, value: &'a str) -> Self {
self.base_url = value;
self
}
pub fn set_ct(mut self, value: String) -> Self {
self.ct = Some(value);
self
}
pub fn set_type(mut self, value: u32) -> Self {
self.r#type = Some(value);
self
}
pub fn set_cd(mut self, value: bool) -> Self {
self.cd = Some(value);
self
}
pub fn set_do(mut self, value: bool) -> Self {
self.r#do = Some(value);
self
}
pub fn set_edns_client_subnet(mut self, value: String) -> Self {
self.edns_client_subnet = Some(value);
self
}
pub async fn resolve(&self) -> Result<Dns> {
let url = format!(
"{}/resolve?name={name}{cd}{ct}{edns_client_subnet}{type}",
&self.base_url,
name = &self.name,
r#type = match &self.r#type {
Some(v) => format!("&type={}", v),
None => "".to_string(),
},
ct = match &self.ct {
Some(v) => format!("&ct={}", v),
None => "".to_string(),
},
cd = match &self.cd {
Some(v) => format!("&cd={}", v),
None => "".to_string(),
},
edns_client_subnet = match &self.edns_client_subnet {
Some(v) => format!("&edns_client_subnet={}", v),
None => "".to_string(),
}
);
Ok(ureq::get(&url).call()?.into_json()?)
}
}
#[cfg(test)]
mod tests {
use serde_json::Value;
use wiremock::{
matchers::{method, path},
Mock, MockServer, ResponseTemplate,
};
use super::*;
async fn setup_mock_api(response: ResponseTemplate) -> MockServer {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/resolve"))
.respond_with(response)
.mount(&server)
.await;
server
}
#[async_std::test]
async fn should_return_dns_information() {
let body: Value = serde_json::from_str(include_str!("../samples/google_A.json")).unwrap();
let template = ResponseTemplate::new(200).set_body_json(body);
let server = setup_mock_api(template).await;
let result = DoH::new("google.com".to_string())
.set_base_url(&server.uri())
.resolve()
.await
.unwrap();
assert_eq!(result.status, 0);
assert_eq!(result.tc, false);
assert_eq!(result.ad, false);
assert_eq!(result.cd, false);
assert_eq!(
result.comment,
Some("Response from 2001:4860:4802:32::a.".to_string())
);
let answer = result.answer.unwrap().into_iter().nth(0).unwrap();
assert_eq!(answer.r#type, 1);
assert_eq!(answer.ttl, 300);
assert_eq!(answer.data, "216.58.208.110".to_string());
let question = result.question.into_iter().nth(0).unwrap();
assert_eq!(question.r#type, 1);
assert_eq!(question.name, "google.com.");
}
}