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