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