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
::{ net
::{ IpAddr
, Ipv4Addr
}, thread
, time
};
15 use serde_json
::{ Value
, json
};
20 use crate::error
::{ Result
, Error
};
21 use crate::config
::Config
;
23 const FILE_CONF
: &str = "config.ron";
25 fn main() -> Result
<()> {
27 println!("GANDI DynDNS");
29 let config
= Config
::read(FILE_CONF
)?
;
31 println!("Configuration: {:?}", Config
{ api_key
: String
::from("*****"), ..config
.clone() });
34 let time_beginning_loop
= time
::Instant
::now();
36 if let Err(err
) = check_and_update_dns(&config
.api_key
, &config
.fqdn
, &config
.domains
, config
.ttl
) {
37 println!("!! Error: {}", err
);
40 let elapsed
= time
::Instant
::now() - time_beginning_loop
;
42 if elapsed
< config
.delay_between_check
{
43 let to_wait
= config
.delay_between_check
- elapsed
;
44 thread
::sleep(to_wait
);
49 fn check_and_update_dns(api_key
: &str, fqdn
: &str, domains
: &Vec
<String
>, ttl
: i32) -> Result
<()> {
50 let real_ip
= get_real_ip()?
;
52 for domain
in domains
{
53 let current_ip
= get_current_record_ip(api_key
, domain
, fqdn
)?
;
55 if real_ip
!= current_ip
{
56 println!("IP addresses don't match for domain {}: real = {}, dns = {}. Renewing DNS...", domain
, real_ip
, current_ip
);
57 update_record_ip(api_key
, domain
, fqdn
, real_ip
, ttl
)?
;
58 println!("Renewing of {} successfully", domain
);
65 fn get_real_ip() -> Result
<Ipv4Addr
> {
66 let url
= "https://api.ipify.org";
67 let client
= reqwest
::blocking
::Client
::new();
69 match client
.get(url
).send() {
71 if resp
.status().is_success() {
72 let content
= resp
.text().unwrap();
73 match content
.parse() {
74 Ok(IpAddr
::V4(ip_v4
)) => Ok(ip_v4
),
75 _
=> Err(Box
::new(Error
{ message
: String
::from("Can't parse IPv4 from ipify") }))
78 Err(Box
::new(Error
{ message
: format!("Request unsuccessful: {:#?}", resp
) }))
82 Err(Box
::new(Error
{ message
: format!("Error during request: {:?}", error
) }))
87 fn get_current_record_ip(api_key
: &str, name
: &str, fqdn
: &str) -> Result
<Ipv4Addr
> {
88 let json_value
= request_livedns_gandi(api_key
, &format!("domains/{}/records/{}/A", fqdn
, name
), Method
::Get
)?
;
90 match &json_value
["rrset_values"][0] {
91 Value
::String(ip_str
) =>
94 Result
::Err(Box
::new(Error
{ message
: format!("Unable to extract the IP from the JSON answer: {}", json_value
) }))
98 fn update_record_ip(api_key
: &str, name
: &str, fqdn
: &str, ip
: Ipv4Addr
, ttl
: i32) -> Result
<()> {
102 "rrset_values": [ format!("{}", ip
) ],
107 let json_value
= request_livedns_gandi(api_key
, &format!("domains/{}/records/{}/A", fqdn
, name
), Method
::Put(json_body
.to_string()))?
;
109 println!("Update response: {}", json_value
);
119 fn request_livedns_gandi(api_key
: &str, url_fragment
: &str, method
: Method
) -> Result
<Value
> {
120 let url
= format!("https://api.gandi.net/v5/livedns/{}", url_fragment
);
121 let client
= reqwest
::blocking
::Client
::new();
123 let request_builder
=
125 Method
::Put(body
) => client
.put(url
).body(body
),
126 Method
::Get
=> client
.get(url
)
129 match request_builder
.header("Authorization", format!("Apikey {}", api_key
)).send() {
131 if resp
.status().is_success() {
132 let content
= resp
.text().unwrap();
133 Ok(serde_json
::from_str(&content
).unwrap())
135 Err(Box
::new(Error
{ message
: format!("Request unsuccessful: {:#?}", resp
) }))
138 Err(Box
::new(Error
{ message
: format!("Error during request: {:?}", error
) }))