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; #[cfg(not(feature = "embed"))] use minijinja::path_loader; use minijinja::{Environment, context}; #[cfg(feature = "embed")] use static_serve::embed_assets; #[cfg(not(feature = "embed"))] use tower_http::services::ServeDir; 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(); #[cfg(feature = "embed")] minijinja_embed::load_templates!(&mut templates); #[cfg(not(feature = "embed"))] templates.set_loader(path_loader("templates")); Self { db, sessions: HttpSessionManager::new(), templates, } } } pub async fn http_listen(listener: Listener, db: Database) { #[cfg(all(feature = "embed", feature = "noembed"))] compile_error!("You cannot have `embed` and `noembed` features enabled at the same time."); #[cfg(not(any(feature = "embed", feature = "noembed")))] compile_error!("You must have `embed` or `noembed` feature enabled."); #[cfg(feature = "embed")] let app = { embed_assets!("assets"); Router::new().nest("/assets", static_router()) }; #[cfg(not(feature = "embed"))] let app = { Router::new().nest_service("/assets", ServeDir::new("assets")) }; let app = app .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 } }