Skip to content

Commit

Permalink
add nts-pool-ke crate and binary
Browse files Browse the repository at this point in the history
  • Loading branch information
Folkert de Vries committed Nov 16, 2023
1 parent 1f37ce5 commit 1e48da4
Show file tree
Hide file tree
Showing 8 changed files with 621 additions and 0 deletions.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"ntp-proto",
"ntp-os-clock",
"ntp-udp",
"nts-pool-ke",
"ntpd"
]
exclude = [ ]
Expand Down
26 changes: 26 additions & 0 deletions nts-pool-ke/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "nts-pool-ke"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
readme.workspace = true
description.workspace = true
publish.workspace = true
rust-version.workspace = true

[dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "io-util", "io-std", "fs", "sync", "net", "macros", "time"] }
toml.workspace = true
tracing.workspace = true
tracing-subscriber = { version = "0.3.0", default-features = false, features = ["std", "fmt", "ansi"] }
rustls.workspace = true
rustls-pemfile.workspace = true
serde.workspace = true
ntp-proto.workspace = true
thiserror.workspace = true

[[bin]]
name = "nts-pool-ke"
path = "bin/nts-pool-ke.rs"
5 changes: 5 additions & 0 deletions nts-pool-ke/bin/nts-pool-ke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[tokio::main]
async fn main() -> ! {
let result = nts_pool_ke::nts_pool_ke_main().await;
std::process::exit(if result.is_ok() { 0 } else { 1 });
}
190 changes: 190 additions & 0 deletions nts-pool-ke/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use crate::daemon_tracing::LogLevel;
use std::path::PathBuf;
use std::str::FromStr;

const USAGE_MSG: &str = "\
usage: nts-pool-ke [-c PATH] [-l LOG_LEVEL]
nts-pool-ke -h
nts-pool-ke -v";

const DESCRIPTOR: &str = "ntp-daemon - synchronize system time";

const HELP_MSG: &str = "Options:
-c, --config=PATH change the config .toml file
-l, --log-level=LOG_LEVEL change the log level
-h, --help display this help text
-v, --version display version information";

pub fn long_help_message() -> String {
format!("{DESCRIPTOR}\n\n{USAGE_MSG}\n\n{HELP_MSG}")
}

#[derive(Debug, Default)]
pub(crate) struct NtsPoolKeOptions {
/// Path of the configuration file
pub config: Option<PathBuf>,
/// Level for messages to display in logs
pub log_level: Option<LogLevel>,
help: bool,
version: bool,
pub action: NtsPoolKeAction,
}

pub enum CliArg {
Flag(String),
Argument(String, String),
Rest(Vec<String>),
}

impl CliArg {
pub fn normalize_arguments<I>(
takes_argument: &[&str],
takes_argument_short: &[char],
iter: I,
) -> Result<Vec<Self>, String>
where
I: IntoIterator<Item = String>,
{
// the first argument is the sudo command - so we can skip it
let mut arg_iter = iter.into_iter().skip(1);
let mut processed = vec![];
let mut rest = vec![];

while let Some(arg) = arg_iter.next() {
match arg.as_str() {
"--" => {
rest.extend(arg_iter);
break;
}
long_arg if long_arg.starts_with("--") => {
// --config=/path/to/config.toml
let invalid = Err(format!("invalid option: '{long_arg}'"));

if let Some((key, value)) = long_arg.split_once('=') {
if takes_argument.contains(&key) {
processed.push(CliArg::Argument(key.to_string(), value.to_string()))
} else {
invalid?
}
} else if takes_argument.contains(&long_arg) {
if let Some(next) = arg_iter.next() {
processed.push(CliArg::Argument(long_arg.to_string(), next))
} else {
Err(format!("'{}' expects an argument", &long_arg))?;
}
} else {
processed.push(CliArg::Flag(arg));
}
}
short_arg if short_arg.starts_with('-') => {
// split combined shorthand options
for (n, char) in short_arg.trim_start_matches('-').chars().enumerate() {
let flag = format!("-{char}");
// convert option argument to seperate segment
if takes_argument_short.contains(&char) {
let rest = short_arg[(n + 2)..].trim().to_string();
// assignment syntax is not accepted for shorthand arguments
if rest.starts_with('=') {
Err("invalid option '='")?;
}
if !rest.is_empty() {
processed.push(CliArg::Argument(flag, rest));
} else if let Some(next) = arg_iter.next() {
processed.push(CliArg::Argument(flag, next));
} else if char == 'h' {
// short version of --help has no arguments
processed.push(CliArg::Flag(flag));
} else {
Err(format!("'-{}' expects an argument", char))?;
}
break;
} else {
processed.push(CliArg::Flag(flag));
}
}
}
_argument => rest.push(arg),
}
}

if !rest.is_empty() {
processed.push(CliArg::Rest(rest));
}

Ok(processed)
}
}

