//! Server config use super::*; use std::{ net::SocketAddr, path::Path, io, borrow::Cow, num::NonZeroU64, error, fmt, }; use tokio::{ fs::OpenOptions, prelude::*, time::Duration, io::BufReader, }; use ipfilt::IpFilter; pub const DEFAULT_FILE_LOCATION: &'static str = "markov.toml"; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash, Serialize, Deserialize)] pub struct Config { pub bindpoint: String, pub file: String, pub max_content_length: u64, pub max_gen_size: usize, pub save_interval_secs: Option, pub trust_x_forwarded_for: bool, #[serde(default)] pub feed_bounds: String, #[serde(default)] pub filter: FilterConfig, #[serde(default)] pub writer: WriterConfig, #[serde(default)] pub mask: IpFilter, } #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Hash, Serialize, Deserialize)] pub struct FilterConfig { #[serde(default)] inbound: String, #[serde(default)] outbound: String, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Hash, Serialize, Deserialize)] pub struct WriterConfig { pub backlog: usize, pub internal_backlog: usize, pub capacity: usize, pub timeout_ms: Option, pub throttle_ms: Option, } impl Default for WriterConfig { #[inline] fn default() -> Self { Self { backlog: 32, internal_backlog: 8, capacity: 4, timeout_ms: None, throttle_ms: None, } } } impl WriterConfig { fn create_settings(self, bounds: range::DynRange) -> handle::Settings { handle::Settings{ backlog: self.backlog, internal_backlog: self.internal_backlog, capacity: self.capacity, timeout: self.timeout_ms.map(tokio::time::Duration::from_millis).unwrap_or(handle::DEFAULT_TIMEOUT), throttle: self.throttle_ms.map(tokio::time::Duration::from_millis), bounds, } } } impl FilterConfig { fn get_inbound_filter(&self) -> sanitise::filter::Filter { let filt: sanitise::filter::Filter = self.inbound.parse().unwrap(); if !filt.is_empty() { info!("Loaded inbound filter: {:?}", filt.iter().collect::()); } filt } fn get_outbound_filter(&self) -> sanitise::filter::Filter { let filt: sanitise::filter::Filter = self.outbound.parse().unwrap(); if !filt.is_empty() { info!("Loaded outbound filter: {:?}", filt.iter().collect::()); } filt } } impl Default for Config { #[inline] fn default() -> Self { Self { bindpoint: SocketAddr::from(([127,0,0,1], 8001)).to_string(), file: "chain.dat".to_owned(), max_content_length: 1024 * 1024 * 4, max_gen_size: 256, save_interval_secs: Some(unsafe{NonZeroU64::new_unchecked(2)}), trust_x_forwarded_for: false, filter: Default::default(), feed_bounds: "2..".to_owned(), writer: Default::default(), mask: Default::default(), } } } impl Config { /// Try to generate a config cache for this instance. pub fn try_gen_cache(&self) -> Result { macro_rules! section { ($name:literal, $expr:expr) => { match $expr { Ok(v) => Ok(v), Err(e) => Err(InvalidConfigError($name, Box::new(e))), } } } use std::ops::RangeBounds; let feed_bounds = section!("feed_bounds", self.parse_feed_bounds()).and_then(|bounds| if bounds.contains(&0) { Err(InvalidConfigError("feed_bounds", Box::new(opaque_error!("Bounds not allowed to contains 0 (they were `{}`)", bounds)))) } else { Ok(bounds) })?; Ok(Cache { handler_settings: self.writer.create_settings(feed_bounds.clone()), feed_bounds, inbound_filter: self.filter.get_inbound_filter(), outbound_filter: self.filter.get_outbound_filter(), }) } /// Try to parse the `feed_bounds` fn parse_feed_bounds(&self) -> Result, range::ParseError> { if self.feed_bounds.len() == 0 { Ok(feed::DEFAULT_FEED_BOUNDS.into()) } else { self.feed_bounds.parse() } } pub fn save_interval(&self) -> Option { self.save_interval_secs.map(|x| Duration::from_secs(x.into())) } pub async fn load(from: impl AsRef) -> io::Result { let file = OpenOptions::new() .read(true) .open(from).await?; let mut buffer= String::new(); let reader = BufReader::new(file); let mut lines = reader.lines(); while let Some(line) = lines.next_line().await? { buffer.push_str(&line[..]); buffer.push('\n'); } toml::de::from_str(&buffer[..]).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e)) } pub async fn save(&self, to: impl AsRef) -> io::Result<()> { let config = toml::ser::to_string_pretty(self).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; let mut file = OpenOptions::new() .write(true) .create(true) .truncate(true) .open(to).await?; file.write_all(config.as_bytes()).await?; file.shutdown().await?; Ok(()) } } /// Try to load config file specified by args, or default config file pub fn load() -> impl futures::future::Future> { load_args(std::env::args().skip(1)) } async fn load_args>(mut from: I) -> Option { let place = if let Some(arg) = from.next() { trace!("File {:?} provided", arg); Cow::Owned(arg) } else { warn!("No config file provided. Using default location {:?}", DEFAULT_FILE_LOCATION); Cow::Borrowed(DEFAULT_FILE_LOCATION) }; match Config::load(place.as_ref()).await { Ok(cfg) => { info!("Loaded config file {:?}", place); Some(cfg) }, Err(err) => { error!("Failed to load config file from {:?}: {}", place, err); None }, } } #[derive(Debug)] pub struct InvalidConfigError(&'static str, Box); impl InvalidConfigError { pub fn field(&self) -> &str { &self.0[..] } } impl error::Error for InvalidConfigError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { Some(self.1.as_ref()) } } impl fmt::Display for InvalidConfigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f,"failed to parse field `{}`: {}", self.0, self.1) } } /// Caches some parsed config arguments #[derive(Clone, PartialEq)] pub struct Cache { pub feed_bounds: range::DynRange, pub inbound_filter: sanitise::filter::Filter, pub outbound_filter: sanitise::filter::Filter, pub handler_settings: handle::Settings, } impl fmt::Debug for Cache { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Cache") .field("feed_bounds", &self.feed_bounds) .field("inbound_filter", &self.inbound_filter.iter().collect::()) .field("outbound_filter", &self.outbound_filter.iter().collect::()) .field("handler_settings", &self.handler_settings) .finish() } }