#![deny(warnings)]
use std::env;
use std::fmt;
use std::fs::File;
use std::io::prelude::*;
use std::marker::PhantomData;
use std::vec::Vec;
use anyhow::{Context, Result};
use serde::de;
use serde::{Deserialize, Deserializer};
use tracing::{trace, warn};
/// The struct representation of a Kapiti TOML config file.
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
/// Where temporary files, such as downloaded override/block files, should be stored. Defaults to `/tmp/kapiti`.
#[serde(default = "default_storage")]
pub storage: String,
/// If the service is started as the root user, what user it should "downgrade" to.
/// Defaults to `nobody`, or may be set to an empty string to disable.
#[serde(default = "default_user")]
pub user: String,
/// Listen endpoint for the server. Currently defaults to `127.0.0.1:53` but may switch to `0.0.0.0:53` in the future.
#[serde(default = "default_listen")]
pub listen: String,
/// One or more DNS server endpoints.
/// These may be provided as hostnames (required for DNS-over-HTTPS), but at least one upstream entry must be provided as an IP to "bootstrap" any hostname entries.
/// Entries are in order of priority, where earlier entries are used before trying later entries. The following protocols are supported:
/// - `127.0.0.1[:53]` for a "classic" port 53 UDP+TCP endpoint, where UDP will take priority
/// - `udp://127.0.0.1[:53]` for a "classic" port 53 endpoint that's UDP-only
/// - `tcp://127.0.0.1[:53]` for a "classic" port 53 endpoint that's TCP-only
/// - `https://example.com[:443]/dns-query` for a DNS-over-HTTP endpoint, see https://github.com/curl/curl/wiki/DNS-over-HTTPS#publicly-available-servers
/// Note that must currently be provided as a hostname due to a bug in rustls: https://github.com/ctz/rustls/issues/184
/// - COMING SOON (TODO(#5)): `tls://127.0.0.1[:853]` for a DNS-over-TLS endpoint, see https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Public+Resolvers
#[serde(default)]
#[serde(alias = "upstream")]
#[serde(deserialize_with = "string_or_seq_string")]
pub upstreams: Vec<String>,
/// Zero or more file paths or URLs for block files, where URLs will be automatically updated periodically.
/// Each listed file should look similar to a `/etc/hosts` file: a newline-separated list of hostnames paired with IP destinations.
#[serde(default)]
#[serde(alias = "override")]
#[serde(deserialize_with = "string_or_seq_string")]
pub overrides: Vec<String>,
/// Zero or more file paths or URLs for block files, where URLs will be automatically updated periodically.
/// Each listed file should contain a newline-separated list of hostnames to be blocked (along with their subdomains).
#[serde(default)]
#[serde(alias = "block")]
#[serde(deserialize_with = "string_or_seq_string")]
pub blocks: Vec<String>,
/// URL endpoint for a Redis cache.
/// For example `redis://127.0.0.1:6379/0`, or `redis://user:password@redis.local:6379/0`.
/// Or empty/unset to disable caching.
#[serde(default)]
pub redis: String,
}
impl Config {
/// Returns a new `Config` instance suitable for use in benchmark tests.
/// Most values are empty or left as their defaults, while the "listen" value is set to `127.0.0.1:0` for an ephemeral port.
pub fn new_for_test(storage: &str, upstream: String) -> Config {
Config {
storage: storage.to_string(),
// Disable user downgrade to avoid system-specific issues (what if 'nobody' doesn't exist in the test environment?)
user: "".to_string(),
listen: "127.0.0.1:0".to_string(),
upstreams: vec![upstream],
overrides: vec![],
blocks: vec![],
redis: "".to_string(),
}
}
}
fn default_storage() -> String {
"/tmp/kapiti".to_string()
}
fn default_user() -> String {
"nobody".to_string()
}
fn default_listen() -> String {
// TODO(#8) switch default to 0.0.0.0:53 if/when confident about service (rate limiting?)
"127.0.0.1:53".to_string()
}
/// A config value that can be provided either as a string or a list of strings in the TOML file.
/// We convert both cases to a `Vec<String>` automatically.
fn string_or_seq_string<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrVec(PhantomData<Vec<String>>);
impl<'de> de::Visitor<'de> for StringOrVec {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or list of strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![value.to_owned()])
}
fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor))
}
}
deserializer.deserialize_any(StringOrVec(PhantomData))
}
pub fn parse_config_file(config_path: &String) -> Result<Config> {
let mut config = String::new();
File::open(&config_path)?.read_to_string(&mut config)?;
// For each "CONFIG_FOO" envvar, replace any instances of "{{FOO}}" with the envvar value
for (oskey, osvalue) in env::vars_os() {
if let Some(key) = oskey.to_str() {
if !key.starts_with("CONFIG_") {
continue;
}
if let Some(value) = osvalue.to_str() {
let replaceme = format!("{{{{{}}}}}", key.to_string()[7..].to_string());
config = config.replace(replaceme.as_str(), value);
} else {
warn!(
"Envvar {} value is not valid UTF8, templating disabled",
key
);
}
}
}
trace!("Rendered config:\n{}", config);
toml::from_str(config.as_str()).with_context(|| "Failed to parse TOML config")
}