1+ use std:: net:: IpAddr ;
2+
13use anyhow:: { anyhow, Result } ;
24use async_trait:: async_trait;
35use reqwest:: Client ;
46use serde:: { Deserialize , Serialize } ;
5- use serde_json:: { json, Value } ;
7+ use serde_json:: json;
68use smallvec:: SmallVec ;
79
810use crate :: client:: { IpUpdate , IpVersion , Provider } ;
@@ -12,7 +14,7 @@ use crate::client::{IpUpdate, IpVersion, Provider};
1214pub struct Cloudflare {
1315 zone : String ,
1416 api_token : String ,
15- domains : SmallVec < [ Domains ; 2 ] > ,
17+ domains : SmallVec < [ Domain ; 2 ] > ,
1618 #[ serde( default = "default_api_url" ) ]
1719 api_url : String ,
1820}
@@ -23,32 +25,38 @@ fn default_api_url() -> String {
2325
2426#[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
2527#[ serde( default ) ]
26- struct Domains {
28+ struct Domain {
2729 name : String ,
30+ #[ serde( default = "default_ttl" ) ]
2831 ttl : u32 ,
32+ #[ serde( default ) ]
2933 proxied : bool ,
30- comment : Option < String > ,
34+ #[ serde( default = "default_comment" ) ]
35+ comment : String ,
36+ }
37+
38+ // TTL of 1 is Cloudflare's auto setting
39+ fn default_ttl ( ) -> u32 {
40+ 1
41+ }
42+
43+ fn default_comment ( ) -> String {
44+ String :: from ( "Created by DDRS" )
3145}
3246
33- /// Zone lookup response
3447#[ derive( Debug , Deserialize ) ]
3548struct ZoneList {
3649 result : Option < Vec < ZoneResult > > ,
37- #[ serde( rename = "errors" ) ]
38- _errors : Vec < Value > ,
3950}
4051
4152#[ derive( Debug , Deserialize ) ]
4253struct ZoneResult {
4354 id : String ,
4455}
4556
46- /// Records lookup response
4757#[ derive( Debug , Deserialize ) ]
4858struct RecordsList {
4959 result : Option < Vec < RecordResult > > ,
50- #[ serde( rename = "errors" ) ]
51- _errors : Vec < Value > ,
5260}
5361
5462#[ derive( Debug , Deserialize ) ]
@@ -58,26 +66,16 @@ struct RecordResult {
5866
5967#[ derive( Debug , Deserialize ) ]
6068struct UpdatedResult {
61- #[ serde( rename = "errors" ) ]
62- _errors : Vec < Value > ,
63- #[ serde( rename = "messages" ) ]
64- _messages : Vec < Value > ,
6569 success : bool ,
6670}
6771
6872#[ derive( Debug , Deserialize ) ]
6973struct CreatedResult {
70- #[ serde( rename = "errors" ) ]
71- _errors : Vec < Value > ,
72- #[ serde( rename = "messages" ) ]
73- _messages : Vec < Value > ,
7474 success : bool ,
7575}
7676
77- #[ async_trait]
78- #[ typetag:: serde( name = "cloudflare" ) ]
79- impl Provider for Cloudflare {
80- async fn update ( & self , update : IpUpdate , request : Client ) -> Result < bool > {
77+ impl Cloudflare {
78+ async fn fetch_zone_id ( & self , request : & Client ) -> Result < String > {
8179 let zones = request
8280 . get ( format ! ( "{}/zones" , self . api_url) )
8381 . query ( & [ ( "name" , & self . zone ) ] )
@@ -89,75 +87,136 @@ impl Provider for Cloudflare {
8987 let zone_result = zones. result . ok_or ( anyhow ! (
9088 "Failed to list Cloudflare zones, is your token valid?"
9189 ) ) ?;
92- let zone_id = & zone_result
90+ Ok ( zone_result
9391 . first ( )
9492 . ok_or ( anyhow ! ( "Failed to find a matching Cloudflare zone" ) ) ?
95- . id ;
93+ . id
94+ . clone ( ) )
95+ }
96+
97+ async fn fetch_dns_records (
98+ & self ,
99+ request : & Client ,
100+ zone_id : & str ,
101+ record_type : & str ,
102+ domain : & Domain ,
103+ ) -> Result < Vec < RecordResult > > {
104+ let records = request
105+ . get ( format ! ( "{}/zones/{}/dns_records" , self . api_url, zone_id) )
106+ . query ( & [ ( "name" , & domain. name ) ] )
107+ . query ( & [ ( "type" , record_type) ] )
108+ . bearer_auth ( & self . api_token )
109+ . send ( )
110+ . await ?
111+ . json :: < RecordsList > ( )
112+ . await ?;
113+ records. result . ok_or ( anyhow ! (
114+ "Failed to list Cloudflare DNS records for {}" ,
115+ domain. name
116+ ) )
117+ }
118+
119+ async fn update_dns_record (
120+ & self ,
121+ request : & Client ,
122+ zone_id : & str ,
123+ record_id : & str ,
124+ record_type : & str ,
125+ domain : & Domain ,
126+ address : & IpAddr ,
127+ ) -> Result < ( ) > {
128+ let updated = request
129+ . put ( format ! (
130+ "{}/zones/{}/dns_records/{}" ,
131+ self . api_url, zone_id, record_id,
132+ ) )
133+ . json ( & json ! ( {
134+ "type" : record_type,
135+ "name" : domain,
136+ "content" : address,
137+ "ttl" : domain. ttl,
138+ "proxied" : domain. proxied,
139+ "comment" : domain. comment,
140+ } ) )
141+ . bearer_auth ( & self . api_token )
142+ . send ( )
143+ . await ?
144+ . json :: < UpdatedResult > ( )
145+ . await ?;
146+ if !updated. success {
147+ return Err ( anyhow ! (
148+ "Failed to update Cloudflare domain ({}) record" ,
149+ domain. name
150+ ) ) ;
151+ }
152+ Ok ( ( ) )
153+ }
154+
155+ async fn create_dns_record (
156+ & self ,
157+ request : & Client ,
158+ zone_id : & str ,
159+ record_type : & str ,
160+ domain : & Domain ,
161+ address : & IpAddr ,
162+ ) -> Result < ( ) > {
163+ let created = request
164+ . post ( format ! ( "{}/zones/{}/dns_records" , self . api_url, zone_id) )
165+ . json ( & json ! ( {
166+ "type" : record_type,
167+ "name" : domain,
168+ "content" : address,
169+ "ttl" : domain. ttl,
170+ "proxied" : domain. proxied,
171+ "comment" : domain. comment,
172+ } ) )
173+ . bearer_auth ( & self . api_token )
174+ . send ( )
175+ . await ?
176+ . json :: < CreatedResult > ( )
177+ . await ?;
178+ if !created. success {
179+ return Err ( anyhow ! (
180+ "Failed to create Cloudflare domain ({}) record" ,
181+ domain. name
182+ ) ) ;
183+ }
184+ Ok ( ( ) )
185+ }
186+ }
187+
188+ #[ async_trait]
189+ #[ typetag:: serde( name = "cloudflare" ) ]
190+ impl Provider for Cloudflare {
191+ async fn update ( & self , update : IpUpdate , request : Client ) -> Result < bool > {
192+ let zone_id = self . fetch_zone_id ( & request) . await ?;
96193 for domain in & self . domains {
97194 for ( version, address) in update. as_array ( ) {
98- if address. is_none ( ) {
99- continue ;
100- }
101- let record_type = match version {
102- IpVersion :: V4 => "A" ,
103- IpVersion :: V6 => "AAAA" ,
104- } ;
105- let records = request
106- . get ( format ! ( "{}/zones/{zone_id}/dns_records" , self . api_url) )
107- . query ( & [ ( "name" , & domain. name ) ] )
108- . query ( & [ ( "type" , record_type) ] )
109- . bearer_auth ( & self . api_token )
110- . send ( )
111- . await ?
112- . json :: < RecordsList > ( )
113- . await ?;
114- if let Some ( record) = records. result . and_then ( |vec| vec. into_iter ( ) . next ( ) ) {
115- let updated = request
116- . put ( format ! (
117- "{}/zones/{zone_id}/dns_records/{}" ,
118- self . api_url, record. id,
119- ) )
120- . json ( & json ! ( {
121- "type" : record_type,
122- "name" : domain. name,
123- "content" : address,
124- "ttl" : domain. ttl,
125- "proxied" : domain. proxied,
126- "comment" : domain. comment,
127- } ) )
128- . bearer_auth ( & self . api_token )
129- . send ( )
195+ if let Some ( addr) = address {
196+ let record_type = match version {
197+ IpVersion :: V4 => "A" ,
198+ IpVersion :: V6 => "AAAA" ,
199+ } ;
200+ if let Some ( record) = self
201+ . fetch_dns_records ( & request, & zone_id, record_type, domain)
130202 . await ?
131- . json :: < UpdatedResult > ( )
203+ . first ( )
204+ {
205+ println ! ( "Updating record: {}" , record. id) ;
206+ self . update_dns_record (
207+ & request,
208+ & zone_id,
209+ & record. id ,
210+ record_type,
211+ domain,
212+ & addr,
213+ )
132214 . await ?;
133- if !updated. success {
134- return Err ( anyhow ! (
135- "Failed to update Cloudflare domain ({}) record" ,
136- domain. name
137- ) ) ;
215+ } else {
216+ println ! ( "Creating record" ) ;
217+ self . create_dns_record ( & request, & zone_id, record_type, domain, & addr)
218+ . await ?;
138219 }
139- continue ;
140- }
141- let created = request
142- . post ( format ! ( "{}/zones/{zone_id}/dns_records" , self . api_url) )
143- . json ( & json ! ( {
144- "type" : record_type,
145- "name" : domain. name,
146- "content" : address,
147- "ttl" : domain. ttl,
148- "proxied" : domain. proxied,
149- "comment" : domain. comment,
150- } ) )
151- . bearer_auth ( & self . api_token )
152- . send ( )
153- . await ?
154- . json :: < CreatedResult > ( )
155- . await ?;
156- if !created. success {
157- return Err ( anyhow ! (
158- "Failed to create Cloudflare domain ({}) record" ,
159- domain. name
160- ) ) ;
161220 }
162221 }
163222 }
@@ -197,11 +256,11 @@ mod tests {
197256 let provider = Cloudflare {
198257 zone : "example.com" . to_string ( ) ,
199258 api_token : "bad_token" . to_string ( ) ,
200- domains : smallvec ! [ Domains {
259+ domains : smallvec ! [ Domain {
201260 name: "example.com" . to_string( ) ,
202261 ttl: 1 ,
203262 proxied: true ,
204- comment: None ,
263+ comment: "Created by DDRS" . to_string ( ) ,
205264 } ] ,
206265 api_url : mock. uri ( ) ,
207266 } ;
@@ -241,11 +300,11 @@ mod tests {
241300 let provider = Cloudflare {
242301 zone : "example.com" . to_string ( ) ,
243302 api_token : "token" . to_string ( ) ,
244- domains : smallvec ! [ Domains {
303+ domains : smallvec ! [ Domain {
245304 name: "example.com" . to_string( ) ,
246305 ttl: 1 ,
247306 proxied: true ,
248- comment: None ,
307+ comment: "Created by DDRS" . to_string ( ) ,
249308 } ] ,
250309 api_url : mock. uri ( ) ,
251310 } ;
@@ -316,11 +375,11 @@ mod tests {
316375 let provider = Cloudflare {
317376 zone : "example.com" . to_string ( ) ,
318377 api_token : "token" . to_string ( ) ,
319- domains : smallvec ! [ Domains {
378+ domains : smallvec ! [ Domain {
320379 name: "example.com" . to_string( ) ,
321380 ttl: 1 ,
322381 proxied: true ,
323- comment: Some ( "Created by DDRS" . to_string( ) ) ,
382+ comment: "Created by DDRS" . to_string( ) ,
324383 } ] ,
325384 api_url : mock. uri ( ) ,
326385 } ;
@@ -426,11 +485,11 @@ mod tests {
426485 let provider = Cloudflare {
427486 zone : "example.com" . to_string ( ) ,
428487 api_token : "token" . to_string ( ) ,
429- domains : smallvec ! [ Domains {
488+ domains : smallvec ! [ Domain {
430489 name: "example.com" . to_string( ) ,
431490 ttl: 1 ,
432491 proxied: true ,
433- comment: Some ( "Created by DDRS" . to_string( ) ) ,
492+ comment: "Created by DDRS" . to_string( ) ,
434493 } ] ,
435494 api_url : mock. uri ( ) ,
436495 } ;
@@ -503,11 +562,11 @@ mod tests {
503562 let provider = Cloudflare {
504563 zone : "example.com" . to_string ( ) ,
505564 api_token : "token" . to_string ( ) ,
506- domains : smallvec ! [ Domains {
565+ domains : smallvec ! [ Domain {
507566 name: "example.com" . to_string( ) ,
508567 ttl: 1 ,
509568 proxied: true ,
510- comment: Some ( "Created by DDRS" . to_string( ) ) ,
569+ comment: "Created by DDRS" . to_string( ) ,
511570 } ] ,
512571 api_url : mock. uri ( ) ,
513572 } ;
@@ -582,11 +641,11 @@ mod tests {
582641 let provider = Cloudflare {
583642 zone : "example.com" . to_string ( ) ,
584643 api_token : "token" . to_string ( ) ,
585- domains : smallvec ! [ Domains {
644+ domains : smallvec ! [ Domain {
586645 name: "example.com" . to_string( ) ,
587646 ttl: 1 ,
588647 proxied: true ,
589- comment: Some ( "Created by DDRS" . to_string( ) ) ,
648+ comment: "Created by DDRS" . to_string( ) ,
590649 } ] ,
591650 api_url : mock. uri ( ) ,
592651 } ;
@@ -672,11 +731,11 @@ mod tests {
672731 let provider = Cloudflare {
673732 zone : "example.com" . to_string ( ) ,
674733 api_token : "token" . to_string ( ) ,
675- domains : smallvec ! [ Domains {
734+ domains : smallvec ! [ Domain {
676735 name: "example.com" . to_string( ) ,
677736 ttl: 1 ,
678737 proxied: true ,
679- comment: Some ( "Created by DDRS" . to_string( ) ) ,
738+ comment: "Created by DDRS" . to_string( ) ,
680739 } ] ,
681740 api_url : mock. uri ( ) ,
682741 } ;
0 commit comments