feat: add config file to webnsupdate
Move flags to config file, and add more options. Mirror some in the module.
This commit is contained in:
parent
3d660314cf
commit
316f2bf576
17 changed files with 641 additions and 532 deletions
253
src/config.rs
Normal file
253
src/config.rs
Normal file
|
@ -0,0 +1,253 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use axum_client_ip::SecureClientIpSource;
|
||||
use miette::{Context, IntoDiagnostic};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, serde::Deserialize, serde::Serialize)]
|
||||
pub enum IpType {
|
||||
#[default]
|
||||
Both,
|
||||
Ipv4Only,
|
||||
Ipv6Only,
|
||||
}
|
||||
|
||||
impl IpType {
|
||||
pub fn valid_for_type(self, ip: IpAddr) -> bool {
|
||||
match self {
|
||||
IpType::Both => true,
|
||||
IpType::Ipv4Only => ip.is_ipv4(),
|
||||
IpType::Ipv6Only => ip.is_ipv6(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IpType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
IpType::Both => f.write_str("both"),
|
||||
IpType::Ipv4Only => f.write_str("ipv4-only"),
|
||||
IpType::Ipv6Only => f.write_str("ipv6-only"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for IpType {
|
||||
type Err = miette::Error;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match s {
|
||||
"both" => Ok(Self::Both),
|
||||
"ipv4-only" => Ok(Self::Ipv4Only),
|
||||
"ipv6-only" => Ok(Self::Ipv6Only),
|
||||
_ => miette::bail!("expected one of 'ipv4-only', 'ipv6-only' or 'both', got '{s}'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Webserver settings
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Server {
|
||||
/// Ip address and port of the server
|
||||
#[serde(default = "default_address")]
|
||||
pub address: SocketAddr,
|
||||
}
|
||||
|
||||
/// Password settings
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Password {
|
||||
/// File containing password to match against
|
||||
///
|
||||
/// Should be of the format `username:password` and contain a single password
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub password_file: Option<PathBuf>,
|
||||
|
||||
/// Salt to get more unique hashed passwords and prevent table based attacks
|
||||
#[serde(default = "default_salt")]
|
||||
pub salt: Box<str>,
|
||||
}
|
||||
|
||||
/// Records settings
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Records {
|
||||
/// Time To Live (in seconds) to set on the DNS records
|
||||
#[serde(
|
||||
default = "default_ttl",
|
||||
serialize_with = "humantime_ser",
|
||||
deserialize_with = "humantime_de"
|
||||
)]
|
||||
pub ttl: humantime::Duration,
|
||||
|
||||
/// List of domain names for which to update the IP when an update is requested
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub records: Vec<Box<str>>,
|
||||
|
||||
/// If provided, when an IPv6 prefix is provided with an update, this will be used to derive
|
||||
/// the full IPv6 address of the client
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub client_id: Option<Ipv6Addr>,
|
||||
|
||||
/// If a client id is provided the ipv6 update will be ignored (only the prefix will be used).
|
||||
/// This domain will point to the ipv6 address instead of the address derived from the client
|
||||
/// id (usually this is the router).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub router_domain: Option<Box<str>>,
|
||||
|
||||
/// Set client IP source
|
||||
///
|
||||
/// see: <https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html>
|
||||
#[serde(default = "default_ip_source")]
|
||||
pub ip_source: SecureClientIpSource,
|
||||
|
||||
/// Set which IPs to allow updating (ipv4, ipv6 or both)
|
||||
#[serde(default = "default_ip_type")]
|
||||
pub ip_type: IpType,
|
||||
|
||||
/// Keyfile `nsupdate` should use
|
||||
///
|
||||
/// If specified, then `webnsupdate` must have read access to the file
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub key_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Config {
|
||||
/// Server Configuration
|
||||
#[serde(flatten)]
|
||||
pub server: Server,
|
||||
|
||||
/// Password Configuration
|
||||
#[serde(flatten)]
|
||||
pub password: Password,
|
||||
|
||||
/// Records Configuration
|
||||
#[serde(flatten)]
|
||||
pub records: Records,
|
||||
|
||||
/// The config schema (used for lsp completions)
|
||||
#[serde(default, rename = "$schema", skip_serializing)]
|
||||
pub _schema: serde::de::IgnoredAny,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load the configuration without verifying it
|
||||
pub fn load(path: &std::path::Path) -> miette::Result<Self> {
|
||||
serde_json::from_reader::<File, Self>(
|
||||
File::open(path)
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("failed open {}", path.display()))?,
|
||||
)
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("failed to load configuration from {}", path.display()))
|
||||
}
|
||||
|
||||
/// Ensure only a verified configuration is returned
|
||||
pub fn verified(self) -> miette::Result<Self> {
|
||||
self.verify()?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Verify the configuration
|
||||
pub fn verify(&self) -> Result<(), Invalid> {
|
||||
let mut invalid_records: Vec<miette::Error> = self
|
||||
.records
|
||||
.records
|
||||
.iter()
|
||||
.filter_map(|record| crate::records::validate_record_str(record).err())
|
||||
.collect();
|
||||
|
||||
invalid_records.extend(
|
||||
self.records
|
||||
.router_domain
|
||||
.as_ref()
|
||||
.and_then(|domain| crate::records::validate_record_str(domain).err()),
|
||||
);
|
||||
|
||||
let err = Invalid { invalid_records };
|
||||
|
||||
if err.invalid_records.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||
#[error("the configuration was invalid")]
|
||||
pub struct Invalid {
|
||||
#[related]
|
||||
pub invalid_records: Vec<miette::Error>,
|
||||
}
|
||||
|
||||
// --- Default Values (sadly serde doesn't have a way to specify a constant as a default value) ---
|
||||
|
||||
fn default_ttl() -> humantime::Duration {
|
||||
super::DEFAULT_TTL.into()
|
||||
}
|
||||
|
||||
fn default_salt() -> Box<str> {
|
||||
super::DEFAULT_SALT.into()
|
||||
}
|
||||
|
||||
fn default_address() -> SocketAddr {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5353)
|
||||
}
|
||||
|
||||
fn default_ip_source() -> SecureClientIpSource {
|
||||
SecureClientIpSource::RightmostXForwardedFor
|
||||
}
|
||||
|
||||
fn default_ip_type() -> IpType {
|
||||
IpType::Both
|
||||
}
|
||||
|
||||
fn humantime_de<'de, D>(de: D) -> Result<humantime::Duration, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct Visitor;
|
||||
impl serde::de::Visitor<'_> for Visitor {
|
||||
type Value = humantime::Duration;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "a duration (e.g. 5s)")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
v.parse().map_err(E::custom)
|
||||
}
|
||||
}
|
||||
de.deserialize_str(Visitor)
|
||||
}
|
||||
|
||||
fn humantime_ser<S>(duration: &humantime::Duration, ser: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
ser.serialize_str(&duration.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_values_config_snapshot() {
|
||||
let config: Config = serde_json::from_str("{}").unwrap();
|
||||
insta::assert_json_snapshot!(config, @r#"
|
||||
{
|
||||
"address": "127.0.0.1:5353",
|
||||
"salt": "UpdateMyDNS",
|
||||
"ttl": {
|
||||
"secs": 60,
|
||||
"nanos": 0
|
||||
},
|
||||
"ip_source": "RightmostXForwardedFor",
|
||||
"ip_type": "Both"
|
||||
}
|
||||
"#);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue