use std::{path::PathBuf, time::Duration};
use sequoia_openpgp::crypto::Password;
use serde::{Deserialize, Serialize};
use crate::config::Credentials;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub server_hostname: String,
pub bridge_hostname: String,
pub bridge_port: u16,
pub request_timeout: Duration,
pub credentials: Credentials,
pub keys: Vec<Key>,
}
impl Default for Config {
fn default() -> Self {
Self {
server_hostname: "server.example.com".to_string(),
bridge_hostname: "bridge.example.com".to_string(),
bridge_port: 44334,
request_timeout: Duration::from_secs(30),
credentials: Credentials {
private_key: PathBuf::from("siguldry.client.private_key.pem"),
certificate: PathBuf::from("siguldry.client.certificate.pem"),
ca_certificate: PathBuf::from("siguldry.ca_certificate.pem"),
},
keys: vec![],
}
}
}
#[cfg(feature = "cli")]
impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
toml::ser::to_string_pretty(&self).unwrap_or_default()
)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Key {
pub key_name: String,
pub passphrase_path: PathBuf,
#[serde(skip)]
pub(crate) passphrase: Password,
}
impl Key {
#[doc(hidden)]
pub fn private_new(key_name: String, passphrase_path: PathBuf) -> Self {
Self {
key_name,
passphrase_path,
passphrase: "".into(),
}
}
}
impl<'de> Deserialize<'de> for Key {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct KeyHelper {
key_name: String,
passphrase_path: PathBuf,
}
let helper = KeyHelper::deserialize(deserializer)?;
let passphrase = std::fs::read_to_string(&helper.passphrase_path)
.map_err(|e| {
serde::de::Error::custom(format!(
"Failed to read passphrase file {}: {}",
helper.passphrase_path.display(),
e
))
})?
.lines()
.next()
.and_then(|pass| {
let pass = pass.trim();
if !pass.is_empty() { Some(pass) } else { None }
})
.ok_or_else(|| {
serde::de::Error::custom(format!(
"Passphrase file {} does not contain a password on the first line",
helper.passphrase_path.display()
))
})?
.to_string()
.into();
Ok(Key {
key_name: helper.key_name,
passphrase_path: helper.passphrase_path,
passphrase,
})
}
}
impl Key {
pub fn password(&self) -> String {
self.passphrase
.map(|p| String::from_utf8(p.to_vec()).expect("The password deserialized to a string"))
}
}
#[cfg(test)]
mod tests {
#[test]
fn load_example_config() -> anyhow::Result<()> {
let example_conf_path =
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("client.toml.example");
let example_conf = std::fs::read_to_string(&example_conf_path)?;
toml::de::from_str::<super::Config>(&example_conf)?;
Ok(())
}
#[test]
fn config_extra_key() -> anyhow::Result<()> {
let config = r#"
server_hostname = "server.example.com"
bridge_hostname = "bridge.example.com"
bridge_port = 44333
another_key = 42
[request_timeout]
secs = 30
nanos = 0
[credentials]
private_key = "siguldry.client.private_key.pem"
certificate = "/etc/siguldry/client.cert"
ca_certificate = "/etc/siguldry/ca.crt"
"#;
if let Err(error) = toml::from_str::<super::Config>(config) {
assert!(error.message().contains("unknown field `another_key`"));
} else {
panic!("Config should fail to load");
}
Ok(())
}
#[test]
fn timeout_extra_key() -> anyhow::Result<()> {
let config = r#"
server_hostname = "server.example.com"
bridge_hostname = "bridge.example.com"
bridge_port = 44333
[request_timeout]
secs = 30
nanos = 0
another_key = 42
[credentials]
private_key = "siguldry.client.private_key.pem"
certificate = "/etc/siguldry/client.cert"
ca_certificate = "/etc/siguldry/ca.crt"
"#;
if let Err(error) = toml::from_str::<super::Config>(config) {
assert!(error.message().contains("unknown field `another_key`"));
} else {
panic!("Config should fail to load");
}
Ok(())
}
}