Skip to main content

dns_update_lite/
utils.rs

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
13/// Ensure every record in `records` has the expected [`DnsRecordType`].
14///
15/// Returns an [`Error::Api`] describing the first mismatch otherwise.
16pub(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
76/// Strip `name` from `origin`, return `return_if_equal` if `name` is the same
77/// as `origin`, or  `@` if `None` given.
78pub(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}