feat: modularize codebase and add multi-route support with live reload

This commit is contained in:
danielvici123
2026-06-08 21:27:11 +02:00
parent 56d3b0f9e0
commit 922b7170a4
8 changed files with 320 additions and 0 deletions

14
Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "dhl"
version = "0.5.0"
edition = "2024"
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
axum-server = { version = "0.7", features = ["tls-rustls"] }
tower-http = { version = "0.5", features = ["trace"] }
chrono = "0.4"

25
config.json Normal file
View File

@@ -0,0 +1,25 @@
{
"host": "127.0.0.1",
"port": 3000,
"routes": [
{
"path": "/",
"response_file": "response.json",
"status_code": 200
},
{
"path": "/api/status",
"response_file": "status.json",
"status_code": 500
}
],
"masking": {
"enabled": true,
"headers": ["authorization", "x-api-key", "cookie"]
},
"tls": {
"enabled": false,
"cert_path": "cert.pem",
"key_path": "key.pem"
}
}

8
response.json Normal file
View File

@@ -0,0 +1,8 @@
{
"status": "success",
"message": "Hello from the Rust HTTP listener!",
"data": {
"id": 1,
"name": "Gemini CLI Sample"
}
}

75
src/config.rs Normal file
View File

@@ -0,0 +1,75 @@
use serde::Deserialize;
use std::path::PathBuf;
use std::fs;
#[derive(Debug, Deserialize, Clone)]
pub struct TlsConfig {
pub enabled: bool,
pub cert_path: PathBuf,
pub key_path: PathBuf,
}
#[derive(Debug, Deserialize, Clone)]
pub struct MaskingConfig {
pub enabled: bool,
pub headers: Vec<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct RouteConfig {
pub path: String,
pub response_file: PathBuf,
pub status_code: u16,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
pub host: String,
pub port: u16,
pub routes: Vec<RouteConfig>,
pub masking: MaskingConfig,
pub tls: TlsConfig,
}
impl Config {
pub fn load() -> Self {
match fs::read_to_string("config.json") {
Ok(content) => {
serde_json::from_str(&content).unwrap_or_else(|e| {
eprintln!("Warning: Failed to parse config.json ({}), using defaults.", e);
Self::default()
})
}
Err(_) => {
Self::default()
}
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 3000,
routes: vec![RouteConfig {
path: "/".to_string(),
response_file: PathBuf::from("response.json"),
status_code: 200,
}],
masking: MaskingConfig {
enabled: true,
headers: vec![
"authorization".to_string(),
"x-api-key".to_string(),
"cookie".to_string(),
],
},
tls: TlsConfig {
enabled: false,
cert_path: PathBuf::from("cert.pem"),
key_path: PathBuf::from("key.pem"),
},
}
}
}

54
src/handlers.rs Normal file
View File

@@ -0,0 +1,54 @@
use axum::{
extract::State,
http::{StatusCode, Uri},
response::IntoResponse,
Json,
};
use serde_json::json;
use std::fs;
use crate::state::SharedState;
use crate::config::Config;
pub async fn handler(
uri: Uri,
State(_state): State<SharedState>,
) -> impl IntoResponse {
let path = uri.path();
// 1. Reload config live on every request
let config = Config::load();
// 2. Find the route that matches the current path
let route = config.routes.iter().find(|r| r.path == path);
if let Some(route) = route {
match fs::read_to_string(&route.response_file) {
Ok(content) => {
match serde_json::from_str::<serde_json::Value>(&content) {
Ok(json) => {
let status = StatusCode::from_u16(route.status_code)
.unwrap_or(StatusCode::OK);
(status, Json(json)).into_response()
},
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error parsing response JSON: {}", e),
).into_response(),
}
}
Err(e) => (
StatusCode::NOT_FOUND,
format!("Error reading response file: {}", e),
).into_response(),
}
} else {
fallback_handler().await.into_response()
}
}
pub async fn fallback_handler() -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
Json(json!({ "error": "Not Found" })),
)
}

