diff options
Diffstat (limited to 'aerogramme')
-rw-r--r-- | aerogramme/Cargo.toml | 12 | ||||
-rw-r--r-- | aerogramme/src/k2v_util.rs | 26 | ||||
-rw-r--r-- | aerogramme/src/lib.rs | 19 | ||||
-rw-r--r-- | aerogramme/src/main.rs | 407 | ||||
-rw-r--r-- | aerogramme/src/server.rs | 147 |
5 files changed, 611 insertions, 0 deletions
diff --git a/aerogramme/Cargo.toml b/aerogramme/Cargo.toml new file mode 100644 index 0000000..e408aec --- /dev/null +++ b/aerogramme/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "aerogramme" +version = "0.3.0" +authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"] +edition = "2021" +license = "EUPL-1.2" +description = "A robust email server" + +[[test]] +name = "behavior" +path = "tests/behavior.rs" +harness = false diff --git a/aerogramme/src/k2v_util.rs b/aerogramme/src/k2v_util.rs new file mode 100644 index 0000000..3cd969b --- /dev/null +++ b/aerogramme/src/k2v_util.rs @@ -0,0 +1,26 @@ +/* +use anyhow::Result; +// ---- UTIL: function to wait for a value to have changed in K2V ---- + +pub async fn k2v_wait_value_changed( + k2v: &storage::RowStore, + key: &storage::RowRef, +) -> Result<CausalValue> { + loop { + if let Some(ct) = prev_ct { + match k2v.poll_item(pk, sk, ct.clone(), None).await? { + None => continue, + Some(cv) => return Ok(cv), + } + } else { + match k2v.read_item(pk, sk).await { + Err(k2v_client::Error::NotFound) => { + k2v.insert_item(pk, sk, vec![0u8], None).await?; + } + Err(e) => return Err(e.into()), + Ok(cv) => return Ok(cv), + } + } + } +} +*/ diff --git a/aerogramme/src/lib.rs b/aerogramme/src/lib.rs new file mode 100644 index 0000000..f065478 --- /dev/null +++ b/aerogramme/src/lib.rs @@ -0,0 +1,19 @@ +#![feature(type_alias_impl_trait)] +#![feature(async_fn_in_trait)] +#![feature(async_closure)] +#![feature(trait_alias)] + +pub mod auth; +pub mod bayou; +pub mod config; +pub mod cryptoblob; +pub mod dav; +pub mod imap; +pub mod k2v_util; +pub mod lmtp; +pub mod login; +pub mod mail; +pub mod server; +pub mod storage; +pub mod timestamp; +pub mod user; 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(()) +} diff --git a/aerogramme/src/server.rs b/aerogramme/src/server.rs new file mode 100644 index 0000000..09e91ad --- /dev/null +++ b/aerogramme/src/server.rs @@ -0,0 +1,147 @@ +use std::io::Write; +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use futures::try_join; +use log::*; +use tokio::sync::watch; + +use crate::auth; +use crate::config::*; +use crate::dav; +use crate::imap; +use crate::lmtp::*; +use crate::login::ArcLoginProvider; +use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*}; + +pub struct Server { + lmtp_server: Option<Arc<LmtpServer>>, + imap_unsecure_server: Option<imap::Server>, + imap_server: Option<imap::Server>, + auth_server: Option<auth::AuthServer>, + dav_unsecure_server: Option<dav::Server>, + pid_file: Option<PathBuf>, +} + +impl Server { + pub async fn from_companion_config(config: CompanionConfig) -> Result<Self> { + tracing::info!("Init as companion"); + let login = Arc::new(StaticLoginProvider::new(config.users).await?); + + let lmtp_server = None; + let imap_unsecure_server = Some(imap::new_unsecure(config.imap, login.clone())); + Ok(Self { + lmtp_server, + imap_unsecure_server, + imap_server: None, + auth_server: None, + dav_unsecure_server: None, + pid_file: config.pid, + }) + } + + pub async fn from_provider_config(config: ProviderConfig) -> Result<Self> { + tracing::info!("Init as provider"); + let login: ArcLoginProvider = match config.users { + UserManagement::Demo => Arc::new(DemoLoginProvider::new()), + UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x).await?), + UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?), + }; + + let lmtp_server = config.lmtp.map(|lmtp| LmtpServer::new(lmtp, login.clone())); + let imap_unsecure_server = config + .imap_unsecure + .map(|imap| imap::new_unsecure(imap, login.clone())); + let imap_server = config + .imap + .map(|imap| imap::new(imap, login.clone())) + .transpose()?; + let auth_server = config + .auth + .map(|auth| auth::AuthServer::new(auth, login.clone())); + let dav_unsecure_server = config + .dav_unsecure + .map(|dav_config| dav::new_unsecure(dav_config, login.clone())); + + Ok(Self { + lmtp_server, + imap_unsecure_server, + imap_server, + dav_unsecure_server, + auth_server, + pid_file: config.pid, + }) + } + + pub async fn run(self) -> Result<()> { + let pid = std::process::id(); + tracing::info!(pid = pid, "Starting main loops"); + + // write the pid file + if let Some(pid_file) = self.pid_file { + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(pid_file)?; + file.write_all(pid.to_string().as_bytes())?; + drop(file); + } + + let (exit_signal, provoke_exit) = watch_ctrl_c(); + let _exit_on_err = move |err: anyhow::Error| { + error!("Error: {}", err); + let _ = provoke_exit.send(true); + }; + + try_join!( + async { + match self.lmtp_server.as_ref() { + None => Ok(()), + Some(s) => s.run(exit_signal.clone()).await, + } + }, + async { + match self.imap_unsecure_server { + None => Ok(()), + Some(s) => s.run(exit_signal.clone()).await, + } + }, + async { + match self.imap_server { + None => Ok(()), + Some(s) => s.run(exit_signal.clone()).await, + } + }, + async { + match self.auth_server { + None => Ok(()), + Some(a) => a.run(exit_signal.clone()).await, + } + }, + async { + match self.dav_unsecure_server { + None => Ok(()), + Some(s) => s.run(exit_signal.clone()).await, + } + } + )?; + + Ok(()) + } +} + +pub fn watch_ctrl_c() -> (watch::Receiver<bool>, Arc<watch::Sender<bool>>) { + let (send_cancel, watch_cancel) = watch::channel(false); + let send_cancel = Arc::new(send_cancel); + let send_cancel_2 = send_cancel.clone(); + tokio::spawn(async move { + tokio::signal::ctrl_c() + .await + .expect("failed to install CTRL+C signal handler"); + info!("Received CTRL+C, shutting down."); + send_cancel.send(true).unwrap(); + }); + (watch_cancel, send_cancel_2) +} |