From e52b8e1c2e110d0feb74feb7905c2ff064b51d55 Mon Sep 17 00:00:00 2001 From: UMTS at Teleco Date: Sat, 13 Dec 2025 02:48:13 +0100 Subject: committing to insanity --- src/main.rs | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 src/main.rs (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..17702f4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,359 @@ +// Main entry point for the SeckelAPI server +use anyhow::Result; +use axum::{ + http::Method, + response::Json, + routing::{get, post}, + Router, +}; +use chrono::Utc; +use serde_json::json; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::time::{sleep, Duration}; +use tower_governor::{ + governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, GovernorLayer, +}; +use tower_http::cors::{Any, CorsLayer}; +use tracing::{error, info, Level}; +use tracing_subscriber; + +mod auth; +mod config; +mod db; +mod logging; +mod models; +mod permissions; +mod routes; +mod scheduler; +mod sql; + +use auth::SessionManager; +use config::{Config, DatabaseConfig}; +use logging::AuditLogger; +use scheduler::QueryScheduler; + +use axum::extract::State; +use db::{Database, DatabaseInitError}; +use permissions::RBACManager; + +// Version pulled from Cargo.toml +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Root handler - simple version info +async fn root() -> Json { + Json(json!({ + "name": "SeckelAPI", + "version": VERSION + })) +} + +// Health check handler with database connectivity test +async fn health_check(State(state): State) -> Json { + // Test database connectivity + let db_status = match sqlx::query("SELECT 1") + .fetch_one(state.database.pool()) + .await + { + Ok(_) => "connected", + Err(_) => "disconnected", + }; + + Json(json!({ + "status": "running", + "message": format!("SeckelAPI v{}: The schizophrenic database application JSON API", VERSION), + "database": db_status + })) +} + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize tracing + tracing_subscriber::fmt().with_max_level(Level::INFO).init(); + + info!("Starting SeckelAPI Server..."); + + // Load configuration + let config = Config::load()?; + let config_arc = Arc::new(config.clone()); + info!("- Configuration might have loaded successfully"); + + // Initialize database connection + let database = wait_for_database(&config.database).await?; + info!("- Database said 'Hello there!'"); + + // Initialize session manager + let session_manager = SessionManager::new(config_arc.clone()); + info!("- Session manager didn't crash on startup (yay!)"); + + // Spawn background task for session cleanup + let cleanup_manager = session_manager.clone(); + let cleanup_interval = config.security.session_cleanup_interval_minutes; + tokio::spawn(async move { + let mut interval = + tokio::time::interval(tokio::time::Duration::from_secs(cleanup_interval * 60)); + loop { + interval.tick().await; + cleanup_manager.cleanup_expired_sessions(); + info!("- Ran session cleanup task"); + } + }); + info!( + "- Background session cleanup task spawned (interval: {}min)", + cleanup_interval + ); + + // Initialize RBAC manager + let rbac = RBACManager::new(&config); + info!("- Overcomplicated RBAC manager has initialized"); + + // Initialize audit logger + let logging = AuditLogger::new( + config.logging.request_log.clone(), + config.logging.query_log.clone(), + config.logging.error_log.clone(), + config.logging.warning_log.clone(), + config.logging.info_log.clone(), + config.logging.combined_log.clone(), + config.logging.mask_passwords, + config.logging.sensitive_fields.clone(), + config.logging.custom_filters.clone(), + )?; + info!("- CIA Surveillance Service Agents have been initialized on your system (just kidding, just the logging stack)"); + if !config.logging.custom_filters.is_empty() { + let enabled_count = config + .logging + .custom_filters + .iter() + .filter(|f| f.enabled) + .count(); + info!("- {} custom log filter(s) active", enabled_count); + } + + spawn_database_monitor(database.clone(), config.database.clone(), logging.clone()); + + // Initialize and spawn scheduled query tasks + let scheduler = QueryScheduler::new(config_arc.clone(), database.clone(), logging.clone()); + scheduler.spawn_tasks(); + + // Create CORS layer + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_headers(Any) + .allow_origin(Any); + + // Build auth routes with rate limiting + let mut auth_routes = Router::new() + .route("/auth/login", post(routes::auth::login)) + .route("/auth/logout", post(routes::auth::logout)) + .route("/auth/status", get(routes::auth::status)); + + if config.security.enable_rate_limiting { + info!( + "- Auth rate limiting set to: {}/min, {}/sec per IP", + config.security.auth_rate_limit_per_minute, config.security.auth_rate_limit_per_second + ); + let auth_governor_conf = Arc::new( + GovernorConfigBuilder::default() + .per_second(config.security.auth_rate_limit_per_second.max(1) as u64) + .burst_size(config.security.auth_rate_limit_per_minute.max(1)) + .key_extractor(SmartIpKeyExtractor) + .finish() + .expect("Failed to build auth rate limiter config ... you sure its configured?"), + ); + auth_routes = auth_routes.layer(GovernorLayer::new(auth_governor_conf)); + } else { + info!("- Auth rate limiting DISABLED (dont run in production like this plz) "); + } + + // Build API routes with rate limiting + let mut api_routes = Router::new() + .route("/query", post(routes::query::execute_query)) + .route("/permissions", get(routes::query::get_permissions)) + .route( + "/preferences", + post(routes::preferences::handle_preferences), + ); + + if config.security.enable_rate_limiting { + info!( + "- API rate limiting set to: {}/min, {}/sec per IP", + config.security.api_rate_limit_per_minute, config.security.api_rate_limit_per_second + ); + let api_governor_conf = Arc::new( + GovernorConfigBuilder::default() + .per_second(config.security.api_rate_limit_per_second.max(1) as u64) + .burst_size(config.security.api_rate_limit_per_minute.max(1)) + .key_extractor(SmartIpKeyExtractor) + .finish() + .expect( + "Failed to build API rate limiter config, ugh ... you sure its configured?", + ), + ); + api_routes = api_routes.layer(GovernorLayer::new(api_governor_conf)); + } + + // das so routes was es chan + let app = Router::new() + .route("/", get(root)) + .route("/health", get(health_check)) + .merge(auth_routes) + .merge(api_routes) + .layer(tower_http::limit::RequestBodyLimitLayer::new( + config.server.request_body_limit_mb * 1024 * 1024, + )) + .layer(cors) + .with_state(AppState { + config: config.clone(), + database, + session_manager, + rbac, + logging, + }); + let addr = SocketAddr::from(([0, 0, 0, 0], config.server.port)); + info!( + "- SeckelAPI somehow started and should now be listening on {} :)", + addr + ); + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await?; + Ok(()) +} + +async fn wait_for_database(config: &DatabaseConfig) -> Result { + let retry_delay = config.connection_timeout_wait.max(1); + + loop { + match Database::new(config).await { + Ok(db) => return Ok(db), + Err(DatabaseInitError::Retryable(err)) => { + error!( + "Database unavailable (retrying in {}s): {}", + retry_delay, err + ); + sleep(Duration::from_secs(retry_delay)).await; + } + Err(DatabaseInitError::Fatal(err)) => { + error!("Fatal database configuration error: {}", err); + return Err(err); + } + } + } +} + +fn spawn_database_monitor(database: Database, db_config: DatabaseConfig, logging: AuditLogger) { + let heartbeat_interval = db_config.connection_check.max(5); + let retry_delay = db_config.connection_timeout_wait.max(1); + + tokio::spawn(async move { + let mut ticker = tokio::time::interval(Duration::from_secs(heartbeat_interval)); + loop { + ticker.tick().await; + match sqlx::query("SELECT 1").fetch_one(database.pool()).await { + Ok(_) => { + if !database.is_available() { + info!("Database connectivity restored"); + log_database_event( + &logging, + "database_reconnect", + "Database connectivity restored", + false, + ) + .await; + } + database.mark_available(); + } + Err(err) => { + database.mark_unavailable(); + error!( + "Database heartbeat failed (retrying every {}s): {}", + retry_delay, err + ); + + log_database_event( + &logging, + "database_heartbeat_failure", + &format!("Heartbeat failed: {}", err), + true, + ) + .await; + + loop { + sleep(Duration::from_secs(retry_delay)).await; + match sqlx::query("SELECT 1").fetch_one(database.pool()).await { + Ok(_) => { + database.mark_available(); + info!("Database reconnected successfully"); + log_database_event( + &logging, + "database_reconnect", + "Database reconnected successfully", + false, + ) + .await; + break; + } + Err(retry_err) => { + error!( + "Database still unavailable (retrying in {}s): {}", + retry_delay, retry_err + ); + log_database_event( + &logging, + "database_retry_failure", + &format!("Database retry failed: {}", retry_err), + true, + ) + .await; + } + } + } + } + } + } + }); +} + +async fn log_database_event(logging: &AuditLogger, context: &str, message: &str, as_error: bool) { + let request_id = AuditLogger::generate_request_id(); + let timestamp = Utc::now(); + let result = if as_error { + logging + .log_error( + &request_id, + timestamp, + message, + Some(context), + Some("system"), + None, + ) + .await + } else { + logging + .log_info( + &request_id, + timestamp, + message, + Some(context), + Some("system"), + None, + ) + .await + }; + + if let Err(err) = result { + error!("Failed to record database event ({}): {}", context, err); + } +} +#[derive(Clone)] +pub struct AppState { + pub config: Config, + pub database: Database, + pub session_manager: SessionManager, + pub rbac: RBACManager, + pub logging: AuditLogger, +} -- cgit v1.2.3-70-g09d2