aboutsummaryrefslogtreecommitdiff
path: root/aerogramme/src/main.rs
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-03-08 08:17:03 +0100
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-03-08 08:17:03 +0100
commit1a43ce5ac7033c148f64a033f2b1d335e95e11d5 (patch)
tree60b234604170fe207248458a9c4cdd3f4b7c36f2 /aerogramme/src/main.rs
parentbb9cb386b65834c44cae86bd100f800883022062 (diff)
downloadaerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.tar.gz
aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.zip
WIP refactor
Diffstat (limited to 'aerogramme/src/main.rs')
-rw-r--r--aerogramme/src/main.rs407
1 files changed, 407 insertions, 0 deletions
diff --git a/aerogramme/src/main.rs b/aerogramme/src/main.rs
new file mode 100644
index 0000000..43b4dca
--- /dev/null
+++ b/aerogramme/src/main.rs
@@ -0,0 +1,407 @@
+use std::io::Read;
+use std::path::PathBuf;
+
+use anyhow::{bail, Context, Result};
+use clap::{Parser, Subcommand};
+use nix::{sys::signal, unistd::Pid};
+
+use aerogramme::config::*;
+use aerogramme::login::{static_provider::*, *};
+use aerogramme::server::Server;
+
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+struct Args {
+ #[clap(subcommand)]
+ command: Command,
+
+ /// A special mode dedicated to developers, NOT INTENDED FOR PRODUCTION
+ #[clap(long)]
+ dev: bool,
+
+ #[clap(
+ short,
+ long,
+ env = "AEROGRAMME_CONFIG",
+ default_value = "aerogramme.toml"
+ )]
+ /// Path to the main Aerogramme configuration file
+ config_file: PathBuf,
+}
+
+#[derive(Subcommand, Debug)]
+enum Command {
+ #[clap(subcommand)]
+ /// A daemon to be run by the end user, on a personal device
+ Companion(CompanionCommand),
+
+ #[clap(subcommand)]
+ /// A daemon to be run by the service provider, on a server
+ Provider(ProviderCommand),
+
+ #[clap(subcommand)]
+ /// Specific tooling, should not be part of a normal workflow, for debug & experimentation only
+ Tools(ToolsCommand),
+ //Test,
+}
+
+#[derive(Subcommand, Debug)]
+enum ToolsCommand {
+ /// Manage crypto roots
+ #[clap(subcommand)]
+ CryptoRoot(CryptoRootCommand),
+
+ PasswordHash {
+ #[clap(env = "AEROGRAMME_PASSWORD")]
+ maybe_password: Option<String>,
+ },
+}
+
+#[derive(Subcommand, Debug)]
+enum CryptoRootCommand {
+ /// Generate a new crypto-root protected with a password
+ New {
+ #[clap(env = "AEROGRAMME_PASSWORD")]
+ maybe_password: Option<String>,
+ },
+ /// Generate a new clear text crypto-root, store it securely!
+ NewClearText,
+ /// Change the password of a crypto key
+ ChangePassword {
+ #[clap(env = "AEROGRAMME_OLD_PASSWORD")]
+ maybe_old_password: Option<String>,
+
+ #[clap(env = "AEROGRAMME_NEW_PASSWORD")]
+ maybe_new_password: Option<String>,
+
+ #[clap(short, long, env = "AEROGRAMME_CRYPTO_ROOT")]
+ crypto_root: String,
+ },
+ /// From a given crypto-key, derive one containing only the public key
+ DeriveIncoming {
+ #[clap(short, long, env = "AEROGRAMME_CRYPTO_ROOT")]
+ crypto_root: String,
+ },
+}
+
+#[derive(Subcommand, Debug)]
+enum CompanionCommand {
+ /// Runs the IMAP proxy
+ Daemon,
+ Reload {
+ #[clap(short, long, env = "AEROGRAMME_PID")]
+ pid: Option<i32>,
+ },
+ Wizard,
+ #[clap(subcommand)]
+ Account(AccountManagement),
+}
+
+#[derive(Subcommand, Debug)]
+enum ProviderCommand {
+ /// Runs the IMAP+LMTP server daemon
+ Daemon,
+ /// Reload the daemon
+ Reload {
+ #[clap(short, long, env = "AEROGRAMME_PID")]
+ pid: Option<i32>,
+ },
+ /// Manage static accounts
+ #[clap(subcommand)]
+ Account(AccountManagement),
+}
+
+#[derive(Subcommand, Debug)]
+enum AccountManagement {
+ /// Add an account
+ Add {
+ #[clap(short, long)]
+ login: String,
+ #[clap(short, long)]
+ setup: PathBuf,
+ },
+ /// Delete an account
+ Delete {
+ #[clap(short, long)]
+ login: String,
+ },
+ /// Change password for a given account
+ ChangePassword {
+ #[clap(env = "AEROGRAMME_OLD_PASSWORD")]
+ maybe_old_password: Option<String>,
+
+ #[clap(env = "AEROGRAMME_NEW_PASSWORD")]
+ maybe_new_password: Option<String>,
+
+ #[clap(short, long)]
+ login: String,
+ },
+}
+
+#[cfg(tokio_unstable)]
+fn tracer() {
+ console_subscriber::init();
+}
+
+#[cfg(not(tokio_unstable))]
+fn tracer() {
+ tracing_subscriber::fmt::init();
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ if std::env::var("RUST_LOG").is_err() {
+ std::env::set_var("RUST_LOG", "main=info,aerogramme=info,k2v_client=info")
+ }
+
+ // Abort on panic (same behavior as in Go)
+ std::panic::set_hook(Box::new(|panic_info| {
+ eprintln!("{}", panic_info);
+ eprintln!("{:?}", backtrace::Backtrace::new());
+ std::process::abort();
+ }));
+
+ tracer();
+
+ let args = Args::parse();
+ let any_config = if args.dev {
+ use std::net::*;
+ AnyConfig::Provider(ProviderConfig {
+ pid: None,
+ imap: None,
+ imap_unsecure: Some(ImapUnsecureConfig {
+ bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143),
+ }),
+ dav_unsecure: Some(DavUnsecureConfig {
+ bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8087),
+ }),
+ lmtp: Some(LmtpConfig {
+ bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1025),
+ hostname: "example.tld".to_string(),
+ }),
+ auth: Some(AuthConfig {
+ bind_addr: SocketAddr::new(
+ IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
+ 12345,
+ ),
+ }),
+ users: UserManagement::Demo,
+ })
+ } else {
+ read_config(args.config_file)?
+ };
+
+ match (&args.command, any_config) {
+ (Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand {
+ CompanionCommand::Daemon => {
+ let server = Server::from_companion_config(config).await?;
+ server.run().await?;
+ }
+ CompanionCommand::Reload { pid } => reload(*pid, config.pid)?,
+ CompanionCommand::Wizard => {
+ unimplemented!();
+ }
+ CompanionCommand::Account(cmd) => {
+ let user_file = config.users.user_list;
+ account_management(&args.command, cmd, user_file)?;
+ }
+ },
+ (Command::Provider(subcommand), AnyConfig::Provider(config)) => match subcommand {
+ ProviderCommand::Daemon => {
+ let server = Server::from_provider_config(config).await?;
+ server.run().await?;
+ }
+ ProviderCommand::Reload { pid } => reload(*pid, config.pid)?,
+ ProviderCommand::Account(cmd) => {
+ let user_file = match config.users {
+ UserManagement::Static(conf) => conf.user_list,
+ _ => {
+ panic!("Only static account management is supported from Aerogramme.")
+ }
+ };
+ account_management(&args.command, cmd, user_file)?;
+ }
+ },
+ (Command::Provider(_), AnyConfig::Companion(_)) => {
+ bail!("Your want to run a 'Provider' command but your configuration file has role 'Companion'.");
+ }
+ (Command::Companion(_), AnyConfig::Provider(_)) => {
+ bail!("Your want to run a 'Companion' command but your configuration file has role 'Provider'.");
+ }
+ (Command::Tools(subcommand), _) => match subcommand {
+ ToolsCommand::PasswordHash { maybe_password } => {
+ let password = match maybe_password {
+ Some(pwd) => pwd.clone(),
+ None => rpassword::prompt_password("Enter password: ")?,
+ };
+ println!("{}", hash_password(&password)?);
+ }
+ ToolsCommand::CryptoRoot(crcommand) => match crcommand {
+ CryptoRootCommand::New { maybe_password } => {
+ let password = match maybe_password {
+ Some(pwd) => pwd.clone(),
+ None => {
+ let password = rpassword::prompt_password("Enter password: ")?;
+ let password_confirm =
+ rpassword::prompt_password("Confirm password: ")?;
+ if password != password_confirm {
+ bail!("Passwords don't match.");
+ }
+ password
+ }
+ };
+ let crypto_keys = CryptoKeys::init();
+ let cr = CryptoRoot::create_pass(&password, &crypto_keys)?;
+ println!("{}", cr.0);
+ }
+ CryptoRootCommand::NewClearText => {
+ let crypto_keys = CryptoKeys::init();
+ let cr = CryptoRoot::create_cleartext(&crypto_keys);
+ println!("{}", cr.0);
+ }
+ CryptoRootCommand::ChangePassword {
+ maybe_old_password,
+ maybe_new_password,
+ crypto_root,
+ } => {
+ let old_password = match maybe_old_password {
+ Some(pwd) => pwd.to_string(),
+ None => rpassword::prompt_password("Enter old password: ")?,
+ };
+
+ let new_password = match maybe_new_password {
+ Some(pwd) => pwd.to_string(),
+ None => {
+ let password = rpassword::prompt_password("Enter new password: ")?;
+ let password_confirm =
+ rpassword::prompt_password("Confirm new password: ")?;
+ if password != password_confirm {
+ bail!("Passwords don't match.");
+ }
+ password
+ }
+ };
+
+ let keys = CryptoRoot(crypto_root.to_string()).crypto_keys(&old_password)?;
+ let cr = CryptoRoot::create_pass(&new_password, &keys)?;
+ println!("{}", cr.0);
+ }
+ CryptoRootCommand::DeriveIncoming { crypto_root } => {
+ let pubkey = CryptoRoot(crypto_root.to_string()).public_key()?;
+ let cr = CryptoRoot::create_incoming(&pubkey);
+ println!("{}", cr.0);
+ }
+ },
+ },
+ }
+
+ Ok(())
+}
+
+fn reload(pid: Option<i32>, pid_path: Option<PathBuf>) -> Result<()> {
+ let final_pid = match (pid, pid_path) {
+ (Some(pid), _) => pid,
+ (_, Some(path)) => {
+ let mut f = std::fs::OpenOptions::new().read(true).open(path)?;
+ let mut pidstr = String::new();
+ f.read_to_string(&mut pidstr)?;
+ pidstr.parse::<i32>()?
+ }
+ _ => bail!("Unable to infer your daemon's PID"),
+ };
+ let pid = Pid::from_raw(final_pid);
+ signal::kill(pid, signal::Signal::SIGUSR1)?;
+ Ok(())
+}
+
+fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) -> Result<()> {
+ let mut ulist: UserList =
+ read_config(users.clone()).context(format!("'{:?}' must be a user database", users))?;
+
+ match cmd {
+ AccountManagement::Add { login, setup } => {
+ tracing::debug!(user = login, "will-create");
+ let stp: SetupEntry = read_config(setup.clone())
+ .context(format!("'{:?}' must be a setup file", setup))?;
+ tracing::debug!(user = login, "loaded setup entry");
+
+ let password = match stp.clear_password {
+ Some(pwd) => pwd,
+ None => {
+ let password = rpassword::prompt_password("Enter password: ")?;
+ let password_confirm = rpassword::prompt_password("Confirm password: ")?;
+ if password != password_confirm {
+ bail!("Passwords don't match.");
+ }
+ password
+ }
+ };
+
+ let crypto_keys = CryptoKeys::init();
+ let crypto_root = match root {
+ Command::Provider(_) => CryptoRoot::create_pass(&password, &crypto_keys)?,
+ Command::Companion(_) => CryptoRoot::create_cleartext(&crypto_keys),
+ _ => unreachable!(),
+ };
+
+ let hash = hash_password(password.as_str()).context("unable to hash password")?;
+
+ ulist.insert(
+ login.clone(),
+ UserEntry {
+ email_addresses: stp.email_addresses,
+ password: hash,
+ crypto_root: crypto_root.0,
+ storage: stp.storage,
+ },
+ );
+
+ write_config(users.clone(), &ulist)?;
+ }
+ AccountManagement::Delete { login } => {
+ tracing::debug!(user = login, "will-delete");
+ ulist.remove(login);
+ write_config(users.clone(), &ulist)?;
+ }
+ AccountManagement::ChangePassword {
+ maybe_old_password,
+ maybe_new_password,
+ login,
+ } => {
+ let mut user = ulist.remove(login).context("user must exist first")?;
+
+ let old_password = match maybe_old_password {
+ Some(pwd) => pwd.to_string(),
+ None => rpassword::prompt_password("Enter old password: ")?,
+ };
+
+ if !verify_password(&old_password, &user.password)? {
+ bail!(format!("invalid password for login {}", login));
+ }
+
+ let crypto_keys = CryptoRoot(user.crypto_root).crypto_keys(&old_password)?;
+
+ let new_password = match maybe_new_password {
+ Some(pwd) => pwd.to_string(),
+ None => {
+ let password = rpassword::prompt_password("Enter new password: ")?;
+ let password_confirm = rpassword::prompt_password("Confirm new password: ")?;
+ if password != password_confirm {
+ bail!("Passwords don't match.");
+ }
+ password
+ }
+ };
+ let new_hash = hash_password(&new_password)?;
+ let new_crypto_root = CryptoRoot::create_pass(&new_password, &crypto_keys)?;
+
+ user.password = new_hash;
+ user.crypto_root = new_crypto_root.0;
+
+ ulist.insert(login.clone(), user);
+ write_config(users.clone(), &ulist)?;
+ }
+ };
+
+ Ok(())
+}