Correct syntax formatting
[gandi_dns_update.git] / src / main.rs
1 /*
2 * API Reference: https://api.gandi.net/docs/livedns/
3 *
4 * Some similar implementations:
5 * - https://github.com/rmarchant/gandi-ddns/blob/master/gandi_ddns.py
6 * - https://github.com/brianhp2/gandi-automatic-dns
7 *
8 */
9
10 #![cfg_attr(debug_assertions, allow(unused_variables, unused_imports, dead_code))]
11
12 use std::{
13 net::{IpAddr, Ipv4Addr},
14 thread, time,
15 };
16
17 use serde_json::{json, Value};
18
19 use crate::{
20 config::Config,
21 error::{Error, Result},
22 };
23
24 mod config;
25 mod error;
26
27 const FILE_CONF: &str = "config.ron";
28
29 fn main() -> Result<()> {
30 println!("=== GANDI LiveDNS updater ===");
31
32 let config = Config::read(FILE_CONF)?;
33
34 println!(
35 "Configuration: {:?}",
36 Config {
37 api_key: String::from("*****"),
38 ..config.clone()
39 }
40 );
41
42 loop {
43 let time_beginning_loop = time::Instant::now();
44
45 if let Err(err) = check_and_update_dns(&config.api_key, &config.domains, config.ttl) {
46 println!("!! {}", err);
47 }
48
49 let elapsed = time::Instant::now() - time_beginning_loop;
50
51 if elapsed < config.delay_between_check {
52 let to_wait = config.delay_between_check - elapsed;
53 thread::sleep(to_wait);
54 }
55 }
56 }
57
58 fn check_and_update_dns(api_key: &str, domains: &Vec<(String, String)>, ttl: i32) -> Result<()> {
59 let real_ip = get_real_ip()?;
60
61 for (hostname, domain) in domains {
62 let current_ip = get_current_record_ip(api_key, hostname, domain)?;
63
64 if real_ip != current_ip {
65 println!(
66 "IP addresses don't match for domain {}: real = {}, dns = {}. Renewing DNS...",
67 domain, real_ip, current_ip
68 );
69 update_record_ip(api_key, hostname, domain, real_ip, ttl)?;
70 println!("Renewing of {}.{} successfully", hostname, domain);
71 }
72 }
73
74 Ok(())
75 }
76
77 fn get_real_ip() -> Result<Ipv4Addr> {
78 let url = "https://api.ipify.org";
79 let client = reqwest::blocking::Client::new();
80
81 match client.get(url).send() {
82 Ok(resp) => {
83 if resp.status().is_success() {
84 let content = resp.text().unwrap();
85 match content.parse() {
86 Ok(IpAddr::V4(ip_v4)) => Ok(ip_v4),
87 _ => Err(Box::new(Error {
88 message: String::from("Can't parse IPv4 from ipify"),
89 })),
90 }
91 } else {
92 Err(Box::new(Error {
93 message: format!("Request unsuccessful to {}: {:#?}", url, resp),
94 }))
95 }
96 }
97 Err(error) => Err(Box::new(Error {
98 message: format!("Error during request to {}: {:?}", url, error),
99 })),
100 }
101 }
102
103 fn get_current_record_ip(api_key: &str, name: &str, fqdn: &str) -> Result<Ipv4Addr> {
104 let json_value = request_livedns_gandi(
105 api_key,
106 &format!("domains/{}/records/{}/A", fqdn, name),
107 Method::Get,
108 )?;
109
110 match &json_value["rrset_values"][0] {
111 Value::String(ip_str) => Ok(ip_str.parse()?),
112 _ => Result::Err(Box::new(Error {
113 message: format!(
114 "Unable to extract the IP from the JSON answer: {}",
115 json_value
116 ),
117 })),
118 }
119 }
120
121 fn update_record_ip(api_key: &str, name: &str, fqdn: &str, ip: Ipv4Addr, ttl: i32) -> Result<()> {
122 let json_body = json!(
123 {
124 "rrset_values": [ format!("{}", ip) ],
125 "rrset_ttl": ttl
126 }
127 );
128
129 let json_value = request_livedns_gandi(
130 api_key,
131 &format!("domains/{}/records/{}/A", fqdn, name),
132 Method::Put(json_body.to_string()),
133 )?;
134
135 println!("Update response: {}", json_value);
136
137 Ok(())
138 }
139
140 enum Method {
141 Put(String),
142 Get,
143 }
144
145 fn request_livedns_gandi(api_key: &str, url_fragment: &str, method: Method) -> Result<Value> {
146 let url = format!("https://api.gandi.net/v5/livedns/{}", url_fragment);
147 let client = reqwest::blocking::Client::new();
148
149 let request_builder = match method {
150 Method::Put(body) => client.put(&url).body(body),
151 Method::Get => client.get(&url),
152 };
153
154 match request_builder
155 .header("Authorization", format!("Apikey {}", api_key))
156 .send()
157 {
158 Ok(resp) => {
159 if resp.status().is_success() {
160 let content = resp.text().unwrap();
161 Ok(serde_json::from_str(&content).unwrap())
162 } else {
163 Err(Box::new(Error {
164 message: format!("Request unsuccessful to {}: {:#?}", &url, resp),
165 }))
166 }
167 }
168 Err(error) => Err(Box::new(Error {
169 message: format!("Error during request to {}: {:?}", &url, error),
170 })),
171 }
172 }