mod client; mod config; mod handlers; mod language; mod templates; use axum::Router; use axum::extract::State; use axum::response::IntoResponse; use axum::routing::get; use clap::Parser; use handlers::{gpg_handler, root_handler}; use sha2::{Digest, Sha256}; use std::net::ToSocketAddrs; use std::ops::RangeInclusive; const STATIC_JS: &'static str = include_str!("../assets/script.js"); const STATIC_CSS: &'static str = include_str!("../assets/style.css"); #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// Path to configuration file #[arg(short, long)] config: std::path::PathBuf, /// Network port to use #[arg(short, long, value_parser = port_in_range, value_name = "PORT", default_value = "8080")] port: u16, /// Host to listen #[arg(long, default_value = "127.0.0.1")] host: String, } #[derive(Clone)] pub struct AppState { config_path: std::path::PathBuf, css_content: &'static str, js_content: &'static str, css_url: String, script_js_url: String, } const PORT_RANGE: RangeInclusive = 1..=65535; fn port_in_range(s: &str) -> Result { let port: usize = s .parse() .map_err(|_| format!("`{s}` isn't a port number"))?; if PORT_RANGE.contains(&port) { Ok(port as u16) } else { Err(format!( "port not in range {}-{}", PORT_RANGE.start(), PORT_RANGE.end() )) } } #[tokio::main] async fn main() { let args = Args::parse(); let css_hash = calculate_sha256(STATIC_CSS); let js_hash = calculate_sha256(STATIC_JS); let css_path = format!("/style-{css_hash}.js"); let js_path = format!("/script-{js_hash}.js"); let state = AppState { config_path: args.config, css_content: STATIC_CSS, js_content: STATIC_JS, css_url: css_path.clone(), script_js_url: js_path.clone(), }; let app = Router::new() .route("/", get(root_handler)) .route("/gpgkey.txt", get(gpg_handler)) .route(css_path.as_str(), get(static_css_handler)) .route(js_path.as_str(), get(static_js_handler)) .with_state(state); let addr = format!("{}:{}", args.host, args.port) .to_socket_addrs() .expect("Fail to get address") .next() .ok_or("failed to resolve address") .expect(""); println!("Server running on http://{}", addr); axum::serve( tokio::net::TcpListener::bind(addr) .await .expect("Failed to bind to address"), app, ) .await .expect("Server failed to start"); } async fn static_css_handler(State(state): State) -> impl IntoResponse { let headers = [ (axum::http::header::CONTENT_TYPE, mime::CSS.as_ref()), (axum::http::header::CACHE_CONTROL, "public, max-age=86400"), ]; (headers, state.css_content).into_response() } async fn static_js_handler(State(state): State) -> impl IntoResponse { let headers = [ ( axum::http::header::CONTENT_TYPE, mime::APPLICATION_JAVASCRIPT.as_ref(), ), (axum::http::header::CACHE_CONTROL, "public, max-age=86400"), ]; (headers, state.js_content).into_response() } fn calculate_sha256(input: &str) -> String { let mut hasher = Sha256::new(); hasher.update(input.as_bytes()); let result = hasher.finalize(); hex::encode(result)[..6].to_string() }