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                allow_credentials: cs.allow_credentials,
388                max_age: cs.max_age,
389            }
390        });
391        pipeline.add_middleware(Arc::new(CorsMiddleware::new(cors)));
392
393        // SPA 自动启用:若应用显式调用 `use_spa`,使用指定根目录;
394        // 否则框架自动检测应用基准目录下的 `wwwroot/`,存在即启用 SPA。
395        // 这样新应用无需在 main.rs 手写 `use_spa("wwwroot")` 样板。
396        // `no_spa()` 可显式禁用此行为(含自动检测),用于纯 API 主机或测试隔离。
397        let spa_root = if self.spa_disabled {
398            None
399        } else {
400            self.spa_root.clone().or_else(|| {
401                let candidate = rust_webx_core::paths::app_base().join("wwwroot");
402                if candidate.is_dir() {
403                    tracing::info!("[Host] Auto-detected SPA root: {}", candidate.display());
404                    Some(candidate.to_string_lossy().into_owned())
405                } else {
406                    None
407                }
408            })
409        };
410        if let Some(ref spa_root) = spa_root {
411            pipeline.add_middleware(Arc::new(SpaMiddleware::new(spa_root.clone())));
412        }
413
414        if self.auth_enabled {
415            let secret = options.jwt.secret.clone();
416            if !secret.is_empty() {
417                let jwt_auth = Arc::new(JwtAuth::new(
418                    DecodingKey::from_secret(secret.as_bytes()),
419                    Validation::default(),
420                ));
421                pipeline.add_middleware(Arc::new(jwt_middleware(jwt_auth)));
422                init_jwt_secret(&secret);
423            } else {
424                tracing::warn!(
425                    "add_authentication() enabled but no JWT secret configured. Set Jwt.Secret in appsettings.json, JWT_SECRET, or APP__Jwt__Secret env var."
426                );
427            }
428        }
429
430        // Users can register additional middleware here, e.g.:
431        // pipeline.add_middleware(Arc::new(RequestIdMiddleware::new()));
432        // pipeline.add_middleware(Arc::new(TimingMiddleware::new()));
433
434        let pipeline = Arc::new(pipeline);
435
436        // Security: fail fast in production when auth is enabled but JWT secret is missing or weak
437        if self.mode == AppMode::Production && self.auth_enabled && is_weak_jwt_secret(&options.jwt.secret)
438        {
439            panic!(
440                "Production requires a strong JWT secret. Set APP__Jwt__Secret or JWT_SECRET env var (min 32 chars, not a placeholder)."
441            );
442        }
443
444        // Security: fail fast in production when CORS allows all origins
445        if self.mode == AppMode::Production && options.cors.origins.iter().any(|o| o == "*") {
446            panic!(
447                "Production forbids CORS origin '*'. Set explicit origins via Cors.Origins or APP__Cors__Origins."
448            );
449        }
450
451        let mut router = Router::new();
452        let mut route_count = 0usize;
453
454        // Build dispatch map: handler_type →dispatch function
455        #[allow(clippy::type_complexity)]
456        let mut dispatch_map: std::collections::HashMap<
457            &'static str,
458            fn(
459                Vec<u8>,
460                std::collections::HashMap<String, String>,
461                std::collections::HashMap<String, String>,
462                Option<Box<dyn rust_webx_core::auth::IClaims>>,
463            ) -> std::pin::Pin<
464                Box<
465                    dyn std::future::Future<
466                            Output = rust_webx_core::error::Result<rust_webx_core::route::scan::ResponseData>,
467                        > + Send,
468                >,
469            >,
470        > = std::collections::HashMap::new();
471
472        for dispatch in inventory::iter::<rust_webx_core::route::scan::RouteDispatch> {
473            dispatch_map.insert(dispatch.handler_type, dispatch.dispatch);
474        }
475
476        for entry in inventory::iter::<RouteEntry> {
477            route_count += 1;
478            let stub = Arc::new(StubEndpoint {
479                method: entry.method.as_str(),
480                path: entry.path,
481                handler_type: entry.handler_type,
482                dispatch_fn: dispatch_map.get(entry.handler_type).copied(),
483                auth_required_role: entry.required_role,
484                authorizers: collect_authorizers(provider.as_ref()),
485            });
486            router.register(entry.method, entry.path, stub);
487        }
488
489        let openapi_spec = generate_openapi_spec("Rust WebX API", env!("CARGO_PKG_VERSION"));
490        if self.mode == AppMode::Development {
491            let openapi_bytes = serde_json::to_vec(&openapi_spec).unwrap_or_default();
492            router.register(
493                HttpMethod::Get,
494                "/api/openapi.json",
495                Arc::new(StaticJsonEndpoint::new(openapi_bytes)),
496            );
497            router.register(
498                HttpMethod::Get,
499                "/api/openapi.html",
500                Arc::new(StaticHtmlEndpoint { body: APIUI_HTML }),
501            );
502        }
503
504        // Health check endpoints (RFC 8407 application/health+json)
505        // Probes are evaluated on each request (not baked at build time).
506        let health_registry = Arc::clone(&self.health_registry);
507        let health_endpoint: Arc<dyn IEndpoint> =
508            Arc::new(HealthCheckEndpoint::new(Arc::clone(&health_registry)));
509        router.register(HttpMethod::Get, "/health", Arc::clone(&health_endpoint));
510        router.register(HttpMethod::Get, "/healthz", health_endpoint);
511
512        // /health/live: liveness (process alive → pass, no probe queries)
513        let live_body = serde_json::to_vec(&serde_json::json!({"status":"pass"}))
514            .unwrap_or_default();
515        router.register(
516            HttpMethod::Get,
517            "/health/live",
518            Arc::new(StaticJsonEndpoint {
519                body: live_body,
520                content_type: "application/health+json",
521            }),
522        );
523
524        // /health/ready: readiness (queries registry, same as /health)
525        router.register(
526            HttpMethod::Get,
527            "/health/ready",
528            Arc::new(HealthCheckEndpoint::new(health_registry)),
529        );
530
531        if options.metrics.enabled {
532            router.register(
533                HttpMethod::Get,
534                "/metrics",
535                Arc::new(MetricsEndpoint::new(http_metrics)),
536            );
537        }
538
539        if self.mode == AppMode::Development {
540            let version = env!("CARGO_PKG_VERSION");
541            tracing::info!("");
542            tracing::info!("  ----------------------------------------------------------------");
543            tracing::info!("    Rust WebX Framework v{}", version);
544            tracing::info!("  ----------------------------------------------------------------");
545            tracing::info!("    App:      {}", options.app.name);
546            tracing::info!("    CORS:     enabled");
547            if let Some(ref root) = self.spa_root {
548                tracing::info!("    SPA Root: {}", root);
549            }
550            if route_count > 0 {
551                tracing::info!("    Routes:   {} registered", route_count);
552            }
553            crate::diagnostics::log_startup_diagnostics();
554            let banner_urls = if options.app.urls.is_empty() {
555                vec!["http://localhost:5000".to_string()]
556            } else {
557                options
558                    .app
559                    .urls
560                    .iter()
561                    .map(|u| u.replace("0.0.0.0", "localhost"))
562                    .collect::<Vec<_>>()
563            };
564            for url in &banner_urls {
565                tracing::info!("    OpenAPI:  {}/api/openapi.html", url);
566                tracing::info!("    OpenAPI:  {}/api/openapi.json", url);
567            }
568            tracing::info!("  ----------------------------------------------------------------");
569            tracing::info!("");
570        } else if route_count > 0 {
571            tracing::info!("{} route(s) registered", route_count);
572        }
573
574        let router = Arc::new(router);
575        let router_handler = make_router_handler(Arc::clone(&router));
576
577        // Resolve all registered hosted services from the DI container.
578        // These will be started when `run()` is called and stopped on shutdown.
579        let hosted_services: Vec<Arc<dyn IHostedService>> =
580            provider.get_all::<dyn IHostedService>();
581
582        Host {
583            provider,
584            options,
585            pipeline,
586            router,
587            router_handler,
588            mode: self.mode,
589            spa_root: self.spa_root,
590            shutdown: Arc::new(tokio::sync::Notify::new()),
591            hosted_services,
592        }
593    }
594}
595
596/// Handle for a running server, allowing programmatic graceful shutdown.
597pub struct ServerHandle {
598    shutdown: Arc<tokio::sync::Notify>,
599}
600
601impl ServerHandle {
602    /// Signal the server to stop accepting new connections and begin
603    /// draining existing ones.
604    pub fn shutdown(self) {
605        self.shutdown.notify_waiters();
606    }
607}
608
609/// A fully built server that owns its runtime lifecycle.
610///
611/// Created via `Host::into_server()`. Provides a graceful shutdown
612/// handle alongside the `run()` method.
613///
614/// ```ignore
615/// let server = Host::builder().build().into_server();
616/// let handle = server.handle();
617/// tokio::spawn(async move { server.run().await });
618/// handle.shutdown();
619/// ```
620pub struct Server {
621    host: Host,
622}
623
624impl Server {
625    /// Start listening on all configured URLs.
626    pub async fn run(self) -> Result<()> {
627        self.host.run().await
628    }
629
630    /// Start listening on a single address (convenience).
631    pub async fn run_at(self, addr: &str) -> Result<()> {
632        self.host.run_at(addr).await
633    }
634
635    /// Obtain a shutdown handle before starting.
636    pub fn handle(&self) -> ServerHandle {
637        self.host.server_handle()
638    }
639}
640
641impl Default for HostBuilder {
642    fn default() -> Self {
643        Self::new()
644    }
645}
646
647impl Host {
648    pub fn builder() -> HostBuilder {
649        HostBuilder::new()
650    }
651
652    pub fn options(&self) -> &AppOptions {
653        &self.options
654    }
655
656    /// Start the server on all URLs configured in AppOptions.app.Urls.
657    ///
658    /// Automatically detects http:// and https:// URLs from the array.
659    /// Starts a plain TCP listener for each http:// URL and a TLS listener
660    /// for each https:// URL. All listeners run concurrently.
661    ///
662    /// Supports graceful shutdown on Ctrl+C/SIGTERM with 30s connection drain.
663    ///
664    /// # Example
665    ///
666    /// ```json
667    /// { "App": { "Urls": ["http://localhost:5000", "https://localhost:5030"] } }
668    /// ```
669    pub async fn run(&self) -> Result<()> {
670        start_hosted_services(&self.hosted_services).await?;
671
672        let urls = if self.options.app.urls.is_empty() {
673            vec!["http://0.0.0.0:5000".to_string()]
674        } else {
675            self.options.app.urls.clone()
676        };
677
678        let mut http_addrs: Vec<String> = Vec::new();
679        let mut https_addrs: Vec<String> = Vec::new();
680
681        for url in &urls {
682            let (scheme, addr) = parse_url(url)?;
683            match scheme {
684                "http" => http_addrs.push(addr),
685                "https" => https_addrs.push(addr),
686                other => {
687                    return Err(rust_webx_core::error::Error::Http(format!(
688                        "Unsupported URL scheme '{}' in '{}'",
689                        other, url
690                    )))
691                }
692            }
693        }
694
695        let acceptor = if !https_addrs.is_empty() {
696            let tls = &self.options.tls;
697            if tls.cert_path.is_empty() || tls.key_path.is_empty() {
698                return Err(rust_webx_core::error::Error::Http(
699                    "HTTPS URLs require Tls.CertPath and Tls.KeyPath".into(),
700                ));
701            }
702            Some(build_tls_acceptor(&tls.cert_path, &tls.key_path)?)
703        } else {
704            None
705        };
706
707        // Banner
708        if self.mode == AppMode::Development {
709            tracing::info!("");
710            for url in &urls {
711                let display_url = url.replace("0.0.0.0", "localhost");
712                tracing::info!("  Listening on {}", display_url);
713            }
714        } else {
715            tracing::info!("Listening on {} url(s)", urls.len());
716        }
717
718        let notify = Arc::clone(&self.shutdown);
719        install_shutdown_handler(Arc::clone(&notify));
720        spawn_async_shutdown_listener(Arc::clone(&notify));
721
722        let mut handles = Vec::new();
723        let pipeline = Arc::clone(&self.pipeline);
724        let router_handler = self.router_handler.clone();
725        let mode = self.mode;
726        let max_body_size = self.options.app.max_body_size;
727
728        for addr in &http_addrs {
729            let addr = addr.clone();
730            let n = std::sync::Arc::clone(&notify);
731            let p = Arc::clone(&pipeline);
732            let rh = router_handler.clone();
733            handles.push(tokio::spawn(serve_http(
734                addr,
735                n,
736                p,
737                rh,
738                mode,
739                max_body_size,
740            )));
741        }
742
743        if let Some(ref tls_acceptor) = acceptor {
744            for addr in &https_addrs {
745                let addr = addr.clone();
746                let n = std::sync::Arc::clone(&notify);
747                let p = Arc::clone(&pipeline);
748                let rh = router_handler.clone();
749                let a = tls_acceptor.clone();
750                handles.push(tokio::spawn(serve_https(
751                    addr,
752                    a,
753                    n,
754                    p,
755                    rh,
756                    mode,
757                    max_body_size,
758                )));
759            }
760        }
761
762        // Wait for all HTTP listeners to finish (they exit after shutdown signal).
763        for h in handles {
764            let _ = h.await;
765        }
766
767        stop_hosted_services(&self.hosted_services).await;
768
769        Ok(())
770    }
771
772    /// Start the server at a single explicit address (convenience wrapper).
773    pub async fn run_at(&self, addr: &str) -> Result<()> {
774        start_hosted_services(&self.hosted_services).await?;
775
776        let notify = Arc::clone(&self.shutdown);
777        install_shutdown_handler(Arc::clone(&notify));
778        spawn_async_shutdown_listener(Arc::clone(&notify));
779        serve_http(
780            addr.to_string(),
781            notify,
782            Arc::clone(&self.pipeline),
783            self.router_handler.clone(),
784            self.mode,
785            self.options.app.max_body_size,
786        )
787        .await;
788
789        stop_hosted_services(&self.hosted_services).await;
790        Ok(())
791    }
792
793    /// Return a `ServerHandle` that can be used to signal graceful shutdown
794    /// from application code (e.g., integration tests, health checks).
795    ///
796    /// ```ignore
797    /// let host = Host::builder().build();
798    /// let handle = host.server_handle();
799    /// tokio::spawn(async move { host.run().await });
800    /// // ... later:
801    /// handle.shutdown();
802    /// ```
803    pub fn server_handle(&self) -> ServerHandle {
804        ServerHandle {
805            shutdown: Arc::clone(&self.shutdown),
806        }
807    }
808
809    /// Consume the host and return a `Server` that owns the runtime lifecycle.
810    ///
811    /// The returned `Server` can be `.run().await`-ed and provides a
812    /// handle for graceful shutdown.
813    pub fn into_server(self) -> Server {
814        Server { host: self }
815    }
816}
817
818#[async_trait::async_trait]
819impl IHost for Host {
820    async fn run(&self, addr: &str) -> Result<()> {
821        self.run_at(addr).await
822    }
823    async fn stop(&self) -> Result<()> {
824        tracing::info!("Stop requested.");
825        self.shutdown.notify_waiters();
826        Ok(())
827    }
828}
829
830fn make_router_handler(router: Arc<Router>) -> HandlerFn {
831    Arc::new(move |ctx: &mut dyn IHttpContext| {
832        let router = Arc::clone(&router);
833        Box::pin(async move {
834            match router.match_route(ctx).await? {
835                Some((endpoint, params, pattern)) => {
836                    drop(router);
837                    for (key, value) in params {
838                        ctx.request_mut().route_params_mut().insert(key, value);
839                    }
840                    *ctx.request_mut().route_pattern_mut() = Some(pattern);
841                    endpoint.handle(ctx).await
842                }
843                None => {
844                    drop(router);
845                    // Don't overwrite if a middleware (e.g. SPA) already
846                    // wrote a response body (static file, index.html fallback).
847                    if !ctx.response().has_body() {
848                        write_error_response(ctx, 404, "Not Found").await;
849                    }
850                    Ok(())
851                }
852            }
853        })
854    })
855}
856
857async fn handle_request(
858    req: Request<Incoming>,
859    pipeline: Arc<MiddlewarePipeline>,
860    router_handler: HandlerFn,
861    max_body_size: usize,
862) -> std::result::Result<hyper::Response<Full<Bytes>>, std::convert::Infallible> {
863    let mut ctx = HttpContext::new(req, max_body_size).await;
864    let result = pipeline.execute(&mut ctx, router_handler).await;
865
866    if let Err(e) = result {
867        let status = e.status_code();
868        write_error_response(&mut ctx, status, &e.to_string()).await;
869    }
870
871    Ok(ctx.into_response())
872}
873
874async fn write_error_response(ctx: &mut dyn IHttpContext, status: u16, message: &str) {
875    write_problem_response(ctx, build_problem(status, message)).await;
876}
877
878// ---------------------------------------------------------------------------
879// Shutdown helpers
880// ---------------------------------------------------------------------------
881
882/// Register a process-wide Ctrl+C / SIGINT handler.
883///
884/// On Windows, `cargo run` may exit the parent without stopping the child;
885/// registering here ensures the server process itself receives the signal.
886/// A second Ctrl+C forces immediate exit.
887fn install_shutdown_handler(shutdown: Arc<tokio::sync::Notify>) {
888    use std::sync::atomic::{AtomicUsize, Ordering};
889
890    static CTRL_COUNT: AtomicUsize = AtomicUsize::new(0);
891
892    if let Err(e) = ctrlc::set_handler(move || {
893        let n = CTRL_COUNT.fetch_add(1, Ordering::SeqCst);
894        if n == 0 {
895            tracing::info!("Shutdown signal received, draining connections...");
896            shutdown.notify_waiters();
897        } else {
898            tracing::warn!("Second interrupt — force exiting.");
899            std::process::exit(130);
900        }
901    }) {
902        tracing::warn!("Could not install Ctrl+C handler: {}", e);
903    }
904}
905
906/// Tokio-based listener for Ctrl+C (all platforms) and SIGTERM (Unix).
907fn spawn_async_shutdown_listener(shutdown: Arc<tokio::sync::Notify>) {
908    tokio::spawn(async move {
909        wait_for_shutdown_signal().await;
910        tracing::info!("Async shutdown signal received, draining connections...");
911        shutdown.notify_waiters();
912    });
913}
914
915async fn wait_for_shutdown_signal() {
916    #[cfg(unix)]
917    {
918        use tokio::signal::unix::{signal, SignalKind};
919        let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
920        tokio::select! {
921            res = tokio::signal::ctrl_c() => {
922                if let Err(e) = res {
923                    tracing::warn!("Ctrl+C listener error: {}", e);
924                }
925            }
926            _ = sigterm.recv() => {}
927        }
928    }
929    #[cfg(not(unix))]
930    {
931        if let Err(e) = tokio::signal::ctrl_c().await {
932            tracing::warn!("Ctrl+C listener error: {}", e);
933        }
934    }
935}
936
937async fn start_hosted_services(services: &[Arc<dyn IHostedService>]) -> Result<()> {
938    if services.is_empty() {
939        return Ok(());
940    }
941    tracing::info!("Starting {} hosted service(s)...", services.len());
942    for svc in services {
943        svc.start().await?;
944    }
945    tracing::info!("All hosted services started.");
946    Ok(())
947}
948
949async fn stop_hosted_services(services: &[Arc<dyn IHostedService>]) {
950    if services.is_empty() {
951        return;
952    }
953    tracing::info!("Stopping {} hosted service(s)...", services.len());
954    for svc in services {
955        if let Err(e) = svc.stop().await {
956            tracing::warn!("Hosted service stop error: {}", e);
957        }
958    }
959    tracing::info!("All hosted services stopped.");
960}
961
962async fn drain_connections(
963    join_set: &mut JoinSet<()>,
964    label: &str,
965    mode: AppMode,
966) {
967    let timeout_secs = if mode == AppMode::Development { 5 } else { 30 };
968    let drain_future = async {
969        while let Some(result) = join_set.join_next().await {
970            if let Err(e) = result {
971                tracing::error!("Connection task panicked: {:?}", e);
972            }
973        }
974    };
975
976    match tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), drain_future).await {
977        Ok(_) => tracing::info!("{}: drained.", label),
978        Err(_) => {
979            tracing::warn!("{}: drain timeout, aborting connections.", label);
980            join_set.abort_all();
981            while join_set.join_next().await.is_some() {}
982        }
983    }
984}
985
986// ---------------------------------------------------------------------------
987// URL parsing & binding helpers
988// ---------------------------------------------------------------------------
989
990/// Parse a URL string into (scheme, addr) pair.
991/// e.g., "https://0.0.0.0:5030" →("https", "0.0.0.0:5030")
992fn parse_url(url: &str) -> Result<(&str, String)> {
993    if let Some(rest) = url.strip_prefix("https://") {
994        Ok(("https", rest.to_string()))
995    } else if let Some(rest) = url.strip_prefix("http://") {
996        Ok(("http", rest.to_string()))
997    } else {
998        Err(rust_webx_core::error::Error::Http(format!(
999            "Invalid URL '{}'. Use http://host:port or https://host:port",
1000            url
1001        )))
1002    }
1003}
1004
1005/// Serve plain HTTP on the given address.
1006async fn serve_http(
1007    addr: String,
1008    shutdown: std::sync::Arc<tokio::sync::Notify>,
1009    pipeline: Arc<MiddlewarePipeline>,
1010    router_handler: HandlerFn,
1011    mode: AppMode,
1012    max_body_size: usize,
1013) {
1014    let listener = match tokio::net::TcpListener::bind(&addr).await {
1015        Ok(l) => l,
1016        Err(e) => {
1017            tracing::error!("Failed to bind HTTP on {}: {}", addr, e);
1018            return;
1019        }
1020    };
1021
1022    let mut join_set = JoinSet::new();
1023
1024    loop {
1025        tokio::select! {
1026            accept_result = listener.accept() => {
1027                let (stream, _) = match accept_result {
1028                    Ok(v) => v,
1029                    Err(e) => {
1030                        tracing::error!("Accept error (will retry): {}", e);
1031                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1032                        continue;
1033                    }
1034                };
1035
1036                while join_set.try_join_next().is_some() {}
1037
1038                let io = TokioIo::new(stream);
1039                let pipeline = Arc::clone(&pipeline);
1040                let router_handler = router_handler.clone();
1041
1042                join_set.spawn(async move {
1043                    let svc_fn = service_fn(move |req: Request<Incoming>| {
1044                        let pipeline = Arc::clone(&pipeline);
1045                        let router_handler = router_handler.clone();
1046                        let mode = mode;
1047                        async move {
1048                            let start = Instant::now();
1049                            let method = req.method().to_string();
1050                            let path = req.uri().path().to_string();
1051                            let result =
1052                                handle_request(req, pipeline, router_handler, max_body_size).await;
1053                            let elapsed = start.elapsed();
1054                            if mode == AppMode::Development {
1055                                let status =
1056                                    result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1057                                tracing::info!(
1058                                    "[{}] {} -> {} ({:.0}ms)",
1059                                    method,
1060                                    path,
1061                                    status,
1062                                    elapsed.as_secs_f64() * 1000.0
1063                                );
1064                            }
1065                            result
1066                        }
1067                    });
1068
1069                    if let Err(err) = hyper::server::conn::http1::Builder::new()
1070                        .serve_connection(io, svc_fn)
1071                        .await
1072                    {
1073                        tracing::error!("Connection error: {}", err);
1074                    }
1075                });
1076            }
1077            _ = shutdown.notified() => {
1078                tracing::info!("HTTP {}: stop accepting, draining...", addr);
1079                break;
1080            }
1081        }
1082    }
1083
1084    drain_connections(&mut join_set, &format!("HTTP {}", addr), mode).await;
1085}
1086
1087/// Serve HTTPS (TLS) on the given address.
1088async fn serve_https(
1089    addr: String,
1090    acceptor: TlsAcceptor,
1091    shutdown: std::sync::Arc<tokio::sync::Notify>,
1092    pipeline: Arc<MiddlewarePipeline>,
1093    router_handler: HandlerFn,
1094    mode: AppMode,
1095    max_body_size: usize,
1096) {
1097    let listener = match tokio::net::TcpListener::bind(&addr).await {
1098        Ok(l) => l,
1099        Err(e) => {
1100            tracing::error!("Failed to bind HTTPS on {}: {}", addr, e);
1101            return;
1102        }
1103    };
1104
1105    let mut join_set = JoinSet::new();
1106
1107    loop {
1108        tokio::select! {
1109            accept_result = listener.accept() => {
1110                let (stream, _) = match accept_result {
1111                    Ok(v) => v,
1112                    Err(e) => {
1113                        tracing::error!("Accept error (will retry): {}", e);
1114                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1115                        continue;
1116                    }
1117                };
1118
1119                while join_set.try_join_next().is_some() {}
1120
1121                let acceptor = acceptor.clone();
1122                let pipeline = Arc::clone(&pipeline);
1123                let router_handler = router_handler.clone();
1124
1125                join_set.spawn(async move {
1126                    match acceptor.accept(stream).await {
1127                        Ok(tls_stream) => {
1128                            let io = TokioIo::new(tls_stream);
1129                            let svc_fn = service_fn(move |req: Request<Incoming>| {
1130                                let pipeline = Arc::clone(&pipeline);
1131                                let router_handler = router_handler.clone();
1132                                let mode = mode;
1133                                async move {
1134                                    let start = Instant::now();
1135                                    let method = req.method().to_string();
1136                                    let path = req.uri().path().to_string();
1137                                    let result =
1138                                        handle_request(req, pipeline, router_handler, max_body_size)
1139                                            .await;
1140                                    let elapsed = start.elapsed();
1141                                    if mode == AppMode::Development {
1142                                        let status =
1143                                            result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1144                                        tracing::info!(
1145                                            "[{}] {} -> {} ({:.0}ms)",
1146                                            method,
1147                                            path,
1148                                            status,
1149                                            elapsed.as_secs_f64() * 1000.0
1150                                        );
1151                                    }
1152                                    result
1153                                }
1154                            });
1155
1156                            if let Err(err) = hyper::server::conn::http1::Builder::new()
1157                                .serve_connection(io, svc_fn)
1158                                .await
1159                            {
1160                                tracing::error!("TLS connection error: {}", err);
1161                            }
1162                        }
1163                        Err(e) => {
1164                            tracing::error!("TLS handshake error: {}", e);
1165                        }
1166                    }
1167                });
1168            }
1169            _ = shutdown.notified() => {
1170                tracing::info!("HTTPS {}: stop accepting, draining...", addr);
1171                break;
1172            }
1173        }
1174    }
1175
1176    drain_connections(&mut join_set, &format!("HTTPS {}", addr), mode).await;
1177}
1178
1179// ---------------------------------------------------------------------------
1180// TLS helpers
1181// ---------------------------------------------------------------------------
1182
1183/// Build a TLS acceptor from PEM certificate and key files.
1184fn build_tls_acceptor(cert_path: &str, key_path: &str) -> Result<TlsAcceptor> {
1185    use std::fs::File;
1186    use std::io::BufReader;
1187
1188    if cert_path.is_empty() || key_path.is_empty() {
1189        return Err(rust_webx_core::error::Error::Http(
1190            "TLS certificate or key path not configured.".into(),
1191        ));
1192    }
1193
1194    let cert_file = File::open(cert_path).map_err(|e| {
1195        rust_webx_core::error::Error::Http(format!("Cannot open cert '{}': {}", cert_path, e))
1196    })?;
1197    let mut cert_reader = BufReader::new(cert_file);
1198    let certs: Vec<CertificateDer> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
1199    if certs.is_empty() {
1200        return Err(rust_webx_core::error::Error::Http(format!(
1201            "No valid certs in '{}'",
1202            cert_path
1203        )));
1204    }
1205
1206    let key_file = File::open(key_path).map_err(|e| {
1207        rust_webx_core::error::Error::Http(format!("Cannot open key '{}': {}", key_path, e))
1208    })?;
1209    let mut key_reader = BufReader::new(key_file);
1210    let key = pkcs8_private_keys(&mut key_reader)
1211        .filter_map(|r| r.ok())
1212        .map(PrivateKeyDer::from)
1213        .next()
1214        .or_else(|| {
1215            let key_file2 = File::open(key_path).map(BufReader::new).ok()?;
1216            let mut kr2 = key_file2;
1217            let rsa_keys: Vec<PrivateKeyDer> = rsa_private_keys(&mut kr2)
1218                .filter_map(|r| r.ok())
1219                .map(PrivateKeyDer::from)
1220                .collect();
1221            rsa_keys.into_iter().next()
1222        })
1223        .ok_or_else(|| {
1224            rust_webx_core::error::Error::Http(format!("No valid private key in '{}'", key_path))
1225        })?;
1226
1227    let config = ServerConfig::builder()
1228        .with_no_client_auth()
1229        .with_single_cert(certs, key)
1230        .map_err(|e| rust_webx_core::error::Error::Http(format!("TLS config error: {}", e)))?;
1231
1232    Ok(TlsAcceptor::from(std::sync::Arc::new(config)))
1233}