1#![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}