TLS + Fix auth

This commit is contained in:
Quentin 2024-03-13 15:45:36 +01:00
parent 98adb1e20d
commit 3abdafb0db
Signed by: quentin
GPG key ID: E9602264D639FF68
5 changed files with 100 additions and 93 deletions

View file

@ -1,4 +1,5 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use base64::Engine; use base64::Engine;
@ -10,23 +11,55 @@ use http_body_util::Full;
use futures::stream::{FuturesUnordered, StreamExt}; use futures::stream::{FuturesUnordered, StreamExt};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::sync::watch; 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_user::login::ArcLoginProvider;
use aero_collections::user::User; use aero_collections::user::User;
pub struct Server { pub struct Server {
bind_addr: SocketAddr, bind_addr: SocketAddr,
login_provider: ArcLoginProvider, login_provider: ArcLoginProvider,
tls: Option<TlsAcceptor>,
} }
pub fn new_unsecure(config: DavUnsecureConfig, login: ArcLoginProvider) -> Server { pub fn new_unsecure(config: DavUnsecureConfig, login: ArcLoginProvider) -> Server {
Server { Server {
bind_addr: config.bind_addr, bind_addr: config.bind_addr,
login_provider: login, login_provider: login,
tls: None,
} }
} }
pub fn new(config: DavConfig, login: ArcLoginProvider) -> Result<Server> {
let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open(
config.certs,
)?))
.collect::<Result<Vec<_>, _>>()?;
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<T: Unpin + AsyncRead + AsyncWrite + Send> Stream for TokioIo<T> {}
impl Server { impl Server {
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> { pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
let tcp = TcpListener::bind(self.bind_addr).await?; let tcp = TcpListener::bind(self.bind_addr).await?;
@ -47,14 +80,24 @@ impl Server {
_ = must_exit.changed() => continue, _ = must_exit.changed() => continue,
}; };
tracing::info!("Accepted connection from {}", remote_addr); 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 login = self.login_provider.clone();
let conn = tokio::spawn(async move { let conn = tokio::spawn(async move {
//@FIXME should create a generic "public web" server on which "routers" could be //@FIXME should create a generic "public web" server on which "routers" could be
//abitrarily bound //abitrarily bound
//@FIXME replace with a handler supporting http2 and TLS //@FIXME replace with a handler supporting http2 and TLS
match http::Builder::new().serve_connection(stream, service_fn(|req: Request<hyper::body::Incoming>| { match http::Builder::new().serve_connection(stream, service_fn(|req: Request<hyper::body::Incoming>| {
let login = login.clone(); let login = login.clone();
tracing::info!("{:?} {:?}", req.method(), req.uri());
async move { async move {
auth(login, req).await auth(login, req).await
} }
@ -72,6 +115,16 @@ impl Server {
Ok(()) Ok(())
} }
async fn build_stream(&self, socket: TcpStream) -> Result<Box<dyn Stream>> {
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 //@FIXME We should not support only BasicAuth
@ -80,18 +133,26 @@ async fn auth(
req: Request<impl hyper::body::Body>, req: Request<impl hyper::body::Body>,
) -> Result<Response<Full<Bytes>>> { ) -> Result<Response<Full<Bytes>>> {
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()?, Some(hv) => hv.to_str()?,
None => return Ok(Response::builder() None => {
tracing::info!("Missing authorization field");
return Ok(Response::builder()
.status(401) .status(401)
.body(Full::new(Bytes::from("Missing Authorization field")))?), .header("WWW-Authenticate", "Basic realm=\"Aerogramme\"")
.body(Full::new(Bytes::from("Missing Authorization field")))?)
},
}; };
let b64_creds_maybe_padded = match auth_val.split_once(" ") { let b64_creds_maybe_padded = match auth_val.split_once(" ") {
Some(("Basic", b64)) => b64, Some(("Basic", b64)) => b64,
_ => return Ok(Response::builder() _ => {
tracing::info!("Unsupported authorization field");
return Ok(Response::builder()
.status(400) .status(400)
.body(Full::new(Bytes::from("Unsupported Authorization field")))?), .body(Full::new(Bytes::from("Unsupported Authorization field")))?)
},
}; };
// base64urlencoded may have trailing equals, base64urlsafe has not // base64urlencoded may have trailing equals, base64urlsafe has not
@ -110,9 +171,13 @@ async fn auth(
// Call login provider // Call login provider
let creds = match login.login(username, password).await { let creds = match login.login(username, password).await {
Ok(c) => c, Ok(c) => c,
Err(_) => return Ok(Response::builder() Err(_) => {
tracing::info!(user=username, "Wrong credentials");
return Ok(Response::builder()
.status(401) .status(401)
.body(Full::new(Bytes::from("Wrong credentials")))?), .header("WWW-Authenticate", "Basic realm=\"Aerogramme\"")
.body(Full::new(Bytes::from("Wrong credentials")))?)
},
}; };
// Build a user // Build a user
@ -124,6 +189,7 @@ async fn auth(
async fn router(user: std::sync::Arc<User>, req: Request<impl hyper::body::Body>) -> Result<Response<Full<Bytes>>> { async fn router(user: std::sync::Arc<User>, req: Request<impl hyper::body::Body>) -> Result<Response<Full<Bytes>>> {
let path_segments: Vec<_> = req.uri().path().split("/").filter(|s| *s != "").collect(); let path_segments: Vec<_> = req.uri().path().split("/").filter(|s| *s != "").collect();
tracing::info!("router");
match path_segments.as_slice() { match path_segments.as_slice() {
[] => tracing::info!("root"), [] => tracing::info!("root"),
[ username, ..] if *username != user.username => return Ok(Response::builder() [ username, ..] if *username != user.username => return Ok(Response::builder()

View file

@ -333,85 +333,4 @@ impl NetLoop {
}; };
} }
} }
/*
async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc<Notify>) -> Result<LoopMode> {
// 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)
},
};
}*/
} }

View file

@ -23,6 +23,7 @@ pub struct ProviderConfig {
pub imap_unsecure: Option<ImapUnsecureConfig>, pub imap_unsecure: Option<ImapUnsecureConfig>,
pub lmtp: Option<LmtpConfig>, pub lmtp: Option<LmtpConfig>,
pub auth: Option<AuthConfig>, pub auth: Option<AuthConfig>,
pub dav: Option<DavConfig>,
pub dav_unsecure: Option<DavUnsecureConfig>, pub dav_unsecure: Option<DavUnsecureConfig>,
pub users: UserManagement, pub users: UserManagement,
} }
@ -58,6 +59,13 @@ pub struct DavUnsecureConfig {
pub bind_addr: SocketAddr, 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)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ImapUnsecureConfig { pub struct ImapUnsecureConfig {
pub bind_addr: SocketAddr, pub bind_addr: SocketAddr,

View file

@ -171,6 +171,7 @@ async fn main() -> Result<()> {
AnyConfig::Provider(ProviderConfig { AnyConfig::Provider(ProviderConfig {
pid: None, pid: None,
imap: None, imap: None,
dav: None,
imap_unsecure: Some(ImapUnsecureConfig { imap_unsecure: Some(ImapUnsecureConfig {
bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143), bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143),
}), }),

View file

@ -21,6 +21,7 @@ pub struct Server {
imap_server: Option<imap::Server>, imap_server: Option<imap::Server>,
auth_server: Option<auth::AuthServer>, auth_server: Option<auth::AuthServer>,
dav_unsecure_server: Option<dav::Server>, dav_unsecure_server: Option<dav::Server>,
dav_server: Option<dav::Server>,
pid_file: Option<PathBuf>, pid_file: Option<PathBuf>,
} }
@ -37,6 +38,7 @@ impl Server {
imap_server: None, imap_server: None,
auth_server: None, auth_server: None,
dav_unsecure_server: None, dav_unsecure_server: None,
dav_server: None,
pid_file: config.pid, pid_file: config.pid,
}) })
} }
@ -63,12 +65,17 @@ impl Server {
let dav_unsecure_server = config let dav_unsecure_server = config
.dav_unsecure .dav_unsecure
.map(|dav_config| dav::new_unsecure(dav_config, login.clone())); .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 { Ok(Self {
lmtp_server, lmtp_server,
imap_unsecure_server, imap_unsecure_server,
imap_server, imap_server,
dav_unsecure_server, dav_unsecure_server,
dav_server,
auth_server, auth_server,
pid_file: config.pid, pid_file: config.pid,
}) })
@ -125,6 +132,12 @@ impl Server {
None => Ok(()), None => Ok(()),
Some(s) => s.run(exit_signal.clone()).await, Some(s) => s.run(exit_signal.clone()).await,
} }
},
async {
match self.dav_server {
None => Ok(()),
Some(s) => s.run(exit_signal.clone()).await,
}
} }
)?; )?;