use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::challenge::DnsProvider;
use crate::error::{AcmeError, Result};
#[derive(Debug, Clone)]
pub struct CloudFlareConfig {
pub api_token: String,
pub zone_id: String,
}
pub struct CloudFlareDnsProvider {
config: CloudFlareConfig,
http_client: reqwest::Client,
}
impl CloudFlareDnsProvider {
pub fn new(config: CloudFlareConfig) -> Self {
tracing::debug!(
"Initializing CloudFlareDnsProvider for Zone: {}",
config.zone_id
);
Self {
config,
http_client: reqwest::Client::new(),
}
}
}
#[derive(Debug, Serialize)]
struct CloudFlareRecordCreateRequest<'a> {
r#type: &'a str,
name: &'a str,
content: &'a str,
ttl: u32,
}
#[derive(Debug, Deserialize)]
struct CloudFlareRecordResponse {
result: CloudFlareRecordResult,
}
#[derive(Debug, Deserialize)]
struct CloudFlareRecordResult {
id: String,
}
#[async_trait]
impl DnsProvider for CloudFlareDnsProvider {
async fn create_txt_record(&self, domain: &str, value: &str) -> Result<String> {
tracing::info!("Creating CloudFlare TXT record for domain: {}", domain);
let url = format!(
"https://api.cloudflare.com/client/v4/zones/{}/dns_records",
self.config.zone_id
);
let payload = CloudFlareRecordCreateRequest {
r#type: "TXT",
name: domain,
content: value,
ttl: 60, };
let response = self
.http_client
.post(url)
.bearer_auth(&self.config.api_token)
.json(&payload)
.send()
.await
.map_err(|e| {
tracing::error!("Network error while creating CloudFlare record: {}", e);
AcmeError::transport(format!("CloudFlare create record failed: {}", e))
})?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
tracing::error!("CloudFlare API error ({}): {}", status, text);
return Err(AcmeError::storage(format!(
"CloudFlare create record failed: {}",
text
)));
}
let body: CloudFlareRecordResponse = response.json().await.map_err(|e| {
tracing::error!("Failed to parse CloudFlare response: {}", e);
AcmeError::storage(format!("CloudFlare parse response failed: {}", e))
})?;
tracing::info!(
"Successfully created CloudFlare TXT record with ID: {}",
body.result.id
);
Ok(body.result.id)
}
async fn delete_txt_record(&self, _domain: &str, record_id: &str) -> Result<()> {
tracing::info!("Deleting CloudFlare TXT record ID: {}", record_id);
let url = format!(
"https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}",
self.config.zone_id, record_id
);
let response = self
.http_client
.delete(url)
.bearer_auth(&self.config.api_token)
.send()
.await
.map_err(|e| {
tracing::error!("Network error while deleting CloudFlare record: {}", e);
AcmeError::transport(format!("CloudFlare delete record failed: {}", e))
})?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
tracing::error!(
"CloudFlare API error during deletion ({}): {}",
status,
text
);
return Err(AcmeError::storage(format!(
"CloudFlare delete record failed: {}",
text
)));
}
tracing::info!("Successfully deleted CloudFlare TXT record: {}", record_id);
Ok(())
}
async fn verify_record(&self, domain: &str, value: &str) -> Result<bool> {
tracing::debug!("Verifying CloudFlare TXT record for domain: {}", domain);
let url = format!(
"https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=TXT&name={}",
self.config.zone_id, domain
);
let response = self
.http_client
.get(url)
.bearer_auth(&self.config.api_token)
.send()
.await
.map_err(|e| {
tracing::error!("Network error while verifying CloudFlare record: {}", e);
AcmeError::transport(format!("CloudFlare verify record failed: {}", e))
})?;
if !response.status().is_success() {
tracing::warn!(
"CloudFlare record verification returned status: {}",
response.status()
);
return Ok(false);
}
let text = response.text().await.unwrap_or_default();
let verified = text.contains(value);
if verified {
tracing::debug!("CloudFlare record verification successful");
} else {
tracing::warn!("CloudFlare record verification failed: value not found in response");
}
Ok(verified)
}
}