use std::{env, fmt, fs, process};
use std::collections::HashMap;
use std::io::Read;
use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::thread::available_parallelism;
use std::time::Duration;
use clap::{
Command, Args, ArgAction, ArgMatches, FromArgMatches, Parser,
crate_version,
};
use dirs::home_dir;
use log::{LevelFilter, error, warn};
#[cfg(unix)] use syslog::Facility;
use toml_edit as toml;
use crate::tals;
use crate::error::Failed;
const DEFAULT_STRICT: bool = false;
const DEFAULT_RSYNC_TIMEOUT: Duration = Duration::from_secs(300);
const DEFAULT_DIRTY_REPOSITORY: bool = false;
const DEFAULT_REFRESH: u64 = 600;
const DEFAULT_RETRY: u64 = 600;
const DEFAULT_EXPIRE: u64 = 7200;
const DEFAULT_HISTORY_SIZE: usize = 10;
const DEFAULT_RRDP_TIMEOUT: Duration = Duration::from_secs(600);
const DEFAULT_RRDP_READ_TIMEOUT: Duration = Duration::from_secs(10);
const DEFAULT_RRDP_TCP_KEEPALIVE: Duration = Duration::from_secs(60);
const DEFAULT_RRDP_FALLBACK: FallbackPolicy = FallbackPolicy::Stale;
const DEFAULT_RRDP_FALLBACK_TIME: Duration = Duration::from_secs(3600);
const DEFAULT_RRDP_MAX_DELTA_COUNT: usize = 100;
const DEFAULT_RRDP_MAX_DELTA_LIST_LEN: usize = 500;
const DEFAULT_RRDP_USER_AGENT: &str = concat!("Routinator/", crate_version!());
const DEFAULT_RTR_TCP_KEEPALIVE: Option<Duration>
= Some(Duration::from_secs(60));
const DEFAULT_STALE_POLICY: FilterPolicy = FilterPolicy::Reject;
const DEFAULT_UNSAFE_VRPS_POLICY: FilterPolicy = FilterPolicy::Accept;
const DEFAULT_UNKNOWN_OBJECTS_POLICY: FilterPolicy = FilterPolicy::Warn;
const DEFAULT_MAX_OBJECT_SIZE: u64 = 20_000_000;
const DEFAULT_MAX_CA_DEPTH: usize = 32;
#[cfg(unix)]
const DEFAULT_SYSLOG_FACILITY: Facility = Facility::LOG_DAEMON;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Config {
pub config_file: PathBuf,
pub cache_dir: PathBuf,
pub no_rir_tals: bool,
pub bundled_tals: Vec<String>,
pub extra_tals_dir: Option<PathBuf>,
pub exceptions: Vec<PathBuf>,
pub strict: bool,
pub stale: FilterPolicy,
pub unsafe_vrps: FilterPolicy,
pub unknown_objects: FilterPolicy,
pub limit_v4_len: Option<u8>,
pub limit_v6_len: Option<u8>,
pub allow_dubious_hosts: bool,
pub fresh: bool,
pub disable_rsync: bool,
pub rsync_command: String,
pub rsync_args: Option<Vec<String>>,
pub rsync_timeout: Option<Duration>,
pub disable_rrdp: bool,
pub rrdp_fallback: FallbackPolicy,
pub rrdp_fallback_time: Duration,
pub rrdp_max_delta_count: usize,
pub rrdp_max_delta_list_len: usize,
pub rrdp_timeout: Option<Duration>,
pub rrdp_read_timeout: Option<Duration>,
pub rrdp_connect_timeout: Option<Duration>,
pub rrdp_tcp_keepalive: Option<Duration>,
pub rrdp_local_addr: Option<IpAddr>,
pub rrdp_root_certs: Vec<PathBuf>,
pub rrdp_proxies: Vec<String>,
pub rrdp_user_agent: String,
pub max_object_size: Option<u64>,
pub max_ca_depth: usize,
pub enable_bgpsec: bool,
pub enable_aspa: bool,
pub dirty_repository: bool,
pub validation_threads: usize,
pub refresh: Duration,
pub min_refresh: Option<Duration>,
pub retry: Duration,
pub expire: Duration,
pub history_size: usize,
pub rtr_listen: Vec<SocketAddr>,
pub rtr_tls_listen: Vec<SocketAddr>,
pub http_listen: Vec<SocketAddr>,
pub http_tls_listen: Vec<SocketAddr>,
pub systemd_listen: bool,
pub rtr_tcp_keepalive: Option<Duration>,
pub rtr_client_metrics: bool,
pub rtr_tls_key: Option<PathBuf>,
pub rtr_tls_cert: Option<PathBuf>,
pub http_tls_key: Option<PathBuf>,
pub http_tls_cert: Option<PathBuf>,
pub log_level: LevelFilter,
pub log_target: LogTarget,
pub log_repository_issues: bool,
pub pid_file: Option<PathBuf>,
pub working_dir: Option<PathBuf>,
pub chroot: Option<PathBuf>,
pub user: Option<String>,
pub group: Option<String>,
pub tal_labels: HashMap<String, String>,
}
impl Config {
pub fn config_args(app: Command) -> Command {
GlobalArgs::augment_args(app)
}
pub fn server_args(app: Command) -> Command {
ServerArgs::augment_args(app)
}
pub fn from_arg_matches(
matches: &ArgMatches,
cur_dir: &Path,
) -> Result<Self, Failed> {
let mut res = Self::create_base_config(
Self::path_value_of(matches, "config", cur_dir)
.as_ref().map(AsRef::as_ref)
)?;
res.apply_arg_matches(matches, cur_dir)?;
Ok(res)
}
fn apply_arg_matches(
&mut self,
matches: &ArgMatches,
cur_dir: &Path,
) -> Result<(), Failed> {
let args = GlobalArgs::from_arg_matches(
matches
).expect("bug in command line arguments parser");
if let Some(tals) = args.bundled_tals.as_ref() {
if tals.iter().any(|tal| tal == "list") {
tals::print_tals();
process::exit(0);
}
}
self.apply_log_matches(&args, cur_dir)?;
if let Some(dir) = args.repository_dir {
self.cache_dir = cur_dir.join(dir)
}
if self.cache_dir == Path::new("") {
error!(
"Couldn’t determine default repository directory: \
no home directory.\n\
Please specify the repository directory with the -r option."
);
return Err(Failed)
}
if args.no_rir_tals {
self.no_rir_tals = true
}
if let Some(tals) = args.bundled_tals {
self.bundled_tals = tals;
}
if let Some(dir) = args.extra_tals_dir {
self.extra_tals_dir = Some(cur_dir.join(dir));
}
if let Some(list) = args.exceptions {
self.exceptions = list.into_iter().map(|path| {
cur_dir.join(path)
}).collect()
}
if args.strict {
self.strict = true
}
if let Some(value) = args.stale {
self.stale = value
}
if let Some(value) = args.unsafe_vrps {
self.unsafe_vrps = value
}
if let Some(value) = args.unknown_objects {
self.unknown_objects = value
}
if let Some(value) = args.limit_v4_len {
self.limit_v4_len = Some(value)
}
if let Some(value) = args.limit_v6_len {
self.limit_v6_len = Some(value)
}
if args.allow_dubious_hosts {
self.allow_dubious_hosts = true
}
if args.fresh {
self.fresh = true
}
if args.disable_rsync {
self.disable_rsync = true
}
if let Some(value) = args.rsync_command {
self.rsync_command = value
}
if let Some(value) = args.rsync_timeout {
self.rsync_timeout = if value == 0 {
None
}
else {
Some(Duration::from_secs(value))
};
}
if args.disable_rrdp {
self.disable_rrdp = true
}
if let Some(value) = args.rrdp_fallback {
self.rrdp_fallback = value
}
if let Some(value) = args.rrdp_fallback_time {
self.rrdp_fallback_time = Duration::from_secs(value)
}
if let Some(value) = args.rrdp_max_delta_count {
self.rrdp_max_delta_count = value
}
if let Some(value) = args.rrdp_max_delta_list_len {
self.rrdp_max_delta_list_len = value
}
if let Some(value) = args.rrdp_timeout {
self.rrdp_timeout = if value == 0 {
None
}
else {
Some(Duration::from_secs(value))
};
}
if let Some(value) = args.rrdp_read_timeout {
self.rrdp_read_timeout = if value == 0 {
None
}
else {
Some(Duration::from_secs(value))
};
}
if let Some(value) = args.rrdp_connect_timeout {
self.rrdp_connect_timeout = Some(Duration::from_secs(value))
}
if let Some(value) = args.rrdp_tcp_keepalive {
self.rrdp_tcp_keepalive = if value == 0 {
None
}
else {
Some(Duration::from_secs(value))
};
}
if let Some(value) = args.rrdp_local_addr {
self.rrdp_local_addr = Some(value)
}
if let Some(list) = args.rrdp_root_cert {
self.rrdp_root_certs = list.into_iter().map(|path| {
cur_dir.join(path)
}).collect()
}
if let Some(list) = args.rrdp_proxy {
self.rrdp_proxies = list
}
if let Some(value) = args.max_object_size {
if value == 0 {
self.max_object_size = None
}
else {
self.max_object_size = Some(value)
}
}
if let Some(value) = args.max_ca_depth {
self.max_ca_depth = value;
}
if args.enable_bgpsec {
self.enable_bgpsec = true
}
if args.enable_aspa {
self.enable_aspa = true
}
if args.dirty_repository {
self.dirty_repository = true
}
if let Some(value) = args.validation_threads {
self.validation_threads = value
}
if args.verbose > 1 {
self.log_level = LevelFilter::Debug
}
else if args.verbose == 1 {
self.log_level = LevelFilter::Info
}
else if args.quiet > 1 {
self.log_level = LevelFilter::Off
}
else if args.quiet == 1 {
self.log_level = LevelFilter::Error
}
if args.log_repository_issues {
self.log_repository_issues = true
}
Ok(())
}
#[cfg(unix)]
fn apply_log_matches(
&mut self,
args: &GlobalArgs,
cur_dir: &Path,
) -> Result<(), Failed> {
if args.syslog {
if let Some(facility) = args.syslog_facility.as_ref() {
self.log_target = LogTarget::Syslog(
match Facility::from_str(facility) {
Ok(value) => value,
Err(_) => {
error!("Invalid value for syslog-facility.");
return Err(Failed);
}
}
)
}
else if !matches!(self.log_target, LogTarget::Syslog(_)) {
self.log_target = LogTarget::Syslog(DEFAULT_SYSLOG_FACILITY)
}
}
else if let Some(file) = args.logfile.as_ref() {
if file == "-" {
self.log_target = LogTarget::Stderr
}
else {
self.log_target = LogTarget::File(cur_dir.join(file))
}
}
Ok(())
}
#[cfg(not(unix))]
#[allow(clippy::unnecessary_wraps)]
fn apply_log_matches(
&mut self,
args: &GlobalArgs,
cur_dir: &Path,
) -> Result<(), Failed> {
if let Some(file) = args.logfile.as_ref() {
if file == "-" {
self.log_target = LogTarget::Stderr
}
else {
self.log_target = LogTarget::File(cur_dir.join(file))
}
}
Ok(())
}
pub fn apply_server_arg_matches(
&mut self,
matches: &ArgMatches,
cur_dir: &Path,
) -> Result<(), Failed> {
let args = ServerArgs::from_arg_matches(
matches
).expect("bug in command line arguments parser");
if let Some(value) = args.refresh {
self.refresh = Duration::from_secs(value)
}
if let Some(value) = args.min_refresh {
self.min_refresh = Some(Duration::from_secs(value))
}
if let Some(value) = args.retry {
self.retry = Duration::from_secs(value)
}
if let Some(value) = args.expire {
self.expire = Duration::from_secs(value)
}
if let Some(value) = args.history {
self.history_size = value
}
if let Some(list) = args.rtr_listen {
self.rtr_listen = list
}
if let Some(list) = args.rtr_tls_listen {
self.rtr_tls_listen = list
}
if let Some(list) = args.http_listen {
self.http_listen = list
}
if let Some(list) = args.http_tls_listen {
self.http_tls_listen = list
}
if args.systemd_listen {
self.systemd_listen = true
}
if let Some(keep) = args.rtr_tcp_keepalive {
self.rtr_tcp_keepalive = if keep == 0 {
None
}
else {
Some(Duration::from_secs(keep))
}
}
if args.rtr_client_metrics {
self.rtr_client_metrics = true
}
if let Some(path) = args.rtr_tls_key {
self.rtr_tls_key = Some(cur_dir.join(path))
}
if let Some(path) = args.rtr_tls_cert {
self.rtr_tls_cert = Some(cur_dir.join(path))
}
if let Some(path) = args.http_tls_key {
self.http_tls_key = Some(cur_dir.join(path))
}
if let Some(path) = args.http_tls_cert {
self.http_tls_cert = Some(cur_dir.join(path))
}
if let Some(pid_file) = args.pid_file {
self.pid_file = Some(cur_dir.join(pid_file))
}
if let Some(working_dir) = args.working_dir {
self.working_dir = Some(cur_dir.join(working_dir))
}
if let Some(chroot) = args.chroot {
self.chroot = Some(cur_dir.join(chroot))
}
if let Some(user) = args.user {
self.user = Some(user)
}
if let Some(group) = args.group {
self.group = Some(group)
}
Ok(())
}
fn path_value_of(
matches: &ArgMatches,
key: &str,
dir: &Path
) -> Option<PathBuf> {
matches.get_one::<PathBuf>(key).map(|path| dir.join(path))
}
fn create_base_config(path: Option<&Path>) -> Result<Self, Failed> {
let file = match path {
Some(path) => {
match ConfigFile::read(path)? {
Some(file) => file,
None => {
error!("Cannot read config file {}", path.display());
return Err(Failed);
}
}
}
None => {
match home_dir() {
Some(dir) => match ConfigFile::read(
&dir.join(".routinator.conf"))? {
Some(file) => file,
None => return Ok(Self::default()),
}
None => return Ok(Self::default())
}
}
};
Self::from_config_file(file)
}
fn from_config_file(mut file: ConfigFile) -> Result<Self, Failed> {
let log_target = Self::log_target_from_config_file(&mut file)?;
let res = Config {
config_file: file.path.clone(),
cache_dir: file.take_mandatory_path("repository-dir")?,
no_rir_tals: file.take_bool("no-rir-tals")?.unwrap_or(false),
bundled_tals: {
file.take_string_array("tals")?
.unwrap_or_default()
},
extra_tals_dir: file.take_path("extra-tals-dir")?,
exceptions: {
file.take_path_array("exceptions")?.unwrap_or_default()
},
strict: file.take_bool("strict")?.unwrap_or(false),
stale: {
file.take_from_str("stale")?.unwrap_or(DEFAULT_STALE_POLICY)
},
unsafe_vrps: {
file.take_from_str("unsafe-vrps")?
.unwrap_or(DEFAULT_UNSAFE_VRPS_POLICY)
},
unknown_objects: {
file.take_from_str("unknown-objects")?
.unwrap_or(DEFAULT_UNKNOWN_OBJECTS_POLICY)
},
limit_v4_len: file.take_limited_u8("limit-v4-len", 32)?,
limit_v6_len: file.take_limited_u8("limit-v6-len", 128)?,
allow_dubious_hosts:
file.take_bool("allow-dubious-hosts")?.unwrap_or(false),
fresh: false,
disable_rsync: file.take_bool("disable-rsync")?.unwrap_or(false),
rsync_command: {
file.take_string("rsync-command")?
.unwrap_or_else(|| "rsync".into())
},
rsync_args: file.take_string_array("rsync-args")?,
rsync_timeout: {
match file.take_u64("rsync-timeout")? {
Some(0) => None,
Some(value) => Some(Duration::from_secs(value)),
None => Some(DEFAULT_RSYNC_TIMEOUT)
}
},
disable_rrdp: file.take_bool("disable-rrdp")?.unwrap_or(false),
rrdp_fallback: {
file.take_from_str("rrdp-fallback")?
.unwrap_or(DEFAULT_RRDP_FALLBACK)
},
rrdp_fallback_time: {
file.take_u64("rrdp-fallback-time")?
.map(Duration::from_secs)
.unwrap_or(DEFAULT_RRDP_FALLBACK_TIME)
},
rrdp_max_delta_count: {
file.take_usize("rrdp-max-delta-count")?
.unwrap_or(DEFAULT_RRDP_MAX_DELTA_COUNT)
},
rrdp_max_delta_list_len: {
file.take_usize("rrdp-max-delta-list-len")?
.unwrap_or(DEFAULT_RRDP_MAX_DELTA_LIST_LEN)
},
rrdp_timeout: {
match file.take_u64("rrdp-timeout")? {
Some(0) => None,
Some(value) => Some(Duration::from_secs(value)),
None => Some(DEFAULT_RRDP_TIMEOUT)
}
},
rrdp_read_timeout: {
match file.take_u64("rrdp-read-timeout")? {
Some(0) => None,
Some(value) => Some(Duration::from_secs(value)),
None => Some(DEFAULT_RRDP_READ_TIMEOUT)
}
},
rrdp_connect_timeout: {
file.take_u64("rrdp-connect-timeout")?.map(Duration::from_secs)
},
rrdp_tcp_keepalive: {
match file.take_u64("rrdp-tcp-keepalive")? {
Some(0) => None,
Some(value) => Some(Duration::from_secs(value)),
None => Some(DEFAULT_RRDP_TCP_KEEPALIVE)
}
},
rrdp_local_addr: file.take_from_str("rrdp-local-addr")?,
rrdp_root_certs: {
file.take_from_str_array("rrdp-root-certs")?
.unwrap_or_default()
},
rrdp_proxies: {
file.take_string_array("rrdp-proxies")?.unwrap_or_default()
},
rrdp_user_agent: DEFAULT_RRDP_USER_AGENT.to_string(),
max_object_size: {
match file.take_u64("max-object-size")? {
Some(0) => None,
Some(value) => Some(value),
None => Some(DEFAULT_MAX_OBJECT_SIZE),
}
},
max_ca_depth: {
file.take_usize("max-ca-depth")?
.unwrap_or(DEFAULT_MAX_CA_DEPTH)
},
enable_bgpsec: file.take_bool("enable-bgpsec")?.unwrap_or(false),
enable_aspa: file.take_bool("enable-aspa")?.unwrap_or(false),
dirty_repository: file.take_bool("dirty")?.unwrap_or(false),
validation_threads: {
file.take_small_usize(
"validation-threads"
)?.unwrap_or_else(|| {
Config::default_validation_threads()
})
},
refresh: {
Duration::from_secs(
file.take_u64("refresh")?.unwrap_or(DEFAULT_REFRESH)
)
},
min_refresh: {
file.take_u64("min-refresh")?.map(Duration::from_secs)
},
retry: {
Duration::from_secs(
file.take_u64("retry")?.unwrap_or(DEFAULT_RETRY)
)
},
expire: {
Duration::from_secs(
file.take_u64("expire")?.unwrap_or(DEFAULT_EXPIRE)
)
},
history_size: {
file.take_small_usize("history-size")?
.unwrap_or(DEFAULT_HISTORY_SIZE)
},
rtr_listen: {
file.take_from_str_array("rtr-listen")?.unwrap_or_default()
},
rtr_tls_listen: {
file.take_from_str_array("rtr-tls-listen")?
.unwrap_or_default()
},
http_listen: {
file.take_from_str_array("http-listen")?.unwrap_or_default()
},
http_tls_listen: {
file.take_from_str_array("http-tls-listen")?
.unwrap_or_default()
},
systemd_listen: file.take_bool("systemd-listen")?.unwrap_or(false),
rtr_tcp_keepalive: {
match file.take_u64("rtr-tcp-keepalive")? {
Some(0) => None,
Some(keep) => Some(Duration::from_secs(keep)),
None => DEFAULT_RTR_TCP_KEEPALIVE,
}
},
rtr_client_metrics: {
file.take_bool("rtr-client-metrics")?.unwrap_or(false)
},
rtr_tls_key: file.take_path("rtr-tls-key")?,
rtr_tls_cert: file.take_path("rtr-tls-cert")?,
http_tls_key: file.take_path("http-tls-key")?,
http_tls_cert: file.take_path("http-tls-cert")?,
log_level: {
file.take_from_str("log-level")?.unwrap_or(LevelFilter::Warn)
},
log_target,
log_repository_issues: {
file.take_bool("log-repository-issues")?.unwrap_or(false)
},
pid_file: file.take_path("pid-file")?,
working_dir: file.take_path("working-dir")?,
chroot: file.take_path("chroot")?,
user: file.take_string("user")?,
group: file.take_string("group")?,
tal_labels: file.take_string_map("tal-labels")?.unwrap_or_default(),
};
if file.take_path("tal-dir")?.is_some() {
warn!(
"Ignoring obsolete \"tal-dir\" option in config file {}.",
file.path.display()
);
}
file.check_exhausted()?;
Ok(res)
}
#[cfg(unix)]
fn log_target_from_config_file(
file: &mut ConfigFile
) -> Result<LogTarget, Failed> {
let facility = file.take_string("syslog-facility")?;
let facility = facility.as_ref().map(AsRef::as_ref)
.unwrap_or("daemon");
let facility = match Facility::from_str(facility) {
Ok(value) => value,
Err(_) => {
error!(
"Failed in config file {}: invalid syslog-facility.",
file.path.display()
);
return Err(Failed);
}
};
let log_target = file.take_string("log")?;
let log_file = file.take_path("log-file")?;
match log_target.as_ref().map(AsRef::as_ref) {
Some("default") | None => Ok(LogTarget::Default(facility)),
Some("syslog") => Ok(LogTarget::Syslog(facility)),
Some("stderr") => Ok(LogTarget::Stderr),
Some("file") => {
match log_file {
Some(file) => Ok(LogTarget::File(file)),
None => {
error!(
"Failed in config file {}: \
log target \"file\" requires 'log-file' value.",
file.path.display()
);
Err(Failed)
}
}
}
Some(value) => {
error!(
"Failed in config file {}: \
invalid log target '{}'",
file.path.display(),
value
);
Err(Failed)
}
}
}
#[cfg(not(unix))]
fn log_target_from_config_file(
file: &mut ConfigFile
) -> Result<LogTarget, Failed> {
let log_target = file.take_string("log")?;
let log_file = file.take_path("log-file")?;
match log_target.as_ref().map(AsRef::as_ref) {
Some("default") | Some("stderr") | None => Ok(LogTarget::Stderr),
Some("file") => {
match log_file {
Some(file) => Ok(LogTarget::File(file)),
None => {
error!(
"Failed in config file {}: \
log target \"file\" requires 'log-file' value.",
file.path.display()
);
Err(Failed)
}
}
}
Some(value) => {
error!(
"Failed in config file {}: \
invalid log target '{}'",
file.path.display(), value
);
Err(Failed)
}
}
}
pub fn default_with_paths(
config_file: PathBuf,
cache_dir: PathBuf,
) -> Self {
Self {
config_file,
cache_dir,
no_rir_tals: false,
bundled_tals: Vec::new(),
extra_tals_dir: None,
exceptions: Vec::new(),
strict: DEFAULT_STRICT,
stale: DEFAULT_STALE_POLICY,
unsafe_vrps: DEFAULT_UNSAFE_VRPS_POLICY,
unknown_objects: DEFAULT_UNKNOWN_OBJECTS_POLICY,
limit_v4_len: None,
limit_v6_len: None,
allow_dubious_hosts: false,
fresh: false,
disable_rsync: false,
rsync_command: "rsync".into(),
rsync_args: None,
rsync_timeout: Some(DEFAULT_RSYNC_TIMEOUT),
disable_rrdp: false,
rrdp_fallback: DEFAULT_RRDP_FALLBACK,
rrdp_fallback_time: DEFAULT_RRDP_FALLBACK_TIME,
rrdp_max_delta_count: DEFAULT_RRDP_MAX_DELTA_COUNT,
rrdp_max_delta_list_len: DEFAULT_RRDP_MAX_DELTA_LIST_LEN,
rrdp_timeout: Some(DEFAULT_RRDP_TIMEOUT),
rrdp_read_timeout: Some(DEFAULT_RRDP_READ_TIMEOUT),
rrdp_connect_timeout: None,
rrdp_tcp_keepalive: Some(DEFAULT_RRDP_TCP_KEEPALIVE),
rrdp_local_addr: None,
rrdp_root_certs: Vec::new(),
rrdp_proxies: Vec::new(),
rrdp_user_agent: DEFAULT_RRDP_USER_AGENT.to_string(),
max_object_size: Some(DEFAULT_MAX_OBJECT_SIZE),
max_ca_depth: DEFAULT_MAX_CA_DEPTH,
enable_bgpsec: false,
enable_aspa: false,
dirty_repository: DEFAULT_DIRTY_REPOSITORY,
validation_threads: Config::default_validation_threads(),
refresh: Duration::from_secs(DEFAULT_REFRESH),
min_refresh: None,
retry: Duration::from_secs(DEFAULT_RETRY),
expire: Duration::from_secs(DEFAULT_EXPIRE),
history_size: DEFAULT_HISTORY_SIZE,
rtr_listen: Vec::new(),
rtr_tls_listen: Vec::new(),
http_listen: Vec::new(),
http_tls_listen: Vec::new(),
systemd_listen: false,
rtr_tcp_keepalive: DEFAULT_RTR_TCP_KEEPALIVE,
rtr_client_metrics: false,
rtr_tls_key: None,
rtr_tls_cert: None,
http_tls_key: None,
http_tls_cert: None,
log_level: LevelFilter::Warn,
log_target: LogTarget::default(),
log_repository_issues: false,
pid_file: None,
working_dir: None,
chroot: None,
user: None,
group: None,
tal_labels: HashMap::new(),
}
}
fn default_validation_threads() -> usize {
available_parallelism().map(|x| x.get()).unwrap_or(1)
}
pub fn adjust_chroot_paths(&mut self) -> Result<(), Failed> {
if let Some(ref chroot) = self.chroot {
self.cache_dir = match self.cache_dir.strip_prefix(chroot) {
Ok(dir) => dir.into(),
Err(_) => {
error!(
"Fatal: Repository directory {} \
not under chroot {}.",
self.cache_dir.display(), chroot.display()
);
return Err(Failed)
}
};
if let Some(extra_tals_dir) = self.extra_tals_dir.take() {
self.extra_tals_dir = match extra_tals_dir.strip_prefix(
chroot
) {
Ok(dir) => Some(dir.into()),
Err(_) => {
error!(
"Fatal: TAL directory {} not under chroot {}.",
extra_tals_dir.display(), chroot.display()
);
return Err(Failed)
}
};
}
for item in &mut self.exceptions {
*item = match item.strip_prefix(chroot) {
Ok(path) => path.into(),
Err(_) => {
error!(
"Fatal: Exception file {} not under chroot {}.",
item.display(), chroot.display()
);
return Err(Failed)
}
}
}
if let LogTarget::File(ref mut file) = self.log_target {
*file = match file.strip_prefix(chroot) {
Ok(path) => path.into(),
Err(_) => {
error!(
"Fatal: Log file {} not under chroot {}.",
file.display(), chroot.display()
);
return Err(Failed)
}
};
}
if let Some(ref mut dir) = self.working_dir {
*dir = match dir.strip_prefix(chroot) {
Ok(path) => path.into(),
Err(_) => {
error!(
"Fatal: working directory {} not under chroot {}.",
dir.display(), chroot.display()
);
return Err(Failed)
}
}
}
}
Ok(())
}
pub fn to_toml(&self) -> toml::Table {
fn insert(
table: &mut toml::Table,
key: &str,
value: impl Into<toml::Value>,
) {
table.insert(key, toml::Item::Value(value.into()));
}
fn insert_int(
table: &mut toml::Table,
key: &str,
value: impl TryInto<i64>,
) {
insert(table, key, value.try_into().unwrap_or(i64::MAX))
}
let mut res = toml::Table::new();
insert(
&mut res, "repository-dir", self.cache_dir.display().to_string()
);
if let Some(extra_tals_dir) = self.extra_tals_dir.as_ref() {
insert(
&mut res, "extra-tals-dir",
extra_tals_dir.display().to_string(),
);
}
insert(
&mut res, "exceptions",
toml::Value::Array(
self.exceptions.iter()
.map(|p| toml::Value::from(p.display().to_string()))
.collect()
)
);
insert(&mut res, "strict", self.strict);
insert(&mut res, "stale", format!("{}", self.stale));
insert(&mut res, "unsafe-vrps", format!("{}", self.unsafe_vrps));
insert(
&mut res, "unknown-objects", format!("{}", self.unknown_objects)
);
if let Some(value) = self.limit_v4_len {
insert(&mut res, "limit-v4-len", i64::from(value));
}
if let Some(value) = self.limit_v6_len {
insert(&mut res, "limit-v6-len", i64::from(value));
}
insert(&mut res, "allow-dubious-hosts", self.allow_dubious_hosts);
insert(&mut res, "disable-rsync", self.disable_rsync);
insert(&mut res, "rsync-command", self.rsync_command.clone());
if let Some(ref args) = self.rsync_args {
insert(
&mut res, "rsync-args",
toml::Value::Array(
args.iter().map(|a| toml::Value::from(a.clone())).collect()
)
);
}
insert_int(
&mut res, "rsync-timeout",
match self.rsync_timeout {
None => 0,
Some(value) => value.as_secs(),
}
);
insert(&mut res, "disable-rrdp", self.disable_rrdp);
insert(&mut res, "rrdp-fallback", self.rrdp_fallback.to_string());
insert_int(
&mut res, "rrdp-fallback-time", self.rrdp_fallback_time.as_secs(),
);
insert_int(
&mut res, "rrdp-max-delta-count", self.rrdp_max_delta_count
);
insert_int(
&mut res, "rrdp-max-delta-list-len", self.rrdp_max_delta_list_len
);
insert_int(
&mut res, "rrdp-timeout",
match self.rrdp_timeout {
None => 0,
Some(value) => value.as_secs(),
}
);
insert_int(
&mut res, "rrdp-read-timeout",
match self.rrdp_read_timeout {
None => 0,
Some(value) => value.as_secs(),
}
);
if let Some(timeout) = self.rrdp_connect_timeout {
insert_int(&mut res, "rrdp-connect-timeout", timeout.as_secs());
}
insert_int(
&mut res, "rrdp-tcp-keepalive",
match self.rrdp_tcp_keepalive {
None => 0,
Some(value) => value.as_secs(),
}
);
if let Some(addr) = self.rrdp_local_addr {
insert(&mut res, "rrdp-local-addr", addr.to_string());
}
insert(
&mut res, "rrdp-root-certs",
toml::Value::Array(
self.rrdp_root_certs.iter()
.map(|p| toml::Value::from(p.display().to_string()))
.collect()
)
);
insert(
&mut res, "rrdp-proxies",
toml::Value::Array(
self.rrdp_proxies.iter().map(|s| {
toml::Value::from(s.clone())
}).collect()
)
);
insert_int(
&mut res, "max-object-size",
self.max_object_size.unwrap_or(0),
);
insert_int(&mut res, "max-ca-depth", self.max_ca_depth);
insert(&mut res, "enable-bgpsec", self.enable_bgpsec);
insert(&mut res, "enable-aspa", self.enable_aspa);
insert(&mut res, "dirty", self.dirty_repository);
insert_int(&mut res, "validation-threads", self.validation_threads);
insert_int(&mut res, "refresh", self.refresh.as_secs());
if let Some(min_refresh) = self.min_refresh.as_ref() {
insert_int(&mut res, "min-refresh", min_refresh.as_secs());
}
insert_int(&mut res, "retry", self.retry.as_secs());
insert_int(&mut res, "expire", self.expire.as_secs());
insert_int(&mut res, "history-size", self.history_size);
insert(
&mut res, "rtr-listen",
toml::Value::Array(
self.rtr_listen.iter().map(|a| {
toml::Value::from(a.to_string())
}).collect()
)
);
insert(
&mut res, "rtr-tls-listen",
toml::Value::Array(
self.rtr_tls_listen.iter().map(|a| {
toml::Value::from(a.to_string())
}).collect()
)
);
insert(
&mut res, "http-listen",
toml::Value::Array(
self.http_listen.iter().map(|a| {
toml::Value::from(a.to_string())
}).collect()
)
);
insert(
&mut res, "http-tls-listen",
toml::Value::Array(
self.http_tls_listen.iter().map(|a| {
toml::Value::from(a.to_string())
}).collect()
)
);
insert(&mut res, "systemd-listen", self.systemd_listen);
insert_int(
&mut res, "rtr-tcp-keepalive",
match self.rtr_tcp_keepalive {
Some(value) => value.as_secs(),
None => 0,
}
);
insert( &mut res, "rtr-client-metrics", self.rtr_client_metrics);
if let Some(ref path) = self.rtr_tls_key {
insert(&mut res, "rtr-tls-key", path.display().to_string());
}
if let Some(ref path) = self.rtr_tls_cert {
insert(&mut res, "rtr-tls-cert", path.display().to_string());
}
if let Some(ref path) = self.http_tls_key {
insert(&mut res, "http-tls-key", path.display().to_string());
}
if let Some(ref path) = self.http_tls_cert {
insert(&mut res, "http-tls-cert", path.display().to_string());
}
insert(&mut res, "log-level", self.log_level.to_string());
match self.log_target {
#[cfg(unix)]
LogTarget::Default(facility) => {
insert(&mut res, "log", "default");
insert(
&mut res, "syslog-facility", facility_to_string(facility)
);
}
#[cfg(unix)]
LogTarget::Syslog(facility) => {
insert(&mut res, "log", "syslog");
insert(
&mut res, "syslog-facility", facility_to_string(facility)
);
}
LogTarget::Stderr => {
insert(&mut res, "log", "stderr");
}
LogTarget::File(ref file) => {
insert(&mut res, "log", "file");
insert(&mut res, "log-file", file.display().to_string());
}
}
insert(&mut res, "log-repository-issues", self.log_repository_issues);
if let Some(ref file) = self.pid_file {
insert(&mut res, "pid-file", file.display().to_string());
}
if let Some(ref dir) = self.working_dir {
insert(&mut res, "working-dir", dir.display().to_string());
}
if let Some(ref dir) = self.chroot {
insert(&mut res, "chroot", dir.display().to_string());
}
if let Some(ref user) = self.user {
insert(&mut res, "user", user.clone());
}
if let Some(ref group) = self.group {
insert(&mut res, "group", group.clone());
}
if !self.tal_labels.is_empty() {
insert(
&mut res, "tal-labels",
toml::Value::Array(
self.tal_labels.iter().map(|(left, right)| {
toml::Value::Array([
toml::Value::from(left.clone()),
toml::Value::from(right.clone()),
].into_iter().collect())
}).collect()
)
);
}
res
}
}
impl Default for Config {
fn default() -> Self {
match home_dir() {
Some(dir) => {
Config::default_with_paths(
dir.join(".routinator.conf"),
dir.join(".rpki-cache/repository"),
)
}
None => {
Config::default_with_paths(
PathBuf::from(""),
PathBuf::from(""),
)
}
}
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_toml())
}
}
#[derive(Clone, Debug)]
pub enum LogTarget {
#[cfg(unix)]
Default(Facility),
#[cfg(unix)]
Syslog(Facility),
Stderr,
File(PathBuf)
}
#[cfg(unix)]
impl Default for LogTarget {
fn default() -> Self {
LogTarget::Default(Facility::LOG_DAEMON)
}
}
#[cfg(not(unix))]
#[allow(clippy::derivable_impls)]
impl Default for LogTarget {
fn default() -> Self {
LogTarget::Stderr
}
}
impl PartialEq for LogTarget {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
#[cfg(unix)]
(&LogTarget::Default(s), &LogTarget::Default(o)) => {
(s as usize) == (o as usize)
}
#[cfg(unix)]
(&LogTarget::Syslog(s), &LogTarget::Syslog(o)) => {
(s as usize) == (o as usize)
}
(&LogTarget::Stderr, &LogTarget::Stderr) => true,
(LogTarget::File(s), LogTarget::File(o)) => {
s == o
}
_ => false
}
}
}
impl Eq for LogTarget { }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FilterPolicy {
Reject,
Warn,
Accept
}
impl FilterPolicy {
pub fn log(self) -> bool {
matches!(self, FilterPolicy::Reject | FilterPolicy::Warn)
}
}
impl FromStr for FilterPolicy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"reject" => Ok(FilterPolicy::Reject),
"warn" => Ok(FilterPolicy::Warn),
"accept" => Ok(FilterPolicy::Accept),
_ => Err(format!("invalid policy '{s}'"))
}
}
}
impl fmt::Display for FilterPolicy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match *self {
FilterPolicy::Reject => "reject",
FilterPolicy::Warn => "warn",
FilterPolicy::Accept => "accept",
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FallbackPolicy {
Never,
Stale,
New,
}
impl FromStr for FallbackPolicy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"never" => Ok(FallbackPolicy::Never),
"stale" => Ok(FallbackPolicy::Stale),
"new" => Ok(FallbackPolicy::New),
_ => Err(format!("invalid policy '{s}'"))
}
}
}
impl fmt::Display for FallbackPolicy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match *self {
FallbackPolicy::Never => "never",
FallbackPolicy::Stale => "stale",
FallbackPolicy::New => "new",
})
}
}
#[derive(Clone, Debug, Parser)]
struct GlobalArgs {
#[arg(short, long, value_name="PATH")]
config: Option<PathBuf>,
#[arg(short, long, value_name="PATH")]
repository_dir: Option<PathBuf>,
#[arg(long)]
no_rir_tals: bool,
#[arg(long = "tal", value_name="NAME")]
bundled_tals: Option<Vec<String>>,
#[arg(long, value_name="PATH")]
extra_tals_dir: Option<PathBuf>,
#[arg(short = 'x', long, value_name="PATH")]
exceptions: Option<Vec<PathBuf>>,
#[arg(long)]
strict: bool,
#[arg(long, value_name = "POLICY")]
stale: Option<FilterPolicy>,
#[arg(long, value_name = "POLICY")]
unsafe_vrps: Option<FilterPolicy>,
#[arg(long, value_name = "POLICY")]
unknown_objects: Option<FilterPolicy>,
#[arg(
long,
value_name = "LENGTH",
value_parser = clap::value_parser!(u8).range(..=32)
)]
limit_v4_len: Option<u8>,
#[arg(
long,
value_name = "LENGTH",
value_parser = clap::value_parser!(u8).range(..=128)
)]
limit_v6_len: Option<u8>,
#[arg(long)]
allow_dubious_hosts: bool,
#[arg(long)]
fresh: bool,
#[arg(long)]
disable_rsync: bool,
#[arg(long, value_name="COMMAND")]
rsync_command: Option<String>,
#[arg(long, value_name = "SECONDS")]
rsync_timeout: Option<u64>,
#[arg(long)]
disable_rrdp: bool,
#[arg(long, value_name = "COUNT")]
rrdp_max_delta_count: Option<usize>,
#[arg(long, value_name = "LEN")]
rrdp_max_delta_list_len: Option<usize>,
#[arg(long, value_name = "POLICY")]
rrdp_fallback: Option<FallbackPolicy>,
#[arg(long, value_name = "SECONDS")]
rrdp_fallback_time: Option<u64>,
#[arg(long, value_name = "SECONDS")]
rrdp_timeout: Option<u64>,
#[arg(long, value_name = "SECONDS")]
rrdp_read_timeout: Option<u64>,
#[arg(long, value_name = "SECONDS")]
rrdp_connect_timeout: Option<u64>,
#[arg(long, value_name = "SECONDS")]
rrdp_tcp_keepalive: Option<u64>,
#[arg(long, value_name = "ADDR")]
rrdp_local_addr: Option<IpAddr>,
#[arg(long, value_name = "PATH")]
rrdp_root_cert: Option<Vec<PathBuf>>,
#[arg(long, value_name = "URI")]
rrdp_proxy: Option<Vec<String>>,
#[arg(long, value_name = "BYTES")]
max_object_size: Option<u64>,
#[arg(long, value_name = "COUNT")]
max_ca_depth: Option<usize>,
#[arg(long)]
enable_bgpsec: bool,
#[arg(long)]
enable_aspa: bool,
#[arg(long)]
dirty_repository: bool,
#[arg(long, value_name = "COUNT")]
validation_threads: Option<usize>,
#[arg(short, long, action = ArgAction::Count)]
verbose: u8,
#[arg(short, long, action = ArgAction::Count, conflicts_with = "verbose")]
quiet: u8,
#[cfg(unix)]
#[arg(long)]
syslog: bool,
#[cfg(unix)]
#[arg(long, value_name = "FACILITY")]
syslog_facility: Option<String>,
#[arg(long, value_name = "PATH")]
logfile: Option<String>,
#[arg(long)]
log_repository_issues: bool,
}
#[derive(Clone, Debug, Parser)]
struct ServerArgs {
#[arg(long, value_name = "SECONDS")]
refresh: Option<u64>,
#[arg(long, value_name = "SECONDS")]
min_refresh: Option<u64>,
#[arg(long, value_name = "SECONDS")]
retry: Option<u64>,
#[arg(long, value_name = "SECONDS")]
expire: Option<u64>,
#[arg(long, value_name = "COUNT")]
history: Option<usize>,
#[arg(long = "rtr", value_name = "ADDR:PORT")]
rtr_listen: Option<Vec<SocketAddr>>,
#[arg(long = "rtr-tls", value_name = "ADDR:PORT")]
rtr_tls_listen: Option<Vec<SocketAddr>>,
#[arg(long = "http", value_name = "ADDR:PORT")]
http_listen: Option<Vec<SocketAddr>>,
#[arg(long = "http-tls", value_name = "ADDR:PORT")]
http_tls_listen: Option<Vec<SocketAddr>>,
#[arg(long)]
systemd_listen: bool,
#[arg(long, value_name = "SECONDS")]
rtr_tcp_keepalive: Option<u64>,
#[arg(long)]
rtr_client_metrics: bool,
#[arg(long, value_name = "PATH")]
rtr_tls_key: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
rtr_tls_cert: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
http_tls_key: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
http_tls_cert: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pid_file: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
working_dir: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
chroot: Option<PathBuf>,
#[arg(long, value_name = "UID")]
user: Option<String>,
#[arg(long, value_name = "GID")]
group: Option<String>,
}
#[derive(Clone, Debug)]
struct ConfigFile {
content: toml::DocumentMut,
path: PathBuf,
dir: PathBuf,
}
impl ConfigFile {
#[allow(clippy::verbose_file_reads)]
fn read(path: &Path) -> Result<Option<Self>, Failed> {
let mut file = match fs::File::open(path) {
Ok(file) => file,
Err(_) => return Ok(None)
};
let mut config = String::new();
if let Err(err) = file.read_to_string(&mut config) {
error!(
"Failed to read config file {}: {}",
path.display(), err
);
return Err(Failed);
}
Self::parse(&config, path).map(Some)
}
fn parse(content: &str, path: &Path) -> Result<Self, Failed> {
let content = match toml::DocumentMut::from_str(content) {
Ok(content) => content,
Err(err) => {
eprintln!(
"Failed to parse config file {}: {}",
path.display(), err
);
return Err(Failed);
}
};
let dir = if path.is_relative() {
path.join(match env::current_dir() {
Ok(dir) => dir,
Err(err) => {
error!(
"Fatal: Can't determine current directory: {err}."
);
return Err(Failed);
}
}).parent().unwrap().into() }
else {
path.parent().unwrap().into()
};
Ok(ConfigFile {
content,
path: path.into(),
dir
})
}
fn take_value(
&mut self, key: &str
) -> Result<Option<toml::Value>, Failed> {
match self.content.remove(key) {
Some(toml::Item::Value(value)) => Ok(Some(value)),
Some(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be a value.",
self.path.display(), key
);
Err(Failed)
}
None => Ok(None)
}
}
fn take_bool(&mut self, key: &str) -> Result<Option<bool>, Failed> {
match self.take_value(key)? {
Some(toml::Value::Boolean(res)) => Ok(Some(res.into_value())),
Some(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be a boolean.",
self.path.display(), key
);
Err(Failed)
}
None => Ok(None)
}
}
fn take_u64(&mut self, key: &str) -> Result<Option<u64>, Failed> {
match self.take_value(key)? {
Some(toml::Value::Integer(value)) => {
match u64::try_from(value.into_value()) {
Ok(value) => Ok(Some(value)),
Err(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be a positive integer.",
self.path.display(), key
);
Err(Failed)
}
}
}
Some(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be an integer.",
self.path.display(), key
);
Err(Failed)
}
None => Ok(None)
}
}
fn take_limited_u8(
&mut self, key: &str, limit: u8,
) -> Result<Option<u8>, Failed> {
match self.take_u64(key)? {
Some(value) => {
match u8::try_from(value) {
Ok(value) => {
if value > limit {
error!(
"Failed in config file {}: \
'{}' expected integer between 0 and {}.",
self.path.display(), key, limit,
);
Err(Failed)
}
else {
Ok(Some(value))
}
}
Err(_) => {
error!(
"Failed in config file {}: \
'{}' expected integer between 0 and {}.",
self.path.display(), key, limit,
);
Err(Failed)
}
}
}
None => Ok(None)
}
}
fn take_usize(&mut self, key: &str) -> Result<Option<usize>, Failed> {
match self.take_u64(key)? {
Some(value) => {
match usize::try_from(value) {
Ok(value) => Ok(Some(value)),
Err(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be a positive integer.",
self.path.display(), key
);
Err(Failed)
}
}
}
None => Ok(None)
}
}
fn take_small_usize(&mut self, key: &str) -> Result<Option<usize>, Failed> {
match self.take_usize(key)? {
Some(value) => {
if value > u16::MAX.into() {
error!(
"Failed in config file {}: \
value for '{}' is too large.",
self.path.display(), key
);
Err(Failed)
}
else {
Ok(Some(value))
}
}
None => Ok(None)
}
}
fn take_string(&mut self, key: &str) -> Result<Option<String>, Failed> {
match self.take_value(key)? {
Some(toml::Value::String(value)) => {
Ok(Some(value.into_value()))
}
Some(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be a string.",
self.path.display(), key
);
Err(Failed)
}
None => Ok(None)
}
}
fn take_from_str<T>(&mut self, key: &str) -> Result<Option<T>, Failed>
where T: FromStr, T::Err: fmt::Display {
match self.take_string(key)? {
Some(value) => {
match T::from_str(&value) {
Ok(some) => Ok(Some(some)),
Err(err) => {
error!(
"Failed in config file {}: \
illegal value in '{}': {}.",
self.path.display(), key, err
);
Err(Failed)
}
}
}
None => Ok(None)
}
}
fn take_path(&mut self, key: &str) -> Result<Option<PathBuf>, Failed> {
self.take_string(key).map(|opt| opt.map(|path| self.dir.join(path)))
}
fn take_mandatory_path(&mut self, key: &str) -> Result<PathBuf, Failed> {
match self.take_path(key)? {
Some(res) => Ok(res),
None => {
error!(
"Failed in config file {}: missing required '{}'.",
self.path.display(), key
);
Err(Failed)
}
}
}
fn take_string_array(
&mut self,
key: &str
) -> Result<Option<Vec<String>>, Failed> {
match self.take_value(key)? {
Some(toml::Value::Array(vec)) => {
let mut res = Vec::new();
for value in vec.into_iter() {
if let toml::Value::String(value) = value {
res.push(value.into_value())
}
else {
error!(
"Failed in config file {}: \
'{}' expected to be a array of strings.",
self.path.display(),
key
);
return Err(Failed);
}
}
Ok(Some(res))
}
Some(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be a array of strings.",
self.path.display(), key
);
Err(Failed)
}
None => Ok(None)
}
}
fn take_from_str_array<T>(
&mut self,
key: &str
) -> Result<Option<Vec<T>>, Failed>
where T: FromStr, T::Err: fmt::Display {
match self.take_value(key)? {
Some(toml::Value::Array(vec)) => {
let mut res = Vec::new();
for value in vec.into_iter() {
if let toml::Value::String(value) = value {
match T::from_str(value.value()) {
Ok(value) => res.push(value),
Err(err) => {
error!(
"Failed in config file {}: \
Invalid value in '{}': {}",
self.path.display(), key, err
);
return Err(Failed)
}
}
}
else {
error!(
"Failed in config file {}: \
'{}' expected to be a array of strings.",
self.path.display(),
key
);
return Err(Failed)
}
}
Ok(Some(res))
}
Some(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be a array of strings.",
self.path.display(), key
);
Err(Failed)
}
None => Ok(None)
}
}
fn take_path_array(
&mut self,
key: &str
) -> Result<Option<Vec<PathBuf>>, Failed> {
match self.take_value(key)? {
Some(toml::Value::String(value)) => {
Ok(Some(vec![self.dir.join(value.into_value())]))
}
Some(toml::Value::Array(vec)) => {
let mut res = Vec::new();
for value in vec.into_iter() {
if let toml::Value::String(value) = value {
res.push(self.dir.join(value.into_value()))
}
else {
error!(
"Failed in config file {}: \
'{}' expected to be a array of paths.",
self.path.display(),
key
);
return Err(Failed);
}
}
Ok(Some(res))
}
Some(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be a array of paths.",
self.path.display(), key
);
Err(Failed)
}
None => Ok(None)
}
}
fn take_string_map(
&mut self,
key: &str
) -> Result<Option<HashMap<String, String>>, Failed> {
match self.take_value(key)? {
Some(toml::Value::Array(vec)) => {
let mut res = HashMap::new();
for value in vec.into_iter() {
let mut pair = match value {
toml::Value::Array(pair) => pair.into_iter(),
_ => {
error!(
"Failed in config file {}: \
'{}' expected to be a array of string pairs.",
self.path.display(),
key
);
return Err(Failed);
}
};
let left = match pair.next() {
Some(toml::Value::String(value)) => value,
_ => {
error!(
"Failed in config file {}: \
'{}' expected to be a array of string pairs.",
self.path.display(),
key
);
return Err(Failed);
}
};
let right = match pair.next() {
Some(toml::Value::String(value)) => value,
_ => {
error!(
"Failed in config file {}: \
'{}' expected to be a array of string pairs.",
self.path.display(),
key
);
return Err(Failed);
}
};
if pair.next().is_some() {
error!(
"Failed in config file {}: \
'{}' expected to be a array of string pairs.",
self.path.display(),
key
);
return Err(Failed);
}
if res.insert(
left.into_value(), right.into_value()
).is_some() {
error!(
"Failed in config file {}: \
'duplicate item in '{}'.",
self.path.display(),
key
);
return Err(Failed);
}
}
Ok(Some(res))
}
Some(_) => {
error!(
"Failed in config file {}: \
'{}' expected to be a array of string pairs.",
self.path.display(), key
);
Err(Failed)
}
None => Ok(None)
}
}
fn check_exhausted(&self) -> Result<(), Failed> {
if !self.content.is_empty() {
print!(
"Failed in config file {}: Unknown settings ",
self.path.display()
);
let mut first = true;
for (key, _) in self.content.iter() {
if !first {
print!(",");
}
else {
first = false
}
print!("{key}");
}
error!(".");
Err(Failed)
}
else {
Ok(())
}
}
}
#[cfg(unix)]
fn facility_to_string(facility: Facility) -> String {
use syslog::Facility::*;
match facility {
LOG_KERN => "kern",
LOG_USER => "user",
LOG_MAIL => "mail",
LOG_DAEMON => "daemon",
LOG_AUTH => "auth",
LOG_SYSLOG => "syslog",
LOG_LPR => "lpr",
LOG_NEWS => "news",
LOG_UUCP => "uucp",
LOG_CRON => "cron",
LOG_AUTHPRIV => "authpriv",
LOG_FTP => "ftp",
LOG_NTP => "ntp",
LOG_AUDIT => "audit",
LOG_ALERT => "alert",
LOG_CLOCK_DAEMON => "clockdaemon",
LOG_LOCAL0 => "local0",
LOG_LOCAL1 => "local1",
LOG_LOCAL2 => "local2",
LOG_LOCAL3 => "local3",
LOG_LOCAL4 => "local4",
LOG_LOCAL5 => "local5",
LOG_LOCAL6 => "local6",
LOG_LOCAL7 => "local7",
}.into()
}
#[cfg(test)]
mod test {
use super::*;
fn get_default_config() -> Config {
::std::env::set_var("HOME", "/home/test");
Config::default()
}
fn process_basic_args(args: &[&str]) -> Config {
let mut config = get_default_config();
config.apply_arg_matches(
&Config::config_args(Command::new("routinator"))
.get_matches_from(args),
Path::new("/test")
).unwrap();
config
}
fn process_server_args(args: &[&str]) -> Config {
let mut config = get_default_config();
let matches = Config::server_args(Config::config_args(
Command::new("routinator"))
).get_matches_from(args);
config.apply_arg_matches(&matches, Path::new("/test")).unwrap();
config.apply_server_arg_matches(&matches, Path::new("/test")).unwrap();
config
}
#[test]
#[cfg(unix)]
fn default_config() {
let config = get_default_config();
assert_eq!(
config.cache_dir,
home_dir().unwrap().join(".rpki-cache").join("repository")
);
assert!(config.extra_tals_dir.is_none());
assert!(config.exceptions.is_empty());
assert_eq!(config.strict, DEFAULT_STRICT);
assert_eq!(
config.validation_threads,
Config::default_validation_threads(),
);
assert_eq!(config.refresh, Duration::from_secs(DEFAULT_REFRESH));
assert_eq!(config.min_refresh, None);
assert_eq!(config.retry, Duration::from_secs(DEFAULT_RETRY));
assert_eq!(config.expire, Duration::from_secs(DEFAULT_EXPIRE));
assert_eq!(config.history_size, DEFAULT_HISTORY_SIZE);
assert!(config.rtr_listen.is_empty());
assert!(config.http_listen.is_empty());
assert!(!config.systemd_listen);
assert_eq!(config.log_level, LevelFilter::Warn);
assert_eq!(config.log_target, LogTarget::Default(Facility::LOG_DAEMON));
}
#[test]
#[cfg(unix)] fn good_config_file() {
let config = ConfigFile::parse(
"repository-dir = \"/repodir\"\n\
extra-tals-dir = \"taldir\"\n\
exceptions = [\"ex1\", \"/ex2\"]\n\
strict = true\n\
validation-threads = 1000\n\
refresh = 6\n\
retry = 7\n\
expire = 8\n\
history-size = 5000\n\
rtr-listen = [\"[2001:db8::4]:323\", \"192.0.2.4:323\"]\n\
http-listen = [\"192.0.2.4:8080\"]\n\
systemd-listen = true\n\
log-level = \"info\"\n\
log = \"file\"\n\
log-file = \"foo.log\"",
Path::new("/test/routinator.conf")
).unwrap();
let config = Config::from_config_file(config).unwrap();
assert_eq!(config.cache_dir.to_str().unwrap(), "/repodir");
assert_eq!(
config.extra_tals_dir.unwrap().to_str().unwrap(),
"/test/taldir"
);
assert_eq!(
config.exceptions,
vec![PathBuf::from("/test/ex1"), PathBuf::from("/ex2")]
);
assert!(config.strict);
assert_eq!(config.validation_threads, 1000);
assert_eq!(config.refresh, Duration::from_secs(6));
assert_eq!(config.retry, Duration::from_secs(7));
assert_eq!(config.expire, Duration::from_secs(8));
assert_eq!(config.history_size, 5000);
assert_eq!(
config.rtr_listen,
vec![
SocketAddr::from_str("[2001:db8::4]:323").unwrap(),
SocketAddr::from_str("192.0.2.4:323").unwrap(),
]
);
assert_eq!(
config.http_listen,
vec![SocketAddr::from_str("192.0.2.4:8080").unwrap()]
);
assert!(config.systemd_listen);
assert_eq!(config.log_level, LevelFilter::Info);
assert_eq!(
config.log_target,
LogTarget::File(PathBuf::from("/test/foo.log"))
);
}
#[test]
#[cfg(unix)] fn minimal_config_file() {
let config = ConfigFile::parse(
"repository-dir = \"/repodir\"\n\
extra-tals-dir = \"taldir\"",
Path::new("/test/routinator.conf")
).unwrap();
let config = Config::from_config_file(config).unwrap();
assert_eq!(config.cache_dir.to_str().unwrap(), "/repodir");
assert_eq!(
config.extra_tals_dir.unwrap().to_str().unwrap(), "/test/taldir"
);
assert!(config.exceptions.is_empty());
assert!(!config.strict);
assert_eq!(
config.validation_threads,
Config::default_validation_threads()
);
assert_eq!(config.refresh, Duration::from_secs(DEFAULT_REFRESH));
assert_eq!(config.retry, Duration::from_secs(DEFAULT_RETRY));
assert_eq!(config.expire, Duration::from_secs(DEFAULT_EXPIRE));
assert_eq!(config.history_size, DEFAULT_HISTORY_SIZE);
assert!(config.rtr_listen.is_empty());
assert!(config.http_listen.is_empty());
assert!(!config.systemd_listen);
assert!(config.http_listen.is_empty());
assert_eq!(config.log_level, LevelFilter::Warn);
assert_eq!(
config.log_target,
LogTarget::default()
);
}
#[test]
fn read_your_own_config() {
let out_config = get_default_config();
let out_path = out_config.config_file.clone();
let out_file = out_config.to_string();
let in_file = ConfigFile::parse(
&out_file, &out_path
).unwrap();
let in_config = Config::from_config_file(in_file).unwrap();
assert_eq!(out_config, in_config);
}
#[test]
#[cfg(unix)]
fn basic_args() {
let config = process_basic_args(&[
"routinator", "-r", "/repository",
"-x", "/x1", "--exceptions", "x2", "--strict",
"--validation-threads", "2000",
"--syslog", "--syslog-facility", "auth"
]);
assert_eq!(config.cache_dir, Path::new("/repository"));
assert_eq!(
config.exceptions, [Path::new("/x1"), Path::new("/test/x2")]
);
assert!(config.strict);
assert_eq!(config.validation_threads, 2000);
assert_eq!(config.log_target, LogTarget::Syslog(Facility::LOG_AUTH));
}
#[test]
fn verbosity() {
let config = process_basic_args(&["routinator"]);
assert_eq!(config.log_level, LevelFilter::Warn);
let config = process_basic_args(&["routinator", "-v"]);
assert_eq!(config.log_level, LevelFilter::Info);
let config = process_basic_args(&["routinator", "-vv"]);
assert_eq!(config.log_level, LevelFilter::Debug);
let config = process_basic_args(&["routinator", "-q"]);
assert_eq!(config.log_level, LevelFilter::Error);
let config = process_basic_args(&["routinator", "-qq"]);
assert_eq!(config.log_level, LevelFilter::Off);
}
#[test]
fn server_args() {
let config = process_server_args(&[
"routinator", "--refresh", "7", "--retry", "8", "--expire", "9",
"--history", "1000",
"--rtr", "[2001:db8::4]:323",
"--rtr", "192.0.2.4:323",
"--http", "192.0.2.4:8080",
"--systemd-listen",
]);
assert_eq!(config.refresh, Duration::from_secs(7));
assert_eq!(config.retry, Duration::from_secs(8));
assert_eq!(config.expire, Duration::from_secs(9));
assert_eq!(config.history_size, 1000);
assert_eq!(
config.rtr_listen,
vec![
SocketAddr::from_str("[2001:db8::4]:323").unwrap(),
SocketAddr::from_str("192.0.2.4:323").unwrap(),
]
);
assert_eq!(
config.http_listen,
vec![SocketAddr::from_str("192.0.2.4:8080").unwrap()]
);
assert!(config.systemd_listen);
}
#[test]
fn check_args() {
crate::operation::Operation::config_args(
GlobalArgs::augment_args(Command::new("test"))
).debug_assert();
}
}