use std::io::Write;
use std::path::Path;
use cmr_core::policy::{RoutingPolicy, SecurityLevel};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PeerConfig {
pub local_address: String,
#[serde(default)]
pub security_level: SecurityLevel,
pub listen: ListenConfig,
#[serde(default)]
pub compressor: CompressorConfig,
#[serde(default)]
pub smtp: Option<SmtpConfig>,
#[serde(default)]
pub ssh: SshConfig,
#[serde(default)]
pub policy: Option<RoutingPolicy>,
#[serde(default)]
pub policy_tuning: PolicyTuningConfig,
#[serde(default)]
pub static_keys: Vec<StaticKeyConfig>,
#[serde(default)]
pub prefer_http_handshake: bool,
#[serde(default)]
pub dashboard: DashboardConfig,
#[serde(default)]
pub ambient: AmbientConfig,
}
impl PeerConfig {
pub fn from_toml_str(data: &str) -> Result<Self, ConfigError> {
toml::from_str::<Self>(data).map_err(ConfigError::Toml)
}
pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
let data = std::fs::read_to_string(path).map_err(ConfigError::Io)?;
Self::from_toml_str(&data)
}
#[must_use]
pub fn effective_policy(&self) -> RoutingPolicy {
let mut policy = self
.policy
.clone()
.unwrap_or_else(|| RoutingPolicy::for_level(self.security_level));
if let Some(value) = self.policy_tuning.max_match_distance {
policy.spam.max_match_distance = value.max(0.0);
}
policy
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct PolicyTuningConfig {
pub max_match_distance: Option<f64>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AmbientConfig {
#[serde(default)]
pub seed_peers: Vec<String>,
#[serde(default = "default_ambient_seed_fanout")]
pub seed_fanout: usize,
}
impl Default for AmbientConfig {
fn default() -> Self {
Self {
seed_peers: Vec::new(),
seed_fanout: default_ambient_seed_fanout(),
}
}
}
pub const EXAMPLE_CONFIG_TOML: &str = include_str!("../cmr-peer.example.toml");
pub fn write_example_config(path: impl AsRef<Path>, overwrite: bool) -> Result<(), std::io::Error> {
if overwrite {
return std::fs::write(path, EXAMPLE_CONFIG_TOML);
}
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?;
file.write_all(EXAMPLE_CONFIG_TOML.as_bytes())
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ListenConfig {
#[serde(default)]
pub http: Option<HttpListenConfig>,
#[serde(default)]
pub https: Option<HttpsListenConfig>,
#[serde(default)]
pub udp: Option<UdpListenConfig>,
#[serde(default)]
pub smtp: Option<SmtpListenConfig>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct HttpListenConfig {
pub bind: String,
#[serde(default = "default_http_path")]
pub path: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct HttpsListenConfig {
pub bind: String,
#[serde(default = "default_http_path")]
pub path: String,
pub cert_path: String,
pub key_path: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UdpListenConfig {
pub bind: String,
#[serde(default = "default_udp_service")]
pub service: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SmtpListenConfig {
pub bind: String,
#[serde(default = "default_smtp_inbound_max_message_bytes")]
pub max_message_bytes: usize,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CompressorConfig {
#[serde(default = "default_compressor_command")]
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default = "default_max_frame")]
pub max_frame_bytes: usize,
}
impl Default for CompressorConfig {
fn default() -> Self {
Self {
command: default_compressor_command(),
args: Vec::new(),
max_frame_bytes: default_max_frame(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SmtpConfig {
pub relay: String,
#[serde(default = "default_smtp_port")]
pub port: u16,
#[serde(default)]
pub allow_insecure: bool,
#[serde(default)]
pub username: Option<String>,
#[serde(default)]
pub password_env: Option<String>,
pub from: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SshConfig {
#[serde(default = "default_ssh_binary")]
pub binary: String,
#[serde(default = "default_ssh_remote_command")]
pub default_remote_command: String,
}
impl Default for SshConfig {
fn default() -> Self {
Self {
binary: default_ssh_binary(),
default_remote_command: default_ssh_remote_command(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DashboardConfig {
#[serde(default = "default_dashboard_enabled")]
pub enabled: bool,
#[serde(default = "default_dashboard_path")]
pub path: String,
#[serde(default)]
pub auth_username: Option<String>,
#[serde(default)]
pub auth_password: Option<String>,
}
impl Default for DashboardConfig {
fn default() -> Self {
Self {
enabled: default_dashboard_enabled(),
path: default_dashboard_path(),
auth_username: None,
auth_password: None,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StaticKeyConfig {
pub peer: String,
pub hex_key: String,
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to read config file: {0}")]
Io(std::io::Error),
#[error("failed to parse config toml: {0}")]
Toml(toml::de::Error),
}
fn default_http_path() -> String {
"/".to_owned()
}
fn default_udp_service() -> String {
"cmr".to_owned()
}
fn default_compressor_command() -> String {
"cmr-compressor".to_owned()
}
fn default_max_frame() -> usize {
8 * 1024 * 1024
}
fn default_smtp_port() -> u16 {
587
}
fn default_smtp_inbound_max_message_bytes() -> usize {
4 * 1024 * 1024
}
fn default_ssh_binary() -> String {
"ssh".to_owned()
}
fn default_ssh_remote_command() -> String {
"cmr-peer receive-stdin".to_owned()
}
fn default_dashboard_enabled() -> bool {
false
}
fn default_dashboard_path() -> String {
"/_cmr".to_owned()
}
fn default_ambient_seed_fanout() -> usize {
8
}
#[cfg(test)]
mod tests {
use super::write_example_config;
#[test]
fn write_example_config_honors_overwrite_flag() {
let path = std::env::temp_dir().join(format!(
"cmr-peer-example-config-{}-{}.toml",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time")
.as_nanos()
));
write_example_config(&path, false).expect("write initial template");
let first = std::fs::read_to_string(&path).expect("read first");
assert!(first.contains("local_address"));
let err = write_example_config(&path, false).expect_err("second create should fail");
assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
write_example_config(&path, true).expect("overwrite template");
let second = std::fs::read_to_string(&path).expect("read second");
assert_eq!(first, second);
let _ = std::fs::remove_file(path);
}
}