1use std::net::{Ipv4Addr, Ipv6Addr};
2
3use clap::Subcommand;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::core::{
9 dns::{
10 responses::ListRecordsResponse,
11 service::{ListRecordsOptions, RecordWrite, ZoneRead},
12 },
13 error::Result,
14};
15
16pub mod query;
17
18pub async fn list_records<C: ZoneRead + ?Sized>(
24 client: &C,
25 domain: &str,
26 zone: Option<&str>,
27 options: ListRecordsOptions,
28) -> Result<ListRecordsResponse> {
29 client.list_records(domain, zone, options).await
30}
31
32pub async fn create_record<C: RecordWrite + ?Sized>(
38 client: &C,
39 zone: &str,
40 domain: &str,
41 ttl: u32,
42 record: &RecordData,
43) -> Result<Value> {
44 client.add_record(zone, domain, ttl, record).await
45}
46
47pub async fn delete_record<'a, C: RecordWrite + ?Sized>(
53 client: &'a C,
54 zone: &'a str,
55 domain: &'a str,
56 type_params: &'a [(&'a str, String)],
57) -> Result<Value> {
58 client.delete_record(zone, domain, type_params).await
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
64pub enum DsAlgorithm {
65 #[serde(rename = "RSAMD5")]
66 Rsamd5,
67 #[serde(rename = "DSA")]
68 Dsa,
69 #[serde(rename = "RSASHA1")]
70 Rsasha1,
71 #[serde(rename = "DSA-NSEC3-SHA1")]
72 DsaNsec3Sha1,
73 #[serde(rename = "RSASHA1-NSEC3-SHA1")]
74 Rsasha1Nsec3Sha1,
75 #[serde(rename = "RSASHA256")]
76 Rsasha256,
77 #[serde(rename = "RSASHA512")]
78 Rsasha512,
79 #[serde(rename = "ECC-GOST")]
80 EccGost,
81 #[serde(rename = "ECDSAP256SHA256")]
82 Ecdsap256sha256,
83 #[serde(rename = "ECDSAP384SHA384")]
84 Ecdsap384sha384,
85 #[serde(rename = "ED25519")]
86 Ed25519,
87 #[serde(rename = "ED448")]
88 Ed448,
89}
90
91impl DsAlgorithm {
92 pub fn as_str(&self) -> &'static str {
93 match self {
94 Self::Rsamd5 => "RSAMD5",
95 Self::Dsa => "DSA",
96 Self::Rsasha1 => "RSASHA1",
97 Self::DsaNsec3Sha1 => "DSA-NSEC3-SHA1",
98 Self::Rsasha1Nsec3Sha1 => "RSASHA1-NSEC3-SHA1",
99 Self::Rsasha256 => "RSASHA256",
100 Self::Rsasha512 => "RSASHA512",
101 Self::EccGost => "ECC-GOST",
102 Self::Ecdsap256sha256 => "ECDSAP256SHA256",
103 Self::Ecdsap384sha384 => "ECDSAP384SHA384",
104 Self::Ed25519 => "ED25519",
105 Self::Ed448 => "ED448",
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
111pub enum DigestType {
112 #[serde(rename = "SHA1")]
113 Sha1,
114 #[serde(rename = "SHA256")]
115 Sha256,
116 #[serde(rename = "GOST-R-34-11-94")]
117 GostR341194,
118 #[serde(rename = "SHA384")]
119 Sha384,
120}
121
122impl DigestType {
123 pub fn as_str(&self) -> &'static str {
124 match self {
125 Self::Sha1 => "SHA1",
126 Self::Sha256 => "SHA256",
127 Self::GostR341194 => "GOST-R-34-11-94",
128 Self::Sha384 => "SHA384",
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
134pub enum SshfpAlgorithm {
135 #[serde(rename = "RSA")]
136 Rsa,
137 #[serde(rename = "DSA")]
138 Dsa,
139 #[serde(rename = "ECDSA")]
140 Ecdsa,
141 #[serde(rename = "Ed25519")]
142 Ed25519,
143 #[serde(rename = "Ed448")]
144 Ed448,
145}
146
147impl SshfpAlgorithm {
148 pub fn as_str(&self) -> &'static str {
149 match self {
150 Self::Rsa => "RSA",
151 Self::Dsa => "DSA",
152 Self::Ecdsa => "ECDSA",
153 Self::Ed25519 => "Ed25519",
154 Self::Ed448 => "Ed448",
155 }
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
160pub enum SshfpFingerprintType {
161 #[serde(rename = "SHA1")]
162 Sha1,
163 #[serde(rename = "SHA256")]
164 Sha256,
165}
166
167impl SshfpFingerprintType {
168 pub fn as_str(&self) -> &'static str {
169 match self {
170 Self::Sha1 => "SHA1",
171 Self::Sha256 => "SHA256",
172 }
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
177pub enum TlsaCertUsage {
178 #[serde(rename = "PKIX-TA")]
179 PkixTa,
180 #[serde(rename = "PKIX-EE")]
181 PkixEe,
182 #[serde(rename = "DANE-TA")]
183 DaneTa,
184 #[serde(rename = "DANE-EE")]
185 DaneEe,
186}
187
188impl TlsaCertUsage {
189 pub fn as_str(&self) -> &'static str {
190 match self {
191 Self::PkixTa => "PKIX-TA",
192 Self::PkixEe => "PKIX-EE",
193 Self::DaneTa => "DANE-TA",
194 Self::DaneEe => "DANE-EE",
195 }
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
200pub enum TlsaSelector {
201 #[serde(rename = "Cert")]
202 Cert,
203 #[serde(rename = "SPKI")]
204 Spki,
205}
206
207impl TlsaSelector {
208 pub fn as_str(&self) -> &'static str {
209 match self {
210 Self::Cert => "Cert",
211 Self::Spki => "SPKI",
212 }
213 }
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
217pub enum TlsaMatchingType {
218 #[serde(rename = "Full")]
219 Full,
220 #[serde(rename = "SHA2-256")]
221 Sha2_256,
222 #[serde(rename = "SHA2-512")]
223 Sha2_512,
224}
225
226impl TlsaMatchingType {
227 pub fn as_str(&self) -> &'static str {
228 match self {
229 Self::Full => "Full",
230 Self::Sha2_256 => "SHA2-256",
231 Self::Sha2_512 => "SHA2-512",
232 }
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
237pub enum FwdProtocol {
238 Udp,
239 Tcp,
240 Tls,
241 Https,
242 Quic,
243}
244
245impl FwdProtocol {
246 pub fn as_str(&self) -> &'static str {
247 match self {
248 Self::Udp => "Udp",
249 Self::Tcp => "Tcp",
250 Self::Tls => "Tls",
251 Self::Https => "Https",
252 Self::Quic => "Quic",
253 }
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Subcommand)]
265#[serde(tag = "type", rename_all = "UPPERCASE")]
266#[command(rename_all = "lower")]
267pub enum RecordData {
268 A {
270 #[serde(rename = "ipAddress")]
271 ip: Ipv4Addr,
272 },
273 Aaaa {
275 #[serde(rename = "ipAddress")]
276 ip: Ipv6Addr,
277 },
278 Aname { aname: String },
280 App {
282 #[serde(rename = "appName")]
283 app_name: String,
284 #[serde(rename = "classPath")]
285 class_path: String,
286 #[serde(rename = "recordData")]
288 record_data: String,
289 },
290 Caa {
292 value: String,
293 #[arg(long, default_value_t = 0)]
294 flags: u8,
295 #[arg(long, default_value = "issue")]
297 tag: String,
298 },
299 Cname {
301 #[serde(rename = "cname")]
302 target: String,
303 },
304 Dname { dname: String },
306 Ds {
308 #[serde(rename = "keyTag")]
309 key_tag: u16,
310 algorithm: DsAlgorithm,
311 #[serde(rename = "digestType")]
312 digest_type: DigestType,
313 digest: String,
314 },
315 Fwd {
317 forwarder: String,
318 #[arg(long, default_value = "Udp")]
319 protocol: FwdProtocol,
320 #[serde(rename = "forwarderPriority", default = "default_fwd_priority")]
321 #[arg(long, default_value_t = 10)]
322 priority: u16,
323 #[serde(rename = "dnssecValidation", default)]
324 #[arg(long, default_value_t = false)]
325 dnssec_validation: bool,
326 },
327 Https {
329 #[serde(rename = "svcTargetName")]
330 svc_target_name: String,
331 #[serde(rename = "svcPriority")]
332 #[arg(long, default_value_t = 1)]
333 svc_priority: u16,
334 #[serde(rename = "svcParams")]
335 #[arg(long)]
336 svc_params: Option<String>,
337 #[serde(rename = "autoIpv4Hint", default)]
338 #[arg(long, default_value_t = false)]
339 auto_ipv4_hint: bool,
340 #[serde(rename = "autoIpv6Hint", default)]
341 #[arg(long, default_value_t = false)]
342 auto_ipv6_hint: bool,
343 },
344 Mx {
346 exchange: String,
347 #[serde(default = "default_mx_preference")]
348 #[arg(long, default_value_t = 10)]
349 preference: u16,
350 },
351 Naptr {
353 #[serde(rename = "naptrOrder")]
354 #[arg(long)]
355 order: u16,
356 #[serde(rename = "naptrPreference")]
357 #[arg(long)]
358 preference: u16,
359 #[serde(rename = "naptrFlags")]
360 #[arg(long, default_value = "")]
361 flags: String,
362 #[serde(rename = "naptrServices")]
363 #[arg(long, default_value = "")]
364 services: String,
365 #[serde(rename = "naptrRegexp")]
366 #[arg(long, default_value = "")]
367 regexp: String,
368 #[serde(rename = "naptrReplacement")]
369 replacement: String,
370 },
371 Ns {
373 #[serde(rename = "nameServer")]
374 nameserver: String,
375 #[arg(long)]
376 glue: Option<String>,
377 },
378 Ptr {
380 #[serde(rename = "ptrName")]
381 name: String,
382 },
383 Sshfp {
385 #[serde(rename = "sshfpAlgorithm")]
386 algorithm: SshfpAlgorithm,
387 #[serde(rename = "sshfpFingerprintType")]
388 fingerprint_type: SshfpFingerprintType,
389 #[serde(rename = "sshfpFingerprint")]
390 fingerprint: String,
391 },
392 Srv {
394 target: String,
395 #[arg(long)]
396 port: u16,
397 #[arg(long, default_value_t = 0)]
398 priority: u16,
399 #[arg(long, default_value_t = 0)]
400 weight: u16,
401 },
402 Svcb {
404 #[serde(rename = "svcTargetName")]
405 svc_target_name: String,
406 #[serde(rename = "svcPriority")]
407 #[arg(long, default_value_t = 1)]
408 svc_priority: u16,
409 #[serde(rename = "svcParams")]
410 #[arg(long)]
411 svc_params: Option<String>,
412 #[serde(rename = "autoIpv4Hint", default)]
413 #[arg(long, default_value_t = false)]
414 auto_ipv4_hint: bool,
415 #[serde(rename = "autoIpv6Hint", default)]
416 #[arg(long, default_value_t = false)]
417 auto_ipv6_hint: bool,
418 },
419 Tlsa {
421 #[serde(rename = "tlsaCertificateUsage")]
422 cert_usage: TlsaCertUsage,
423 #[serde(rename = "tlsaSelector")]
424 selector: TlsaSelector,
425 #[serde(rename = "tlsaMatchingType")]
426 matching_type: TlsaMatchingType,
427 #[serde(rename = "tlsaCertificateAssociationData")]
428 cert_association_data: String,
429 },
430 Txt {
432 text: String,
433 #[serde(rename = "splitText", default)]
434 #[arg(long, default_value_t = false)]
435 split_text: bool,
436 },
437 Uri {
439 uri: String,
440 #[serde(rename = "uriPriority")]
441 #[arg(long, default_value_t = 10)]
442 priority: u16,
443 #[serde(rename = "uriWeight")]
444 #[arg(long, default_value_t = 1)]
445 weight: u16,
446 },
447 Unknown { rdata: String },
449}
450
451fn default_mx_preference() -> u16 {
452 10
453}
454fn default_fwd_priority() -> u16 {
455 10
456}
457
458impl RecordData {
459 pub fn type_name(&self) -> &'static str {
460 match self {
461 Self::A { .. } => "A",
462 Self::Aaaa { .. } => "AAAA",
463 Self::Aname { .. } => "ANAME",
464 Self::App { .. } => "APP",
465 Self::Caa { .. } => "CAA",
466 Self::Cname { .. } => "CNAME",
467 Self::Dname { .. } => "DNAME",
468 Self::Ds { .. } => "DS",
469 Self::Fwd { .. } => "FWD",
470 Self::Https { .. } => "HTTPS",
471 Self::Mx { .. } => "MX",
472 Self::Naptr { .. } => "NAPTR",
473 Self::Ns { .. } => "NS",
474 Self::Ptr { .. } => "PTR",
475 Self::Sshfp { .. } => "SSHFP",
476 Self::Srv { .. } => "SRV",
477 Self::Svcb { .. } => "SVCB",
478 Self::Tlsa { .. } => "TLSA",
479 Self::Txt { .. } => "TXT",
480 Self::Uri { .. } => "URI",
481 Self::Unknown { .. } => "UNKNOWN",
482 }
483 }
484
485 pub fn to_api_params(&self) -> Vec<(&'static str, String)> {
486 let mut p = vec![("type", self.type_name().into())];
487 match self {
488 Self::A { ip } => p.push(("ipAddress", ip.to_string())),
489 Self::Aaaa { ip } => p.push(("ipAddress", ip.to_string())),
490 Self::Aname { aname } => p.push(("aname", aname.clone())),
491 Self::App {
492 app_name,
493 class_path,
494 record_data,
495 } => {
496 p.push(("appName", app_name.clone()));
497 p.push(("classPath", class_path.clone()));
498 p.push(("recordData", record_data.clone()));
499 }
500 Self::Caa { flags, tag, value } => {
501 p.push(("flags", flags.to_string()));
502 p.push(("tag", tag.clone()));
503 p.push(("value", value.clone()));
504 }
505 Self::Cname { target } => p.push(("cname", target.clone())),
506 Self::Dname { dname } => p.push(("dname", dname.clone())),
507 Self::Ds {
508 key_tag,
509 algorithm,
510 digest_type,
511 digest,
512 } => {
513 p.push(("keyTag", key_tag.to_string()));
514 p.push(("algorithm", algorithm.as_str().into()));
515 p.push(("digestType", digest_type.as_str().into()));
516 p.push(("digest", digest.clone()));
517 }
518 Self::Fwd {
519 forwarder,
520 protocol,
521 priority,
522 dnssec_validation,
523 } => {
524 p.push(("forwarder", forwarder.clone()));
525 p.push(("protocol", protocol.as_str().into()));
526 p.push(("forwarderPriority", priority.to_string()));
527 p.push(("dnssecValidation", dnssec_validation.to_string()));
528 }
529 Self::Https {
530 svc_priority,
531 svc_target_name,
532 svc_params,
533 auto_ipv4_hint,
534 auto_ipv6_hint,
535 }
536 | Self::Svcb {
537 svc_priority,
538 svc_target_name,
539 svc_params,
540 auto_ipv4_hint,
541 auto_ipv6_hint,
542 } => {
543 p.push(("svcPriority", svc_priority.to_string()));
544 p.push(("svcTargetName", svc_target_name.clone()));
545 if let Some(params) = svc_params {
546 p.push(("svcParams", params.clone()));
547 }
548 p.push(("autoIpv4Hint", auto_ipv4_hint.to_string()));
549 p.push(("autoIpv6Hint", auto_ipv6_hint.to_string()));
550 }
551 Self::Mx {
552 preference,
553 exchange,
554 } => {
555 p.push(("preference", preference.to_string()));
556 p.push(("exchange", exchange.clone()));
557 }
558 Self::Naptr {
559 order,
560 preference,
561 flags,
562 services,
563 regexp,
564 replacement,
565 } => {
566 p.push(("naptrOrder", order.to_string()));
567 p.push(("naptrPreference", preference.to_string()));
568 p.push(("naptrFlags", flags.clone()));
569 p.push(("naptrServices", services.clone()));
570 p.push(("naptrRegexp", regexp.clone()));
571 p.push(("naptrReplacement", replacement.clone()));
572 }
573 Self::Ns { nameserver, glue } => {
574 p.push(("nameServer", nameserver.clone()));
575 if let Some(g) = glue {
576 p.push(("glue", g.clone()));
577 }
578 }
579 Self::Ptr { name } => p.push(("ptrName", name.clone())),
580 Self::Sshfp {
581 algorithm,
582 fingerprint_type,
583 fingerprint,
584 } => {
585 p.push(("sshfpAlgorithm", algorithm.as_str().into()));
586 p.push(("sshfpFingerprintType", fingerprint_type.as_str().into()));
587 p.push(("sshfpFingerprint", fingerprint.clone()));
588 }
589 Self::Srv {
590 priority,
591 weight,
592 port,
593 target,
594 } => {
595 p.push(("priority", priority.to_string()));
596 p.push(("weight", weight.to_string()));
597 p.push(("port", port.to_string()));
598 p.push(("target", target.clone()));
599 }
600 Self::Tlsa {
601 cert_usage,
602 selector,
603 matching_type,
604 cert_association_data,
605 } => {
606 p.push(("tlsaCertificateUsage", cert_usage.as_str().into()));
607 p.push(("tlsaSelector", selector.as_str().into()));
608 p.push(("tlsaMatchingType", matching_type.as_str().into()));
609 p.push((
610 "tlsaCertificateAssociationData",
611 cert_association_data.clone(),
612 ));
613 }
614 Self::Txt { text, split_text } => {
615 p.push(("text", text.clone()));
616 p.push(("splitText", split_text.to_string()));
617 }
618 Self::Uri {
619 priority,
620 weight,
621 uri,
622 } => {
623 p.push(("uriPriority", priority.to_string()));
624 p.push(("uriWeight", weight.to_string()));
625 p.push(("uri", uri.clone()));
626 }
627 Self::Unknown { rdata } => p.push(("rdata", rdata.clone())),
628 }
629 p
630 }
631}
632
633#[derive(Debug, Clone, Deserialize, JsonSchema, Subcommand)]
643#[serde(tag = "type", rename_all = "UPPERCASE")]
644#[command(rename_all = "lower")]
645pub enum RecordSelector {
646 A {
648 #[serde(rename = "ipAddress")]
649 ip: Option<Ipv4Addr>,
650 },
651 Aaaa {
653 #[serde(rename = "ipAddress")]
654 ip: Option<Ipv6Addr>,
655 },
656 Aname {
657 aname: Option<String>,
658 },
659 App {
660 #[serde(rename = "appName")]
661 app_name: Option<String>,
662 #[serde(rename = "classPath")]
663 class_path: Option<String>,
664 },
665 Caa {
666 value: Option<String>,
667 },
668 Cname {
669 #[serde(rename = "cname")]
670 target: Option<String>,
671 },
672 Dname {
673 dname: Option<String>,
674 },
675 Ds {
676 #[serde(rename = "keyTag")]
677 key_tag: Option<u16>,
678 },
679 Fwd {
680 forwarder: Option<String>,
681 },
682 Https {
683 #[serde(rename = "svcTargetName")]
684 svc_target_name: Option<String>,
685 },
686 Mx {
687 exchange: Option<String>,
688 },
689 Naptr {
690 #[serde(rename = "naptrReplacement")]
691 replacement: Option<String>,
692 },
693 Ns {
694 #[serde(rename = "nameServer")]
695 nameserver: Option<String>,
696 },
697 Ptr {
698 #[serde(rename = "ptrName")]
699 name: Option<String>,
700 },
701 Sshfp {
702 #[serde(rename = "sshfpFingerprint")]
703 fingerprint: Option<String>,
704 },
705 Srv {
706 target: Option<String>,
707 #[arg(long)]
708 port: Option<u16>,
709 #[arg(long)]
710 priority: Option<u16>,
711 #[arg(long)]
712 weight: Option<u16>,
713 },
714 Svcb {
715 #[serde(rename = "svcTargetName")]
716 svc_target_name: Option<String>,
717 },
718 Tlsa {
719 #[serde(rename = "tlsaCertificateAssociationData")]
720 cert_association_data: Option<String>,
721 },
722 Txt {
723 text: Option<String>,
724 },
725 Uri {
726 uri: Option<String>,
727 },
728 Unknown {
729 rdata: Option<String>,
730 },
731}
732
733impl RecordSelector {
734 pub fn type_name(&self) -> &'static str {
735 match self {
736 Self::A { .. } => "A",
737 Self::Aaaa { .. } => "AAAA",
738 Self::Aname { .. } => "ANAME",
739 Self::App { .. } => "APP",
740 Self::Caa { .. } => "CAA",
741 Self::Cname { .. } => "CNAME",
742 Self::Dname { .. } => "DNAME",
743 Self::Ds { .. } => "DS",
744 Self::Fwd { .. } => "FWD",
745 Self::Https { .. } => "HTTPS",
746 Self::Mx { .. } => "MX",
747 Self::Naptr { .. } => "NAPTR",
748 Self::Ns { .. } => "NS",
749 Self::Ptr { .. } => "PTR",
750 Self::Sshfp { .. } => "SSHFP",
751 Self::Srv { .. } => "SRV",
752 Self::Svcb { .. } => "SVCB",
753 Self::Tlsa { .. } => "TLSA",
754 Self::Txt { .. } => "TXT",
755 Self::Uri { .. } => "URI",
756 Self::Unknown { .. } => "UNKNOWN",
757 }
758 }
759
760 pub fn to_api_params(&self) -> Vec<(&'static str, String)> {
761 let mut p = vec![("type", self.type_name().into())];
762 match self {
763 Self::A { ip } => {
764 if let Some(v) = ip {
765 p.push(("ipAddress", v.to_string()));
766 }
767 }
768 Self::Aaaa { ip } => {
769 if let Some(v) = ip {
770 p.push(("ipAddress", v.to_string()));
771 }
772 }
773 Self::Aname { aname } => {
774 if let Some(v) = aname {
775 p.push(("aname", v.clone()));
776 }
777 }
778 Self::App {
779 app_name,
780 class_path,
781 } => {
782 if let Some(v) = app_name {
783 p.push(("appName", v.clone()));
784 }
785 if let Some(v) = class_path {
786 p.push(("classPath", v.clone()));
787 }
788 }
789 Self::Caa { value } => {
790 if let Some(v) = value {
791 p.push(("value", v.clone()));
792 }
793 }
794 Self::Cname { target } => {
795 if let Some(v) = target {
796 p.push(("cname", v.clone()));
797 }
798 }
799 Self::Dname { dname } => {
800 if let Some(v) = dname {
801 p.push(("dname", v.clone()));
802 }
803 }
804 Self::Ds { key_tag } => {
805 if let Some(v) = key_tag {
806 p.push(("keyTag", v.to_string()));
807 }
808 }
809 Self::Fwd { forwarder } => {
810 if let Some(v) = forwarder {
811 p.push(("forwarder", v.clone()));
812 }
813 }
814 Self::Https { svc_target_name } | Self::Svcb { svc_target_name } => {
815 if let Some(v) = svc_target_name {
816 p.push(("svcTargetName", v.clone()));
817 }
818 }
819 Self::Mx { exchange } => {
820 if let Some(v) = exchange {
821 p.push(("exchange", v.clone()));
822 }
823 }
824 Self::Naptr { replacement } => {
825 if let Some(v) = replacement {
826 p.push(("naptrReplacement", v.clone()));
827 }
828 }
829 Self::Ns { nameserver } => {
830 if let Some(v) = nameserver {
831 p.push(("nameServer", v.clone()));
832 }
833 }
834 Self::Ptr { name } => {
835 if let Some(v) = name {
836 p.push(("ptrName", v.clone()));
837 }
838 }
839 Self::Sshfp { fingerprint } => {
840 if let Some(v) = fingerprint {
841 p.push(("sshfpFingerprint", v.clone()));
842 }
843 }
844 Self::Srv {
845 target,
846 port,
847 priority,
848 weight,
849 } => {
850 if let Some(v) = target {
851 p.push(("target", v.clone()));
852 }
853 if let Some(v) = port {
854 p.push(("port", v.to_string()));
855 }
856 if let Some(v) = priority {
857 p.push(("priority", v.to_string()));
858 }
859 if let Some(v) = weight {
860 p.push(("weight", v.to_string()));
861 }
862 }
863 Self::Tlsa {
864 cert_association_data,
865 } => {
866 if let Some(v) = cert_association_data {
867 p.push(("tlsaCertificateAssociationData", v.clone()));
868 }
869 }
870 Self::Txt { text } => {
871 if let Some(v) = text {
872 p.push(("text", v.clone()));
873 }
874 }
875 Self::Uri { uri } => {
876 if let Some(v) = uri {
877 p.push(("uri", v.clone()));
878 }
879 }
880 Self::Unknown { rdata } => {
881 if let Some(v) = rdata {
882 p.push(("rdata", v.clone()));
883 }
884 }
885 }
886 p
887 }
888}
889
890#[cfg(test)]
893mod tests {
894 use super::*;
895 use rstest::{fixture, rstest};
896
897 #[fixture]
900 fn a_record() -> RecordData {
901 RecordData::A {
902 ip: "1.2.3.4".parse().unwrap(),
903 }
904 }
905
906 #[fixture]
907 fn mx_record() -> RecordData {
908 RecordData::Mx {
909 preference: 10,
910 exchange: "mail.example.com".into(),
911 }
912 }
913
914 #[fixture]
915 fn srv_record() -> RecordData {
916 RecordData::Srv {
917 priority: 10,
918 weight: 20,
919 port: 5060,
920 target: "sip.example.com".into(),
921 }
922 }
923
924 #[fixture]
925 fn ns_with_glue() -> RecordData {
926 RecordData::Ns {
927 nameserver: "ns1.example.com".into(),
928 glue: Some("1.2.3.4".into()),
929 }
930 }
931
932 #[fixture]
933 fn ns_without_glue() -> RecordData {
934 RecordData::Ns {
935 nameserver: "ns1.example.com".into(),
936 glue: None,
937 }
938 }
939
940 #[rstest]
943 #[case::a(RecordData::A { ip: "1.2.3.4".parse().unwrap() }, "A")]
944 #[case::aaaa(RecordData::Aaaa { ip: "::1".parse().unwrap() }, "AAAA")]
945 #[case::aname(RecordData::Aname { aname: "t.example.com".into() }, "ANAME")]
946 #[case::app(RecordData::App { app_name: "App".into(), class_path: "C".into(), record_data: "{}".into() }, "APP")]
947 #[case::caa(RecordData::Caa { flags: 0, tag: "issue".into(), value: "le.org".into() }, "CAA")]
948 #[case::cname(RecordData::Cname { target: "www.example.com".into() }, "CNAME")]
949 #[case::dname(RecordData::Dname { dname: "other.example.com".into() }, "DNAME")]
950 #[case::ds(RecordData::Ds { key_tag: 1, algorithm: DsAlgorithm::Rsasha256, digest_type: DigestType::Sha256, digest: "ab".into() }, "DS")]
951 #[case::fwd(RecordData::Fwd { forwarder: "1.1.1.1".into(), protocol: FwdProtocol::Udp, priority: 10, dnssec_validation: false }, "FWD")]
952 #[case::https(RecordData::Https { svc_priority: 1, svc_target_name: "svc.example.com".into(), svc_params: None, auto_ipv4_hint: false, auto_ipv6_hint: false }, "HTTPS")]
953 #[case::mx(RecordData::Mx { preference: 10, exchange: "mail.example.com".into() }, "MX")]
954 #[case::naptr(RecordData::Naptr { order: 10, preference: 20, flags: "U".into(), services: "E2U+sip".into(), regexp: "".into(), replacement: ".".into() }, "NAPTR")]
955 #[case::ns(RecordData::Ns { nameserver: "ns1.example.com".into(), glue: None }, "NS")]
956 #[case::ptr(RecordData::Ptr { name: "host.example.com".into() }, "PTR")]
957 #[case::sshfp(RecordData::Sshfp { algorithm: SshfpAlgorithm::Rsa, fingerprint_type: SshfpFingerprintType::Sha256, fingerprint: "abcd".into() }, "SSHFP")]
958 #[case::srv(RecordData::Srv { priority: 0, weight: 0, port: 80, target: "t.example.com".into() }, "SRV")]
959 #[case::svcb(RecordData::Svcb { svc_priority: 1, svc_target_name: "svc.example.com".into(), svc_params: None, auto_ipv4_hint: false, auto_ipv6_hint: false }, "SVCB")]
960 #[case::tlsa(RecordData::Tlsa { cert_usage: TlsaCertUsage::DaneEe, selector: TlsaSelector::Spki, matching_type: TlsaMatchingType::Sha2_256, cert_association_data: "ab".into() }, "TLSA")]
961 #[case::txt(RecordData::Txt { text: "v=spf1 ~all".into(), split_text: false }, "TXT")]
962 #[case::uri(RecordData::Uri { priority: 1, weight: 1, uri: "https://example.com".into() }, "URI")]
963 #[case::unknown(RecordData::Unknown { rdata: "0a0b".into() }, "UNKNOWN")]
964 fn type_name_matches_variant(#[case] record: RecordData, #[case] expected: &str) {
965 assert_eq!(record.type_name(), expected);
966 }
967
968 fn params_map(record: &RecordData) -> std::collections::HashMap<&'static str, String> {
971 record.to_api_params().into_iter().collect()
972 }
973
974 #[rstest]
975 fn a_uses_ip_address_key(a_record: RecordData) {
976 let p = params_map(&a_record);
977 assert_eq!(p["type"], "A");
978 assert_eq!(p["ipAddress"], "1.2.3.4");
979 assert!(!p.contains_key("ip"));
981 }
982
983 #[rstest]
984 fn aaaa_uses_ip_address_key() {
985 let r = RecordData::Aaaa {
986 ip: "2001:db8::1".parse().unwrap(),
987 };
988 let p = params_map(&r);
989 assert_eq!(p["type"], "AAAA");
990 assert_eq!(p["ipAddress"], "2001:db8::1");
991 }
992
993 #[rstest]
994 fn mx_uses_exchange_and_preference(mx_record: RecordData) {
995 let p = params_map(&mx_record);
996 assert_eq!(p["type"], "MX");
997 assert_eq!(p["exchange"], "mail.example.com");
998 assert_eq!(p["preference"], "10");
999 }
1000
1001 #[rstest]
1002 fn ns_uses_name_server_key(ns_without_glue: RecordData) {
1003 let p = params_map(&ns_without_glue);
1004 assert_eq!(p["type"], "NS");
1005 assert_eq!(p["nameServer"], "ns1.example.com"); assert!(!p.contains_key("glue"));
1007 }
1008
1009 #[rstest]
1010 fn ns_includes_glue_when_present(ns_with_glue: RecordData) {
1011 let p = params_map(&ns_with_glue);
1012 assert_eq!(p["glue"], "1.2.3.4");
1013 }
1014
1015 #[rstest]
1016 fn ptr_uses_ptr_name_key() {
1017 let r = RecordData::Ptr {
1018 name: "host.example.com".into(),
1019 };
1020 let p = params_map(&r);
1021 assert_eq!(p["ptrName"], "host.example.com");
1022 assert!(!p.contains_key("name"));
1023 }
1024
1025 #[rstest]
1026 fn cname_uses_cname_key() {
1027 let r = RecordData::Cname {
1028 target: "www.example.com".into(),
1029 };
1030 let p = params_map(&r);
1031 assert_eq!(p["cname"], "www.example.com");
1032 assert!(!p.contains_key("target"));
1033 }
1034
1035 #[rstest]
1036 fn srv_uses_correct_keys(srv_record: RecordData) {
1037 let p = params_map(&srv_record);
1038 assert_eq!(p["type"], "SRV");
1039 assert_eq!(p["priority"], "10");
1040 assert_eq!(p["weight"], "20");
1041 assert_eq!(p["port"], "5060");
1042 assert_eq!(p["target"], "sip.example.com");
1043 }
1044
1045 #[rstest]
1046 fn ds_uses_camel_case_keys() {
1047 let r = RecordData::Ds {
1048 key_tag: 12345,
1049 algorithm: DsAlgorithm::Ecdsap256sha256,
1050 digest_type: DigestType::Sha256,
1051 digest: "deadbeef".into(),
1052 };
1053 let p = params_map(&r);
1054 assert_eq!(p["keyTag"], "12345");
1055 assert_eq!(p["algorithm"], "ECDSAP256SHA256");
1056 assert_eq!(p["digestType"], "SHA256");
1057 assert_eq!(p["digest"], "deadbeef");
1058 }
1059
1060 #[rstest]
1061 fn tlsa_uses_full_key_names() {
1062 let r = RecordData::Tlsa {
1063 cert_usage: TlsaCertUsage::DaneTa,
1064 selector: TlsaSelector::Cert,
1065 matching_type: TlsaMatchingType::Sha2_512,
1066 cert_association_data: "cafebabe".into(),
1067 };
1068 let p = params_map(&r);
1069 assert_eq!(p["tlsaCertificateUsage"], "DANE-TA");
1070 assert_eq!(p["tlsaSelector"], "Cert");
1071 assert_eq!(p["tlsaMatchingType"], "SHA2-512");
1072 assert_eq!(p["tlsaCertificateAssociationData"], "cafebabe");
1073 }
1074
1075 #[rstest]
1076 fn fwd_uses_forwarder_priority_key() {
1077 let r = RecordData::Fwd {
1078 forwarder: "8.8.8.8".into(),
1079 protocol: FwdProtocol::Tls,
1080 priority: 5,
1081 dnssec_validation: true,
1082 };
1083 let p = params_map(&r);
1084 assert_eq!(p["forwarder"], "8.8.8.8");
1085 assert_eq!(p["protocol"], "Tls");
1086 assert_eq!(p["forwarderPriority"], "5"); assert_eq!(p["dnssecValidation"], "true");
1088 }
1089
1090 #[rstest]
1091 fn https_and_svcb_use_svc_prefix() {
1092 let https = RecordData::Https {
1093 svc_priority: 1,
1094 svc_target_name: "svc.example.com".into(),
1095 svc_params: Some("alpn|h2".into()),
1096 auto_ipv4_hint: true,
1097 auto_ipv6_hint: false,
1098 };
1099 let svcb = RecordData::Svcb {
1100 svc_priority: 1,
1101 svc_target_name: "svc.example.com".into(),
1102 svc_params: Some("alpn|h2".into()),
1103 auto_ipv4_hint: true,
1104 auto_ipv6_hint: false,
1105 };
1106 for r in [&https, &svcb] {
1107 let p = params_map(r);
1108 assert_eq!(p["svcPriority"], "1");
1109 assert_eq!(p["svcTargetName"], "svc.example.com");
1110 assert_eq!(p["svcParams"], "alpn|h2");
1111 assert_eq!(p["autoIpv4Hint"], "true");
1112 assert_eq!(p["autoIpv6Hint"], "false");
1113 }
1114 }
1115
1116 #[rstest]
1117 fn https_omits_svc_params_when_none() {
1118 let r = RecordData::Https {
1119 svc_priority: 1,
1120 svc_target_name: "svc.example.com".into(),
1121 svc_params: None,
1122 auto_ipv4_hint: false,
1123 auto_ipv6_hint: false,
1124 };
1125 let p = params_map(&r);
1126 assert!(!p.contains_key("svcParams"));
1127 }
1128
1129 #[rstest]
1130 fn uri_uses_uri_prefix_keys() {
1131 let r = RecordData::Uri {
1132 priority: 5,
1133 weight: 3,
1134 uri: "https://example.com/path".into(),
1135 };
1136 let p = params_map(&r);
1137 assert_eq!(p["uriPriority"], "5");
1138 assert_eq!(p["uriWeight"], "3");
1139 assert_eq!(p["uri"], "https://example.com/path");
1140 }
1141
1142 #[rstest]
1143 fn naptr_uses_naptr_prefix_keys() {
1144 let r = RecordData::Naptr {
1145 order: 10,
1146 preference: 20,
1147 flags: "U".into(),
1148 services: "E2U+sip".into(),
1149 regexp: "!^.*$!sip:info@example.com!".into(),
1150 replacement: ".".into(),
1151 };
1152 let p = params_map(&r);
1153 assert_eq!(p["naptrOrder"], "10");
1154 assert_eq!(p["naptrPreference"], "20");
1155 assert_eq!(p["naptrFlags"], "U");
1156 assert_eq!(p["naptrServices"], "E2U+sip");
1157 assert_eq!(p["naptrRegexp"], "!^.*$!sip:info@example.com!");
1158 assert_eq!(p["naptrReplacement"], ".");
1159 }
1160
1161 #[rstest]
1162 fn txt_includes_split_text_flag() {
1163 let r = RecordData::Txt {
1164 text: "v=spf1 ~all".into(),
1165 split_text: true,
1166 };
1167 let p = params_map(&r);
1168 assert_eq!(p["text"], "v=spf1 ~all");
1169 assert_eq!(p["splitText"], "true");
1170 }
1171
1172 #[rstest]
1175 fn type_param_is_always_first(
1176 #[values(
1177 RecordData::A { ip: "1.2.3.4".parse().unwrap() },
1178 RecordData::Cname { target: "www.example.com".into() },
1179 RecordData::Txt { text: "test".into(), split_text: false }
1180 )]
1181 record: RecordData,
1182 ) {
1183 let params = record.to_api_params();
1184 assert_eq!(params[0].0, "type");
1185 assert_eq!(params[0].1, record.type_name());
1186 }
1187}