Skip to main content

rust_webx_host/
server.rs

1//! Host builder and hyper server integration.
2//!
3//! Includes built-in exception middleware: errors produced by endpoints
4//! are caught and converted to well-formed HTTP error responses using
5//! `Error::status_code()`.
6
7use hyper::body::Incoming;
8use hyper::service::service_fn;
9use hyper::Request;
10use hyper_util::rt::TokioIo;
11use rust_dix::{ServiceCollection, ServiceProvider};
12use rust_webx_core::app::IHost;
13use rust_webx_core::config::{self, AppOptions};
14use rust_webx_core::error::Result;
15use rust_webx_core::handler::IHostedService;
16use rust_webx_core::http::IHttpContext;
17use rust_webx_core::middleware::IMiddleware;
18use rust_webx_core::mode::AppMode;
19use rust_webx_core::routing::{HttpMethod, IEndpoint, IRouter};
20
21use crate::auth_jwt::{init_jwt_secret, jwt_middleware, JwtAuth};
22use crate::authz::{build_resource_policy_from_routes, collect_authorizers};
23use crate::context::{BodyLimits, HttpContext, RespBody};
24use crate::cors::{CorsConfig, CorsMiddleware};
25use crate::endpoint::{StaticHtmlEndpoint, StaticJsonEndpoint, StubEndpoint};
26use crate::health::{HealthCheckEndpoint, HealthCheckRegistry, HealthStatus};
27use crate::memory_cache::MemoryCache;
28use crate::metrics::{HttpMetrics, MetricsEndpoint, MetricsMiddleware};
29use crate::pipeline::{HandlerFn, MiddlewarePipeline};
30use crate::problem_response::{build_problem, write_problem_response};
31use crate::rate_limit::RateLimitMiddleware;
32use crate::router::Router;
33use jsonwebtoken::{DecodingKey, Validation};
34use rust_webx_core::route::scan::RouteEntry;
35use rust_webx_core::DispatchRuntime;
36use rust_webx_openapi::{generate_openapi_spec, APIUI_HTML};
37use rust_webx_spa::SpaMiddleware;
38
39use rustls::pki_types::{CertificateDer, PrivateKeyDer};
40use rustls::ServerConfig;
41use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
42use std::sync::Arc;
43use std::time::Instant;
44use tokio::task::JoinSet;
45use tokio_rustls::TlsAcceptor;
46
47/// Returns true when the JWT secret is missing, too short, or a known placeholder.
48fn is_weak_jwt_secret(secret: &str) -> bool {
49    secret.is_empty()
50        || secret.len() < 32
51        || secret.contains("change-in-production")
52        || secret.contains("please-change")
53        || secret.contains("demo-secret")
54        || secret.contains("dev-secret")
55}
56
57pub struct Host {
58    #[allow(dead_code)]
59    provider: Arc<ServiceProvider>,
60    dispatch_runtime: Arc<DispatchRuntime>,
61    pub options: AppOptions,
62    /// Body and upload limits, derived from `App` + `Form` configuration.
63    body_limits: BodyLimits,
64    pipeline: Arc<MiddlewarePipeline>,
65    /// The matchit router (retained for introspection).
66    #[allow(dead_code)]
67    router: Arc<Router>,
68    /// Pre-built router handler —eliminates per-request Arc::new.
69    router_handler: HandlerFn,
70    mode: AppMode,
71    #[allow(dead_code)]
72    spa_root: Option<String>,
73    shutdown: Arc<tokio::sync::Notify>,
74    /// Hosted services that are started on host start
75    /// and stopped on graceful shutdown.
76    hosted_services: Vec<Arc<dyn IHostedService>>,
77}
78
79#[allow(clippy::type_complexity)]
80pub struct HostBuilder {
81    service_configs: Vec<Box<dyn FnOnce(ServiceCollection) -> ServiceCollection + Send>>,
82    mode: AppMode,
83    spa_root: Option<String>,
84    spa_disabled: bool,
85    options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
86    cors_config: Option<CorsConfig>,
87    auth_enabled: bool,
88    resource_auth_enabled: bool,
89    health_registry: Arc<HealthCheckRegistry>,
90}
91
92#[allow(clippy::type_complexity)]
93pub struct HostAppBuilder {
94    options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
95}
96
97impl HostAppBuilder {
98    fn new() -> Self {
99        Self {
100            options_modifiers: Vec::new(),
101        }
102    }
103
104    #[allow(non_snake_case)]
105    pub fn useOptions<F>(&mut self, f: F)
106    where
107        F: FnOnce(&mut AppOptions) + Send + 'static,
108    {
109        self.options_modifiers.push(Box::new(f));
110    }
111}
112
113impl HostBuilder {
114    pub fn new() -> Self {
115        Self {
116            service_configs: Vec::new(),
117            // 默认从 `APP_ENV` 环境变量解析运行模式;未设置时为 Development。
118            // 应用可通过 `.mode()` 显式覆盖。
119            mode: AppMode::from_env(),
120            spa_root: None,
121            spa_disabled: false,
122            options_modifiers: Vec::new(),
123            cors_config: None,
124            auth_enabled: false,
125            resource_auth_enabled: false,
126            health_registry: HealthCheckRegistry::new(),
127        }
128    }
129
130    pub fn register<F>(mut self, f: F) -> Self
131    where
132        F: FnOnce(ServiceCollection) -> ServiceCollection + Send + 'static,
133    {
134        self.service_configs.push(Box::new(f));
135        self
136    }
137
138    pub fn configure<F>(mut self, f: F) -> Self
139    where
140        F: FnOnce(&mut HostAppBuilder) + Send + 'static,
141    {
142        let mut builder = HostAppBuilder::new();
143        f(&mut builder);
144        self.options_modifiers
145            .append(&mut builder.options_modifiers);
146        self
147    }
148
149    pub fn mode(mut self, mode: AppMode) -> Self {
150        self.mode = mode;
151        self
152    }
153
154    /// 自动绑定 appsettings 配置节到 `T`,并以 `Arc<T>` 注册到 DI 容器。
155    ///
156    /// 参考 ASP.NET Core 的 `services.Configure<T>(configuration.GetSection(...))`:
157    /// 框架在 `build()` 时读取已合并的 appsettings(含 `{Mode}` overlay 与 env override),
158    /// 取 `section` 子节点反序列化为 `T`,注册为单例供 handler 通过 `Arc<T>` 注入。
159    ///
160    /// ```ignore
161    /// Host::builder()
162    ///     .add_options::<SiteConfig>("Site")
163    ///     .build()
164    /// ```
165    pub fn add_options<T>(self, section: &str) -> Self
166    where
167        T: for<'de> serde::Deserialize<'de> + Default + Send + Sync + 'static,
168    {
169        let mode = self.mode;
170        let section = section.to_string();
171        self.register(move |svc| {
172            let appsettings = config::load_appsettings(mode).unwrap_or_default();
173            let bound: T = config::bind_config(&appsettings, &section);
174            tracing::info!(
175                "[Host] add_options<{}>(\"{}\") registered",
176                std::any::type_name::<T>(),
177                section
178            );
179            svc.instance(Arc::new(bound))
180        })
181    }
182
183    pub fn use_spa(mut self, root: impl Into<String>) -> Self {
184        self.spa_root = Some(root.into());
185        self
186    }
187
188    /// Explicitly disable SPA, including auto-detection.
189    ///
190    /// By default, the framework auto-detects `wwwroot/` under `app_base()`.
191    /// Call this when you want a pure API host without SPA fallback,
192    /// or in tests to isolate framework behavior from filesystem state.
193    pub fn no_spa(mut self) -> Self {
194        self.spa_disabled = true;
195        self
196    }
197
198    pub fn use_cors(mut self, config: CorsConfig) -> Self {
199        self.cors_config = Some(config);
200        self
201    }
202
203    /// Register JWT authentication service and middleware.
204    ///
205    /// 参考 ASP.NET Core 的 `services.AddAuthentication()` + `app.UseAuthentication()`:
206    /// 启用 JWT Bearer Token 认证,中间件自动添加到管道。授权(`#[authorize]`)
207    /// 由框架通过编译期元数据自动收集,无需显式调用。
208    ///
209    /// ```ignore
210    /// Host::builder()
211    ///     .add_authentication()
212    ///     .build()
213    /// ```
214    pub fn add_authentication(mut self) -> Self {
215        self.auth_enabled = true;
216        self
217    }
218
219    /// Enable resource-based authorization using compile-time `#[authorize]` metadata.
220    ///
221    /// Builds a [`ResourceAuthorization`](crate::authz::ResourceAuthorization)
222    /// policy from route inventory and enforces it
223    /// at the endpoint layer (after routing, when `route_pattern()` is available).
224    /// Requires `add_authentication()` for JWT claims.
225    pub fn use_resource_authorization(mut self) -> Self {
226        self.resource_auth_enabled = true;
227        self
228    }
229
230    /// Register a memory cache instance for use via `IDistributedCache` trait.
231    ///
232    /// The cache is registered as a singleton in the DI container.
233    /// Handlers can access it by implementing `From<Arc<MemoryCache>>`.
234    ///
235    /// ```ignore
236    /// Host::builder()
237    ///     .add_memory_cache()
238    ///     .build()
239    ///     .run().await;
240    /// ```
241    pub fn add_memory_cache(mut self) -> Self {
242        let cache = Arc::new(MemoryCache::new());
243        self.service_configs
244            .push(Box::new(move |svc| svc.instance(cache)));
245        self
246    }
247
248    /// Register a memory cache with custom configuration.
249    pub fn add_memory_cache_with(mut self, max_entries: usize) -> Self {
250        let cache = Arc::new(MemoryCache::new().with_max_entries(max_entries));
251        self.service_configs
252            .push(Box::new(move |svc| svc.instance(Arc::clone(&cache))));
253        self
254    }
255
256    /// Register a middleware by type.
257    ///
258    /// Middleware `T` is registered as Singleton in the DI container and
259    /// automatically collected by `provider.get_all::<dyn IMiddleware>()`
260    /// during `build()`.
261    ///
262    /// ```ignore
263    /// Host::builder()
264    ///     .use_middleware::<RequestIdMiddleware>()
265    ///     .build()
266    /// ```
267    pub fn use_middleware<T: IMiddleware + Default + 'static>(mut self) -> Self {
268        self.service_configs.push(Box::new(|svc| {
269            svc.singleton::<dyn IMiddleware>(|_| Arc::new(T::default()) as Arc<dyn IMiddleware>)
270        }));
271        self
272    }
273
274    /// Register a middleware with a custom factory function.
275    ///
276    /// Use this when the middleware requires constructor parameters:
277    ///
278    /// ```ignore
279    /// Host::builder()
280    ///     .use_middleware_with(|| {
281    ///         Arc::new(RateLimitMiddleware::new(10.0, 20)) as Arc<dyn IMiddleware>
282    ///     })
283    ///     .build()
284    /// ```
285    pub fn use_middleware_with<F>(mut self, factory: F) -> Self
286    where
287        F: Fn() -> Arc<dyn IMiddleware> + Send + Sync + 'static,
288    {
289        self.service_configs.push(Box::new(move |svc| {
290            svc.singleton::<dyn IMiddleware>(move |_| factory())
291        }));
292        self
293    }
294
295    /// Register a health check probe.
296    ///
297    /// Health checks are exposed via `/health` and `/health/ready` endpoints
298    /// following RFC 8407 (`application/health+json` content type).
299    ///
300    /// ```ignore
301    /// Host::builder()
302    ///     .add_health_check("database", || -> HealthStatus {
303    ///         if db_ping() { HealthStatus::pass() } else { HealthStatus::fail("db unreachable") }
304    ///     })
305    ///     .build()
306    /// ```
307    pub fn add_health_check<F>(self, name: impl Into<String>, check: F) -> Self
308    where
309        F: Fn() -> HealthStatus + Send + Sync + 'static,
310    {
311        self.health_registry.register(name, Arc::new(check));
312        self
313    }
314
315    pub fn build(self) -> Host {
316        // Initialize structured logging based on app mode.
317        // This is idempotent —subsequent calls are no-ops.
318        let env_filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into());
319        if self.mode == AppMode::Development {
320            let _ = tracing_subscriber::fmt()
321                .with_env_filter(env_filter)
322                .without_time()
323                .with_target(false)
324                .try_init();
325        } else {
326            let _ = tracing_subscriber::fmt()
327                .json()
328                .with_env_filter(env_filter)
329                .try_init();
330        }
331
332        let mut svc = ServiceCollection::from_injected();
333        for cfg in self.service_configs {
334            svc = cfg(svc);
335        }
336
337        // Register HealthCheckRegistry as a singleton for handler injection.
338        let health_registry = Arc::clone(&self.health_registry);
339        svc = svc.instance(health_registry);
340
341        let provider = svc.build().unwrap_or_else(|e| {
342            panic!(
343                "Failed to build ServiceProvider: {}. Check your DI registrations.",
344                e
345            )
346        });
347
348        // Per-host dispatch runtime (provider + handler registry).
349        let handler_cache = Arc::new(rust_webx_core::route::scan::HandlerCache::build());
350        let dispatch_runtime = Arc::new(DispatchRuntime::new(Arc::clone(&provider), handler_cache));
351
352        let appsettings =
353            config::load_appsettings(self.mode).unwrap_or_else(|| serde_json::json!({}));
354        let mut options: AppOptions = config::bind_root(&appsettings);
355        for modifier in self.options_modifiers {
356            modifier(&mut options);
357        }
358
359        // Upload spooling and body limits come from the same merged config.
360        let body_limits = BodyLimits::from_options(&options);
361        body_limits.apply_spool_root();
362        tracing::info!(
363            "[Host] Uploads: max request {} bytes, max file {} bytes, spool >{} bytes to {}",
364            body_limits.multipart_body,
365            body_limits.file,
366            body_limits.memory_threshold,
367            body_limits.spool_dir.display()
368        );
369
370        let http_metrics = HttpMetrics::new();
371
372        let mut pipeline = MiddlewarePipeline::new();
373
374        // Default security & observability middleware (zero-config safe defaults).
375        // Order: SecurityHeaders → RequestId → RateLimit → Metrics → user → CORS → Auth → SPA
376        pipeline.add_middleware(Arc::new(
377            crate::security_headers::SecurityHeadersMiddleware::new(),
378        ));
379        pipeline.add_middleware(Arc::new(crate::request_id::RequestIdMiddleware::new()));
380
381        if options.rate_limit.enabled {
382            tracing::info!(
383                "[Host] RateLimit enabled: {} req/s, burst {}, max_ips {}",
384                options.rate_limit.requests_per_second,
385                options.rate_limit.burst_size,
386                options.rate_limit.max_tracked_ips
387            );
388            pipeline.add_middleware(Arc::new(RateLimitMiddleware::from_config(
389                &options.rate_limit,
390            )));
391        }
392
393        if options.metrics.enabled {
394            tracing::info!("[Host] Metrics enabled at GET /metrics");
395            pipeline.add_middleware(Arc::new(MetricsMiddleware::new(Arc::clone(&http_metrics))));
396        }
397
398        let middlewares: Vec<Arc<dyn IMiddleware>> = provider.get_all::<dyn IMiddleware>();
399        for mw in middlewares {
400            pipeline.add_middleware(mw);
401        }
402
403        let cors = self.cors_config.unwrap_or_else(|| {
404            let cs = &options.cors;
405            CorsConfig {
406                origins: cs.origins.clone(),
407                methods: cs.methods.clone(),
408                headers: cs.headers.clone(),
409                expose_headers: cs.expose_headers.clone(),
410                allow_credentials: cs.allow_credentials,
411                max_age: cs.max_age,
412            }
413        });
414        pipeline.add_middleware(Arc::new(CorsMiddleware::new(cors)));
415
416        if self.auth_enabled {
417            let secret = options.jwt.secret.clone();
418            if !secret.is_empty() {
419                let jwt_auth = Arc::new(JwtAuth::new(
420                    DecodingKey::from_secret(secret.as_bytes()),
421                    Validation::default(),
422                ));
423                pipeline.add_middleware(Arc::new(jwt_middleware(jwt_auth)));
424                init_jwt_secret(&secret);
425            } else {
426                tracing::warn!(
427                    "add_authentication() enabled but no JWT secret configured. Set Jwt.Secret in appsettings.json, JWT_SECRET, or APP__Jwt__Secret env var."
428                );
429            }
430        }
431
432        // SPA 自动启用:若应用显式调用 `use_spa`,使用指定根目录;
433        // 否则框架自动检测应用基准目录下的 `wwwroot/`,存在即启用 SPA。
434        // 这样新应用无需在 main.rs 手写 `use_spa("wwwroot")` 样板。
435        // `no_spa()` 可显式禁用此行为(含自动检测),用于纯 API 主机或测试隔离。
436        // SPA runs after JWT so auth middleware sees API requests first; SpaMiddleware
437        // skips /api/* paths so unmatched API routes return 404 from the router.
438        let spa_root = if self.spa_disabled {
439            None
440        } else {
441            self.spa_root.clone().or_else(|| {
442                let candidate = rust_webx_core::paths::app_base().join("wwwroot");
443                if candidate.is_dir() {
444                    tracing::info!("[Host] Auto-detected SPA root: {}", candidate.display());
445                    Some(candidate.to_string_lossy().into_owned())
446                } else {
447                    None
448                }
449            })
450        };
451        if let Some(ref spa_root) = spa_root {
452            pipeline.add_middleware(Arc::new(SpaMiddleware::new(spa_root.clone())));
453        }
454
455        // Users can register additional middleware here, e.g.:
456        // pipeline.add_middleware(Arc::new(RequestIdMiddleware::new()));
457        // pipeline.add_middleware(Arc::new(TimingMiddleware::new()));
458
459        let pipeline = Arc::new(pipeline);
460
461        // Security: fail fast in production when auth is enabled but JWT secret is missing or weak
462        if self.mode == AppMode::Production
463            && self.auth_enabled
464            && is_weak_jwt_secret(&options.jwt.secret)
465        {
466            panic!(
467                "Production requires a strong JWT secret. Set APP__Jwt__Secret or JWT_SECRET env var (min 32 chars, not a placeholder)."
468            );
469        }
470
471        // Security: fail fast in production when CORS allows all origins
472        if self.mode == AppMode::Production && options.cors.origins.iter().any(|o| o == "*") {
473            panic!(
474                "Production forbids CORS origin '*'. Set explicit origins via Cors.Origins or APP__Cors__Origins."
475            );
476        }
477
478        let mut router = Router::new();
479        let mut route_count = 0usize;
480
481        crate::diagnostics::assert_route_configuration_valid();
482
483        // Build dispatch map: handler_type → dispatch function
484        let mut dispatch_map: std::collections::HashMap<
485            &'static str,
486            rust_webx_core::route::scan::RouteDispatchFn,
487        > = std::collections::HashMap::new();
488
489        for dispatch in inventory::iter::<rust_webx_core::route::scan::RouteDispatch> {
490            dispatch_map.insert(dispatch.handler_type, dispatch.dispatch);
491        }
492
493        let resource_policy: Option<Arc<crate::authz::ResourceAuthorization>> =
494            if self.resource_auth_enabled {
495                Some(Arc::new(build_resource_policy_from_routes()))
496            } else {
497                None
498            };
499
500        for entry in inventory::iter::<RouteEntry> {
501            route_count += 1;
502            let stub = Arc::new(StubEndpoint {
503                method: entry.method.as_str(),
504                path: entry.path,
505                handler_type: entry.handler_type,
506                dispatch_fn: dispatch_map.get(entry.handler_type).copied(),
507                auth_required_role: entry.required_role,
508                auth_required_permission: entry.required_permission,
509                resource_policy: resource_policy.clone(),
510                authorizers: collect_authorizers(provider.as_ref()),
511            });
512            router.register(entry.method, entry.path, stub);
513        }
514
515        let openapi_spec = generate_openapi_spec("Rust WebX API", env!("CARGO_PKG_VERSION"));
516        if self.mode == AppMode::Development {
517            let openapi_bytes = serde_json::to_vec(&openapi_spec).unwrap_or_default();
518            router.register(
519                HttpMethod::Get,
520                "/api/openapi.json",
521                Arc::new(StaticJsonEndpoint::new(openapi_bytes)),
522            );
523            router.register(
524                HttpMethod::Get,
525                "/api/openapi.html",
526                Arc::new(StaticHtmlEndpoint { body: APIUI_HTML }),
527            );
528        }
529
530        // Health check endpoints (RFC 8407 application/health+json)
531        // Probes are evaluated on each request (not baked at build time).
532        let health_registry = Arc::clone(&self.health_registry);
533        let health_endpoint: Arc<dyn IEndpoint> =
534            Arc::new(HealthCheckEndpoint::new(Arc::clone(&health_registry)));
535        router.register(HttpMethod::Get, "/health", Arc::clone(&health_endpoint));
536        router.register(HttpMethod::Get, "/healthz", health_endpoint);
537
538        // /health/live: liveness (process alive → pass, no probe queries)
539        let live_body =
540            serde_json::to_vec(&serde_json::json!({"status":"pass"})).unwrap_or_default();
541        router.register(
542            HttpMethod::Get,
543            "/health/live",
544            Arc::new(StaticJsonEndpoint {
545                body: live_body,
546                content_type: "application/health+json",
547            }),
548        );
549
550        // /health/ready: readiness (queries registry, same as /health)
551        router.register(
552            HttpMethod::Get,
553            "/health/ready",
554            Arc::new(HealthCheckEndpoint::new(health_registry)),
555        );
556
557        if options.metrics.enabled {
558            router.register(
559                HttpMethod::Get,
560                "/metrics",
561                Arc::new(MetricsEndpoint::new(http_metrics)),
562            );
563        }
564
565        if self.mode == AppMode::Development {
566            let version = env!("CARGO_PKG_VERSION");
567            tracing::info!("");
568            tracing::info!("  ----------------------------------------------------------------");
569            tracing::info!("    Rust WebX Framework v{}", version);
570            tracing::info!("  ----------------------------------------------------------------");
571            tracing::info!("    App:      {}", options.app.name);
572            tracing::info!("    CORS:     enabled");
573            if let Some(ref root) = self.spa_root {
574                tracing::info!("    SPA Root: {}", root);
575            }
576            if route_count > 0 {
577                tracing::info!("    Routes:   {} registered", route_count);
578            }
579            crate::diagnostics::log_startup_diagnostics();
580            let banner_urls = if options.app.urls.is_empty() {
581                vec!["http://localhost:5000".to_string()]
582            } else {
583                options
584                    .app
585                    .urls
586                    .iter()
587                    .map(|u| u.replace("0.0.0.0", "localhost"))
588                    .collect::<Vec<_>>()
589            };
590            for url in &banner_urls {
591                tracing::info!("    OpenAPI:  {}/api/openapi.html", url);
592                tracing::info!("    OpenAPI:  {}/api/openapi.json", url);
593            }
594            tracing::info!("  ----------------------------------------------------------------");
595            tracing::info!("");
596        } else if route_count > 0 {
597            tracing::info!("{} route(s) registered", route_count);
598        }
599
600        let router = Arc::new(router);
601        let router_handler = make_router_handler(Arc::clone(&router));
602
603        // Resolve all registered hosted services from the DI container.
604        // These will be started when `run()` is called and stopped on shutdown.
605        let hosted_services: Vec<Arc<dyn IHostedService>> =
606            provider.get_all::<dyn IHostedService>();
607
608        Host {
609            provider,
610            dispatch_runtime,
611            options,
612            body_limits,
613            pipeline,
614            router,
615            router_handler,
616            mode: self.mode,
617            spa_root: self.spa_root,
618            shutdown: Arc::new(tokio::sync::Notify::new()),
619            hosted_services,
620        }
621    }
622}
623
624/// Handle for a running server, allowing programmatic graceful shutdown.
625pub struct ServerHandle {
626    shutdown: Arc<tokio::sync::Notify>,
627}
628
629impl ServerHandle {
630    /// Signal the server to stop accepting new connections and begin
631    /// draining existing ones.
632    pub fn shutdown(self) {
633        self.shutdown.notify_waiters();
634    }
635}
636
637/// A fully built server that owns its runtime lifecycle.
638///
639/// Created via `Host::into_server()`. Provides a graceful shutdown
640/// handle alongside the `run()` method.
641///
642/// ```ignore
643/// let server = Host::builder().build().into_server();
644/// let handle = server.handle();
645/// tokio::spawn(async move { server.run().await });
646/// handle.shutdown();
647/// ```
648pub struct Server {
649    host: Host,
650}
651
652impl Server {
653    /// Start listening on all configured URLs.
654    pub async fn run(self) -> Result<()> {
655        self.host.run().await
656    }
657
658    /// Start listening on a single address (convenience).
659    pub async fn run_at(self, addr: &str) -> Result<()> {
660        self.host.run_at(addr).await
661    }
662
663    /// Obtain a shutdown handle before starting.
664    pub fn handle(&self) -> ServerHandle {
665        self.host.server_handle()
666    }
667}
668
669impl Default for HostBuilder {
670    fn default() -> Self {
671        Self::new()
672    }
673}
674
675impl Host {
676    pub fn builder() -> HostBuilder {
677        HostBuilder::new()
678    }
679
680    pub fn options(&self) -> &AppOptions {
681        &self.options
682    }
683
684    /// Root DI container for this host instance.
685    pub fn provider(&self) -> &Arc<ServiceProvider> {
686        &self.provider
687    }
688
689    /// Instance-scoped dispatch runtime (provider + handler cache).
690    pub fn dispatch_runtime(&self) -> &Arc<DispatchRuntime> {
691        &self.dispatch_runtime
692    }
693
694    /// Start the server on all URLs configured in AppOptions.app.Urls.
695    ///
696    /// Automatically detects http:// and https:// URLs from the array.
697    /// Starts a plain TCP listener for each http:// URL and a TLS listener
698    /// for each https:// URL. All listeners run concurrently.
699    ///
700    /// Supports graceful shutdown on Ctrl+C/SIGTERM with 30s connection drain.
701    ///
702    /// # Example
703    ///
704    /// ```json
705    /// { "App": { "Urls": ["http://localhost:5000", "https://localhost:5030"] } }
706    /// ```
707    pub async fn run(&self) -> Result<()> {
708        start_hosted_services(&self.hosted_services, &self.dispatch_runtime).await?;
709
710        let urls = if self.options.app.urls.is_empty() {
711            vec!["http://0.0.0.0:5000".to_string()]
712        } else {
713            self.options.app.urls.clone()
714        };
715
716        let mut http_addrs: Vec<String> = Vec::new();
717        let mut https_addrs: Vec<String> = Vec::new();
718
719        for url in &urls {
720            let (scheme, addr) = parse_url(url)?;
721            match scheme {
722                "http" => http_addrs.push(addr),
723                "https" => https_addrs.push(addr),
724                other => {
725                    return Err(rust_webx_core::error::Error::Http(format!(
726                        "Unsupported URL scheme '{}' in '{}'",
727                        other, url
728                    )))
729                }
730            }
731        }
732
733        let acceptor = if !https_addrs.is_empty() {
734            let tls = &self.options.tls;
735            if tls.cert_path.is_empty() || tls.key_path.is_empty() {
736                return Err(rust_webx_core::error::Error::Http(
737                    "HTTPS URLs require Tls.CertPath and Tls.KeyPath".into(),
738                ));
739            }
740            Some(build_tls_acceptor(&tls.cert_path, &tls.key_path)?)
741        } else {
742            None
743        };
744
745        // Banner
746        if self.mode == AppMode::Development {
747            tracing::info!("");
748            for url in &urls {
749                let display_url = url.replace("0.0.0.0", "localhost");
750                tracing::info!("  Listening on {}", display_url);
751            }
752        } else {
753            tracing::info!("Listening on {} url(s)", urls.len());
754        }
755
756        let notify = Arc::clone(&self.shutdown);
757        install_shutdown_handler(Arc::clone(&notify));
758        spawn_async_shutdown_listener(Arc::clone(&notify));
759
760        let mut handles = Vec::new();
761        let pipeline = Arc::clone(&self.pipeline);
762        let router_handler = self.router_handler.clone();
763        let dispatch_runtime = Arc::clone(&self.dispatch_runtime);
764        let mode = self.mode;
765        let body_limits = self.body_limits.clone();
766        let max_connections = self.options.app.max_connections;
767
768        for addr in &http_addrs {
769            let addr = addr.clone();
770            let n = std::sync::Arc::clone(&notify);
771            let p = Arc::clone(&pipeline);
772            let rh = router_handler.clone();
773            let dr = Arc::clone(&dispatch_runtime);
774            handles.push(tokio::spawn(serve_http(
775                addr,
776                n,
777                p,
778                rh,
779                dr,
780                mode,
781                body_limits.clone(),
782                max_connections,
783            )));
784        }
785
786        if let Some(ref tls_acceptor) = acceptor {
787            for addr in &https_addrs {
788                let addr = addr.clone();
789                let n = std::sync::Arc::clone(&notify);
790                let p = Arc::clone(&pipeline);
791                let rh = router_handler.clone();
792                let dr = Arc::clone(&dispatch_runtime);
793                let a = tls_acceptor.clone();
794                handles.push(tokio::spawn(serve_https(
795                    addr,
796                    a,
797                    n,
798                    p,
799                    rh,
800                    dr,
801                    mode,
802                    body_limits.clone(),
803                    max_connections,
804                )));
805            }
806        }
807
808        // Wait for all HTTP listeners to finish (they exit after shutdown signal).
809        for h in handles {
810            let _ = h.await;
811        }
812
813        stop_hosted_services(&self.hosted_services).await;
814
815        Ok(())
816    }
817
818    /// Start the server at a single explicit address (convenience wrapper).
819    pub async fn run_at(&self, addr: &str) -> Result<()> {
820        start_hosted_services(&self.hosted_services, &self.dispatch_runtime).await?;
821
822        let notify = Arc::clone(&self.shutdown);
823        install_shutdown_handler(Arc::clone(&notify));
824        spawn_async_shutdown_listener(Arc::clone(&notify));
825        serve_http(
826            addr.to_string(),
827            notify,
828            Arc::clone(&self.pipeline),
829            self.router_handler.clone(),
830            Arc::clone(&self.dispatch_runtime),
831            self.mode,
832            self.body_limits.clone(),
833            self.options.app.max_connections,
834        )
835        .await;
836
837        stop_hosted_services(&self.hosted_services).await;
838        Ok(())
839    }
840
841    /// Return a `ServerHandle` that can be used to signal graceful shutdown
842    /// from application code (e.g., integration tests, health checks).
843    ///
844    /// ```ignore
845    /// let host = Host::builder().build();
846    /// let handle = host.server_handle();
847    /// tokio::spawn(async move { host.run().await });
848    /// // ... later:
849    /// handle.shutdown();
850    /// ```
851    pub fn server_handle(&self) -> ServerHandle {
852        ServerHandle {
853            shutdown: Arc::clone(&self.shutdown),
854        }
855    }
856
857    /// Consume the host and return a `Server` that owns the runtime lifecycle.
858    ///
859    /// The returned `Server` can be `.run().await`-ed and provides a
860    /// handle for graceful shutdown.
861    pub fn into_server(self) -> Server {
862        Server { host: self }
863    }
864}
865
866#[async_trait::async_trait]
867impl IHost for Host {
868    async fn run(&self, addr: &str) -> Result<()> {
869        self.run_at(addr).await
870    }
871    async fn stop(&self) -> Result<()> {
872        tracing::info!("Stop requested.");
873        self.shutdown.notify_waiters();
874        Ok(())
875    }
876}
877
878fn make_router_handler(router: Arc<Router>) -> HandlerFn {
879    Arc::new(move |ctx: &mut dyn IHttpContext| {
880        let router = Arc::clone(&router);
881        Box::pin(async move {
882            match router.match_route(ctx).await? {
883                Some((endpoint, params, pattern)) => {
884                    drop(router);
885                    for (key, value) in params {
886                        ctx.request_mut().route_params_mut().insert(key, value);
887                    }
888                    *ctx.request_mut().route_pattern_mut() = Some(pattern);
889                    endpoint.handle(ctx).await
890                }
891                None => {
892                    let path_exists = router.path_exists(ctx.request().path());
893                    drop(router);
894                    // Don't overwrite if a middleware (e.g. SPA) already
895                    // wrote a response body (static file, index.html fallback).
896                    if !ctx.response().has_body() {
897                        if path_exists {
898                            write_error_response(ctx, 405, "Method Not Allowed").await;
899                        } else {
900                            write_error_response(ctx, 404, "Not Found").await;
901                        }
902                    }
903                    Ok(())
904                }
905            }
906        })
907    })
908}
909
910async fn handle_request(
911    req: Request<Incoming>,
912    pipeline: Arc<MiddlewarePipeline>,
913    router_handler: HandlerFn,
914    dispatch_runtime: Arc<DispatchRuntime>,
915    body_limits: BodyLimits,
916) -> std::result::Result<hyper::Response<RespBody>, std::convert::Infallible> {
917    let mut ctx = HttpContext::new(req, body_limits);
918
919    // A request rejected during construction (e.g. a declared Content-Length
920    // above the limit) already carries its final response.
921    if ctx.response().status() < 400 {
922        let result = dispatch_runtime
923            .run(async { pipeline.execute(&mut ctx, router_handler).await })
924            .await;
925        if let Err(e) = result {
926            let status = e.status_code();
927            write_error_response(&mut ctx, status, &e.to_string()).await;
928        }
929    }
930
931    Ok(ctx.into_response().await)
932}
933
934async fn write_error_response(ctx: &mut dyn IHttpContext, status: u16, message: &str) {
935    write_problem_response(ctx, build_problem(status, message)).await;
936}
937
938// ---------------------------------------------------------------------------
939// Shutdown helpers
940// ---------------------------------------------------------------------------
941
942/// Register a process-wide Ctrl+C / SIGINT handler.
943///
944/// On Windows, `cargo run` may exit the parent without stopping the child;
945/// registering here ensures the server process itself receives the signal.
946/// A second Ctrl+C forces immediate exit.
947fn install_shutdown_handler(shutdown: Arc<tokio::sync::Notify>) {
948    use std::sync::atomic::{AtomicUsize, Ordering};
949
950    static CTRL_COUNT: AtomicUsize = AtomicUsize::new(0);
951
952    if let Err(e) = ctrlc::set_handler(move || {
953        let n = CTRL_COUNT.fetch_add(1, Ordering::SeqCst);
954        if n == 0 {
955            tracing::info!("Shutdown signal received, draining connections...");
956            shutdown.notify_waiters();
957        } else {
958            tracing::warn!("Second interrupt — force exiting.");
959            std::process::exit(130);
960        }
961    }) {
962        tracing::warn!("Could not install Ctrl+C handler: {}", e);
963    }
964}
965
966/// Tokio-based listener for Ctrl+C (all platforms) and SIGTERM (Unix).
967fn spawn_async_shutdown_listener(shutdown: Arc<tokio::sync::Notify>) {
968    tokio::spawn(async move {
969        wait_for_shutdown_signal().await;
970        tracing::info!("Async shutdown signal received, draining connections...");
971        shutdown.notify_waiters();
972    });
973}
974
975async fn wait_for_shutdown_signal() {
976    #[cfg(unix)]
977    {
978        use tokio::signal::unix::{signal, SignalKind};
979        let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
980        tokio::select! {
981            res = tokio::signal::ctrl_c() => {
982                if let Err(e) = res {
983                    tracing::warn!("Ctrl+C listener error: {}", e);
984                }
985            }
986            _ = sigterm.recv() => {}
987        }
988    }
989    #[cfg(not(unix))]
990    {
991        if let Err(e) = tokio::signal::ctrl_c().await {
992            tracing::warn!("Ctrl+C listener error: {}", e);
993        }
994    }
995}
996
997async fn start_hosted_services(
998    services: &[Arc<dyn IHostedService>],
999    dispatch_runtime: &DispatchRuntime,
1000) -> Result<()> {
1001    if services.is_empty() {
1002        return Ok(());
1003    }
1004    tracing::info!("Starting {} hosted service(s)...", services.len());
1005    dispatch_runtime
1006        .run(async {
1007            for svc in services {
1008                svc.start().await?;
1009            }
1010            Ok::<(), rust_webx_core::error::Error>(())
1011        })
1012        .await?;
1013    tracing::info!("All hosted services started.");
1014    Ok(())
1015}
1016
1017async fn stop_hosted_services(services: &[Arc<dyn IHostedService>]) {
1018    if services.is_empty() {
1019        return;
1020    }
1021    tracing::info!("Stopping {} hosted service(s)...", services.len());
1022    for svc in services {
1023        if let Err(e) = svc.stop().await {
1024            tracing::warn!("Hosted service stop error: {}", e);
1025        }
1026    }
1027    tracing::info!("All hosted services stopped.");
1028}
1029
1030async fn drain_connections(join_set: &mut JoinSet<()>, label: &str, mode: AppMode) {
1031    let timeout_secs = if mode == AppMode::Development { 5 } else { 30 };
1032    let drain_future = async {
1033        while let Some(result) = join_set.join_next().await {
1034            if let Err(e) = result {
1035                tracing::error!("Connection task panicked: {:?}", e);
1036            }
1037        }
1038    };
1039
1040    match tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), drain_future).await {
1041        Ok(_) => tracing::info!("{}: drained.", label),
1042        Err(_) => {
1043            tracing::warn!("{}: drain timeout, aborting connections.", label);
1044            join_set.abort_all();
1045            while join_set.join_next().await.is_some() {}
1046        }
1047    }
1048}
1049
1050// ---------------------------------------------------------------------------
1051// URL parsing & binding helpers
1052// ---------------------------------------------------------------------------
1053
1054/// Parse a URL string into (scheme, addr) pair.
1055/// e.g., "https://0.0.0.0:5030" →("https", "0.0.0.0:5030")
1056fn parse_url(url: &str) -> Result<(&str, String)> {
1057    if let Some(rest) = url.strip_prefix("https://") {
1058        Ok(("https", rest.to_string()))
1059    } else if let Some(rest) = url.strip_prefix("http://") {
1060        Ok(("http", rest.to_string()))
1061    } else {
1062        Err(rust_webx_core::error::Error::Http(format!(
1063            "Invalid URL '{}'. Use http://host:port or https://host:port",
1064            url
1065        )))
1066    }
1067}
1068
1069/// Serve plain HTTP on the given address.
1070#[allow(clippy::too_many_arguments)]
1071async fn serve_http(
1072    addr: String,
1073    shutdown: std::sync::Arc<tokio::sync::Notify>,
1074    pipeline: Arc<MiddlewarePipeline>,
1075    router_handler: HandlerFn,
1076    dispatch_runtime: Arc<DispatchRuntime>,
1077    mode: AppMode,
1078    body_limits: BodyLimits,
1079    max_connections: usize,
1080) {
1081    let listener = match tokio::net::TcpListener::bind(&addr).await {
1082        Ok(l) => l,
1083        Err(e) => {
1084            tracing::error!("Failed to bind HTTP on {}: {}", addr, e);
1085            return;
1086        }
1087    };
1088
1089    let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1090    let mut join_set = JoinSet::new();
1091
1092    loop {
1093        tokio::select! {
1094            accept_result = listener.accept() => {
1095                let (stream, _) = match accept_result {
1096                    Ok(v) => v,
1097                    Err(e) => {
1098                        tracing::error!("Accept error (will retry): {}", e);
1099                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1100                        continue;
1101                    }
1102                };
1103
1104                while join_set.try_join_next().is_some() {}
1105
1106                let permit = match sem.clone().acquire_owned().await {
1107                    Ok(p) => p,
1108                    Err(_) => continue,
1109                };
1110
1111                let io = TokioIo::new(stream);
1112                let pipeline = Arc::clone(&pipeline);
1113                let router_handler = router_handler.clone();
1114                let dispatch_runtime = Arc::clone(&dispatch_runtime);
1115                let body_limits = body_limits.clone();
1116
1117                join_set.spawn(async move {
1118                    let _permit = permit;
1119                    let svc_fn = service_fn(move |req: Request<Incoming>| {
1120                        let pipeline = Arc::clone(&pipeline);
1121                        let router_handler = router_handler.clone();
1122                        let dispatch_runtime = Arc::clone(&dispatch_runtime);
1123                        let body_limits = body_limits.clone();
1124                        let mode = mode;
1125                        async move {
1126                            let start = Instant::now();
1127                            let method = req.method().to_string();
1128                            let path = req.uri().path().to_string();
1129                            let result = handle_request(
1130                                req,
1131                                pipeline,
1132                                router_handler,
1133                                dispatch_runtime,
1134                                body_limits,
1135                            )
1136                            .await;
1137                            let elapsed = start.elapsed();
1138                            if mode == AppMode::Development {
1139                                let status =
1140                                    result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1141                                tracing::info!(
1142                                    "[{}] {} -> {} ({:.0}ms)",
1143                                    method,
1144                                    path,
1145                                    status,
1146                                    elapsed.as_secs_f64() * 1000.0
1147                                );
1148                            }
1149                            result
1150                        }
1151                    });
1152
1153                    if let Err(err) = hyper::server::conn::http1::Builder::new()
1154                        .serve_connection(io, svc_fn)
1155                        .await
1156                    {
1157                        tracing::error!("Connection error: {}", err);
1158                    }
1159                });
1160            }
1161            _ = shutdown.notified() => {
1162                tracing::info!("HTTP {}: stop accepting, draining...", addr);
1163                break;
1164            }
1165        }
1166    }
1167
1168    drain_connections(&mut join_set, &format!("HTTP {}", addr), mode).await;
1169}
1170
1171/// Serve HTTPS (TLS) on the given address.
1172#[allow(clippy::too_many_arguments)]
1173async fn serve_https(
1174    addr: String,
1175    acceptor: TlsAcceptor,
1176    shutdown: std::sync::Arc<tokio::sync::Notify>,
1177    pipeline: Arc<MiddlewarePipeline>,
1178    router_handler: HandlerFn,
1179    dispatch_runtime: Arc<DispatchRuntime>,
1180    mode: AppMode,
1181    body_limits: BodyLimits,
1182    max_connections: usize,
1183) {
1184    let listener = match tokio::net::TcpListener::bind(&addr).await {
1185        Ok(l) => l,
1186        Err(e) => {
1187            tracing::error!("Failed to bind HTTPS on {}: {}", addr, e);
1188            return;
1189        }
1190    };
1191
1192    let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1193    let mut join_set = JoinSet::new();
1194
1195    loop {
1196        tokio::select! {
1197            accept_result = listener.accept() => {
1198                let (stream, _) = match accept_result {
1199                    Ok(v) => v,
1200                    Err(e) => {
1201                        tracing::error!("Accept error (will retry): {}", e);
1202                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1203                        continue;
1204                    }
1205                };
1206
1207                while join_set.try_join_next().is_some() {}
1208
1209                let permit = match sem.clone().acquire_owned().await {
1210                    Ok(p) => p,
1211                    Err(_) => continue,
1212                };
1213
1214                let acceptor = acceptor.clone();
1215                let pipeline = Arc::clone(&pipeline);
1216                let router_handler = router_handler.clone();
1217                let dispatch_runtime = Arc::clone(&dispatch_runtime);
1218                let body_limits = body_limits.clone();
1219
1220                join_set.spawn(async move {
1221                    let _permit = permit;
1222                    match acceptor.accept(stream).await {
1223                        Ok(tls_stream) => {
1224                            let io = TokioIo::new(tls_stream);
1225                            let svc_fn = service_fn(move |req: Request<Incoming>| {
1226                                let pipeline = Arc::clone(&pipeline);
1227                                let router_handler = router_handler.clone();
1228                                let dispatch_runtime = Arc::clone(&dispatch_runtime);
1229                                let body_limits = body_limits.clone();
1230                                let mode = mode;
1231                                async move {
1232                                    let start = Instant::now();
1233                                    let method = req.method().to_string();
1234                                    let path = req.uri().path().to_string();
1235                                    let result = handle_request(
1236                                        req,
1237                                        pipeline,
1238                                        router_handler,
1239                                        dispatch_runtime,
1240                                        body_limits,
1241                                    )
1242                                    .await;
1243                                    let elapsed = start.elapsed();
1244                                    if mode == AppMode::Development {
1245                                        let status =
1246                                            result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1247                                        tracing::info!(
1248                                            "[{}] {} -> {} ({:.0}ms)",
1249                                            method,
1250                                            path,
1251                                            status,
1252                                            elapsed.as_secs_f64() * 1000.0
1253                                        );
1254                                    }
1255                                    result
1256                                }
1257                            });
1258
1259                            if let Err(err) = hyper::server::conn::http1::Builder::new()
1260                                .serve_connection(io, svc_fn)
1261                                .await
1262                            {
1263                                tracing::error!("TLS connection error: {}", err);
1264                            }
1265                        }
1266                        Err(e) => {
1267                            tracing::error!("TLS handshake error: {}", e);
1268                        }
1269                    }
1270                });
1271            }
1272            _ = shutdown.notified() => {
1273                tracing::info!("HTTPS {}: stop accepting, draining...", addr);
1274                break;
1275            }
1276        }
1277    }
1278
1279    drain_connections(&mut join_set, &format!("HTTPS {}", addr), mode).await;
1280}
1281
1282// ---------------------------------------------------------------------------
1283// TLS helpers
1284// ---------------------------------------------------------------------------
1285
1286/// Build a TLS acceptor from PEM certificate and key files.
1287fn build_tls_acceptor(cert_path: &str, key_path: &str) -> Result<TlsAcceptor> {
1288    use std::fs::File;
1289    use std::io::BufReader;
1290
1291    if cert_path.is_empty() || key_path.is_empty() {
1292        return Err(rust_webx_core::error::Error::Http(
1293            "TLS certificate or key path not configured.".into(),
1294        ));
1295    }
1296
1297    let cert_file = File::open(cert_path).map_err(|e| {
1298        rust_webx_core::error::Error::Http(format!("Cannot open cert '{}': {}", cert_path, e))
1299    })?;
1300    let mut cert_reader = BufReader::new(cert_file);
1301    let certs: Vec<CertificateDer> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
1302    if certs.is_empty() {
1303        return Err(rust_webx_core::error::Error::Http(format!(
1304            "No valid certs in '{}'",
1305            cert_path
1306        )));
1307    }
1308
1309    let key_file = File::open(key_path).map_err(|e| {
1310        rust_webx_core::error::Error::Http(format!("Cannot open key '{}': {}", key_path, e))
1311    })?;
1312    let mut key_reader = BufReader::new(key_file);
1313    let key = pkcs8_private_keys(&mut key_reader)
1314        .filter_map(|r| r.ok())
1315        .map(PrivateKeyDer::from)
1316        .next()
1317        .or_else(|| {
1318            let key_file2 = File::open(key_path).map(BufReader::new).ok()?;
1319            let mut kr2 = key_file2;
1320            let rsa_keys: Vec<PrivateKeyDer> = rsa_private_keys(&mut kr2)
1321                .filter_map(|r| r.ok())
1322                .map(PrivateKeyDer::from)
1323                .collect();
1324            rsa_keys.into_iter().next()
1325        })
1326        .ok_or_else(|| {
1327            rust_webx_core::error::Error::Http(format!("No valid private key in '{}'", key_path))
1328        })?;
1329
1330    let config = ServerConfig::builder()
1331        .with_no_client_auth()
1332        .with_single_cert(certs, key)
1333        .map_err(|e| rust_webx_core::error::Error::Http(format!("TLS config error: {}", e)))?;
1334
1335    Ok(TlsAcceptor::from(std::sync::Arc::new(config)))
1336}