diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..354d600 --- /dev/null +++ b/Cargo.toml @@ -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" + diff --git a/config.json b/config.json new file mode 100644 index 0000000..46075f6 --- /dev/null +++ b/config.json @@ -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" + } +} diff --git a/response.json b/response.json new file mode 100644 index 0000000..9863bb7 --- /dev/null +++ b/response.json @@ -0,0 +1,8 @@ +{ + "status": "success", + "message": "Hello from the Rust HTTP listener!", + "data": { + "id": 1, + "name": "Gemini CLI Sample" + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..638bded --- /dev/null +++ b/src/config.rs @@ -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, +} + +#[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, + 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"), + }, + } + } +} diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..91dabb8 --- /dev/null +++ b/src/handlers.rs @@ -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, +) -> 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::(&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" })), + ) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2537935 --- /dev/null +++ b/src/main.rs @@ -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> { + // 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!("------------------------------------------"); +} diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 0000000..884cea7 --- /dev/null +++ b/src/middleware.rs @@ -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, + req: Request, + 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 +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..7fb646d --- /dev/null +++ b/src/state.rs @@ -0,0 +1,5 @@ +use std::sync::Arc; + +pub struct AppState {} + +pub type SharedState = Arc;