diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.rs | 39 | ||||
| -rw-r--r-- | src/dyndns_service.rs | 2 | ||||
| -rw-r--r-- | src/error.rs | 96 | ||||
| -rw-r--r-- | src/ip_service.rs | 37 | ||||
| -rw-r--r-- | src/main.rs | 40 |
5 files changed, 204 insertions, 10 deletions
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(()) } |
