aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
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(())
}