135 lines
3.4 KiB
Rust
135 lines
3.4 KiB
Rust
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<usize> = 1..=65535;
|
|
|
|
fn port_in_range(s: &str) -> Result<u16, String> {
|
|
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<AppState>) -> 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<AppState>) -> 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()
|
|
}
|