Skip to main content

dns_update/
utils.rs

1/*
2 * Copyright Stalwart Labs LLC See the COPYING
3 * file at the top-level directory of this distribution.
4 *
5 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8 * option. This file may not be copied, modified, or distributed
9 * except according to those terms.
10 */
11
12use 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
68/// Strip `name` from `origin`, return `return_if_equal` if `name` is the same
69/// as `origin`, or  `@` if `None` given.
70pub(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}