Skip to main content

dnslib/vendors/cloudflare/
mapping.rs

1//! Cloudflare-specific DNS record mapping and normalization.
2//!
3//! Cloudflare's API uses its own JSON payload shapes that differ from the
4//! vendor-neutral `core::dns` types. The functions here translate between
5//! Cloudflare's format and internal zone-record representations.
6
7use serde_json::Value;
8
9use crate::core::dns::{records::RecordData, responses::ZoneRecord};
10
11/// Strip `.{zone_name}` suffix from a fully qualified domain name.
12/// Returns `"@"` for the zone apex itself.
13pub fn extract_relative_name(fqdn: &str, zone_name: &str) -> String {
14    let fqdn_lower = fqdn.to_lowercase();
15    let zone_lower = zone_name.to_lowercase();
16
17    if fqdn_lower == zone_lower {
18        return "@".to_string();
19    }
20
21    let suffix = format!(".{}", zone_lower);
22    if fqdn_lower.ends_with(&suffix) {
23        fqdn[..fqdn.len() - suffix.len()].to_string()
24    } else {
25        fqdn.to_string()
26    }
27}
28
29// ─── SSHFP ─────────────────────────────────────────────────────────────────
30
31pub fn sshfp_algorithm_to_str(n: u64) -> &'static str {
32    match n {
33        1 => "RSA",
34        2 => "DSA",
35        3 => "ECDSA",
36        4 => "Ed25519",
37        6 => "Ed448",
38        _ => "RSA",
39    }
40}
41
42pub fn sshfp_algorithm_to_num(alg: &crate::core::dns::records::SshfpAlgorithm) -> u8 {
43    use crate::core::dns::records::SshfpAlgorithm::*;
44    match alg {
45        Rsa => 1,
46        Dsa => 2,
47        Ecdsa => 3,
48        Ed25519 => 4,
49        Ed448 => 6,
50    }
51}
52
53pub fn sshfp_fp_type_to_str(n: u64) -> &'static str {
54    match n {
55        1 => "SHA1",
56        2 => "SHA256",
57        _ => "SHA256",
58    }
59}
60
61pub fn sshfp_fp_type_to_num(ft: &crate::core::dns::records::SshfpFingerprintType) -> u8 {
62    use crate::core::dns::records::SshfpFingerprintType::*;
63    match ft {
64        Sha1 => 1,
65        Sha256 => 2,
66    }
67}
68
69// ─── TLSA ──────────────────────────────────────────────────────────────────
70
71pub fn tlsa_cert_usage_to_num(cu: &crate::core::dns::records::TlsaCertUsage) -> u8 {
72    use crate::core::dns::records::TlsaCertUsage::*;
73    match cu {
74        PkixTa => 0,
75        PkixEe => 1,
76        DaneTa => 2,
77        DaneEe => 3,
78    }
79}
80
81pub fn tlsa_cert_usage_to_str(n: u64) -> &'static str {
82    match n {
83        0 => "PKIX-TA",
84        1 => "PKIX-EE",
85        2 => "DANE-TA",
86        3 => "DANE-EE",
87        _ => "DANE-EE",
88    }
89}
90
91pub fn tlsa_selector_to_num(s: &crate::core::dns::records::TlsaSelector) -> u8 {
92    use crate::core::dns::records::TlsaSelector::*;
93    match s {
94        Cert => 0,
95        Spki => 1,
96    }
97}
98
99pub fn tlsa_selector_to_str(n: u64) -> &'static str {
100    match n {
101        0 => "Cert",
102        1 => "SPKI",
103        _ => "Cert",
104    }
105}
106
107pub fn tlsa_matching_type_to_num(mt: &crate::core::dns::records::TlsaMatchingType) -> u8 {
108    use crate::core::dns::records::TlsaMatchingType::*;
109    match mt {
110        Full => 0,
111        Sha2_256 => 1,
112        Sha2_512 => 2,
113    }
114}
115
116pub fn tlsa_matching_type_to_str(n: u64) -> &'static str {
117    match n {
118        0 => "Full",
119        1 => "SHA2-256",
120        2 => "SHA2-512",
121        _ => "Full",
122    }
123}
124
125// ─── DS ────────────────────────────────────────────────────────────────────
126
127pub fn ds_algorithm_to_num(alg: &crate::core::dns::records::DsAlgorithm) -> u8 {
128    use crate::core::dns::records::DsAlgorithm::*;
129    match alg {
130        Rsamd5 => 1,
131        Dsa => 3,
132        Rsasha1 => 5,
133        DsaNsec3Sha1 => 6,
134        Rsasha1Nsec3Sha1 => 7,
135        Rsasha256 => 8,
136        Rsasha512 => 10,
137        EccGost => 12,
138        Ecdsap256sha256 => 13,
139        Ecdsap384sha384 => 14,
140        Ed25519 => 15,
141        Ed448 => 16,
142    }
143}
144
145pub fn ds_algorithm_to_str(n: u64) -> &'static str {
146    match n {
147        1 => "RSAMD5",
148        3 => "DSA",
149        5 => "RSASHA1",
150        6 => "DSA-NSEC3-SHA1",
151        7 => "RSASHA1-NSEC3-SHA1",
152        8 => "RSASHA256",
153        10 => "RSASHA512",
154        12 => "ECC-GOST",
155        13 => "ECDSAP256SHA256",
156        14 => "ECDSAP384SHA384",
157        15 => "ED25519",
158        16 => "ED448",
159        _ => "RSASHA256",
160    }
161}
162
163pub fn ds_digest_type_to_num(dt: &crate::core::dns::records::DigestType) -> u8 {
164    use crate::core::dns::records::DigestType::*;
165    match dt {
166        Sha1 => 1,
167        Sha256 => 2,
168        GostR341194 => 3,
169        Sha384 => 4,
170    }
171}
172
173pub fn ds_digest_type_to_str(n: u64) -> &'static str {
174    match n {
175        1 => "SHA1",
176        2 => "SHA256",
177        3 => "GOST-R-34-11-94",
178        4 => "SHA384",
179        _ => "SHA256",
180    }
181}
182
183// ─── rData normalization (Cloudflare → internal) ───────────────────────────
184
185pub fn normalize_rdata(record_type: &str, content: &str, cf_record: &Value) -> Value {
186    match record_type {
187        "A" | "AAAA" => serde_json::json!({ "ipAddress": content }),
188        "CNAME" => serde_json::json!({ "cname": content }),
189        "DNAME" => serde_json::json!({ "dname": content }),
190        "MX" => {
191            let priority = cf_record
192                .get("priority")
193                .and_then(|p| p.as_u64())
194                .unwrap_or(10);
195            serde_json::json!({ "preference": priority, "exchange": content })
196        }
197        "TXT" => serde_json::json!({ "text": content, "splitText": false }),
198        "NS" => serde_json::json!({ "nameServer": content, "glue": null }),
199        "PTR" => serde_json::json!({ "ptrName": content }),
200        "SRV" => {
201            if let Some(data) = cf_record.get("data") {
202                let priority = data.get("priority").and_then(|p| p.as_u64()).unwrap_or(0);
203                let weight = data.get("weight").and_then(|w| w.as_u64()).unwrap_or(0);
204                let port = data.get("port").and_then(|p| p.as_u64()).unwrap_or(0);
205                let target = data.get("target").and_then(|t| t.as_str()).unwrap_or("");
206                serde_json::json!({
207                    "priority": priority,
208                    "weight": weight,
209                    "port": port,
210                    "target": target,
211                })
212            } else {
213                serde_json::json!({ "value": content })
214            }
215        }
216        "CAA" => {
217            if let Some(data) = cf_record.get("data") {
218                let flags = data.get("flags").and_then(|f| f.as_u64()).unwrap_or(0);
219                let tag = data.get("tag").and_then(|t| t.as_str()).unwrap_or("");
220                let value = data.get("value").and_then(|v| v.as_str()).unwrap_or("");
221                serde_json::json!({ "flags": flags, "tag": tag, "value": value })
222            } else {
223                serde_json::json!({ "value": content })
224            }
225        }
226        "SSHFP" => {
227            if let Some(data) = cf_record.get("data") {
228                let alg = data.get("algorithm").and_then(|a| a.as_u64()).unwrap_or(1);
229                let fp_type = data.get("type").and_then(|t| t.as_u64()).unwrap_or(2);
230                let fingerprint = data
231                    .get("fingerprint")
232                    .and_then(|f| f.as_str())
233                    .unwrap_or("");
234                serde_json::json!({
235                    "sshfpAlgorithm": sshfp_algorithm_to_str(alg),
236                    "sshfpFingerprintType": sshfp_fp_type_to_str(fp_type),
237                    "sshfpFingerprint": fingerprint,
238                })
239            } else {
240                serde_json::json!({ "value": content })
241            }
242        }
243        "TLSA" => {
244            if let Some(data) = cf_record.get("data") {
245                let usage = data.get("usage").and_then(|u| u.as_u64()).unwrap_or(3);
246                let selector = data.get("selector").and_then(|s| s.as_u64()).unwrap_or(1);
247                let matching_type = data
248                    .get("matching_type")
249                    .and_then(|m| m.as_u64())
250                    .unwrap_or(1);
251                let certificate = data
252                    .get("certificate")
253                    .and_then(|c| c.as_str())
254                    .unwrap_or("");
255                serde_json::json!({
256                    "tlsaCertificateUsage": tlsa_cert_usage_to_str(usage),
257                    "tlsaSelector": tlsa_selector_to_str(selector),
258                    "tlsaMatchingType": tlsa_matching_type_to_str(matching_type),
259                    "tlsaCertificateAssociationData": certificate,
260                })
261            } else {
262                serde_json::json!({ "value": content })
263            }
264        }
265        "DS" => {
266            if let Some(data) = cf_record.get("data") {
267                let key_tag = data.get("key_tag").and_then(|k| k.as_u64()).unwrap_or(0);
268                let algorithm = data.get("algorithm").and_then(|a| a.as_u64()).unwrap_or(13);
269                let digest_type = data
270                    .get("digest_type")
271                    .and_then(|d| d.as_u64())
272                    .unwrap_or(2);
273                let digest = data.get("digest").and_then(|d| d.as_str()).unwrap_or("");
274                serde_json::json!({
275                    "keyTag": key_tag,
276                    "algorithm": ds_algorithm_to_str(algorithm),
277                    "digestType": ds_digest_type_to_str(digest_type),
278                    "digest": digest,
279                })
280            } else {
281                serde_json::json!({ "value": content })
282            }
283        }
284        "HTTPS" | "SVCB" => {
285            if let Some(data) = cf_record.get("data") {
286                let priority = data.get("priority").and_then(|p| p.as_u64()).unwrap_or(1);
287                let target = data.get("target").and_then(|t| t.as_str()).unwrap_or(".");
288                let params = data.get("value").and_then(|v| v.as_str());
289                serde_json::json!({
290                    "svcPriority": priority,
291                    "svcTargetName": target,
292                    "svcParams": params,
293                    "autoIpv4Hint": false,
294                    "autoIpv6Hint": false,
295                })
296            } else {
297                serde_json::json!({ "value": content })
298            }
299        }
300        "NAPTR" => {
301            if let Some(data) = cf_record.get("data") {
302                let order = data.get("order").and_then(|o| o.as_u64()).unwrap_or(100);
303                let preference = data
304                    .get("preference")
305                    .and_then(|p| p.as_u64())
306                    .unwrap_or(10);
307                let flags = data.get("flags").and_then(|f| f.as_str()).unwrap_or("");
308                let services = data.get("service").and_then(|s| s.as_str()).unwrap_or("");
309                let regexp = data.get("regexp").and_then(|r| r.as_str()).unwrap_or("");
310                let replacement = data
311                    .get("replacement")
312                    .and_then(|r| r.as_str())
313                    .unwrap_or(".");
314                serde_json::json!({
315                    "naptrOrder": order,
316                    "naptrPreference": preference,
317                    "naptrFlags": flags,
318                    "naptrServices": services,
319                    "naptrRegexp": regexp,
320                    "naptrReplacement": replacement,
321                })
322            } else {
323                serde_json::json!({ "value": content })
324            }
325        }
326        "URI" => {
327            if let Some(data) = cf_record.get("data") {
328                let priority = data.get("priority").and_then(|p| p.as_u64()).unwrap_or(10);
329                let weight = data.get("weight").and_then(|w| w.as_u64()).unwrap_or(1);
330                let uri = data.get("content").and_then(|c| c.as_str()).unwrap_or("");
331                serde_json::json!({
332                    "uriPriority": priority,
333                    "uriWeight": weight,
334                    "uri": uri,
335                })
336            } else {
337                serde_json::json!({ "value": content })
338            }
339        }
340        _ => serde_json::json!({ "value": content }),
341    }
342}
343
344// ─── Record conversion ─────────────────────────────────────────────────────
345
346pub fn cloudflare_record_to_zone_record(cf: &Value, zone_name: &str) -> ZoneRecord {
347    let record_type = cf
348        .get("type")
349        .and_then(|t| t.as_str())
350        .unwrap_or("UNKNOWN")
351        .to_uppercase();
352    let cf_name = cf.get("name").and_then(|n| n.as_str()).unwrap_or("");
353    let name = extract_relative_name(cf_name, zone_name);
354    let ttl = cf.get("ttl").and_then(|t| t.as_u64()).unwrap_or(0) as u32;
355    let content = cf.get("content").and_then(|c| c.as_str()).unwrap_or("");
356    let proxied = cf.get("proxied").and_then(|p| p.as_bool()).unwrap_or(false);
357    let comment = cf
358        .get("comment")
359        .and_then(|c| c.as_str())
360        .unwrap_or("")
361        .to_string();
362    let cf_id = cf
363        .get("id")
364        .and_then(|i| i.as_str())
365        .unwrap_or("")
366        .to_string();
367
368    let mut data = normalize_rdata(&record_type, content, cf);
369    if let Some(obj) = data.as_object_mut() {
370        obj.insert("proxied".into(), Value::Bool(proxied));
371        if !cf_id.is_empty() {
372            obj.insert("id".into(), Value::String(cf_id));
373        }
374    }
375
376    ZoneRecord {
377        name,
378        record_type,
379        ttl,
380        disabled: false,
381        comments: comment,
382        expiry_ttl: 0,
383        data,
384        parsed: None,
385    }
386}
387
388pub fn record_data_to_cloudflare_body(name: &str, ttl: u32, record: &RecordData) -> Value {
389    let record_type = record.type_name();
390    match record {
391        RecordData::A { ip } => serde_json::json!({
392            "name": name, "type": record_type,
393            "content": ip.to_string(), "ttl": ttl, "proxied": false,
394        }),
395        RecordData::Aaaa { ip } => serde_json::json!({
396            "name": name, "type": record_type,
397            "content": ip.to_string(), "ttl": ttl, "proxied": false,
398        }),
399        RecordData::Cname { target } => serde_json::json!({
400            "name": name, "type": record_type,
401            "content": target, "ttl": ttl, "proxied": false,
402        }),
403        RecordData::Mx {
404            preference,
405            exchange,
406        } => serde_json::json!({
407            "name": name, "type": record_type,
408            "content": exchange, "priority": preference, "ttl": ttl,
409        }),
410        RecordData::Txt { text, .. } => serde_json::json!({
411            "name": name, "type": record_type,
412            "content": text, "ttl": ttl,
413        }),
414        RecordData::Ns { nameserver, .. } => serde_json::json!({
415            "name": name, "type": record_type,
416            "content": nameserver, "ttl": ttl,
417        }),
418        RecordData::Ptr { name: ptr_name } => serde_json::json!({
419            "name": name, "type": record_type,
420            "content": ptr_name, "ttl": ttl,
421        }),
422        RecordData::Srv {
423            priority,
424            weight,
425            port,
426            target,
427        } => serde_json::json!({
428            "name": name, "type": record_type,
429            "data": { "priority": priority, "weight": weight, "port": port, "target": target },
430            "ttl": ttl,
431        }),
432        RecordData::Caa { flags, tag, value } => serde_json::json!({
433            "name": name, "type": record_type,
434            "data": { "flags": flags, "tag": tag, "value": value },
435            "ttl": ttl,
436        }),
437        RecordData::Dname { dname } => serde_json::json!({
438            "name": name, "type": record_type,
439            "content": dname, "ttl": ttl,
440        }),
441        RecordData::Sshfp {
442            algorithm,
443            fingerprint_type,
444            fingerprint,
445        } => serde_json::json!({
446            "name": name, "type": record_type,
447            "data": {
448                "algorithm": sshfp_algorithm_to_num(algorithm),
449                "type": sshfp_fp_type_to_num(fingerprint_type),
450                "fingerprint": fingerprint,
451            },
452            "ttl": ttl,
453        }),
454        RecordData::Tlsa {
455            cert_usage,
456            selector,
457            matching_type,
458            cert_association_data,
459        } => serde_json::json!({
460            "name": name, "type": record_type,
461            "data": {
462                "usage": tlsa_cert_usage_to_num(cert_usage),
463                "selector": tlsa_selector_to_num(selector),
464                "matching_type": tlsa_matching_type_to_num(matching_type),
465                "certificate": cert_association_data,
466            },
467            "ttl": ttl,
468        }),
469        RecordData::Ds {
470            key_tag,
471            algorithm,
472            digest_type,
473            digest,
474        } => serde_json::json!({
475            "name": name, "type": record_type,
476            "data": {
477                "key_tag": key_tag,
478                "algorithm": ds_algorithm_to_num(algorithm),
479                "digest_type": ds_digest_type_to_num(digest_type),
480                "digest": digest,
481            },
482            "ttl": ttl,
483        }),
484        RecordData::Https {
485            svc_priority,
486            svc_target_name,
487            svc_params,
488            ..
489        }
490        | RecordData::Svcb {
491            svc_priority,
492            svc_target_name,
493            svc_params,
494            ..
495        } => serde_json::json!({
496            "name": name, "type": record_type,
497            "data": {
498                "priority": svc_priority,
499                "target": svc_target_name,
500                "value": svc_params,
501            },
502            "ttl": ttl,
503        }),
504        RecordData::Naptr {
505            order,
506            preference,
507            flags,
508            services,
509            regexp,
510            replacement,
511        } => serde_json::json!({
512            "name": name, "type": record_type,
513            "data": {
514                "order": order,
515                "preference": preference,
516                "flags": flags,
517                "service": services,
518                "regexp": regexp,
519                "replacement": replacement,
520            },
521            "ttl": ttl,
522        }),
523        RecordData::Uri {
524            priority,
525            weight,
526            uri,
527        } => serde_json::json!({
528            "name": name, "type": record_type,
529            "data": { "priority": priority, "weight": weight, "content": uri },
530            "ttl": ttl,
531        }),
532        _ => {
533            let params = record.to_api_params();
534            let content = params
535                .iter()
536                .find(|(k, _)| *k != "type")
537                .map(|(_, v)| v.clone())
538                .unwrap_or_default();
539            serde_json::json!({
540                "name": name, "type": record_type,
541                "content": content, "ttl": ttl,
542            })
543        }
544    }
545}