use std::{env, fmt, fs, process};
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::io::Read;
use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf};
use std::str::FromStr;
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 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(300);
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_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 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_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 rrdp_keep_responses: Option<PathBuf>,
pub max_object_size: Option<u64>,
pub max_ca_depth: usize,
pub enable_bgpsec: bool,
pub dirty_repository: bool,
pub validation_threads: usize,
pub refresh: 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 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_timeout {
self.rrdp_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(path) = args.rrdp_keep_responses {
self.rrdp_keep_responses = Some(path)
}
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.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
}
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.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 {
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_timeout: {
match file.take_u64("rrdp-timeout")? {
Some(0) => None,
Some(value) => Some(Duration::from_secs(value)),
None => Some(DEFAULT_RRDP_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(),
rrdp_keep_responses: file.take_path("rrdp-keep-responses")?,
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),
dirty_repository: file.take_bool("dirty")?.unwrap_or(false),
validation_threads: {
file.take_small_usize("validation-threads")?
.unwrap_or_else(::num_cpus::get)
},
refresh: {
Duration::from_secs(
file.take_u64("refresh")?.unwrap_or(DEFAULT_REFRESH)
)
},
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,
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)
}
}
}
fn default_with_paths(cache_dir: PathBuf) -> Self {
Config {
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_timeout: Some(DEFAULT_RRDP_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(),
rrdp_keep_responses: None,
max_object_size: Some(DEFAULT_MAX_OBJECT_SIZE),
max_ca_depth: DEFAULT_MAX_CA_DEPTH,
enable_bgpsec: false,
dirty_repository: DEFAULT_DIRTY_REPOSITORY,
validation_threads: ::num_cpus::get(),
refresh: Duration::from_secs(DEFAULT_REFRESH),
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(),
pid_file: None,
working_dir: None,
chroot: None,
user: None,
group: None,
tal_labels: HashMap::new(),
}
}
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::Value {
let mut res = toml::value::Table::new();
res.insert(
"repository-dir".into(),
self.cache_dir.display().to_string().into()
);
if let Some(extra_tals_dir) = self.extra_tals_dir.as_ref() {
res.insert(
"extra-tals-dir".into(),
extra_tals_dir.display().to_string().into()
);
}
res.insert(
"exceptions".into(),
toml::Value::Array(
self.exceptions.iter()
.map(|p| p.display().to_string().into())
.collect()
)
);
res.insert("strict".into(), self.strict.into());
res.insert("stale".into(), format!("{}", self.stale).into());
res.insert(
"unsafe-vrps".into(), format!("{}", self.unsafe_vrps).into()
);
res.insert(
"unknown-objects".into(),
format!("{}", self.unknown_objects).into(),
);
if let Some(value) = self.limit_v4_len {
res.insert("limit-v4-len".into(), value.into());
}
if let Some(value) = self.limit_v6_len {
res.insert("limit-v6-len".into(), value.into());
}
res.insert(
"allow-dubious-hosts".into(), self.allow_dubious_hosts.into()
);
res.insert("disable-rsync".into(), self.disable_rsync.into());
res.insert("rsync-command".into(), self.rsync_command.clone().into());
if let Some(ref args) = self.rsync_args {
res.insert(
"rsync-args".into(),
toml::Value::Array(
args.iter().map(|a| a.clone().into()).collect()
)
);
}
res.insert(
"rsync-timeout".into(),
match self.rsync_timeout {
None => 0.into(),
Some(value) => {
value.as_secs().try_into().unwrap_or(i64::MAX).into()
}
}
);
res.insert("disable-rrdp".into(), self.disable_rrdp.into());
res.insert(
"rrdp-fallback".into(),
self.rrdp_fallback.to_string().into(),
);
res.insert(
"rrdp-fallback-time".into(),
(self.rrdp_fallback_time.as_secs() as i64).into()
);
res.insert(
"rrdp-max-delta-count".into(),
i64::try_from(self.rrdp_max_delta_count).unwrap_or(i64::MAX).into()
);
res.insert(
"rrdp-timeout".into(),
match self.rrdp_timeout {
None => 0.into(),
Some(value) => {
value.as_secs().try_into().unwrap_or(i64::MAX).into()
}
}
);
if let Some(timeout) = self.rrdp_connect_timeout {
res.insert(
"rrdp-connect-timeout".into(),
(timeout.as_secs() as i64).into()
);
}
res.insert(
"rrdp-tcp-keepalive".into(),
match self.rrdp_tcp_keepalive {
None => 0.into(),
Some(value) => {
value.as_secs().try_into().unwrap_or(i64::MAX).into()
}
}
);
if let Some(addr) = self.rrdp_local_addr {
res.insert("rrdp-local-addr".into(), addr.to_string().into());
}
res.insert(
"rrdp-root-certs".into(),
toml::Value::Array(
self.rrdp_root_certs.iter()
.map(|p| p.display().to_string().into())
.collect()
)
);
res.insert(
"rrdp-proxies".into(),
toml::Value::Array(
self.rrdp_proxies.iter().map(|s| s.clone().into()).collect()
)
);
if let Some(path) = self.rrdp_keep_responses.as_ref() {
res.insert(
"rrdp-keep-responses".into(),
format!("{}", path.display()).into()
);
}
res.insert("max-object-size".into(),
match self.max_object_size {
Some(value) => value as i64,
None => 0,
}.into()
);
res.insert("max-ca-depth".into(),
(self.max_ca_depth as i64).into()
);
res.insert("enable-bgpsec".into(), self.enable_bgpsec.into());
res.insert("dirty".into(), self.dirty_repository.into());
res.insert(
"validation-threads".into(),
(self.validation_threads as i64).into()
);
res.insert("refresh".into(), (self.refresh.as_secs() as i64).into());
res.insert("retry".into(), (self.retry.as_secs() as i64).into());
res.insert("expire".into(), (self.expire.as_secs() as i64).into());
res.insert("history-size".into(), (self.history_size as i64).into());
res.insert(
"rtr-listen".into(),
toml::Value::Array(
self.rtr_listen.iter().map(|a| a.to_string().into()).collect()
)
);
res.insert(
"rtr-tls-listen".into(),
toml::Value::Array(
self.rtr_tls_listen.iter().map(|a| {
a.to_string().into()
}).collect()
)
);
res.insert(
"http-listen".into(),
toml::Value::Array(
self.http_listen.iter().map(|a| a.to_string().into()).collect()
)
);
res.insert(
"http-tls-listen".into(),
toml::Value::Array(
self.http_tls_listen.iter().map(|a| {
a.to_string().into()
}).collect()
)
);
res.insert("systemd-listen".into(), self.systemd_listen.into());
res.insert("rtr-tcp-keepalive".into(),
match self.rtr_tcp_keepalive {
Some(keep) => (keep.as_secs() as i64).into(),
None => 0.into(),
}
);
res.insert(
"rtr-client-metrics".into(),
self.rtr_client_metrics.into()
);
if let Some(ref path) = self.rtr_tls_key {
res.insert(
"rtr-tls-key".into(),
path.display().to_string().into()
);
}
if let Some(ref path) = self.rtr_tls_cert {
res.insert(
"rtr-tls-cert".into(),
path.display().to_string().into()
);
}
if let Some(ref path) = self.http_tls_key {
res.insert(
"http-tls-key".into(),
path.display().to_string().into()
);
}
if let Some(ref path) = self.http_tls_cert {
res.insert(
"http-tls-cert".into(),
path.display().to_string().into()
);
}
res.insert("log-level".into(), self.log_level.to_string().into());
match self.log_target {
#[cfg(unix)]
LogTarget::Default(facility) => {
res.insert("log".into(), "default".into());
res.insert(
"syslog-facility".into(),
facility_to_string(facility).into()
);
}
#[cfg(unix)]
LogTarget::Syslog(facility) => {
res.insert("log".into(), "syslog".into());
res.insert(
"syslog-facility".into(),
facility_to_string(facility).into()
);
}
LogTarget::Stderr => {
res.insert("log".into(), "stderr".into());
}
LogTarget::File(ref file) => {
res.insert("log".into(), "file".into());
res.insert(
"log-file".into(),
file.display().to_string().into()
);
}
}
if let Some(ref file) = self.pid_file {
res.insert("pid-file".into(), file.display().to_string().into());
}
if let Some(ref dir) = self.working_dir {
res.insert("working-dir".into(), dir.display().to_string().into());
}
if let Some(ref dir) = self.chroot {
res.insert("chroot".into(), dir.display().to_string().into());
}
if let Some(ref user) = self.user {
res.insert("user".into(), user.clone().into());
}
if let Some(ref group) = self.group {
res.insert("group".into(), group.clone().into());
}
if !self.tal_labels.is_empty() {
res.insert(
"tal-labels".into(),
toml::Value::Array(
self.tal_labels.iter().map(|(left, right)| {
toml::Value::Array(vec![
left.clone().into(), right.clone().into()
])
}).collect()
)
);
}
res.into()
}
}
impl Default for Config {
fn default() -> Self {
match home_dir() {
Some(dir) => {
Config::default_with_paths(
dir.join(".rpki-cache/repository"),
)
}
None => {
Config::default_with_paths(
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))]
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(ref s), &LogTarget::File(ref 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 = "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_connect_timeout: Option<u64>,
#[arg(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 = "PATH")]
rrdp_keep_responses: Option<PathBuf>,
#[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)]
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>,
}
#[derive(Clone, Debug, Parser)]
struct ServerArgs {
#[arg(long, value_name = "SECONDS")]
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::value::Table,
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::from_str(content) {
Ok(toml::Value::Table(content)) => content,
Ok(_) => {
error!(
"Failed to parse config file {}: Not a mapping.",
path.display()
);
return Err(Failed);
}
Err(err) => {
error!(
"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_bool(&mut self, key: &str) -> Result<Option<bool>, Failed> {
match self.content.remove(key) {
Some(value) => {
if let toml::Value::Boolean(res) = value {
Ok(Some(res))
}
else {
error!(
"Failed in config file {}: \
'{}' expected to be a boolean.",
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.content.remove(key) {
Some(value) => {
if let toml::Value::Integer(res) = value {
if res < 0 || res > limit as i64 {
error!(
"Failed in config file {}: \
'{}' expected integer between 0 and {}.",
self.path.display(), key, limit,
);
return Err(Failed)
}
Ok(Some(res as u8))
}
else {
error!(
"Failed in config file {}: \
'{}' expected to be an integer.",
self.path.display(), key
);
Err(Failed)
}
}
None => Ok(None)
}
}
fn take_u64(&mut self, key: &str) -> Result<Option<u64>, Failed> {
match self.content.remove(key) {
Some(value) => {
if let toml::Value::Integer(res) = value {
if res < 0 {
error!(
"Failed in config file {}: \
'{}' expected to be a positive integer.",
self.path.display(), key
);
Err(Failed)
}
else {
Ok(Some(res as u64))
}
}
else {
error!(
"Failed in config file {}: \
'{}' expected to be an integer.",
self.path.display(), key
);
Err(Failed)
}
}
None => Ok(None)
}
}
fn take_usize(&mut self, key: &str) -> Result<Option<usize>, Failed> {
match self.content.remove(key) {
Some(value) => {
if let toml::Value::Integer(res) = value {
usize::try_from(res).map(Some).map_err(|_| {
error!(
"Failed in config file {}: \
'{}' expected to be a positive integer.",
self.path.display(), key
);
Failed
})
}
else {
error!(
"Failed in config file {}: \
'{}' expected to be an integer.",
self.path.display(), key
);
Err(Failed)
}
}
None => Ok(None)
}
}
fn take_small_usize(&mut self, key: &str) -> Result<Option<usize>, Failed> {
match self.content.remove(key) {
Some(value) => {
if let toml::Value::Integer(res) = value {
if res < 0 {
error!(
"Failed in config file {}: \
'{}' expected to be a positive integer.",
self.path.display(), key
);
Err(Failed)
}
else if res > ::std::u16::MAX.into() {
error!(
"Failed in config file {}: \
value for '{}' is too large.",
self.path.display(), key
);
Err(Failed)
}
else {
Ok(Some(res as usize))
}
}
else {
error!(
"Failed in config file {}: \
'{}' expected to be a integer.",
self.path.display(), key
);
Err(Failed)
}
}
None => Ok(None)
}
}
fn take_string(&mut self, key: &str) -> Result<Option<String>, Failed> {
match self.content.remove(key) {
Some(value) => {
if let toml::Value::String(res) = value {
Ok(Some(res))
}
else {
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.content.remove(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)
}
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.content.remove(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) {
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.content.remove(key) {
Some(toml::Value::String(value)) => {
Ok(Some(vec![self.dir.join(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))
}
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.content.remove(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, right).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.keys() {
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_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, ::num_cpus::get());
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_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, ::num_cpus::get());
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_file = format!("{}", out_config.to_toml());
let in_file = ConfigFile::parse(
&out_file, Path::new("/test/routinator.conf")
).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();
}
}