1pub mod ai_handler;
166pub mod auth;
167pub mod chain_handlers;
168pub mod contract_diff_middleware;
170pub mod coverage;
171pub mod file_generator;
173pub mod file_server;
175pub mod health;
177pub mod http_tracing_middleware;
178pub mod latency_profiles;
180pub mod management;
182pub mod management_ws;
184pub mod metrics_middleware;
185pub mod middleware;
186pub mod op_middleware;
187pub mod proxy_server;
189pub mod quick_mock;
191pub mod rag_ai_generator;
193pub mod replay_listing;
195pub mod request_logging;
196pub mod spec_import;
198pub mod sse;
200pub mod state_machine_api;
202pub mod tls;
204pub mod token_response;
206pub mod ui_builder;
208pub mod verification;
210
211pub mod handlers;
213
214pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
216pub use health::{HealthManager, ServiceStatus};
218
219pub use management::{
221 management_router, management_router_with_ui_builder, ManagementState, MockConfig,
222 ServerConfig, ServerStats,
223};
224
225pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
227
228pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
230
231pub use verification::verification_router;
233
234pub use metrics_middleware::collect_http_metrics;
236
237pub use http_tracing_middleware::http_tracing_middleware;
239
240pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
242
243async fn load_persona_from_config() -> Option<Arc<Persona>> {
246 use mockforge_core::config::load_config;
247
248 let config_paths = [
250 "config.yaml",
251 "mockforge.yaml",
252 "tools/mockforge/config.yaml",
253 "../tools/mockforge/config.yaml",
254 ];
255
256 for path in &config_paths {
257 if let Ok(config) = load_config(path).await {
258 if let Some(persona) = config.mockai.intelligent_behavior.personas.get_active_persona() {
261 tracing::info!(
262 "Loaded active persona '{}' from config file: {}",
263 persona.name,
264 path
265 );
266 return Some(Arc::new(persona.clone()));
267 } else {
268 tracing::debug!(
269 "No active persona found in config file: {} (personas count: {})",
270 path,
271 config.mockai.intelligent_behavior.personas.personas.len()
272 );
273 }
274 } else {
275 tracing::debug!("Could not load config from: {}", path);
276 }
277 }
278
279 tracing::debug!("No persona found in config files, persona-based generation will be disabled");
280 None
281}
282
283use axum::middleware::from_fn_with_state;
284use axum::{extract::State, response::Json, Router};
285use mockforge_core::failure_injection::{FailureConfig, FailureInjector};
286use mockforge_core::latency::LatencyInjector;
287use mockforge_core::openapi::OpenApiSpec;
288use mockforge_core::openapi_routes::OpenApiRouteRegistry;
289use mockforge_core::openapi_routes::ValidationOptions;
290use mockforge_core::intelligent_behavior::config::Persona;
291use std::sync::Arc;
292use tower_http::cors::{Any, CorsLayer};
293
294use mockforge_core::LatencyProfile;
295#[cfg(feature = "data-faker")]
296use mockforge_data::provider::register_core_faker_provider;
297use std::collections::HashMap;
298use std::ffi::OsStr;
299use std::path::Path;
300use tokio::fs;
301use tokio::sync::RwLock;
302use tracing::*;
303
304#[derive(Clone)]
306pub struct RouteInfo {
307 pub method: String,
309 pub path: String,
311 pub operation_id: Option<String>,
313 pub summary: Option<String>,
315 pub description: Option<String>,
317 pub parameters: Vec<String>,
319}
320
321#[derive(Clone)]
323pub struct HttpServerState {
324 pub routes: Vec<RouteInfo>,
326 pub rate_limiter: Option<std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>>,
328 pub production_headers: Option<std::sync::Arc<std::collections::HashMap<String, String>>>,
330}
331
332impl Default for HttpServerState {
333 fn default() -> Self {
334 Self::new()
335 }
336}
337
338impl HttpServerState {
339 pub fn new() -> Self {
341 Self {
342 routes: Vec::new(),
343 rate_limiter: None,
344 production_headers: None,
345 }
346 }
347
348 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
350 Self {
351 routes,
352 rate_limiter: None,
353 production_headers: None,
354 }
355 }
356
357 pub fn with_rate_limiter(
359 mut self,
360 rate_limiter: std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>,
361 ) -> Self {
362 self.rate_limiter = Some(rate_limiter);
363 self
364 }
365
366 pub fn with_production_headers(
368 mut self,
369 headers: std::sync::Arc<std::collections::HashMap<String, String>>,
370 ) -> Self {
371 self.production_headers = Some(headers);
372 self
373 }
374}
375
376async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
378 let route_info: Vec<serde_json::Value> = state
379 .routes
380 .iter()
381 .map(|route| {
382 serde_json::json!({
383 "method": route.method,
384 "path": route.path,
385 "operation_id": route.operation_id,
386 "summary": route.summary,
387 "description": route.description,
388 "parameters": route.parameters
389 })
390 })
391 .collect();
392
393 Json(serde_json::json!({
394 "routes": route_info,
395 "total": state.routes.len()
396 }))
397}
398
399pub async fn build_router(
401 spec_path: Option<String>,
402 options: Option<ValidationOptions>,
403 failure_config: Option<FailureConfig>,
404) -> Router {
405 build_router_with_multi_tenant(
406 spec_path,
407 options,
408 failure_config,
409 None,
410 None,
411 None,
412 None,
413 None,
414 None,
415 None,
416 )
417 .await
418}
419
420fn apply_cors_middleware(
422 app: Router,
423 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
424) -> Router {
425 use http::Method;
426 use tower_http::cors::AllowOrigin;
427
428 if let Some(config) = cors_config {
429 if !config.enabled {
430 return app;
431 }
432
433 let mut cors_layer = CorsLayer::new();
434 let mut is_wildcard_origin = false;
435
436 if config.allowed_origins.contains(&"*".to_string()) {
438 cors_layer = cors_layer.allow_origin(Any);
439 is_wildcard_origin = true;
440 } else if !config.allowed_origins.is_empty() {
441 let origins: Vec<_> = config
443 .allowed_origins
444 .iter()
445 .filter_map(|origin| {
446 origin.parse::<http::HeaderValue>().ok().map(|hv| AllowOrigin::exact(hv))
447 })
448 .collect();
449
450 if origins.is_empty() {
451 warn!("No valid CORS origins configured, using permissive CORS");
453 cors_layer = cors_layer.allow_origin(Any);
454 is_wildcard_origin = true;
455 } else {
456 if origins.len() == 1 {
459 cors_layer = cors_layer.allow_origin(origins[0].clone());
460 is_wildcard_origin = false;
461 } else {
462 warn!(
464 "Multiple CORS origins configured, using permissive CORS. \
465 Consider using '*' for all origins."
466 );
467 cors_layer = cors_layer.allow_origin(Any);
468 is_wildcard_origin = true;
469 }
470 }
471 } else {
472 cors_layer = cors_layer.allow_origin(Any);
474 is_wildcard_origin = true;
475 }
476
477 if !config.allowed_methods.is_empty() {
479 let methods: Vec<Method> =
480 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
481 if !methods.is_empty() {
482 cors_layer = cors_layer.allow_methods(methods);
483 }
484 } else {
485 cors_layer = cors_layer.allow_methods([
487 Method::GET,
488 Method::POST,
489 Method::PUT,
490 Method::DELETE,
491 Method::PATCH,
492 Method::OPTIONS,
493 ]);
494 }
495
496 if !config.allowed_headers.is_empty() {
498 let headers: Vec<_> = config
499 .allowed_headers
500 .iter()
501 .filter_map(|h| h.parse::<http::HeaderName>().ok())
502 .collect();
503 if !headers.is_empty() {
504 cors_layer = cors_layer.allow_headers(headers);
505 }
506 } else {
507 cors_layer =
509 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
510 }
511
512 let should_allow_credentials = if is_wildcard_origin {
516 false
518 } else {
519 config.allow_credentials
521 };
522
523 cors_layer = cors_layer.allow_credentials(should_allow_credentials);
524
525 info!(
526 "CORS middleware enabled with configured settings (credentials: {})",
527 should_allow_credentials
528 );
529 app.layer(cors_layer)
530 } else {
531 debug!("No CORS config provided, using permissive CORS for development");
535 app.layer(CorsLayer::permissive().allow_credentials(false))
538 }
539}
540
541#[allow(clippy::too_many_arguments)]
543pub async fn build_router_with_multi_tenant(
544 spec_path: Option<String>,
545 options: Option<ValidationOptions>,
546 failure_config: Option<FailureConfig>,
547 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
548 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
549 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
550 ai_generator: Option<
551 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
552 >,
553 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
554 mockai: Option<
555 std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
556 >,
557 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
558) -> Router {
559 use std::time::Instant;
560
561 let startup_start = Instant::now();
562
563 let mut app = Router::new();
565
566 let mut rate_limit_config = crate::middleware::RateLimitConfig {
569 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
570 .ok()
571 .and_then(|v| v.parse().ok())
572 .unwrap_or(1000),
573 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
574 .ok()
575 .and_then(|v| v.parse().ok())
576 .unwrap_or(2000),
577 per_ip: true,
578 per_endpoint: false,
579 };
580
581 let mut final_cors_config = cors_config;
583 let mut production_headers: Option<std::sync::Arc<std::collections::HashMap<String, String>>> =
584 None;
585 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
587
588 if let Some(deploy_config) = &deceptive_deploy_config {
589 if deploy_config.enabled {
590 info!("Deceptive deploy mode enabled - applying production-like configuration");
591
592 if let Some(prod_cors) = &deploy_config.cors {
594 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
595 enabled: true,
596 allowed_origins: prod_cors.allowed_origins.clone(),
597 allowed_methods: prod_cors.allowed_methods.clone(),
598 allowed_headers: prod_cors.allowed_headers.clone(),
599 allow_credentials: prod_cors.allow_credentials,
600 });
601 info!("Applied production-like CORS configuration");
602 }
603
604 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
606 rate_limit_config = crate::middleware::RateLimitConfig {
607 requests_per_minute: prod_rate_limit.requests_per_minute,
608 burst: prod_rate_limit.burst,
609 per_ip: prod_rate_limit.per_ip,
610 per_endpoint: false,
611 };
612 info!(
613 "Applied production-like rate limiting: {} req/min, burst: {}",
614 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
615 );
616 }
617
618 if !deploy_config.headers.is_empty() {
620 let headers_map: std::collections::HashMap<String, String> =
621 deploy_config.headers.clone();
622 production_headers = Some(std::sync::Arc::new(headers_map));
623 info!("Configured {} production headers", deploy_config.headers.len());
624 }
625
626 if let Some(prod_oauth) = &deploy_config.oauth {
628 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
629 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
630 oauth2: Some(oauth2_config),
631 ..Default::default()
632 });
633 info!("Applied production-like OAuth configuration for deceptive deploy");
634 }
635 }
636 }
637
638 let rate_limiter =
639 std::sync::Arc::new(crate::middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
640
641 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
642
643 if let Some(headers) = production_headers.clone() {
645 state = state.with_production_headers(headers);
646 }
647
648 let spec_path_for_mgmt = spec_path.clone();
650
651 if let Some(spec_path) = spec_path {
653 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
654
655 let spec_load_start = Instant::now();
657 match OpenApiSpec::from_file(&spec_path).await {
658 Ok(openapi) => {
659 let spec_load_duration = spec_load_start.elapsed();
660 info!(
661 "Successfully loaded OpenAPI spec from {} (took {:?})",
662 spec_path, spec_load_duration
663 );
664
665 tracing::debug!("Creating OpenAPI route registry...");
667 let registry_start = Instant::now();
668
669 let persona = load_persona_from_config().await;
671
672 let registry = if let Some(opts) = options {
673 tracing::debug!("Using custom validation options");
674 if let Some(ref persona) = persona {
675 tracing::info!("Using persona '{}' for route generation", persona.name);
676 }
677 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
678 } else {
679 tracing::debug!("Using environment-based options");
680 if let Some(ref persona) = persona {
681 tracing::info!("Using persona '{}' for route generation", persona.name);
682 }
683 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
684 };
685 let registry_duration = registry_start.elapsed();
686 info!(
687 "Created OpenAPI route registry with {} routes (took {:?})",
688 registry.routes().len(),
689 registry_duration
690 );
691
692 let extract_start = Instant::now();
694 let route_info: Vec<RouteInfo> = registry
695 .routes()
696 .iter()
697 .map(|route| RouteInfo {
698 method: route.method.clone(),
699 path: route.path.clone(),
700 operation_id: route.operation.operation_id.clone(),
701 summary: route.operation.summary.clone(),
702 description: route.operation.description.clone(),
703 parameters: route.parameters.clone(),
704 })
705 .collect();
706 state.routes = route_info;
707 let extract_duration = extract_start.elapsed();
708 debug!("Extracted route information (took {:?})", extract_duration);
709
710 let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
712 tracing::debug!("Loading overrides from environment variable");
713 let overrides_start = Instant::now();
714 match mockforge_core::Overrides::load_from_globs(&[]).await {
715 Ok(overrides) => {
716 let overrides_duration = overrides_start.elapsed();
717 info!(
718 "Loaded {} override rules (took {:?})",
719 overrides.rules().len(),
720 overrides_duration
721 );
722 Some(overrides)
723 }
724 Err(e) => {
725 tracing::warn!("Failed to load overrides: {}", e);
726 None
727 }
728 }
729 } else {
730 None
731 };
732
733 let router_build_start = Instant::now();
735 let overrides_enabled = overrides.is_some();
736 let openapi_router = if let Some(mockai_instance) = &mockai {
737 tracing::debug!("Building router with MockAI support");
738 registry.build_router_with_mockai(Some(mockai_instance.clone()))
739 } else if let Some(ai_generator) = &ai_generator {
740 tracing::debug!("Building router with AI generator support");
741 registry.build_router_with_ai(Some(ai_generator.clone()))
742 } else if let Some(failure_config) = &failure_config {
743 tracing::debug!("Building router with failure injection and overrides");
744 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
745 registry.build_router_with_injectors_and_overrides(
746 LatencyInjector::default(),
747 Some(failure_injector),
748 overrides,
749 overrides_enabled,
750 )
751 } else {
752 tracing::debug!("Building router with overrides");
753 registry.build_router_with_injectors_and_overrides(
754 LatencyInjector::default(),
755 None,
756 overrides,
757 overrides_enabled,
758 )
759 };
760 let router_build_duration = router_build_start.elapsed();
761 debug!("Built OpenAPI router (took {:?})", router_build_duration);
762
763 tracing::debug!("Merging OpenAPI router with main router");
764 app = app.merge(openapi_router);
765 tracing::debug!("Router built successfully");
766 }
767 Err(e) => {
768 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
769 }
770 }
771 }
772
773 app = app.route(
775 "/health",
776 axum::routing::get(|| async {
777 use mockforge_core::server_utils::health::HealthStatus;
778 {
779 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
781 Ok(value) => axum::Json(value),
782 Err(e) => {
783 tracing::error!("Failed to serialize health status: {}", e);
785 axum::Json(serde_json::json!({
786 "status": "healthy",
787 "service": "mockforge-http",
788 "uptime_seconds": 0
789 }))
790 }
791 }
792 }
793 }),
794 )
795 .merge(sse::sse_router())
797 .merge(file_server::file_serving_router());
799
800 let state_for_routes = state.clone();
802
803 let routes_router = Router::new()
805 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
806 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
807 .with_state(state_for_routes);
808
809 app = app.merge(routes_router);
811
812 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
815 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
816
817 if std::path::Path::new(&coverage_html_path).exists() {
819 app = app.nest_service(
820 "/__mockforge/coverage.html",
821 tower_http::services::ServeFile::new(&coverage_html_path),
822 );
823 debug!("Serving coverage UI from: {}", coverage_html_path);
824 } else {
825 debug!(
826 "Coverage UI file not found at: {}. Skipping static file serving.",
827 coverage_html_path
828 );
829 }
830
831 let mut management_state = ManagementState::new(None, spec_path_for_mgmt, 3000); use std::sync::Arc;
836 let ws_state = WsManagementState::new();
837 let ws_broadcast = Arc::new(ws_state.tx.clone());
838 let management_state = management_state.with_ws_broadcast(ws_broadcast);
839
840 #[cfg(feature = "smtp")]
844 let management_state = {
845 if let Some(smtp_reg) = smtp_registry {
846 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
847 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
848 Err(e) => {
849 error!(
850 "Invalid SMTP registry type passed to HTTP management state: {:?}",
851 e.type_id()
852 );
853 management_state
854 }
855 }
856 } else {
857 management_state
858 }
859 };
860 #[cfg(not(feature = "smtp"))]
861 let management_state = management_state;
862 #[cfg(not(feature = "smtp"))]
863 let _ = smtp_registry;
864 app = app.nest("/__mockforge/api", management_router(management_state));
865
866 app = app.merge(verification_router());
868
869 use crate::auth::oidc::oidc_router;
871 app = app.merge(oidc_router());
872
873 {
875 use mockforge_core::security::get_global_access_review_service;
876 if let Some(service) = get_global_access_review_service().await {
877 use crate::handlers::access_review::{access_review_router, AccessReviewState};
878 let review_state = AccessReviewState { service };
879 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
880 debug!("Access review API mounted at /api/v1/security/access-reviews");
881 }
882 }
883
884 {
886 use mockforge_core::security::get_global_privileged_access_manager;
887 if let Some(manager) = get_global_privileged_access_manager().await {
888 use crate::handlers::privileged_access::{privileged_access_router, PrivilegedAccessState};
889 let privileged_state = PrivilegedAccessState { manager };
890 app = app.nest("/api/v1/security/privileged-access", privileged_access_router(privileged_state));
891 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
892 }
893 }
894
895 {
897 use mockforge_core::security::get_global_change_management_engine;
898 if let Some(engine) = get_global_change_management_engine().await {
899 use crate::handlers::change_management::{change_management_router, ChangeManagementState};
900 let change_state = ChangeManagementState { engine };
901 app = app.nest("/api/v1/change-management", change_management_router(change_state));
902 debug!("Change management API mounted at /api/v1/change-management");
903 }
904 }
905
906 {
908 use mockforge_core::security::get_global_risk_assessment_engine;
909 if let Some(engine) = get_global_risk_assessment_engine().await {
910 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
911 let risk_state = RiskAssessmentState { engine };
912 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
913 debug!("Risk assessment API mounted at /api/v1/security/risks");
914 }
915 }
916
917 {
919 use crate::auth::token_lifecycle::TokenLifecycleManager;
920 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
921 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
922 let lifecycle_state = TokenLifecycleState {
923 manager: lifecycle_manager,
924 };
925 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
926 debug!("Token lifecycle API mounted at /api/v1/auth");
927 }
928
929 {
931 use crate::auth::oidc::OidcState;
932 use crate::auth::token_lifecycle::TokenLifecycleManager;
933 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
934 let oidc_state = Arc::new(RwLock::new(None::<OidcState>)); let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
936 let oauth2_state = OAuth2ServerState {
937 oidc_state,
938 lifecycle_manager,
939 auth_codes: Arc::new(RwLock::new(HashMap::new())),
940 };
941 app = app.merge(oauth2_server_router(oauth2_state));
942 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
943 }
944
945 {
947 use crate::auth::risk_engine::RiskEngine;
948 use crate::auth::token_lifecycle::TokenLifecycleManager;
949 use crate::handlers::consent::{consent_router, ConsentState};
950 use crate::handlers::oauth2_server::OAuth2ServerState;
951 use crate::auth::oidc::OidcState;
952 let oidc_state = Arc::new(RwLock::new(None::<OidcState>)); let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
954 let oauth2_state = OAuth2ServerState {
955 oidc_state: oidc_state.clone(),
956 lifecycle_manager: lifecycle_manager.clone(),
957 auth_codes: Arc::new(RwLock::new(HashMap::new())),
958 };
959 let risk_engine = Arc::new(RiskEngine::default());
960 let consent_state = ConsentState {
961 oauth2_state,
962 risk_engine,
963 };
964 app = app.merge(consent_router(consent_state));
965 debug!("Consent screen endpoints mounted at /consent");
966 }
967
968 {
970 use crate::auth::risk_engine::RiskEngine;
971 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
972 let risk_engine = Arc::new(RiskEngine::default());
973 let risk_state = RiskSimulationState { risk_engine };
974 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
975 debug!("Risk simulation API mounted at /api/v1/auth/risk");
976 }
977
978 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
980
981 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
983
984 app = app.layer(axum::middleware::from_fn(crate::middleware::security_middleware));
986
987 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
990
991 app = app.layer(from_fn_with_state(state.clone(), crate::middleware::rate_limit_middleware));
993
994 if state.production_headers.is_some() {
996 app = app.layer(from_fn_with_state(
997 state.clone(),
998 crate::middleware::production_headers_middleware,
999 ));
1000 }
1001
1002 if let Some(auth_config) = deceptive_deploy_auth_config {
1004 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1005 use std::collections::HashMap;
1006 use std::sync::Arc;
1007 use tokio::sync::RwLock;
1008
1009 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1011 match create_oauth2_client(oauth2_config) {
1012 Ok(client) => Some(client),
1013 Err(e) => {
1014 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1015 None
1016 }
1017 }
1018 } else {
1019 None
1020 };
1021
1022 let auth_state = AuthState {
1024 config: auth_config,
1025 spec: None, oauth2_client,
1027 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1028 };
1029
1030 app = app.layer(axum::middleware::from_fn_with_state(auth_state, auth_middleware));
1032 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1033 }
1034
1035 app = apply_cors_middleware(app, final_cors_config);
1037
1038 if let Some(mt_config) = multi_tenant_config {
1040 if mt_config.enabled {
1041 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1042 use std::sync::Arc;
1043
1044 info!(
1045 "Multi-tenant mode enabled with {} routing strategy",
1046 match mt_config.routing_strategy {
1047 mockforge_core::RoutingStrategy::Path => "path-based",
1048 mockforge_core::RoutingStrategy::Port => "port-based",
1049 mockforge_core::RoutingStrategy::Both => "hybrid",
1050 }
1051 );
1052
1053 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1055
1056 let default_workspace =
1058 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1059 if let Err(e) =
1060 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1061 {
1062 warn!("Failed to register default workspace: {}", e);
1063 } else {
1064 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1065 }
1066
1067 if mt_config.auto_discover {
1069 if let Some(config_dir) = &mt_config.config_directory {
1070 let config_path = Path::new(config_dir);
1071 if config_path.exists() && config_path.is_dir() {
1072 match fs::read_dir(config_path).await {
1073 Ok(mut entries) => {
1074 while let Ok(Some(entry)) = entries.next_entry().await {
1075 let path = entry.path();
1076 if path.extension() == Some(OsStr::new("yaml")) {
1077 match fs::read_to_string(&path).await {
1078 Ok(content) => {
1079 match serde_yaml::from_str::<
1080 mockforge_core::Workspace,
1081 >(
1082 &content
1083 ) {
1084 Ok(workspace) => {
1085 if let Err(e) = registry.register_workspace(
1086 workspace.id.clone(),
1087 workspace,
1088 ) {
1089 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1090 } else {
1091 info!("Auto-registered workspace from {:?}", path);
1092 }
1093 }
1094 Err(e) => {
1095 warn!("Failed to parse workspace from {:?}: {}", path, e);
1096 }
1097 }
1098 }
1099 Err(e) => {
1100 warn!(
1101 "Failed to read workspace file {:?}: {}",
1102 path, e
1103 );
1104 }
1105 }
1106 }
1107 }
1108 }
1109 Err(e) => {
1110 warn!("Failed to read config directory {:?}: {}", config_path, e);
1111 }
1112 }
1113 } else {
1114 warn!(
1115 "Config directory {:?} does not exist or is not a directory",
1116 config_path
1117 );
1118 }
1119 }
1120 }
1121
1122 let registry = Arc::new(registry);
1124
1125 let _workspace_router = WorkspaceRouter::new(registry);
1127
1128 info!("Workspace routing middleware initialized for HTTP server");
1131 }
1132 }
1133
1134 let total_startup_duration = startup_start.elapsed();
1135 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1136
1137 app
1138}
1139
1140pub async fn build_router_with_auth_and_latency(
1142 _spec_path: Option<String>,
1143 _options: Option<()>,
1144 _auth_config: Option<mockforge_core::config::AuthConfig>,
1145 _latency_injector: Option<LatencyInjector>,
1146) -> Router {
1147 build_router(None, None, None).await
1149}
1150
1151pub async fn build_router_with_latency(
1153 _spec_path: Option<String>,
1154 _options: Option<ValidationOptions>,
1155 _latency_injector: Option<LatencyInjector>,
1156) -> Router {
1157 build_router(None, None, None).await
1159}
1160
1161pub async fn build_router_with_auth(
1163 spec_path: Option<String>,
1164 options: Option<ValidationOptions>,
1165 auth_config: Option<mockforge_core::config::AuthConfig>,
1166) -> Router {
1167 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1168 use std::sync::Arc;
1169
1170 #[cfg(feature = "data-faker")]
1172 {
1173 register_core_faker_provider();
1174 }
1175
1176 let spec = if let Some(spec_path) = &spec_path {
1178 match mockforge_core::openapi::OpenApiSpec::from_file(&spec_path).await {
1179 Ok(spec) => Some(Arc::new(spec)),
1180 Err(e) => {
1181 warn!("Failed to load OpenAPI spec for auth: {}", e);
1182 None
1183 }
1184 }
1185 } else {
1186 None
1187 };
1188
1189 let oauth2_client = if let Some(auth_config) = &auth_config {
1191 if let Some(oauth2_config) = &auth_config.oauth2 {
1192 match create_oauth2_client(oauth2_config) {
1193 Ok(client) => Some(client),
1194 Err(e) => {
1195 warn!("Failed to create OAuth2 client: {}", e);
1196 None
1197 }
1198 }
1199 } else {
1200 None
1201 }
1202 } else {
1203 None
1204 };
1205
1206 let auth_state = AuthState {
1207 config: auth_config.unwrap_or_default(),
1208 spec,
1209 oauth2_client,
1210 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1211 };
1212
1213 let mut app = Router::new().with_state(auth_state.clone());
1215
1216 if let Some(spec_path) = spec_path {
1218 match OpenApiSpec::from_file(&spec_path).await {
1219 Ok(openapi) => {
1220 info!("Loaded OpenAPI spec from {}", spec_path);
1221 let registry = if let Some(opts) = options {
1222 OpenApiRouteRegistry::new_with_options(openapi, opts)
1223 } else {
1224 OpenApiRouteRegistry::new_with_env(openapi)
1225 };
1226
1227 app = registry.build_router();
1228 }
1229 Err(e) => {
1230 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1231 }
1232 }
1233 }
1234
1235 app = app.route(
1237 "/health",
1238 axum::routing::get(|| async {
1239 use mockforge_core::server_utils::health::HealthStatus;
1240 {
1241 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1243 Ok(value) => axum::Json(value),
1244 Err(e) => {
1245 tracing::error!("Failed to serialize health status: {}", e);
1247 axum::Json(serde_json::json!({
1248 "status": "healthy",
1249 "service": "mockforge-http",
1250 "uptime_seconds": 0
1251 }))
1252 }
1253 }
1254 }
1255 }),
1256 )
1257 .merge(sse::sse_router())
1259 .merge(file_server::file_serving_router())
1261 .layer(axum::middleware::from_fn_with_state(auth_state.clone(), auth_middleware))
1263 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1265
1266 app
1267}
1268
1269pub async fn serve_router(
1271 port: u16,
1272 app: Router,
1273) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1274 serve_router_with_tls(port, app, None).await
1275}
1276
1277pub async fn serve_router_with_tls(
1279 port: u16,
1280 app: Router,
1281 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1282) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1283 use std::net::SocketAddr;
1284
1285 let addr = mockforge_core::wildcard_socket_addr(port);
1286
1287 if let Some(ref tls) = tls_config {
1288 if tls.enabled {
1289 info!("HTTPS listening on {}", addr);
1290 return serve_with_tls(addr, app, tls).await;
1291 }
1292 }
1293
1294 info!("HTTP listening on {}", addr);
1295
1296 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1297 format!(
1298 "Failed to bind HTTP server to port {}: {}\n\
1299 Hint: The port may already be in use. Try using a different port with --http-port or check if another process is using this port with: lsof -i :{} or netstat -tulpn | grep {}",
1300 port, e, port, port
1301 )
1302 })?;
1303
1304 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
1305 Ok(())
1306}
1307
1308async fn serve_with_tls(
1314 addr: std::net::SocketAddr,
1315 _app: Router,
1316 tls_config: &mockforge_core::config::HttpTlsConfig,
1317) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1318 let _acceptor = tls::load_tls_acceptor(tls_config)?;
1320
1321 Err(format!(
1324 "TLS/HTTPS support is configured but requires a reverse proxy (nginx) for production use.\n\
1325 Certificate validation passed: {} and {}\n\
1326 For native TLS support, please use a reverse proxy or wait for axum-server integration.\n\
1327 You can configure nginx with TLS termination pointing to the HTTP server on port {}.",
1328 tls_config.cert_file,
1329 tls_config.key_file,
1330 addr.port()
1331 )
1332 .into())
1333}
1334
1335pub async fn start(
1337 port: u16,
1338 spec_path: Option<String>,
1339 options: Option<ValidationOptions>,
1340) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1341 start_with_latency(port, spec_path, options, None).await
1342}
1343
1344pub async fn start_with_auth_and_latency(
1346 port: u16,
1347 spec_path: Option<String>,
1348 options: Option<ValidationOptions>,
1349 auth_config: Option<mockforge_core::config::AuthConfig>,
1350 latency_profile: Option<LatencyProfile>,
1351) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1352 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1353 .await
1354}
1355
1356pub async fn start_with_auth_and_injectors(
1358 port: u16,
1359 spec_path: Option<String>,
1360 options: Option<ValidationOptions>,
1361 auth_config: Option<mockforge_core::config::AuthConfig>,
1362 _latency_profile: Option<LatencyProfile>,
1363 _failure_injector: Option<mockforge_core::FailureInjector>,
1364) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1365 let app = build_router_with_auth(spec_path, options, auth_config).await;
1367 serve_router(port, app).await
1368}
1369
1370pub async fn start_with_latency(
1372 port: u16,
1373 spec_path: Option<String>,
1374 options: Option<ValidationOptions>,
1375 latency_profile: Option<LatencyProfile>,
1376) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1377 let latency_injector =
1378 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1379
1380 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1381 serve_router(port, app).await
1382}
1383
1384pub async fn build_router_with_chains(
1386 spec_path: Option<String>,
1387 options: Option<ValidationOptions>,
1388 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1389) -> Router {
1390 build_router_with_chains_and_multi_tenant(
1391 spec_path,
1392 options,
1393 circling_config,
1394 None,
1395 None,
1396 None,
1397 None,
1398 None,
1399 None,
1400 None,
1401 false,
1402 None, None, None, None, )
1407 .await
1408}
1409
1410#[allow(clippy::too_many_arguments)]
1412pub async fn build_router_with_chains_and_multi_tenant(
1413 spec_path: Option<String>,
1414 options: Option<ValidationOptions>,
1415 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1416 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
1417 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1418 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1419 _ai_generator: Option<
1420 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
1421 >,
1422 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
1423 mqtt_broker: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
1424 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1425 traffic_shaping_enabled: bool,
1426 health_manager: Option<std::sync::Arc<health::HealthManager>>,
1427 _mockai: Option<
1428 std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
1429 >,
1430 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1431 proxy_config: Option<mockforge_core::proxy::config::ProxyConfig>,
1432) -> Router {
1433 use crate::latency_profiles::LatencyProfiles;
1434 use crate::op_middleware::Shared;
1435 use mockforge_core::Overrides;
1436
1437 let template_expand = options.as_ref()
1439 .map(|o| o.response_template_expand)
1440 .unwrap_or_else(|| {
1441 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1442 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1443 .unwrap_or(false)
1444 });
1445
1446 let _shared = Shared {
1447 profiles: LatencyProfiles::default(),
1448 overrides: Overrides::default(),
1449 failure_injector: None,
1450 traffic_shaper,
1451 overrides_enabled: false,
1452 traffic_shaping_enabled,
1453 };
1454
1455 let mut app = Router::new();
1457 let mut include_default_health = true;
1458
1459 if let Some(ref spec) = spec_path {
1461 match OpenApiSpec::from_file(&spec).await {
1462 Ok(openapi) => {
1463 info!("Loaded OpenAPI spec from {}", spec);
1464
1465 let persona = load_persona_from_config().await;
1467
1468 let mut registry = if let Some(opts) = options {
1469 tracing::debug!("Using custom validation options");
1470 if let Some(ref persona) = persona {
1471 tracing::info!("Using persona '{}' for route generation", persona.name);
1472 }
1473 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1474 } else {
1475 tracing::debug!("Using environment-based options");
1476 if let Some(ref persona) = persona {
1477 tracing::info!("Using persona '{}' for route generation", persona.name);
1478 }
1479 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1480 };
1481
1482 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1484 .unwrap_or_else(|_| "/app/fixtures".to_string());
1485 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1486 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1487 .unwrap_or(true); if custom_fixtures_enabled {
1490 use mockforge_core::CustomFixtureLoader;
1491 use std::path::PathBuf;
1492 use std::sync::Arc;
1493
1494 let fixtures_path = PathBuf::from(&fixtures_dir);
1495 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1496
1497 if let Err(e) = custom_loader.load_fixtures().await {
1498 tracing::warn!("Failed to load custom fixtures: {}", e);
1499 } else {
1500 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1501 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1502 }
1503 }
1504
1505 if registry
1506 .routes()
1507 .iter()
1508 .any(|route| route.method == "GET" && route.path == "/health")
1509 {
1510 include_default_health = false;
1511 }
1512 let spec_router = if let Some(ref mockai_instance) = _mockai {
1514 tracing::debug!("Building router with MockAI support");
1515 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1516 } else {
1517 registry.build_router()
1518 };
1519 app = app.merge(spec_router);
1520 }
1521 Err(e) => {
1522 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1523 }
1524 }
1525 }
1526
1527 fn expand_templates_in_json(value: &serde_json::Value, context: &mockforge_core::ai_response::RequestContext) -> serde_json::Value {
1529 use mockforge_core::ai_response::expand_prompt_template;
1530 use serde_json::Value;
1531
1532 match value {
1533 Value::String(s) => {
1534 let normalized = s
1536 .replace("{{request.query.", "{{query.")
1537 .replace("{{request.path.", "{{path.")
1538 .replace("{{request.headers.", "{{headers.")
1539 .replace("{{request.body.", "{{body.")
1540 .replace("{{request.method}}", "{{method}}")
1541 .replace("{{request.path}}", "{{path}}");
1542
1543 let (template_part, default_value) = if normalized.contains("||") {
1546 if let Some(open_idx) = normalized.find("{{") {
1550 if let Some(close_idx) = normalized[open_idx..].find("}}") {
1551 let template_block = &normalized[open_idx..open_idx+close_idx+2];
1552 if let Some(pipe_idx) = template_block.find("||") {
1553 let before_pipe = &template_block[..pipe_idx].trim();
1555 let after_pipe = &template_block[pipe_idx+2..].trim();
1556
1557 let template_var = before_pipe.trim_start_matches("{{").trim();
1559 let replacement = format!("{{{{{}}}}}}}", template_var);
1561 let template = normalized.replace(template_block, &replacement);
1562
1563 let mut default = after_pipe.trim_end_matches("}}").trim().to_string();
1565 default = default.trim_matches('"').trim_matches('\'').trim_matches('\\').to_string();
1567 default = default.trim().to_string();
1568
1569 (template, Some(default))
1570 } else {
1571 (normalized, None)
1572 }
1573 } else {
1574 (normalized, None)
1575 }
1576 } else {
1577 (normalized, None)
1578 }
1579 } else {
1580 (normalized, None)
1581 };
1582
1583 let mut expanded = expand_prompt_template(&template_part, context);
1585
1586 let final_expanded = if (expanded.contains("{{query.") || expanded.contains("{{path.") || expanded.contains("{{headers."))
1589 && default_value.is_some() {
1590 default_value.unwrap()
1591 } else {
1592 while expanded.ends_with('}') && !expanded.ends_with("}}") {
1595 expanded.pop();
1596 }
1597 expanded
1598 };
1599
1600 Value::String(final_expanded)
1601 }
1602 Value::Array(arr) => {
1603 Value::Array(arr.iter().map(|v| expand_templates_in_json(v, context)).collect())
1604 }
1605 Value::Object(obj) => {
1606 let mut new_obj = serde_json::Map::new();
1607 for (k, v) in obj {
1608 new_obj.insert(k.clone(), expand_templates_in_json(v, context));
1609 }
1610 Value::Object(new_obj)
1611 }
1612 _ => value.clone(),
1613 }
1614 }
1615
1616 if let Some(route_configs) = route_configs {
1618 use axum::http::StatusCode;
1619 use axum::response::IntoResponse;
1620
1621 if !route_configs.is_empty() {
1622 info!("Registering {} custom route(s) from config", route_configs.len());
1623 }
1624
1625 for route_config in route_configs {
1626 let status = route_config.response.status;
1627 let body = route_config.response.body.clone();
1628 let headers = route_config.response.headers.clone();
1629 let path = route_config.path.clone();
1630 let method = route_config.method.clone();
1631 let latency_config = route_config.latency.clone();
1632
1633 let expected_method = method.to_uppercase();
1638 app = app.route(&path, axum::routing::any(move |req: axum::http::Request<axum::body::Body>| {
1639 let body = body.clone();
1640 let headers = headers.clone();
1641 let expand = template_expand;
1642 let latency = latency_config.clone();
1643 let expected = expected_method.clone();
1644 let status_code = status;
1645
1646 async move {
1647 if req.method().as_str() != expected.as_str() {
1649 return axum::response::Response::builder()
1651 .status(axum::http::StatusCode::METHOD_NOT_ALLOWED)
1652 .header("Allow", &expected)
1653 .body(axum::body::Body::empty())
1654 .unwrap()
1655 .into_response();
1656 }
1657
1658 let delay_ms = if let Some(ref lat) = latency {
1661 if lat.enabled {
1662 use rand::{rng, Rng};
1663
1664 let mut rng = rng();
1666 let roll: f64 = rng.random();
1667
1668 if roll < lat.probability {
1669 if let Some(fixed) = lat.fixed_delay_ms {
1670 let jitter = (fixed as f64 * lat.jitter_percent / 100.0) as u64;
1672 let jitter_amount = if jitter > 0 {
1673 rng.random_range(0..=jitter)
1674 } else {
1675 0
1676 };
1677 Some(fixed + jitter_amount)
1678 } else if let Some((min, max)) = lat.random_delay_range_ms {
1679 Some(rng.random_range(min..=max))
1681 } else {
1682 Some(0)
1684 }
1685 } else {
1686 None
1687 }
1688 } else {
1689 None
1690 }
1691 } else {
1692 None
1693 };
1694
1695 if let Some(delay) = delay_ms {
1697 if delay > 0 {
1698 use tokio::time::{sleep, Duration};
1699 sleep(Duration::from_millis(delay)).await;
1700 }
1701 }
1702
1703 let mut body_value = body.unwrap_or(serde_json::json!({}));
1705
1706 if expand {
1708 use mockforge_core::ai_response::RequestContext;
1709 use std::collections::HashMap;
1710 use serde_json::Value;
1711
1712 let method = req.method().to_string();
1714 let path = req.uri().path().to_string();
1715
1716 let query_params: HashMap<String, Value> = req
1718 .uri()
1719 .query()
1720 .map(|q| {
1721 url::form_urlencoded::parse(q.as_bytes())
1722 .into_owned()
1723 .map(|(k, v)| (k, Value::String(v)))
1724 .collect()
1725 })
1726 .unwrap_or_default();
1727
1728 let request_headers: HashMap<String, Value> = req
1730 .headers()
1731 .iter()
1732 .filter_map(|(name, value)| {
1733 value.to_str().ok().map(|v| {
1734 (name.to_string(), Value::String(v.to_string()))
1735 })
1736 })
1737 .collect();
1738
1739 let context = RequestContext::new(method.clone(), path.clone())
1744 .with_query_params(query_params)
1745 .with_headers(request_headers);
1746
1747 body_value = expand_templates_in_json(&body_value, &context);
1749 }
1750
1751 let mut response = axum::Json(body_value).into_response();
1752
1753 *response.status_mut() = StatusCode::from_u16(status_code)
1755 .unwrap_or(StatusCode::OK);
1756
1757 for (key, value) in headers {
1759 if let Ok(header_name) = axum::http::HeaderName::from_bytes(key.as_bytes()) {
1760 if let Ok(header_value) = axum::http::HeaderValue::from_str(&value) {
1761 response.headers_mut().insert(header_name, header_value);
1762 }
1763 }
1764 }
1765
1766 response
1767 }
1768 }));
1769
1770 debug!("Registered route: {} {}", method, path);
1771 }
1772 }
1773
1774 if let Some(health) = health_manager {
1776 app = app.merge(health::health_router(health));
1778 info!(
1779 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
1780 );
1781 } else if include_default_health {
1782 app = app.route(
1784 "/health",
1785 axum::routing::get(|| async {
1786 use mockforge_core::server_utils::health::HealthStatus;
1787 {
1788 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1790 Ok(value) => axum::Json(value),
1791 Err(e) => {
1792 tracing::error!("Failed to serialize health status: {}", e);
1794 axum::Json(serde_json::json!({
1795 "status": "healthy",
1796 "service": "mockforge-http",
1797 "uptime_seconds": 0
1798 }))
1799 }
1800 }
1801 }
1802 }),
1803 );
1804 }
1805
1806 app = app.merge(sse::sse_router());
1807 app = app.merge(file_server::file_serving_router());
1809
1810 let mut management_state = ManagementState::new(None, spec_path, 3000); use std::sync::Arc;
1815 let ws_state = WsManagementState::new();
1816 let ws_broadcast = Arc::new(ws_state.tx.clone());
1817 let management_state = management_state.with_ws_broadcast(ws_broadcast);
1818
1819 let management_state = if let Some(proxy_cfg) = proxy_config {
1821 use tokio::sync::RwLock;
1822 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
1823 management_state.with_proxy_config(proxy_config_arc)
1824 } else {
1825 management_state
1826 };
1827
1828 #[cfg(feature = "smtp")]
1829 let management_state = {
1830 if let Some(smtp_reg) = smtp_registry {
1831 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
1832 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
1833 Err(e) => {
1834 error!(
1835 "Invalid SMTP registry type passed to HTTP management state: {:?}",
1836 e.type_id()
1837 );
1838 management_state
1839 }
1840 }
1841 } else {
1842 management_state
1843 }
1844 };
1845 #[cfg(not(feature = "smtp"))]
1846 let management_state = {
1847 let _ = smtp_registry;
1848 management_state
1849 };
1850 #[cfg(feature = "mqtt")]
1851 let management_state = {
1852 if let Some(broker) = mqtt_broker {
1853 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
1854 Ok(broker) => management_state.with_mqtt_broker(broker),
1855 Err(e) => {
1856 error!(
1857 "Invalid MQTT broker passed to HTTP management state: {:?}",
1858 e.type_id()
1859 );
1860 management_state
1861 }
1862 }
1863 } else {
1864 management_state
1865 }
1866 };
1867 #[cfg(not(feature = "mqtt"))]
1868 let management_state = {
1869 let _ = mqtt_broker;
1870 management_state
1871 };
1872 app = app.nest("/__mockforge/api", management_router(management_state));
1873
1874 app = app.merge(verification_router());
1876
1877 use crate::auth::oidc::oidc_router;
1879 app = app.merge(oidc_router());
1880
1881 {
1883 use mockforge_core::security::get_global_access_review_service;
1884 if let Some(service) = get_global_access_review_service().await {
1885 use crate::handlers::access_review::{access_review_router, AccessReviewState};
1886 let review_state = AccessReviewState { service };
1887 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
1888 debug!("Access review API mounted at /api/v1/security/access-reviews");
1889 }
1890 }
1891
1892 {
1894 use mockforge_core::security::get_global_privileged_access_manager;
1895 if let Some(manager) = get_global_privileged_access_manager().await {
1896 use crate::handlers::privileged_access::{privileged_access_router, PrivilegedAccessState};
1897 let privileged_state = PrivilegedAccessState { manager };
1898 app = app.nest("/api/v1/security/privileged-access", privileged_access_router(privileged_state));
1899 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
1900 }
1901 }
1902
1903 {
1905 use mockforge_core::security::get_global_change_management_engine;
1906 if let Some(engine) = get_global_change_management_engine().await {
1907 use crate::handlers::change_management::{change_management_router, ChangeManagementState};
1908 let change_state = ChangeManagementState { engine };
1909 app = app.nest("/api/v1/change-management", change_management_router(change_state));
1910 debug!("Change management API mounted at /api/v1/change-management");
1911 }
1912 }
1913
1914 {
1916 use mockforge_core::security::get_global_risk_assessment_engine;
1917 if let Some(engine) = get_global_risk_assessment_engine().await {
1918 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
1919 let risk_state = RiskAssessmentState { engine };
1920 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
1921 debug!("Risk assessment API mounted at /api/v1/security/risks");
1922 }
1923 }
1924
1925 {
1927 use crate::auth::token_lifecycle::TokenLifecycleManager;
1928 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
1929 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1930 let lifecycle_state = TokenLifecycleState {
1931 manager: lifecycle_manager,
1932 };
1933 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
1934 debug!("Token lifecycle API mounted at /api/v1/auth");
1935 }
1936
1937 {
1939 use crate::auth::oidc::OidcState;
1940 use crate::auth::token_lifecycle::TokenLifecycleManager;
1941 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
1942 let oidc_state = Arc::new(RwLock::new(None::<OidcState>)); let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1944 let oauth2_state = OAuth2ServerState {
1945 oidc_state,
1946 lifecycle_manager,
1947 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1948 };
1949 app = app.merge(oauth2_server_router(oauth2_state));
1950 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
1951 }
1952
1953 {
1955 use crate::auth::risk_engine::RiskEngine;
1956 use crate::auth::token_lifecycle::TokenLifecycleManager;
1957 use crate::handlers::consent::{consent_router, ConsentState};
1958 use crate::handlers::oauth2_server::OAuth2ServerState;
1959 use crate::auth::oidc::OidcState;
1960 let oidc_state = Arc::new(RwLock::new(None::<OidcState>)); let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1962 let oauth2_state = OAuth2ServerState {
1963 oidc_state: oidc_state.clone(),
1964 lifecycle_manager: lifecycle_manager.clone(),
1965 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1966 };
1967 let risk_engine = Arc::new(RiskEngine::default());
1968 let consent_state = ConsentState {
1969 oauth2_state,
1970 risk_engine,
1971 };
1972 app = app.merge(consent_router(consent_state));
1973 debug!("Consent screen endpoints mounted at /consent");
1974 }
1975
1976 {
1978 use crate::auth::risk_engine::RiskEngine;
1979 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
1980 let risk_engine = Arc::new(RiskEngine::default());
1981 let risk_state = RiskSimulationState { risk_engine };
1982 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
1983 debug!("Risk simulation API mounted at /api/v1/auth/risk");
1984 }
1985
1986 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1988
1989 if let Some(mt_config) = multi_tenant_config {
1991 if mt_config.enabled {
1992 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1993 use std::sync::Arc;
1994
1995 info!(
1996 "Multi-tenant mode enabled with {} routing strategy",
1997 match mt_config.routing_strategy {
1998 mockforge_core::RoutingStrategy::Path => "path-based",
1999 mockforge_core::RoutingStrategy::Port => "port-based",
2000 mockforge_core::RoutingStrategy::Both => "hybrid",
2001 }
2002 );
2003
2004 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
2006
2007 let default_workspace =
2009 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
2010 if let Err(e) =
2011 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
2012 {
2013 warn!("Failed to register default workspace: {}", e);
2014 } else {
2015 info!("Registered default workspace: '{}'", mt_config.default_workspace);
2016 }
2017
2018 let registry = Arc::new(registry);
2020
2021 let _workspace_router = WorkspaceRouter::new(registry);
2023 info!("Workspace routing middleware initialized for HTTP server");
2024 }
2025 }
2026
2027 let mut final_cors_config = cors_config;
2029 let mut production_headers: Option<std::sync::Arc<std::collections::HashMap<String, String>>> =
2030 None;
2031 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
2033 let mut rate_limit_config = crate::middleware::RateLimitConfig {
2034 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
2035 .ok()
2036 .and_then(|v| v.parse().ok())
2037 .unwrap_or(1000),
2038 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
2039 .ok()
2040 .and_then(|v| v.parse().ok())
2041 .unwrap_or(2000),
2042 per_ip: true,
2043 per_endpoint: false,
2044 };
2045
2046 if let Some(deploy_config) = &deceptive_deploy_config {
2047 if deploy_config.enabled {
2048 info!("Deceptive deploy mode enabled - applying production-like configuration");
2049
2050 if let Some(prod_cors) = &deploy_config.cors {
2052 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
2053 enabled: true,
2054 allowed_origins: prod_cors.allowed_origins.clone(),
2055 allowed_methods: prod_cors.allowed_methods.clone(),
2056 allowed_headers: prod_cors.allowed_headers.clone(),
2057 allow_credentials: prod_cors.allow_credentials,
2058 });
2059 info!("Applied production-like CORS configuration");
2060 }
2061
2062 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
2064 rate_limit_config = crate::middleware::RateLimitConfig {
2065 requests_per_minute: prod_rate_limit.requests_per_minute,
2066 burst: prod_rate_limit.burst,
2067 per_ip: prod_rate_limit.per_ip,
2068 per_endpoint: false,
2069 };
2070 info!(
2071 "Applied production-like rate limiting: {} req/min, burst: {}",
2072 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
2073 );
2074 }
2075
2076 if !deploy_config.headers.is_empty() {
2078 let headers_map: std::collections::HashMap<String, String> =
2079 deploy_config.headers.clone();
2080 production_headers = Some(std::sync::Arc::new(headers_map));
2081 info!("Configured {} production headers", deploy_config.headers.len());
2082 }
2083
2084 if let Some(prod_oauth) = &deploy_config.oauth {
2086 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
2087 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
2088 oauth2: Some(oauth2_config),
2089 ..Default::default()
2090 });
2091 info!("Applied production-like OAuth configuration for deceptive deploy");
2092 }
2093 }
2094 }
2095
2096 let rate_limiter =
2098 std::sync::Arc::new(crate::middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
2099
2100 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
2101
2102 if let Some(headers) = production_headers.clone() {
2104 state = state.with_production_headers(headers);
2105 }
2106
2107 app = app.layer(from_fn_with_state(state.clone(), crate::middleware::rate_limit_middleware));
2109
2110 if state.production_headers.is_some() {
2112 app = app.layer(from_fn_with_state(
2113 state.clone(),
2114 crate::middleware::production_headers_middleware,
2115 ));
2116 }
2117
2118 if let Some(auth_config) = deceptive_deploy_auth_config {
2120 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
2121 use std::collections::HashMap;
2122 use std::sync::Arc;
2123 use tokio::sync::RwLock;
2124
2125 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
2127 match create_oauth2_client(oauth2_config) {
2128 Ok(client) => Some(client),
2129 Err(e) => {
2130 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
2131 None
2132 }
2133 }
2134 } else {
2135 None
2136 };
2137
2138 let auth_state = AuthState {
2140 config: auth_config,
2141 spec: None, oauth2_client,
2143 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
2144 };
2145
2146 app = app.layer(axum::middleware::from_fn_with_state(auth_state, auth_middleware));
2148 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
2149 }
2150
2151 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
2154
2155 app = apply_cors_middleware(app, final_cors_config);
2157
2158 app
2159}
2160
2161#[test]
2165fn test_route_info_clone() {
2166 let route = RouteInfo {
2167 method: "POST".to_string(),
2168 path: "/users".to_string(),
2169 operation_id: Some("createUser".to_string()),
2170 summary: None,
2171 description: None,
2172 parameters: vec![],
2173 };
2174
2175 let cloned = route.clone();
2176 assert_eq!(route.method, cloned.method);
2177 assert_eq!(route.path, cloned.path);
2178 assert_eq!(route.operation_id, cloned.operation_id);
2179}
2180
2181#[test]
2182fn test_http_server_state_new() {
2183 let state = HttpServerState::new();
2184 assert_eq!(state.routes.len(), 0);
2185}
2186
2187#[test]
2188fn test_http_server_state_with_routes() {
2189 let routes = vec![
2190 RouteInfo {
2191 method: "GET".to_string(),
2192 path: "/users".to_string(),
2193 operation_id: Some("getUsers".to_string()),
2194 summary: None,
2195 description: None,
2196 parameters: vec![],
2197 },
2198 RouteInfo {
2199 method: "POST".to_string(),
2200 path: "/users".to_string(),
2201 operation_id: Some("createUser".to_string()),
2202 summary: None,
2203 description: None,
2204 parameters: vec![],
2205 },
2206 ];
2207
2208 let state = HttpServerState::with_routes(routes.clone());
2209 assert_eq!(state.routes.len(), 2);
2210 assert_eq!(state.routes[0].method, "GET");
2211 assert_eq!(state.routes[1].method, "POST");
2212}
2213
2214#[test]
2215fn test_http_server_state_clone() {
2216 let routes = vec![RouteInfo {
2217 method: "GET".to_string(),
2218 path: "/test".to_string(),
2219 operation_id: None,
2220 summary: None,
2221 description: None,
2222 parameters: vec![],
2223 }];
2224
2225 let state = HttpServerState::with_routes(routes);
2226 let cloned = state.clone();
2227
2228 assert_eq!(state.routes.len(), cloned.routes.len());
2229 assert_eq!(state.routes[0].method, cloned.routes[0].method);
2230}
2231
2232#[tokio::test]
2233async fn test_build_router_without_openapi() {
2234 let _router = build_router(None, None, None).await;
2235 }
2237
2238#[tokio::test]
2239async fn test_build_router_with_nonexistent_spec() {
2240 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
2241 }
2243
2244#[tokio::test]
2245async fn test_build_router_with_auth_and_latency() {
2246 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
2247 }
2249
2250#[tokio::test]
2251async fn test_build_router_with_latency() {
2252 let _router = build_router_with_latency(None, None, None).await;
2253 }
2255
2256#[tokio::test]
2257async fn test_build_router_with_auth() {
2258 let _router = build_router_with_auth(None, None, None).await;
2259 }
2261
2262#[tokio::test]
2263async fn test_build_router_with_chains() {
2264 let _router = build_router_with_chains(None, None, None).await;
2265 }
2267
2268#[test]
2269fn test_route_info_with_all_fields() {
2270 let route = RouteInfo {
2271 method: "PUT".to_string(),
2272 path: "/users/{id}".to_string(),
2273 operation_id: Some("updateUser".to_string()),
2274 summary: Some("Update user".to_string()),
2275 description: Some("Updates an existing user".to_string()),
2276 parameters: vec!["id".to_string(), "body".to_string()],
2277 };
2278
2279 assert!(route.operation_id.is_some());
2280 assert!(route.summary.is_some());
2281 assert!(route.description.is_some());
2282 assert_eq!(route.parameters.len(), 2);
2283}
2284
2285#[test]
2286fn test_route_info_with_minimal_fields() {
2287 let route = RouteInfo {
2288 method: "DELETE".to_string(),
2289 path: "/users/{id}".to_string(),
2290 operation_id: None,
2291 summary: None,
2292 description: None,
2293 parameters: vec![],
2294 };
2295
2296 assert!(route.operation_id.is_none());
2297 assert!(route.summary.is_none());
2298 assert!(route.description.is_none());
2299 assert_eq!(route.parameters.len(), 0);
2300}
2301
2302#[test]
2303fn test_http_server_state_empty_routes() {
2304 let state = HttpServerState::with_routes(vec![]);
2305 assert_eq!(state.routes.len(), 0);
2306}
2307
2308#[test]
2309fn test_http_server_state_multiple_routes() {
2310 let routes = vec![
2311 RouteInfo {
2312 method: "GET".to_string(),
2313 path: "/users".to_string(),
2314 operation_id: Some("listUsers".to_string()),
2315 summary: Some("List all users".to_string()),
2316 description: None,
2317 parameters: vec![],
2318 },
2319 RouteInfo {
2320 method: "GET".to_string(),
2321 path: "/users/{id}".to_string(),
2322 operation_id: Some("getUser".to_string()),
2323 summary: Some("Get a user".to_string()),
2324 description: None,
2325 parameters: vec!["id".to_string()],
2326 },
2327 RouteInfo {
2328 method: "POST".to_string(),
2329 path: "/users".to_string(),
2330 operation_id: Some("createUser".to_string()),
2331 summary: Some("Create a user".to_string()),
2332 description: None,
2333 parameters: vec!["body".to_string()],
2334 },
2335 ];
2336
2337 let state = HttpServerState::with_routes(routes);
2338 assert_eq!(state.routes.len(), 3);
2339
2340 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
2342 assert!(methods.contains(&"GET"));
2343 assert!(methods.contains(&"POST"));
2344}
2345
2346#[test]
2347fn test_http_server_state_with_rate_limiter() {
2348 use std::sync::Arc;
2349
2350 let config = crate::middleware::RateLimitConfig::default();
2351 let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
2352
2353 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
2354
2355 assert!(state.rate_limiter.is_some());
2356 assert_eq!(state.routes.len(), 0);
2357}
2358
2359#[tokio::test]
2360async fn test_build_router_includes_rate_limiter() {
2361 let _router = build_router(None, None, None).await;
2362 }