1use crate::{
2 CAARecord, DnsRecord, DnsRecordType, Error, IntoFqdn, KeyValue, MXRecord, SRVRecord,
3 TLSARecord, TlsaCertUsage, TlsaMatching, TlsaSelector, TsigAlgorithm,
4};
5use std::{
6 borrow::Cow,
7 fmt::{self, Display, Formatter},
8 str::FromStr,
9};
10
11const MAX_CHUNK_BYTES: usize = 255;
12
13pub(crate) fn check_record_types(
17 expected: DnsRecordType,
18 records: &[DnsRecord],
19) -> crate::Result<()> {
20 for r in records {
21 if r.as_type() != expected {
22 return Err(Error::Api(format!(
23 "RRSet record type mismatch: expected {}, got {}",
24 expected.as_str(),
25 r.as_type().as_str(),
26 )));
27 }
28 }
29 Ok(())
30}
31
32pub(crate) fn txt_chunks_to_text(output: &mut String, text: &str, separator: &str) {
33 output.push('"');
34 let mut current_bytes: usize = 0;
35 for ch in text.chars() {
36 let ch_len = ch.len_utf8();
37 if current_bytes > 0 && current_bytes + ch_len > MAX_CHUNK_BYTES {
38 output.push('"');
39 output.push_str(separator);
40 output.push('"');
41 current_bytes = 0;
42 }
43 match ch {
44 '\\' => output.push_str("\\\\"),
45 '"' => output.push_str("\\\""),
46 _ => output.push(ch),
47 }
48 current_bytes += ch_len;
49 }
50 output.push('"');
51}
52
53pub(crate) fn txt_chunks(content: String) -> Vec<String> {
54 if content.len() <= MAX_CHUNK_BYTES {
55 return vec![content];
56 }
57
58 let mut chunks = Vec::new();
59 let mut chunk = String::new();
60
61 for ch in content.chars() {
62 let ch_len = ch.len_utf8();
63 if !chunk.is_empty() && chunk.len() + ch_len > MAX_CHUNK_BYTES {
64 chunks.push(std::mem::take(&mut chunk));
65 }
66 chunk.push(ch);
67 }
68
69 if !chunk.is_empty() {
70 chunks.push(chunk);
71 }
72
73 chunks
74}
75
76pub(crate) fn strip_origin_from_name(
79 name: &str,
80 origin: &str,
81 return_if_equal: Option<&str>,
82) -> String {
83 let name = name.trim_end_matches('.');
84 let origin = origin.trim_end_matches('.');
85
86 if name == origin {
87 return return_if_equal.unwrap_or("@").to_string();
88 }
89
90 if name.ends_with(&format!(".{}", origin)) {
91 name[..name.len() - origin.len() - 1].to_string()
92 } else {
93 name.to_string()
94 }
95}
96
97impl fmt::Display for TLSARecord {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
99 write!(
100 f,
101 "{} {} {} ",
102 u8::from(self.cert_usage),
103 u8::from(self.selector),
104 u8::from(self.matching),
105 )?;
106
107 for ch in &self.cert_data {
108 write!(f, "{:02x}", ch)?;
109 }
110
111 Ok(())
112 }
113}
114
115impl fmt::Display for KeyValue {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
117 f.write_str(&self.key)?;
118 if !self.value.is_empty() {
119 write!(f, "={}", self.value)?;
120 }
121
122 Ok(())
123 }
124}
125
126impl CAARecord {
127 pub fn decompose(self) -> (u8, String, String) {
128 match self {
129 CAARecord::Issue {
130 issuer_critical,
131 name,
132 options,
133 } => {
134 let flags = if issuer_critical { 128 } else { 0 };
135 let mut value = name.unwrap_or_default();
136 for opt in &options {
137 use std::fmt::Write;
138 write!(value, "; {}", opt).unwrap();
139 }
140 (flags, "issue".to_string(), value)
141 }
142 CAARecord::IssueWild {
143 issuer_critical,
144 name,
145 options,
146 } => {
147 let flags = if issuer_critical { 128 } else { 0 };
148 let mut value = name.unwrap_or_default();
149 for opt in &options {
150 use std::fmt::Write;
151 write!(value, "; {}", opt).unwrap();
152 }
153 (flags, "issuewild".to_string(), value)
154 }
155 CAARecord::Iodef {
156 issuer_critical,
157 url,
158 } => {
159 let flags = if issuer_critical { 128 } else { 0 };
160 (flags, "iodef".to_string(), url)
161 }
162 }
163 }
164}
165
166impl fmt::Display for CAARecord {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
168 match self {
169 CAARecord::Issue {
170 issuer_critical,
171 name,
172 options,
173 } => {
174 if *issuer_critical {
175 f.write_str("128 ")?;
176 } else {
177 f.write_str("0 ")?;
178 }
179 f.write_str("issue ")?;
180 f.write_str("\"")?;
181 if let Some(name) = name {
182 f.write_str(name)?;
183 }
184 for opt in options {
185 write!(f, ";{}", opt)?;
186 }
187 f.write_str("\"")?;
188 }
189 CAARecord::IssueWild {
190 issuer_critical,
191 name,
192 options,
193 } => {
194 if *issuer_critical {
195 f.write_str("128 ")?;
196 } else {
197 f.write_str("0 ")?;
198 }
199 f.write_str("issuewild ")?;
200 f.write_str("\"")?;
201 if let Some(name) = name {
202 f.write_str(name)?;
203 }
204 for opt in options {
205 write!(f, ";{}", opt)?;
206 }
207 f.write_str("\"")?;
208 }
209 CAARecord::Iodef {
210 issuer_critical,
211 url,
212 } => {
213 if *issuer_critical {
214 f.write_str("128 ")?;
215 } else {
216 f.write_str("0 ")?;
217 }
218 f.write_str("iodef ")?;
219 f.write_str("\"")?;
220 f.write_str(url)?;
221 f.write_str("\"")?;
222 }
223 }
224 Ok(())
225 }
226}
227
228impl Display for MXRecord {
229 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
230 write!(f, "{} {}", self.priority, self.exchange)
231 }
232}
233
234impl Display for SRVRecord {
235 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
236 write!(
237 f,
238 "{} {} {} {}",
239 self.priority, self.weight, self.port, self.target
240 )
241 }
242}
243
244impl Display for crate::HTTPSRecord {
245 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
246 let params = self
247 .svc_params
248 .iter()
249 .map(|p| p.to_string())
250 .collect::<Vec<_>>()
251 .join(" ");
252 write!(f, "{} {} {}", self.svc_priority, self.target_name, params)
253 }
254}
255
256impl DnsRecord {
257 pub fn as_type(&self) -> DnsRecordType {
258 match self {
259 DnsRecord::A { .. } => DnsRecordType::A,
260 DnsRecord::AAAA { .. } => DnsRecordType::AAAA,
261 DnsRecord::CNAME { .. } => DnsRecordType::CNAME,
262 DnsRecord::NS { .. } => DnsRecordType::NS,
263 DnsRecord::MX { .. } => DnsRecordType::MX,
264 DnsRecord::TXT { .. } => DnsRecordType::TXT,
265 DnsRecord::SRV { .. } => DnsRecordType::SRV,
266 DnsRecord::TLSA { .. } => DnsRecordType::TLSA,
267 DnsRecord::CAA { .. } => DnsRecordType::CAA,
268 DnsRecord::HTTPS { .. } => DnsRecordType::HTTPS,
269 }
270 }
271}
272
273impl Display for DnsRecord {
274 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
275 match self {
276 DnsRecord::A(addr) => Display::fmt(addr, f),
277 DnsRecord::AAAA(addr) => Display::fmt(addr, f),
278 DnsRecord::CNAME(name) => f.write_str(name),
279 DnsRecord::NS(name) => f.write_str(name),
280 DnsRecord::MX(record) => Display::fmt(record, f),
281 DnsRecord::TXT(text) => f.write_str(text),
282 DnsRecord::SRV(record) => Display::fmt(record, f),
283 DnsRecord::TLSA(record) => Display::fmt(record, f),
284 DnsRecord::CAA(record) => Display::fmt(record, f),
285 DnsRecord::HTTPS(record) => Display::fmt(record, f),
286 }
287 }
288}
289
290impl DnsRecordType {
291 pub fn as_str(&self) -> &'static str {
292 match self {
293 DnsRecordType::A => "A",
294 DnsRecordType::AAAA => "AAAA",
295 DnsRecordType::CNAME => "CNAME",
296 DnsRecordType::NS => "NS",
297 DnsRecordType::MX => "MX",
298 DnsRecordType::TXT => "TXT",
299 DnsRecordType::SRV => "SRV",
300 DnsRecordType::TLSA => "TLSA",
301 DnsRecordType::CAA => "CAA",
302 DnsRecordType::HTTPS => "HTTPS",
303 }
304 }
305}
306
307impl From<TlsaCertUsage> for u8 {
308 fn from(usage: TlsaCertUsage) -> Self {
309 match usage {
310 TlsaCertUsage::PkixTa => 0,
311 TlsaCertUsage::PkixEe => 1,
312 TlsaCertUsage::DaneTa => 2,
313 TlsaCertUsage::DaneEe => 3,
314 TlsaCertUsage::Private => 255,
315 }
316 }
317}
318
319impl From<TlsaSelector> for u8 {
320 fn from(selector: TlsaSelector) -> Self {
321 match selector {
322 TlsaSelector::Full => 0,
323 TlsaSelector::Spki => 1,
324 TlsaSelector::Private => 255,
325 }
326 }
327}
328
329impl From<TlsaMatching> for u8 {
330 fn from(matching: TlsaMatching) -> Self {
331 match matching {
332 TlsaMatching::Raw => 0,
333 TlsaMatching::Sha256 => 1,
334 TlsaMatching::Sha512 => 2,
335 TlsaMatching::Private => 255,
336 }
337 }
338}
339
340pub(crate) fn strip_trailing_dot(value: &str) -> &str {
341 value.strip_suffix('.').unwrap_or(value)
342}
343
344pub(crate) fn parse_srv(value: &str) -> crate::Result<DnsRecord> {
345 let mut parts = value.split_whitespace();
346 let priority = parts
347 .next()
348 .ok_or_else(|| Error::Parse(format!("invalid SRV value '{value}'")))?
349 .parse()
350 .map_err(|e| Error::Parse(format!("invalid SRV priority in '{value}': {e}")))?;
351 let weight = parts
352 .next()
353 .ok_or_else(|| Error::Parse(format!("invalid SRV value '{value}'")))?
354 .parse()
355 .map_err(|e| Error::Parse(format!("invalid SRV weight in '{value}': {e}")))?;
356 let port = parts
357 .next()
358 .ok_or_else(|| Error::Parse(format!("invalid SRV value '{value}'")))?
359 .parse()
360 .map_err(|e| Error::Parse(format!("invalid SRV port in '{value}': {e}")))?;
361 let target = parts
362 .next()
363 .ok_or_else(|| Error::Parse(format!("invalid SRV value '{value}'")))?;
364 Ok(DnsRecord::SRV(SRVRecord {
365 priority,
366 weight,
367 port,
368 target: strip_trailing_dot(target).to_string(),
369 }))
370}
371
372pub(crate) fn parse_mx(value: &str) -> crate::Result<DnsRecord> {
373 let mut parts = value.splitn(2, char::is_whitespace);
374 let priority = parts
375 .next()
376 .ok_or_else(|| Error::Parse(format!("invalid MX value '{value}'")))?
377 .parse()
378 .map_err(|e| Error::Parse(format!("invalid MX priority in '{value}': {e}")))?;
379 let exchange = parts
380 .next()
381 .ok_or_else(|| Error::Parse(format!("invalid MX value '{value}'")))?
382 .trim();
383 Ok(DnsRecord::MX(MXRecord {
384 priority,
385 exchange: strip_trailing_dot(exchange).to_string(),
386 }))
387}
388
389pub(crate) fn parse_tlsa(value: &str) -> crate::Result<DnsRecord> {
390 let mut parts = value.split_whitespace();
391 let usage: u8 = parts
392 .next()
393 .ok_or_else(|| Error::Parse(format!("invalid TLSA value '{value}'")))?
394 .parse()
395 .map_err(|e| Error::Parse(format!("invalid TLSA usage in '{value}': {e}")))?;
396 let selector: u8 = parts
397 .next()
398 .ok_or_else(|| Error::Parse(format!("invalid TLSA value '{value}'")))?
399 .parse()
400 .map_err(|e| Error::Parse(format!("invalid TLSA selector in '{value}': {e}")))?;
401 let matching: u8 = parts
402 .next()
403 .ok_or_else(|| Error::Parse(format!("invalid TLSA value '{value}'")))?
404 .parse()
405 .map_err(|e| Error::Parse(format!("invalid TLSA matching in '{value}': {e}")))?;
406 let hex: String = parts.collect();
407 if hex.is_empty() {
408 return Err(Error::Parse(format!("invalid TLSA value '{value}'")));
409 }
410 Ok(DnsRecord::TLSA(TLSARecord {
411 cert_usage: tlsa_cert_usage_from_u8(usage)?,
412 selector: tlsa_selector_from_u8(selector)?,
413 matching: tlsa_matching_from_u8(matching)?,
414 cert_data: decode_hex(&hex)?,
415 }))
416}
417
418pub(crate) fn unquote_txt(content: &str) -> String {
419 if !content.contains('"') {
420 return content.to_string();
421 }
422 let mut out = String::with_capacity(content.len());
423 let mut in_quotes = false;
424 let mut escaped = false;
425 for ch in content.chars() {
426 if escaped {
427 out.push(ch);
428 escaped = false;
429 } else if in_quotes && ch == '\\' {
430 escaped = true;
431 } else if ch == '"' {
432 in_quotes = !in_quotes;
433 } else if in_quotes {
434 out.push(ch);
435 }
436 }
437 out
438}
439
440pub(crate) fn build_caa(flags: u8, tag: &str, value: &str) -> crate::Result<CAARecord> {
441 let issuer_critical = flags & 0x80 != 0;
442 match tag {
443 "issue" => {
444 let (name, options) = split_caa_value(value);
445 Ok(CAARecord::Issue {
446 issuer_critical,
447 name,
448 options,
449 })
450 }
451 "issuewild" => {
452 let (name, options) = split_caa_value(value);
453 Ok(CAARecord::IssueWild {
454 issuer_critical,
455 name,
456 options,
457 })
458 }
459 "iodef" => Ok(CAARecord::Iodef {
460 issuer_critical,
461 url: value.to_string(),
462 }),
463 other => Err(Error::Parse(format!("unknown CAA tag: {other}"))),
464 }
465}
466
467pub(crate) fn split_caa_value(value: &str) -> (Option<String>, Vec<KeyValue>) {
468 let mut parts = value.split(';').map(str::trim);
469 let name = match parts.next().unwrap_or("") {
470 "" => None,
471 head => Some(head.to_string()),
472 };
473 let options = parts
474 .filter(|p| !p.is_empty())
475 .map(|p| match p.split_once('=') {
476 Some((k, v)) => KeyValue {
477 key: k.trim().to_string(),
478 value: v.trim().to_string(),
479 },
480 None => KeyValue {
481 key: p.trim().to_string(),
482 value: String::new(),
483 },
484 })
485 .collect();
486 (name, options)
487}
488
489pub(crate) fn tlsa_cert_usage_from_u8(value: u8) -> crate::Result<TlsaCertUsage> {
490 Ok(match value {
491 0 => TlsaCertUsage::PkixTa,
492 1 => TlsaCertUsage::PkixEe,
493 2 => TlsaCertUsage::DaneTa,
494 3 => TlsaCertUsage::DaneEe,
495 255 => TlsaCertUsage::Private,
496 _ => return Err(Error::Parse(format!("unknown TLSA cert usage: {value}"))),
497 })
498}
499
500pub(crate) fn tlsa_selector_from_u8(value: u8) -> crate::Result<TlsaSelector> {
501 Ok(match value {
502 0 => TlsaSelector::Full,
503 1 => TlsaSelector::Spki,
504 255 => TlsaSelector::Private,
505 _ => return Err(Error::Parse(format!("unknown TLSA selector: {value}"))),
506 })
507}
508
509pub(crate) fn tlsa_matching_from_u8(value: u8) -> crate::Result<TlsaMatching> {
510 Ok(match value {
511 0 => TlsaMatching::Raw,
512 1 => TlsaMatching::Sha256,
513 2 => TlsaMatching::Sha512,
514 255 => TlsaMatching::Private,
515 _ => return Err(Error::Parse(format!("unknown TLSA matching: {value}"))),
516 })
517}
518
519pub(crate) fn decode_hex(hex: &str) -> crate::Result<Vec<u8>> {
520 hex::decode(hex).map_err(|e| Error::Parse(format!("invalid hex string: {e}")))
521}
522
523impl<'x> IntoFqdn<'x> for &'x str {
524 fn into_fqdn(self) -> Cow<'x, str> {
525 if self.ends_with('.') {
526 Cow::Borrowed(self)
527 } else {
528 Cow::Owned(format!("{}.", self))
529 }
530 }
531
532 fn into_name(self) -> Cow<'x, str> {
533 if let Some(name) = self.strip_suffix('.') {
534 Cow::Borrowed(name)
535 } else {
536 Cow::Borrowed(self)
537 }
538 }
539}
540
541impl<'x> IntoFqdn<'x> for &'x String {
542 fn into_fqdn(self) -> Cow<'x, str> {
543 self.as_str().into_fqdn()
544 }
545
546 fn into_name(self) -> Cow<'x, str> {
547 self.as_str().into_name()
548 }
549}
550
551impl<'x> IntoFqdn<'x> for String {
552 fn into_fqdn(self) -> Cow<'x, str> {
553 if self.ends_with('.') {
554 Cow::Owned(self)
555 } else {
556 Cow::Owned(format!("{}.", self))
557 }
558 }
559
560 fn into_name(self) -> Cow<'x, str> {
561 if let Some(name) = self.strip_suffix('.') {
562 Cow::Owned(name.to_string())
563 } else {
564 Cow::Owned(self)
565 }
566 }
567}
568
569impl FromStr for TsigAlgorithm {
570 type Err = ();
571
572 fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
573 match s {
574 "hmac-md5" => Ok(TsigAlgorithm::HmacMd5),
575 "gss" => Ok(TsigAlgorithm::Gss),
576 "hmac-sha1" => Ok(TsigAlgorithm::HmacSha1),
577 "hmac-sha224" => Ok(TsigAlgorithm::HmacSha224),
578 "hmac-sha256" => Ok(TsigAlgorithm::HmacSha256),
579 "hmac-sha256-128" => Ok(TsigAlgorithm::HmacSha256_128),
580 "hmac-sha384" => Ok(TsigAlgorithm::HmacSha384),
581 "hmac-sha384-192" => Ok(TsigAlgorithm::HmacSha384_192),
582 "hmac-sha512" => Ok(TsigAlgorithm::HmacSha512),
583 "hmac-sha512-256" => Ok(TsigAlgorithm::HmacSha512_256),
584 _ => Err(()),
585 }
586 }
587}
588
589impl std::error::Error for Error {}
590
591impl Display for Error {
592 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
593 match self {
594 Error::Protocol(e) => write!(f, "Protocol error: {}", e),
595 Error::Parse(e) => write!(f, "Parse error: {}", e),
596 Error::Client(e) => write!(f, "Client error: {}", e),
597 Error::Response(e) => write!(f, "Response error: {}", e),
598 Error::Api(e) => write!(f, "API error: {}", e),
599 Error::Serialize(e) => write!(f, "Serialize error: {}", e),
600 Error::Unauthorized => write!(f, "Unauthorized"),
601 Error::NotFound => write!(f, "Not found"),
602 Error::BadRequest => write!(f, "Bad request"),
603 Error::Unsupported(e) => write!(f, "Unsupported: {}", e),
604 }
605 }
606}
607
608impl Display for DnsRecordType {
609 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
610 write!(f, "{:?}", self)
611 }
612}