From dd1483cb6d9c060a17dc68357975de2b1ec09c08 Mon Sep 17 00:00:00 2001 From: Gus Power Date: Wed, 14 May 2025 13:21:08 +0100 Subject: reduce size of DnsRecordType (introduce enum), add overall config w/ supporting "full" test file. --- Cargo.lock | 254 ++++++-------------------------------------- Cargo.toml | 3 +- src/config.rs | 115 ++++++++++++++++++++ src/dyndns_service.rs | 12 ++- src/dyndns_service/gandi.rs | 55 ++++++---- src/main.rs | 1 + test/full-config.json | 26 +++++ 7 files changed, 222 insertions(+), 244 deletions(-) create mode 100644 src/config.rs create mode 100644 test/full-config.json diff --git a/Cargo.lock b/Cargo.lock index f5dbdfc..2d47202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,12 +192,6 @@ dependencies = [ "syn", ] -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - [[package]] name = "deadpool" version = "0.10.0" @@ -246,24 +240,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -316,6 +292,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fqdn" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f5d7f7b3eed2f771fc7f6fcb651f9560d7b0c483d75876082acb4649d266b3" +dependencies = [ + "serde", +] + [[package]] name = "futures" version = "0.3.31" @@ -719,16 +704,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -854,13 +829,14 @@ dependencies = [ name = "multiwan-dyndns" version = "0.1.0" dependencies = [ + "fqdn", "http", "reqwest", "serde", "serde_json", "serde_with", + "strum", "tokio", - "trust-dns-client", "wiremock", ] @@ -881,15 +857,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -1013,15 +980,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "proc-macro2" version = "1.0.95" @@ -1046,46 +1004,6 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - [[package]] name = "regex" version = "1.11.1" @@ -1395,6 +1313,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1466,26 +1406,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "time" version = "0.3.41" @@ -1527,21 +1447,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.45.0" @@ -1636,21 +1541,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" version = "0.1.33" @@ -1660,77 +1553,18 @@ dependencies = [ "once_cell", ] -[[package]] -name = "trust-dns-client" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14135e72c7e6d4c9b6902d4437881a8598f0145dbb2e3f86f92dbad845b61e63" -dependencies = [ - "cfg-if", - "data-encoding", - "futures-channel", - "futures-util", - "once_cell", - "radix_trie", - "rand", - "thiserror", - "tokio", - "tracing", - "trust-dns-proto", -] - -[[package]] -name = "trust-dns-proto" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna 0.4.0", - "ipnet", - "once_cell", - "rand", - "smallvec", - "thiserror", - "tinyvec", - "tokio", - "tracing", - "url", -] - [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - [[package]] name = "untrusted" version = "0.9.0" @@ -1744,7 +1578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna", "percent-encoding", ] @@ -2153,26 +1987,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index e94ffb9..399d42e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,11 @@ license = "MIT OR Apache-2.0" reqwest = "0.12.15" tokio = "1.45.0" http = "1.3.1" -trust-dns-client = "0.23.2" serde_json = "1.0.140" serde = { version = "1.0.219", features = ["derive"] } serde_with = "3.12.0" +fqdn = { version = "0.4.6", features = ["serde"] } +strum = { version = "0.27.1", features = ["derive"] } [dev-dependencies] wiremock = "0.6.3" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9230f38 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,115 @@ +use crate::dyndns_service::DynDnsProvider; +use fqdn::FQDN; +use serde::{Deserialize, Serialize}; +use serde_with::DisplayFromStr; +use serde_with::serde_as; +use strum::Display; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(transparent)] +struct Config { + wans: Vec +} + +#[derive(Debug, Deserialize, Serialize)] +struct WanConfig { + interface: Option, + #[serde(flatten)] + dns_record: DnsRecord, + providers: Vec, +} + +#[derive(Debug, Display, Deserialize, Serialize)] +#[derive(PartialEq)] +pub enum DnsRecordType { + A, + AAAA +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize)] +struct DnsRecord { + #[serde_as(as = "DisplayFromStr")] + fqdn: FQDN, + #[serde(default = "default_ttl")] + ttl: u32, + #[serde(default = "default_record_type")] + record_type: DnsRecordType, +} + +fn default_record_type() -> DnsRecordType { + DnsRecordType::A +} +fn default_ttl() -> u32 { + 300 +} + +#[cfg(test)] +mod tests { + use std::error::Error; + use std::fs::{read_dir, File}; + use std::io::BufReader; + use super::*; + use serde_json::json; + use std::str::FromStr; + use crate::dyndns_service::DynDnsProvider::GANDI; + use crate::dyndns_service::gandi::Gandi; + + #[test] + fn check_minimal_config() { + let input = json!({ + "fqdn": "dyn.domain.com", + "providers": [ { + "type": "GANDI", + "api_key": "SOME-API-KEY", + } ] + }); + + let wan_config = serde_json::from_value::(input).unwrap(); + + assert_eq!( + wan_config.dns_record.fqdn, + FQDN::from_str("dyn.domain.com").unwrap() + ); + assert_eq!(wan_config.interface, None); + + assert_eq!(wan_config.providers.len(), 1); + let expected = Gandi::new("SOME-API-KEY".to_string()); + let actual = wan_config.providers.get(0).unwrap(); + assert_eq!(&GANDI(expected), actual); + } + + #[test] + fn check_defaults_on_dns_record_deserialization() { + let input = json!({ + "fqdn": "dyn.mydomain.com", + }); + + let dns_record = serde_json::from_value::(input).unwrap(); + + assert_eq!(dns_record.record_type, default_record_type()); + assert_eq!(dns_record.ttl, default_ttl()); + + assert_eq!(dns_record.fqdn, FQDN::from_str("dyn.mydomain.com").unwrap()); + } + + #[test] + fn check_file_configs() -> Result<(), Box> { + let path = std::path::Path::new("test"); + for entry in read_dir(path)? { + let entry = entry?; + let test_file_path = entry.path(); + if test_file_path.extension().unwrap_or_default() != "json" { + continue; + } + + let file = File::open(test_file_path)?; + let reader = BufReader::new(file); + + let actual: Config = serde_json::from_reader(reader)?; + assert_eq!(actual.wans.len(), 2); + } + Ok(()) + } + +} diff --git a/src/dyndns_service.rs b/src/dyndns_service.rs index 7c903e0..fac34e7 100644 --- a/src/dyndns_service.rs +++ b/src/dyndns_service.rs @@ -1,7 +1,15 @@ -mod gandi; +pub mod gandi; use reqwest::ClientBuilder; use std::error::Error; +use serde::{Deserialize, Serialize}; +use crate::dyndns_service::gandi::Gandi; + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(tag = "type")] +pub enum DynDnsProvider { + GANDI(Gandi) +} struct DynDnsService {} @@ -13,7 +21,7 @@ impl DynDnsService { } } -trait DynDnsServiceConfiguration { +pub trait DynDnsServiceConfiguration { fn get_service_url(&self) -> String; } diff --git a/src/dyndns_service/gandi.rs b/src/dyndns_service/gandi.rs index 72c95ad..03ecf6f 100644 --- a/src/dyndns_service/gandi.rs +++ b/src/dyndns_service/gandi.rs @@ -4,54 +4,67 @@ // --header 'content-type: application/json' \ // --data "{ \"rrset_ttl\": 300, \"rrset_values\": [\"1.2.3.4\"] }" +use crate::dyndns_service::DynDnsServiceConfiguration; +use http::Method; +use serde::{Deserialize, Serialize}; use serde_with::DisplayFromStr; -use serde_with::TryFromInto; use serde_with::serde_as; -use http::Method; -use trust_dns_client::rr::RecordType; -use serde::Deserialize; -use crate::dyndns_service::DynDnsServiceConfiguration; +use crate::config::DnsRecordType; // See https://api.gandi.net/docs/livedns/ for more info - -const API_URL_TEMPLATE: &str = "https://api.gandi.net/api/v5/domains/{domain}/records/{subdomain}/{record_type}"; - #[serde_as] -#[derive(Debug, Deserialize)] -struct Gandi { - domain: String, - subdomain: String, +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct Gandi { + api_key: String, #[serde(default = "Gandi::default_method")] #[serde_as(as = "DisplayFromStr")] method: Method, #[serde(default = "Gandi::default_record_type")] - #[serde_as(as = "DisplayFromStr")] - record_type: RecordType, + record_type: DnsRecordType, } impl Gandi { + pub fn new(api_key: String) -> Self { + Self { + api_key, + method: Self::default_method(), + record_type: Self::default_record_type(), + } + } - fn default_method() -> Method { Method::PUT } - fn default_record_type() -> RecordType { RecordType::A } - + fn default_method() -> Method { + Method::PUT + } + fn default_record_type() -> DnsRecordType { + DnsRecordType::A + } } impl DynDnsServiceConfiguration for Gandi { fn get_service_url(&self) -> String { - format!("https://api.gandi.net/api/v5/domains/{domain}/records/{subdomain}/{record_type}", domain = self.domain, subdomain = self.subdomain, record_type = self.record_type) + format!( + "https://api.gandi.net/api/v5/domains/{domain}/records/{subdomain}/{record_type}", + domain = 'a', + subdomain = 'b', + record_type = self.record_type + ) } } #[cfg(test)] mod tests { - + use serde_json::json; use super::*; #[test] fn check_defaults() { - let gandi = serde_json::from_str::("{ \"domain\":\"mydomain.com\", \"subdomain\":\"dyn\" }").unwrap(); + let input = json!({ + "api_key": "SOME-API-KEY", + }); + + let gandi = serde_json::from_value::(input).unwrap(); - assert_eq!(gandi.record_type, RecordType::A); + assert_eq!(gandi.record_type, DnsRecordType::A); assert_eq!(gandi.method, Method::PUT); } } diff --git a/src/main.rs b/src/main.rs index 1ae22b8..62d2f38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod dyndns_service; mod ip_service; +mod config; fn main() { println!("Hello, world!"); diff --git a/test/full-config.json b/test/full-config.json new file mode 100644 index 0000000..fab3129 --- /dev/null +++ b/test/full-config.json @@ -0,0 +1,26 @@ +[ + { + "interface": "eth0", + "fqdn": "vpn.private-client.com", + "ttl": 600, + "record_type": "A", + "providers": [ + { + "type": "GANDI", + "api_key": "SOME_API_KEY" + } + ] + }, + { + "interface": "eth1", + "fqdn": "vpn2.private-client2.com", + "ttl": 1800, + "record_type": "AAAA", + "providers": [ + { + "type": "GANDI", + "api_key": "SOME_OTHER_API_KEY" + } + ] + } +] -- cgit v1.2.3