1use hyper::body::Incoming;
8use hyper::service::service_fn;
9use hyper::Request;
10use hyper_util::rt::TokioIo;
11use rust_dix::{ServiceCollection, ServiceProvider};
12use webx_core::app::IHost;
13use webx_core::config::{self, AppOptions};
14use webx_core::error::{Error, Result};
15use webx_core::handler::IHostedService;
16use webx_core::http::IHttpContext;
17use webx_core::localization::{IStringLocalizer, LocalizationOptions, StringLocalizer};
18use webx_core::middleware::IMiddleware;
19use webx_core::mode::AppMode;
20use webx_core::route::catalog::{EndpointCatalog, IEndpointCatalog};
21use webx_core::routing::{HttpMethod, IEndpoint, IRouter};
22use webx_core::security::{ISessionStore, ITokenIssuer};
23
24use crate::auth_jwt::{
25 init_jwt_secret, jwt_middleware, jwt_middleware_with_revocation, JwtAuth, JwtTokenIssuer,
26};
27use crate::authz::collect_authorizers;
28use crate::context::{BodyLimits, HttpContext, RespBody};
29use crate::cors::{CorsConfig, CorsMiddleware};
30use crate::endpoint::{StaticHtmlEndpoint, StaticJsonEndpoint, StubEndpoint};
31use crate::health::{HealthCheckEndpoint, HealthCheckRegistry, HealthStatus};
32use crate::memory_cache::MemoryCache;
33use crate::metrics::{HttpMetrics, MetricsEndpoint, MetricsMiddleware};
34use crate::pipeline::{HandlerFn, MiddlewarePipeline};
35use crate::problem_response::{build_problem, write_problem_response, write_unauthorized};
36use crate::rate_limit::RateLimitMiddleware;
37use crate::router::Router;
38use jsonwebtoken::{DecodingKey, Validation};
39use webx_core::route::scan::RouteEntry;
40use webx_core::DispatchRuntime;
41use webx_openapi::{generate_openapi_spec, APIUI_HTML};
42use webx_spa::{EmbeddedAssets, SpaMiddleware, SpaSource};
43
44use rustls::pki_types::{CertificateDer, PrivateKeyDer};
45use rustls::ServerConfig;
46use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
47use std::sync::Arc;
48use std::time::{Duration, Instant};
49use tokio::task::JoinSet;
50use tokio_rustls::TlsAcceptor;
51
52fn is_weak_jwt_secret(secret: &str) -> bool {
54 secret.is_empty()
55 || secret.len() < 32
56 || secret.contains("change-in-production")
57 || secret.contains("please-change")
58 || secret.contains("demo-secret")
59 || secret.contains("dev-secret")
60}
61
62fn log_spa_source(spa: &SpaMiddleware) {
67 tracing::info!("[Host] SPA root: {}", spa.resolved_root().display());
68 let Some(assets) = spa.embedded() else {
69 return;
70 };
71 tracing::info!(
72 "[Host] Embedded assets: {} files, {:.1} MiB embedded ({:.1} MiB uncompressed), built from `{}`",
73 assets.len(),
74 assets.total_bytes() as f64 / (1024.0 * 1024.0),
75 assets.total_raw_bytes() as f64 / (1024.0 * 1024.0),
76 assets.root(),
77 );
78 let shadowed = assets.shadowed_by(spa.resolved_root());
79 if !shadowed.is_empty() {
80 tracing::warn!(
81 "[Host] {} embedded file(s) are overridden on disk: {}. \
82 An override older than the current build stays in effect until it is \
83 updated or deleted.",
84 shadowed.len(),
85 shadowed.join(", "),
86 );
87 }
88}
89
90fn with_embedded_assets(
96 source: Option<SpaSource>,
97 assets: Option<EmbeddedAssets>,
98) -> Option<SpaSource> {
99 let Some(assets) = assets else {
100 return source;
101 };
102 let overlay_root = match source.as_ref() {
103 Some(existing) => existing.disk_root().to_string(),
104 None => assets.root().to_string(),
105 };
106 Some(SpaSource::Embedded {
107 assets,
108 overlay_root,
109 })
110}
111
112type ShutdownSignal = Arc<tokio::sync::watch::Sender<bool>>;
119
120pub struct Host {
121 #[allow(dead_code)]
122 provider: Arc<ServiceProvider>,
123 dispatch_runtime: Arc<DispatchRuntime>,
124 pub options: AppOptions,
125 endpoints: Arc<dyn IEndpointCatalog>,
127 localizer: Arc<dyn IStringLocalizer>,
129 body_limits: BodyLimits,
131 pipeline: Arc<MiddlewarePipeline>,
132 #[allow(dead_code)]
134 router: Arc<Router>,
135 router_handler: HandlerFn,
137 mode: AppMode,
138 #[allow(dead_code)]
140 spa_source: Option<SpaSource>,
141 shutdown: ShutdownSignal,
142 hosted_services: Arc<Vec<Arc<dyn IHostedService>>>,
145 hosted_stopped: Arc<tokio::sync::OnceCell<()>>,
149}
150
151type MiddlewareFactory = Box<dyn Fn() -> Arc<dyn IMiddleware> + Send + Sync>;
153
154struct MiddlewareSpec {
156 slot: webx_core::middleware::MiddlewareSlot,
157 factory: MiddlewareFactory,
158}
159
160#[allow(clippy::type_complexity)]
161pub struct HostBuilder {
162 service_configs: Vec<Box<dyn FnOnce(ServiceCollection) -> ServiceCollection + Send>>,
163 mode: AppMode,
164 spa_source: Option<SpaSource>,
165 spa_disabled: bool,
166 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
167 cors_config: Option<CorsConfig>,
168 auth_enabled: bool,
169 localization: Option<LocalizationOptions>,
170 health_registry: Arc<HealthCheckRegistry>,
171 middleware_specs: Vec<MiddlewareSpec>,
174 lenient_scope_validation: bool,
177}
178
179struct PreparedHost {
185 builder: HostBuilder,
186 svc: ServiceCollection,
187 provider_options: rust_dix::ServiceProviderOptions,
188 options: AppOptions,
189 endpoints: Arc<dyn IEndpointCatalog>,
190 localizer: Arc<dyn IStringLocalizer>,
191 handler_cache: Arc<webx_core::route::scan::HandlerCache>,
192}
193
194#[allow(clippy::type_complexity)]
195pub struct HostAppBuilder {
196 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
197}
198
199impl HostAppBuilder {
200 fn new() -> Self {
201 Self {
202 options_modifiers: Vec::new(),
203 }
204 }
205
206 #[allow(non_snake_case)]
207 pub fn useOptions<F>(&mut self, f: F)
208 where
209 F: FnOnce(&mut AppOptions) + Send + 'static,
210 {
211 self.options_modifiers.push(Box::new(f));
212 }
213}
214
215impl HostBuilder {
216 pub fn new() -> Self {
217 Self {
218 service_configs: Vec::new(),
219 mode: AppMode::from_env(),
222 spa_source: None,
223 spa_disabled: false,
224 options_modifiers: Vec::new(),
225 cors_config: None,
226 auth_enabled: false,
227 localization: None,
228 health_registry: HealthCheckRegistry::new(),
229 middleware_specs: Vec::new(),
230 lenient_scope_validation: false,
231 }
232 }
233
234 pub fn register<F>(mut self, f: F) -> Self
235 where
236 F: FnOnce(ServiceCollection) -> ServiceCollection + Send + 'static,
237 {
238 self.service_configs.push(Box::new(f));
239 self
240 }
241
242 pub fn configure<F>(mut self, f: F) -> Self
243 where
244 F: FnOnce(&mut HostAppBuilder) + Send + 'static,
245 {
246 let mut builder = HostAppBuilder::new();
247 f(&mut builder);
248 self.options_modifiers
249 .append(&mut builder.options_modifiers);
250 self
251 }
252
253 pub fn mode(mut self, mode: AppMode) -> Self {
254 self.mode = mode;
255 self
256 }
257
258 pub fn lenient_scope_validation(mut self) -> Self {
268 self.lenient_scope_validation = true;
269 self
270 }
271
272 pub fn add_options<T>(self, section: &str) -> Self
284 where
285 T: for<'de> serde::Deserialize<'de> + Default + Send + Sync + 'static,
286 {
287 let mode = self.mode;
288 let section = section.to_string();
289 self.register(move |svc| {
290 let appsettings = config::load_appsettings(mode).unwrap_or_default();
291 let bound: T = config::bind_config(&appsettings, §ion);
292 tracing::info!(
293 "[Host] add_options<{}>(\"{}\") registered",
294 std::any::type_name::<T>(),
295 section
296 );
297 svc.instance(Arc::new(bound))
298 })
299 }
300
301 pub fn use_spa(mut self, source: impl Into<SpaSource>) -> Self {
316 self.spa_source = Some(match (self.spa_source.take(), source.into()) {
317 (Some(SpaSource::Embedded { assets, .. }), SpaSource::Disk(root)) => {
320 SpaSource::Embedded {
321 assets,
322 overlay_root: root,
323 }
324 }
325 (_, incoming) => incoming,
326 });
327 self
328 }
329
330 pub fn no_spa(mut self) -> Self {
336 self.spa_disabled = true;
337 self
338 }
339
340 pub fn use_cors(mut self, config: CorsConfig) -> Self {
341 self.cors_config = Some(config);
342 self
343 }
344
345 pub fn add_authentication(mut self) -> Self {
359 self.auth_enabled = true;
360 self
361 }
362
363 pub fn use_session_store(mut self, store: Arc<dyn ISessionStore>) -> Self {
394 self.service_configs.push(Box::new(move |svc| {
395 let store = Arc::clone(&store);
396 svc.try_add_singleton::<dyn ISessionStore>(move |_| Arc::clone(&store))
397 }));
398 self
399 }
400
401 pub fn use_password_hasher(
417 mut self,
418 hasher: Arc<dyn webx_core::security::IPasswordHasher>,
419 ) -> Self {
420 self.service_configs.push(Box::new(move |svc| {
421 let hasher = Arc::clone(&hasher);
422 svc.try_add_singleton::<dyn webx_core::security::IPasswordHasher>(move |_| {
423 Arc::clone(&hasher)
424 })
425 }));
426 self
427 }
428
429 pub fn add_localization(mut self) -> Self {
445 self.localization = Some(LocalizationOptions::new());
446 self
447 }
448
449 pub fn add_localization_with(mut self, options: LocalizationOptions) -> Self {
465 self.localization = Some(options);
466 self
467 }
468
469 pub fn add_memory_cache(mut self) -> Self {
481 let cache = Arc::new(MemoryCache::new());
482 self.service_configs
483 .push(Box::new(move |svc| svc.instance(cache)));
484 self
485 }
486
487 pub fn add_memory_cache_with(mut self, max_entries: usize) -> Self {
489 let cache = Arc::new(MemoryCache::new().with_max_entries(max_entries));
490 self.service_configs
491 .push(Box::new(move |svc| svc.instance(Arc::clone(&cache))));
492 self
493 }
494
495 pub fn use_middleware<T: IMiddleware + Default + 'static>(self) -> Self {
508 self.use_middleware_at::<T>(webx_core::middleware::MiddlewareSlot::AfterAuth)
509 }
510
511 pub fn use_middleware_at<T: IMiddleware + Default + 'static>(
513 mut self,
514 slot: webx_core::middleware::MiddlewareSlot,
515 ) -> Self {
516 self.middleware_specs.push(MiddlewareSpec {
517 slot,
518 factory: Box::new(|| Arc::new(T::default()) as Arc<dyn IMiddleware>),
519 });
520 self
521 }
522
523 pub fn use_middleware_with<F>(self, factory: F) -> Self
536 where
537 F: Fn() -> Arc<dyn IMiddleware> + Send + Sync + 'static,
538 {
539 self.use_middleware_with_at(webx_core::middleware::MiddlewareSlot::AfterAuth, factory)
540 }
541
542 pub fn use_middleware_with_at<F>(
544 mut self,
545 slot: webx_core::middleware::MiddlewareSlot,
546 factory: F,
547 ) -> Self
548 where
549 F: Fn() -> Arc<dyn IMiddleware> + Send + Sync + 'static,
550 {
551 self.middleware_specs.push(MiddlewareSpec {
552 slot,
553 factory: Box::new(factory),
554 });
555 self
556 }
557
558 pub fn add_health_check<F>(self, name: impl Into<String>, check: F) -> Self
571 where
572 F: Fn() -> HealthStatus + Send + Sync + 'static,
573 {
574 self.health_registry.register(name, Arc::new(check));
575 self
576 }
577
578 pub fn try_build(self) -> Result<Host> {
594 let prepared = self.prepare()?;
595 let PreparedHost {
596 builder,
597 svc,
598 provider_options,
599 options,
600 endpoints,
601 localizer,
602 handler_cache,
603 } = prepared;
604 let provider = svc
605 .build_with(provider_options)
606 .map_err(build_provider_error)?;
607 builder.assemble(provider, handler_cache, options, endpoints, localizer)
608 }
609
610 pub async fn try_build_async(self) -> Result<Host> {
624 let prepared = self.prepare()?;
625 let PreparedHost {
626 builder,
627 svc,
628 provider_options,
629 options,
630 endpoints,
631 localizer,
632 handler_cache,
633 } = prepared;
634 let provider = svc
635 .build_async_with(provider_options)
636 .await
637 .map_err(build_provider_error)?;
638 builder.assemble(provider, handler_cache, options, endpoints, localizer)
639 }
640
641 fn prepare(mut self) -> Result<PreparedHost> {
642 let env_filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into());
645 if self.mode == AppMode::Development {
646 let _ = tracing_subscriber::fmt()
647 .with_env_filter(env_filter)
648 .without_time()
649 .with_target(false)
650 .try_init();
651 } else {
652 let _ = tracing_subscriber::fmt()
653 .json()
654 .with_env_filter(env_filter)
655 .try_init();
656 }
657
658 let appsettings =
661 config::load_appsettings(self.mode).unwrap_or_else(|| serde_json::json!({}));
662 let mut options: AppOptions = config::bind_root(&appsettings);
663 for modifier in std::mem::take(&mut self.options_modifiers) {
664 modifier(&mut options);
665 }
666
667 crate::auth_jwt::apply_jwt_secret_file(&mut options)?;
669
670 let default_culture = if options.culture.default.trim().is_empty() {
671 "en".to_string()
672 } else {
673 options.culture.default.clone()
674 };
675 let localization = self
676 .localization
677 .take()
678 .unwrap_or_else(LocalizationOptions::none);
679 let localizer: Arc<dyn IStringLocalizer> = Arc::new(
680 StringLocalizer::new(default_culture, &localization)
681 .map_err(|error| Error::Di(format!("Invalid localization resources: {error}")))?,
682 );
683 let endpoints: Arc<dyn IEndpointCatalog> = Arc::new(EndpointCatalog::from_inventory());
684
685 let mut svc = ServiceCollection::from_injected();
686 for cfg in std::mem::take(&mut self.service_configs) {
687 svc = cfg(svc);
688 }
689
690 if self.auth_enabled && !options.jwt.secret.trim().is_empty() {
697 let secret = options.jwt.secret.clone();
698 let lifetime_minutes = match options.jwt.expiration_minutes {
699 0 => 60,
700 minutes => minutes,
701 };
702 let lifetime = Duration::from_secs(lifetime_minutes.saturating_mul(60));
703 svc = svc.try_add_singleton::<dyn ITokenIssuer>(move |_| {
704 Arc::new(JwtTokenIssuer::from_secret(secret.as_bytes(), lifetime))
705 });
706 }
707
708 #[cfg(feature = "password-argon2")]
713 {
714 svc = svc.try_add_singleton::<dyn webx_core::security::IPasswordHasher>(|_| {
715 Arc::new(crate::password_argon2::Argon2PasswordHasher::default())
716 });
717 }
718
719 let handler_cache = Arc::new(webx_core::route::scan::HandlerCache::build());
722 svc = svc.instance(Arc::clone(&handler_cache));
723
724 let health_registry = Arc::clone(&self.health_registry);
727 svc = svc.try_add(move |_| Arc::clone(&health_registry));
728
729 let endpoints_for_di = Arc::clone(&endpoints);
731 svc = svc.try_add_singleton::<dyn IEndpointCatalog>(move |_| Arc::clone(&endpoints_for_di));
732 let localizer_for_di = Arc::clone(&localizer);
733 svc = svc.try_add_singleton::<dyn IStringLocalizer>(move |_| Arc::clone(&localizer_for_di));
734
735 let provider_options = if self.lenient_scope_validation {
736 rust_dix::ServiceProviderOptions::lenient()
737 } else {
738 rust_dix::ServiceProviderOptions::default()
739 };
740 Ok(PreparedHost {
741 builder: self,
742 svc,
743 provider_options,
744 options,
745 endpoints,
746 localizer,
747 handler_cache,
748 })
749 }
750
751 #[allow(clippy::too_many_arguments)]
752 fn assemble(
753 self,
754 provider: Arc<ServiceProvider>,
755 handler_cache: Arc<webx_core::route::scan::HandlerCache>,
756 options: AppOptions,
757 endpoints: Arc<dyn IEndpointCatalog>,
758 localizer: Arc<dyn IStringLocalizer>,
759 ) -> Result<Host> {
760 let dispatch_runtime = Arc::new(DispatchRuntime::new(Arc::clone(&provider), handler_cache));
762
763 let body_limits = BodyLimits::from_options(&options);
765 body_limits.apply_spool_root();
766 tracing::info!(
767 "[Host] Uploads: max request {} bytes, max file {} bytes, spool >{} bytes to {}",
768 body_limits.multipart_body,
769 body_limits.file,
770 body_limits.memory_threshold,
771 body_limits.spool_dir.display()
772 );
773
774 let http_metrics = HttpMetrics::new();
775
776 let mut pipeline = MiddlewarePipeline::new();
777
778 pipeline.add_middleware(Arc::new(
788 crate::security_headers::SecurityHeadersMiddleware::new(),
789 ));
790 pipeline.add_middleware(Arc::new(crate::request_id::RequestIdMiddleware::new()));
791
792 for spec in self
793 .middleware_specs
794 .iter()
795 .filter(|spec| spec.slot == webx_core::middleware::MiddlewareSlot::BeforeCors)
796 {
797 pipeline.add_middleware((spec.factory)());
798 }
799
800 let cors = self.cors_config.unwrap_or_else(|| {
801 let cs = &options.cors;
802 CorsConfig {
803 origins: cs.origins.clone(),
804 methods: cs.methods.clone(),
805 headers: cs.headers.clone(),
806 expose_headers: cs.expose_headers.clone(),
807 allow_credentials: cs.allow_credentials,
808 max_age: cs.max_age,
809 }
810 });
811 pipeline.add_middleware(Arc::new(CorsMiddleware::new(cors)));
812
813 if options.metrics.enabled {
814 tracing::info!("[Host] Metrics enabled at GET /metrics");
815 pipeline.add_middleware(Arc::new(MetricsMiddleware::new(Arc::clone(&http_metrics))));
816 }
817
818 if options.rate_limit.enabled {
819 tracing::info!(
820 "[Host] RateLimit enabled: {} req/s, burst {}, max_ips {}",
821 options.rate_limit.requests_per_second,
822 options.rate_limit.burst_size,
823 options.rate_limit.max_tracked_ips
824 );
825 pipeline.add_middleware(Arc::new(RateLimitMiddleware::from_config(
826 &options.rate_limit,
827 )));
828 }
829
830 if self.auth_enabled {
831 let secret = options.jwt.secret.clone();
832 if !secret.is_empty() {
833 let jwt_auth = Arc::new(JwtAuth::new(
834 DecodingKey::from_secret(secret.as_bytes()),
835 Validation::default(),
836 ));
837 let session_store = provider.get_all::<dyn ISessionStore>().into_iter().next();
841 match &session_store {
842 Some(_) => tracing::debug!(
843 "[Host] JWT revocation enabled (a dyn ISessionStore is registered)"
844 ),
845 None => tracing::debug!(
846 "[Host] JWT revocation disabled (no dyn ISessionStore registered; \
847 use HostBuilder::use_session_store)"
848 ),
849 }
850 let auth_middleware: Arc<dyn IMiddleware> = match session_store {
851 Some(store) => Arc::new(jwt_middleware_with_revocation(jwt_auth, store)),
852 None => Arc::new(jwt_middleware(jwt_auth)),
853 };
854 pipeline.add_middleware(auth_middleware);
855 init_jwt_secret(&secret);
856 } else {
857 tracing::warn!(
858 "add_authentication() enabled but no JWT secret configured. Set Jwt.Secret in appsettings.json, JWT_SECRET, or APP__Jwt__Secret env var."
859 );
860 }
861 }
862
863 for spec in self
867 .middleware_specs
868 .iter()
869 .filter(|spec| spec.slot == webx_core::middleware::MiddlewareSlot::AfterAuth)
870 {
871 pipeline.add_middleware((spec.factory)());
872 }
873 for mw in provider.get_all::<dyn IMiddleware>() {
874 pipeline.add_middleware(mw);
875 }
876
877 let spa_source = if self.spa_disabled {
885 None
886 } else {
887 let base = self.spa_source.clone().or_else(|| {
888 let candidate = webx_core::paths::app_base().join("wwwroot");
889 if candidate.is_dir() {
890 tracing::info!("[Host] Auto-detected SPA root: {}", candidate.display());
891 Some(SpaSource::Disk(candidate.to_string_lossy().into_owned()))
892 } else {
893 None
894 }
895 });
896 with_embedded_assets(base, EmbeddedAssets::registered().copied())
897 };
898 let mut spa_fallback: Option<Arc<SpaMiddleware>> = None;
899 if let Some(ref source) = spa_source {
900 let spa = Arc::new(SpaMiddleware::from_source(source.clone()));
901 log_spa_source(&spa);
902 pipeline.add_middleware(Arc::clone(&spa) as Arc<dyn IMiddleware>);
903 spa_fallback = Some(spa);
907 }
908
909 for spec in self
911 .middleware_specs
912 .iter()
913 .filter(|spec| spec.slot == webx_core::middleware::MiddlewareSlot::AfterSpa)
914 {
915 pipeline.add_middleware((spec.factory)());
916 }
917
918 let pipeline = Arc::new(pipeline);
919
920 if self.mode == AppMode::Production
922 && self.auth_enabled
923 && is_weak_jwt_secret(&options.jwt.secret)
924 {
925 return Err(Error::Di(
926 "Production requires a strong JWT secret. Set APP__Jwt__Secret or JWT_SECRET env var (min 32 chars, not a placeholder)."
927 .to_string(),
928 ));
929 }
930
931 if self.mode == AppMode::Production && options.cors.origins.iter().any(|o| o == "*") {
933 return Err(Error::Di(
934 "Production forbids CORS origin '*'. Set explicit origins via Cors.Origins or APP__Cors__Origins."
935 .to_string(),
936 ));
937 }
938
939 let mut router = Router::new();
940 let mut route_count = 0usize;
941
942 crate::diagnostics::validate_route_configuration()?;
943
944 let mut dispatch_map: std::collections::HashMap<
946 &'static str,
947 webx_core::route::scan::RouteDispatchFn,
948 > = std::collections::HashMap::new();
949
950 for dispatch in inventory::iter::<webx_core::route::scan::RouteDispatch> {
951 dispatch_map.insert(dispatch.handler_type, dispatch.dispatch);
952 }
953
954 for entry in inventory::iter::<RouteEntry> {
955 route_count += 1;
956 let stub = Arc::new(StubEndpoint {
957 method: entry.method.as_str(),
958 path: entry.path,
959 handler_type: entry.handler_type,
960 dispatch_fn: dispatch_map.get(entry.handler_type).copied(),
961 authorization: entry.authorization,
962 authorization_dynamic: entry.authorization_dynamic,
963 authorizers: collect_authorizers(provider.as_ref()),
964 });
965 router.register(entry.method, entry.path, stub);
966 }
967
968 let openapi_spec = generate_openapi_spec(
969 "Rust WebX API",
970 env!("CARGO_PKG_VERSION"),
971 localizer.as_ref(),
972 );
973 if self.mode == AppMode::Development {
974 let openapi_bytes = serde_json::to_vec(&openapi_spec).unwrap_or_default();
975 router.register(
976 HttpMethod::Get,
977 "/api/openapi.json",
978 Arc::new(StaticJsonEndpoint::new(openapi_bytes)),
979 );
980 router.register(
981 HttpMethod::Get,
982 "/api/openapi.html",
983 Arc::new(StaticHtmlEndpoint { body: APIUI_HTML }),
984 );
985 }
986
987 let health_registry = Arc::clone(&self.health_registry);
990 let health_endpoint: Arc<dyn IEndpoint> =
991 Arc::new(HealthCheckEndpoint::new(Arc::clone(&health_registry)));
992 router.register(HttpMethod::Get, "/health", Arc::clone(&health_endpoint));
993 router.register(HttpMethod::Get, "/healthz", health_endpoint);
994
995 let live_body =
997 serde_json::to_vec(&serde_json::json!({"status":"pass"})).unwrap_or_default();
998 router.register(
999 HttpMethod::Get,
1000 "/health/live",
1001 Arc::new(StaticJsonEndpoint {
1002 body: live_body,
1003 content_type: "application/health+json",
1004 }),
1005 );
1006
1007 router.register(
1009 HttpMethod::Get,
1010 "/health/ready",
1011 Arc::new(HealthCheckEndpoint::new(health_registry)),
1012 );
1013
1014 if options.metrics.enabled {
1015 router.register(
1016 HttpMethod::Get,
1017 "/metrics",
1018 Arc::new(MetricsEndpoint::new(http_metrics)),
1019 );
1020 }
1021
1022 if self.mode == AppMode::Development {
1023 let version = env!("CARGO_PKG_VERSION");
1024 tracing::info!("");
1025 tracing::info!(" ----------------------------------------------------------------");
1026 tracing::info!(" Rust WebX Framework v{}", version);
1027 tracing::info!(" ----------------------------------------------------------------");
1028 tracing::info!(" App: {}", options.app.name);
1029 tracing::info!(" CORS: enabled");
1030 if let Some(source) = spa_source.as_ref() {
1031 tracing::info!(" SPA Root: {}", source.disk_root());
1032 if let Some(assets) = source.embedded() {
1033 tracing::info!(
1034 " Embedded: {} files, {:.1} MiB from {:.1} MiB (built from {})",
1035 assets.len(),
1036 assets.total_bytes() as f64 / (1024.0 * 1024.0),
1037 assets.total_raw_bytes() as f64 / (1024.0 * 1024.0),
1038 assets.root(),
1039 );
1040 }
1041 }
1042 if route_count > 0 {
1043 tracing::info!(" Routes: {} registered", route_count);
1044 }
1045 crate::diagnostics::log_startup_diagnostics();
1046 let banner_urls = if options.app.urls.is_empty() {
1047 vec!["http://localhost:5000".to_string()]
1048 } else {
1049 options
1050 .app
1051 .urls
1052 .iter()
1053 .map(|u| u.replace("0.0.0.0", "localhost"))
1054 .collect::<Vec<_>>()
1055 };
1056 for url in &banner_urls {
1057 tracing::info!(" OpenAPI: {}/api/openapi.html", url);
1058 tracing::info!(" OpenAPI: {}/api/openapi.json", url);
1059 }
1060 tracing::info!(" ----------------------------------------------------------------");
1061 tracing::info!("");
1062 } else if route_count > 0 {
1063 tracing::info!("{} route(s) registered", route_count);
1064 }
1065
1066 let router = Arc::new(router);
1067 let router_handler = make_router_handler(Arc::clone(&router), spa_fallback);
1068
1069 let mut hosted_services: Vec<Arc<dyn IHostedService>> =
1076 provider.get_all::<dyn IHostedService>();
1077 hosted_services.sort_by_key(|service| service.order());
1078
1079 Ok(Host {
1080 provider,
1081 dispatch_runtime,
1082 options,
1083 endpoints,
1084 localizer,
1085 body_limits,
1086 pipeline,
1087 router,
1088 router_handler,
1089 mode: self.mode,
1090 spa_source,
1091 shutdown: Arc::new(tokio::sync::watch::channel(false).0),
1092 hosted_services: Arc::new(hosted_services),
1093 hosted_stopped: Arc::new(tokio::sync::OnceCell::new()),
1094 })
1095 }
1096
1097 pub fn build(self) -> Host {
1104 match self.try_build() {
1105 Ok(host) => host,
1106 Err(error) => panic!("{}", build_failure_message(&error)),
1109 }
1110 }
1111}
1112
1113fn build_provider_error(error: rust_dix::RdiError) -> Error {
1115 Error::Di(format!(
1116 "Failed to build ServiceProvider: {}. Check your DI registrations.",
1117 error
1118 ))
1119}
1120
1121fn build_failure_message(error: &Error) -> String {
1124 match error {
1125 Error::Di(message) => message.clone(),
1126 other => other.to_string(),
1127 }
1128}
1129
1130pub struct ServerHandle {
1132 shutdown: ShutdownSignal,
1133}
1134
1135impl ServerHandle {
1136 pub fn shutdown(self) {
1142 trigger_shutdown(&self.shutdown);
1143 }
1144}
1145
1146pub struct Server {
1158 host: Host,
1159}
1160
1161impl Server {
1162 pub async fn run(self) -> Result<()> {
1164 self.host.run().await
1165 }
1166
1167 pub async fn run_at(self, addr: &str) -> Result<()> {
1169 self.host.run_at(addr).await
1170 }
1171
1172 pub fn handle(&self) -> ServerHandle {
1174 self.host.server_handle()
1175 }
1176}
1177
1178impl Default for HostBuilder {
1179 fn default() -> Self {
1180 Self::new()
1181 }
1182}
1183
1184impl Host {
1185 pub fn builder() -> HostBuilder {
1186 HostBuilder::new()
1187 }
1188
1189 pub fn options(&self) -> &AppOptions {
1190 &self.options
1191 }
1192
1193 pub fn provider(&self) -> &Arc<ServiceProvider> {
1195 &self.provider
1196 }
1197
1198 fn dispose_di(&self) {
1205 self.provider.dispose();
1206 tracing::debug!("[Host] DI provider disposed");
1207 }
1208
1209 async fn stop_hosted_services_once(&self) {
1216 let services = Arc::clone(&self.hosted_services);
1217 self.hosted_stopped
1218 .get_or_init(|| async move { stop_hosted_services(&services).await })
1219 .await;
1220 }
1221
1222 fn spawn_hosted_shutdown_watcher(&self) -> tokio::task::JoinHandle<()> {
1230 let mut shutdown = self.shutdown.subscribe();
1231 let stopped = Arc::clone(&self.hosted_stopped);
1232 let services = Arc::clone(&self.hosted_services);
1233 tokio::spawn(async move {
1234 if shutdown.wait_for(|signalled| *signalled).await.is_ok() {
1236 stopped
1237 .get_or_init(|| async move { stop_hosted_services(&services).await })
1238 .await;
1239 }
1240 })
1241 }
1242
1243 pub fn dispatch_runtime(&self) -> &Arc<DispatchRuntime> {
1245 &self.dispatch_runtime
1246 }
1247
1248 pub fn endpoints(&self) -> &dyn IEndpointCatalog {
1254 &*self.endpoints
1255 }
1256
1257 pub fn localizer(&self) -> &dyn IStringLocalizer {
1259 &*self.localizer
1260 }
1261
1262 pub async fn run(&self) -> Result<()> {
1276 let urls = if self.options.app.urls.is_empty() {
1277 vec!["http://0.0.0.0:5000".to_string()]
1278 } else {
1279 self.options.app.urls.clone()
1280 };
1281
1282 let mut http_addrs: Vec<String> = Vec::new();
1283 let mut https_addrs: Vec<String> = Vec::new();
1284
1285 for url in &urls {
1286 let (scheme, addr) = parse_url(url)?;
1287 match scheme {
1288 "http" => http_addrs.push(addr),
1289 "https" => https_addrs.push(addr),
1290 other => {
1291 return Err(webx_core::error::Error::Http(format!(
1292 "Unsupported URL scheme '{}' in '{}'",
1293 other, url
1294 )))
1295 }
1296 }
1297 }
1298
1299 let acceptor = if !https_addrs.is_empty() {
1300 let tls = &self.options.tls;
1301 if tls.cert_path.is_empty() || tls.key_path.is_empty() {
1302 return Err(webx_core::error::Error::Http(
1303 "HTTPS URLs require Tls.CertPath and Tls.KeyPath".into(),
1304 ));
1305 }
1306 Some(build_tls_acceptor(&tls.cert_path, &tls.key_path)?)
1307 } else {
1308 None
1309 };
1310
1311 let mut http_listeners = Vec::with_capacity(http_addrs.len());
1315 for addr in &http_addrs {
1316 http_listeners.push(bind_listener(addr, "HTTP").await?);
1317 }
1318 let mut https_listeners = Vec::with_capacity(https_addrs.len());
1319 for addr in &https_addrs {
1320 https_listeners.push(bind_listener(addr, "HTTPS").await?);
1321 }
1322
1323 if let Err(err) = start_hosted_services(&self.hosted_services).await {
1324 self.dispose_di();
1325 return Err(err);
1326 }
1327
1328 if self.mode == AppMode::Development {
1330 tracing::info!("");
1331 for url in &urls {
1332 let display_url = url.replace("0.0.0.0", "localhost");
1333 tracing::info!(" Listening on {}", display_url);
1334 }
1335 } else {
1336 tracing::info!("Listening on {} url(s)", urls.len());
1337 }
1338
1339 install_shutdown_handler(Arc::clone(&self.shutdown));
1340 spawn_async_shutdown_listener(Arc::clone(&self.shutdown));
1341 let hosted_watcher = self.spawn_hosted_shutdown_watcher();
1343
1344 let mut handles = Vec::new();
1345 let pipeline = Arc::clone(&self.pipeline);
1346 let router_handler = self.router_handler.clone();
1347 let dispatch_runtime = Arc::clone(&self.dispatch_runtime);
1348 let mode = self.mode;
1349 let body_limits = self.body_limits.clone();
1350 let max_connections = self.options.app.max_connections;
1351
1352 for (addr, listener) in http_addrs.into_iter().zip(http_listeners) {
1353 handles.push(tokio::spawn(serve_http(
1354 addr,
1355 listener,
1356 self.shutdown.subscribe(),
1357 Arc::clone(&pipeline),
1358 router_handler.clone(),
1359 Arc::clone(&dispatch_runtime),
1360 mode,
1361 body_limits.clone(),
1362 max_connections,
1363 )));
1364 }
1365
1366 if let Some(ref tls_acceptor) = acceptor {
1367 for (addr, listener) in https_addrs.into_iter().zip(https_listeners) {
1368 handles.push(tokio::spawn(serve_https(
1369 addr,
1370 tls_acceptor.clone(),
1371 listener,
1372 self.shutdown.subscribe(),
1373 Arc::clone(&pipeline),
1374 router_handler.clone(),
1375 Arc::clone(&dispatch_runtime),
1376 mode,
1377 body_limits.clone(),
1378 max_connections,
1379 )));
1380 }
1381 }
1382
1383 let mut run_error: Option<webx_core::error::Error> = None;
1386 for h in handles {
1387 if let Err(join_err) = h.await {
1388 if run_error.is_none() {
1389 run_error = Some(webx_core::error::Error::Internal(format!(
1390 "a listener task failed: {join_err}"
1391 )));
1392 }
1393 }
1394 }
1395
1396 self.stop_hosted_services_once().await;
1399 hosted_watcher.abort();
1400 self.dispose_di();
1401
1402 match run_error {
1403 Some(err) => Err(err),
1404 None => Ok(()),
1405 }
1406 }
1407
1408 pub async fn run_at(&self, addr: &str) -> Result<()> {
1410 let listener = bind_listener(addr, "HTTP").await?;
1411 if let Err(err) = start_hosted_services(&self.hosted_services).await {
1412 self.dispose_di();
1413 return Err(err);
1414 }
1415
1416 install_shutdown_handler(Arc::clone(&self.shutdown));
1417 spawn_async_shutdown_listener(Arc::clone(&self.shutdown));
1418 let hosted_watcher = self.spawn_hosted_shutdown_watcher();
1420 serve_http(
1421 addr.to_string(),
1422 listener,
1423 self.shutdown.subscribe(),
1424 Arc::clone(&self.pipeline),
1425 self.router_handler.clone(),
1426 Arc::clone(&self.dispatch_runtime),
1427 self.mode,
1428 self.body_limits.clone(),
1429 self.options.app.max_connections,
1430 )
1431 .await;
1432
1433 self.stop_hosted_services_once().await;
1436 hosted_watcher.abort();
1437 self.dispose_di();
1438 Ok(())
1439 }
1440
1441 pub fn server_handle(&self) -> ServerHandle {
1452 ServerHandle {
1453 shutdown: Arc::clone(&self.shutdown),
1454 }
1455 }
1456
1457 pub fn into_server(self) -> Server {
1462 Server { host: self }
1463 }
1464}
1465
1466#[async_trait::async_trait]
1467impl IHost for Host {
1468 async fn run(&self, addr: &str) -> Result<()> {
1469 self.run_at(addr).await
1470 }
1471 async fn stop(&self) -> Result<()> {
1472 tracing::info!("Stop requested.");
1473 trigger_shutdown(&self.shutdown);
1474 Ok(())
1475 }
1476}
1477
1478fn make_router_handler(router: Arc<Router>, spa_fallback: Option<Arc<SpaMiddleware>>) -> HandlerFn {
1479 Arc::new(move |ctx: &mut dyn IHttpContext| {
1480 let router = Arc::clone(&router);
1481 let spa_fallback = spa_fallback.clone();
1482 Box::pin(async move {
1483 if let Some(err) = ctx.take_early_error() {
1487 return Err(err);
1488 }
1489 match router.match_route(ctx).await? {
1490 Some((endpoint, params, pattern)) => {
1491 drop(router);
1492 for (key, value) in params {
1493 ctx.request_mut().route_params_mut().insert(key, value);
1494 }
1495 *ctx.request_mut().route_pattern_mut() = Some(pattern);
1496 endpoint.handle(ctx).await
1497 }
1498 None => {
1499 let path_exists = router.path_exists(ctx.request().path());
1500 let allowed = if path_exists {
1501 router.allowed_methods(ctx.request().path())
1502 } else {
1503 Vec::new()
1504 };
1505 drop(router);
1506 if !ctx.response().has_body() {
1509 if let Some(spa) = spa_fallback {
1512 if spa.try_index_fallback(ctx).await? {
1513 return Ok(());
1514 }
1515 }
1516 if path_exists {
1517 if ctx.request().method().eq_ignore_ascii_case("OPTIONS") {
1518 ctx.response_mut().set_status(204);
1523 ctx.response_mut().set_header("allow", &allowed.join(", "));
1524 } else {
1525 write_method_not_allowed(ctx, &allowed).await;
1527 }
1528 } else {
1529 write_plain_error(ctx, 404, "Not Found").await;
1530 }
1531 }
1532 Ok(())
1533 }
1534 }
1535 })
1536 })
1537}
1538
1539async fn handle_request(
1540 req: Request<Incoming>,
1541 pipeline: Arc<MiddlewarePipeline>,
1542 router_handler: HandlerFn,
1543 dispatch_runtime: Arc<DispatchRuntime>,
1544 body_limits: BodyLimits,
1545 remote_addr: Option<std::net::SocketAddr>,
1546) -> std::result::Result<hyper::Response<RespBody>, std::convert::Infallible> {
1547 let mut ctx = HttpContext::new(req, body_limits, remote_addr, Arc::clone(&dispatch_runtime));
1548
1549 let result = pipeline.execute(&mut ctx, router_handler).await;
1550 if let Err(e) = result {
1551 match e {
1552 webx_core::error::Error::Problem(problem) => {
1556 write_problem_response(&mut ctx, *problem).await
1557 }
1558 other => {
1559 write_framework_error(&mut ctx, &other).await;
1560 }
1561 }
1562 }
1563
1564 Ok(ctx.into_response().await)
1565}
1566
1567async fn write_framework_error(ctx: &mut dyn IHttpContext, error: &webx_core::error::Error) {
1579 let status = error.status_code();
1580 if status == 401 {
1581 write_unauthorized(
1582 ctx,
1583 error.public_detail().unwrap_or("authentication required"),
1584 )
1585 .await;
1586 } else if status >= 500 {
1587 tracing::error!(status, error = %error, "unhandled server error");
1588 write_problem_response(ctx, build_problem(status, "an unexpected error occurred")).await;
1589 } else {
1590 write_problem_response(
1591 ctx,
1592 build_problem(status, error.public_detail().unwrap_or("request failed")),
1593 )
1594 .await;
1595 }
1596}
1597
1598async fn write_plain_error(ctx: &mut dyn IHttpContext, status: u16, message: &str) {
1601 write_problem_response(ctx, build_problem(status, message)).await;
1602}
1603
1604async fn write_method_not_allowed(ctx: &mut dyn IHttpContext, allowed: &[&str]) {
1606 let allow = allowed.join(", ");
1607 ctx.response_mut().set_header("allow", &allow);
1608 write_plain_error(ctx, 405, "Method Not Allowed").await;
1609}
1610
1611fn trigger_shutdown(shutdown: &ShutdownSignal) {
1621 shutdown.send_replace(true);
1626}
1627
1628async fn wait_for_shutdown(shutdown: &mut tokio::sync::watch::Receiver<bool>) {
1633 if *shutdown.borrow() {
1634 return;
1635 }
1636 loop {
1637 if shutdown.changed().await.is_err() {
1638 return;
1639 }
1640 if *shutdown.borrow() {
1641 return;
1642 }
1643 }
1644}
1645
1646fn install_shutdown_handler(shutdown: ShutdownSignal) {
1657 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1658 use std::sync::{Mutex, OnceLock, Weak};
1659
1660 static HANDLERS: OnceLock<Mutex<Vec<Weak<tokio::sync::watch::Sender<bool>>>>> = OnceLock::new();
1661 static CTRL_COUNT: AtomicUsize = AtomicUsize::new(0);
1662 static INSTALLED: AtomicBool = AtomicBool::new(false);
1663
1664 let handlers = HANDLERS.get_or_init(|| Mutex::new(Vec::new()));
1665 {
1666 let mut registered = handlers
1667 .lock()
1668 .unwrap_or_else(|poisoned| poisoned.into_inner());
1669 registered.retain(|weak| weak.strong_count() > 0);
1671 registered.push(Arc::downgrade(&shutdown));
1672 }
1673
1674 if INSTALLED.swap(true, Ordering::SeqCst) {
1678 return;
1679 }
1680
1681 if let Err(e) = ctrlc::set_handler(move || {
1682 let n = CTRL_COUNT.fetch_add(1, Ordering::SeqCst);
1683 if n == 0 {
1684 tracing::info!("Shutdown signal received, draining connections...");
1685 let registered = handlers
1686 .lock()
1687 .unwrap_or_else(|poisoned| poisoned.into_inner());
1688 for weak in registered.iter() {
1689 if let Some(shutdown) = weak.upgrade() {
1690 trigger_shutdown(&shutdown);
1691 }
1692 }
1693 } else {
1694 tracing::warn!("Second interrupt — force exiting.");
1695 std::process::exit(130);
1696 }
1697 }) {
1698 tracing::warn!("Could not install Ctrl+C handler: {}", e);
1699 }
1700}
1701
1702fn spawn_async_shutdown_listener(shutdown: ShutdownSignal) {
1704 tokio::spawn(async move {
1705 wait_for_shutdown_signal().await;
1706 tracing::info!("Async shutdown signal received, draining connections...");
1707 trigger_shutdown(&shutdown);
1708 });
1709}
1710
1711async fn wait_for_shutdown_signal() {
1712 #[cfg(unix)]
1713 {
1714 use tokio::signal::unix::{signal, SignalKind};
1715 let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
1716 tokio::select! {
1717 res = tokio::signal::ctrl_c() => {
1718 if let Err(e) = res {
1719 tracing::warn!("Ctrl+C listener error: {}", e);
1720 }
1721 }
1722 _ = sigterm.recv() => {}
1723 }
1724 }
1725 #[cfg(not(unix))]
1726 {
1727 if let Err(e) = tokio::signal::ctrl_c().await {
1728 tracing::warn!("Ctrl+C listener error: {}", e);
1729 }
1730 }
1731}
1732
1733const HOSTED_START_TIMEOUT_SECS: u64 = 30;
1735const HOSTED_STOP_TIMEOUT_SECS: u64 = 30;
1737
1738async fn start_hosted_services(services: &[Arc<dyn IHostedService>]) -> Result<()> {
1739 if services.is_empty() {
1740 return Ok(());
1741 }
1742 tracing::info!("Starting {} hosted service(s)...", services.len());
1743 let mut started = 0usize;
1744 let mut failure: Option<webx_core::error::Error> = None;
1745
1746 for svc in services {
1747 let attempt = tokio::time::timeout(
1751 std::time::Duration::from_secs(HOSTED_START_TIMEOUT_SECS),
1752 svc.start(),
1753 )
1754 .await;
1755 match attempt {
1756 Ok(Ok(())) => started += 1,
1757 Ok(Err(err)) => {
1758 failure = Some(err);
1759 break;
1760 }
1761 Err(_) => {
1762 failure = Some(webx_core::error::Error::Internal(format!(
1763 "hosted service start timed out after {HOSTED_START_TIMEOUT_SECS}s"
1764 )));
1765 break;
1766 }
1767 }
1768 }
1769
1770 match failure {
1771 None => {
1772 tracing::info!("All {started} hosted service(s) started.");
1773 Ok(())
1774 }
1775 Some(err) => {
1776 for (index, svc) in services[..started].iter().enumerate().rev() {
1779 stop_hosted_service(index, svc).await;
1780 }
1781 Err(err)
1782 }
1783 }
1784}
1785
1786async fn stop_hosted_services(services: &[Arc<dyn IHostedService>]) {
1787 if services.is_empty() {
1788 return;
1789 }
1790 tracing::info!("Stopping {} hosted service(s)...", services.len());
1791 for (index, svc) in services.iter().enumerate().rev() {
1792 stop_hosted_service(index, svc).await;
1793 }
1794 tracing::info!("All hosted services stopped.");
1795}
1796
1797async fn stop_hosted_service(index: usize, svc: &Arc<dyn IHostedService>) {
1799 match tokio::time::timeout(
1800 std::time::Duration::from_secs(HOSTED_STOP_TIMEOUT_SECS),
1801 svc.stop(),
1802 )
1803 .await
1804 {
1805 Ok(Ok(())) => {}
1806 Ok(Err(err)) => {
1807 tracing::warn!(service_index = index, error = %err, "hosted service stop error")
1808 }
1809 Err(_) => tracing::error!(
1810 service_index = index,
1811 timeout_secs = HOSTED_STOP_TIMEOUT_SECS,
1812 "hosted service stop timed out"
1813 ),
1814 }
1815}
1816
1817async fn drain_connections(join_set: &mut JoinSet<()>, label: &str, mode: AppMode) {
1818 let timeout_secs = if mode == AppMode::Development { 5 } else { 30 };
1819 let drain_future = async {
1820 while let Some(result) = join_set.join_next().await {
1821 if let Err(e) = result {
1822 tracing::error!("Connection task panicked: {:?}", e);
1823 }
1824 }
1825 };
1826
1827 match tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), drain_future).await {
1828 Ok(_) => tracing::info!("{}: drained.", label),
1829 Err(_) => {
1830 tracing::warn!("{}: drain timeout, aborting connections.", label);
1831 join_set.abort_all();
1832 while join_set.join_next().await.is_some() {}
1833 }
1834 }
1835}
1836
1837async fn bind_listener(addr: &str, label: &str) -> Result<tokio::net::TcpListener> {
1847 tokio::net::TcpListener::bind(addr).await.map_err(|err| {
1848 webx_core::error::Error::Http(format!("failed to bind {label} on {addr}: {err}"))
1849 })
1850}
1851
1852fn parse_url(url: &str) -> Result<(&str, String)> {
1855 if let Some(rest) = url.strip_prefix("https://") {
1856 Ok(("https", rest.to_string()))
1857 } else if let Some(rest) = url.strip_prefix("http://") {
1858 Ok(("http", rest.to_string()))
1859 } else {
1860 Err(webx_core::error::Error::Http(format!(
1861 "Invalid URL '{}'. Use http://host:port or https://host:port",
1862 url
1863 )))
1864 }
1865}
1866
1867#[allow(clippy::too_many_arguments)]
1869async fn serve_http(
1870 addr: String,
1871 listener: tokio::net::TcpListener,
1872 mut shutdown: tokio::sync::watch::Receiver<bool>,
1873 pipeline: Arc<MiddlewarePipeline>,
1874 router_handler: HandlerFn,
1875 dispatch_runtime: Arc<DispatchRuntime>,
1876 mode: AppMode,
1877 body_limits: BodyLimits,
1878 max_connections: usize,
1879) {
1880 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1881 let mut join_set = JoinSet::new();
1882
1883 loop {
1884 if *shutdown.borrow() {
1888 tracing::info!("HTTP {}: stop accepting, draining...", addr);
1889 break;
1890 }
1891 tokio::select! {
1892 _ = shutdown.changed() => {
1893 tracing::info!("HTTP {}: stop accepting, draining...", addr);
1894 break;
1895 }
1896 accept_result = listener.accept() => {
1897 let (stream, peer_addr) = match accept_result {
1898 Ok(v) => v,
1899 Err(e) => {
1900 tracing::error!("Accept error (will retry): {}", e);
1901 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1902 continue;
1903 }
1904 };
1905 let remote_addr = Some(peer_addr);
1906
1907 while join_set.try_join_next().is_some() {}
1908
1909 let permit = match sem.clone().acquire_owned().await {
1910 Ok(p) => p,
1911 Err(_) => continue,
1912 };
1913
1914 let io = TokioIo::new(stream);
1915 let pipeline = Arc::clone(&pipeline);
1916 let router_handler = router_handler.clone();
1917 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1918 let body_limits = body_limits.clone();
1919 let conn_shutdown = shutdown.clone();
1920
1921 join_set.spawn(async move {
1922 let _permit = permit;
1923 let svc_fn = service_fn(move |req: Request<Incoming>| {
1924 let pipeline = Arc::clone(&pipeline);
1925 let router_handler = router_handler.clone();
1926 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1927 let body_limits = body_limits.clone();
1928 let mode = mode;
1929 let remote_addr = remote_addr;
1932 async move {
1933 let start = Instant::now();
1934 let method = req.method().to_string();
1935 let path = req.uri().path().to_string();
1936 let result = handle_request(
1937 req,
1938 pipeline,
1939 router_handler,
1940 dispatch_runtime,
1941 body_limits,
1942 remote_addr,
1943 )
1944 .await;
1945 let elapsed = start.elapsed();
1946 if mode == AppMode::Development {
1947 let status =
1948 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1949 tracing::info!(
1950 "[{}] {} -> {} ({:.0}ms)",
1951 method,
1952 path,
1953 status,
1954 elapsed.as_secs_f64() * 1000.0
1955 );
1956 }
1957 result
1958 }
1959 });
1960
1961 let conn = hyper::server::conn::http1::Builder::new()
1962 .serve_connection(io, svc_fn);
1963 let mut conn = std::pin::pin!(conn);
1964 let mut conn_shutdown = conn_shutdown;
1965 tokio::select! {
1966 result = conn.as_mut() => {
1967 if let Err(err) = result {
1968 tracing::error!("Connection error: {}", err);
1969 }
1970 }
1971 _ = wait_for_shutdown(&mut conn_shutdown) => {
1972 conn.as_mut().graceful_shutdown();
1976 if let Err(err) = conn.await {
1977 tracing::debug!(error = %err, "connection closed during shutdown");
1978 }
1979 }
1980 }
1981 });
1982 }
1983 }
1984 }
1985
1986 drain_connections(&mut join_set, &format!("HTTP {}", addr), mode).await;
1987}
1988
1989#[allow(clippy::too_many_arguments)]
1991async fn serve_https(
1992 addr: String,
1993 acceptor: TlsAcceptor,
1994 listener: tokio::net::TcpListener,
1995 mut shutdown: tokio::sync::watch::Receiver<bool>,
1996 pipeline: Arc<MiddlewarePipeline>,
1997 router_handler: HandlerFn,
1998 dispatch_runtime: Arc<DispatchRuntime>,
1999 mode: AppMode,
2000 body_limits: BodyLimits,
2001 max_connections: usize,
2002) {
2003 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
2004 let mut join_set = JoinSet::new();
2005
2006 loop {
2007 if *shutdown.borrow() {
2008 tracing::info!("HTTPS {}: stop accepting, draining...", addr);
2009 break;
2010 }
2011 tokio::select! {
2012 _ = shutdown.changed() => {
2013 tracing::info!("HTTPS {}: stop accepting, draining...", addr);
2014 break;
2015 }
2016 accept_result = listener.accept() => {
2017 let (stream, peer_addr) = match accept_result {
2018 Ok(v) => v,
2019 Err(e) => {
2020 tracing::error!("Accept error (will retry): {}", e);
2021 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2022 continue;
2023 }
2024 };
2025 let remote_addr = Some(peer_addr);
2026
2027 while join_set.try_join_next().is_some() {}
2028
2029 let permit = match sem.clone().acquire_owned().await {
2030 Ok(p) => p,
2031 Err(_) => continue,
2032 };
2033
2034 let acceptor = acceptor.clone();
2035 let pipeline = Arc::clone(&pipeline);
2036 let router_handler = router_handler.clone();
2037 let dispatch_runtime = Arc::clone(&dispatch_runtime);
2038 let body_limits = body_limits.clone();
2039 let conn_shutdown = shutdown.clone();
2040
2041 join_set.spawn(async move {
2042 let _permit = permit;
2043 match acceptor.accept(stream).await {
2044 Ok(tls_stream) => {
2045 let io = TokioIo::new(tls_stream);
2046 let svc_fn = service_fn(move |req: Request<Incoming>| {
2047 let pipeline = Arc::clone(&pipeline);
2048 let router_handler = router_handler.clone();
2049 let dispatch_runtime = Arc::clone(&dispatch_runtime);
2050 let body_limits = body_limits.clone();
2051 let mode = mode;
2052 let remote_addr = remote_addr;
2054 async move {
2055 let start = Instant::now();
2056 let method = req.method().to_string();
2057 let path = req.uri().path().to_string();
2058 let result = handle_request(
2059 req,
2060 pipeline,
2061 router_handler,
2062 dispatch_runtime,
2063 body_limits,
2064 remote_addr,
2065 )
2066 .await;
2067 let elapsed = start.elapsed();
2068 if mode == AppMode::Development {
2069 let status =
2070 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
2071 tracing::info!(
2072 "[{}] {} -> {} ({:.0}ms)",
2073 method,
2074 path,
2075 status,
2076 elapsed.as_secs_f64() * 1000.0
2077 );
2078 }
2079 result
2080 }
2081 });
2082
2083 let conn = hyper::server::conn::http1::Builder::new()
2084 .serve_connection(io, svc_fn);
2085 let mut conn = std::pin::pin!(conn);
2086 let mut conn_shutdown = conn_shutdown;
2087 tokio::select! {
2088 result = conn.as_mut() => {
2089 if let Err(err) = result {
2090 tracing::error!("TLS connection error: {}", err);
2091 }
2092 }
2093 _ = wait_for_shutdown(&mut conn_shutdown) => {
2094 conn.as_mut().graceful_shutdown();
2095 if let Err(err) = conn.await {
2096 tracing::debug!(error = %err, "TLS connection closed during shutdown");
2097 }
2098 }
2099 }
2100 }
2101 Err(e) => {
2102 tracing::error!("TLS handshake error: {}", e);
2103 }
2104 }
2105 });
2106 }
2107 }
2108 }
2109
2110 drain_connections(&mut join_set, &format!("HTTPS {}", addr), mode).await;
2111}
2112
2113fn build_tls_acceptor(cert_path: &str, key_path: &str) -> Result<TlsAcceptor> {
2119 use std::fs::File;
2120 use std::io::BufReader;
2121
2122 if cert_path.is_empty() || key_path.is_empty() {
2123 return Err(webx_core::error::Error::Http(
2124 "TLS certificate or key path not configured.".into(),
2125 ));
2126 }
2127
2128 let cert_file = File::open(cert_path).map_err(|e| {
2129 webx_core::error::Error::Http(format!("Cannot open cert '{}': {}", cert_path, e))
2130 })?;
2131 let mut cert_reader = BufReader::new(cert_file);
2132 let certs: Vec<CertificateDer> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
2133 if certs.is_empty() {
2134 return Err(webx_core::error::Error::Http(format!(
2135 "No valid certs in '{}'",
2136 cert_path
2137 )));
2138 }
2139
2140 let key_file = File::open(key_path).map_err(|e| {
2141 webx_core::error::Error::Http(format!("Cannot open key '{}': {}", key_path, e))
2142 })?;
2143 let mut key_reader = BufReader::new(key_file);
2144 let key = pkcs8_private_keys(&mut key_reader)
2145 .filter_map(|r| r.ok())
2146 .map(PrivateKeyDer::from)
2147 .next()
2148 .or_else(|| {
2149 let key_file2 = File::open(key_path).map(BufReader::new).ok()?;
2150 let mut kr2 = key_file2;
2151 let rsa_keys: Vec<PrivateKeyDer> = rsa_private_keys(&mut kr2)
2152 .filter_map(|r| r.ok())
2153 .map(PrivateKeyDer::from)
2154 .collect();
2155 rsa_keys.into_iter().next()
2156 })
2157 .ok_or_else(|| {
2158 webx_core::error::Error::Http(format!("No valid private key in '{}'", key_path))
2159 })?;
2160
2161 let config = ServerConfig::builder()
2162 .with_no_client_auth()
2163 .with_single_cert(certs, key)
2164 .map_err(|e| webx_core::error::Error::Http(format!("TLS config error: {}", e)))?;
2165
2166 Ok(TlsAcceptor::from(std::sync::Arc::new(config)))
2167}
2168
2169#[cfg(test)]
2170mod embedded_attach_tests {
2171 use super::with_embedded_assets;
2172 use webx_spa::{ContentEncoding, EmbeddedAsset, EmbeddedAssets, SpaSource};
2173
2174 const FILES: &[EmbeddedAsset] = &[EmbeddedAsset {
2175 path: "index.html",
2176 bytes: b"hi",
2177 raw_len: 2,
2178 encoding: ContentEncoding::Identity,
2179 content_type: "text/html",
2180 etag: "\"x\"",
2181 }];
2182
2183 #[test]
2184 fn no_table_leaves_disk_source() {
2185 let source = Some(SpaSource::Disk("wwwroot".into()));
2186 let merged = with_embedded_assets(source, None);
2187 assert!(matches!(merged, Some(SpaSource::Disk(ref r)) if r == "wwwroot"));
2188 }
2189
2190 #[test]
2191 fn table_layers_under_use_spa_overlay() {
2192 let assets = EmbeddedAssets::new("../wwwroot", FILES);
2193 let merged = with_embedded_assets(Some(SpaSource::Disk("wwwroot".into())), Some(assets));
2194 match merged {
2195 Some(SpaSource::Embedded {
2196 overlay_root,
2197 assets,
2198 }) => {
2199 assert_eq!(overlay_root, "wwwroot");
2200 assert_eq!(assets.root(), "../wwwroot");
2201 }
2202 other => panic!("expected embedded, got {other:?}"),
2203 }
2204 }
2205
2206 #[test]
2207 fn table_alone_uses_build_root_as_overlay() {
2208 let assets = EmbeddedAssets::new("../wwwroot", FILES);
2209 let merged = with_embedded_assets(None, Some(assets));
2210 match merged {
2211 Some(SpaSource::Embedded { overlay_root, .. }) => {
2212 assert_eq!(overlay_root, "../wwwroot");
2213 }
2214 other => panic!("expected embedded, got {other:?}"),
2215 }
2216 }
2217
2218 #[test]
2219 fn host_crate_registers_no_asset_table() {
2220 assert!(EmbeddedAssets::registered().is_none());
2221 }
2222}