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::Result;
15use webx_core::handler::IHostedService;
16use webx_core::http::IHttpContext;
17use webx_core::middleware::IMiddleware;
18use webx_core::mode::AppMode;
19use webx_core::routing::{HttpMethod, IEndpoint, IRouter};
20
21use crate::auth_jwt::{init_jwt_secret, jwt_middleware, JwtAuth};
22use crate::authz::{build_resource_policy_from_routes, collect_authorizers};
23use crate::context::{BodyLimits, HttpContext, RespBody};
24use crate::cors::{CorsConfig, CorsMiddleware};
25use crate::endpoint::{StaticHtmlEndpoint, StaticJsonEndpoint, StubEndpoint};
26use crate::health::{HealthCheckEndpoint, HealthCheckRegistry, HealthStatus};
27use crate::memory_cache::MemoryCache;
28use crate::metrics::{HttpMetrics, MetricsEndpoint, MetricsMiddleware};
29use crate::pipeline::{HandlerFn, MiddlewarePipeline};
30use crate::problem_response::{build_problem, write_problem_response};
31use crate::rate_limit::RateLimitMiddleware;
32use crate::router::Router;
33use jsonwebtoken::{DecodingKey, Validation};
34use webx_core::route::scan::RouteEntry;
35use webx_core::DispatchRuntime;
36use webx_openapi::{generate_openapi_spec, APIUI_HTML};
37use webx_spa::{EmbeddedAssets, SpaMiddleware, SpaSource};
38
39use rustls::pki_types::{CertificateDer, PrivateKeyDer};
40use rustls::ServerConfig;
41use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
42use std::sync::Arc;
43use std::time::Instant;
44use tokio::task::JoinSet;
45use tokio_rustls::TlsAcceptor;
46
47fn is_weak_jwt_secret(secret: &str) -> bool {
49 secret.is_empty()
50 || secret.len() < 32
51 || secret.contains("change-in-production")
52 || secret.contains("please-change")
53 || secret.contains("demo-secret")
54 || secret.contains("dev-secret")
55}
56
57fn log_spa_source(spa: &SpaMiddleware) {
62 tracing::info!("[Host] SPA root: {}", spa.resolved_root().display());
63 let Some(assets) = spa.embedded() else {
64 return;
65 };
66 tracing::info!(
67 "[Host] Embedded assets: {} files, {:.1} MiB embedded ({:.1} MiB uncompressed), built from `{}`",
68 assets.len(),
69 assets.total_bytes() as f64 / (1024.0 * 1024.0),
70 assets.total_raw_bytes() as f64 / (1024.0 * 1024.0),
71 assets.root(),
72 );
73 let shadowed = assets.shadowed_by(spa.resolved_root());
74 if !shadowed.is_empty() {
75 tracing::warn!(
76 "[Host] {} embedded file(s) are overridden on disk: {}. \
77 An override older than the current build stays in effect until it is \
78 updated or deleted.",
79 shadowed.len(),
80 shadowed.join(", "),
81 );
82 }
83}
84
85fn with_embedded_assets(
91 source: Option<SpaSource>,
92 assets: Option<EmbeddedAssets>,
93) -> Option<SpaSource> {
94 let Some(assets) = assets else {
95 return source;
96 };
97 let overlay_root = match source.as_ref() {
98 Some(existing) => existing.disk_root().to_string(),
99 None => assets.root().to_string(),
100 };
101 Some(SpaSource::Embedded {
102 assets,
103 overlay_root,
104 })
105}
106
107pub struct Host {
108 #[allow(dead_code)]
109 provider: Arc<ServiceProvider>,
110 dispatch_runtime: Arc<DispatchRuntime>,
111 pub options: AppOptions,
112 body_limits: BodyLimits,
114 pipeline: Arc<MiddlewarePipeline>,
115 #[allow(dead_code)]
117 router: Arc<Router>,
118 router_handler: HandlerFn,
120 mode: AppMode,
121 #[allow(dead_code)]
123 spa_source: Option<SpaSource>,
124 shutdown: Arc<tokio::sync::Notify>,
125 hosted_services: Vec<Arc<dyn IHostedService>>,
128}
129
130#[allow(clippy::type_complexity)]
131pub struct HostBuilder {
132 service_configs: Vec<Box<dyn FnOnce(ServiceCollection) -> ServiceCollection + Send>>,
133 mode: AppMode,
134 spa_source: Option<SpaSource>,
135 spa_disabled: bool,
136 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
137 cors_config: Option<CorsConfig>,
138 auth_enabled: bool,
139 resource_auth_enabled: bool,
140 health_registry: Arc<HealthCheckRegistry>,
141}
142
143#[allow(clippy::type_complexity)]
144pub struct HostAppBuilder {
145 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
146}
147
148impl HostAppBuilder {
149 fn new() -> Self {
150 Self {
151 options_modifiers: Vec::new(),
152 }
153 }
154
155 #[allow(non_snake_case)]
156 pub fn useOptions<F>(&mut self, f: F)
157 where
158 F: FnOnce(&mut AppOptions) + Send + 'static,
159 {
160 self.options_modifiers.push(Box::new(f));
161 }
162}
163
164impl HostBuilder {
165 pub fn new() -> Self {
166 Self {
167 service_configs: Vec::new(),
168 mode: AppMode::from_env(),
171 spa_source: None,
172 spa_disabled: false,
173 options_modifiers: Vec::new(),
174 cors_config: None,
175 auth_enabled: false,
176 resource_auth_enabled: false,
177 health_registry: HealthCheckRegistry::new(),
178 }
179 }
180
181 pub fn register<F>(mut self, f: F) -> Self
182 where
183 F: FnOnce(ServiceCollection) -> ServiceCollection + Send + 'static,
184 {
185 self.service_configs.push(Box::new(f));
186 self
187 }
188
189 pub fn configure<F>(mut self, f: F) -> Self
190 where
191 F: FnOnce(&mut HostAppBuilder) + Send + 'static,
192 {
193 let mut builder = HostAppBuilder::new();
194 f(&mut builder);
195 self.options_modifiers
196 .append(&mut builder.options_modifiers);
197 self
198 }
199
200 pub fn mode(mut self, mode: AppMode) -> Self {
201 self.mode = mode;
202 self
203 }
204
205 pub fn add_options<T>(self, section: &str) -> Self
217 where
218 T: for<'de> serde::Deserialize<'de> + Default + Send + Sync + 'static,
219 {
220 let mode = self.mode;
221 let section = section.to_string();
222 self.register(move |svc| {
223 let appsettings = config::load_appsettings(mode).unwrap_or_default();
224 let bound: T = config::bind_config(&appsettings, §ion);
225 tracing::info!(
226 "[Host] add_options<{}>(\"{}\") registered",
227 std::any::type_name::<T>(),
228 section
229 );
230 svc.instance(Arc::new(bound))
231 })
232 }
233
234 pub fn use_spa(mut self, source: impl Into<SpaSource>) -> Self {
249 self.spa_source = Some(match (self.spa_source.take(), source.into()) {
250 (Some(SpaSource::Embedded { assets, .. }), SpaSource::Disk(root)) => {
253 SpaSource::Embedded {
254 assets,
255 overlay_root: root,
256 }
257 }
258 (_, incoming) => incoming,
259 });
260 self
261 }
262
263 pub fn no_spa(mut self) -> Self {
269 self.spa_disabled = true;
270 self
271 }
272
273 pub fn use_cors(mut self, config: CorsConfig) -> Self {
274 self.cors_config = Some(config);
275 self
276 }
277
278 pub fn add_authentication(mut self) -> Self {
290 self.auth_enabled = true;
291 self
292 }
293
294 pub fn use_resource_authorization(mut self) -> Self {
301 self.resource_auth_enabled = true;
302 self
303 }
304
305 pub fn add_memory_cache(mut self) -> Self {
317 let cache = Arc::new(MemoryCache::new());
318 self.service_configs
319 .push(Box::new(move |svc| svc.instance(cache)));
320 self
321 }
322
323 pub fn add_memory_cache_with(mut self, max_entries: usize) -> Self {
325 let cache = Arc::new(MemoryCache::new().with_max_entries(max_entries));
326 self.service_configs
327 .push(Box::new(move |svc| svc.instance(Arc::clone(&cache))));
328 self
329 }
330
331 pub fn use_middleware<T: IMiddleware + Default + 'static>(mut self) -> Self {
343 self.service_configs.push(Box::new(|svc| {
344 svc.singleton::<dyn IMiddleware>(|_| Arc::new(T::default()) as Arc<dyn IMiddleware>)
345 }));
346 self
347 }
348
349 pub fn use_middleware_with<F>(mut self, factory: F) -> Self
361 where
362 F: Fn() -> Arc<dyn IMiddleware> + Send + Sync + 'static,
363 {
364 self.service_configs.push(Box::new(move |svc| {
365 svc.singleton::<dyn IMiddleware>(move |_| factory())
366 }));
367 self
368 }
369
370 pub fn add_health_check<F>(self, name: impl Into<String>, check: F) -> Self
383 where
384 F: Fn() -> HealthStatus + Send + Sync + 'static,
385 {
386 self.health_registry.register(name, Arc::new(check));
387 self
388 }
389
390 pub fn build(self) -> Host {
391 let env_filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into());
394 if self.mode == AppMode::Development {
395 let _ = tracing_subscriber::fmt()
396 .with_env_filter(env_filter)
397 .without_time()
398 .with_target(false)
399 .try_init();
400 } else {
401 let _ = tracing_subscriber::fmt()
402 .json()
403 .with_env_filter(env_filter)
404 .try_init();
405 }
406
407 let mut svc = ServiceCollection::from_injected();
408 for cfg in self.service_configs {
409 svc = cfg(svc);
410 }
411
412 let health_registry = Arc::clone(&self.health_registry);
414 svc = svc.instance(health_registry);
415
416 let provider = svc.build().unwrap_or_else(|e| {
417 panic!(
418 "Failed to build ServiceProvider: {}. Check your DI registrations.",
419 e
420 )
421 });
422
423 let handler_cache = Arc::new(webx_core::route::scan::HandlerCache::build());
425 let dispatch_runtime = Arc::new(DispatchRuntime::new(Arc::clone(&provider), handler_cache));
426
427 let appsettings =
428 config::load_appsettings(self.mode).unwrap_or_else(|| serde_json::json!({}));
429 let mut options: AppOptions = config::bind_root(&appsettings);
430 for modifier in self.options_modifiers {
431 modifier(&mut options);
432 }
433
434 let body_limits = BodyLimits::from_options(&options);
436 body_limits.apply_spool_root();
437 tracing::info!(
438 "[Host] Uploads: max request {} bytes, max file {} bytes, spool >{} bytes to {}",
439 body_limits.multipart_body,
440 body_limits.file,
441 body_limits.memory_threshold,
442 body_limits.spool_dir.display()
443 );
444
445 let http_metrics = HttpMetrics::new();
446
447 let mut pipeline = MiddlewarePipeline::new();
448
449 pipeline.add_middleware(Arc::new(
452 crate::security_headers::SecurityHeadersMiddleware::new(),
453 ));
454 pipeline.add_middleware(Arc::new(crate::request_id::RequestIdMiddleware::new()));
455
456 if options.rate_limit.enabled {
457 tracing::info!(
458 "[Host] RateLimit enabled: {} req/s, burst {}, max_ips {}",
459 options.rate_limit.requests_per_second,
460 options.rate_limit.burst_size,
461 options.rate_limit.max_tracked_ips
462 );
463 pipeline.add_middleware(Arc::new(RateLimitMiddleware::from_config(
464 &options.rate_limit,
465 )));
466 }
467
468 if options.metrics.enabled {
469 tracing::info!("[Host] Metrics enabled at GET /metrics");
470 pipeline.add_middleware(Arc::new(MetricsMiddleware::new(Arc::clone(&http_metrics))));
471 }
472
473 let middlewares: Vec<Arc<dyn IMiddleware>> = provider.get_all::<dyn IMiddleware>();
474 for mw in middlewares {
475 pipeline.add_middleware(mw);
476 }
477
478 let cors = self.cors_config.unwrap_or_else(|| {
479 let cs = &options.cors;
480 CorsConfig {
481 origins: cs.origins.clone(),
482 methods: cs.methods.clone(),
483 headers: cs.headers.clone(),
484 expose_headers: cs.expose_headers.clone(),
485 allow_credentials: cs.allow_credentials,
486 max_age: cs.max_age,
487 }
488 });
489 pipeline.add_middleware(Arc::new(CorsMiddleware::new(cors)));
490
491 if self.auth_enabled {
492 let secret = options.jwt.secret.clone();
493 if !secret.is_empty() {
494 let jwt_auth = Arc::new(JwtAuth::new(
495 DecodingKey::from_secret(secret.as_bytes()),
496 Validation::default(),
497 ));
498 pipeline.add_middleware(Arc::new(jwt_middleware(jwt_auth)));
499 init_jwt_secret(&secret);
500 } else {
501 tracing::warn!(
502 "add_authentication() enabled but no JWT secret configured. Set Jwt.Secret in appsettings.json, JWT_SECRET, or APP__Jwt__Secret env var."
503 );
504 }
505 }
506
507 let spa_source = if self.spa_disabled {
515 None
516 } else {
517 let base = self.spa_source.clone().or_else(|| {
518 let candidate = webx_core::paths::app_base().join("wwwroot");
519 if candidate.is_dir() {
520 tracing::info!("[Host] Auto-detected SPA root: {}", candidate.display());
521 Some(SpaSource::Disk(candidate.to_string_lossy().into_owned()))
522 } else {
523 None
524 }
525 });
526 with_embedded_assets(base, EmbeddedAssets::registered().copied())
527 };
528 if let Some(ref source) = spa_source {
529 let spa = SpaMiddleware::from_source(source.clone());
530 log_spa_source(&spa);
531 pipeline.add_middleware(Arc::new(spa));
532 }
533
534 let pipeline = Arc::new(pipeline);
539
540 if self.mode == AppMode::Production
542 && self.auth_enabled
543 && is_weak_jwt_secret(&options.jwt.secret)
544 {
545 panic!(
546 "Production requires a strong JWT secret. Set APP__Jwt__Secret or JWT_SECRET env var (min 32 chars, not a placeholder)."
547 );
548 }
549
550 if self.mode == AppMode::Production && options.cors.origins.iter().any(|o| o == "*") {
552 panic!(
553 "Production forbids CORS origin '*'. Set explicit origins via Cors.Origins or APP__Cors__Origins."
554 );
555 }
556
557 let mut router = Router::new();
558 let mut route_count = 0usize;
559
560 crate::diagnostics::assert_route_configuration_valid();
561
562 let mut dispatch_map: std::collections::HashMap<
564 &'static str,
565 webx_core::route::scan::RouteDispatchFn,
566 > = std::collections::HashMap::new();
567
568 for dispatch in inventory::iter::<webx_core::route::scan::RouteDispatch> {
569 dispatch_map.insert(dispatch.handler_type, dispatch.dispatch);
570 }
571
572 let resource_policy: Option<Arc<crate::authz::ResourceAuthorization>> =
573 if self.resource_auth_enabled {
574 Some(Arc::new(build_resource_policy_from_routes()))
575 } else {
576 None
577 };
578
579 for entry in inventory::iter::<RouteEntry> {
580 route_count += 1;
581 let stub = Arc::new(StubEndpoint {
582 method: entry.method.as_str(),
583 path: entry.path,
584 handler_type: entry.handler_type,
585 dispatch_fn: dispatch_map.get(entry.handler_type).copied(),
586 auth_required_role: entry.required_role,
587 auth_required_permission: entry.required_permission,
588 resource_policy: resource_policy.clone(),
589 authorizers: collect_authorizers(provider.as_ref()),
590 });
591 router.register(entry.method, entry.path, stub);
592 }
593
594 let openapi_spec = generate_openapi_spec("Rust WebX API", env!("CARGO_PKG_VERSION"));
595 if self.mode == AppMode::Development {
596 let openapi_bytes = serde_json::to_vec(&openapi_spec).unwrap_or_default();
597 router.register(
598 HttpMethod::Get,
599 "/api/openapi.json",
600 Arc::new(StaticJsonEndpoint::new(openapi_bytes)),
601 );
602 router.register(
603 HttpMethod::Get,
604 "/api/openapi.html",
605 Arc::new(StaticHtmlEndpoint { body: APIUI_HTML }),
606 );
607 }
608
609 let health_registry = Arc::clone(&self.health_registry);
612 let health_endpoint: Arc<dyn IEndpoint> =
613 Arc::new(HealthCheckEndpoint::new(Arc::clone(&health_registry)));
614 router.register(HttpMethod::Get, "/health", Arc::clone(&health_endpoint));
615 router.register(HttpMethod::Get, "/healthz", health_endpoint);
616
617 let live_body =
619 serde_json::to_vec(&serde_json::json!({"status":"pass"})).unwrap_or_default();
620 router.register(
621 HttpMethod::Get,
622 "/health/live",
623 Arc::new(StaticJsonEndpoint {
624 body: live_body,
625 content_type: "application/health+json",
626 }),
627 );
628
629 router.register(
631 HttpMethod::Get,
632 "/health/ready",
633 Arc::new(HealthCheckEndpoint::new(health_registry)),
634 );
635
636 if options.metrics.enabled {
637 router.register(
638 HttpMethod::Get,
639 "/metrics",
640 Arc::new(MetricsEndpoint::new(http_metrics)),
641 );
642 }
643
644 if self.mode == AppMode::Development {
645 let version = env!("CARGO_PKG_VERSION");
646 tracing::info!("");
647 tracing::info!(" ----------------------------------------------------------------");
648 tracing::info!(" Rust WebX Framework v{}", version);
649 tracing::info!(" ----------------------------------------------------------------");
650 tracing::info!(" App: {}", options.app.name);
651 tracing::info!(" CORS: enabled");
652 if let Some(source) = spa_source.as_ref() {
653 tracing::info!(" SPA Root: {}", source.disk_root());
654 if let Some(assets) = source.embedded() {
655 tracing::info!(
656 " Embedded: {} files, {:.1} MiB from {:.1} MiB (built from {})",
657 assets.len(),
658 assets.total_bytes() as f64 / (1024.0 * 1024.0),
659 assets.total_raw_bytes() as f64 / (1024.0 * 1024.0),
660 assets.root(),
661 );
662 }
663 }
664 if route_count > 0 {
665 tracing::info!(" Routes: {} registered", route_count);
666 }
667 crate::diagnostics::log_startup_diagnostics();
668 let banner_urls = if options.app.urls.is_empty() {
669 vec!["http://localhost:5000".to_string()]
670 } else {
671 options
672 .app
673 .urls
674 .iter()
675 .map(|u| u.replace("0.0.0.0", "localhost"))
676 .collect::<Vec<_>>()
677 };
678 for url in &banner_urls {
679 tracing::info!(" OpenAPI: {}/api/openapi.html", url);
680 tracing::info!(" OpenAPI: {}/api/openapi.json", url);
681 }
682 tracing::info!(" ----------------------------------------------------------------");
683 tracing::info!("");
684 } else if route_count > 0 {
685 tracing::info!("{} route(s) registered", route_count);
686 }
687
688 let router = Arc::new(router);
689 let router_handler = make_router_handler(Arc::clone(&router));
690
691 let hosted_services: Vec<Arc<dyn IHostedService>> =
694 provider.get_all::<dyn IHostedService>();
695
696 Host {
697 provider,
698 dispatch_runtime,
699 options,
700 body_limits,
701 pipeline,
702 router,
703 router_handler,
704 mode: self.mode,
705 spa_source,
706 shutdown: Arc::new(tokio::sync::Notify::new()),
707 hosted_services,
708 }
709 }
710}
711
712pub struct ServerHandle {
714 shutdown: Arc<tokio::sync::Notify>,
715}
716
717impl ServerHandle {
718 pub fn shutdown(self) {
721 self.shutdown.notify_waiters();
722 }
723}
724
725pub struct Server {
737 host: Host,
738}
739
740impl Server {
741 pub async fn run(self) -> Result<()> {
743 self.host.run().await
744 }
745
746 pub async fn run_at(self, addr: &str) -> Result<()> {
748 self.host.run_at(addr).await
749 }
750
751 pub fn handle(&self) -> ServerHandle {
753 self.host.server_handle()
754 }
755}
756
757impl Default for HostBuilder {
758 fn default() -> Self {
759 Self::new()
760 }
761}
762
763impl Host {
764 pub fn builder() -> HostBuilder {
765 HostBuilder::new()
766 }
767
768 pub fn options(&self) -> &AppOptions {
769 &self.options
770 }
771
772 pub fn provider(&self) -> &Arc<ServiceProvider> {
774 &self.provider
775 }
776
777 pub fn dispatch_runtime(&self) -> &Arc<DispatchRuntime> {
779 &self.dispatch_runtime
780 }
781
782 pub async fn run(&self) -> Result<()> {
796 start_hosted_services(&self.hosted_services, &self.dispatch_runtime).await?;
797
798 let urls = if self.options.app.urls.is_empty() {
799 vec!["http://0.0.0.0:5000".to_string()]
800 } else {
801 self.options.app.urls.clone()
802 };
803
804 let mut http_addrs: Vec<String> = Vec::new();
805 let mut https_addrs: Vec<String> = Vec::new();
806
807 for url in &urls {
808 let (scheme, addr) = parse_url(url)?;
809 match scheme {
810 "http" => http_addrs.push(addr),
811 "https" => https_addrs.push(addr),
812 other => {
813 return Err(webx_core::error::Error::Http(format!(
814 "Unsupported URL scheme '{}' in '{}'",
815 other, url
816 )))
817 }
818 }
819 }
820
821 let acceptor = if !https_addrs.is_empty() {
822 let tls = &self.options.tls;
823 if tls.cert_path.is_empty() || tls.key_path.is_empty() {
824 return Err(webx_core::error::Error::Http(
825 "HTTPS URLs require Tls.CertPath and Tls.KeyPath".into(),
826 ));
827 }
828 Some(build_tls_acceptor(&tls.cert_path, &tls.key_path)?)
829 } else {
830 None
831 };
832
833 if self.mode == AppMode::Development {
835 tracing::info!("");
836 for url in &urls {
837 let display_url = url.replace("0.0.0.0", "localhost");
838 tracing::info!(" Listening on {}", display_url);
839 }
840 } else {
841 tracing::info!("Listening on {} url(s)", urls.len());
842 }
843
844 let notify = Arc::clone(&self.shutdown);
845 install_shutdown_handler(Arc::clone(¬ify));
846 spawn_async_shutdown_listener(Arc::clone(¬ify));
847
848 let mut handles = Vec::new();
849 let pipeline = Arc::clone(&self.pipeline);
850 let router_handler = self.router_handler.clone();
851 let dispatch_runtime = Arc::clone(&self.dispatch_runtime);
852 let mode = self.mode;
853 let body_limits = self.body_limits.clone();
854 let max_connections = self.options.app.max_connections;
855
856 for addr in &http_addrs {
857 let addr = addr.clone();
858 let n = std::sync::Arc::clone(¬ify);
859 let p = Arc::clone(&pipeline);
860 let rh = router_handler.clone();
861 let dr = Arc::clone(&dispatch_runtime);
862 handles.push(tokio::spawn(serve_http(
863 addr,
864 n,
865 p,
866 rh,
867 dr,
868 mode,
869 body_limits.clone(),
870 max_connections,
871 )));
872 }
873
874 if let Some(ref tls_acceptor) = acceptor {
875 for addr in &https_addrs {
876 let addr = addr.clone();
877 let n = std::sync::Arc::clone(¬ify);
878 let p = Arc::clone(&pipeline);
879 let rh = router_handler.clone();
880 let dr = Arc::clone(&dispatch_runtime);
881 let a = tls_acceptor.clone();
882 handles.push(tokio::spawn(serve_https(
883 addr,
884 a,
885 n,
886 p,
887 rh,
888 dr,
889 mode,
890 body_limits.clone(),
891 max_connections,
892 )));
893 }
894 }
895
896 for h in handles {
898 let _ = h.await;
899 }
900
901 stop_hosted_services(&self.hosted_services).await;
902
903 Ok(())
904 }
905
906 pub async fn run_at(&self, addr: &str) -> Result<()> {
908 start_hosted_services(&self.hosted_services, &self.dispatch_runtime).await?;
909
910 let notify = Arc::clone(&self.shutdown);
911 install_shutdown_handler(Arc::clone(¬ify));
912 spawn_async_shutdown_listener(Arc::clone(¬ify));
913 serve_http(
914 addr.to_string(),
915 notify,
916 Arc::clone(&self.pipeline),
917 self.router_handler.clone(),
918 Arc::clone(&self.dispatch_runtime),
919 self.mode,
920 self.body_limits.clone(),
921 self.options.app.max_connections,
922 )
923 .await;
924
925 stop_hosted_services(&self.hosted_services).await;
926 Ok(())
927 }
928
929 pub fn server_handle(&self) -> ServerHandle {
940 ServerHandle {
941 shutdown: Arc::clone(&self.shutdown),
942 }
943 }
944
945 pub fn into_server(self) -> Server {
950 Server { host: self }
951 }
952}
953
954#[async_trait::async_trait]
955impl IHost for Host {
956 async fn run(&self, addr: &str) -> Result<()> {
957 self.run_at(addr).await
958 }
959 async fn stop(&self) -> Result<()> {
960 tracing::info!("Stop requested.");
961 self.shutdown.notify_waiters();
962 Ok(())
963 }
964}
965
966fn make_router_handler(router: Arc<Router>) -> HandlerFn {
967 Arc::new(move |ctx: &mut dyn IHttpContext| {
968 let router = Arc::clone(&router);
969 Box::pin(async move {
970 match router.match_route(ctx).await? {
971 Some((endpoint, params, pattern)) => {
972 drop(router);
973 for (key, value) in params {
974 ctx.request_mut().route_params_mut().insert(key, value);
975 }
976 *ctx.request_mut().route_pattern_mut() = Some(pattern);
977 endpoint.handle(ctx).await
978 }
979 None => {
980 let path_exists = router.path_exists(ctx.request().path());
981 drop(router);
982 if !ctx.response().has_body() {
985 if path_exists {
986 write_error_response(ctx, 405, "Method Not Allowed").await;
987 } else {
988 write_error_response(ctx, 404, "Not Found").await;
989 }
990 }
991 Ok(())
992 }
993 }
994 })
995 })
996}
997
998async fn handle_request(
999 req: Request<Incoming>,
1000 pipeline: Arc<MiddlewarePipeline>,
1001 router_handler: HandlerFn,
1002 dispatch_runtime: Arc<DispatchRuntime>,
1003 body_limits: BodyLimits,
1004) -> std::result::Result<hyper::Response<RespBody>, std::convert::Infallible> {
1005 let mut ctx = HttpContext::new(req, body_limits);
1006
1007 if ctx.response().status() < 400 {
1010 let result = dispatch_runtime
1011 .run(async { pipeline.execute(&mut ctx, router_handler).await })
1012 .await;
1013 if let Err(e) = result {
1014 let status = e.status_code();
1015 write_error_response(&mut ctx, status, &e.to_string()).await;
1016 }
1017 }
1018
1019 Ok(ctx.into_response().await)
1020}
1021
1022async fn write_error_response(ctx: &mut dyn IHttpContext, status: u16, message: &str) {
1023 write_problem_response(ctx, build_problem(status, message)).await;
1024}
1025
1026fn install_shutdown_handler(shutdown: Arc<tokio::sync::Notify>) {
1036 use std::sync::atomic::{AtomicUsize, Ordering};
1037
1038 static CTRL_COUNT: AtomicUsize = AtomicUsize::new(0);
1039
1040 if let Err(e) = ctrlc::set_handler(move || {
1041 let n = CTRL_COUNT.fetch_add(1, Ordering::SeqCst);
1042 if n == 0 {
1043 tracing::info!("Shutdown signal received, draining connections...");
1044 shutdown.notify_waiters();
1045 } else {
1046 tracing::warn!("Second interrupt — force exiting.");
1047 std::process::exit(130);
1048 }
1049 }) {
1050 tracing::warn!("Could not install Ctrl+C handler: {}", e);
1051 }
1052}
1053
1054fn spawn_async_shutdown_listener(shutdown: Arc<tokio::sync::Notify>) {
1056 tokio::spawn(async move {
1057 wait_for_shutdown_signal().await;
1058 tracing::info!("Async shutdown signal received, draining connections...");
1059 shutdown.notify_waiters();
1060 });
1061}
1062
1063async fn wait_for_shutdown_signal() {
1064 #[cfg(unix)]
1065 {
1066 use tokio::signal::unix::{signal, SignalKind};
1067 let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
1068 tokio::select! {
1069 res = tokio::signal::ctrl_c() => {
1070 if let Err(e) = res {
1071 tracing::warn!("Ctrl+C listener error: {}", e);
1072 }
1073 }
1074 _ = sigterm.recv() => {}
1075 }
1076 }
1077 #[cfg(not(unix))]
1078 {
1079 if let Err(e) = tokio::signal::ctrl_c().await {
1080 tracing::warn!("Ctrl+C listener error: {}", e);
1081 }
1082 }
1083}
1084
1085async fn start_hosted_services(
1086 services: &[Arc<dyn IHostedService>],
1087 dispatch_runtime: &DispatchRuntime,
1088) -> Result<()> {
1089 if services.is_empty() {
1090 return Ok(());
1091 }
1092 tracing::info!("Starting {} hosted service(s)...", services.len());
1093 dispatch_runtime
1094 .run(async {
1095 for svc in services {
1096 svc.start().await?;
1097 }
1098 Ok::<(), webx_core::error::Error>(())
1099 })
1100 .await?;
1101 tracing::info!("All hosted services started.");
1102 Ok(())
1103}
1104
1105async fn stop_hosted_services(services: &[Arc<dyn IHostedService>]) {
1106 if services.is_empty() {
1107 return;
1108 }
1109 tracing::info!("Stopping {} hosted service(s)...", services.len());
1110 for svc in services {
1111 if let Err(e) = svc.stop().await {
1112 tracing::warn!("Hosted service stop error: {}", e);
1113 }
1114 }
1115 tracing::info!("All hosted services stopped.");
1116}
1117
1118async fn drain_connections(join_set: &mut JoinSet<()>, label: &str, mode: AppMode) {
1119 let timeout_secs = if mode == AppMode::Development { 5 } else { 30 };
1120 let drain_future = async {
1121 while let Some(result) = join_set.join_next().await {
1122 if let Err(e) = result {
1123 tracing::error!("Connection task panicked: {:?}", e);
1124 }
1125 }
1126 };
1127
1128 match tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), drain_future).await {
1129 Ok(_) => tracing::info!("{}: drained.", label),
1130 Err(_) => {
1131 tracing::warn!("{}: drain timeout, aborting connections.", label);
1132 join_set.abort_all();
1133 while join_set.join_next().await.is_some() {}
1134 }
1135 }
1136}
1137
1138fn parse_url(url: &str) -> Result<(&str, String)> {
1145 if let Some(rest) = url.strip_prefix("https://") {
1146 Ok(("https", rest.to_string()))
1147 } else if let Some(rest) = url.strip_prefix("http://") {
1148 Ok(("http", rest.to_string()))
1149 } else {
1150 Err(webx_core::error::Error::Http(format!(
1151 "Invalid URL '{}'. Use http://host:port or https://host:port",
1152 url
1153 )))
1154 }
1155}
1156
1157#[allow(clippy::too_many_arguments)]
1159async fn serve_http(
1160 addr: String,
1161 shutdown: std::sync::Arc<tokio::sync::Notify>,
1162 pipeline: Arc<MiddlewarePipeline>,
1163 router_handler: HandlerFn,
1164 dispatch_runtime: Arc<DispatchRuntime>,
1165 mode: AppMode,
1166 body_limits: BodyLimits,
1167 max_connections: usize,
1168) {
1169 let listener = match tokio::net::TcpListener::bind(&addr).await {
1170 Ok(l) => l,
1171 Err(e) => {
1172 tracing::error!("Failed to bind HTTP on {}: {}", addr, e);
1173 return;
1174 }
1175 };
1176
1177 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1178 let mut join_set = JoinSet::new();
1179
1180 loop {
1181 tokio::select! {
1182 accept_result = listener.accept() => {
1183 let (stream, _) = match accept_result {
1184 Ok(v) => v,
1185 Err(e) => {
1186 tracing::error!("Accept error (will retry): {}", e);
1187 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1188 continue;
1189 }
1190 };
1191
1192 while join_set.try_join_next().is_some() {}
1193
1194 let permit = match sem.clone().acquire_owned().await {
1195 Ok(p) => p,
1196 Err(_) => continue,
1197 };
1198
1199 let io = TokioIo::new(stream);
1200 let pipeline = Arc::clone(&pipeline);
1201 let router_handler = router_handler.clone();
1202 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1203 let body_limits = body_limits.clone();
1204
1205 join_set.spawn(async move {
1206 let _permit = permit;
1207 let svc_fn = service_fn(move |req: Request<Incoming>| {
1208 let pipeline = Arc::clone(&pipeline);
1209 let router_handler = router_handler.clone();
1210 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1211 let body_limits = body_limits.clone();
1212 let mode = mode;
1213 async move {
1214 let start = Instant::now();
1215 let method = req.method().to_string();
1216 let path = req.uri().path().to_string();
1217 let result = handle_request(
1218 req,
1219 pipeline,
1220 router_handler,
1221 dispatch_runtime,
1222 body_limits,
1223 )
1224 .await;
1225 let elapsed = start.elapsed();
1226 if mode == AppMode::Development {
1227 let status =
1228 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1229 tracing::info!(
1230 "[{}] {} -> {} ({:.0}ms)",
1231 method,
1232 path,
1233 status,
1234 elapsed.as_secs_f64() * 1000.0
1235 );
1236 }
1237 result
1238 }
1239 });
1240
1241 if let Err(err) = hyper::server::conn::http1::Builder::new()
1242 .serve_connection(io, svc_fn)
1243 .await
1244 {
1245 tracing::error!("Connection error: {}", err);
1246 }
1247 });
1248 }
1249 _ = shutdown.notified() => {
1250 tracing::info!("HTTP {}: stop accepting, draining...", addr);
1251 break;
1252 }
1253 }
1254 }
1255
1256 drain_connections(&mut join_set, &format!("HTTP {}", addr), mode).await;
1257}
1258
1259#[allow(clippy::too_many_arguments)]
1261async fn serve_https(
1262 addr: String,
1263 acceptor: TlsAcceptor,
1264 shutdown: std::sync::Arc<tokio::sync::Notify>,
1265 pipeline: Arc<MiddlewarePipeline>,
1266 router_handler: HandlerFn,
1267 dispatch_runtime: Arc<DispatchRuntime>,
1268 mode: AppMode,
1269 body_limits: BodyLimits,
1270 max_connections: usize,
1271) {
1272 let listener = match tokio::net::TcpListener::bind(&addr).await {
1273 Ok(l) => l,
1274 Err(e) => {
1275 tracing::error!("Failed to bind HTTPS on {}: {}", addr, e);
1276 return;
1277 }
1278 };
1279
1280 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1281 let mut join_set = JoinSet::new();
1282
1283 loop {
1284 tokio::select! {
1285 accept_result = listener.accept() => {
1286 let (stream, _) = match accept_result {
1287 Ok(v) => v,
1288 Err(e) => {
1289 tracing::error!("Accept error (will retry): {}", e);
1290 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1291 continue;
1292 }
1293 };
1294
1295 while join_set.try_join_next().is_some() {}
1296
1297 let permit = match sem.clone().acquire_owned().await {
1298 Ok(p) => p,
1299 Err(_) => continue,
1300 };
1301
1302 let acceptor = acceptor.clone();
1303 let pipeline = Arc::clone(&pipeline);
1304 let router_handler = router_handler.clone();
1305 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1306 let body_limits = body_limits.clone();
1307
1308 join_set.spawn(async move {
1309 let _permit = permit;
1310 match acceptor.accept(stream).await {
1311 Ok(tls_stream) => {
1312 let io = TokioIo::new(tls_stream);
1313 let svc_fn = service_fn(move |req: Request<Incoming>| {
1314 let pipeline = Arc::clone(&pipeline);
1315 let router_handler = router_handler.clone();
1316 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1317 let body_limits = body_limits.clone();
1318 let mode = mode;
1319 async move {
1320 let start = Instant::now();
1321 let method = req.method().to_string();
1322 let path = req.uri().path().to_string();
1323 let result = handle_request(
1324 req,
1325 pipeline,
1326 router_handler,
1327 dispatch_runtime,
1328 body_limits,
1329 )
1330 .await;
1331 let elapsed = start.elapsed();
1332 if mode == AppMode::Development {
1333 let status =
1334 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1335 tracing::info!(
1336 "[{}] {} -> {} ({:.0}ms)",
1337 method,
1338 path,
1339 status,
1340 elapsed.as_secs_f64() * 1000.0
1341 );
1342 }
1343 result
1344 }
1345 });
1346
1347 if let Err(err) = hyper::server::conn::http1::Builder::new()
1348 .serve_connection(io, svc_fn)
1349 .await
1350 {
1351 tracing::error!("TLS connection error: {}", err);
1352 }
1353 }
1354 Err(e) => {
1355 tracing::error!("TLS handshake error: {}", e);
1356 }
1357 }
1358 });
1359 }
1360 _ = shutdown.notified() => {
1361 tracing::info!("HTTPS {}: stop accepting, draining...", addr);
1362 break;
1363 }
1364 }
1365 }
1366
1367 drain_connections(&mut join_set, &format!("HTTPS {}", addr), mode).await;
1368}
1369
1370fn build_tls_acceptor(cert_path: &str, key_path: &str) -> Result<TlsAcceptor> {
1376 use std::fs::File;
1377 use std::io::BufReader;
1378
1379 if cert_path.is_empty() || key_path.is_empty() {
1380 return Err(webx_core::error::Error::Http(
1381 "TLS certificate or key path not configured.".into(),
1382 ));
1383 }
1384
1385 let cert_file = File::open(cert_path).map_err(|e| {
1386 webx_core::error::Error::Http(format!("Cannot open cert '{}': {}", cert_path, e))
1387 })?;
1388 let mut cert_reader = BufReader::new(cert_file);
1389 let certs: Vec<CertificateDer> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
1390 if certs.is_empty() {
1391 return Err(webx_core::error::Error::Http(format!(
1392 "No valid certs in '{}'",
1393 cert_path
1394 )));
1395 }
1396
1397 let key_file = File::open(key_path).map_err(|e| {
1398 webx_core::error::Error::Http(format!("Cannot open key '{}': {}", key_path, e))
1399 })?;
1400 let mut key_reader = BufReader::new(key_file);
1401 let key = pkcs8_private_keys(&mut key_reader)
1402 .filter_map(|r| r.ok())
1403 .map(PrivateKeyDer::from)
1404 .next()
1405 .or_else(|| {
1406 let key_file2 = File::open(key_path).map(BufReader::new).ok()?;
1407 let mut kr2 = key_file2;
1408 let rsa_keys: Vec<PrivateKeyDer> = rsa_private_keys(&mut kr2)
1409 .filter_map(|r| r.ok())
1410 .map(PrivateKeyDer::from)
1411 .collect();
1412 rsa_keys.into_iter().next()
1413 })
1414 .ok_or_else(|| {
1415 webx_core::error::Error::Http(format!("No valid private key in '{}'", key_path))
1416 })?;
1417
1418 let config = ServerConfig::builder()
1419 .with_no_client_auth()
1420 .with_single_cert(certs, key)
1421 .map_err(|e| webx_core::error::Error::Http(format!("TLS config error: {}", e)))?;
1422
1423 Ok(TlsAcceptor::from(std::sync::Arc::new(config)))
1424}
1425
1426#[cfg(test)]
1427mod embedded_attach_tests {
1428 use super::with_embedded_assets;
1429 use webx_spa::{ContentEncoding, EmbeddedAsset, EmbeddedAssets, SpaSource};
1430
1431 const FILES: &[EmbeddedAsset] = &[EmbeddedAsset {
1432 path: "index.html",
1433 bytes: b"hi",
1434 raw_len: 2,
1435 encoding: ContentEncoding::Identity,
1436 content_type: "text/html",
1437 etag: "\"x\"",
1438 }];
1439
1440 #[test]
1441 fn no_table_leaves_disk_source() {
1442 let source = Some(SpaSource::Disk("wwwroot".into()));
1443 let merged = with_embedded_assets(source, None);
1444 assert!(matches!(merged, Some(SpaSource::Disk(ref r)) if r == "wwwroot"));
1445 }
1446
1447 #[test]
1448 fn table_layers_under_use_spa_overlay() {
1449 let assets = EmbeddedAssets::new("../wwwroot", FILES);
1450 let merged = with_embedded_assets(Some(SpaSource::Disk("wwwroot".into())), Some(assets));
1451 match merged {
1452 Some(SpaSource::Embedded {
1453 overlay_root,
1454 assets,
1455 }) => {
1456 assert_eq!(overlay_root, "wwwroot");
1457 assert_eq!(assets.root(), "../wwwroot");
1458 }
1459 other => panic!("expected embedded, got {other:?}"),
1460 }
1461 }
1462
1463 #[test]
1464 fn table_alone_uses_build_root_as_overlay() {
1465 let assets = EmbeddedAssets::new("../wwwroot", FILES);
1466 let merged = with_embedded_assets(None, Some(assets));
1467 match merged {
1468 Some(SpaSource::Embedded { overlay_root, .. }) => {
1469 assert_eq!(overlay_root, "../wwwroot");
1470 }
1471 other => panic!("expected embedded, got {other:?}"),
1472 }
1473 }
1474
1475 #[test]
1476 fn host_crate_registers_no_asset_table() {
1477 assert!(EmbeddedAssets::registered().is_none());
1478 }
1479}