use super::http::{AuthStrategy, HttpError, RequestFactory, ResponseBytes};
use super::models::{
CloudflareDnsRecord, CloudflareDnsRecordBatch, CloudflareDnsRecordWrite, CloudflareDnsSettings,
CloudflareDnssec, CloudflareDnssecEdit, CloudflareEnvelope, CloudflareZone,
CloudflareZoneFilters, DomainAvailability, PaginationInfo, RegisteredDomain, SecretRecord,
SecretsQuota, SecretsStore,
};
use reqwest::header::{HeaderName, HeaderValue};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
const CLOUDFLARE_API_BASE: &str = "https://api.cloudflare.com/client/v4";
#[derive(Debug, Clone)]
pub struct CloudflareClient {
account_id: String,
http: RequestFactory,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudflareStoreCreateRequest {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudflareSecretCreateRequest {
pub name: String,
pub value: String,
#[serde(default)]
pub scopes: Vec<String>,
#[serde(default)]
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CloudflareSecretEditRequest {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub value: Option<String>,
#[serde(default)]
pub scopes: Option<Vec<String>>,
#[serde(default)]
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudflareSecretDuplicateRequest {
pub name: String,
#[serde(default)]
pub scopes: Vec<String>,
#[serde(default)]
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudflareBulkDeleteRequest {
pub ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct CloudflareZoneCreateRequest {
pub name: String,
#[serde(default)]
pub account: Option<CloudflareZoneAccountWrite>,
#[serde(default)]
pub jump_start: Option<bool>,
#[serde(rename = "type", default)]
pub zone_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct CloudflareZoneEditRequest {
#[serde(default)]
pub paused: Option<bool>,
#[serde(rename = "type", default)]
pub zone_type: Option<String>,
#[serde(default)]
pub vanity_name_servers: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct CloudflareZoneAccountWrite {
#[serde(default)]
pub id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct CloudflareDnsRecordListFilters {
#[serde(default)]
pub name: Option<String>,
#[serde(rename = "type", default)]
pub record_type: Option<String>,
#[serde(default)]
pub page: Option<u64>,
#[serde(default)]
pub per_page: Option<u64>,
}
impl CloudflareClient {
pub fn new(
api_token: impl Into<String>,
account_id: impl Into<String>,
) -> Result<Self, String> {
let token = api_token.into();
let http = RequestFactory::new(CLOUDFLARE_API_BASE)
.map_err(|error| error.to_string())?
.with_auth(AuthStrategy::Bearer(token))
.with_default_header(
HeaderName::from_static("content-type"),
HeaderValue::from_static("application/json"),
);
Ok(Self {
account_id: account_id.into(),
http,
})
}
pub async fn search_domains(
&self,
query: &str,
extension: &[String],
) -> Result<Vec<DomainAvailability>, String> {
#[derive(Serialize)]
struct SearchQuery<'a> {
query: &'a str,
#[serde(skip_serializing_if = "<[String]>::is_empty")]
tlds: &'a [String],
}
let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
.http
.get_json(
&format!("/accounts/{}/registrar/domain-search", self.account_id),
Some(&SearchQuery {
query,
tlds: extension,
}),
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn check_domains(
&self,
domains: &[String],
) -> Result<Vec<DomainAvailability>, String> {
let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
.http
.post_json(
&format!("/accounts/{}/registrar/domain-check", self.account_id),
&json!({ "domains": domains }),
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn list_registered_domains(
&self,
) -> Result<(Vec<RegisteredDomain>, Option<PaginationInfo>), String> {
let envelope: CloudflareEnvelope<Vec<RegisteredDomain>> = self
.http
.get_json(
&format!("/accounts/{}/registrar/domains", self.account_id),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok((envelope.result, envelope.result_info))
}
pub async fn list_stores(&self) -> Result<(Vec<SecretsStore>, Option<PaginationInfo>), String> {
let envelope: CloudflareEnvelope<Vec<SecretsStore>> = self
.http
.get_json(
&format!("/accounts/{}/secrets_store/stores", self.account_id),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok((envelope.result, envelope.result_info))
}
pub async fn get_store(&self, store_id: &str) -> Result<SecretsStore, String> {
let envelope: CloudflareEnvelope<SecretsStore> = self
.http
.get_json(
&format!(
"/accounts/{}/secrets_store/stores/{}",
self.account_id, store_id
),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn create_store(
&self,
request: &CloudflareStoreCreateRequest,
) -> Result<SecretsStore, String> {
let envelope: CloudflareEnvelope<SecretsStore> = self
.http
.post_json(
&format!("/accounts/{}/secrets_store/stores", self.account_id),
request,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn delete_store(&self, store_id: &str) -> Result<(), String> {
let _: CloudflareEnvelope<Option<Value>> = self
.http
.delete_json(
&format!(
"/accounts/{}/secrets_store/stores/{}",
self.account_id, store_id
),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok(())
}
pub async fn list_secrets(
&self,
store_id: &str,
) -> Result<(Vec<SecretRecord>, Option<PaginationInfo>), String> {
let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
.http
.get_json(
&format!(
"/accounts/{}/secrets_store/stores/{}/secrets",
self.account_id, store_id
),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok((envelope.result, envelope.result_info))
}
pub async fn get_secret(
&self,
store_id: &str,
secret_id: &str,
) -> Result<SecretRecord, String> {
let envelope: CloudflareEnvelope<SecretRecord> = self
.http
.get_json(
&format!(
"/accounts/{}/secrets_store/stores/{}/secrets/{}",
self.account_id, store_id, secret_id
),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn create_secret(
&self,
store_id: &str,
request: &CloudflareSecretCreateRequest,
) -> Result<SecretRecord, String> {
let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
.http
.post_json(
&format!(
"/accounts/{}/secrets_store/stores/{}/secrets",
self.account_id, store_id
),
&vec![request],
)
.await
.map_err(render_http_error)?;
envelope
.result
.into_iter()
.next()
.ok_or_else(|| "Cloudflare did not return a created secret.".to_string())
}
pub async fn edit_secret(
&self,
store_id: &str,
secret_id: &str,
request: &CloudflareSecretEditRequest,
) -> Result<SecretRecord, String> {
let envelope: CloudflareEnvelope<SecretRecord> = self
.http
.patch_json(
&format!(
"/accounts/{}/secrets_store/stores/{}/secrets/{}",
self.account_id, store_id, secret_id
),
request,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn delete_secret(&self, store_id: &str, secret_id: &str) -> Result<(), String> {
let _: CloudflareEnvelope<Option<Value>> = self
.http
.delete_json(
&format!(
"/accounts/{}/secrets_store/stores/{}/secrets/{}",
self.account_id, store_id, secret_id
),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok(())
}
pub async fn bulk_delete_secrets(
&self,
store_id: &str,
request: &CloudflareBulkDeleteRequest,
) -> Result<(), String> {
let _: CloudflareEnvelope<Option<Value>> = self
.http
.delete_json_with_body(
&format!(
"/accounts/{}/secrets_store/stores/{}/secrets",
self.account_id, store_id
),
Option::<&Value>::None,
request,
)
.await
.map_err(render_http_error)?;
Ok(())
}
pub async fn duplicate_secret(
&self,
store_id: &str,
secret_id: &str,
request: &CloudflareSecretDuplicateRequest,
) -> Result<SecretRecord, String> {
let envelope: CloudflareEnvelope<SecretRecord> = self
.http
.post_json(
&format!(
"/accounts/{}/secrets_store/stores/{}/secrets/{}/duplicate",
self.account_id, store_id, secret_id
),
request,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn get_quota(&self) -> Result<SecretsQuota, String> {
let envelope: CloudflareEnvelope<SecretsQuota> = self
.http
.get_json(
&format!("/accounts/{}/secrets_store/quota", self.account_id),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn list_zones(
&self,
filters: &CloudflareZoneFilters,
) -> Result<(Vec<CloudflareZone>, Option<PaginationInfo>), String> {
let envelope: CloudflareEnvelope<Vec<CloudflareZone>> = self
.http
.get_json("/zones", Some(filters))
.await
.map_err(render_http_error)?;
Ok((envelope.result, envelope.result_info))
}
pub async fn get_zone(&self, zone_id: &str) -> Result<CloudflareZone, String> {
let envelope: CloudflareEnvelope<CloudflareZone> = self
.http
.get_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn create_zone(
&self,
request: &CloudflareZoneCreateRequest,
) -> Result<CloudflareZone, String> {
let envelope: CloudflareEnvelope<CloudflareZone> = self
.http
.post_json("/zones", request)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn edit_zone(
&self,
zone_id: &str,
request: &CloudflareZoneEditRequest,
) -> Result<CloudflareZone, String> {
let envelope: CloudflareEnvelope<CloudflareZone> = self
.http
.patch_json(&format!("/zones/{}", zone_id), request)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn delete_zone(&self, zone_id: &str) -> Result<(), String> {
let _: CloudflareEnvelope<Option<Value>> = self
.http
.delete_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
.await
.map_err(render_http_error)?;
Ok(())
}
pub async fn list_records(
&self,
zone_id: &str,
filters: &CloudflareDnsRecordListFilters,
) -> Result<(Vec<CloudflareDnsRecord>, Option<PaginationInfo>), String> {
let envelope: CloudflareEnvelope<Vec<CloudflareDnsRecord>> = self
.http
.get_json(&format!("/zones/{}/dns_records", zone_id), Some(filters))
.await
.map_err(render_http_error)?;
Ok((envelope.result, envelope.result_info))
}
pub async fn get_record(
&self,
zone_id: &str,
record_id: &str,
) -> Result<CloudflareDnsRecord, String> {
let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
.http
.get_json(
&format!("/zones/{}/dns_records/{}", zone_id, record_id),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn create_record(
&self,
zone_id: &str,
request: &CloudflareDnsRecordWrite,
) -> Result<CloudflareDnsRecord, String> {
let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
.http
.post_json(&format!("/zones/{}/dns_records", zone_id), request)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn replace_record(
&self,
zone_id: &str,
record_id: &str,
request: &CloudflareDnsRecordWrite,
) -> Result<CloudflareDnsRecord, String> {
let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
.http
.put_json(
&format!("/zones/{}/dns_records/{}", zone_id, record_id),
request,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn edit_record(
&self,
zone_id: &str,
record_id: &str,
request: &CloudflareDnsRecordWrite,
) -> Result<CloudflareDnsRecord, String> {
let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
.http
.patch_json(
&format!("/zones/{}/dns_records/{}", zone_id, record_id),
request,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn delete_record(&self, zone_id: &str, record_id: &str) -> Result<(), String> {
let _: CloudflareEnvelope<Option<Value>> = self
.http
.delete_json(
&format!("/zones/{}/dns_records/{}", zone_id, record_id),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok(())
}
pub async fn batch_records(
&self,
zone_id: &str,
request: &CloudflareDnsRecordBatch,
) -> Result<Value, String> {
let envelope: CloudflareEnvelope<Value> = self
.http
.post_json(&format!("/zones/{}/dns_records/batch", zone_id), request)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn export_records(&self, zone_id: &str) -> Result<ResponseBytes, String> {
self.http
.get_bytes(
&format!("/zones/{}/dns_records/export", zone_id),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)
}
pub async fn import_records(&self, zone_id: &str, bytes: Vec<u8>) -> Result<Value, String> {
let response = self
.http
.post_bytes(
&format!("/zones/{}/dns_records/import", zone_id),
bytes,
"text/plain",
)
.await
.map_err(render_http_error)?;
let envelope: CloudflareEnvelope<Value> =
serde_json::from_slice(&response.body).map_err(|error| error.to_string())?;
Ok(envelope.result)
}
pub async fn get_dnssec(&self, zone_id: &str) -> Result<CloudflareDnssec, String> {
let envelope: CloudflareEnvelope<CloudflareDnssec> = self
.http
.get_json(
&format!("/zones/{}/dnssec", zone_id),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn edit_dnssec(
&self,
zone_id: &str,
request: &CloudflareDnssecEdit,
) -> Result<CloudflareDnssec, String> {
let envelope: CloudflareEnvelope<CloudflareDnssec> = self
.http
.patch_json(&format!("/zones/{}/dnssec", zone_id), request)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn get_dns_settings(&self, zone_id: &str) -> Result<CloudflareDnsSettings, String> {
let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
.http
.get_json(
&format!("/zones/{}/dns_settings", zone_id),
Option::<&Value>::None,
)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
pub async fn edit_dns_settings(
&self,
zone_id: &str,
request: &CloudflareDnsSettings,
) -> Result<CloudflareDnsSettings, String> {
let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
.http
.patch_json(&format!("/zones/{}/dns_settings", zone_id), request)
.await
.map_err(render_http_error)?;
Ok(envelope.result)
}
}
fn render_http_error(error: HttpError) -> String {
error.to_string()
}