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