-
-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Folkert de Vries
committed
Nov 16, 2023
1 parent
1f37ce5
commit 1e48da4
Showing
8 changed files
with
621 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ members = [ | |
"ntp-proto", | ||
"ntp-os-clock", | ||
"ntp-udp", | ||
"nts-pool-ke", | ||
"ntpd" | ||
] | ||
exclude = [ ] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
); | ||
} | ||
} |
Oops, something went wrong.