87
src/main.rs Normal file
View File

@@ -0,0 +1,87 @@
mod config;
mod state;
mod middleware;
mod handlers;
use axum::{
routing::any,
Router,
middleware as axum_middleware,
};
use axum_server::tls_rustls::RustlsConfig;
use std::net::SocketAddr;
use std::sync::Arc;
use std::fs;
use crate::config::Config;
use crate::state::AppState;
use crate::handlers::{handler, fallback_handler};
use crate::middleware::logging_middleware;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Initial load to check for files and get host/port
let config = Config::load();
// Ensure initial response files exist
for route in &config.routes {
if !route.response_file.exists() {
println!("Response file {:?} not found, creating default.", route.response_file);
let default_response = serde_json::json!({ "message": format!("edit me for path {}", route.path) });
fs::write(&route.response_file, serde_json::to_string_pretty(&default_response)?)?;
}
}
// Print custom startup banner
print_banner(&config);
// 2. Prepare application state
let state = Arc::new(AppState {});
// 3. Build router with a catch-all route for live config reloading
let app = Router::new()
.route("/", any(handler))
.route("/*path", any(handler))
.fallback(fallback_handler)
.layer(axum_middleware::from_fn_with_state(state.clone(), logging_middleware))
.with_state(state);
let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
// 4. Start server
if config.tls.enabled {
let tls_config = RustlsConfig::from_pem_file(
config.tls.cert_path.clone(),
config.tls.key_path.clone(),
)
.await?;
println!("Starting server with TLS on {}", addr);
axum_server::bind_rustls(addr, tls_config)
.serve(app.into_make_service())
.await?;
} else {
println!("Starting server on {}", addr);
axum_server::bind(addr)
.serve(app.into_make_service())
.await?;
}
Ok(())
}
fn print_banner(config: &Config) {
println!("------------------------------------------");
println!(" ____ ___ _ ______________ ");
println!(" / __ |/ | / | / / _/ ____/ / ");
println!(" / / / / /| | / |/ // // __/ / / ");
println!(" / /_/ / ___ |/ /| // // /___/ /___ ");
println!(" /_____/_/ |_/_/ |_/___/_____/_____/ ");
println!("------------------------------------------");
println!(" ");
println!("------ HTTP LISTENER ------");
println!("host : {}", config.host);
println!("port : {}", config.port);
println!("Live Config Reloading: ENABLED");
println!("------------------------------------------");
}

52
src/middleware.rs Normal file
View File

@@ -0,0 +1,52 @@
use axum::{
body::Body,
extract::{Request, State},
middleware::Next,
response::Response,
};
use chrono::Local;
use std::time::Instant;
use crate::state::SharedState;
// Note: Axum body might need to be imported differently depending on version
// but since the original used axum::body::Body, I'll stick to that or similar.
// Actually, let's use axum::body::Body.
pub async fn logging_middleware(
State(_state): State<SharedState>,
req: Request<Body>,
next: Next,
) -> Response {
let start_time = Local::now();
let start_instant = Instant::now();
// Load config live for masking settings
let config = crate::config::Config::load();
println!("---- REQUEST START {} ----", start_time.format("%Y-%m-%d %H:%M:%S"));
println!("Method: {}", req.method());
println!("Path: {}", req.uri().path());
println!("Headers:");
for (name, value) in req.headers() {
let name_str = name.as_str();
let should_mask = config.masking.enabled &&
config.masking.headers.iter().any(|h| h.eq_ignore_ascii_case(name_str));
if should_mask {
println!(" {}: ***", name_str);
} else {
println!(" {}: {:?}", name_str, value);
}
}
let response = next.run(req).await;
let end_time = Local::now();
let duration = start_instant.elapsed();
println!("--- REQUEST END {} - {:?} ---------", end_time.format("%H:%M:%S"), duration);
response
}

5
src/state.rs Normal file
View File

@@ -0,0 +1,5 @@
use std::sync::Arc;
pub struct AppState {}
pub type SharedState = Arc<AppState>;