1use std::{
11 cmp::min,
12 time::{Duration, Instant},
13};
14
15use crate::{
16 cache::MAX_TTL,
17 proto::{
18 op::{Message, OpCode, Query},
19 rr::{RData, Record},
20 },
21};
22
23#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct Lookup {
28 message: Message,
29 valid_until: Instant,
30}
31
32impl Lookup {
33 pub(crate) fn new(message: Message, valid_until: Instant) -> Self {
35 debug_assert!(
36 !message.queries.is_empty(),
37 "lookup message must have at least one query"
38 );
39
40 Self {
41 message,
42 valid_until,
43 }
44 }
45
46 pub fn from_rdata(query: Query, rdata: RData) -> Self {
48 let record = Record::from_rdata(query.name().clone(), MAX_TTL, rdata);
49 Self::new_with_max_ttl(query, [record])
50 }
51
52 pub fn new_with_max_ttl(query: Query, answers: impl IntoIterator<Item = Record>) -> Self {
54 let valid_until = Instant::now() + Duration::from_secs(u64::from(MAX_TTL));
55 Self::new_with_deadline(query, answers, valid_until)
56 }
57
58 pub fn new_with_deadline(
60 query: Query,
61 answers: impl IntoIterator<Item = Record>,
62 valid_until: Instant,
63 ) -> Self {
64 let mut message = Message::response(0, OpCode::Query);
65 message.add_query(query.clone());
66 message.add_answers(answers);
67
68 Self {
69 message,
70 valid_until,
71 }
72 }
73
74 pub fn query(&self) -> &Query {
76 self.message
77 .queries
78 .first()
79 .expect("Lookup message always has a query")
80 }
81
82 pub fn message(&self) -> &Message {
84 &self.message
85 }
86
87 pub fn answers(&self) -> &[Record] {
89 &self.message.answers
90 }
91
92 pub fn authorities(&self) -> &[Record] {
94 &self.message.authorities
95 }
96
97 pub fn additionals(&self) -> &[Record] {
99 &self.message.additionals
100 }
101
102 pub fn valid_until(&self) -> Instant {
104 self.valid_until
105 }
106
107 pub(crate) fn append(&self, other: Self) -> Self {
111 let mut result = self.clone();
113
114 result.message.add_answers(other.answers().iter().cloned());
116 result
117 .message
118 .add_authorities(other.authorities().iter().cloned());
119 result
120 .message
121 .add_additionals(other.additionals().iter().cloned());
122
123 result.valid_until = min(self.valid_until(), other.valid_until());
125 result
126 }
127
128 #[doc(hidden)] pub fn extend_authorities(&mut self, records: impl IntoIterator<Item = Record>) {
130 self.message.add_authorities(records);
131 }
132
133 #[doc(hidden)] pub fn extend_additionals(&mut self, records: impl IntoIterator<Item = Record>) {
135 self.message.add_additionals(records);
136 }
137
138 #[cfg(test)]
142 fn extend_answers(&mut self, other: Vec<Record>) {
143 self.message.add_answers(other);
145 }
146}
147
148impl From<Lookup> for Message {
149 fn from(lookup: Lookup) -> Self {
150 lookup.message
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use std::str::FromStr;
157
158 use crate::proto::op::Query;
159 use crate::proto::rr::rdata::{A, NS};
160 use crate::proto::rr::{Name, RData, Record, RecordType};
161
162 use super::*;
163
164 #[test]
165 #[cfg(feature = "__dnssec")]
166 fn test_dnssec_lookup() {
167 use hickory_proto::dnssec::Proof;
168
169 let mut a1 = Record::from_rdata(
170 Name::from_str("www.example.com.").unwrap(),
171 80,
172 RData::A(A::new(127, 0, 0, 1)),
173 );
174 a1.proof = Proof::Secure;
175
176 let mut a2 = Record::from_rdata(
177 Name::from_str("www.example.com.").unwrap(),
178 80,
179 RData::A(A::new(127, 0, 0, 2)),
180 );
181 a2.proof = Proof::Insecure;
182
183 let mut message = Message::response(0, OpCode::Query);
184 message.add_query(Query::default());
185 message.add_answers([a1.clone(), a2.clone()]);
186
187 let lookup = Lookup {
188 message,
189 valid_until: Instant::now(),
190 };
191
192 let mut lookup = lookup.message().dnssec_answers();
193
194 assert_eq!(*lookup.next().unwrap().require(Proof::Secure).unwrap(), a1);
195 assert_eq!(
196 *lookup.next().unwrap().require(Proof::Insecure).unwrap(),
197 a2
198 );
199 assert_eq!(lookup.next(), None);
200 }
201
202 #[test]
203 fn test_extend_answers_preserves_sections() {
204 let mut message = Message::response(0, OpCode::Query);
206 let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
207 message.add_query(query.clone());
208
209 message.add_answers(vec![Record::from_rdata(
210 Name::from_str("www.example.com.").unwrap(),
211 80,
212 RData::A(A::new(127, 0, 0, 1)),
213 )]);
214 message.add_authority(Record::from_rdata(
215 Name::from_str("example.com.").unwrap(),
216 80,
217 RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
218 ));
219 message.add_additionals(vec![Record::from_rdata(
220 Name::from_str("ns1.example.com.").unwrap(),
221 80,
222 RData::A(A::new(192, 0, 2, 1)),
223 )]);
224
225 let mut lookup = Lookup {
226 message,
227 valid_until: Instant::now(),
228 };
229
230 let new_record = Record::from_rdata(
232 Name::from_str("www.example.com.").unwrap(),
233 80,
234 RData::A(A::new(127, 0, 0, 2)),
235 );
236 lookup.extend_answers(vec![new_record.clone()]);
237
238 assert_eq!(lookup.answers().len(), 2);
240 assert_eq!(lookup.answers()[1], new_record);
241
242 assert_eq!(lookup.authorities().len(), 1);
244 assert_eq!(lookup.additionals().len(), 1);
245
246 if let RData::NS(ns) = &lookup.authorities()[0].data {
248 assert_eq!(ns.0, Name::from_str("ns1.example.com.").unwrap());
249 } else {
250 panic!("Authority record should be NS");
251 }
252
253 if let RData::A(a) = lookup.additionals()[0].data {
254 assert_eq!(a, A::new(192, 0, 2, 1));
255 } else {
256 panic!("Additional record should be A");
257 }
258 }
259
260 #[test]
261 fn test_append_preserves_sections() {
262 let mut message1 = Message::response(0, OpCode::Query);
264 let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
265 message1.add_query(query.clone());
266 message1.add_answers(vec![Record::from_rdata(
267 Name::from_str("www.example.com.").unwrap(),
268 80,
269 RData::A(A::new(127, 0, 0, 1)),
270 )]);
271 message1.add_authority(Record::from_rdata(
272 Name::from_str("example.com.").unwrap(),
273 80,
274 RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
275 ));
276 message1.add_additionals(vec![Record::from_rdata(
277 Name::from_str("ns1.example.com.").unwrap(),
278 80,
279 RData::A(A::new(192, 0, 2, 1)),
280 )]);
281
282 let lookup1 = Lookup {
283 message: message1,
284 valid_until: Instant::now(),
285 };
286
287 let mut message2 = Message::response(0, OpCode::Query);
289 message2.add_query(query.clone());
290 message2.add_answers(vec![Record::from_rdata(
291 Name::from_str("www.example.com.").unwrap(),
292 80,
293 RData::A(A::new(127, 0, 0, 2)),
294 )]);
295 message2.add_authority(Record::from_rdata(
296 Name::from_str("example.com.").unwrap(),
297 80,
298 RData::NS(NS(Name::from_str("ns2.example.com.").unwrap())),
299 ));
300 message2.add_additionals(vec![Record::from_rdata(
301 Name::from_str("ns2.example.com.").unwrap(),
302 80,
303 RData::A(A::new(192, 0, 2, 2)),
304 )]);
305
306 let lookup2 = Lookup {
307 message: message2,
308 valid_until: Instant::now(),
309 };
310
311 let combined = lookup1.append(lookup2);
313
314 assert_eq!(combined.answers().len(), 2);
316 assert_eq!(combined.authorities().len(), 2);
317 assert_eq!(combined.additionals().len(), 2);
318
319 if let RData::A(a) = combined.answers()[0].data {
321 assert_eq!(a, A::new(127, 0, 0, 1));
322 } else {
323 panic!("First answer should be A");
324 }
325 if let RData::A(a) = combined.answers()[1].data {
326 assert_eq!(a, A::new(127, 0, 0, 2));
327 } else {
328 panic!("Second answer should be A");
329 }
330
331 if let RData::NS(ns) = &combined.authorities()[0].data {
333 assert_eq!(ns.0, Name::from_str("ns1.example.com.").unwrap());
334 } else {
335 panic!("First authority should be NS");
336 }
337 if let RData::NS(ns) = &combined.authorities()[1].data {
338 assert_eq!(ns.0, Name::from_str("ns2.example.com.").unwrap());
339 } else {
340 panic!("Second authority should be NS");
341 }
342
343 if let RData::A(a) = combined.additionals()[0].data {
345 assert_eq!(a, A::new(192, 0, 2, 1));
346 } else {
347 panic!("First additional should be A");
348 }
349 if let RData::A(a) = combined.additionals()[1].data {
350 assert_eq!(a, A::new(192, 0, 2, 2));
351 } else {
352 panic!("Second additional should be A");
353 }
354 }
355}