139 lines
3.9 KiB
Rust
139 lines
3.9 KiB
Rust
use anyhow::{Context, Result};
|
|
use log::{error, info};
|
|
use reqwest::Client;
|
|
use serde::{self, Deserialize, Serialize};
|
|
use std::{
|
|
collections::HashMap,
|
|
net::{IpAddr, Ipv4Addr},
|
|
sync::Arc,
|
|
};
|
|
|
|
use crate::get_current_public_ipv4;
|
|
|
|
pub struct CloudflareClient {
|
|
client: Client,
|
|
domains: Vec<Arc<str>>,
|
|
current_ip: Ipv4Addr,
|
|
api_key: Box<str>,
|
|
zone_id: Box<str>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
struct DnsRecord {
|
|
id: String,
|
|
#[serde(rename = "type")]
|
|
record_type: Box<str>,
|
|
name: Box<str>,
|
|
content: Box<str>,
|
|
ttl: u32,
|
|
proxied: bool,
|
|
locked: bool,
|
|
zone_id: Box<str>,
|
|
zone_name: Box<str>,
|
|
modified_on: Box<str>,
|
|
created_on: Box<str>,
|
|
meta: HashMap<Box<str>, serde_json::Value>,
|
|
}
|
|
|
|
impl CloudflareClient {
|
|
pub async fn new(api_key: Box<str>, zone_id: Box<str>, domains: Vec<Arc<str>>) -> Result<Self> {
|
|
let force_ipv4 = IpAddr::from([0, 0, 0, 0]);
|
|
let client = reqwest::ClientBuilder::new()
|
|
.local_address(force_ipv4)
|
|
.build()?;
|
|
|
|
let current_ip = get_current_public_ipv4(&client).await?;
|
|
|
|
Ok(Self {
|
|
client,
|
|
domains,
|
|
current_ip,
|
|
api_key,
|
|
zone_id,
|
|
})
|
|
}
|
|
|
|
pub async fn check(&mut self) -> Result<()> {
|
|
let new_ip = get_current_public_ipv4(&self.client).await?;
|
|
if new_ip == self.current_ip {
|
|
return Ok(());
|
|
}
|
|
self.update_dns_records(new_ip).await;
|
|
self.current_ip = new_ip;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn update_dns_records(&self, new_ip: Ipv4Addr) {
|
|
for domain in &self.domains {
|
|
let records = match self.get_dns_records(domain).await {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
error!(
|
|
"Could not getch dns records for domain '{}': {:?}",
|
|
&domain, &e
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let new_ip_s = new_ip.to_string().into_boxed_str();
|
|
for mut record in records.into_iter() {
|
|
if record.content == new_ip_s {
|
|
continue;
|
|
}
|
|
info!(
|
|
"On {}, updating {}: '{}' -> '{}'",
|
|
&domain, record.name, record.content, &new_ip_s
|
|
);
|
|
if let Err(e) = self.update_dns_record(&mut record, new_ip_s.clone()).await {
|
|
error!(
|
|
"On {}, failed to update {}: '{}' -> '{}': {:?}",
|
|
&domain, record.name, record.content, &new_ip_s, &e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn get_dns_records(&self, domain: &str) -> Result<Vec<DnsRecord>> {
|
|
let url = format!(
|
|
"https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A&name={}",
|
|
self.zone_id, domain
|
|
);
|
|
let mut response = self
|
|
.client
|
|
.get(&url)
|
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
|
.header("Content-Type", "application/json")
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json::<HashMap<String, Vec<DnsRecord>>>()
|
|
.await?;
|
|
|
|
response
|
|
.remove("result")
|
|
.context("Key result not in return")
|
|
}
|
|
|
|
async fn update_dns_record(&self, record: &mut DnsRecord, ip_content: Box<str>) -> Result<()> {
|
|
let url = format!(
|
|
"https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}",
|
|
self.zone_id, record.id
|
|
);
|
|
|
|
record.content = ip_content;
|
|
self.client
|
|
.put(&url)
|
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
|
.header("Content-Type", "application/json")
|
|
.json(&record)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|