#[derive(Debug, Default, PartialEq, Eq)]
pub enum NtsPoolKeAction {
#[default]
Help,
Version,
Run,
}

impl NtsPoolKeOptions {
const TAKES_ARGUMENT: &'static [&'static str] = &["--config", "--log-level"];
const TAKES_ARGUMENT_SHORT: &'static [char] = &['c', 'l'];

/// parse an iterator over command line arguments
pub fn try_parse_from<I, T>(iter: I) -> Result<Self, String>
where
I: IntoIterator<Item = T>,
T: AsRef<str> + Clone,
{
let mut options = NtsPoolKeOptions::default();
let arg_iter = CliArg::normalize_arguments(
Self::TAKES_ARGUMENT,
Self::TAKES_ARGUMENT_SHORT,
iter.into_iter().map(|x| x.as_ref().to_string()),
)?
.into_iter()
.peekable();

for arg in arg_iter {
match arg {
CliArg::Flag(flag) => match flag.as_str() {
"-h" | "--help" => {
options.help = true;
}
"-v" | "--version" => {
options.version = true;
}
option => {
Err(format!("invalid option provided: {option}"))?;
}
},
CliArg::Argument(option, value) => match option.as_str() {
"-c" | "--config" => {
options.config = Some(PathBuf::from(value));
}
"-l" | "--log-level" => match LogLevel::from_str(&value) {
Ok(level) => options.log_level = Some(level),
Err(_) => return Err("invalid log level".into()),
},
option => {
Err(format!("invalid option provided: {option}"))?;
}
},
CliArg::Rest(_rest) => { /* do nothing, drop remaining arguments */ }
}
}

options.resolve_action();
// nothing to validate at the moment

Ok(options)
}

/// from the arguments resolve which action should be performed
fn resolve_action(&mut self) {
if self.help {
self.action = NtsPoolKeAction::Help;
} else if self.version {
self.action = NtsPoolKeAction::Version;
} else {
self.action = NtsPoolKeAction::Run;
}
}
}
104 changes: 104 additions & 0 deletions nts-pool-ke/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::{
net::SocketAddr,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
};

use serde::Deserialize;
use thiserror::Error;
use tracing::{info, warn};

#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Config {
pub nts_pool_ke_server: NtsPoolKeConfig,
#[serde(default)]
pub observability: ObservabilityConfig,
}

#[derive(Error, Debug)]
pub enum ConfigError {
#[error("io error while reading config: {0}")]
Io(#[from] std::io::Error),
#[error("config toml parsing error: {0}")]
Toml(#[from] toml::de::Error),
}

impl Config {
pub fn check(&self) -> bool {
true
}

async fn from_file(file: impl AsRef<Path>) -> Result<Config, ConfigError> {
let meta = std::fs::metadata(&file)?;
let perm = meta.permissions();

const S_IWOTH: u32 = 2;
if perm.mode() & S_IWOTH != 0 {
warn!("Unrestricted config file permissions: Others can write.");
}

let contents = tokio::fs::read_to_string(file).await?;
Ok(toml::de::from_str(&contents)?)
}

pub async fn from_args(file: impl AsRef<Path>) -> Result<Config, ConfigError> {
let path = file.as_ref();
info!(?path, "using config file");

let config = Config::from_file(path).await?;

Ok(config)
}
}

#[derive(Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ObservabilityConfig {
#[serde(default)]
pub log_level: Option<crate::daemon_tracing::LogLevel>,
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct NtsPoolKeConfig {
pub certificate_chain_path: PathBuf,
pub private_key_path: PathBuf,
#[serde(default = "default_nts_ke_timeout")]
pub key_exchange_timeout_ms: u64,
pub listen: SocketAddr,
}

fn default_nts_ke_timeout() -> u64 {
1000
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_deserialize_nts_pool_ke() {
let test: Config = toml::from_str(
r#"
[nts-pool-ke-server]
listen = "0.0.0.0:4460"
certificate-chain-path = "/foo/bar/baz.pem"
private-key-path = "spam.der"
"#,
)
.unwrap();

let pem = PathBuf::from("/foo/bar/baz.pem");
assert_eq!(test.nts_pool_ke_server.certificate_chain_path, pem);
assert_eq!(
test.nts_pool_ke_server.private_key_path,
PathBuf::from("spam.der")
);
assert_eq!(test.nts_pool_ke_server.key_exchange_timeout_ms, 1000,);
assert_eq!(
test.nts_pool_ke_server.listen,
"0.0.0.0:4460".parse().unwrap(),
);
}
}
Loading

0 comments on commit 1e48da4

Please sign in to comment.