From c33feb1ee7429314da9ce5d99d7bd0bd688be09e Mon Sep 17 00:00:00 2001 From: Malte Meiboom Date: Sun, 22 Feb 2026 12:51:05 +0100 Subject: [PATCH] First dispatcher - Create (sub)command structure. - First experimental dispatcher to start the daemon. --- Cargo.lock | 121 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +- src/cli/cli_args.rs | 107 +++++++++++++++++++++++++++++++++ src/cli/mod.rs | 1 + src/commands.rs | 19 ++++++ src/commands/daemon.rs | 21 +++++++ src/config.rs | 2 +- src/crypto.rs | 48 +++++++-------- src/daemon/mod.rs | 1 - src/main.rs | 19 ++++-- src/types/defaults.rs | 5 ++ src/types/husk_context.rs | 4 +- src/types/introducer.rs | 5 +- src/types/mod.rs | 2 + 14 files changed, 323 insertions(+), 35 deletions(-) create mode 100644 src/cli/cli_args.rs create mode 100644 src/cli/mod.rs create mode 100644 src/commands.rs create mode 100644 src/commands/daemon.rs create mode 100644 src/types/defaults.rs diff --git a/Cargo.lock b/Cargo.lock index b5bbcf1..37f75b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.101" @@ -254,6 +304,52 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "core-foundation" version = "0.9.4" @@ -893,6 +989,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "clap", "indymilter", "log", "log4rs", @@ -1164,6 +1261,12 @@ dependencies = [ "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -1506,6 +1609,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openpgp-cert-d" version = "0.3.4" @@ -2354,6 +2463,12 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2762,6 +2877,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 2f34943..8d717f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,12 @@ authors = [ "Malte Meiboom " ] readme = "README.md" keywords = ["cryptography", "openpgp", "pgp", "encryption", "email"] categories = ["cryptography", "email"] -rust-version = "1.79" +rust-version = "1.85" [dependencies] anyhow = "1.0.100" bytes = "1.11.1" +clap = { version = "4", features = ["derive"] } indymilter = "0.3.0" log = "0.4.27" log4rs = "1.3.0" diff --git a/src/cli/cli_args.rs b/src/cli/cli_args.rs new file mode 100644 index 0000000..122dd75 --- /dev/null +++ b/src/cli/cli_args.rs @@ -0,0 +1,107 @@ +use clap::Parser; +use clap::Subcommand; + +#[derive(Parser, Debug)] +#[clap( + name = "Husk milter", + about = "Encrypting milter.", + arg_required_else_help = true, +)] +pub struct CliArgs { + #[clap( + long, short, + help = "Configuration file.", + )] + pub config: Option, + + #[clap(subcommand)] + pub subcommand: HuskSubcommands, +} + +// Subcommands Level 1 + +#[derive(Debug, Subcommand)] +pub enum HuskSubcommands { + Daemon(DaemonCommand), + Introducer(IntroducerCommand), + Local, +} + +// Daemon subcommands + +#[derive(Parser, Debug)] +#[clap( + name = "daemon", + about = "Manage milter daemon.", + subcommand_required = true, + arg_required_else_help = true, + disable_colored_help = true, + disable_version_flag = true, +)] +pub struct DaemonCommand { + #[clap(subcommand)] + pub subcommand: DaemonSubcommand, +} +#[derive(Debug, Subcommand)] +pub enum DaemonSubcommand { + Start, + Stop, + Status +} + +// Introducer subcommands + +#[derive(Parser, Debug)] +#[clap( + name = "introducer", + about = "Manage introducers.", + subcommand_required = true, + arg_required_else_help = true, + disable_colored_help = true, + disable_version_flag = true, +)] +pub struct IntroducerCommand { + #[clap(subcommand)] + pub subcommand: IntroducerSubcommands, +} +#[derive(Debug, Subcommand)] +pub enum IntroducerSubcommands { + Add(IntroducerAddCommand), + Remove(IntroducerRemoveCommand), + List +} + +#[derive(Parser, Debug)] +#[clap( + name = "introducer", + about = "Add introducers.", +)] +pub struct IntroducerAddCommand { + #[clap( + long = "cert", + help = "Certificate to declare as introducer." + )] + pub cert: String, + + #[clap( + long = "domains", + help = "Domains this certificate introduces.", + required = true, + num_args = 1.., + value_delimiter = ' ' + )] + pub domains: Vec, +} + +#[derive(Parser, Debug)] +#[clap( + name = "introducer", + about = "Remove introducers.", +)] +pub struct IntroducerRemoveCommand { + #[clap( + long = "cert", + help = "Certificate to be removed as introducer." + )] + pub cert: String, +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..aa6a42c --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1 @@ +pub mod cli_args; diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..2ca8edf --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,19 @@ +use anyhow::Result; + +use crate::commands; +use crate::cli::cli_args::{CliArgs, HuskSubcommands}; +use crate::config::HuskConfigContainer; + +pub mod daemon; + +pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> { + + match cli.subcommand { + HuskSubcommands::Daemon(subcmd) => { + commands::daemon::dispatch(subcmd, config).await?; + }, + _ => { println!("something else"); } + } + + Ok(()) +} diff --git a/src/commands/daemon.rs b/src/commands/daemon.rs new file mode 100644 index 0000000..3405484 --- /dev/null +++ b/src/commands/daemon.rs @@ -0,0 +1,21 @@ +use anyhow::Result; + +use crate::cli::cli_args::{DaemonCommand, DaemonSubcommand}; +use crate::config::HuskConfigContainer; +use crate::Daemon; + +pub async fn dispatch(cmd: DaemonCommand, config: HuskConfigContainer) -> Result<()> { + + match cmd.subcommand { + DaemonSubcommand::Start => { + Daemon::run(config).await?; + }, + DaemonSubcommand::Stop => { + println!("stop"); + }, + DaemonSubcommand::Status => { + println!("status"); + } + } + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 6e4eb0c..5e95778 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,7 +19,7 @@ pub struct HuskConfig { impl HuskConfig { - pub fn load(path: &String) -> anyhow::Result { + pub fn load(path: &str) -> anyhow::Result { match fs::read_to_string(path) { Ok(data) => { match toml::from_str(data.as_str()) { diff --git a/src/crypto.rs b/src/crypto.rs index a1d9824..d6d0b23 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -26,7 +26,7 @@ use wot::{Depth, Path}; use crate::types::errors::HuskError; use crate::types::husk_context::HuskContext; -use crate::types::introducer::{self, Introducer}; +use crate::types::introducer::Introducer; use crate::types::recipient::Recipient; /// Returns the local trust root from the cert_store. If the trust root @@ -72,7 +72,7 @@ pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result { pub fn get_local_certificates<'hc>(context: &HuskContext<'hc>, email: &str) -> Vec>> { - log::debug!("local lookup for {}", email); + log::debug!("local lookup for {}", email); let cert_store = &context.cert_store; let trust_roots = vec![(context.local_trust_root.fingerprint(), wot::FULLY_TRUSTED)]; @@ -107,14 +107,14 @@ const USER_AGENT: &'static str = concat!("husk/", env!("CARGO_PKG_VERSION")); const CONNECT_TIMEOUT: Duration = Duration::new(5, 0); const REQUEST_TIMEOUT: Duration = Duration::new(5, 0); -/// Try to fetch certificates for `email` from online sources (keyservers, wkd, -/// dane). -pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) +/// Try to fetch certificates for `email` from online sources (keyservers, wkd, +/// dane). +pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) -> anyhow::Result> { - log::debug!("remote lookup for {}", email); + log::debug!("remote lookup for {}", email); - let mut jobs = JoinSet::new(); + let mut jobs = JoinSet::new(); let http_client = sequoia_net::reqwest::Client::builder() .user_agent(USER_AGENT) .connect_timeout(CONNECT_TIMEOUT) @@ -128,8 +128,8 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) for cert in certs { if let Ok(c) = cert { if let Ok(vc) = c.with_policy(policy, None) { - - // check if the returned certificate contains a userid with the + + // check if the returned certificate contains a userid with the // email address in question. let mut userid_found = false; for userid in vc.userids() { @@ -139,7 +139,7 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) } } } - + if userid_found { // check if the certificate can be used (for encryption). vc.keys() @@ -147,10 +147,10 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) .alive() .revoked(false) .for_transport_encryption() - .for_each(|_| { + .for_each(|_| { // XXX: cert gets returned for each transport key // once would be enough - result.push(c.clone()); + result.push(c.clone()); }); } } @@ -158,22 +158,22 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) } result }; - + // Keyservers for keyserver in &context.keyservers { log::debug!("querying {}", keyserver); if let Ok(ks) = KeyServer::with_client(keyserver.as_str(), http_client.clone()) { - + let e = email.to_string(); let p = StandardPolicy::new(); - jobs.spawn(async move { + jobs.spawn(async move { let mut partial_result = Vec::new(); if let Ok(certs) = ks.search(e.as_str()).await { - partial_result = collect_certs(certs, e, &p); + partial_result = collect_certs(certs, e, &p); } partial_result }); @@ -191,8 +191,8 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) } partial_result }); - - // DANE + + // DANE let e = email.to_string(); let p = StandardPolicy::new(); jobs.spawn(async move { @@ -215,13 +215,13 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) log::debug!("found cert {}", fpr); match set.entry(fpr.clone()) { - Occupied(entry) => { + Occupied(entry) => { let existing_cert = entry.into_mut(); if let Ok(merged_cert) = existing_cert.clone().merge_public(c) { *existing_cert = merged_cert; } }, - Vacant(_entry) => { + Vacant(_entry) => { set.insert(fpr.clone(), c); }, } @@ -231,7 +231,7 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) Ok(result) } - + pub async fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str) -> Vec>> { @@ -239,14 +239,14 @@ pub async fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str) log::debug!("get certificate for {}", email); let locals = get_local_certificates(context, email); - if locals.len() > 0 { - return locals; + if locals.len() > 0 { + return locals; } let mut result = Vec::new(); for introducer in &context.introducers { - log::debug!("consider introducer {:?}", + log::debug!("consider introducer {:?}", introducer.cert.userids().map(|u| u.userid()).collect::>()); if introducer.can_introduce(format!("<{}>", email).as_str()) { log::debug!(" introducer regex matches!"); diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 5a94a23..3d6be54 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -20,7 +20,6 @@ use indymilter::{ ProtoOpts, SocketInfo, Status, - Config, ContextActions, }; diff --git a/src/main.rs b/src/main.rs index 4c0c09e..56ba307 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,15 @@ // Husk milter // -use std::{env, process}; +use std::process; use std::sync::{Arc, Mutex}; +use clap::Parser; +use types::defaults; + +pub mod cli; +use cli::cli_args::CliArgs; +pub mod commands; pub mod config; use config::{HuskConfig, HuskConfigContainer}; pub mod types; @@ -15,9 +21,14 @@ use daemon::Daemon; #[tokio::main] async fn main() { + let args = CliArgs::parse(); - let config_file = env::args().nth(1).expect("config file missing"); - let husk_config = match HuskConfig::load(&config_file) { + let config_file = match &args.config { + Some(c) => c.as_str(), + None => defaults::CONFIG_FILE_LOCATION + }; + + let husk_config = match HuskConfig::load(config_file) { Ok(config) => config, Err(e) => { eprintln!("{:?}", e); @@ -31,7 +42,7 @@ async fn main() { let config_container: HuskConfigContainer = Arc::new(Mutex::new(husk_config)); - match Daemon::run(config_container).await { + match commands::dispatch(args, config_container).await { Ok(_) => { println!("exiting..."); }, diff --git a/src/types/defaults.rs b/src/types/defaults.rs new file mode 100644 index 0000000..404cae8 --- /dev/null +++ b/src/types/defaults.rs @@ -0,0 +1,5 @@ +// +// Some defaults +// + +pub const CONFIG_FILE_LOCATION: &'static str = "/etc/husk/config.toml"; diff --git a/src/types/husk_context.rs b/src/types/husk_context.rs index b3a7ae6..eb3a264 100644 --- a/src/types/husk_context.rs +++ b/src/types/husk_context.rs @@ -8,7 +8,7 @@ use anyhow; use sequoia_openpgp::policy::StandardPolicy; use std::path::PathBuf; use sequoia_openpgp::Cert; -use sequoia_cert_store::{CertStore, StoreUpdate}; +use sequoia_cert_store::CertStore; use sequoia_directories::Home; use crate::{config::HuskConfig, crypto}; @@ -58,7 +58,7 @@ impl<'hc> HuskContext<'hc> { } pub async fn add_recipient(&mut self, rcpt: String) { - + let certs = crypto::get_certificates(&self, rcpt.as_str()).await; self.mail.add_recipient(rcpt, certs) diff --git a/src/types/introducer.rs b/src/types/introducer.rs index 4c0862f..6b3af39 100644 --- a/src/types/introducer.rs +++ b/src/types/introducer.rs @@ -6,7 +6,8 @@ use sequoia_wot as wot; use wot::{CertSynopsis, Certification}; -use sequoia_openpgp::{policy::StandardPolicy, Cert, KeyID}; +use sequoia_openpgp::policy::StandardPolicy; +use sequoia_openpgp::Cert; #[derive(Debug, Clone)] pub struct Introducer { @@ -44,7 +45,7 @@ impl Introducer { u.certifications().for_each(|s| { if s.signature_alive(None, None).is_ok() { s.issuers().for_each(|i| { - if i == &my_keyid { result = true; } + if i == &my_keyid { result = true; } }); } }) diff --git a/src/types/mod.rs b/src/types/mod.rs index 16cda95..7d91ab1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,7 @@ +pub mod defaults; pub mod husk_context; pub mod mail_context; pub mod errors; pub mod recipient; pub mod introducer; +