feat: modularize codebase and add multi-route support with live reload
This commit is contained in:
75
src/config.rs
Normal file
75
src/config.rs
Normal 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
54
src/handlers.rs
Normal 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
87
src/main.rs
Normal 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
52
src/middleware.rs
Normal 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
5
src/state.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AppState {}
|
||||
|
||||
pub type SharedState = Arc<AppState>;
|
||||
Reference in New Issue
Block a user