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