From 3abdafb0dbbc9290329e4974e821933426b32f91 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 13 Mar 2024 15:45:36 +0100 Subject: [PATCH] TLS + Fix auth --- aero-proto/src/dav.rs | 90 +++++++++++++++++++++++++++++++++----- aero-proto/src/imap/mod.rs | 81 ---------------------------------- aero-user/src/config.rs | 8 ++++ aerogramme/src/main.rs | 1 + aerogramme/src/server.rs | 13 ++++++ 5 files changed, 100 insertions(+), 93 deletions(-) diff --git a/aero-proto/src/dav.rs b/aero-proto/src/dav.rs index 2852d34..42fde2c 100644 --- a/aero-proto/src/dav.rs +++ b/aero-proto/src/dav.rs @@ -1,4 +1,5 @@ use std::net::SocketAddr; +use std::sync::Arc; use anyhow::{anyhow, Result}; use base64::Engine; @@ -10,23 +11,55 @@ use http_body_util::Full; use futures::stream::{FuturesUnordered, StreamExt}; use tokio::net::TcpListener; use tokio::sync::watch; +use tokio_rustls::TlsAcceptor; +use tokio::net::TcpStream; +use hyper::rt::{Read, Write}; +use tokio::io::{AsyncRead, AsyncWrite}; +use rustls_pemfile::{certs, private_key}; -use aero_user::config::DavUnsecureConfig; +use aero_user::config::{DavConfig, DavUnsecureConfig}; use aero_user::login::ArcLoginProvider; use aero_collections::user::User; pub struct Server { bind_addr: SocketAddr, login_provider: ArcLoginProvider, + tls: Option, } pub fn new_unsecure(config: DavUnsecureConfig, login: ArcLoginProvider) -> Server { Server { bind_addr: config.bind_addr, login_provider: login, + tls: None, } } +pub fn new(config: DavConfig, login: ArcLoginProvider) -> Result { + let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open( + config.certs, + )?)) + .collect::, _>>()?; + let loaded_key = private_key(&mut std::io::BufReader::new(std::fs::File::open( + config.key, + )?))? + .unwrap(); + + let tls_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(loaded_certs, loaded_key)?; + let acceptor = TlsAcceptor::from(Arc::new(tls_config)); + + Ok(Server { + bind_addr: config.bind_addr, + login_provider: login, + tls: Some(acceptor), + }) +} + +trait Stream: Read + Write + Send + Unpin {} +impl Stream for TokioIo {} + impl Server { pub async fn run(self: Self, mut must_exit: watch::Receiver) -> Result<()> { let tcp = TcpListener::bind(self.bind_addr).await?; @@ -47,14 +80,24 @@ impl Server { _ = must_exit.changed() => continue, }; tracing::info!("Accepted connection from {}", remote_addr); - let stream = TokioIo::new(socket); + let stream = match self.build_stream(socket).await { + Ok(v) => v, + Err(e) => { + tracing::error!(err=?e, "TLS acceptor failed"); + continue + } + }; + let login = self.login_provider.clone(); let conn = tokio::spawn(async move { //@FIXME should create a generic "public web" server on which "routers" could be //abitrarily bound //@FIXME replace with a handler supporting http2 and TLS + + match http::Builder::new().serve_connection(stream, service_fn(|req: Request| { let login = login.clone(); + tracing::info!("{:?} {:?}", req.method(), req.uri()); async move { auth(login, req).await } @@ -72,6 +115,16 @@ impl Server { Ok(()) } + + async fn build_stream(&self, socket: TcpStream) -> Result> { + match self.tls.clone() { + Some(acceptor) => { + let stream = acceptor.accept(socket).await?; + Ok(Box::new(TokioIo::new(stream))) + } + None => Ok(Box::new(TokioIo::new(socket))), + } + } } //@FIXME We should not support only BasicAuth @@ -80,18 +133,26 @@ async fn auth( req: Request, ) -> Result>> { - let auth_val = match req.headers().get("Authorization") { + tracing::info!("headers: {:?}", req.headers()); + let auth_val = match req.headers().get(hyper::header::AUTHORIZATION) { Some(hv) => hv.to_str()?, - None => return Ok(Response::builder() - .status(401) - .body(Full::new(Bytes::from("Missing Authorization field")))?), + None => { + tracing::info!("Missing authorization field"); + return Ok(Response::builder() + .status(401) + .header("WWW-Authenticate", "Basic realm=\"Aerogramme\"") + .body(Full::new(Bytes::from("Missing Authorization field")))?) + }, }; let b64_creds_maybe_padded = match auth_val.split_once(" ") { Some(("Basic", b64)) => b64, - _ => return Ok(Response::builder() - .status(400) - .body(Full::new(Bytes::from("Unsupported Authorization field")))?), + _ => { + tracing::info!("Unsupported authorization field"); + return Ok(Response::builder() + .status(400) + .body(Full::new(Bytes::from("Unsupported Authorization field")))?) + }, }; // base64urlencoded may have trailing equals, base64urlsafe has not @@ -110,9 +171,13 @@ async fn auth( // Call login provider let creds = match login.login(username, password).await { Ok(c) => c, - Err(_) => return Ok(Response::builder() - .status(401) - .body(Full::new(Bytes::from("Wrong credentials")))?), + Err(_) => { + tracing::info!(user=username, "Wrong credentials"); + return Ok(Response::builder() + .status(401) + .header("WWW-Authenticate", "Basic realm=\"Aerogramme\"") + .body(Full::new(Bytes::from("Wrong credentials")))?) + }, }; // Build a user @@ -124,6 +189,7 @@ async fn auth( async fn router(user: std::sync::Arc, req: Request) -> Result>> { let path_segments: Vec<_> = req.uri().path().split("/").filter(|s| *s != "").collect(); + tracing::info!("router"); match path_segments.as_slice() { [] => tracing::info!("root"), [ username, ..] if *username != user.username => return Ok(Response::builder() diff --git a/aero-proto/src/imap/mod.rs b/aero-proto/src/imap/mod.rs index ae3b58f..7183a78 100644 --- a/aero-proto/src/imap/mod.rs +++ b/aero-proto/src/imap/mod.rs @@ -333,85 +333,4 @@ impl NetLoop { }; } } - - /* - async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc) -> Result { - // Flush send - loop { - tracing::trace!("flush server send"); - match self.server.progress_send().await? { - Some(..) => continue, - None => break, - } - } - - tokio::select! { - // Receiving IDLE event from background - maybe_msg = self.resp_rx.recv() => match maybe_msg { - // Session decided idle is terminated - Some(ResponseOrIdle::Response(response)) => { - tracing::trace!("server imap session said idle is done, sending response done, switching to interactive"); - for body_elem in response.body.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.server.enqueue_status(response.completion); - return Ok(LoopMode::Interactive) - }, - // Session has some information for user - Some(ResponseOrIdle::IdleEvent(elems)) => { - tracing::trace!("server imap session has some change to communicate to the client"); - for body_elem in elems.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.cmd_tx.try_send(Request::Idle)?; - return Ok(LoopMode::Idle(buff, stop)) - }, - - // Session crashed - None => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - return Ok(LoopMode::Interactive) - }, - - // Session can't start idling while already idling, it's a logic error! - Some(ResponseOrIdle::StartIdle(..)) => bail!("can't start idling while already idling!"), - }, - - // User is trying to interact with us - read_client_result = self.server.stream.read(&mut buff) => { - let _bytes_read = read_client_result?; - use imap_codec::decode::Decoder; - let codec = imap_codec::IdleDoneCodec::new(); - tracing::trace!("client sent some data for the server IMAP session"); - match codec.decode(&buff) { - Ok(([], imap_codec::imap_types::extensions::idle::IdleDone)) => { - // Session will be informed that it must stop idle - // It will generate the "done" message and change the loop mode - tracing::trace!("client sent DONE and want to stop IDLE"); - stop.notify_one() - }, - Err(_) => { - tracing::trace!("Unable to decode DONE, maybe not enough data were sent?"); - }, - _ => bail!("Client sent data after terminating the continuation without waiting for the server. This is an unsupported behavior and bug in Aerogramme, quitting."), - }; - - return Ok(LoopMode::Idle(buff, stop)) - }, - - // When receiving a CTRL+C - _ = self.ctx.must_exit.changed() => { - tracing::trace!("CTRL+C sent, aborting IDLE for this session"); - self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap()); - return Ok(LoopMode::Interactive) - }, - }; - }*/ } diff --git a/aero-user/src/config.rs b/aero-user/src/config.rs index 7de2eac..44b1239 100644 --- a/aero-user/src/config.rs +++ b/aero-user/src/config.rs @@ -23,6 +23,7 @@ pub struct ProviderConfig { pub imap_unsecure: Option, pub lmtp: Option, pub auth: Option, + pub dav: Option, pub dav_unsecure: Option, pub users: UserManagement, } @@ -58,6 +59,13 @@ pub struct DavUnsecureConfig { pub bind_addr: SocketAddr, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DavConfig { + pub bind_addr: SocketAddr, + pub certs: PathBuf, + pub key: PathBuf, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImapUnsecureConfig { pub bind_addr: SocketAddr, diff --git a/aerogramme/src/main.rs b/aerogramme/src/main.rs index 4251520..624e8e2 100644 --- a/aerogramme/src/main.rs +++ b/aerogramme/src/main.rs @@ -171,6 +171,7 @@ async fn main() -> Result<()> { AnyConfig::Provider(ProviderConfig { pid: None, imap: None, + dav: None, imap_unsecure: Some(ImapUnsecureConfig { bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143), }), diff --git a/aerogramme/src/server.rs b/aerogramme/src/server.rs index e302db3..e57cd72 100644 --- a/aerogramme/src/server.rs +++ b/aerogramme/src/server.rs @@ -21,6 +21,7 @@ pub struct Server { imap_server: Option, auth_server: Option, dav_unsecure_server: Option, + dav_server: Option, pid_file: Option, } @@ -37,6 +38,7 @@ impl Server { imap_server: None, auth_server: None, dav_unsecure_server: None, + dav_server: None, pid_file: config.pid, }) } @@ -63,12 +65,17 @@ impl Server { let dav_unsecure_server = config .dav_unsecure .map(|dav_config| dav::new_unsecure(dav_config, login.clone())); + let dav_server = config + .dav + .map(|dav_config| dav::new(dav_config, login.clone())) + .transpose()?; Ok(Self { lmtp_server, imap_unsecure_server, imap_server, dav_unsecure_server, + dav_server, auth_server, pid_file: config.pid, }) @@ -125,6 +132,12 @@ impl Server { None => Ok(()), Some(s) => s.run(exit_signal.clone()).await, } + }, + async { + match self.dav_server { + None => Ok(()), + Some(s) => s.run(exit_signal.clone()).await, + } } )?;