1use crate::{
13 CAARecord, DnsRecord, DnsRecordType, Error, IntoFqdn, KeyValue as DnsKeyValue, MXRecord,
14 SRVRecord, TLSARecord, TlsaCertUsage, TlsaMatching, TlsaSelector,
15 http::{HttpClient, HttpClientBuilder},
16};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::{borrow::Cow, time::Duration};
20
21const DEFAULT_API_ENDPOINT: &str = "https://api.gcore.com/dns";
22
23#[derive(Clone)]
24pub struct GcoreProvider {
25 client: HttpClient,
26 endpoint: Cow<'static, str>,
27}
28
29#[derive(Deserialize, Debug)]
30struct Zone {
31 name: String,
32}
33
34#[derive(Serialize, Debug)]
35struct RrSet {
36 ttl: u32,
37 resource_records: Vec<ResourceRecord>,
38}
39
40#[derive(Serialize, Debug)]
41struct ResourceRecord {
42 content: Vec<Value>,
43}
44
45#[derive(Deserialize, Debug)]
46struct RrSetResponse {
47 #[serde(default)]
48 ttl: u32,
49 #[serde(default)]
50 resource_records: Vec<ResourceRecordResponse>,
51}
52
53#[derive(Deserialize, Debug)]
54struct ResourceRecordResponse {
55 content: Vec<Value>,
56}
57
58impl GcoreProvider {
59 pub(crate) fn new(api_token: impl AsRef<str>, timeout: Option<Duration>) -> Self {
60 let client = HttpClientBuilder::default()
61 .with_header("Authorization", format!("APIKey {}", api_token.as_ref()))
62 .with_timeout(timeout)
63 .build();
64 Self {
65 client,
66 endpoint: Cow::Borrowed(DEFAULT_API_ENDPOINT),
67 }
68 }
69
70 #[cfg(test)]
71 pub(crate) fn with_endpoint(self, endpoint: impl Into<Cow<'static, str>>) -> Self {
72 Self {
73 endpoint: endpoint.into(),
74 ..self
75 }
76 }
77
78 pub(crate) async fn set_rrset(
79 &self,
80 name: impl IntoFqdn<'_>,
81 record_type: DnsRecordType,
82 ttl: u32,
83 records: Vec<DnsRecord>,
84 origin: impl IntoFqdn<'_>,
85 ) -> crate::Result<()> {
86 check_record_types(record_type, &records)?;
87 let zone = self.obtain_zone(&origin.into_name()).await?;
88 let fqdn = name.into_name();
89 let url = self.rrset_url(&zone, &fqdn, record_type);
90
91 if records.is_empty() {
92 return self
93 .client
94 .delete(url)
95 .send_raw()
96 .await
97 .map(|_| ())
98 .or_else(|err| match err {
99 Error::NotFound => Ok(()),
100 err => Err(err),
101 });
102 }
103
104 let body = build_rrset_from_many(records, ttl)?;
105 self.put_or_post(url, body).await
106 }
107
108 pub(crate) async fn add_to_rrset(
109 &self,
110 name: impl IntoFqdn<'_>,
111 record_type: DnsRecordType,
112 ttl: u32,
113 records: Vec<DnsRecord>,
114 origin: impl IntoFqdn<'_>,
115 ) -> crate::Result<()> {
116 if records.is_empty() {
117 return Ok(());
118 }
119 check_record_types(record_type, &records)?;
120 let zone = self.obtain_zone(&origin.into_name()).await?;
121 let fqdn = name.into_name();
122 let url = self.rrset_url(&zone, &fqdn, record_type);
123
124 let to_add: Vec<Vec<Value>> = records.into_iter().map(record_to_content).collect();
125
126 let (mut current, existed, effective_ttl) = match self.fetch_rrset(&url).await? {
127 Some(existing) => {
128 let existing_ttl = existing.ttl;
129 let contents = existing
130 .resource_records
131 .into_iter()
132 .map(|r| r.content)
133 .collect::<Vec<_>>();
134 (contents, true, existing_ttl)
135 }
136 None => (Vec::new(), false, ttl),
137 };
138
139 let before = current.len();
140 for content in to_add {
141 if !current.iter().any(|c| contents_equal(c, &content)) {
142 current.push(content);
143 }
144 }
145
146 if existed && current.len() == before {
147 return Ok(());
148 }
149
150 let body = RrSet {
151 ttl: effective_ttl,
152 resource_records: current
153 .into_iter()
154 .map(|content| ResourceRecord { content })
155 .collect(),
156 };
157 self.put_or_post(url, body).await
158 }
159
160 pub(crate) async fn remove_from_rrset(
161 &self,
162 name: impl IntoFqdn<'_>,
163 record_type: DnsRecordType,
164 records: Vec<DnsRecord>,
165 origin: impl IntoFqdn<'_>,
166 ) -> crate::Result<()> {
167 if records.is_empty() {
168 return Ok(());
169 }
170 check_record_types(record_type, &records)?;
171 let zone = self.obtain_zone(&origin.into_name()).await?;
172 let fqdn = name.into_name();
173 let url = self.rrset_url(&zone, &fqdn, record_type);
174
175 let existing = match self.fetch_rrset(&url).await? {
176 Some(existing) => existing,
177 None => return Ok(()),
178 };
179
180 let to_remove: Vec<Vec<Value>> = records.into_iter().map(record_to_content).collect();
181 let original_len = existing.resource_records.len();
182 let filtered: Vec<Vec<Value>> = existing
183 .resource_records
184 .into_iter()
185 .map(|r| r.content)
186 .filter(|content| !to_remove.iter().any(|r| contents_equal(r, content)))
187 .collect();
188
189 if filtered.len() == original_len {
190 return Ok(());
191 }
192
193 if filtered.is_empty() {
194 return self
195 .client
196 .delete(url)
197 .send_raw()
198 .await
199 .map(|_| ())
200 .or_else(|err| match err {
201 Error::NotFound => Ok(()),
202 err => Err(err),
203 });
204 }
205
206 let body = RrSet {
207 ttl: existing.ttl,
208 resource_records: filtered
209 .into_iter()
210 .map(|content| ResourceRecord { content })
211 .collect(),
212 };
213 self.client
214 .put(url)
215 .with_body(body)?
216 .send_raw()
217 .await
218 .map(|_| ())
219 }
220
221 pub(crate) async fn list_rrset(
222 &self,
223 name: impl IntoFqdn<'_>,
224 record_type: DnsRecordType,
225 origin: impl IntoFqdn<'_>,
226 ) -> crate::Result<Vec<DnsRecord>> {
227 let zone = self.obtain_zone(&origin.into_name()).await?;
228 let fqdn = name.into_name();
229 let url = self.rrset_url(&zone, &fqdn, record_type);
230
231 let existing = match self.fetch_rrset(&url).await? {
232 Some(existing) => existing,
233 None => return Ok(Vec::new()),
234 };
235
236 existing
237 .resource_records
238 .into_iter()
239 .map(|r| parse_content(record_type, &r.content))
240 .collect()
241 }
242
243 fn rrset_url(&self, zone: &str, fqdn: &str, record_type: DnsRecordType) -> String {
244 format!(
245 "{}/v2/zones/{}/{}/{}",
246 self.endpoint,
247 zone,
248 fqdn,
249 record_type.as_str()
250 )
251 }
252
253 async fn fetch_rrset(&self, url: &str) -> crate::Result<Option<RrSetResponse>> {
254 match self
255 .client
256 .get(url.to_string())
257 .send_with_retry::<RrSetResponse>(3)
258 .await
259 {
260 Ok(rrset) => Ok(Some(rrset)),
261 Err(Error::NotFound) => Ok(None),
262 Err(err) => Err(err),
263 }
264 }
265
266 async fn put_or_post(&self, url: String, body: RrSet) -> crate::Result<()> {
267 match self
268 .client
269 .put(url.clone())
270 .with_body(&body)?
271 .send_raw()
272 .await
273 {
274 Ok(_) => Ok(()),
275 Err(Error::NotFound) => self
276 .client
277 .post(url)
278 .with_body(body)?
279 .send_raw()
280 .await
281 .map(|_| ()),
282 Err(err) => Err(err),
283 }
284 }
285
286 async fn obtain_zone(&self, origin: &str) -> crate::Result<String> {
287 let mut candidate: &str = origin;
288 loop {
289 let result = self
290 .client
291 .get(format!("{}/v2/zones/{}", self.endpoint, candidate))
292 .send_with_retry::<Zone>(3)
293 .await;
294 match result {
295 Ok(zone) => return Ok(zone.name),
296 Err(Error::NotFound) => {}
297 Err(err) => return Err(err),
298 }
299 match candidate.split_once('.') {
300 Some((_, rest)) if rest.contains('.') => candidate = rest,
301 _ => {
302 return Err(Error::Api(format!("No Gcore zone found for {origin}")));
303 }
304 }
305 }
306 }
307}
308
309fn check_record_types(expected: DnsRecordType, records: &[DnsRecord]) -> crate::Result<()> {
310 for record in records {
311 if record.as_type() != expected {
312 return Err(Error::Api(format!(
313 "RRSet record type mismatch: expected {}, got {}",
314 expected.as_str(),
315 record.as_type().as_str(),
316 )));
317 }
318 }
319 Ok(())
320}
321
322fn build_rrset_from_many(records: Vec<DnsRecord>, ttl: u32) -> crate::Result<RrSet> {
323 Ok(RrSet {
324 ttl,
325 resource_records: records
326 .into_iter()
327 .map(|r| ResourceRecord {
328 content: record_to_content(r),
329 })
330 .collect(),
331 })
332}
333
334fn record_to_content(record: DnsRecord) -> Vec<Value> {
335 match record {
336 DnsRecord::A(addr) => vec![Value::String(addr.to_string())],
337 DnsRecord::AAAA(addr) => vec![Value::String(addr.to_string())],
338 DnsRecord::CNAME(content) => vec![Value::String(content)],
339 DnsRecord::NS(content) => vec![Value::String(content)],
340 DnsRecord::MX(mx) => vec![
341 Value::Number(mx.priority.into()),
342 Value::String(mx.exchange),
343 ],
344 DnsRecord::TXT(content) => vec![Value::String(content)],
345 DnsRecord::SRV(srv) => vec![
346 Value::Number(srv.priority.into()),
347 Value::Number(srv.weight.into()),
348 Value::Number(srv.port.into()),
349 Value::String(srv.target),
350 ],
351 DnsRecord::TLSA(tlsa) => vec![Value::String(tlsa.to_string())],
352 DnsRecord::CAA(caa) => {
353 let (flags, tag, value) = caa.decompose();
354 vec![
355 Value::Number(flags.into()),
356 Value::String(tag),
357 Value::String(value),
358 ]
359 }
360 }
361}
362
363fn contents_equal(a: &[Value], b: &[Value]) -> bool {
364 if a.len() != b.len() {
365 return false;
366 }
367 a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))
368}
369
370fn values_equal(a: &Value, b: &Value) -> bool {
371 match (a, b) {
372 (Value::String(x), Value::String(y)) => x == y,
373 (Value::Number(x), Value::Number(y)) => match (x.as_i64(), y.as_i64()) {
374 (Some(xi), Some(yi)) => xi == yi,
375 _ => match (x.as_u64(), y.as_u64()) {
376 (Some(xu), Some(yu)) => xu == yu,
377 _ => match (x.as_f64(), y.as_f64()) {
378 (Some(xf), Some(yf)) => xf == yf,
379 _ => false,
380 },
381 },
382 },
383 (Value::Bool(x), Value::Bool(y)) => x == y,
384 (Value::Null, Value::Null) => true,
385 _ => a == b,
386 }
387}
388
389fn parse_content(record_type: DnsRecordType, content: &[Value]) -> crate::Result<DnsRecord> {
390 match record_type {
391 DnsRecordType::A => {
392 let s = expect_string(content, 0, "A")?;
393 s.parse()
394 .map(DnsRecord::A)
395 .map_err(|e| Error::Parse(format!("invalid A record: {e}")))
396 }
397 DnsRecordType::AAAA => {
398 let s = expect_string(content, 0, "AAAA")?;
399 s.parse()
400 .map(DnsRecord::AAAA)
401 .map_err(|e| Error::Parse(format!("invalid AAAA record: {e}")))
402 }
403 DnsRecordType::CNAME => {
404 let s = expect_string(content, 0, "CNAME")?;
405 Ok(DnsRecord::CNAME(strip_trailing_dot(s)))
406 }
407 DnsRecordType::NS => {
408 let s = expect_string(content, 0, "NS")?;
409 Ok(DnsRecord::NS(strip_trailing_dot(s)))
410 }
411 DnsRecordType::MX => {
412 if content.len() < 2 {
413 return Err(Error::Parse(format!(
414 "invalid MX content array length: {}",
415 content.len()
416 )));
417 }
418 let priority = expect_u16(content, 0, "MX")?;
419 let exchange = expect_string(content, 1, "MX")?;
420 Ok(DnsRecord::MX(MXRecord {
421 priority,
422 exchange: strip_trailing_dot(exchange),
423 }))
424 }
425 DnsRecordType::TXT => {
426 let s = expect_string(content, 0, "TXT")?;
427 Ok(DnsRecord::TXT(s.to_string()))
428 }
429 DnsRecordType::SRV => {
430 if content.len() < 4 {
431 return Err(Error::Parse(format!(
432 "invalid SRV content array length: {}",
433 content.len()
434 )));
435 }
436 let priority = expect_u16(content, 0, "SRV")?;
437 let weight = expect_u16(content, 1, "SRV")?;
438 let port = expect_u16(content, 2, "SRV")?;
439 let target = expect_string(content, 3, "SRV")?;
440 Ok(DnsRecord::SRV(SRVRecord {
441 priority,
442 weight,
443 port,
444 target: strip_trailing_dot(target),
445 }))
446 }
447 DnsRecordType::TLSA => {
448 let s = expect_string(content, 0, "TLSA")?;
449 parse_tlsa_text(s)
450 }
451 DnsRecordType::CAA => {
452 if content.len() < 3 {
453 return Err(Error::Parse(format!(
454 "invalid CAA content array length: {}",
455 content.len()
456 )));
457 }
458 let flags = expect_u8(content, 0, "CAA")?;
459 let tag = expect_string(content, 1, "CAA")?.to_string();
460 let value = expect_string(content, 2, "CAA")?.to_string();
461 build_caa(flags, &tag, value)
462 }
463 }
464}
465
466fn expect_string<'a>(content: &'a [Value], idx: usize, rtype: &str) -> crate::Result<&'a str> {
467 match content.get(idx) {
468 Some(Value::String(s)) => Ok(s.as_str()),
469 Some(other) => Err(Error::Parse(format!(
470 "expected string at position {idx} for {rtype}, got: {other}"
471 ))),
472 None => Err(Error::Parse(format!(
473 "missing element at position {idx} for {rtype}"
474 ))),
475 }
476}
477
478fn expect_u16(content: &[Value], idx: usize, rtype: &str) -> crate::Result<u16> {
479 match content.get(idx) {
480 Some(Value::Number(n)) => n
481 .as_u64()
482 .and_then(|v| u16::try_from(v).ok())
483 .ok_or_else(|| Error::Parse(format!("invalid u16 at position {idx} for {rtype}: {n}"))),
484 Some(other) => Err(Error::Parse(format!(
485 "expected number at position {idx} for {rtype}, got: {other}"
486 ))),
487 None => Err(Error::Parse(format!(
488 "missing element at position {idx} for {rtype}"
489 ))),
490 }
491}
492
493fn expect_u8(content: &[Value], idx: usize, rtype: &str) -> crate::Result<u8> {
494 match content.get(idx) {
495 Some(Value::Number(n)) => n
496 .as_u64()
497 .and_then(|v| u8::try_from(v).ok())
498 .ok_or_else(|| Error::Parse(format!("invalid u8 at position {idx} for {rtype}: {n}"))),
499 Some(other) => Err(Error::Parse(format!(
500 "expected number at position {idx} for {rtype}, got: {other}"
501 ))),
502 None => Err(Error::Parse(format!(
503 "missing element at position {idx} for {rtype}"
504 ))),
505 }
506}
507
508fn strip_trailing_dot(s: &str) -> String {
509 s.strip_suffix('.').unwrap_or(s).to_string()
510}
511
512fn parse_tlsa_text(text: &str) -> crate::Result<DnsRecord> {
513 let mut parts = text.split_whitespace();
514 let usage: u8 = parts
515 .next()
516 .ok_or_else(|| Error::Parse(format!("invalid TLSA record: {text}")))?
517 .parse()
518 .map_err(|e| Error::Parse(format!("invalid TLSA usage: {e}")))?;
519 let selector: u8 = parts
520 .next()
521 .ok_or_else(|| Error::Parse(format!("invalid TLSA record: {text}")))?
522 .parse()
523 .map_err(|e| Error::Parse(format!("invalid TLSA selector: {e}")))?;
524 let matching: u8 = parts
525 .next()
526 .ok_or_else(|| Error::Parse(format!("invalid TLSA record: {text}")))?
527 .parse()
528 .map_err(|e| Error::Parse(format!("invalid TLSA matching: {e}")))?;
529 let hex: String = parts.collect::<Vec<_>>().join("");
530 Ok(DnsRecord::TLSA(TLSARecord {
531 cert_usage: tlsa_cert_usage_from_u8(usage)?,
532 selector: tlsa_selector_from_u8(selector)?,
533 matching: tlsa_matching_from_u8(matching)?,
534 cert_data: decode_hex(&hex)?,
535 }))
536}
537
538fn tlsa_cert_usage_from_u8(value: u8) -> crate::Result<TlsaCertUsage> {
539 Ok(match value {
540 0 => TlsaCertUsage::PkixTa,
541 1 => TlsaCertUsage::PkixEe,
542 2 => TlsaCertUsage::DaneTa,
543 3 => TlsaCertUsage::DaneEe,
544 255 => TlsaCertUsage::Private,
545 _ => return Err(Error::Parse(format!("unknown TLSA cert usage: {value}"))),
546 })
547}
548
549fn tlsa_selector_from_u8(value: u8) -> crate::Result<TlsaSelector> {
550 Ok(match value {
551 0 => TlsaSelector::Full,
552 1 => TlsaSelector::Spki,
553 255 => TlsaSelector::Private,
554 _ => return Err(Error::Parse(format!("unknown TLSA selector: {value}"))),
555 })
556}
557
558fn tlsa_matching_from_u8(value: u8) -> crate::Result<TlsaMatching> {
559 Ok(match value {
560 0 => TlsaMatching::Raw,
561 1 => TlsaMatching::Sha256,
562 2 => TlsaMatching::Sha512,
563 255 => TlsaMatching::Private,
564 _ => return Err(Error::Parse(format!("unknown TLSA matching: {value}"))),
565 })
566}
567
568fn decode_hex(hex: &str) -> crate::Result<Vec<u8>> {
569 if !hex.len().is_multiple_of(2) {
570 return Err(Error::Parse(format!("invalid hex string: {hex}")));
571 }
572 (0..hex.len())
573 .step_by(2)
574 .map(|i| {
575 u8::from_str_radix(&hex[i..i + 2], 16)
576 .map_err(|e| Error::Parse(format!("invalid hex byte: {e}")))
577 })
578 .collect()
579}
580
581fn build_caa(flags: u8, tag: &str, value: String) -> crate::Result<DnsRecord> {
582 let issuer_critical = flags & 0x80 != 0;
583 Ok(DnsRecord::CAA(match tag {
584 "issue" => {
585 let (name, options) = parse_caa_value(&value);
586 CAARecord::Issue {
587 issuer_critical,
588 name,
589 options,
590 }
591 }
592 "issuewild" => {
593 let (name, options) = parse_caa_value(&value);
594 CAARecord::IssueWild {
595 issuer_critical,
596 name,
597 options,
598 }
599 }
600 "iodef" => CAARecord::Iodef {
601 issuer_critical,
602 url: value,
603 },
604 other => return Err(Error::Parse(format!("unknown CAA tag: {other}"))),
605 }))
606}
607
608fn parse_caa_value(value: &str) -> (Option<String>, Vec<DnsKeyValue>) {
609 let mut parts = value.split(';').map(str::trim);
610 let name_part = parts.next().unwrap_or("").trim().to_string();
611 let name = if name_part.is_empty() {
612 None
613 } else {
614 Some(name_part)
615 };
616 let options = parts
617 .filter(|p| !p.is_empty())
618 .map(|p| match p.split_once('=') {
619 Some((k, v)) => DnsKeyValue {
620 key: k.trim().to_string(),
621 value: v.trim().to_string(),
622 },
623 None => DnsKeyValue {
624 key: p.trim().to_string(),
625 value: String::new(),
626 },
627 })
628 .collect();
629 (name, options)
630}