From ed2653ae7dba9c072dcca1aed03b7cda0d910c85 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 7 Dec 2022 15:35:12 +0100 Subject: First version of D53 that does something First working version --- src/provider/gandi.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/provider/mod.rs | 20 ++++++++++ 2 files changed, 122 insertions(+) create mode 100644 src/provider/gandi.rs create mode 100644 src/provider/mod.rs (limited to 'src/provider') diff --git a/src/provider/gandi.rs b/src/provider/gandi.rs new file mode 100644 index 0000000..1f4f51f --- /dev/null +++ b/src/provider/gandi.rs @@ -0,0 +1,102 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use log::{info, warn}; +use reqwest::header; +use serde::Serialize; + +use crate::provider::DnsProvider; +use crate::Opt; + +pub struct GandiProvider { + client: reqwest::Client, +} + +impl GandiProvider { + pub fn new(opts: &Opt) -> Result { + let api_key = opts + .gandi_api_key + .clone() + .ok_or_else(|| anyhow!("Must specify D53_GANDI_API_KEY"))?; + + let mut headers = header::HeaderMap::new(); + let mut auth_value = header::HeaderValue::from_str(&format!("Apikey {}", api_key))?; + auth_value.set_sensitive(true); + headers.insert(header::AUTHORIZATION, auth_value); + + let client = reqwest::Client::builder() + .default_headers(headers) + .use_rustls_tls() + .build()?; + + Ok(Self { client }) + } + + async fn put_rrset(&self, url: &str, rrset: &GandiRrset) -> Result<()> { + info!("PUT {} with {:?}", url, rrset); + let http = self.client.put(url).json(rrset).send().await?; + + if !http.status().is_success() { + warn!("PUT {} returned {}", url, http.status()); + } + + http.error_for_status()?; + Ok(()) + } +} + +#[async_trait] +impl DnsProvider for GandiProvider { + fn provider(&self) -> &'static str { + "gandi" + } + + async fn update_a(&self, domain: &str, subdomain: &str, targets: &[Ipv4Addr]) -> Result<()> { + let url = format!( + "https://api.gandi.net/v5/livedns/domains/{}/records/{}/A", + domain, subdomain + ); + + let rrset = GandiRrset { + rrset_values: targets.iter().map(ToString::to_string).collect::>(), + rrset_ttl: 300, + }; + + self.put_rrset(&url, &rrset).await + } + + async fn update_aaaa(&self, domain: &str, subdomain: &str, targets: &[Ipv6Addr]) -> Result<()> { + let url = format!( + "https://api.gandi.net/v5/livedns/domains/{}/records/{}/AAAA", + domain, subdomain + ); + + let rrset = GandiRrset { + rrset_values: targets.iter().map(ToString::to_string).collect::>(), + rrset_ttl: 300, + }; + + self.put_rrset(&url, &rrset).await + } + + async fn update_cname(&self, domain: &str, subdomain: &str, target: &str) -> Result<()> { + let url = format!( + "https://api.gandi.net/v5/livedns/domains/{}/records/{}/CNAME", + domain, subdomain + ); + + let rrset = GandiRrset { + rrset_values: vec![target.to_string()], + rrset_ttl: 300, + }; + + self.put_rrset(&url, &rrset).await + } +} + +#[derive(Serialize, Debug)] +struct GandiRrset { + rrset_values: Vec, + rrset_ttl: u32, +} diff --git a/src/provider/mod.rs b/src/provider/mod.rs new file mode 100644 index 0000000..6527631 --- /dev/null +++ b/src/provider/mod.rs @@ -0,0 +1,20 @@ +pub mod gandi; + +use std::net::{Ipv4Addr, Ipv6Addr}; + +use anyhow::Result; +use async_trait::async_trait; + +#[async_trait] +pub trait DnsProvider: Send + Sync { + fn provider(&self) -> &'static str; + async fn update_a(&self, domain: &str, subdomain: &str, targets: &[Ipv4Addr]) -> Result<()>; + async fn update_aaaa(&self, domain: &str, subdomain: &str, targets: &[Ipv6Addr]) -> Result<()>; + async fn update_cname(&self, domain: &str, subdomain: &str, target: &str) -> Result<()>; +} + +impl std::fmt::Debug for dyn DnsProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "DnsProvider({})", self.provider()) + } +} -- cgit v1.2.3