diff --git a/crates/erooster_imap/src/commands/mod.rs b/crates/erooster_imap/src/commands/mod.rs index 18018d6f..9e56fac7 100644 --- a/crates/erooster_imap/src/commands/mod.rs +++ b/crates/erooster_imap/src/commands/mod.rs @@ -68,7 +68,7 @@ mod subscribe; mod uid; mod unsubscribe; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Data { pub con_state: Arc>, } @@ -112,6 +112,7 @@ pub enum Commands { Search, Select, Status, + Starttls, Store, Subscribe, Unsubscribe, @@ -147,6 +148,7 @@ impl TryFrom<&str> for Commands { "enable" => Ok(Commands::Enable), "status" => Ok(Commands::Status), "search" => Ok(Commands::Search), + "starttls" => Ok(Commands::Starttls), _ => { warn!("[IMAP] Got unknown command: {}", i); Err(String::from("no other commands supported")) @@ -203,6 +205,13 @@ fn arguments(input: &str) -> Res> { .map(|(x, y)| (x, y)) } +#[allow(clippy::upper_case_acronyms)] +pub enum Response { + Exit, + Continue, + STARTTLS(String), +} + impl Data { #[instrument(skip(line))] fn parse_internal(line: &str) -> Res<(&str, Result, Vec<&str>)> { @@ -218,7 +227,7 @@ impl Data { database: DB, storage: Arc, line: String, - ) -> color_eyre::eyre::Result + ) -> color_eyre::eyre::Result where E: std::error::Error + std::marker::Sync + std::marker::Send + 'static, S: Sink + std::marker::Unpin + std::marker::Send, @@ -242,18 +251,18 @@ impl Data { .plain(lines, database, &command_data) .await?; // We are done here - return Ok(false); + return Ok(Response::Continue); } else if let State::Appending(state) = state { Append { data: self } .append(lines, storage, &line, config, state.tag) .await?; // We are done here - return Ok(false); + return Ok(Response::Continue); } else if let State::GotAppendData = state { if line == ")" { con_clone.write().await.state = State::Authenticated; // We are done here - return Ok(false); + return Ok(Response::Continue); } } debug!("Starting to parse"); @@ -271,11 +280,14 @@ impl Data { lines .send(String::from("* BAD [SERVERBUG] unable to parse command")) .await?; - return Ok(false); + return Ok(Response::Continue); } }; debug!("Command data: {:?}", command_data); match command_data.command { + Commands::Starttls => { + return Ok(Response::STARTTLS(tag.to_string())); + } Commands::Enable => { Enable { data: self }.exec(lines, &command_data).await?; } @@ -288,7 +300,7 @@ impl Data { Commands::Logout => { Logout.exec(lines, &command_data).await?; // We return true here early as we want to make sure that this closes the connection - return Ok(true); + return Ok(Response::Exit); } Commands::Authenticate => { let auth_data = command_data.arguments[command_data.arguments.len() - 1]; @@ -399,11 +411,11 @@ impl Data { lines .send(String::from("* BAD [SERVERBUG] unable to parse command")) .await?; - return Ok(false); + return Ok(Response::Continue); } } - Ok(false) + Ok(Response::Continue) } } diff --git a/crates/erooster_imap/src/servers/encrypted.rs b/crates/erooster_imap/src/servers/encrypted.rs index 41c3dfcb..30217e82 100644 --- a/crates/erooster_imap/src/servers/encrypted.rs +++ b/crates/erooster_imap/src/servers/encrypted.rs @@ -1,5 +1,10 @@ -use crate::{commands::Data, servers::state::Connection, Server, CAPABILITY_HELLO}; +use crate::{ + commands::{Data, Response}, + servers::state::Connection, + Server, CAPABILITY_HELLO, +}; use async_trait::async_trait; +use color_eyre::eyre::Context; use erooster_core::{ backend::{database::DB, storage::Storage}, config::Config, @@ -14,7 +19,7 @@ use std::{ path::Path, sync::Arc, }; -use tokio::net::TcpListener; +use tokio::net::{TcpListener, TcpStream}; use tokio_rustls::{ rustls::{self, Certificate, PrivateKey}, TlsAcceptor, @@ -26,39 +31,53 @@ use tracing::{debug, error, info, instrument}; /// An encrypted imap Server pub struct Encrypted; -impl Encrypted { - // Loads the certfile from the filesystem - #[instrument(skip(path))] - fn load_certs(path: &Path) -> color_eyre::eyre::Result> { - let certfile = fs::File::open(path)?; - let mut reader = BufReader::new(certfile); - Ok(rustls_pemfile::certs(&mut reader)? - .iter() - .map(|v| rustls::Certificate(v.clone())) - .collect()) - } - #[instrument(skip(path))] - fn load_key(path: &Path) -> color_eyre::eyre::Result { - let keyfile = fs::File::open(path)?; - let mut reader = BufReader::new(keyfile); - - loop { - match rustls_pemfile::read_one(&mut reader)? { - Some( - rustls_pemfile::Item::RSAKey(key) - | rustls_pemfile::Item::PKCS8Key(key) - | rustls_pemfile::Item::ECKey(key), - ) => return Ok(rustls::PrivateKey(key)), - None => break, - _ => {} - } - } +pub fn get_tls_acceptor(config: &Arc) -> color_eyre::eyre::Result { + // Load SSL Keys + let certs = load_certs(Path::new(&config.tls.cert_path))?; + let key = load_key(Path::new(&config.tls.key_path))?; + + // Sets up the TLS acceptor. + let server_config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(certs, key) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; - Err(color_eyre::eyre::eyre!( - "no keys found in {:?} (encrypted keys not supported)", - path - )) + // Starts a TLS accepting thing. + Ok(TlsAcceptor::from(Arc::new(server_config))) +} + +// Loads the certfile from the filesystem +#[instrument(skip(path))] +fn load_certs(path: &Path) -> color_eyre::eyre::Result> { + let certfile = fs::File::open(path)?; + let mut reader = BufReader::new(certfile); + Ok(rustls_pemfile::certs(&mut reader)? + .iter() + .map(|v| rustls::Certificate(v.clone())) + .collect()) +} +#[instrument(skip(path))] +fn load_key(path: &Path) -> color_eyre::eyre::Result { + let keyfile = fs::File::open(path)?; + let mut reader = BufReader::new(keyfile); + + loop { + match rustls_pemfile::read_one(&mut reader)? { + Some( + rustls_pemfile::Item::RSAKey(key) + | rustls_pemfile::Item::PKCS8Key(key) + | rustls_pemfile::Item::ECKey(key), + ) => return Ok(rustls::PrivateKey(key)), + None => break, + _ => {} + } } + + Err(color_eyre::eyre::eyre!( + "no keys found in {:?} (encrypted keys not supported)", + path + )) } #[async_trait] @@ -76,18 +95,7 @@ impl Server for Encrypted { storage: Arc, ) -> color_eyre::eyre::Result<()> { // Load SSL Keys - let certs = Encrypted::load_certs(Path::new(&config.tls.cert_path))?; - let key = Encrypted::load_key(Path::new(&config.tls.key_path))?; - - // Sets up the TLS acceptor. - let server_config = rustls::ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(certs, key) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; - - // Starts a TLS accepting thing. - let acceptor = TlsAcceptor::from(Arc::new(server_config)); + let acceptor = get_tls_acceptor(&config)?; // Opens the listener let addrs: Vec = if let Some(listen_ips) = &config.listen_ips { @@ -135,31 +143,58 @@ async fn listen( ) { // Looks for new peers while let Some(Ok(tcp_stream)) = stream.next().await { - debug!("[IMAP] Got new TLS peer: {:?}", tcp_stream.peer_addr()); - let peer = tcp_stream.peer_addr().expect("peer addr to exist"); - - // We need to clone these as we move into a new thread - let config = Arc::clone(&config); - let database = Arc::clone(&database); - let storage = Arc::clone(&storage); - - // Start talking with new peer on new thread - let acceptor = acceptor.clone(); - tokio::spawn(async move { - // Accept TCP connection - let tls_stream = acceptor.accept(tcp_stream).await; - - // Continue if it worked - match tls_stream { - Ok(stream) => { - debug!("[IMAP] TLS negotiation done"); - - // Proceed as normal - let lines = Framed::new(stream, LinesCodec::new_with_max_length(LINE_LIMIT)); - // We split these as we handle the sink in a broadcast instead to be able to push non linear data over the socket - let (mut lines_sender, mut lines_reader) = lines.split(); - - // Greet the client with the capabilities we provide + if let Err(e) = listen_tls( + tcp_stream, + Arc::clone(&config), + Arc::clone(&database), + Arc::clone(&storage), + acceptor.clone(), + None, + false, + ) + .await + { + error!("[IMAP][ENCRYPTED] Error while listening: {}", e); + } + } +} + +pub async fn listen_tls( + tcp_stream: TcpStream, + config: Arc, + database: DB, + storage: Arc, + acceptor: TlsAcceptor, + upper_data: Option, + starttls: bool, +) -> color_eyre::eyre::Result<()> { + let peer = tcp_stream + .peer_addr() + .context("[IMAP] peer addr to exist")?; + debug!("[IMAP] Got new TLS peer: {:?}", peer); + + // We need to clone these as we move into a new thread + let config = Arc::clone(&config); + let database = Arc::clone(&database); + let storage = Arc::clone(&storage); + + // Start talking with new peer on new thread + tokio::spawn(async move { + // Accept TCP connection + let tls_stream = acceptor.accept(tcp_stream).await; + + // Continue if it worked + match tls_stream { + Ok(stream) => { + debug!("[IMAP] TLS negotiation done"); + + // Proceed as normal + let lines = Framed::new(stream, LinesCodec::new_with_max_length(LINE_LIMIT)); + // We split these as we handle the sink in a broadcast instead to be able to push non linear data over the socket + let (mut lines_sender, mut lines_reader) = lines.split(); + + // Greet the client with the capabilities we provide + if !starttls { if let Err(e) = lines_sender.send(CAPABILITY_HELLO.to_string()).await { error!( "Unable to send greeting to client. Closing connection. Error: {}", @@ -167,56 +202,62 @@ async fn listen( ); return; } - // Create our Connection - let connection = Connection::new(true); + } + // Create our Connection + let connection = Connection::new(true); - // Read lines from the stream - while let Some(Ok(line)) = lines_reader.next().await { - let data = Data { - con_state: Arc::clone(&connection), + // Read lines from the stream + while let Some(Ok(line)) = lines_reader.next().await { + let data = if let Some(data) = upper_data.clone() { + { + data.con_state.write().await.secure = true; }; - debug!("[IMAP] [{}] Got Command: {}", peer, line); - // TODO make sure to handle IDLE different as it needs us to stream lines + data + } else { + Data { + con_state: Arc::clone(&connection), + } + }; + debug!("[IMAP] [{}] Got Command: {}", peer, line); + // TODO make sure to handle IDLE different as it needs us to stream lines - { - let close = data - .parse( - &mut lines_sender, - Arc::clone(&config), - Arc::clone(&database), - Arc::clone(&storage), - line, - ) - .await; - match close { - Ok(close) => { - // Cleanup timeout managers - if close { - // Used for later session timer management - debug!("[IMAP] Closing TLS connection"); - break; - } - } - // We try a last time to do a graceful shutdown before closing - Err(e) => { - if let Err(e) = lines_sender - .send(format!( - "* BAD [SERVERBUG] This should not happen: {e}" - )) - .await - { - error!("Unable to send error response: {}", e); - } - error!("[IMAP] Failure happened: {}", e); + { + let response = data + .parse( + &mut lines_sender, + Arc::clone(&config), + Arc::clone(&database), + Arc::clone(&storage), + line, + ) + .await; + match response { + Ok(response) => { + // Cleanup timeout managers + if let Response::Exit = response { + // Used for later session timer management debug!("[IMAP] Closing TLS connection"); break; } } - }; - } + // We try a last time to do a graceful shutdown before closing + Err(e) => { + if let Err(e) = lines_sender + .send(format!("* BAD [SERVERBUG] This should not happen: {e}")) + .await + { + error!("Unable to send error response: {}", e); + } + error!("[IMAP] Failure happened: {}", e); + debug!("[IMAP] Closing TLS connection"); + break; + } + } + }; } - Err(e) => error!("[IMAP] Got error while accepting TLS: {}", e), } - }); - } + Err(e) => error!("[IMAP] Got error while accepting TLS: {}", e), + } + }); + Ok(()) } diff --git a/crates/erooster_imap/src/servers/unencrypted.rs b/crates/erooster_imap/src/servers/unencrypted.rs index 363efded..978eff1f 100644 --- a/crates/erooster_imap/src/servers/unencrypted.rs +++ b/crates/erooster_imap/src/servers/unencrypted.rs @@ -1,5 +1,13 @@ -use crate::{commands::Data, servers::state::Connection, Server, CAPABILITY_HELLO}; +use crate::{ + commands::{Data, Response}, + servers::{ + encrypted::{get_tls_acceptor, listen_tls}, + state::Connection, + }, + Server, CAPABILITY_HELLO, +}; use async_trait::async_trait; +use color_eyre::Result; use erooster_core::{ backend::{database::DB, storage::Storage}, config::Config, @@ -8,7 +16,7 @@ use erooster_core::{ }; use futures::{SinkExt, StreamExt}; use std::{net::SocketAddr, sync::Arc}; -use tokio::net::TcpListener; +use tokio::{net::TcpListener, task::JoinHandle}; use tokio_stream::wrappers::TcpListenerStream; use tokio_util::codec::Framed; use tracing::{debug, error, info, instrument}; @@ -70,7 +78,7 @@ async fn listen( let config = Arc::clone(&config); let database = Arc::clone(&database); let storage = Arc::clone(&storage); - tokio::spawn(async move { + let connection: JoinHandle> = tokio::spawn(async move { let lines = Framed::new(tcp_stream, LinesCodec::new_with_max_length(LINE_LIMIT)); let (mut lines_sender, mut lines_reader) = lines.split(); if let Err(e) = lines_sender.send(CAPABILITY_HELLO.to_string()).await { @@ -78,15 +86,15 @@ async fn listen( "Unable to send greeting to client. Closing connection. Error: {}", e ); - return; + return Ok(()); } let state = Connection::new(false); + let data = Data { + con_state: Arc::clone(&state), + }; + let mut do_starttls = false; while let Some(Ok(line)) = lines_reader.next().await { - let data = Data { - con_state: Arc::clone(&state), - }; - debug!("[IMAP] [{}] Got Command: {}", peer, line); // TODO make sure to handle IDLE different as it needs us to stream lines @@ -104,10 +112,21 @@ async fn listen( match response { Ok(response) => { // Cleanup timeout managers - if response { - // Used for later session timer management - debug!("[IMAP] Closing connection"); - break; + match response { + Response::Exit => { + // Used for later session timer management + debug!("[IMAP] Closing connection"); + break; + } + Response::STARTTLS(tag) => { + debug!("[IMAP] Switching context"); + do_starttls = true; + lines_sender + .send(format!("{tag} OK Begin TLS negotiation now")) + .await?; + break; + } + Response::Continue => {} } } // We try a last time to do a graceful shutdown before closing @@ -124,6 +143,32 @@ async fn listen( } } } + if do_starttls { + debug!("[IMAP] Starting to reunite"); + let framed_stream = lines_sender.reunite(lines_reader)?; + let stream = framed_stream.into_inner(); + debug!("[IMAP] Finished to reunite"); + let acceptor = get_tls_acceptor(&config)?; + debug!("[IMAP] Starting to listen using tls"); + if let Err(e) = listen_tls( + stream, + config, + database, + storage, + acceptor, + Some(data), + true, + ) + .await + { + error!("[SMTP] Error while upgrading to tls: {}", e); + } + } + Ok(()) }); + let resp = connection.await; + if let Ok(Err(e)) = resp { + error!("[IMAP] Error: {:?}", e); + } } }