1use crate::error;
2use serde::Deserialize;
3
4pub type Result<T, E = error::Error> = std::result::Result<T, E>;
10
11const GOOGLEDNS_BASE_URL: &str = "https://dns.google";
12
13#[derive(Debug, Clone, Deserialize)]
14pub struct DnsQuestion {
15 pub name: String,
17 pub r#type: u32,
19}
20
21#[derive(Debug, Clone, Deserialize)]
22pub struct DnsAnswer {
23 pub r#type: u32,
25 #[serde(rename = "TTL")]
27 pub ttl: u32,
28 pub data: String,
30}
31
32#[derive(Debug, Clone, Deserialize)]
33pub struct Dns {
34 #[serde(rename = "Status")]
35 pub status: u32,
37 #[serde(rename = "TC")]
42 pub tc: bool,
43 #[serde(rename = "AD")]
45 pub ad: bool,
46 #[serde(rename = "CD")]
48 pub cd: bool,
49 #[serde(rename = "Question")]
50 pub question: Vec<DnsQuestion>,
51 #[serde(rename = "Answer")]
52 pub answer: Option<Vec<DnsAnswer>>,
53 #[serde(rename = "Comment")]
54 pub comment: Option<String>,
55}
56
57#[derive(Debug, Clone)]
58pub struct DoH<'a> {
59 base_url: &'a str,
61
62 pub name: String,
63 pub r#type: Option<u32>,
66 pub cd: Option<bool>,
70 pub ct: Option<String>,
75 pub r#do: Option<bool>,
80 pub edns_client_subnet: Option<String>,
84}
85
86impl<'a> DoH<'a> {
87 pub fn new(name: String) -> Self {
88 DoH {
89 base_url: GOOGLEDNS_BASE_URL,
90 name,
91 r#type: None,
92 cd: None,
93 ct: None,
94 r#do: None,
95 edns_client_subnet: None,
96 }
97 }
98
99 pub fn set_base_url(mut self, value: &'a str) -> Self {
100 self.base_url = value;
101 self
102 }
103
104 pub fn set_ct(mut self, value: String) -> Self {
106 self.ct = Some(value);
107 self
108 }
109
110 pub fn set_type(mut self, value: u32) -> Self {
112 self.r#type = Some(value);
113 self
114 }
115
116 pub fn set_cd(mut self, value: bool) -> Self {
118 self.cd = Some(value);
119 self
120 }
121
122 pub fn set_do(mut self, value: bool) -> Self {
124 self.r#do = Some(value);
125 self
126 }
127
128 pub fn set_edns_client_subnet(mut self, value: String) -> Self {
130 self.edns_client_subnet = Some(value);
131 self
132 }
133
134 pub async fn resolve(&self) -> Result<Dns> {
135 let url = format!(
136 "{}/resolve?name={name}{cd}{ct}{edns_client_subnet}{type}",
137 &self.base_url,
138 name = &self.name,
139 r#type = match &self.r#type {
140 Some(v) => format!("&type={}", v),
141 None => "".to_string(),
142 },
143 ct = match &self.ct {
144 Some(v) => format!("&ct={}", v),
145 None => "".to_string(),
146 },
147 cd = match &self.cd {
148 Some(v) => format!("&cd={}", v),
149 None => "".to_string(),
150 },
151 edns_client_subnet = match &self.edns_client_subnet {
152 Some(v) => format!("&edns_client_subnet={}", v),
153 None => "".to_string(),
154 }
155 );
156
157 Ok(ureq::get(&url).call()?.into_json()?)
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use serde_json::Value;
164 use wiremock::{
165 matchers::{method, path},
166 Mock, MockServer, ResponseTemplate,
167 };
168
169 use super::*;
170
171 async fn setup_mock_api(response: ResponseTemplate) -> MockServer {
172 let server = MockServer::start().await;
173 Mock::given(method("GET"))
174 .and(path("/resolve"))
175 .respond_with(response)
176 .mount(&server)
177 .await;
178 server
179 }
180
181 #[async_std::test]
182 async fn should_return_dns_information() {
183 let body: Value = serde_json::from_str(include_str!("../samples/google_A.json")).unwrap();
184 let template = ResponseTemplate::new(200).set_body_json(body);
185 let server = setup_mock_api(template).await;
186 let result = DoH::new("google.com".to_string())
187 .set_base_url(&server.uri())
188 .resolve()
189 .await
190 .unwrap();
191
192 assert_eq!(result.status, 0);
193 assert_eq!(result.tc, false);
194 assert_eq!(result.ad, false);
195 assert_eq!(result.cd, false);
196 assert_eq!(
197 result.comment,
198 Some("Response from 2001:4860:4802:32::a.".to_string())
199 );
200
201 let answer = result.answer.unwrap().into_iter().nth(0).unwrap();
202 assert_eq!(answer.r#type, 1);
203 assert_eq!(answer.ttl, 300);
204 assert_eq!(answer.data, "216.58.208.110".to_string());
205
206 let question = result.question.into_iter().nth(0).unwrap();
207 assert_eq!(question.r#type, 1);
208 assert_eq!(question.name, "google.com.");
209 }
210}