1use super::http::{AuthStrategy, HttpError, RequestFactory, ResponseBytes};
2use super::models::{
3 CloudflareDnsRecord, CloudflareDnsRecordBatch, CloudflareDnsRecordWrite, CloudflareDnsSettings,
4 CloudflareDnssec, CloudflareDnssecEdit, CloudflareEnvelope, CloudflareWorkerSecret,
5 CloudflareWorkerSettings, CloudflareZone, CloudflareZoneFilters, DomainAvailability,
6 PaginationInfo, RegisteredDomain, SecretRecord, SecretsQuota, SecretsStore,
7};
8use reqwest::header::{HeaderName, HeaderValue};
9use reqwest::StatusCode;
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12
13const CLOUDFLARE_API_BASE: &str = "https://api.cloudflare.com/client/v4";
14
15#[derive(Debug, Clone)]
16pub struct CloudflareClient {
17 account_id: String,
18 http: RequestFactory,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CloudflareStoreCreateRequest {
23 pub name: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CloudflareSecretCreateRequest {
28 pub name: String,
29 pub value: String,
30 #[serde(default)]
31 pub scopes: Vec<String>,
32 #[serde(default)]
33 pub comment: Option<String>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct CloudflareSecretEditRequest {
38 #[serde(default)]
39 pub name: Option<String>,
40 #[serde(default)]
41 pub value: Option<String>,
42 #[serde(default)]
43 pub scopes: Option<Vec<String>>,
44 #[serde(default)]
45 pub comment: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct CloudflareSecretDuplicateRequest {
50 pub name: String,
51 #[serde(default)]
52 pub scopes: Vec<String>,
53 #[serde(default)]
54 pub comment: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CloudflareBulkDeleteRequest {
59 pub ids: Vec<String>,
60}
61
62#[derive(Debug, Clone, Serialize, Default)]
63pub struct CloudflareZoneCreateRequest {
64 pub name: String,
65 #[serde(default)]
66 pub account: Option<CloudflareZoneAccountWrite>,
67 #[serde(default)]
68 pub jump_start: Option<bool>,
69 #[serde(rename = "type", default)]
70 pub zone_type: Option<String>,
71}
72
73#[derive(Debug, Clone, Serialize, Default)]
74pub struct CloudflareZoneEditRequest {
75 #[serde(default)]
76 pub paused: Option<bool>,
77 #[serde(rename = "type", default)]
78 pub zone_type: Option<String>,
79 #[serde(default)]
80 pub vanity_name_servers: Option<Vec<String>>,
81}
82
83#[derive(Debug, Clone, Serialize, Default)]
84pub struct CloudflareZoneAccountWrite {
85 #[serde(default)]
86 pub id: Option<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Default)]
90pub struct CloudflareDnsRecordListFilters {
91 #[serde(default)]
92 pub name: Option<String>,
93 #[serde(rename = "type", default)]
94 pub record_type: Option<String>,
95 #[serde(default)]
96 pub page: Option<u64>,
97 #[serde(default)]
98 pub per_page: Option<u64>,
99}
100
101impl CloudflareClient {
102 pub fn new(
103 api_token: impl Into<String>,
104 account_id: impl Into<String>,
105 ) -> Result<Self, String> {
106 let token = api_token.into();
107 let http = RequestFactory::new(CLOUDFLARE_API_BASE)
108 .map_err(|error| error.to_string())?
109 .with_auth(AuthStrategy::Bearer(token))
110 .with_default_header(
111 HeaderName::from_static("content-type"),
112 HeaderValue::from_static("application/json"),
113 );
114 Ok(Self {
115 account_id: account_id.into(),
116 http,
117 })
118 }
119
120 pub async fn search_domains(
121 &self,
122 query: &str,
123 extension: &[String],
124 ) -> Result<Vec<DomainAvailability>, String> {
125 #[derive(Serialize)]
126 struct SearchQuery<'a> {
127 query: &'a str,
128 #[serde(skip_serializing_if = "<[String]>::is_empty")]
129 tlds: &'a [String],
130 }
131
132 let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
133 .http
134 .get_json(
135 &format!("/accounts/{}/registrar/domain-search", self.account_id),
136 Some(&SearchQuery {
137 query,
138 tlds: extension,
139 }),
140 )
141 .await
142 .map_err(render_http_error)?;
143 Ok(envelope.result)
144 }
145
146 pub async fn check_domains(
147 &self,
148 domains: &[String],
149 ) -> Result<Vec<DomainAvailability>, String> {
150 let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
151 .http
152 .post_json(
153 &format!("/accounts/{}/registrar/domain-check", self.account_id),
154 &json!({ "domains": domains }),
155 )
156 .await
157 .map_err(render_http_error)?;
158 Ok(envelope.result)
159 }
160
161 pub async fn list_registered_domains(
162 &self,
163 ) -> Result<(Vec<RegisteredDomain>, Option<PaginationInfo>), String> {
164 let envelope: CloudflareEnvelope<Vec<RegisteredDomain>> = self
165 .http
166 .get_json(
167 &format!("/accounts/{}/registrar/domains", self.account_id),
168 Option::<&Value>::None,
169 )
170 .await
171 .map_err(render_http_error)?;
172 Ok((envelope.result, envelope.result_info))
173 }
174
175 pub async fn list_stores(&self) -> Result<(Vec<SecretsStore>, Option<PaginationInfo>), String> {
176 let envelope: CloudflareEnvelope<Vec<SecretsStore>> = self
177 .http
178 .get_json(
179 &format!("/accounts/{}/secrets_store/stores", self.account_id),
180 Option::<&Value>::None,
181 )
182 .await
183 .map_err(render_http_error)?;
184 Ok((envelope.result, envelope.result_info))
185 }
186
187 pub async fn get_store(&self, store_id: &str) -> Result<SecretsStore, String> {
188 let envelope: CloudflareEnvelope<SecretsStore> = self
189 .http
190 .get_json(
191 &format!(
192 "/accounts/{}/secrets_store/stores/{}",
193 self.account_id, store_id
194 ),
195 Option::<&Value>::None,
196 )
197 .await
198 .map_err(render_http_error)?;
199 Ok(envelope.result)
200 }
201
202 pub async fn create_store(
203 &self,
204 request: &CloudflareStoreCreateRequest,
205 ) -> Result<SecretsStore, String> {
206 let envelope: CloudflareEnvelope<SecretsStore> = self
207 .http
208 .post_json(
209 &format!("/accounts/{}/secrets_store/stores", self.account_id),
210 request,
211 )
212 .await
213 .map_err(render_http_error)?;
214 Ok(envelope.result)
215 }
216
217 pub async fn delete_store(&self, store_id: &str) -> Result<(), String> {
218 let _: CloudflareEnvelope<Option<Value>> = self
219 .http
220 .delete_json(
221 &format!(
222 "/accounts/{}/secrets_store/stores/{}",
223 self.account_id, store_id
224 ),
225 Option::<&Value>::None,
226 )
227 .await
228 .map_err(render_http_error)?;
229 Ok(())
230 }
231
232 pub async fn list_secrets(
233 &self,
234 store_id: &str,
235 ) -> Result<(Vec<SecretRecord>, Option<PaginationInfo>), String> {
236 let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
237 .http
238 .get_json(
239 &format!(
240 "/accounts/{}/secrets_store/stores/{}/secrets",
241 self.account_id, store_id
242 ),
243 Option::<&Value>::None,
244 )
245 .await
246 .map_err(render_http_error)?;
247 Ok((envelope.result, envelope.result_info))
248 }
249
250 pub async fn get_secret(
251 &self,
252 store_id: &str,
253 secret_id: &str,
254 ) -> Result<SecretRecord, String> {
255 let envelope: CloudflareEnvelope<SecretRecord> = self
256 .http
257 .get_json(
258 &format!(
259 "/accounts/{}/secrets_store/stores/{}/secrets/{}",
260 self.account_id, store_id, secret_id
261 ),
262 Option::<&Value>::None,
263 )
264 .await
265 .map_err(render_http_error)?;
266 Ok(envelope.result)
267 }
268
269 pub async fn create_secret(
270 &self,
271 store_id: &str,
272 request: &CloudflareSecretCreateRequest,
273 ) -> Result<SecretRecord, String> {
274 let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
275 .http
276 .post_json(
277 &format!(
278 "/accounts/{}/secrets_store/stores/{}/secrets",
279 self.account_id, store_id
280 ),
281 &vec![request],
282 )
283 .await
284 .map_err(render_http_error)?;
285 envelope
286 .result
287 .into_iter()
288 .next()
289 .ok_or_else(|| "Cloudflare did not return a created secret.".to_string())
290 }
291
292 pub async fn edit_secret(
293 &self,
294 store_id: &str,
295 secret_id: &str,
296 request: &CloudflareSecretEditRequest,
297 ) -> Result<SecretRecord, String> {
298 let envelope: CloudflareEnvelope<SecretRecord> = self
299 .http
300 .patch_json(
301 &format!(
302 "/accounts/{}/secrets_store/stores/{}/secrets/{}",
303 self.account_id, store_id, secret_id
304 ),
305 request,
306 )
307 .await
308 .map_err(render_http_error)?;
309 Ok(envelope.result)
310 }
311
312 pub async fn delete_secret(&self, store_id: &str, secret_id: &str) -> Result<(), String> {
313 let _: CloudflareEnvelope<Option<Value>> = self
314 .http
315 .delete_json(
316 &format!(
317 "/accounts/{}/secrets_store/stores/{}/secrets/{}",
318 self.account_id, store_id, secret_id
319 ),
320 Option::<&Value>::None,
321 )
322 .await
323 .map_err(render_http_error)?;
324 Ok(())
325 }
326
327 pub async fn bulk_delete_secrets(
328 &self,
329 store_id: &str,
330 request: &CloudflareBulkDeleteRequest,
331 ) -> Result<(), String> {
332 let _: CloudflareEnvelope<Option<Value>> = self
333 .http
334 .delete_json_with_body(
335 &format!(
336 "/accounts/{}/secrets_store/stores/{}/secrets",
337 self.account_id, store_id
338 ),
339 Option::<&Value>::None,
340 request,
341 )
342 .await
343 .map_err(render_http_error)?;
344 Ok(())
345 }
346
347 pub async fn duplicate_secret(
348 &self,
349 store_id: &str,
350 secret_id: &str,
351 request: &CloudflareSecretDuplicateRequest,
352 ) -> Result<SecretRecord, String> {
353 let envelope: CloudflareEnvelope<SecretRecord> = self
354 .http
355 .post_json(
356 &format!(
357 "/accounts/{}/secrets_store/stores/{}/secrets/{}/duplicate",
358 self.account_id, store_id, secret_id
359 ),
360 request,
361 )
362 .await
363 .map_err(render_http_error)?;
364 Ok(envelope.result)
365 }
366
367 pub async fn get_quota(&self) -> Result<SecretsQuota, String> {
368 let envelope: CloudflareEnvelope<SecretsQuota> = self
369 .http
370 .get_json(
371 &format!("/accounts/{}/secrets_store/quota", self.account_id),
372 Option::<&Value>::None,
373 )
374 .await
375 .map_err(render_http_error)?;
376 Ok(envelope.result)
377 }
378
379 pub async fn list_worker_secrets(
380 &self,
381 script_name: &str,
382 ) -> Result<Vec<CloudflareWorkerSecret>, String> {
383 let envelope: CloudflareEnvelope<Vec<CloudflareWorkerSecret>> = self
384 .http
385 .get_json(
386 &format!(
387 "/accounts/{}/workers/scripts/{}/secrets",
388 self.account_id, script_name
389 ),
390 Option::<&Value>::None,
391 )
392 .await
393 .map_err(render_http_error)?;
394 Ok(envelope.result)
395 }
396
397 pub async fn get_worker_secret(
398 &self,
399 script_name: &str,
400 secret_name: &str,
401 ) -> Result<CloudflareWorkerSecret, String> {
402 let envelope: CloudflareEnvelope<CloudflareWorkerSecret> = self
403 .http
404 .get_json(
405 &format!(
406 "/accounts/{}/workers/scripts/{}/secrets/{}",
407 self.account_id, script_name, secret_name
408 ),
409 Option::<&Value>::None,
410 )
411 .await
412 .map_err(render_http_error)?;
413 Ok(envelope.result)
414 }
415
416 pub async fn put_worker_secret(
417 &self,
418 script_name: &str,
419 secret_name: &str,
420 secret_value: &str,
421 ) -> Result<CloudflareWorkerSecret, String> {
422 let envelope: CloudflareEnvelope<CloudflareWorkerSecret> = self
423 .http
424 .put_json(
425 &format!(
426 "/accounts/{}/workers/scripts/{}/secrets",
427 self.account_id, script_name
428 ),
429 &json!({
430 "name": secret_name,
431 "text": secret_value,
432 "type": "secret_text"
433 }),
434 )
435 .await
436 .map_err(render_http_error)?;
437 Ok(envelope.result)
438 }
439
440 pub async fn delete_worker_secret(
441 &self,
442 script_name: &str,
443 secret_name: &str,
444 ) -> Result<(), String> {
445 let _: CloudflareEnvelope<Option<Value>> = self
446 .http
447 .delete_json(
448 &format!(
449 "/accounts/{}/workers/scripts/{}/secrets/{}",
450 self.account_id, script_name, secret_name
451 ),
452 Option::<&Value>::None,
453 )
454 .await
455 .map_err(render_http_error)?;
456 Ok(())
457 }
458
459 pub async fn patch_worker_secrets_bulk(
460 &self,
461 script_name: &str,
462 patch: &Value,
463 ) -> Result<Value, String> {
464 let envelope: CloudflareEnvelope<Value> = self
465 .http
466 .patch_json(
467 &format!(
468 "/accounts/{}/workers/scripts/{}/secrets-bulk",
469 self.account_id, script_name
470 ),
471 patch,
472 )
473 .await
474 .map_err(render_http_error)?;
475 Ok(envelope.result)
476 }
477
478 pub async fn get_worker_settings(
479 &self,
480 script_name: &str,
481 ) -> Result<Option<CloudflareWorkerSettings>, String> {
482 let result: Result<CloudflareEnvelope<CloudflareWorkerSettings>, HttpError> = self
483 .http
484 .get_json(
485 &format!(
486 "/accounts/{}/workers/scripts/{}/settings",
487 self.account_id, script_name
488 ),
489 Option::<&Value>::None,
490 )
491 .await;
492
493 match result {
494 Ok(envelope) => Ok(Some(envelope.result)),
495 Err(HttpError::Request {
496 status: Some(StatusCode::NOT_FOUND),
497 ..
498 }) => Ok(None),
499 Err(error) => Err(render_http_error(error)),
500 }
501 }
502
503 pub async fn list_zones(
504 &self,
505 filters: &CloudflareZoneFilters,
506 ) -> Result<(Vec<CloudflareZone>, Option<PaginationInfo>), String> {
507 let envelope: CloudflareEnvelope<Vec<CloudflareZone>> = self
508 .http
509 .get_json("/zones", Some(filters))
510 .await
511 .map_err(render_http_error)?;
512 Ok((envelope.result, envelope.result_info))
513 }
514
515 pub async fn get_zone(&self, zone_id: &str) -> Result<CloudflareZone, String> {
516 let envelope: CloudflareEnvelope<CloudflareZone> = self
517 .http
518 .get_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
519 .await
520 .map_err(render_http_error)?;
521 Ok(envelope.result)
522 }
523
524 pub async fn create_zone(
525 &self,
526 request: &CloudflareZoneCreateRequest,
527 ) -> Result<CloudflareZone, String> {
528 let envelope: CloudflareEnvelope<CloudflareZone> = self
529 .http
530 .post_json("/zones", request)
531 .await
532 .map_err(render_http_error)?;
533 Ok(envelope.result)
534 }
535
536 pub async fn edit_zone(
537 &self,
538 zone_id: &str,
539 request: &CloudflareZoneEditRequest,
540 ) -> Result<CloudflareZone, String> {
541 let envelope: CloudflareEnvelope<CloudflareZone> = self
542 .http
543 .patch_json(&format!("/zones/{}", zone_id), request)
544 .await
545 .map_err(render_http_error)?;
546 Ok(envelope.result)
547 }
548
549 pub async fn delete_zone(&self, zone_id: &str) -> Result<(), String> {
550 let _: CloudflareEnvelope<Option<Value>> = self
551 .http
552 .delete_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
553 .await
554 .map_err(render_http_error)?;
555 Ok(())
556 }
557
558 pub async fn list_records(
559 &self,
560 zone_id: &str,
561 filters: &CloudflareDnsRecordListFilters,
562 ) -> Result<(Vec<CloudflareDnsRecord>, Option<PaginationInfo>), String> {
563 let envelope: CloudflareEnvelope<Vec<CloudflareDnsRecord>> = self
564 .http
565 .get_json(&format!("/zones/{}/dns_records", zone_id), Some(filters))
566 .await
567 .map_err(render_http_error)?;
568 Ok((envelope.result, envelope.result_info))
569 }
570
571 pub async fn get_record(
572 &self,
573 zone_id: &str,
574 record_id: &str,
575 ) -> Result<CloudflareDnsRecord, String> {
576 let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
577 .http
578 .get_json(
579 &format!("/zones/{}/dns_records/{}", zone_id, record_id),
580 Option::<&Value>::None,
581 )
582 .await
583 .map_err(render_http_error)?;
584 Ok(envelope.result)
585 }
586
587 pub async fn create_record(
588 &self,
589 zone_id: &str,
590 request: &CloudflareDnsRecordWrite,
591 ) -> Result<CloudflareDnsRecord, String> {
592 let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
593 .http
594 .post_json(&format!("/zones/{}/dns_records", zone_id), request)
595 .await
596 .map_err(render_http_error)?;
597 Ok(envelope.result)
598 }
599
600 pub async fn replace_record(
601 &self,
602 zone_id: &str,
603 record_id: &str,
604 request: &CloudflareDnsRecordWrite,
605 ) -> Result<CloudflareDnsRecord, String> {
606 let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
607 .http
608 .put_json(
609 &format!("/zones/{}/dns_records/{}", zone_id, record_id),
610 request,
611 )
612 .await
613 .map_err(render_http_error)?;
614 Ok(envelope.result)
615 }
616
617 pub async fn edit_record(
618 &self,
619 zone_id: &str,
620 record_id: &str,
621 request: &CloudflareDnsRecordWrite,
622 ) -> Result<CloudflareDnsRecord, String> {
623 let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
624 .http
625 .patch_json(
626 &format!("/zones/{}/dns_records/{}", zone_id, record_id),
627 request,
628 )
629 .await
630 .map_err(render_http_error)?;
631 Ok(envelope.result)
632 }
633
634 pub async fn delete_record(&self, zone_id: &str, record_id: &str) -> Result<(), String> {
635 let _: CloudflareEnvelope<Option<Value>> = self
636 .http
637 .delete_json(
638 &format!("/zones/{}/dns_records/{}", zone_id, record_id),
639 Option::<&Value>::None,
640 )
641 .await
642 .map_err(render_http_error)?;
643 Ok(())
644 }
645
646 pub async fn batch_records(
647 &self,
648 zone_id: &str,
649 request: &CloudflareDnsRecordBatch,
650 ) -> Result<Value, String> {
651 let envelope: CloudflareEnvelope<Value> = self
652 .http
653 .post_json(&format!("/zones/{}/dns_records/batch", zone_id), request)
654 .await
655 .map_err(render_http_error)?;
656 Ok(envelope.result)
657 }
658
659 pub async fn export_records(&self, zone_id: &str) -> Result<ResponseBytes, String> {
660 self.http
661 .get_bytes(
662 &format!("/zones/{}/dns_records/export", zone_id),
663 Option::<&Value>::None,
664 )
665 .await
666 .map_err(render_http_error)
667 }
668
669 pub async fn import_records(&self, zone_id: &str, bytes: Vec<u8>) -> Result<Value, String> {
670 let response = self
671 .http
672 .post_bytes(
673 &format!("/zones/{}/dns_records/import", zone_id),
674 bytes,
675 "text/plain",
676 )
677 .await
678 .map_err(render_http_error)?;
679 let envelope: CloudflareEnvelope<Value> =
680 serde_json::from_slice(&response.body).map_err(|error| error.to_string())?;
681 Ok(envelope.result)
682 }
683
684 pub async fn get_dnssec(&self, zone_id: &str) -> Result<CloudflareDnssec, String> {
685 let envelope: CloudflareEnvelope<CloudflareDnssec> = self
686 .http
687 .get_json(
688 &format!("/zones/{}/dnssec", zone_id),
689 Option::<&Value>::None,
690 )
691 .await
692 .map_err(render_http_error)?;
693 Ok(envelope.result)
694 }
695
696 pub async fn edit_dnssec(
697 &self,
698 zone_id: &str,
699 request: &CloudflareDnssecEdit,
700 ) -> Result<CloudflareDnssec, String> {
701 let envelope: CloudflareEnvelope<CloudflareDnssec> = self
702 .http
703 .patch_json(&format!("/zones/{}/dnssec", zone_id), request)
704 .await
705 .map_err(render_http_error)?;
706 Ok(envelope.result)
707 }
708
709 pub async fn get_dns_settings(&self, zone_id: &str) -> Result<CloudflareDnsSettings, String> {
710 let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
711 .http
712 .get_json(
713 &format!("/zones/{}/dns_settings", zone_id),
714 Option::<&Value>::None,
715 )
716 .await
717 .map_err(render_http_error)?;
718 Ok(envelope.result)
719 }
720
721 pub async fn edit_dns_settings(
722 &self,
723 zone_id: &str,
724 request: &CloudflareDnsSettings,
725 ) -> Result<CloudflareDnsSettings, String> {
726 let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
727 .http
728 .patch_json(&format!("/zones/{}/dns_settings", zone_id), request)
729 .await
730 .map_err(render_http_error)?;
731 Ok(envelope.result)
732 }
733}
734
735fn render_http_error(error: HttpError) -> String {
736 error.to_string()
737}