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