microtld/backend/src/cloudflare/mod.rs
CanadianBaconBoi f4b83d14ec initial
2026-08-19 07:24:40 +02:00

291 lines
10 KiB
Rust

use std::str::FromStr;
use std::time::Instant;
use lmrc_cloudflare::{CloudflareClient};
use crate::{ClientNotInitializedError, DnsProvider, DnsRecord, RecordType};
#[derive(Debug)]
pub struct CloudflareDnsProvider {
api_key: String,
zone_id: String,
client: Option<CloudflareClient>
}
impl CloudflareDnsProvider {
pub fn new(api_key: String, zone_id: String) -> anyhow::Result<Self> {
Ok(Self {
client: Some(CloudflareClient::new(&api_key)?), api_key, zone_id
})
}
fn cloudflare_records_to_generic(&self, records: Vec<lmrc_cloudflare::DnsRecord>) -> anyhow::Result<impl IntoIterator<Item=anyhow::Result<CloudflareDnsRecord<'_>>>> {
if records.len() == 0 {
Err(anyhow::anyhow!("DNS record not found"))
} else {
Ok(records.into_iter().map(|rec|
CloudflareDnsRecord::from_record(self, rec)
))
}
}
}
impl<'a> DnsProvider<'a> for CloudflareDnsProvider {
type Client = CloudflareClient;
type Record = CloudflareDnsRecord<'a>;
fn init_client(&mut self) -> anyhow::Result<()> {
if let None = self.client {
self.client = Some(CloudflareClient::new(&self.api_key)?);
}
Ok(())
}
fn get_client(&self) -> Result<&CloudflareClient, ClientNotInitializedError> {
self.client.as_ref().ok_or(ClientNotInitializedError {message: "Client not initialized".to_string()})
}
async fn get_dns_records_by_name(&'a self, name: impl Into<String>) -> anyhow::Result<impl IntoIterator<Item=anyhow::Result<Self::Record>>> {
let client = self.get_client()?;
let dns = client.dns();
let records = dns.list_records(&self.zone_id).name(name.into()).send().await?;
self.cloudflare_records_to_generic(records)
}
async fn get_dns_records_by_ip(&'a self, ip: impl Into<String>) -> anyhow::Result<impl IntoIterator<Item = anyhow::Result<Self::Record>>> {
let client = self.get_client()?;
let dns = client.dns();
let records = dns.list_records(&self.zone_id).content(ip.into()).send().await?;
self.cloudflare_records_to_generic(records)
}
async fn get_dns_records(&'a self) -> anyhow::Result<impl IntoIterator<Item = anyhow::Result<Self::Record>>> {
let client = self.get_client()?;
let dns = client.dns();
let records = dns.list_records(&self.zone_id).send().await?;
self.cloudflare_records_to_generic(records)
}
async fn get_dns_record_by_id(&'a self, id: &str) -> anyhow::Result<Self::Record> {
let client = self.get_client()?;
let dns = client.dns();
match dns.get_record(&self.zone_id, id).await {
Ok(record) => {
Ok(CloudflareDnsRecord::from_record(self, record)?)
}
Err(e) => {
Err(anyhow::anyhow!("Failed to get DNS record: {}", e))
}
}
}
async fn create_dns_record(&'a self, subdomain: &str, target: &str, record_type: RecordType, ttl: Option<u32>) -> anyhow::Result<Self::Record> {
let client = self.get_client()?;
let dns = client.dns();
match dns.create_record(&self.zone_id)
.name(subdomain)
.content(target)
.record_type(record_type.into())
.ttl(ttl.unwrap_or(1))
.send().await {
Ok(record) => {
Ok(CloudflareDnsRecord::from_record(self, record)?)
}
Err(e) => {
Err(anyhow::anyhow!("Failed to get DNS record: {}", e))
}
}
}
}
pub struct CloudflareDnsRecord<'a> {
provider: &'a CloudflareDnsProvider,
record_id: String,
name: String,
target: String,
record_type: RecordType,
ttl: u32,
priority: Option<u16>,
last_refresh: Instant
}
impl<'a> CloudflareDnsRecord<'a> {
fn from_record(provider: &'a CloudflareDnsProvider, record: lmrc_cloudflare::DnsRecord) -> anyhow::Result<Self>
{
Ok(
CloudflareDnsRecord {
provider,
record_id: record.id,
name: record.name,
target: record.content,
record_type: RecordType::from_str(&record.record_type)?,
ttl: record.ttl,
priority: record.priority,
last_refresh: Instant::now(),
}
)
}
}
impl<'a> DnsRecord<'a> for CloudflareDnsRecord<'a> {
type Client = CloudflareClient;
type Provider = CloudflareDnsProvider;
fn get_provider(&self) -> &Self::Provider {
self.provider
}
async fn name(&mut self) -> anyhow::Result<String> {
if self.last_refresh.elapsed().as_secs() > 300 {
self.refresh().await?;
}
Ok(self.name.clone())
}
async fn target(&mut self) -> anyhow::Result<String> {
if self.last_refresh.elapsed().as_secs() > 300 {
self.refresh().await?;
}
Ok(self.target.clone())
}
async fn ttl(&mut self) -> anyhow::Result<u32> {
if self.last_refresh.elapsed().as_secs() > 300 {
self.refresh().await?;
}
Ok(self.ttl)
}
async fn record_type(&mut self) -> anyhow::Result<RecordType> {
if self.last_refresh.elapsed().as_secs() > 300 {
self.refresh().await?;
}
Ok(self.record_type)
}
async fn is_valid(&self) -> anyhow::Result<bool> {
if self.provider.get_client()?.dns().get_record(&self.provider.zone_id, &self.record_id).await.is_err() {
Ok(false)
} else {
Ok(true)
}
}
async fn priority(&mut self) -> anyhow::Result<Option<u16>> {
if self.last_refresh.elapsed().as_secs() > 300 {
self.refresh().await?;
}
Ok(self.priority)
}
async fn set_target(&mut self, ip: &str) -> anyhow::Result<()> {
self.refresh().await?;
self.provider.get_client()?.dns().update_record(
&self.provider.zone_id,
&self.record_id
).content(ip).send().await?;
self.refresh().await?;
Ok(())
}
async fn set_ttl(&mut self, ttl: u32) -> anyhow::Result<()> {
self.refresh().await?;
self.provider.get_client()?.dns().update_record(
&self.provider.zone_id,
&self.record_id
).ttl(ttl).send().await?;
self.refresh().await?;
Ok(())
}
async fn set_record_type(&mut self, record_type: RecordType) -> anyhow::Result<()> {
self.refresh().await?;
self.provider.get_client()?.dns().update_record(
&self.provider.zone_id,
&self.record_id
).record_type(record_type.into()).send().await?;
self.refresh().await?;
Ok(())
}
async fn set_priority(&mut self, priority: u16) -> anyhow::Result<()> {
self.refresh().await?;
self.provider.get_client()?.dns().update_record(
&self.provider.zone_id,
&self.record_id
).priority(priority).send().await?;
self.refresh().await?;
Ok(())
}
async fn delete(&mut self) -> anyhow::Result<()> {
self.refresh().await?;
self.provider.get_client()?.dns().delete_record(&self.provider.zone_id, &self.record_id).await?;
Ok(())
}
async fn refresh(&mut self) -> anyhow::Result<()> {
let client = self.provider.get_client()?;
let dns = client.dns();
let record = match dns.get_record(&self.provider.zone_id, &self.record_id).await {
Ok(record) => {Ok(record)}
Err(_) => {
match dns.find_record(&self.provider.zone_id, &self.name, self.record_type.into()).await? {
Some(record) => Ok(record),
None => {
match dns.list_records(&self.provider.zone_id).name(&self.name).send().await {
Ok(records) => {
let mut record = None;
for r in records {
if r.name == self.name && r.content == self.target {
record = Some(r);
}
}
if let Some(record) = record {
Ok(record)
} else {
Err(anyhow::anyhow!("DNS record not found"))
}
}
Err(_) => Err(anyhow::anyhow!("Failed to list DNS records"))
}
}
}
}
}?;
self.record_id = record.id;
self.name = record.name;
self.target = record.content;
self.priority = record.priority;
self.ttl = record.ttl;
self.record_type = RecordType::from_str(&record.record_type)?;
self.last_refresh = Instant::now();
Ok(())
}
}
impl Into<lmrc_cloudflare::RecordType> for RecordType {
fn into(self) -> lmrc_cloudflare::RecordType {
match self {
RecordType::A => lmrc_cloudflare::RecordType::A,
RecordType::AAAA => lmrc_cloudflare::RecordType::AAAA,
RecordType::CNAME => lmrc_cloudflare::RecordType::CNAME,
RecordType::MX => lmrc_cloudflare::RecordType::MX,
RecordType::TXT => lmrc_cloudflare::RecordType::TXT,
RecordType::SRV => lmrc_cloudflare::RecordType::SRV,
RecordType::NS => lmrc_cloudflare::RecordType::NS,
RecordType::CAA => lmrc_cloudflare::RecordType::CAA,
RecordType::PTR => lmrc_cloudflare::RecordType::PTR,
RecordType::DNSKEY => lmrc_cloudflare::RecordType::DNSKEY,
RecordType::DS => lmrc_cloudflare::RecordType::DS,
RecordType::HTTPS => lmrc_cloudflare::RecordType::HTTPS,
RecordType::LOC => lmrc_cloudflare::RecordType::LOC,
RecordType::NAPTR => lmrc_cloudflare::RecordType::NAPTR,
RecordType::SMIMEA => lmrc_cloudflare::RecordType::SMIMEA,
RecordType::SSHFP => lmrc_cloudflare::RecordType::SSHFP,
RecordType::SVCB => lmrc_cloudflare::RecordType::SVCB,
RecordType::TLSA => lmrc_cloudflare::RecordType::TLSA,
RecordType::URI => lmrc_cloudflare::RecordType::URI
}
}
}