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;
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
83/// Strip `name` from `origin`, return `return_if_equal` if `name` is the same
84/// as `origin`, or  `@` if `None` given.
85pub(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}