ptth/crates/ptth_relay/src/config.rs

242 lines
5.5 KiB
Rust

// False positive with itertools::process_results
#![allow (clippy::redundant_closure)]
use std::{
collections::{
HashMap,
},
convert::{TryFrom},
net::IpAddr,
path::Path,
str::FromStr,
};
use crate::{
errors::ConfigError,
key_validity::{
ScraperKey,
},
};
/// Machine-editable configs.
/// These are stored in the `data` directory and shouldn't be touched by
/// humans. `ptth_relay` will re-write them while it's running.
pub mod machine_editable {
use std::{
collections::BTreeMap,
path::Path,
};
use serde::{Deserialize, Serialize};
use super::file::Server;
#[derive (Deserialize, Serialize)]
pub struct ConfigFile {
pub servers: Vec <Server>,
}
#[derive (Default)]
pub struct Config {
pub servers: BTreeMap <String, Server>,
}
impl ConfigFile {
pub fn from_file (path: &Path) -> Result <Self, crate::ConfigError>
{
let config_s = std::fs::read_to_string (path)?;
Ok (toml::from_str (&config_s)?)
}
pub async fn save (&self, path: &Path) -> Result <(), crate::ConfigError>
{
let s = toml::to_string (self)?;
// This is way easier in C++ but also not safe
let mut temp_path = path.file_name ().unwrap ().to_os_string ();
temp_path.push (".partial");
let temp_path = path.with_file_name (temp_path);
tokio::fs::write (&temp_path, &s).await?;
tokio::fs::rename (&temp_path, path).await?;
Ok (())
}
}
impl Config {
pub fn from_file (path: &Path) -> Result <Self, crate::ConfigError>
{
let c = ConfigFile::from_file (path)?;
let servers = c.servers.into_iter ()
.map (|s| (s.name.clone (), s))
.collect ();
Ok (Self {
servers,
})
}
pub async fn save (&self, path: &Path) -> Result <(), crate::ConfigError>
{
let servers = self.servers.values ()
.cloned ().into_iter ().collect ();
let c = ConfigFile {
servers,
};
c.save (path).await?;
Ok (())
}
}
}
/// Config fields as they are loaded from the config file
pub mod file {
use serde::{Deserialize, Serialize};
use crate::key_validity::{
BlakeHashWrapper,
ScraperKey,
};
#[derive (Clone, Debug, Deserialize, Serialize)]
pub struct Server {
/// This is duplicated in the hashmap, but it's not a problem
pub name: String,
pub tripcode: BlakeHashWrapper,
/// This allows a relay-side rename of servers
pub display_name: Option <String>,
}
/// Empty
#[derive (Deserialize)]
pub struct DevMode {
}
/// Config fields that are identical in the file and at runtime
#[derive (Default, Deserialize)]
pub struct Isomorphic {
#[serde (default)]
pub enable_scraper_api: bool,
/// If any of the `DevMode` fields are used, we are in dev mode
/// and have to show extra warnings, since auth may be weakened
pub dev_mode: Option <DevMode>,
}
#[derive (Deserialize)]
pub struct Config {
#[serde (flatten)]
pub iso: Isomorphic,
pub address: Option <String>,
pub port: Option <u16>,
pub servers: Option <Vec <Server>>,
// Adding a DB will take a while, so I'm moving these out of dev mode.
pub scraper_keys: Option <Vec <ScraperKey>>,
pub news_url: Option <String>,
pub hide_audit_log: Option <bool>,
}
}
/// Config fields as they are used at runtime
pub struct Config {
pub iso: file::Isomorphic,
pub address: IpAddr,
pub port: Option <u16>,
pub servers: HashMap <String, file::Server>,
pub scraper_keys: HashMap <String, ScraperKey>,
pub news_url: Option <String>,
pub hide_audit_log: bool,
}
impl Default for Config {
fn default () -> Self {
Self {
iso: Default::default (),
address: IpAddr::from ([0, 0, 0, 0]),
port: None,
servers: Default::default (),
scraper_keys: Default::default (),
news_url: None,
hide_audit_log: false,
}
}
}
impl TryFrom <file::Config> for Config {
type Error = ConfigError;
fn try_from (f: file::Config) -> Result <Self, Self::Error> {
let servers = f.servers.unwrap_or_else (|| vec! []);
let servers = servers.into_iter ().map (|server| Ok::<_, ConfigError> ((server.name.clone (), server)));
let servers = itertools::process_results (servers, |i| i.collect ())?;
let scraper_keys = f.scraper_keys.unwrap_or_else (|| vec! []);
let scraper_keys = if f.iso.enable_scraper_api {
scraper_keys.into_iter ().map (|key| (key.hash.encode_base64 (), key)).collect ()
}
else {
Default::default ()
};
Ok (Self {
iso: f.iso,
address: parse_address (f.address.as_ref ().map (|s| &s[..]))?,
port: f.port,
servers,
scraper_keys,
news_url: f.news_url,
hide_audit_log: f.hide_audit_log.unwrap_or (false),
})
}
}
fn parse_address (s: Option <&str>) -> Result <IpAddr, ConfigError> {
Ok (s
.map (|s| IpAddr::from_str (s))
.transpose ().map_err (|_| ConfigError::BadServerAddress)?
.unwrap_or_else (|| IpAddr::from ([0, 0, 0, 0]))
)
}
impl Config {
pub async fn from_file (path: &Path) -> Result <Self, ConfigError> {
let config_s = tokio::fs::read_to_string (path).await?;
let new_config: file::Config = toml::from_str (&config_s)?;
Self::try_from (new_config)
}
}
#[cfg (test)]
mod tests {
use super::*;
#[test]
fn ip_address () {
for (input, expected) in vec! [
(None, Some (IpAddr::from ([0, 0, 0, 0]))),
(Some ("bogus"), None),
(Some ("0.0.0.0"), Some (IpAddr::from ([0, 0, 0, 0]))),
(Some ("0"), None),
(Some ("127.0.0.1"), Some (IpAddr::from ([127, 0, 0, 1]))),
(Some ("10.0.0.1"), Some (IpAddr::from ([10, 0, 0, 1]))),
].into_iter () {
let actual = parse_address (input).ok ();
assert_eq! (actual, expected);
}
}
}