2 * API Reference: https://api.gandi.net/docs/livedns/
3 * Some similar implementations:
4 * - https://github.com/rmarchant/gandi-ddns/blob/master/gandi_ddns.py
5 * - https://github.com/brianhp2/gandi-automatic-dns
8 * - Log to stdout with (at least) timestamps.
12 #![cfg_attr(debug_assertions, allow(unused_variables, unused_imports, dead_code))]
14 use std
::{ fmt
::format
, fs
::File
, net
::{ IpAddr
, Ipv4Addr
}, thread
, time
};
15 use ron
::{ de
::from_reader
, ser
::to_writer
};
16 use serde
::{ Deserialize
, Serialize
};
17 use serde_json
::{ Value
, json
};
19 // A generic result of type 'T'.
20 type Result
<T
> = std
::result
::Result
<T
, Box
<dyn std
::error
::Error
>>;
27 impl std
::fmt
::Display
for Error
{
28 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
29 write!(f
, "Error: {}", &self.message
)
33 impl std
::error
::Error
for Error
{ }
35 #[derive(Debug, Clone, Deserialize, Serialize)]
37 delay_between_check
: time
::Duration
,
45 fn default() -> Self {
46 Config
{ delay_between_check
: time
::Duration
::from_secs(120), api_key
: String
::from(""), fqdn
: String
::from(""), domains
: Vec
::new(), ttl
: 300 }
49 fn read(file_path
: &str) -> Result
<Config
> {
50 match File
::open(file_path
) {
51 Ok(file
) => from_reader(file
).map_err(|e
| e
.into()),
52 // The file doesn't exit -> create it with default values.
54 let file
= File
::create(file_path
)?
;
55 let default_config
= Config
::default();
56 to_writer(file
, &default_config
)?
;
63 const FILE_CONF
: &str = "config.ron";
65 fn main() -> Result
<()> {
67 println!("GANDI DynDNS");
69 let config
= Config
::read(FILE_CONF
)?
;
71 println!("Configuration: {:?}", Config
{ api_key
: String
::from("*****"), ..config
.clone() });
74 let time_beginning_loop
= time
::Instant
::now();
76 if let Err(err
) = check_and_update_dns(&config
.api_key
, &config
.fqdn
, &config
.domains
, config
.ttl
) {
77 println!("!! Error: {}", err
);
80 let elapsed
= time
::Instant
::now() - time_beginning_loop
;
82 if elapsed
< config
.delay_between_check
{
83 let to_wait
= config
.delay_between_check
- elapsed
;
84 thread
::sleep(to_wait
);
89 fn check_and_update_dns(api_key
: &str, fqdn
: &str, domains
: &Vec
<String
>, ttl
: i32) -> Result
<()> {
90 let real_ip
= get_real_ip()?
;
93 for domain
in domains
{
94 let current_ip
= get_current_record_ip(api_key
, domain
, fqdn
)?
;
95 dbg!(domain
, current_ip
);
97 if real_ip
!= current_ip
{
98 println!("IP addresses don't match for domain {}: real = {}, dns = {}. Renewing DNS...", domain
, real_ip
, current_ip
);
99 update_record_ip(api_key
, domain
, fqdn
, real_ip
, ttl
)?
;
100 println!("Renewing of {} successfully", domain
);
107 fn get_real_ip() -> Result
<Ipv4Addr
> {
109 let url
= "https://api.ipify.org";
110 let client
= reqwest
::blocking
::Client
::new();
112 match client
.get(url
).send() {
114 if resp
.status().is_success() {
115 let content
= resp
.text().unwrap();
116 match content
.parse
::<IpAddr
>() {
117 Ok(IpAddr
::V4(ip_v4
)) => Ok(ip_v4
),
118 _
=> Err(Box
::new(Error
{ message
: String
::from("Can't parse IPv4 from ipify") }))
121 Err(Box
::new(Error
{ message
: format!("Request unsuccessful: {:#?}", resp
) }))
125 Err(Box
::new(Error
{ message
: format!("Error during request: {:?}", error
) }))
135 fn request_livedns_gandi(api_key
: &str, url_fragment
: &str, method
: Method
) -> Result
<Value
> {
136 let url
= format!("https://api.gandi.net/v5/livedns/{}", url_fragment
);
137 let client
= reqwest
::blocking
::Client
::new();
139 let request_builder
=
141 Method
::Put(body
) => client
.put(url
).body(body
),
142 Method
::Get
=> client
.get(url
)
145 match request_builder
.header("Authorization", format!("Apikey {}", api_key
)).send() {
147 if resp
.status().is_success() {
148 let content
= resp
.text().unwrap();
149 Ok(serde_json
::from_str(&content
).unwrap())
151 Err(Box
::new(Error
{ message
: format!("Request unsuccessful: {:#?}", resp
) }))
154 Err(Box
::new(Error
{ message
: format!("Error during request: {:?}", error
) }))
158 fn get_current_record_ip(api_key
: &str, name
: &str, fqdn
: &str) -> Result
<Ipv4Addr
> {
159 let json_value
= request_livedns_gandi(api_key
, &format!("domains/{}/records/{}/A", fqdn
, name
), Method
::Get
)?
;
161 match &json_value
["rrset_values"][0] {
162 Value
::String(ip_str
) =>
165 Result
::Err(Box
::new(Error
{ message
: format!("Unable to extract the IP from the JSON answer: {}", json_value
) }))
169 fn update_record_ip(api_key
: &str, name
: &str, fqdn
: &str, ip
: Ipv4Addr
, ttl
: i32) -> Result
<()> {
173 "rrset_values": [ format!("{}", ip
) ],
178 let json_value
= request_livedns_gandi(api_key
, &format!("domains/{}/records/{}/A", fqdn
, name
), Method
::Put(json_body
.to_string()))?
;