Skip to main content

dns_update/providers/
oraclecloud.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
12#![cfg(any(feature = "ring", feature = "aws-lc-rs"))]
13
14use crate::crypto::sha256_digest;
15use crate::jwt::{parse_rsa_pkcs8_pem, rsa_sha256_sign};
16use crate::utils::txt_chunks_to_text;
17use crate::{
18    CAARecord, DnsRecord, DnsRecordType, Error, IntoFqdn, KeyValue as DnsKeyValue, MXRecord,
19    Result, SRVRecord, TLSARecord, TlsaCertUsage, TlsaMatching, TlsaSelector,
20};
21use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
22use chrono::Utc;
23use reqwest::Method;
24use reqwest::header::{HeaderMap, HeaderValue};
25use serde::{Deserialize, Serialize};
26use std::net::AddrParseError;
27use std::sync::Arc;
28use std::time::Duration;
29
30#[cfg(feature = "ring")]
31use ring::signature::RsaKeyPair;
32
33#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
34use aws_lc_rs::signature::RsaKeyPair;
35
36const RETRIES: u32 = 3;
37const PAGE_LIMIT: u32 = 1000;
38
39#[derive(Debug, Clone)]
40pub struct OracleCloudConfig {
41    pub tenancy_ocid: String,
42    pub user_ocid: String,
43    pub fingerprint: String,
44    pub private_key_pem: String,
45    pub private_key_password: Option<String>,
46    pub region: String,
47    pub compartment_ocid: String,
48    pub request_timeout: Option<Duration>,
49}
50
51#[derive(Clone)]
52pub struct OracleCloudProvider {
53    config: OracleCloudConfig,
54    key_pair: Arc<RsaKeyPair>,
55    endpoint: String,
56}
57
58#[derive(Debug, Serialize, Deserialize, Clone)]
59struct OciRecord {
60    domain: String,
61    rtype: String,
62    rdata: String,
63    ttl: u32,
64    #[serde(rename = "isProtected", skip_serializing_if = "Option::is_none")]
65    is_protected: Option<bool>,
66    #[serde(rename = "recordHash", skip_serializing_if = "Option::is_none")]
67    record_hash: Option<String>,
68}
69
70#[derive(Debug, Serialize)]
71struct UpdateRecordsRequest {
72    items: Vec<OciRecord>,
73}
74
75#[derive(Debug, Serialize)]
76struct PatchRecordsRequest {
77    items: Vec<PatchOperation>,
78}
79
80#[derive(Debug, Serialize)]
81struct PatchOperation {
82    operation: &'static str,
83    rdata: String,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    ttl: Option<u32>,
86}
87
88#[derive(Debug, Deserialize)]
89struct RecordCollection {
90    items: Vec<OciRecord>,
91}
92
93#[derive(Debug, Deserialize)]
94struct Zone {
95    name: String,
96    id: String,
97}
98
99impl OracleCloudProvider {
100    pub(crate) fn new(config: OracleCloudConfig) -> Result<Self> {
101        if config.tenancy_ocid.is_empty() {
102            return Err(Error::Client("tenancy_ocid is required".into()));
103        }
104        if config.user_ocid.is_empty() {
105            return Err(Error::Client("user_ocid is required".into()));
106        }
107        if config.fingerprint.is_empty() {
108            return Err(Error::Client("fingerprint is required".into()));
109        }
110        if config.region.is_empty() {
111            return Err(Error::Client("region is required".into()));
112        }
113        if config.compartment_ocid.is_empty() {
114            return Err(Error::Client("compartment_ocid is required".into()));
115        }
116        if config
117            .private_key_password
118            .as_ref()
119            .is_some_and(|p| !p.is_empty())
120        {
121            return Err(Error::Unsupported(
122                "OCI private keys with a passphrase are not supported".into(),
123            ));
124        }
125
126        let key_pair = parse_rsa_pkcs8_pem(&config.private_key_pem)
127            .map_err(|e| Error::Client(format!("Failed to parse OCI private key: {}", e)))?;
128
129        let endpoint = format!("https://dns.{}.oraclecloud.com", config.region);
130
131        Ok(Self {
132            config,
133            key_pair: Arc::new(key_pair),
134            endpoint,
135        })
136    }
137
138    #[cfg(test)]
139    pub(crate) fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
140        self.endpoint = endpoint.into().trim_end_matches('/').to_string();
141        self
142    }
143
144    fn key_id(&self) -> String {
145        format!(
146            "{}/{}/{}",
147            self.config.tenancy_ocid, self.config.user_ocid, self.config.fingerprint
148        )
149    }
150
151    fn sign_request(&self, method: &Method, url: &str, body: Option<&str>) -> Result<HeaderMap> {
152        let parsed = reqwest::Url::parse(url)
153            .map_err(|e| Error::Client(format!("Failed to parse URL {}: {}", url, e)))?;
154        let host = parsed
155            .host_str()
156            .ok_or_else(|| Error::Client(format!("URL missing host: {}", url)))?
157            .to_string();
158        let host_header = if let Some(port) = parsed.port() {
159            format!("{}:{}", host, port)
160        } else {
161            host.clone()
162        };
163        let mut path_and_query = parsed.path().to_string();
164        if let Some(q) = parsed.query() {
165            path_and_query.push('?');
166            path_and_query.push_str(q);
167        }
168
169        let method_lower = method.as_str().to_lowercase();
170        let date = Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string();
171
172        let mut signed_pairs: Vec<(String, String)> = Vec::new();
173        signed_pairs.push((
174            "(request-target)".to_string(),
175            format!("{} {}", method_lower, path_and_query),
176        ));
177        signed_pairs.push(("host".to_string(), host_header.clone()));
178        signed_pairs.push(("date".to_string(), date.clone()));
179
180        let needs_body_headers = matches!(*method, Method::POST | Method::PUT | Method::PATCH);
181        let body_bytes = body.unwrap_or("").as_bytes();
182        let content_sha256 = B64.encode(sha256_digest(body_bytes));
183        let content_length = body_bytes.len().to_string();
184        if needs_body_headers {
185            signed_pairs.push(("x-content-sha256".to_string(), content_sha256.clone()));
186            signed_pairs.push(("content-type".to_string(), "application/json".to_string()));
187            signed_pairs.push(("content-length".to_string(), content_length.clone()));
188        }
189
190        let signing_string = signed_pairs
191            .iter()
192            .map(|(k, v)| format!("{}: {}", k, v))
193            .collect::<Vec<_>>()
194            .join("\n");
195        let signature = rsa_sha256_sign(&self.key_pair, signing_string.as_bytes())
196            .map_err(|e| Error::Client(format!("Failed to sign request: {}", e)))?;
197        let signature_b64 = B64.encode(&signature);
198
199        let headers_list = signed_pairs
200            .iter()
201            .map(|(k, _)| k.as_str())
202            .collect::<Vec<_>>()
203            .join(" ");
204        let authorization = format!(
205            "Signature version=\"1\",keyId=\"{}\",algorithm=\"rsa-sha256\",headers=\"{}\",signature=\"{}\"",
206            self.key_id(),
207            headers_list,
208            signature_b64,
209        );
210
211        let mut headers = HeaderMap::new();
212        headers.insert(
213            "host",
214            HeaderValue::from_str(&host_header)
215                .map_err(|e| Error::Client(format!("Invalid host header: {}", e)))?,
216        );
217        headers.insert(
218            "date",
219            HeaderValue::from_str(&date)
220                .map_err(|e| Error::Client(format!("Invalid date header: {}", e)))?,
221        );
222        headers.insert(
223            "authorization",
224            HeaderValue::from_str(&authorization)
225                .map_err(|e| Error::Client(format!("Invalid authorization header: {}", e)))?,
226        );
227        if needs_body_headers {
228            headers.insert(
229                "x-content-sha256",
230                HeaderValue::from_str(&content_sha256)
231                    .map_err(|e| Error::Client(format!("Invalid x-content-sha256: {}", e)))?,
232            );
233            headers.insert("content-type", HeaderValue::from_static("application/json"));
234            headers.insert(
235                "content-length",
236                HeaderValue::from_str(&content_length)
237                    .map_err(|e| Error::Client(format!("Invalid content-length: {}", e)))?,
238            );
239        }
240
241        Ok(headers)
242    }
243
244    async fn send_signed(
245        &self,
246        method: Method,
247        url: &str,
248        body: Option<String>,
249    ) -> Result<(reqwest::StatusCode, String, HeaderMap)> {
250        let client = reqwest::Client::builder()
251            .timeout(
252                self.config
253                    .request_timeout
254                    .unwrap_or(Duration::from_secs(30)),
255            )
256            .build()
257            .map_err(|e| Error::Client(format!("Failed to build HTTP client: {}", e)))?;
258
259        let mut attempts: u32 = 0;
260        loop {
261            let headers = self.sign_request(&method, url, body.as_deref())?;
262            let mut request = client.request(method.clone(), url).headers(headers);
263            if let Some(b) = body.as_ref() {
264                request = request.body(b.clone());
265            }
266            let response = request
267                .send()
268                .await
269                .map_err(|e| Error::Api(format!("Failed to send request to {}: {}", url, e)))?;
270            let status = response.status();
271            let response_headers = response.headers().clone();
272
273            if status.as_u16() == 429 && attempts < RETRIES {
274                let retry_after = response_headers
275                    .get("retry-after")
276                    .and_then(|v| v.to_str().ok())
277                    .and_then(|s| s.parse::<u64>().ok())
278                    .unwrap_or(1);
279                tokio::time::sleep(Duration::from_secs(retry_after)).await;
280                attempts += 1;
281                continue;
282            }
283
284            let text = response
285                .text()
286                .await
287                .map_err(|e| Error::Api(format!("Failed to read response body: {}", e)))?;
288            return Ok((status, text, response_headers));
289        }
290    }
291
292    fn record_to_rdata(record: &DnsRecord) -> Result<(String, String)> {
293        let (rtype, rdata) = match record {
294            DnsRecord::A(ip) => ("A".to_string(), ip.to_string()),
295            DnsRecord::AAAA(ip) => ("AAAA".to_string(), ip.to_string()),
296            DnsRecord::CNAME(c) => ("CNAME".to_string(), format_target(c)),
297            DnsRecord::NS(n) => ("NS".to_string(), format_target(n)),
298            DnsRecord::MX(mx) => (
299                "MX".to_string(),
300                format!("{} {}", mx.priority, format_target(&mx.exchange)),
301            ),
302            DnsRecord::TXT(txt) => {
303                let mut rdata = String::new();
304                txt_chunks_to_text(&mut rdata, txt, " ");
305                ("TXT".to_string(), rdata)
306            }
307            DnsRecord::SRV(srv) => (
308                "SRV".to_string(),
309                format!(
310                    "{} {} {} {}",
311                    srv.priority,
312                    srv.weight,
313                    srv.port,
314                    format_target(&srv.target)
315                ),
316            ),
317            DnsRecord::CAA(caa) => {
318                let (flags, tag, value) = caa.clone().decompose();
319                (
320                    "CAA".to_string(),
321                    format!("{} {} \"{}\"", flags, tag, value),
322                )
323            }
324            DnsRecord::TLSA(tlsa) => ("TLSA".to_string(), tlsa.to_string()),
325        };
326        Ok((rtype, rdata))
327    }
328
329    async fn resolve_zone(&self, origin: &str) -> Result<String> {
330        let trimmed = origin.trim_end_matches('.');
331        let url = format!(
332            "{}/20180115/zones?compartmentId={}&name={}",
333            self.endpoint,
334            urlencode(&self.config.compartment_ocid),
335            urlencode(trimmed),
336        );
337        let (status, body, _) = self.send_signed(Method::GET, &url, None).await?;
338        if !status.is_success() {
339            return Err(map_error(status, &body));
340        }
341        let zones: Vec<Zone> = serde_json::from_str(&body)
342            .map_err(|e| Error::Serialize(format!("Failed to parse zones list: {}", e)))?;
343        zones
344            .into_iter()
345            .find(|z| z.name.trim_end_matches('.') == trimmed)
346            .map(|z| z.id)
347            .ok_or(Error::NotFound)
348    }
349
350    fn records_url(&self, zone_id: &str, domain: &str, rtype: &str) -> String {
351        format!(
352            "{}/20180115/zones/{}/records/{}/{}?compartmentId={}",
353            self.endpoint,
354            urlencode(zone_id),
355            urlencode(domain),
356            urlencode(rtype),
357            urlencode(&self.config.compartment_ocid),
358        )
359    }
360
361    fn records_url_paged(
362        &self,
363        zone_id: &str,
364        domain: &str,
365        rtype: &str,
366        page: Option<&str>,
367    ) -> String {
368        let mut url = format!(
369            "{}/20180115/zones/{}/records/{}/{}?compartmentId={}&limit={}",
370            self.endpoint,
371            urlencode(zone_id),
372            urlencode(domain),
373            urlencode(rtype),
374            urlencode(&self.config.compartment_ocid),
375            PAGE_LIMIT,
376        );
377        if let Some(page) = page {
378            url.push_str("&page=");
379            url.push_str(&urlencode(page));
380        }
381        url
382    }
383
384    async fn get_records(
385        &self,
386        zone_id: &str,
387        domain: &str,
388        rtype: &str,
389    ) -> Result<Vec<OciRecord>> {
390        let mut all: Vec<OciRecord> = Vec::new();
391        let mut next_page: Option<String> = None;
392        loop {
393            let url = self.records_url_paged(zone_id, domain, rtype, next_page.as_deref());
394            let (status, body, headers) = self.send_signed(Method::GET, &url, None).await?;
395            if status.as_u16() == 404 {
396                return Ok(Vec::new());
397            }
398            if !status.is_success() {
399                return Err(map_error(status, &body));
400            }
401            let collection: RecordCollection = serde_json::from_str(&body)
402                .map_err(|e| Error::Serialize(format!("Failed to parse records: {}", e)))?;
403            all.extend(collection.items);
404            next_page = headers
405                .get("opc-next-page")
406                .and_then(|v| v.to_str().ok())
407                .map(|s| s.to_string());
408            if next_page.is_none() {
409                return Ok(all);
410            }
411        }
412    }
413
414    async fn put_records(
415        &self,
416        zone_id: &str,
417        domain: &str,
418        rtype: &str,
419        items: Vec<OciRecord>,
420    ) -> Result<()> {
421        let url = self.records_url(zone_id, domain, rtype);
422        let request = UpdateRecordsRequest { items };
423        let body = serde_json::to_string(&request)
424            .map_err(|e| Error::Serialize(format!("Failed to serialize request: {}", e)))?;
425        let (status, response_body, _) = self.send_signed(Method::PUT, &url, Some(body)).await?;
426        if !status.is_success() {
427            return Err(map_error(status, &response_body));
428        }
429        Ok(())
430    }
431
432    async fn patch_records(
433        &self,
434        zone_id: &str,
435        domain: &str,
436        rtype: &str,
437        items: Vec<PatchOperation>,
438    ) -> Result<()> {
439        let url = self.records_url(zone_id, domain, rtype);
440        let request = PatchRecordsRequest { items };
441        let body = serde_json::to_string(&request)
442            .map_err(|e| Error::Serialize(format!("Failed to serialize request: {}", e)))?;
443        let (status, response_body, _) = self.send_signed(Method::PATCH, &url, Some(body)).await?;
444        if !status.is_success() {
445            return Err(map_error(status, &response_body));
446        }
447        Ok(())
448    }
449
450    async fn delete_rrset(&self, zone_id: &str, domain: &str, rtype: &str) -> Result<()> {
451        let url = self.records_url(zone_id, domain, rtype);
452        let (status, body, _) = self.send_signed(Method::DELETE, &url, None).await?;
453        if status.as_u16() == 404 {
454            return Ok(());
455        }
456        if !status.is_success() {
457            return Err(map_error(status, &body));
458        }
459        Ok(())
460    }
461
462    pub(crate) async fn set_rrset(
463        &self,
464        name: impl IntoFqdn<'_>,
465        record_type: DnsRecordType,
466        ttl: u32,
467        records: Vec<DnsRecord>,
468        origin: impl IntoFqdn<'_>,
469    ) -> Result<()> {
470        check_record_types(record_type, &records)?;
471        let name = name.into_name().to_string();
472        let origin = origin.into_name().to_string();
473        let zone_id = self.resolve_zone(&origin).await?;
474        let rtype = record_type.as_str();
475
476        if records.is_empty() {
477            return self.delete_rrset(&zone_id, &name, rtype).await;
478        }
479
480        let mut items = Vec::with_capacity(records.len());
481        for record in records {
482            let (_, rdata) = Self::record_to_rdata(&record)?;
483            items.push(OciRecord {
484                domain: name.clone(),
485                rtype: rtype.to_string(),
486                rdata,
487                ttl,
488                is_protected: None,
489                record_hash: None,
490            });
491        }
492        self.put_records(&zone_id, &name, rtype, items).await
493    }
494
495    pub(crate) async fn add_to_rrset(
496        &self,
497        name: impl IntoFqdn<'_>,
498        record_type: DnsRecordType,
499        ttl: u32,
500        records: Vec<DnsRecord>,
501        origin: impl IntoFqdn<'_>,
502    ) -> Result<()> {
503        check_record_types(record_type, &records)?;
504        if records.is_empty() {
505            return Ok(());
506        }
507        let name = name.into_name().to_string();
508        let origin = origin.into_name().to_string();
509        let zone_id = self.resolve_zone(&origin).await?;
510        let rtype = record_type.as_str();
511
512        let existing = self.get_records(&zone_id, &name, rtype).await?;
513        let mut items = Vec::with_capacity(records.len());
514        for record in records {
515            let (_, rdata) = Self::record_to_rdata(&record)?;
516            if existing
517                .iter()
518                .any(|e| e.rtype.eq_ignore_ascii_case(rtype) && e.rdata == rdata)
519            {
520                continue;
521            }
522            items.push(PatchOperation {
523                operation: "ADD",
524                rdata,
525                ttl: Some(ttl),
526            });
527        }
528        if items.is_empty() {
529            return Ok(());
530        }
531        self.patch_records(&zone_id, &name, rtype, items).await
532    }
533
534    pub(crate) async fn remove_from_rrset(
535        &self,
536        name: impl IntoFqdn<'_>,
537        record_type: DnsRecordType,
538        records: Vec<DnsRecord>,
539        origin: impl IntoFqdn<'_>,
540    ) -> Result<()> {
541        check_record_types(record_type, &records)?;
542        if records.is_empty() {
543            return Ok(());
544        }
545        let name = name.into_name().to_string();
546        let origin = origin.into_name().to_string();
547        let zone_id = self.resolve_zone(&origin).await?;
548        let rtype = record_type.as_str();
549
550        let mut items = Vec::with_capacity(records.len());
551        for record in records {
552            let (_, rdata) = Self::record_to_rdata(&record)?;
553            items.push(PatchOperation {
554                operation: "REMOVE",
555                rdata,
556                ttl: None,
557            });
558        }
559        match self.patch_records(&zone_id, &name, rtype, items).await {
560            Ok(()) => Ok(()),
561            Err(Error::NotFound) => Ok(()),
562            Err(e) => Err(e),
563        }
564    }
565
566    pub(crate) async fn list_rrset(
567        &self,
568        name: impl IntoFqdn<'_>,
569        record_type: DnsRecordType,
570        origin: impl IntoFqdn<'_>,
571    ) -> Result<Vec<DnsRecord>> {
572        let name = name.into_name().to_string();
573        let origin = origin.into_name().to_string();
574        let zone_id = match self.resolve_zone(&origin).await {
575            Ok(id) => id,
576            Err(Error::NotFound) => return Ok(Vec::new()),
577            Err(e) => return Err(e),
578        };
579        let rtype = record_type.as_str();
580        let items = self.get_records(&zone_id, &name, rtype).await?;
581        let mut out = Vec::with_capacity(items.len());
582        for item in items {
583            if !item.rtype.eq_ignore_ascii_case(rtype) {
584                continue;
585            }
586            out.push(parse_rdata(record_type, &item.rdata)?);
587        }
588        Ok(out)
589    }
590}
591
592fn format_target(value: &str) -> String {
593    format!("{}.", value.trim_end_matches('.'))
594}
595
596fn urlencode(value: &str) -> String {
597    serde_urlencoded::to_string([("v", value)])
598        .ok()
599        .and_then(|s| s.strip_prefix("v=").map(str::to_string))
600        .unwrap_or_else(|| value.to_string())
601}
602
603fn map_error(status: reqwest::StatusCode, body: &str) -> Error {
604    match status.as_u16() {
605        400 => Error::BadRequest,
606        401 | 403 => Error::Unauthorized,
607        404 => Error::NotFound,
608        _ => Error::Api(format!("Oracle Cloud DNS error {}: {}", status, body)),
609    }
610}
611
612fn check_record_types(expected: DnsRecordType, records: &[DnsRecord]) -> Result<()> {
613    for r in records {
614        if r.as_type() != expected {
615            return Err(Error::Api(format!(
616                "RRSet record type mismatch: expected {}, got {}",
617                expected.as_str(),
618                r.as_type().as_str(),
619            )));
620        }
621    }
622    Ok(())
623}
624
625fn parse_rdata(record_type: DnsRecordType, value: &str) -> Result<DnsRecord> {
626    Ok(match record_type {
627        DnsRecordType::A => DnsRecord::A(value.parse().map_err(|e: AddrParseError| {
628            Error::Parse(format!("invalid A value '{value}': {e}"))
629        })?),
630        DnsRecordType::AAAA => DnsRecord::AAAA(value.parse().map_err(|e: AddrParseError| {
631            Error::Parse(format!("invalid AAAA value '{value}': {e}"))
632        })?),
633        DnsRecordType::CNAME => DnsRecord::CNAME(strip_trailing_dot(value)),
634        DnsRecordType::NS => DnsRecord::NS(strip_trailing_dot(value)),
635        DnsRecordType::MX => parse_mx(value)?,
636        DnsRecordType::TXT => DnsRecord::TXT(parse_txt(value)),
637        DnsRecordType::SRV => parse_srv(value)?,
638        DnsRecordType::TLSA => parse_tlsa(value)?,
639        DnsRecordType::CAA => parse_caa(value)?,
640    })
641}
642
643fn parse_mx(value: &str) -> Result<DnsRecord> {
644    let mut parts = value.splitn(2, char::is_whitespace);
645    let priority = parts
646        .next()
647        .ok_or_else(|| Error::Parse(format!("invalid MX value '{value}'")))?
648        .parse()
649        .map_err(|e| Error::Parse(format!("invalid MX priority in '{value}': {e}")))?;
650    let exchange = parts
651        .next()
652        .ok_or_else(|| Error::Parse(format!("invalid MX value '{value}'")))?
653        .trim();
654    Ok(DnsRecord::MX(MXRecord {
655        priority,
656        exchange: strip_trailing_dot(exchange),
657    }))
658}
659
660fn parse_srv(value: &str) -> Result<DnsRecord> {
661    let mut parts = value.split_whitespace();
662    let priority = parts
663        .next()
664        .ok_or_else(|| Error::Parse(format!("invalid SRV value '{value}'")))?
665        .parse()
666        .map_err(|e| Error::Parse(format!("invalid SRV priority in '{value}': {e}")))?;
667    let weight = parts
668        .next()
669        .ok_or_else(|| Error::Parse(format!("invalid SRV value '{value}'")))?
670        .parse()
671        .map_err(|e| Error::Parse(format!("invalid SRV weight in '{value}': {e}")))?;
672    let port = parts
673        .next()
674        .ok_or_else(|| Error::Parse(format!("invalid SRV value '{value}'")))?
675        .parse()
676        .map_err(|e| Error::Parse(format!("invalid SRV port in '{value}': {e}")))?;
677    let target = parts
678        .next()
679        .ok_or_else(|| Error::Parse(format!("invalid SRV value '{value}'")))?;
680    Ok(DnsRecord::SRV(SRVRecord {
681        priority,
682        weight,
683        port,
684        target: strip_trailing_dot(target),
685    }))
686}
687
688fn parse_txt(value: &str) -> String {
689    let trimmed = value.trim();
690    let mut out = String::with_capacity(trimmed.len());
691    let mut bytes = trimmed.bytes().peekable();
692    while let Some(&b) = bytes.peek() {
693        if b != b'"' {
694            bytes.next();
695            continue;
696        }
697        bytes.next();
698        loop {
699            match bytes.next() {
700                Some(b'"') => break,
701                Some(b'\\') => {
702                    if let Some(next) = bytes.next() {
703                        out.push(next as char);
704                    }
705                }
706                Some(other) => out.push(other as char),
707                None => break,
708            }
709        }
710    }
711    if out.is_empty() && !trimmed.is_empty() && !trimmed.starts_with('"') {
712        return trimmed.to_string();
713    }
714    out
715}
716
717fn parse_caa(value: &str) -> Result<DnsRecord> {
718    let mut parts = value.splitn(3, char::is_whitespace);
719    let flags: u8 = parts
720        .next()
721        .ok_or_else(|| Error::Parse(format!("invalid CAA value '{value}'")))?
722        .parse()
723        .map_err(|e| Error::Parse(format!("invalid CAA flags in '{value}': {e}")))?;
724    let tag = parts
725        .next()
726        .ok_or_else(|| Error::Parse(format!("invalid CAA value '{value}'")))?
727        .to_ascii_lowercase();
728    let raw_value = parts
729        .next()
730        .ok_or_else(|| Error::Parse(format!("invalid CAA value '{value}'")))?
731        .trim();
732    let unquoted = raw_value
733        .strip_prefix('"')
734        .and_then(|s| s.strip_suffix('"'))
735        .map(|s| s.replace("\\\"", "\""))
736        .unwrap_or_else(|| raw_value.to_string());
737
738    let issuer_critical = flags & 0x80 != 0;
739    match tag.as_str() {
740        "issue" => {
741            let (name, options) = parse_caa_kv(&unquoted);
742            Ok(DnsRecord::CAA(CAARecord::Issue {
743                issuer_critical,
744                name,
745                options,
746            }))
747        }
748        "issuewild" => {
749            let (name, options) = parse_caa_kv(&unquoted);
750            Ok(DnsRecord::CAA(CAARecord::IssueWild {
751                issuer_critical,
752                name,
753                options,
754            }))
755        }
756        "iodef" => Ok(DnsRecord::CAA(CAARecord::Iodef {
757            issuer_critical,
758            url: unquoted,
759        })),
760        other => Err(Error::Parse(format!("unknown CAA tag: {other}"))),
761    }
762}
763
764fn parse_caa_kv(value: &str) -> (Option<String>, Vec<DnsKeyValue>) {
765    let mut parts = value.split(';').map(str::trim);
766    let name_part = parts.next().unwrap_or("").trim().to_string();
767    let name = if name_part.is_empty() {
768        None
769    } else {
770        Some(name_part)
771    };
772    let options = parts
773        .filter(|p| !p.is_empty())
774        .map(|p| match p.split_once('=') {
775            Some((k, v)) => DnsKeyValue {
776                key: k.trim().to_string(),
777                value: v.trim().to_string(),
778            },
779            None => DnsKeyValue {
780                key: p.trim().to_string(),
781                value: String::new(),
782            },
783        })
784        .collect();
785    (name, options)
786}
787
788fn parse_tlsa(value: &str) -> Result<DnsRecord> {
789    let mut parts = value.split_whitespace();
790    let cert_usage_n: u8 = parts
791        .next()
792        .ok_or_else(|| Error::Parse(format!("invalid TLSA value '{value}'")))?
793        .parse()
794        .map_err(|e| Error::Parse(format!("invalid TLSA cert usage in '{value}': {e}")))?;
795    let selector_n: u8 = parts
796        .next()
797        .ok_or_else(|| Error::Parse(format!("invalid TLSA value '{value}'")))?
798        .parse()
799        .map_err(|e| Error::Parse(format!("invalid TLSA selector in '{value}': {e}")))?;
800    let matching_n: u8 = parts
801        .next()
802        .ok_or_else(|| Error::Parse(format!("invalid TLSA value '{value}'")))?
803        .parse()
804        .map_err(|e| Error::Parse(format!("invalid TLSA matching in '{value}': {e}")))?;
805    let hex: String = parts.collect::<Vec<_>>().join("");
806    let cert_data = hex_decode(&hex)
807        .map_err(|e| Error::Parse(format!("invalid TLSA hex in '{value}': {e}")))?;
808    Ok(DnsRecord::TLSA(TLSARecord {
809        cert_usage: tlsa_cert_usage_from(cert_usage_n)?,
810        selector: tlsa_selector_from(selector_n)?,
811        matching: tlsa_matching_from(matching_n)?,
812        cert_data,
813    }))
814}
815
816fn tlsa_cert_usage_from(n: u8) -> Result<TlsaCertUsage> {
817    Ok(match n {
818        0 => TlsaCertUsage::PkixTa,
819        1 => TlsaCertUsage::PkixEe,
820        2 => TlsaCertUsage::DaneTa,
821        3 => TlsaCertUsage::DaneEe,
822        255 => TlsaCertUsage::Private,
823        other => return Err(Error::Parse(format!("unknown TLSA cert usage: {other}"))),
824    })
825}
826
827fn tlsa_selector_from(n: u8) -> Result<TlsaSelector> {
828    Ok(match n {
829        0 => TlsaSelector::Full,
830        1 => TlsaSelector::Spki,
831        255 => TlsaSelector::Private,
832        other => return Err(Error::Parse(format!("unknown TLSA selector: {other}"))),
833    })
834}
835
836fn tlsa_matching_from(n: u8) -> Result<TlsaMatching> {
837    Ok(match n {
838        0 => TlsaMatching::Raw,
839        1 => TlsaMatching::Sha256,
840        2 => TlsaMatching::Sha512,
841        255 => TlsaMatching::Private,
842        other => return Err(Error::Parse(format!("unknown TLSA matching: {other}"))),
843    })
844}
845
846fn hex_decode(s: &str) -> std::result::Result<Vec<u8>, String> {
847    let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
848    if !s.len().is_multiple_of(2) {
849        return Err("odd hex length".to_string());
850    }
851    let mut out = Vec::with_capacity(s.len() / 2);
852    let bytes = s.as_bytes();
853    for i in (0..bytes.len()).step_by(2) {
854        let pair = std::str::from_utf8(&bytes[i..i + 2]).map_err(|e| e.to_string())?;
855        let byte = u8::from_str_radix(pair, 16).map_err(|e| e.to_string())?;
856        out.push(byte);
857    }
858    Ok(out)
859}
860
861fn strip_trailing_dot(s: &str) -> String {
862    s.strip_suffix('.').unwrap_or(s).to_string()
863}