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