use axum::{ error_handling::HandleErrorLayer, handler::Handler, http::StatusCode, middleware, response::IntoResponse, routing::{get, post}, Extension, Json, Router, }; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::{Deserialize, Serialize}; use solarlib::star::Star; use std::{net::SocketAddr, str::FromStr, sync::Arc, time::Duration}; use tower::{BoxError, ServiceBuilder}; use tower_http::trace::TraceLayer; use tracing_subscriber::prelude::*; mod errors; mod handlers; #[derive(Clone)] pub struct State { pub hw_url: String, pub secret_key: String, pub gen_key: String, } #[tokio::main] async fn main() { kankyo::init(); color_eyre::install().unwrap(); tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| "solard=info,tower_http=debug".into()), )) .with(tracing_subscriber::fmt::layer()) .init(); let rand_key: String = thread_rng() .sample_iter(&Alphanumeric) .take(30) .map(char::from) .collect(); let shared_state = Arc::new(State { hw_url: std::env::var("HOMEWORLD_URL").expect("No Homeworld URL set"), secret_key: std::env::var("SECRET_KEY").unwrap_or("bad-key".to_string()), gen_key: rand_key, }); if shared_state.secret_key == "bad-key" { tracing::warn!("No secret key set! This is a bad idea."); tracing::warn!("Using default of `bad-key`"); } tracing::info!("Random Key: {}", shared_state.gen_key); let app = Router::new() .route("/health", get(health_check)) .route("/planets/list", get(handlers::planets::list)) .route("/planets/new", post(handlers::planets::new_planet)) .route_layer(middleware::from_fn(handlers::auth::requires_auth)) .route("/planets/:uuid", get(handlers::planets::get)) .route("/planets/:uuid/shutdown", post(handlers::planets::shutdown)) .route_layer(middleware::from_fn(handlers::auth::requires_auth)) .route( "/planets/:uuid/shutdown/hard", post(handlers::planets::force_shutdown), ) .route_layer(middleware::from_fn(handlers::auth::requires_auth)) .route("/planets/:uuid/start", post(handlers::planets::start)) .route_layer(middleware::from_fn(handlers::auth::requires_auth)) .route("/planets/:uuid/pause", post(handlers::planets::pause)) .route_layer(middleware::from_fn(handlers::auth::requires_auth)) .route("/planets/:uuid/reboot", post(handlers::planets::reboot)) .route_layer(middleware::from_fn(handlers::auth::requires_auth)) .route( "/planets/:uuid/reboot/hard", post(handlers::planets::force_reboot), ) .route_layer(middleware::from_fn(handlers::auth::requires_auth)) .route("/planets/:uuid/destroy", post(handlers::planets::no_planet)) .route_layer(middleware::from_fn(handlers::auth::requires_auth)) // Authentication .route("/auth/begin", post(handlers::auth::begin)) .layer( ServiceBuilder::new() .layer(HandleErrorLayer::new(|error: BoxError| async move { if error.is::() { Ok(StatusCode::REQUEST_TIMEOUT) } else { Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Unhandled internal error: {}", error), )) } })) .timeout(Duration::from_secs(10)) .layer(TraceLayer::new_for_http()) .into_inner(), ) .layer(Extension(shared_state)) .fallback(handler_404.into_service()); let addr = SocketAddr::from_str(std::env::var("BIND_ADDR").unwrap().as_str().into()).unwrap(); tracing::info!("Listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } async fn health_check() -> &'static str { "OK" } fn get_star() -> Result { let con_url = std::env::var("QEMU_URL").unwrap_or("qemu:///system".to_string()); Ok(Star::new(con_url)?) } async fn handler_404() -> impl IntoResponse { StatusCode::NOT_FOUND }