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};
22
23use crate::auth_jwt::{init_jwt_secret, jwt_middleware, JwtAuth};
24use crate::authz::collect_authorizers;
25use crate::context::{BodyLimits, HttpContext, RespBody};
26use crate::cors::{CorsConfig, CorsMiddleware};
27use crate::endpoint::{StaticHtmlEndpoint, StaticJsonEndpoint, StubEndpoint};
28use crate::health::{HealthCheckEndpoint, HealthCheckRegistry, HealthStatus};
29use crate::memory_cache::MemoryCache;
30use crate::metrics::{HttpMetrics, MetricsEndpoint, MetricsMiddleware};
31use crate::pipeline::{HandlerFn, MiddlewarePipeline};
32use crate::problem_response::{build_problem, write_problem_response, write_unauthorized};
33use crate::rate_limit::RateLimitMiddleware;
34use crate::router::Router;
35use jsonwebtoken::{DecodingKey, Validation};
36use webx_core::route::scan::RouteEntry;
37use webx_core::DispatchRuntime;
38use webx_openapi::{generate_openapi_spec, APIUI_HTML};
39use webx_spa::{EmbeddedAssets, SpaMiddleware, SpaSource};
40
41use rustls::pki_types::{CertificateDer, PrivateKeyDer};
42use rustls::ServerConfig;
43use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
44use std::sync::Arc;
45use std::time::Instant;
46use tokio::task::JoinSet;
47use tokio_rustls::TlsAcceptor;
48
49fn is_weak_jwt_secret(secret: &str) -> bool {
51 secret.is_empty()
52 || secret.len() < 32
53 || secret.contains("change-in-production")
54 || secret.contains("please-change")
55 || secret.contains("demo-secret")
56 || secret.contains("dev-secret")
57}
58
59fn log_spa_source(spa: &SpaMiddleware) {
64 tracing::info!("[Host] SPA root: {}", spa.resolved_root().display());
65 let Some(assets) = spa.embedded() else {
66 return;
67 };
68 tracing::info!(
69 "[Host] Embedded assets: {} files, {:.1} MiB embedded ({:.1} MiB uncompressed), built from `{}`",
70 assets.len(),
71 assets.total_bytes() as f64 / (1024.0 * 1024.0),
72 assets.total_raw_bytes() as f64 / (1024.0 * 1024.0),
73 assets.root(),
74 );
75 let shadowed = assets.shadowed_by(spa.resolved_root());
76 if !shadowed.is_empty() {
77 tracing::warn!(
78 "[Host] {} embedded file(s) are overridden on disk: {}. \
79 An override older than the current build stays in effect until it is \
80 updated or deleted.",
81 shadowed.len(),
82 shadowed.join(", "),
83 );
84 }
85}
86
87fn with_embedded_assets(
93 source: Option<SpaSource>,
94 assets: Option<EmbeddedAssets>,
95) -> Option<SpaSource> {
96 let Some(assets) = assets else {
97 return source;
98 };
99 let overlay_root = match source.as_ref() {
100 Some(existing) => existing.disk_root().to_string(),
101 None => assets.root().to_string(),
102 };
103 Some(SpaSource::Embedded {
104 assets,
105 overlay_root,
106 })
107}
108
109type ShutdownSignal = Arc<tokio::sync::watch::Sender<bool>>;
116
117pub struct Host {
118 #[allow(dead_code)]
119 provider: Arc<ServiceProvider>,
120 dispatch_runtime: Arc<DispatchRuntime>,
121 pub options: AppOptions,
122 endpoints: Arc<dyn IEndpointCatalog>,
124 localizer: Arc<dyn IStringLocalizer>,
126 body_limits: BodyLimits,
128 pipeline: Arc<MiddlewarePipeline>,
129 #[allow(dead_code)]
131 router: Arc<Router>,
132 router_handler: HandlerFn,
134 mode: AppMode,
135 #[allow(dead_code)]
137 spa_source: Option<SpaSource>,
138 shutdown: ShutdownSignal,
139 hosted_services: Vec<Arc<dyn IHostedService>>,
142}
143
144type MiddlewareFactory = Box<dyn Fn() -> Arc<dyn IMiddleware> + Send + Sync>;
146
147struct MiddlewareSpec {
149 slot: webx_core::middleware::MiddlewareSlot,
150 factory: MiddlewareFactory,
151}
152
153#[allow(clippy::type_complexity)]
154pub struct HostBuilder {
155 service_configs: Vec<Box<dyn FnOnce(ServiceCollection) -> ServiceCollection + Send>>,
156 mode: AppMode,
157 spa_source: Option<SpaSource>,
158 spa_disabled: bool,
159 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
160 cors_config: Option<CorsConfig>,
161 auth_enabled: bool,
162 localization: Option<LocalizationOptions>,
163 health_registry: Arc<HealthCheckRegistry>,
164 middleware_specs: Vec<MiddlewareSpec>,
167 lenient_scope_validation: bool,
170}
171
172struct PreparedHost {
178 builder: HostBuilder,
179 svc: ServiceCollection,
180 provider_options: rust_dix::ServiceProviderOptions,
181 options: AppOptions,
182 endpoints: Arc<dyn IEndpointCatalog>,
183 localizer: Arc<dyn IStringLocalizer>,
184 handler_cache: Arc<webx_core::route::scan::HandlerCache>,
185}
186
187#[allow(clippy::type_complexity)]
188pub struct HostAppBuilder {
189 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
190}
191
192impl HostAppBuilder {
193 fn new() -> Self {
194 Self {
195 options_modifiers: Vec::new(),
196 }
197 }
198
199 #[allow(non_snake_case)]
200 pub fn useOptions<F>(&mut self, f: F)
201 where
202 F: FnOnce(&mut AppOptions) + Send + 'static,
203 {
204 self.options_modifiers.push(Box::new(f));
205 }
206}
207
208impl HostBuilder {
209 pub fn new() -> Self {
210 Self {
211 service_configs: Vec::new(),
212 mode: AppMode::from_env(),
215 spa_source: None,
216 spa_disabled: false,
217 options_modifiers: Vec::new(),
218 cors_config: None,
219 auth_enabled: false,
220 localization: None,
221 health_registry: HealthCheckRegistry::new(),
222 middleware_specs: Vec::new(),
223 lenient_scope_validation: false,
224 }
225 }
226
227 pub fn register<F>(mut self, f: F) -> Self
228 where
229 F: FnOnce(ServiceCollection) -> ServiceCollection + Send + 'static,
230 {
231 self.service_configs.push(Box::new(f));
232 self
233 }
234
235 pub fn configure<F>(mut self, f: F) -> Self
236 where
237 F: FnOnce(&mut HostAppBuilder) + Send + 'static,
238 {
239 let mut builder = HostAppBuilder::new();
240 f(&mut builder);
241 self.options_modifiers
242 .append(&mut builder.options_modifiers);
243 self
244 }
245
246 pub fn mode(mut self, mode: AppMode) -> Self {
247 self.mode = mode;
248 self
249 }
250
251 pub fn lenient_scope_validation(mut self) -> Self {
261 self.lenient_scope_validation = true;
262 self
263 }
264
265 pub fn add_options<T>(self, section: &str) -> Self
277 where
278 T: for<'de> serde::Deserialize<'de> + Default + Send + Sync + 'static,
279 {
280 let mode = self.mode;
281 let section = section.to_string();
282 self.register(move |svc| {
283 let appsettings = config::load_appsettings(mode).unwrap_or_default();
284 let bound: T = config::bind_config(&appsettings, §ion);
285 tracing::info!(
286 "[Host] add_options<{}>(\"{}\") registered",
287 std::any::type_name::<T>(),
288 section
289 );
290 svc.instance(Arc::new(bound))
291 })
292 }
293
294 pub fn use_spa(mut self, source: impl Into<SpaSource>) -> Self {
309 self.spa_source = Some(match (self.spa_source.take(), source.into()) {
310 (Some(SpaSource::Embedded { assets, .. }), SpaSource::Disk(root)) => {
313 SpaSource::Embedded {
314 assets,
315 overlay_root: root,
316 }
317 }
318 (_, incoming) => incoming,
319 });
320 self
321 }
322
323 pub fn no_spa(mut self) -> Self {
329 self.spa_disabled = true;
330 self
331 }
332
333 pub fn use_cors(mut self, config: CorsConfig) -> Self {
334 self.cors_config = Some(config);
335 self
336 }
337
338 pub fn add_authentication(mut self) -> Self {
352 self.auth_enabled = true;
353 self
354 }
355
356 pub fn add_localization(mut self) -> Self {
372 self.localization = Some(LocalizationOptions::new());
373 self
374 }
375
376 pub fn add_localization_with(mut self, options: LocalizationOptions) -> Self {
392 self.localization = Some(options);
393 self
394 }
395
396 pub fn add_memory_cache(mut self) -> Self {
408 let cache = Arc::new(MemoryCache::new());
409 self.service_configs
410 .push(Box::new(move |svc| svc.instance(cache)));
411 self
412 }
413
414 pub fn add_memory_cache_with(mut self, max_entries: usize) -> Self {
416 let cache = Arc::new(MemoryCache::new().with_max_entries(max_entries));
417 self.service_configs
418 .push(Box::new(move |svc| svc.instance(Arc::clone(&cache))));
419 self
420 }
421
422 pub fn use_middleware<T: IMiddleware + Default + 'static>(self) -> Self {
435 self.use_middleware_at::<T>(webx_core::middleware::MiddlewareSlot::AfterAuth)
436 }
437
438 pub fn use_middleware_at<T: IMiddleware + Default + 'static>(
440 mut self,
441 slot: webx_core::middleware::MiddlewareSlot,
442 ) -> Self {
443 self.middleware_specs.push(MiddlewareSpec {
444 slot,
445 factory: Box::new(|| Arc::new(T::default()) as Arc<dyn IMiddleware>),
446 });
447 self
448 }
449
450 pub fn use_middleware_with<F>(self, factory: F) -> Self
463 where
464 F: Fn() -> Arc<dyn IMiddleware> + Send + Sync + 'static,
465 {
466 self.use_middleware_with_at(webx_core::middleware::MiddlewareSlot::AfterAuth, factory)
467 }
468
469 pub fn use_middleware_with_at<F>(
471 mut self,
472 slot: webx_core::middleware::MiddlewareSlot,
473 factory: F,
474 ) -> Self
475 where
476 F: Fn() -> Arc<dyn IMiddleware> + Send + Sync + 'static,
477 {
478 self.middleware_specs.push(MiddlewareSpec {
479 slot,
480 factory: Box::new(factory),
481 });
482 self
483 }
484
485 pub fn add_health_check<F>(self, name: impl Into<String>, check: F) -> Self
498 where
499 F: Fn() -> HealthStatus + Send + Sync + 'static,
500 {
501 self.health_registry.register(name, Arc::new(check));
502 self
503 }
504
505 pub fn try_build(self) -> Result<Host> {
521 let prepared = self.prepare()?;
522 let PreparedHost {
523 builder,
524 svc,
525 provider_options,
526 options,
527 endpoints,
528 localizer,
529 handler_cache,
530 } = prepared;
531 let provider = svc
532 .build_with(provider_options)
533 .map_err(build_provider_error)?;
534 builder.assemble(provider, handler_cache, options, endpoints, localizer)
535 }
536
537 pub async fn try_build_async(self) -> Result<Host> {
551 let prepared = self.prepare()?;
552 let PreparedHost {
553 builder,
554 svc,
555 provider_options,
556 options,
557 endpoints,
558 localizer,
559 handler_cache,
560 } = prepared;
561 let provider = svc
562 .build_async_with(provider_options)
563 .await
564 .map_err(build_provider_error)?;
565 builder.assemble(provider, handler_cache, options, endpoints, localizer)
566 }
567
568 fn prepare(mut self) -> Result<PreparedHost> {
569 let env_filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into());
572 if self.mode == AppMode::Development {
573 let _ = tracing_subscriber::fmt()
574 .with_env_filter(env_filter)
575 .without_time()
576 .with_target(false)
577 .try_init();
578 } else {
579 let _ = tracing_subscriber::fmt()
580 .json()
581 .with_env_filter(env_filter)
582 .try_init();
583 }
584
585 let appsettings =
588 config::load_appsettings(self.mode).unwrap_or_else(|| serde_json::json!({}));
589 let mut options: AppOptions = config::bind_root(&appsettings);
590 for modifier in std::mem::take(&mut self.options_modifiers) {
591 modifier(&mut options);
592 }
593
594 let default_culture = if options.culture.default.trim().is_empty() {
595 "en".to_string()
596 } else {
597 options.culture.default.clone()
598 };
599 let localization = self
600 .localization
601 .take()
602 .unwrap_or_else(LocalizationOptions::none);
603 let localizer: Arc<dyn IStringLocalizer> = Arc::new(
604 StringLocalizer::new(default_culture, &localization)
605 .map_err(|error| Error::Di(format!("Invalid localization resources: {error}")))?,
606 );
607 let endpoints: Arc<dyn IEndpointCatalog> = Arc::new(EndpointCatalog::from_inventory());
608
609 let mut svc = ServiceCollection::from_injected();
610 for cfg in std::mem::take(&mut self.service_configs) {
611 svc = cfg(svc);
612 }
613
614 let handler_cache = Arc::new(webx_core::route::scan::HandlerCache::build());
617 svc = svc.instance(Arc::clone(&handler_cache));
618
619 let health_registry = Arc::clone(&self.health_registry);
622 svc = svc.try_add(move |_| Arc::clone(&health_registry));
623
624 let endpoints_for_di = Arc::clone(&endpoints);
626 svc = svc.try_add_singleton::<dyn IEndpointCatalog>(move |_| Arc::clone(&endpoints_for_di));
627 let localizer_for_di = Arc::clone(&localizer);
628 svc = svc.try_add_singleton::<dyn IStringLocalizer>(move |_| Arc::clone(&localizer_for_di));
629
630 let provider_options = if self.lenient_scope_validation {
631 rust_dix::ServiceProviderOptions::lenient()
632 } else {
633 rust_dix::ServiceProviderOptions::default()
634 };
635 Ok(PreparedHost {
636 builder: self,
637 svc,
638 provider_options,
639 options,
640 endpoints,
641 localizer,
642 handler_cache,
643 })
644 }
645
646 #[allow(clippy::too_many_arguments)]
647 fn assemble(
648 self,
649 provider: Arc<ServiceProvider>,
650 handler_cache: Arc<webx_core::route::scan::HandlerCache>,
651 options: AppOptions,
652 endpoints: Arc<dyn IEndpointCatalog>,
653 localizer: Arc<dyn IStringLocalizer>,
654 ) -> Result<Host> {
655 let dispatch_runtime = Arc::new(DispatchRuntime::new(Arc::clone(&provider), handler_cache));
657
658 let body_limits = BodyLimits::from_options(&options);
660 body_limits.apply_spool_root();
661 tracing::info!(
662 "[Host] Uploads: max request {} bytes, max file {} bytes, spool >{} bytes to {}",
663 body_limits.multipart_body,
664 body_limits.file,
665 body_limits.memory_threshold,
666 body_limits.spool_dir.display()
667 );
668
669 let http_metrics = HttpMetrics::new();
670
671 let mut pipeline = MiddlewarePipeline::new();
672
673 pipeline.add_middleware(Arc::new(
683 crate::security_headers::SecurityHeadersMiddleware::new(),
684 ));
685 pipeline.add_middleware(Arc::new(crate::request_id::RequestIdMiddleware::new()));
686
687 for spec in self
688 .middleware_specs
689 .iter()
690 .filter(|spec| spec.slot == webx_core::middleware::MiddlewareSlot::BeforeCors)
691 {
692 pipeline.add_middleware((spec.factory)());
693 }
694
695 let cors = self.cors_config.unwrap_or_else(|| {
696 let cs = &options.cors;
697 CorsConfig {
698 origins: cs.origins.clone(),
699 methods: cs.methods.clone(),
700 headers: cs.headers.clone(),
701 expose_headers: cs.expose_headers.clone(),
702 allow_credentials: cs.allow_credentials,
703 max_age: cs.max_age,
704 }
705 });
706 pipeline.add_middleware(Arc::new(CorsMiddleware::new(cors)));
707
708 if options.metrics.enabled {
709 tracing::info!("[Host] Metrics enabled at GET /metrics");
710 pipeline.add_middleware(Arc::new(MetricsMiddleware::new(Arc::clone(&http_metrics))));
711 }
712
713 if options.rate_limit.enabled {
714 tracing::info!(
715 "[Host] RateLimit enabled: {} req/s, burst {}, max_ips {}",
716 options.rate_limit.requests_per_second,
717 options.rate_limit.burst_size,
718 options.rate_limit.max_tracked_ips
719 );
720 pipeline.add_middleware(Arc::new(RateLimitMiddleware::from_config(
721 &options.rate_limit,
722 )));
723 }
724
725 if self.auth_enabled {
726 let secret = options.jwt.secret.clone();
727 if !secret.is_empty() {
728 let jwt_auth = Arc::new(JwtAuth::new(
729 DecodingKey::from_secret(secret.as_bytes()),
730 Validation::default(),
731 ));
732 pipeline.add_middleware(Arc::new(jwt_middleware(jwt_auth)));
733 init_jwt_secret(&secret);
734 } else {
735 tracing::warn!(
736 "add_authentication() enabled but no JWT secret configured. Set Jwt.Secret in appsettings.json, JWT_SECRET, or APP__Jwt__Secret env var."
737 );
738 }
739 }
740
741 for spec in self
745 .middleware_specs
746 .iter()
747 .filter(|spec| spec.slot == webx_core::middleware::MiddlewareSlot::AfterAuth)
748 {
749 pipeline.add_middleware((spec.factory)());
750 }
751 for mw in provider.get_all::<dyn IMiddleware>() {
752 pipeline.add_middleware(mw);
753 }
754
755 let spa_source = if self.spa_disabled {
763 None
764 } else {
765 let base = self.spa_source.clone().or_else(|| {
766 let candidate = webx_core::paths::app_base().join("wwwroot");
767 if candidate.is_dir() {
768 tracing::info!("[Host] Auto-detected SPA root: {}", candidate.display());
769 Some(SpaSource::Disk(candidate.to_string_lossy().into_owned()))
770 } else {
771 None
772 }
773 });
774 with_embedded_assets(base, EmbeddedAssets::registered().copied())
775 };
776 let mut spa_fallback: Option<Arc<SpaMiddleware>> = None;
777 if let Some(ref source) = spa_source {
778 let spa = Arc::new(SpaMiddleware::from_source(source.clone()));
779 log_spa_source(&spa);
780 pipeline.add_middleware(Arc::clone(&spa) as Arc<dyn IMiddleware>);
781 spa_fallback = Some(spa);
785 }
786
787 for spec in self
789 .middleware_specs
790 .iter()
791 .filter(|spec| spec.slot == webx_core::middleware::MiddlewareSlot::AfterSpa)
792 {
793 pipeline.add_middleware((spec.factory)());
794 }
795
796 let pipeline = Arc::new(pipeline);
797
798 if self.mode == AppMode::Production
800 && self.auth_enabled
801 && is_weak_jwt_secret(&options.jwt.secret)
802 {
803 return Err(Error::Di(
804 "Production requires a strong JWT secret. Set APP__Jwt__Secret or JWT_SECRET env var (min 32 chars, not a placeholder)."
805 .to_string(),
806 ));
807 }
808
809 if self.mode == AppMode::Production && options.cors.origins.iter().any(|o| o == "*") {
811 return Err(Error::Di(
812 "Production forbids CORS origin '*'. Set explicit origins via Cors.Origins or APP__Cors__Origins."
813 .to_string(),
814 ));
815 }
816
817 let mut router = Router::new();
818 let mut route_count = 0usize;
819
820 crate::diagnostics::validate_route_configuration()?;
821
822 let mut dispatch_map: std::collections::HashMap<
824 &'static str,
825 webx_core::route::scan::RouteDispatchFn,
826 > = std::collections::HashMap::new();
827
828 for dispatch in inventory::iter::<webx_core::route::scan::RouteDispatch> {
829 dispatch_map.insert(dispatch.handler_type, dispatch.dispatch);
830 }
831
832 for entry in inventory::iter::<RouteEntry> {
833 route_count += 1;
834 let stub = Arc::new(StubEndpoint {
835 method: entry.method.as_str(),
836 path: entry.path,
837 handler_type: entry.handler_type,
838 dispatch_fn: dispatch_map.get(entry.handler_type).copied(),
839 authorization: entry.authorization,
840 authorizers: collect_authorizers(provider.as_ref()),
841 });
842 router.register(entry.method, entry.path, stub);
843 }
844
845 let openapi_spec = generate_openapi_spec(
846 "Rust WebX API",
847 env!("CARGO_PKG_VERSION"),
848 localizer.as_ref(),
849 );
850 if self.mode == AppMode::Development {
851 let openapi_bytes = serde_json::to_vec(&openapi_spec).unwrap_or_default();
852 router.register(
853 HttpMethod::Get,
854 "/api/openapi.json",
855 Arc::new(StaticJsonEndpoint::new(openapi_bytes)),
856 );
857 router.register(
858 HttpMethod::Get,
859 "/api/openapi.html",
860 Arc::new(StaticHtmlEndpoint { body: APIUI_HTML }),
861 );
862 }
863
864 let health_registry = Arc::clone(&self.health_registry);
867 let health_endpoint: Arc<dyn IEndpoint> =
868 Arc::new(HealthCheckEndpoint::new(Arc::clone(&health_registry)));
869 router.register(HttpMethod::Get, "/health", Arc::clone(&health_endpoint));
870 router.register(HttpMethod::Get, "/healthz", health_endpoint);
871
872 let live_body =
874 serde_json::to_vec(&serde_json::json!({"status":"pass"})).unwrap_or_default();
875 router.register(
876 HttpMethod::Get,
877 "/health/live",
878 Arc::new(StaticJsonEndpoint {
879 body: live_body,
880 content_type: "application/health+json",
881 }),
882 );
883
884 router.register(
886 HttpMethod::Get,
887 "/health/ready",
888 Arc::new(HealthCheckEndpoint::new(health_registry)),
889 );
890
891 if options.metrics.enabled {
892 router.register(
893 HttpMethod::Get,
894 "/metrics",
895 Arc::new(MetricsEndpoint::new(http_metrics)),
896 );
897 }
898
899 if self.mode == AppMode::Development {
900 let version = env!("CARGO_PKG_VERSION");
901 tracing::info!("");
902 tracing::info!(" ----------------------------------------------------------------");
903 tracing::info!(" Rust WebX Framework v{}", version);
904 tracing::info!(" ----------------------------------------------------------------");
905 tracing::info!(" App: {}", options.app.name);
906 tracing::info!(" CORS: enabled");
907 if let Some(source) = spa_source.as_ref() {
908 tracing::info!(" SPA Root: {}", source.disk_root());
909 if let Some(assets) = source.embedded() {
910 tracing::info!(
911 " Embedded: {} files, {:.1} MiB from {:.1} MiB (built from {})",
912 assets.len(),
913 assets.total_bytes() as f64 / (1024.0 * 1024.0),
914 assets.total_raw_bytes() as f64 / (1024.0 * 1024.0),
915 assets.root(),
916 );
917 }
918 }
919 if route_count > 0 {
920 tracing::info!(" Routes: {} registered", route_count);
921 }
922 crate::diagnostics::log_startup_diagnostics();
923 let banner_urls = if options.app.urls.is_empty() {
924 vec!["http://localhost:5000".to_string()]
925 } else {
926 options
927 .app
928 .urls
929 .iter()
930 .map(|u| u.replace("0.0.0.0", "localhost"))
931 .collect::<Vec<_>>()
932 };
933 for url in &banner_urls {
934 tracing::info!(" OpenAPI: {}/api/openapi.html", url);
935 tracing::info!(" OpenAPI: {}/api/openapi.json", url);
936 }
937 tracing::info!(" ----------------------------------------------------------------");
938 tracing::info!("");
939 } else if route_count > 0 {
940 tracing::info!("{} route(s) registered", route_count);
941 }
942
943 let router = Arc::new(router);
944 let router_handler = make_router_handler(Arc::clone(&router), spa_fallback);
945
946 let hosted_services: Vec<Arc<dyn IHostedService>> =
949 provider.get_all::<dyn IHostedService>();
950
951 Ok(Host {
952 provider,
953 dispatch_runtime,
954 options,
955 endpoints,
956 localizer,
957 body_limits,
958 pipeline,
959 router,
960 router_handler,
961 mode: self.mode,
962 spa_source,
963 shutdown: Arc::new(tokio::sync::watch::channel(false).0),
964 hosted_services,
965 })
966 }
967
968 pub fn build(self) -> Host {
975 match self.try_build() {
976 Ok(host) => host,
977 Err(error) => panic!("{}", build_failure_message(&error)),
980 }
981 }
982}
983
984fn build_provider_error(error: rust_dix::RdiError) -> Error {
986 Error::Di(format!(
987 "Failed to build ServiceProvider: {}. Check your DI registrations.",
988 error
989 ))
990}
991
992fn build_failure_message(error: &Error) -> String {
995 match error {
996 Error::Di(message) => message.clone(),
997 other => other.to_string(),
998 }
999}
1000
1001pub struct ServerHandle {
1003 shutdown: ShutdownSignal,
1004}
1005
1006impl ServerHandle {
1007 pub fn shutdown(self) {
1013 trigger_shutdown(&self.shutdown);
1014 }
1015}
1016
1017pub struct Server {
1029 host: Host,
1030}
1031
1032impl Server {
1033 pub async fn run(self) -> Result<()> {
1035 self.host.run().await
1036 }
1037
1038 pub async fn run_at(self, addr: &str) -> Result<()> {
1040 self.host.run_at(addr).await
1041 }
1042
1043 pub fn handle(&self) -> ServerHandle {
1045 self.host.server_handle()
1046 }
1047}
1048
1049impl Default for HostBuilder {
1050 fn default() -> Self {
1051 Self::new()
1052 }
1053}
1054
1055impl Host {
1056 pub fn builder() -> HostBuilder {
1057 HostBuilder::new()
1058 }
1059
1060 pub fn options(&self) -> &AppOptions {
1061 &self.options
1062 }
1063
1064 pub fn provider(&self) -> &Arc<ServiceProvider> {
1066 &self.provider
1067 }
1068
1069 fn dispose_di(&self) {
1076 self.provider.dispose();
1077 tracing::debug!("[Host] DI provider disposed");
1078 }
1079
1080 pub fn dispatch_runtime(&self) -> &Arc<DispatchRuntime> {
1082 &self.dispatch_runtime
1083 }
1084
1085 pub fn endpoints(&self) -> &dyn IEndpointCatalog {
1091 &*self.endpoints
1092 }
1093
1094 pub fn localizer(&self) -> &dyn IStringLocalizer {
1096 &*self.localizer
1097 }
1098
1099 pub async fn run(&self) -> Result<()> {
1113 let urls = if self.options.app.urls.is_empty() {
1114 vec!["http://0.0.0.0:5000".to_string()]
1115 } else {
1116 self.options.app.urls.clone()
1117 };
1118
1119 let mut http_addrs: Vec<String> = Vec::new();
1120 let mut https_addrs: Vec<String> = Vec::new();
1121
1122 for url in &urls {
1123 let (scheme, addr) = parse_url(url)?;
1124 match scheme {
1125 "http" => http_addrs.push(addr),
1126 "https" => https_addrs.push(addr),
1127 other => {
1128 return Err(webx_core::error::Error::Http(format!(
1129 "Unsupported URL scheme '{}' in '{}'",
1130 other, url
1131 )))
1132 }
1133 }
1134 }
1135
1136 let acceptor = if !https_addrs.is_empty() {
1137 let tls = &self.options.tls;
1138 if tls.cert_path.is_empty() || tls.key_path.is_empty() {
1139 return Err(webx_core::error::Error::Http(
1140 "HTTPS URLs require Tls.CertPath and Tls.KeyPath".into(),
1141 ));
1142 }
1143 Some(build_tls_acceptor(&tls.cert_path, &tls.key_path)?)
1144 } else {
1145 None
1146 };
1147
1148 let mut http_listeners = Vec::with_capacity(http_addrs.len());
1152 for addr in &http_addrs {
1153 http_listeners.push(bind_listener(addr, "HTTP").await?);
1154 }
1155 let mut https_listeners = Vec::with_capacity(https_addrs.len());
1156 for addr in &https_addrs {
1157 https_listeners.push(bind_listener(addr, "HTTPS").await?);
1158 }
1159
1160 if let Err(err) = start_hosted_services(&self.hosted_services).await {
1161 self.dispose_di();
1162 return Err(err);
1163 }
1164
1165 if self.mode == AppMode::Development {
1167 tracing::info!("");
1168 for url in &urls {
1169 let display_url = url.replace("0.0.0.0", "localhost");
1170 tracing::info!(" Listening on {}", display_url);
1171 }
1172 } else {
1173 tracing::info!("Listening on {} url(s)", urls.len());
1174 }
1175
1176 install_shutdown_handler(Arc::clone(&self.shutdown));
1177 spawn_async_shutdown_listener(Arc::clone(&self.shutdown));
1178
1179 let mut handles = Vec::new();
1180 let pipeline = Arc::clone(&self.pipeline);
1181 let router_handler = self.router_handler.clone();
1182 let dispatch_runtime = Arc::clone(&self.dispatch_runtime);
1183 let mode = self.mode;
1184 let body_limits = self.body_limits.clone();
1185 let max_connections = self.options.app.max_connections;
1186
1187 for (addr, listener) in http_addrs.into_iter().zip(http_listeners) {
1188 handles.push(tokio::spawn(serve_http(
1189 addr,
1190 listener,
1191 self.shutdown.subscribe(),
1192 Arc::clone(&pipeline),
1193 router_handler.clone(),
1194 Arc::clone(&dispatch_runtime),
1195 mode,
1196 body_limits.clone(),
1197 max_connections,
1198 )));
1199 }
1200
1201 if let Some(ref tls_acceptor) = acceptor {
1202 for (addr, listener) in https_addrs.into_iter().zip(https_listeners) {
1203 handles.push(tokio::spawn(serve_https(
1204 addr,
1205 tls_acceptor.clone(),
1206 listener,
1207 self.shutdown.subscribe(),
1208 Arc::clone(&pipeline),
1209 router_handler.clone(),
1210 Arc::clone(&dispatch_runtime),
1211 mode,
1212 body_limits.clone(),
1213 max_connections,
1214 )));
1215 }
1216 }
1217
1218 let mut run_error: Option<webx_core::error::Error> = None;
1221 for h in handles {
1222 if let Err(join_err) = h.await {
1223 if run_error.is_none() {
1224 run_error = Some(webx_core::error::Error::Internal(format!(
1225 "a listener task failed: {join_err}"
1226 )));
1227 }
1228 }
1229 }
1230
1231 stop_hosted_services(&self.hosted_services).await;
1232 self.dispose_di();
1233
1234 match run_error {
1235 Some(err) => Err(err),
1236 None => Ok(()),
1237 }
1238 }
1239
1240 pub async fn run_at(&self, addr: &str) -> Result<()> {
1242 let listener = bind_listener(addr, "HTTP").await?;
1243 if let Err(err) = start_hosted_services(&self.hosted_services).await {
1244 self.dispose_di();
1245 return Err(err);
1246 }
1247
1248 install_shutdown_handler(Arc::clone(&self.shutdown));
1249 spawn_async_shutdown_listener(Arc::clone(&self.shutdown));
1250 serve_http(
1251 addr.to_string(),
1252 listener,
1253 self.shutdown.subscribe(),
1254 Arc::clone(&self.pipeline),
1255 self.router_handler.clone(),
1256 Arc::clone(&self.dispatch_runtime),
1257 self.mode,
1258 self.body_limits.clone(),
1259 self.options.app.max_connections,
1260 )
1261 .await;
1262
1263 stop_hosted_services(&self.hosted_services).await;
1264 self.dispose_di();
1265 Ok(())
1266 }
1267
1268 pub fn server_handle(&self) -> ServerHandle {
1279 ServerHandle {
1280 shutdown: Arc::clone(&self.shutdown),
1281 }
1282 }
1283
1284 pub fn into_server(self) -> Server {
1289 Server { host: self }
1290 }
1291}
1292
1293#[async_trait::async_trait]
1294impl IHost for Host {
1295 async fn run(&self, addr: &str) -> Result<()> {
1296 self.run_at(addr).await
1297 }
1298 async fn stop(&self) -> Result<()> {
1299 tracing::info!("Stop requested.");
1300 trigger_shutdown(&self.shutdown);
1301 Ok(())
1302 }
1303}
1304
1305fn make_router_handler(router: Arc<Router>, spa_fallback: Option<Arc<SpaMiddleware>>) -> HandlerFn {
1306 Arc::new(move |ctx: &mut dyn IHttpContext| {
1307 let router = Arc::clone(&router);
1308 let spa_fallback = spa_fallback.clone();
1309 Box::pin(async move {
1310 if let Some(err) = ctx.take_early_error() {
1314 return Err(err);
1315 }
1316 match router.match_route(ctx).await? {
1317 Some((endpoint, params, pattern)) => {
1318 drop(router);
1319 for (key, value) in params {
1320 ctx.request_mut().route_params_mut().insert(key, value);
1321 }
1322 *ctx.request_mut().route_pattern_mut() = Some(pattern);
1323 endpoint.handle(ctx).await
1324 }
1325 None => {
1326 let path_exists = router.path_exists(ctx.request().path());
1327 let allowed = if path_exists {
1328 router.allowed_methods(ctx.request().path())
1329 } else {
1330 Vec::new()
1331 };
1332 drop(router);
1333 if !ctx.response().has_body() {
1336 if let Some(spa) = spa_fallback {
1339 if spa.try_index_fallback(ctx).await? {
1340 return Ok(());
1341 }
1342 }
1343 if path_exists {
1344 if ctx.request().method().eq_ignore_ascii_case("OPTIONS") {
1345 ctx.response_mut().set_status(204);
1350 ctx.response_mut().set_header("allow", &allowed.join(", "));
1351 } else {
1352 write_method_not_allowed(ctx, &allowed).await;
1354 }
1355 } else {
1356 write_plain_error(ctx, 404, "Not Found").await;
1357 }
1358 }
1359 Ok(())
1360 }
1361 }
1362 })
1363 })
1364}
1365
1366async fn handle_request(
1367 req: Request<Incoming>,
1368 pipeline: Arc<MiddlewarePipeline>,
1369 router_handler: HandlerFn,
1370 dispatch_runtime: Arc<DispatchRuntime>,
1371 body_limits: BodyLimits,
1372 remote_addr: Option<std::net::SocketAddr>,
1373) -> std::result::Result<hyper::Response<RespBody>, std::convert::Infallible> {
1374 let mut ctx = HttpContext::new(req, body_limits, remote_addr, Arc::clone(&dispatch_runtime));
1375
1376 let result = pipeline.execute(&mut ctx, router_handler).await;
1377 if let Err(e) = result {
1378 match e {
1379 webx_core::error::Error::Problem(problem) => {
1383 write_problem_response(&mut ctx, *problem).await
1384 }
1385 other => {
1386 write_framework_error(&mut ctx, &other).await;
1387 }
1388 }
1389 }
1390
1391 Ok(ctx.into_response().await)
1392}
1393
1394async fn write_framework_error(ctx: &mut dyn IHttpContext, error: &webx_core::error::Error) {
1406 let status = error.status_code();
1407 if status == 401 {
1408 write_unauthorized(
1409 ctx,
1410 error.public_detail().unwrap_or("authentication required"),
1411 )
1412 .await;
1413 } else if status >= 500 {
1414 tracing::error!(status, error = %error, "unhandled server error");
1415 write_problem_response(ctx, build_problem(status, "an unexpected error occurred")).await;
1416 } else {
1417 write_problem_response(
1418 ctx,
1419 build_problem(status, error.public_detail().unwrap_or("request failed")),
1420 )
1421 .await;
1422 }
1423}
1424
1425async fn write_plain_error(ctx: &mut dyn IHttpContext, status: u16, message: &str) {
1428 write_problem_response(ctx, build_problem(status, message)).await;
1429}
1430
1431async fn write_method_not_allowed(ctx: &mut dyn IHttpContext, allowed: &[&str]) {
1433 let allow = allowed.join(", ");
1434 ctx.response_mut().set_header("allow", &allow);
1435 write_plain_error(ctx, 405, "Method Not Allowed").await;
1436}
1437
1438fn trigger_shutdown(shutdown: &ShutdownSignal) {
1448 shutdown.send_replace(true);
1453}
1454
1455async fn wait_for_shutdown(shutdown: &mut tokio::sync::watch::Receiver<bool>) {
1460 if *shutdown.borrow() {
1461 return;
1462 }
1463 loop {
1464 if shutdown.changed().await.is_err() {
1465 return;
1466 }
1467 if *shutdown.borrow() {
1468 return;
1469 }
1470 }
1471}
1472
1473fn install_shutdown_handler(shutdown: ShutdownSignal) {
1484 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1485 use std::sync::{Mutex, OnceLock, Weak};
1486
1487 static HANDLERS: OnceLock<Mutex<Vec<Weak<tokio::sync::watch::Sender<bool>>>>> = OnceLock::new();
1488 static CTRL_COUNT: AtomicUsize = AtomicUsize::new(0);
1489 static INSTALLED: AtomicBool = AtomicBool::new(false);
1490
1491 let handlers = HANDLERS.get_or_init(|| Mutex::new(Vec::new()));
1492 {
1493 let mut registered = handlers
1494 .lock()
1495 .unwrap_or_else(|poisoned| poisoned.into_inner());
1496 registered.retain(|weak| weak.strong_count() > 0);
1498 registered.push(Arc::downgrade(&shutdown));
1499 }
1500
1501 if INSTALLED.swap(true, Ordering::SeqCst) {
1505 return;
1506 }
1507
1508 if let Err(e) = ctrlc::set_handler(move || {
1509 let n = CTRL_COUNT.fetch_add(1, Ordering::SeqCst);
1510 if n == 0 {
1511 tracing::info!("Shutdown signal received, draining connections...");
1512 let registered = handlers
1513 .lock()
1514 .unwrap_or_else(|poisoned| poisoned.into_inner());
1515 for weak in registered.iter() {
1516 if let Some(shutdown) = weak.upgrade() {
1517 trigger_shutdown(&shutdown);
1518 }
1519 }
1520 } else {
1521 tracing::warn!("Second interrupt — force exiting.");
1522 std::process::exit(130);
1523 }
1524 }) {
1525 tracing::warn!("Could not install Ctrl+C handler: {}", e);
1526 }
1527}
1528
1529fn spawn_async_shutdown_listener(shutdown: ShutdownSignal) {
1531 tokio::spawn(async move {
1532 wait_for_shutdown_signal().await;
1533 tracing::info!("Async shutdown signal received, draining connections...");
1534 trigger_shutdown(&shutdown);
1535 });
1536}
1537
1538async fn wait_for_shutdown_signal() {
1539 #[cfg(unix)]
1540 {
1541 use tokio::signal::unix::{signal, SignalKind};
1542 let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
1543 tokio::select! {
1544 res = tokio::signal::ctrl_c() => {
1545 if let Err(e) = res {
1546 tracing::warn!("Ctrl+C listener error: {}", e);
1547 }
1548 }
1549 _ = sigterm.recv() => {}
1550 }
1551 }
1552 #[cfg(not(unix))]
1553 {
1554 if let Err(e) = tokio::signal::ctrl_c().await {
1555 tracing::warn!("Ctrl+C listener error: {}", e);
1556 }
1557 }
1558}
1559
1560const HOSTED_START_TIMEOUT_SECS: u64 = 30;
1562const HOSTED_STOP_TIMEOUT_SECS: u64 = 30;
1564
1565async fn start_hosted_services(services: &[Arc<dyn IHostedService>]) -> Result<()> {
1566 if services.is_empty() {
1567 return Ok(());
1568 }
1569 tracing::info!("Starting {} hosted service(s)...", services.len());
1570 let mut started = 0usize;
1571 let mut failure: Option<webx_core::error::Error> = None;
1572
1573 for svc in services {
1574 let attempt = tokio::time::timeout(
1578 std::time::Duration::from_secs(HOSTED_START_TIMEOUT_SECS),
1579 svc.start(),
1580 )
1581 .await;
1582 match attempt {
1583 Ok(Ok(())) => started += 1,
1584 Ok(Err(err)) => {
1585 failure = Some(err);
1586 break;
1587 }
1588 Err(_) => {
1589 failure = Some(webx_core::error::Error::Internal(format!(
1590 "hosted service start timed out after {HOSTED_START_TIMEOUT_SECS}s"
1591 )));
1592 break;
1593 }
1594 }
1595 }
1596
1597 match failure {
1598 None => {
1599 tracing::info!("All {started} hosted service(s) started.");
1600 Ok(())
1601 }
1602 Some(err) => {
1603 for (index, svc) in services[..started].iter().enumerate().rev() {
1606 stop_hosted_service(index, svc).await;
1607 }
1608 Err(err)
1609 }
1610 }
1611}
1612
1613async fn stop_hosted_services(services: &[Arc<dyn IHostedService>]) {
1614 if services.is_empty() {
1615 return;
1616 }
1617 tracing::info!("Stopping {} hosted service(s)...", services.len());
1618 for (index, svc) in services.iter().enumerate().rev() {
1619 stop_hosted_service(index, svc).await;
1620 }
1621 tracing::info!("All hosted services stopped.");
1622}
1623
1624async fn stop_hosted_service(index: usize, svc: &Arc<dyn IHostedService>) {
1626 match tokio::time::timeout(
1627 std::time::Duration::from_secs(HOSTED_STOP_TIMEOUT_SECS),
1628 svc.stop(),
1629 )
1630 .await
1631 {
1632 Ok(Ok(())) => {}
1633 Ok(Err(err)) => {
1634 tracing::warn!(service_index = index, error = %err, "hosted service stop error")
1635 }
1636 Err(_) => tracing::error!(
1637 service_index = index,
1638 timeout_secs = HOSTED_STOP_TIMEOUT_SECS,
1639 "hosted service stop timed out"
1640 ),
1641 }
1642}
1643
1644async fn drain_connections(join_set: &mut JoinSet<()>, label: &str, mode: AppMode) {
1645 let timeout_secs = if mode == AppMode::Development { 5 } else { 30 };
1646 let drain_future = async {
1647 while let Some(result) = join_set.join_next().await {
1648 if let Err(e) = result {
1649 tracing::error!("Connection task panicked: {:?}", e);
1650 }
1651 }
1652 };
1653
1654 match tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), drain_future).await {
1655 Ok(_) => tracing::info!("{}: drained.", label),
1656 Err(_) => {
1657 tracing::warn!("{}: drain timeout, aborting connections.", label);
1658 join_set.abort_all();
1659 while join_set.join_next().await.is_some() {}
1660 }
1661 }
1662}
1663
1664async fn bind_listener(addr: &str, label: &str) -> Result<tokio::net::TcpListener> {
1674 tokio::net::TcpListener::bind(addr).await.map_err(|err| {
1675 webx_core::error::Error::Http(format!("failed to bind {label} on {addr}: {err}"))
1676 })
1677}
1678
1679fn parse_url(url: &str) -> Result<(&str, String)> {
1682 if let Some(rest) = url.strip_prefix("https://") {
1683 Ok(("https", rest.to_string()))
1684 } else if let Some(rest) = url.strip_prefix("http://") {
1685 Ok(("http", rest.to_string()))
1686 } else {
1687 Err(webx_core::error::Error::Http(format!(
1688 "Invalid URL '{}'. Use http://host:port or https://host:port",
1689 url
1690 )))
1691 }
1692}
1693
1694#[allow(clippy::too_many_arguments)]
1696async fn serve_http(
1697 addr: String,
1698 listener: tokio::net::TcpListener,
1699 mut shutdown: tokio::sync::watch::Receiver<bool>,
1700 pipeline: Arc<MiddlewarePipeline>,
1701 router_handler: HandlerFn,
1702 dispatch_runtime: Arc<DispatchRuntime>,
1703 mode: AppMode,
1704 body_limits: BodyLimits,
1705 max_connections: usize,
1706) {
1707 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1708 let mut join_set = JoinSet::new();
1709
1710 loop {
1711 if *shutdown.borrow() {
1715 tracing::info!("HTTP {}: stop accepting, draining...", addr);
1716 break;
1717 }
1718 tokio::select! {
1719 _ = shutdown.changed() => {
1720 tracing::info!("HTTP {}: stop accepting, draining...", addr);
1721 break;
1722 }
1723 accept_result = listener.accept() => {
1724 let (stream, peer_addr) = match accept_result {
1725 Ok(v) => v,
1726 Err(e) => {
1727 tracing::error!("Accept error (will retry): {}", e);
1728 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1729 continue;
1730 }
1731 };
1732 let remote_addr = Some(peer_addr);
1733
1734 while join_set.try_join_next().is_some() {}
1735
1736 let permit = match sem.clone().acquire_owned().await {
1737 Ok(p) => p,
1738 Err(_) => continue,
1739 };
1740
1741 let io = TokioIo::new(stream);
1742 let pipeline = Arc::clone(&pipeline);
1743 let router_handler = router_handler.clone();
1744 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1745 let body_limits = body_limits.clone();
1746 let conn_shutdown = shutdown.clone();
1747
1748 join_set.spawn(async move {
1749 let _permit = permit;
1750 let svc_fn = service_fn(move |req: Request<Incoming>| {
1751 let pipeline = Arc::clone(&pipeline);
1752 let router_handler = router_handler.clone();
1753 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1754 let body_limits = body_limits.clone();
1755 let mode = mode;
1756 let remote_addr = remote_addr;
1759 async move {
1760 let start = Instant::now();
1761 let method = req.method().to_string();
1762 let path = req.uri().path().to_string();
1763 let result = handle_request(
1764 req,
1765 pipeline,
1766 router_handler,
1767 dispatch_runtime,
1768 body_limits,
1769 remote_addr,
1770 )
1771 .await;
1772 let elapsed = start.elapsed();
1773 if mode == AppMode::Development {
1774 let status =
1775 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1776 tracing::info!(
1777 "[{}] {} -> {} ({:.0}ms)",
1778 method,
1779 path,
1780 status,
1781 elapsed.as_secs_f64() * 1000.0
1782 );
1783 }
1784 result
1785 }
1786 });
1787
1788 let conn = hyper::server::conn::http1::Builder::new()
1789 .serve_connection(io, svc_fn);
1790 let mut conn = std::pin::pin!(conn);
1791 let mut conn_shutdown = conn_shutdown;
1792 tokio::select! {
1793 result = conn.as_mut() => {
1794 if let Err(err) = result {
1795 tracing::error!("Connection error: {}", err);
1796 }
1797 }
1798 _ = wait_for_shutdown(&mut conn_shutdown) => {
1799 conn.as_mut().graceful_shutdown();
1803 if let Err(err) = conn.await {
1804 tracing::debug!(error = %err, "connection closed during shutdown");
1805 }
1806 }
1807 }
1808 });
1809 }
1810 }
1811 }
1812
1813 drain_connections(&mut join_set, &format!("HTTP {}", addr), mode).await;
1814}
1815
1816#[allow(clippy::too_many_arguments)]
1818async fn serve_https(
1819 addr: String,
1820 acceptor: TlsAcceptor,
1821 listener: tokio::net::TcpListener,
1822 mut shutdown: tokio::sync::watch::Receiver<bool>,
1823 pipeline: Arc<MiddlewarePipeline>,
1824 router_handler: HandlerFn,
1825 dispatch_runtime: Arc<DispatchRuntime>,
1826 mode: AppMode,
1827 body_limits: BodyLimits,
1828 max_connections: usize,
1829) {
1830 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1831 let mut join_set = JoinSet::new();
1832
1833 loop {
1834 if *shutdown.borrow() {
1835 tracing::info!("HTTPS {}: stop accepting, draining...", addr);
1836 break;
1837 }
1838 tokio::select! {
1839 _ = shutdown.changed() => {
1840 tracing::info!("HTTPS {}: stop accepting, draining...", addr);
1841 break;
1842 }
1843 accept_result = listener.accept() => {
1844 let (stream, peer_addr) = match accept_result {
1845 Ok(v) => v,
1846 Err(e) => {
1847 tracing::error!("Accept error (will retry): {}", e);
1848 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1849 continue;
1850 }
1851 };
1852 let remote_addr = Some(peer_addr);
1853
1854 while join_set.try_join_next().is_some() {}
1855
1856 let permit = match sem.clone().acquire_owned().await {
1857 Ok(p) => p,
1858 Err(_) => continue,
1859 };
1860
1861 let acceptor = acceptor.clone();
1862 let pipeline = Arc::clone(&pipeline);
1863 let router_handler = router_handler.clone();
1864 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1865 let body_limits = body_limits.clone();
1866 let conn_shutdown = shutdown.clone();
1867
1868 join_set.spawn(async move {
1869 let _permit = permit;
1870 match acceptor.accept(stream).await {
1871 Ok(tls_stream) => {
1872 let io = TokioIo::new(tls_stream);
1873 let svc_fn = service_fn(move |req: Request<Incoming>| {
1874 let pipeline = Arc::clone(&pipeline);
1875 let router_handler = router_handler.clone();
1876 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1877 let body_limits = body_limits.clone();
1878 let mode = mode;
1879 let remote_addr = remote_addr;
1881 async move {
1882 let start = Instant::now();
1883 let method = req.method().to_string();
1884 let path = req.uri().path().to_string();
1885 let result = handle_request(
1886 req,
1887 pipeline,
1888 router_handler,
1889 dispatch_runtime,
1890 body_limits,
1891 remote_addr,
1892 )
1893 .await;
1894 let elapsed = start.elapsed();
1895 if mode == AppMode::Development {
1896 let status =
1897 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1898 tracing::info!(
1899 "[{}] {} -> {} ({:.0}ms)",
1900 method,
1901 path,
1902 status,
1903 elapsed.as_secs_f64() * 1000.0
1904 );
1905 }
1906 result
1907 }
1908 });
1909
1910 let conn = hyper::server::conn::http1::Builder::new()
1911 .serve_connection(io, svc_fn);
1912 let mut conn = std::pin::pin!(conn);
1913 let mut conn_shutdown = conn_shutdown;
1914 tokio::select! {
1915 result = conn.as_mut() => {
1916 if let Err(err) = result {
1917 tracing::error!("TLS connection error: {}", err);
1918 }
1919 }
1920 _ = wait_for_shutdown(&mut conn_shutdown) => {
1921 conn.as_mut().graceful_shutdown();
1922 if let Err(err) = conn.await {
1923 tracing::debug!(error = %err, "TLS connection closed during shutdown");
1924 }
1925 }
1926 }
1927 }
1928 Err(e) => {
1929 tracing::error!("TLS handshake error: {}", e);
1930 }
1931 }
1932 });
1933 }
1934 }
1935 }
1936
1937 drain_connections(&mut join_set, &format!("HTTPS {}", addr), mode).await;
1938}
1939
1940fn build_tls_acceptor(cert_path: &str, key_path: &str) -> Result<TlsAcceptor> {
1946 use std::fs::File;
1947 use std::io::BufReader;
1948
1949 if cert_path.is_empty() || key_path.is_empty() {
1950 return Err(webx_core::error::Error::Http(
1951 "TLS certificate or key path not configured.".into(),
1952 ));
1953 }
1954
1955 let cert_file = File::open(cert_path).map_err(|e| {
1956 webx_core::error::Error::Http(format!("Cannot open cert '{}': {}", cert_path, e))
1957 })?;
1958 let mut cert_reader = BufReader::new(cert_file);
1959 let certs: Vec<CertificateDer> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
1960 if certs.is_empty() {
1961 return Err(webx_core::error::Error::Http(format!(
1962 "No valid certs in '{}'",
1963 cert_path
1964 )));
1965 }
1966
1967 let key_file = File::open(key_path).map_err(|e| {
1968 webx_core::error::Error::Http(format!("Cannot open key '{}': {}", key_path, e))
1969 })?;
1970 let mut key_reader = BufReader::new(key_file);
1971 let key = pkcs8_private_keys(&mut key_reader)
1972 .filter_map(|r| r.ok())
1973 .map(PrivateKeyDer::from)
1974 .next()
1975 .or_else(|| {
1976 let key_file2 = File::open(key_path).map(BufReader::new).ok()?;
1977 let mut kr2 = key_file2;
1978 let rsa_keys: Vec<PrivateKeyDer> = rsa_private_keys(&mut kr2)
1979 .filter_map(|r| r.ok())
1980 .map(PrivateKeyDer::from)
1981 .collect();
1982 rsa_keys.into_iter().next()
1983 })
1984 .ok_or_else(|| {
1985 webx_core::error::Error::Http(format!("No valid private key in '{}'", key_path))
1986 })?;
1987
1988 let config = ServerConfig::builder()
1989 .with_no_client_auth()
1990 .with_single_cert(certs, key)
1991 .map_err(|e| webx_core::error::Error::Http(format!("TLS config error: {}", e)))?;
1992
1993 Ok(TlsAcceptor::from(std::sync::Arc::new(config)))
1994}
1995
1996#[cfg(test)]
1997mod embedded_attach_tests {
1998 use super::with_embedded_assets;
1999 use webx_spa::{ContentEncoding, EmbeddedAsset, EmbeddedAssets, SpaSource};
2000
2001 const FILES: &[EmbeddedAsset] = &[EmbeddedAsset {
2002 path: "index.html",
2003 bytes: b"hi",
2004 raw_len: 2,
2005 encoding: ContentEncoding::Identity,
2006 content_type: "text/html",
2007 etag: "\"x\"",
2008 }];
2009
2010 #[test]
2011 fn no_table_leaves_disk_source() {
2012 let source = Some(SpaSource::Disk("wwwroot".into()));
2013 let merged = with_embedded_assets(source, None);
2014 assert!(matches!(merged, Some(SpaSource::Disk(ref r)) if r == "wwwroot"));
2015 }
2016
2017 #[test]
2018 fn table_layers_under_use_spa_overlay() {
2019 let assets = EmbeddedAssets::new("../wwwroot", FILES);
2020 let merged = with_embedded_assets(Some(SpaSource::Disk("wwwroot".into())), Some(assets));
2021 match merged {
2022 Some(SpaSource::Embedded {
2023 overlay_root,
2024 assets,
2025 }) => {
2026 assert_eq!(overlay_root, "wwwroot");
2027 assert_eq!(assets.root(), "../wwwroot");
2028 }
2029 other => panic!("expected embedded, got {other:?}"),
2030 }
2031 }
2032
2033 #[test]
2034 fn table_alone_uses_build_root_as_overlay() {
2035 let assets = EmbeddedAssets::new("../wwwroot", FILES);
2036 let merged = with_embedded_assets(None, Some(assets));
2037 match merged {
2038 Some(SpaSource::Embedded { overlay_root, .. }) => {
2039 assert_eq!(overlay_root, "../wwwroot");
2040 }
2041 other => panic!("expected embedded, got {other:?}"),
2042 }
2043 }
2044
2045 #[test]
2046 fn host_crate_registers_no_asset_table() {
2047 assert!(EmbeddedAssets::registered().is_none());
2048 }
2049}