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
::{ 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
<()> {
66 println!("GANDI LiveDNS updater");
68 let config
= Config
::read(FILE_CONF
)?
;
70 println!("Configuration: {:?}", Config
{ api_key
: String
::from("*****"), ..config
.clone() });
73 let time_beginning_loop
= time
::Instant
::now();
75 if let Err(err
) = check_and_update_dns(&config
.api_key
, &config
.fqdn
, &config
.domains
, config
.ttl
) {
76 println!("!! Error: {}", err
);
79 let elapsed
= time
::Instant
::now() - time_beginning_loop
;
81 if elapsed
< config
.delay_between_check
{
82 let to_wait
= config
.delay_between_check
- elapsed
;
83 thread
::sleep(to_wait
);
88 fn check_and_update_dns(api_key
: &str, fqdn
: &str, domains
: &Vec
<String
>, ttl
: i32) -> Result
<()> {
89 let real_ip
= get_real_ip()?
;
91 for domain
in domains
{
92 let current_ip
= get_current_record_ip(api_key
, domain
, fqdn
)?
;
94 if real_ip
!= current_ip
{
95 println!("IP addresses don't match for domain {}: real = {}, dns = {}. Renewing DNS...", domain
, real_ip
, current_ip
);
96 update_record_ip(api_key
, domain
, fqdn
, real_ip
, ttl
)?
;
97 println!("Renewing of {} successfully", domain
);
104 fn get_real_ip() -> Result
<Ipv4Addr
> {
106 let url
= "https://api.ipify.org";
107 let client
= reqwest
::blocking
::Client
::new();
109 match client
.get(url
).send() {
111 if resp
.status().is_success() {
112 let content
= resp
.text().unwrap();
113 match content
.parse
::<IpAddr
>() {
114 Ok(IpAddr
::V4(ip_v4
)) => Ok(ip_v4
),
115 _
=> Err(Box
::new(Error
{ message
: String
::from("Can't parse IPv4 from ipify") }))
118 Err(Box
::new(Error
{ message
: format!("Request unsuccessful: {:#?}", resp
) }))
122 Err(Box
::new(Error
{ message
: format!("Error during request: {:?}", error
) }))
132 fn request_livedns_gandi(api_key
: &str, url_fragment
: &str, method
: Method
) -> Result
<Value
> {
133 let url
= format!("https://api.gandi.net/v5/livedns/{}", url_fragment
);
134 let client
= reqwest
::blocking
::Client
::new();
136 let request_builder
=
138 Method
::Put(body
) => client
.put(url
).body(body
),
139 Method
::Get
=> client
.get(url
)
142 match request_builder
.header("Authorization", format!("Apikey {}", api_key
)).send() {
144 if resp
.status().is_success() {
145 let content
= resp
.text().unwrap();
146 Ok(serde_json
::from_str(&content
).unwrap())
148 Err(Box
::new(Error
{ message
: format!("Request unsuccessful: {:#?}", resp
) }))
151 Err(Box
::new(Error
{ message
: format!("Error during request: {:?}", error
) }))
155 fn get_current_record_ip(api_key
: &str, name
: &str, fqdn
: &str) -> Result
<Ipv4Addr
> {
156 let json_value
= request_livedns_gandi(api_key
, &format!("domains/{}/records/{}/A", fqdn
, name
), Method
::Get
)?
;
158 match &json_value
["rrset_values"][0] {
159 Value
::String(ip_str
) =>
162 Result
::Err(Box
::new(Error
{ message
: format!("Unable to extract the IP from the JSON answer: {}", json_value
) }))
166 fn update_record_ip(api_key
: &str, name
: &str, fqdn
: &str, ip
: Ipv4Addr
, ttl
: i32) -> Result
<()> {
170 "rrset_values": [ format!("{}", ip
) ],
175 let json_value
= request_livedns_gandi(api_key
, &format!("domains/{}/records/{}/A", fqdn
, name
), Method
::Put(json_body
.to_string()))?
;
177 println!("Update response: {}", json_value
);