google_dns_rs/
api.rs

1use crate::error;
2use serde::Deserialize;
3
4/// The comments here described were (partially) copied from
5/// JSON API specification reference:
6/// https://developers.google.com/speed/public-dns/docs/doh/json
7
8/// Result using default error type error::Error [`Error`]
9pub 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    /// Fully qualified domain name with trailing dot
16    pub name: String,
17    /// Standard DNS RR type
18    pub r#type: u32,
19}
20
21#[derive(Debug, Clone, Deserialize)]
22pub struct DnsAnswer {
23    /// RR type represented as a number
24    pub r#type: u32,
25    /// Record's time-to-live in seconds
26    #[serde(rename = "TTL")]
27    pub ttl: u32,
28    /// The value for "type"
29    pub data: String,
30}
31
32#[derive(Debug, Clone, Deserialize)]
33pub struct Dns {
34    #[serde(rename = "Status")]
35    /// NOERROR - Standard DNS response code (32 bit integer).
36    pub status: u32,
37    /// The response will be trucated if Google DNS cannot get complete
38    /// and un-truncated responses from authoritative name servers or in cases
39    /// where the DNS response (binary DNS message form) would exceed the 64 KiB
40    /// limit for TCP DNS messages
41    #[serde(rename = "TC")]
42    pub tc: bool,
43    /// Whether all response data was validated with DNSSEC
44    #[serde(rename = "AD")]
45    pub ad: bool,
46    /// Whether the client asked to disable DNSSEC
47    #[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    // Google DNS api base URL
60    base_url: &'a str,
61
62    pub name: String,
63    /// RR type
64    /// default = 1
65    pub r#type: Option<u32>,
66    /// The checking disabled flag
67    /// Use true to disable DNSSEC or false to enable DNSSEC validation
68    /// default = false
69    pub cd: Option<bool>,
70    /// Desired content type
71    /// ct=application/dns-message for binary DNS-message
72    /// ct=application/x-javascript for JSON text
73    /// default = empty
74    pub ct: Option<String>,
75    /// DNSSEC OK flag
76    /// if true, the DNSSEC records will be included in the response (RRSIG, NSEC, NSEC3)
77    /// or false to ommit these values in the response
78    /// default = false
79    pub r#do: Option<bool>,
80    /// Use IP address format with a subnet mask
81    /// Examples: 1.2.3.4/24, 2001:700:300::/48
82    /// default = empty
83    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    /// Sets the desired content type
105    pub fn set_ct(mut self, value: String) -> Self {
106        self.ct = Some(value);
107        self
108    }
109
110    /// Sets the RR type
111    pub fn set_type(mut self, value: u32) -> Self {
112        self.r#type = Some(value);
113        self
114    }
115
116    /// Disable or enable DNSSEC check
117    pub fn set_cd(mut self, value: bool) -> Self {
118        self.cd = Some(value);
119        self
120    }
121
122    /// Include or ommit DNSSEC records
123    pub fn set_do(mut self, value: bool) -> Self {
124        self.r#do = Some(value);
125        self
126    }
127
128    /// Include or ommit DNSSEC records
129    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}