use axum::Router; use axum::extract::State; use axum::response::{Html, IntoResponse, Response}; use axum::routing::{get, post}; use axum::serve::Listener as AxumListener; use axum_extra::extract::cookie::CookieJar; use http::StatusCode; use minijinja::{Environment, context, path_loader}; use static_serve::embed_assets; use crate::db::{Database, DatabaseInterface, Operation}; use crate::listener::{Listener, ListenerKind}; use crate::stream::{AbstractSocketAddr, AbstractStreamKind}; mod login; mod logout; mod session; use session::HttpSessionManager; impl AxumListener for Listener { type Io = AbstractStreamKind; type Addr = AbstractSocketAddr; async fn accept(&mut self) -> (Self::Io, Self::Addr) { loop { let res = match &self.kind { ListenerKind::Tcp(l) => l .accept() .await .map(|(stream, remote_addr)| (stream.into(), remote_addr.into())), ListenerKind::Uds(l) => l .accept() .await .map(|(stream, remote_addr)| (stream.into(), remote_addr.into())), }; match res { Ok((stream, remote_addr)) => return (stream, remote_addr), Err(e) => { // Here the error could be fatal, or could simply be that a client aborted the connected, // in which case we don't want to crash the server, simply skip this client connection. // https://doc.rust-lang.org/stable/std/net/struct.TcpListener.html#errors match e.kind() { std::io::ErrorKind::ConnectionAborted => {} _ => panic!("Unrecoverable HTTP client connection error: {e}"), } } } } } fn local_addr(&self) -> std::io::Result { match &self.kind { ListenerKind::Tcp(l) => l.local_addr().map(Into::into), ListenerKind::Uds(l) => l.local_addr().map(Into::into), } } } #[derive(Clone)] pub struct HttpState { pub db: Database, pub sessions: HttpSessionManager, pub templates: Environment<'static>, } impl HttpState { pub fn new(db: Database) -> Self { let mut templates = Environment::new(); templates.set_loader(path_loader("templates")); Self { db, sessions: HttpSessionManager::new(), templates, } } } pub async fn http_listen(listener: Listener, db: Database) { embed_assets!("assets"); let app = Router::new() .nest("/assets", static_router()) .route("/", get(home)) .route("/login", get(home)) .route("/login", post(login::post_login)) .route("/logout", get(logout::logout)) .with_state(HttpState::new(db)); axum::serve(listener, app).await.unwrap(); } pub async fn home( State(state): State>, cookies: CookieJar, ) -> Response { if let Some(session) = state.sessions.get_session(&cookies) { // When the user has no domain (service admin) list all domains let op = Operation::ListUsers(session.user.domain.clone()); let other_users = if session.user.can_perform(&op) { match state.db.list_users(session.user.domain.clone()).await { Ok(other_users) => other_users, Err(e) => { return format!("Database error: {e}").into_response(); } } } else { vec![] }; tracing::info!( "Found {} users on domain {:?}", other_users.len(), session.user.domain ); let ctx = context! { user => session.user, other_users, }; let page = state .templates .get_template("home.html") .unwrap() .render(ctx) .unwrap(); (StatusCode::OK, Html(page)).into_response() } else { login::login_page(State(state), None).await } }