aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock173
-rw-r--r--Cargo.toml5
-rw-r--r--src/config.rs39
-rw-r--r--src/dyndns_service.rs2
-rw-r--r--src/error.rs96
-rw-r--r--src/ip_service.rs37
-rw-r--r--src/main.rs40
-rw-r--r--test/full-config.json12
8 files changed, 391 insertions, 13 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2d47202..526f328 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -42,6 +42,56 @@ dependencies = [
]
[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+dependencies = [
+ "anstyle",
+ "once_cell",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -142,6 +192,52 @@ dependencies = [
]
[[package]]
+name = "clap"
+version = "4.5.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -241,6 +337,29 @@ dependencies = [
]
[[package]]
+name = "env_filter"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -754,12 +873,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
+name = "jiff"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -829,8 +978,11 @@ dependencies = [
name = "multiwan-dyndns"
version = "0.1.0"
dependencies = [
+ "clap",
+ "env_logger",
"fqdn",
"http",
+ "log",
"reqwest",
"serde",
"serde_json",
@@ -966,6 +1118,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
+name = "portable-atomic"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
name = "potential_utf"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1589,6 +1756,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 399d42e..4b0da6c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,12 +9,15 @@ license = "MIT OR Apache-2.0"
[dependencies]
reqwest = "0.12.15"
tokio = "1.45.0"
-http = "1.3.1"
+http = { version = "1.3.1" }
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"] }
+env_logger = "0.11.8"
+clap = { version = "4.5.38", features = ["derive"] }
+log = "0.4.27"
[dev-dependencies]
wiremock = "0.6.3"
diff --git a/src/config.rs b/src/config.rs
index 9230f38..f098838 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,22 +1,47 @@
+use std::{fs, io};
+use std::path::{Path, PathBuf};
use crate::dyndns_service::DynDnsProvider;
use fqdn::FQDN;
use serde::{Deserialize, Serialize};
use serde_with::DisplayFromStr;
use serde_with::serde_as;
use strum::Display;
+use crate::error::{AppError, AppResult};
+use crate::ip_service::IpServiceProvider;
#[derive(Debug, Deserialize, Serialize)]
#[serde(transparent)]
-struct Config {
- wans: Vec<WanConfig>
+pub struct Config {
+ pub networks: Vec<WanConfig>
+}
+
+impl Config {
+
+ pub fn load<P: AsRef<Path>>(path: P) -> AppResult<Config> {
+ let path = path.as_ref();
+ let content = fs::read_to_string(path)
+ .map_err(|e| match e.kind() {
+ io::ErrorKind::NotFound => AppError::FileNotFound(path.to_path_buf()),
+ _ => AppError::IoError(e),
+ })?;
+
+ serde_json::from_str(&content)
+ .map_err(|e| AppError::ConfigParseError {
+ source: e,
+ path: path.to_path_buf(),
+ })
+ }
+
}
#[derive(Debug, Deserialize, Serialize)]
-struct WanConfig {
- interface: Option<String>,
+pub struct WanConfig {
+ pub interface: Option<String>,
#[serde(flatten)]
- dns_record: DnsRecord,
- providers: Vec<DynDnsProvider>,
+ pub dns_record: DnsRecord,
+ pub providers: Vec<DynDnsProvider>,
+ #[serde(default)]
+ pub ip_service: IpServiceProvider
}
#[derive(Debug, Display, Deserialize, Serialize)]
@@ -107,7 +132,7 @@ mod tests {
let reader = BufReader::new(file);
let actual: Config = serde_json::from_reader(reader)?;
- assert_eq!(actual.wans.len(), 2);
+ assert_eq!(actual.networks.len(), 2);
}
Ok(())
}
diff --git a/src/dyndns_service.rs b/src/dyndns_service.rs
index fac34e7..d1449f5 100644
--- a/src/dyndns_service.rs
+++ b/src/dyndns_service.rs
@@ -11,7 +11,7 @@ pub enum DynDnsProvider {
GANDI(Gandi)
}
-struct DynDnsService {}
+pub struct DynDnsService {}
impl DynDnsService {
pub async fn register(config: &impl DynDnsServiceConfiguration) -> Result<(), Box<dyn Error>> {
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..d7bbfaf
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,96 @@
+use std::fmt;
+use std::io;
+use std::path::PathBuf;
+use reqwest::Error as ReqwestError;
+use serde_json::Error as JsonError;
+
+pub type AppResult<T> = Result<T, AppError>;
+#[derive(Debug)]
+pub enum AppError {
+ FileNotFound(PathBuf),
+ IoError(io::Error),
+ ConfigParseError { source: JsonError, path: PathBuf },
+ RequestFailed { url: String, source: ReqwestError },
+ InvalidResponse { url: String, reason: String },
+ Timeout { url: String },
+}
+
+impl AppError {
+ pub fn with_path(self, path: impl Into<PathBuf>) -> Self {
+ match self {
+ Self::ConfigParseError { source, .. } => Self::ConfigParseError {
+ source,
+ path: path.into(),
+ },
+ err => err,
+ }
+ }
+
+ pub fn with_url(self, url: impl Into<String>) -> Self {
+ match self {
+ Self::RequestFailed { source, .. } => Self::RequestFailed {
+ source,
+ url: url.into(),
+ },
+ Self::InvalidResponse { reason, .. } => Self::InvalidResponse {
+ reason,
+ url: url.into(),
+ },
+ Self::Timeout { .. } => Self::Timeout {
+ url: url.into(),
+ },
+ err => err,
+ }
+ }
+}
+
+impl fmt::Display for AppError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::FileNotFound(path) => write!(f, "File not found: {}", path.display()),
+ Self::IoError(err) => write!(f, "I/O error: {}", err),
+ Self::ConfigParseError { path, .. } => write!(f, "Failed to parse config at {}", path.display()),
+ Self::RequestFailed { url, .. } => write!(f, "Request to {} failed", url),
+ Self::InvalidResponse { url, reason } => {
+ write!(f, "Invalid response from {}: {}", url, reason)
+ }
+ Self::Timeout { url } => write!(f, "Request to {} timed out", url),
+ }
+ }
+}
+
+impl std::error::Error for AppError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Self::IoError(err) => Some(err),
+ Self::ConfigParseError { source, .. } => Some(source),
+ Self::RequestFailed { source, .. } => Some(source),
+ _ => None,
+ }
+ }
+}
+
+// Convenient conversions from library errors
+impl From<io::Error> for AppError {
+ fn from(err: io::Error) -> Self {
+ Self::IoError(err)
+ }
+}
+
+impl From<ReqwestError> for AppError {
+ fn from(err: ReqwestError) -> Self {
+ Self::RequestFailed {
+ url: err.url().map_or_else(|| "unknown".to_string(), |u| u.to_string()),
+ source: err,
+ }
+ }
+}
+
+impl From<JsonError> for AppError {
+ fn from(err: JsonError) -> Self {
+ Self::ConfigParseError {
+ source: err,
+ path: PathBuf::from("unknown"), // Default path
+ }
+ }
+}
diff --git a/src/ip_service.rs b/src/ip_service.rs
index 8e2bee8..ee1a092 100644
--- a/src/ip_service.rs
+++ b/src/ip_service.rs
@@ -1,12 +1,28 @@
+use serde_with::DisplayFromStr;
use reqwest::Url;
use std::error::Error;
use std::net::IpAddr;
use std::str::FromStr;
+use serde::{Deserialize, Serialize};
+use serde_with::serde_as;
+use crate::ip_service::IpServiceProvider::IDENTME;
+
+#[derive(Debug, Deserialize, PartialEq, Serialize)]
+#[serde(tag = "type")]
+pub enum IpServiceProvider {
+ IDENTME(IdentMe)
+}
+
+impl Default for IpServiceProvider {
+ fn default() -> Self {
+ IDENTME(IdentMe::default())
+ }
+}
pub struct IpService {}
impl IpService {
- async fn resolve(config: &impl IpServiceConfiguration) -> Result<IpAddr, Box<dyn Error>> {
+ pub(crate) async fn resolve(config: &impl IpServiceConfiguration) -> Result<IpAddr, Box<dyn Error>> {
let response = reqwest::get(config.get_service_url()).await.unwrap();
Ok(IpAddr::from_str(&response.text().await.unwrap())?)
}
@@ -16,6 +32,25 @@ pub trait IpServiceConfiguration {
fn get_service_url(&self) -> Url;
}
+#[serde_as]
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+pub struct IdentMe {
+ #[serde_as(as = "DisplayFromStr")]
+ url: Url
+}
+
+impl Default for IdentMe {
+ fn default() -> Self {
+ Self { url: Url::parse("https://v4.ident.me/").unwrap() }
+ }
+}
+
+impl IpServiceConfiguration for IdentMe {
+ fn get_service_url(&self) -> Url {
+ self.url.clone()
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/main.rs b/src/main.rs
index 62d2f38..7fce580 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,45 @@
+use std::path::{Path, PathBuf};
+use std::process::exit;
+use clap::Parser;
+use log::info;
+use crate::config::Config;
+use crate::dyndns_service::DynDnsService;
+use crate::error::AppResult;
+use crate::ip_service::IpService;
+
mod dyndns_service;
mod ip_service;
mod config;
+mod error;
+
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+struct Args {
+ #[arg(short, long, default_value = "~/.config/multiwan-dyndns/config.json")]
+ config_file: PathBuf,
+}
fn main() {
- println!("Hello, world!");
+ env_logger::init();
+
+ let args = Args::parse();
+ match run(args) {
+ Ok(_) => exit(0),
+ Err(e) => {
+ log::error!("{}", e);
+ exit(1);
+ }
+ }
+}
+
+fn run(args: Args) -> AppResult<()> {
+ let config = Config::load(&args.config_file)?;
+ for network in config.networks {
+ // let ip = IpService::resolve(&network.ip_service)?;
+ // for dyndns in network.providers {
+ // DynDnsService::register(&dyndns, &ip)?;
+ // }
+ }
+
+ Ok(())
}
diff --git a/test/full-config.json b/test/full-config.json
index fab3129..fe70d94 100644
--- a/test/full-config.json
+++ b/test/full-config.json
@@ -9,7 +9,11 @@
"type": "GANDI",
"api_key": "SOME_API_KEY"
}
- ]
+ ],
+ "ip_service": {
+ "type": "IDENTME",
+ "url": "https://v4.ident.me/"
+ }
},
{
"interface": "eth1",
@@ -21,6 +25,10 @@
"type": "GANDI",
"api_key": "SOME_OTHER_API_KEY"
}
- ]
+ ],
+ "ip_service": {
+ "type": "IDENTME",
+ "url": "https://v4.ident.me/"
+ }
}
]