1use hyper::body::Incoming;
8use hyper::service::service_fn;
9use hyper::Request;
10use hyper_util::rt::TokioIo;
11use rust_dix::{ServiceCollection, ServiceProvider};
12use rust_webx_core::app::IHost;
13use rust_webx_core::config::{self, AppOptions};
14use rust_webx_core::error::Result;
15use rust_webx_core::handler::IHostedService;
16use rust_webx_core::http::IHttpContext;
17use rust_webx_core::middleware::IMiddleware;
18use rust_webx_core::mode::AppMode;
19use rust_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 rust_webx_core::route::scan::RouteEntry;
35use rust_webx_core::DispatchRuntime;
36use rust_webx_openapi::{generate_openapi_spec, APIUI_HTML};
37use rust_webx_spa::SpaMiddleware;
38
39use rustls::pki_types::{CertificateDer, PrivateKeyDer};
40use rustls::ServerConfig;
41use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
42use std::sync::Arc;
43use std::time::Instant;
44use tokio::task::JoinSet;
45use tokio_rustls::TlsAcceptor;
46
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
57pub struct Host {
58 #[allow(dead_code)]
59 provider: Arc<ServiceProvider>,
60 dispatch_runtime: Arc<DispatchRuntime>,
61 pub options: AppOptions,
62 body_limits: BodyLimits,
64 pipeline: Arc<MiddlewarePipeline>,
65 #[allow(dead_code)]
67 router: Arc<Router>,
68 router_handler: HandlerFn,
70 mode: AppMode,
71 #[allow(dead_code)]
72 spa_root: Option<String>,
73 shutdown: Arc<tokio::sync::Notify>,
74 hosted_services: Vec<Arc<dyn IHostedService>>,
77}
78
79#[allow(clippy::type_complexity)]
80pub struct HostBuilder {
81 service_configs: Vec<Box<dyn FnOnce(ServiceCollection) -> ServiceCollection + Send>>,
82 mode: AppMode,
83 spa_root: Option<String>,
84 spa_disabled: bool,
85 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
86 cors_config: Option<CorsConfig>,
87 auth_enabled: bool,
88 resource_auth_enabled: bool,
89 health_registry: Arc<HealthCheckRegistry>,
90}
91
92#[allow(clippy::type_complexity)]
93pub struct HostAppBuilder {
94 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
95}
96
97impl HostAppBuilder {
98 fn new() -> Self {
99 Self {
100 options_modifiers: Vec::new(),
101 }
102 }
103
104 #[allow(non_snake_case)]
105 pub fn useOptions<F>(&mut self, f: F)
106 where
107 F: FnOnce(&mut AppOptions) + Send + 'static,
108 {
109 self.options_modifiers.push(Box::new(f));
110 }
111}
112
113impl HostBuilder {
114 pub fn new() -> Self {
115 Self {
116 service_configs: Vec::new(),
117 mode: AppMode::from_env(),
120 spa_root: None,
121 spa_disabled: false,
122 options_modifiers: Vec::new(),
123 cors_config: None,
124 auth_enabled: false,
125 resource_auth_enabled: false,
126 health_registry: HealthCheckRegistry::new(),
127 }
128 }
129
130 pub fn register<F>(mut self, f: F) -> Self
131 where
132 F: FnOnce(ServiceCollection) -> ServiceCollection + Send + 'static,
133 {
134 self.service_configs.push(Box::new(f));
135 self
136 }
137
138 pub fn configure<F>(mut self, f: F) -> Self
139 where
140 F: FnOnce(&mut HostAppBuilder) + Send + 'static,
141 {
142 let mut builder = HostAppBuilder::new();
143 f(&mut builder);
144 self.options_modifiers
145 .append(&mut builder.options_modifiers);
146 self
147 }
148
149 pub fn mode(mut self, mode: AppMode) -> Self {
150 self.mode = mode;
151 self
152 }
153
154 pub fn add_options<T>(self, section: &str) -> Self
166 where
167 T: for<'de> serde::Deserialize<'de> + Default + Send + Sync + 'static,
168 {
169 let mode = self.mode;
170 let section = section.to_string();
171 self.register(move |svc| {
172 let appsettings = config::load_appsettings(mode).unwrap_or_default();
173 let bound: T = config::bind_config(&appsettings, §ion);
174 tracing::info!(
175 "[Host] add_options<{}>(\"{}\") registered",
176 std::any::type_name::<T>(),
177 section
178 );
179 svc.instance(Arc::new(bound))
180 })
181 }
182
183 pub fn use_spa(mut self, root: impl Into<String>) -> Self {
184 self.spa_root = Some(root.into());
185 self
186 }
187
188 pub fn no_spa(mut self) -> Self {
194 self.spa_disabled = true;
195 self
196 }
197
198 pub fn use_cors(mut self, config: CorsConfig) -> Self {
199 self.cors_config = Some(config);
200 self
201 }
202
203 pub fn add_authentication(mut self) -> Self {
215 self.auth_enabled = true;
216 self
217 }
218
219 pub fn use_resource_authorization(mut self) -> Self {
226 self.resource_auth_enabled = true;
227 self
228 }
229
230 pub fn add_memory_cache(mut self) -> Self {
242 let cache = Arc::new(MemoryCache::new());
243 self.service_configs
244 .push(Box::new(move |svc| svc.instance(cache)));
245 self
246 }
247
248 pub fn add_memory_cache_with(mut self, max_entries: usize) -> Self {
250 let cache = Arc::new(MemoryCache::new().with_max_entries(max_entries));
251 self.service_configs
252 .push(Box::new(move |svc| svc.instance(Arc::clone(&cache))));
253 self
254 }
255
256 pub fn use_middleware<T: IMiddleware + Default + 'static>(mut self) -> Self {
268 self.service_configs.push(Box::new(|svc| {
269 svc.singleton::<dyn IMiddleware>(|_| Arc::new(T::default()) as Arc<dyn IMiddleware>)
270 }));
271 self
272 }
273
274 pub fn use_middleware_with<F>(mut self, factory: F) -> Self
286 where
287 F: Fn() -> Arc<dyn IMiddleware> + Send + Sync + 'static,
288 {
289 self.service_configs.push(Box::new(move |svc| {
290 svc.singleton::<dyn IMiddleware>(move |_| factory())
291 }));
292 self
293 }
294
295 pub fn add_health_check<F>(self, name: impl Into<String>, check: F) -> Self
308 where
309 F: Fn() -> HealthStatus + Send + Sync + 'static,
310 {
311 self.health_registry.register(name, Arc::new(check));
312 self
313 }
314
315 pub fn build(self) -> Host {
316 let env_filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into());
319 if self.mode == AppMode::Development {
320 let _ = tracing_subscriber::fmt()
321 .with_env_filter(env_filter)
322 .without_time()
323 .with_target(false)
324 .try_init();
325 } else {
326 let _ = tracing_subscriber::fmt()
327 .json()
328 .with_env_filter(env_filter)
329 .try_init();
330 }
331
332 let mut svc = ServiceCollection::from_injected();
333 for cfg in self.service_configs {
334 svc = cfg(svc);
335 }
336
337 let health_registry = Arc::clone(&self.health_registry);
339 svc = svc.instance(health_registry);
340
341 let provider = svc.build().unwrap_or_else(|e| {
342 panic!(
343 "Failed to build ServiceProvider: {}. Check your DI registrations.",
344 e
345 )
346 });
347
348 let handler_cache = Arc::new(rust_webx_core::route::scan::HandlerCache::build());
350 let dispatch_runtime = Arc::new(DispatchRuntime::new(Arc::clone(&provider), handler_cache));
351
352 let appsettings =
353 config::load_appsettings(self.mode).unwrap_or_else(|| serde_json::json!({}));
354 let mut options: AppOptions = config::bind_root(&appsettings);
355 for modifier in self.options_modifiers {
356 modifier(&mut options);
357 }
358
359 let body_limits = BodyLimits::from_options(&options);
361 body_limits.apply_spool_root();
362 tracing::info!(
363 "[Host] Uploads: max request {} bytes, max file {} bytes, spool >{} bytes to {}",
364 body_limits.multipart_body,
365 body_limits.file,
366 body_limits.memory_threshold,
367 body_limits.spool_dir.display()
368 );
369
370 let http_metrics = HttpMetrics::new();
371
372 let mut pipeline = MiddlewarePipeline::new();
373
374 pipeline.add_middleware(Arc::new(
377 crate::security_headers::SecurityHeadersMiddleware::new(),
378 ));
379 pipeline.add_middleware(Arc::new(crate::request_id::RequestIdMiddleware::new()));
380
381 if options.rate_limit.enabled {
382 tracing::info!(
383 "[Host] RateLimit enabled: {} req/s, burst {}, max_ips {}",
384 options.rate_limit.requests_per_second,
385 options.rate_limit.burst_size,
386 options.rate_limit.max_tracked_ips
387 );
388 pipeline.add_middleware(Arc::new(RateLimitMiddleware::from_config(
389 &options.rate_limit,
390 )));
391 }
392
393 if options.metrics.enabled {
394 tracing::info!("[Host] Metrics enabled at GET /metrics");
395 pipeline.add_middleware(Arc::new(MetricsMiddleware::new(Arc::clone(&http_metrics))));
396 }
397
398 let middlewares: Vec<Arc<dyn IMiddleware>> = provider.get_all::<dyn IMiddleware>();
399 for mw in middlewares {
400 pipeline.add_middleware(mw);
401 }
402
403 let cors = self.cors_config.unwrap_or_else(|| {
404 let cs = &options.cors;
405 CorsConfig {
406 origins: cs.origins.clone(),
407 methods: cs.methods.clone(),
408 headers: cs.headers.clone(),
409 expose_headers: cs.expose_headers.clone(),
410 allow_credentials: cs.allow_credentials,
411 max_age: cs.max_age,
412 }
413 });
414 pipeline.add_middleware(Arc::new(CorsMiddleware::new(cors)));
415
416 if self.auth_enabled {
417 let secret = options.jwt.secret.clone();
418 if !secret.is_empty() {
419 let jwt_auth = Arc::new(JwtAuth::new(
420 DecodingKey::from_secret(secret.as_bytes()),
421 Validation::default(),
422 ));
423 pipeline.add_middleware(Arc::new(jwt_middleware(jwt_auth)));
424 init_jwt_secret(&secret);
425 } else {
426 tracing::warn!(
427 "add_authentication() enabled but no JWT secret configured. Set Jwt.Secret in appsettings.json, JWT_SECRET, or APP__Jwt__Secret env var."
428 );
429 }
430 }
431
432 let spa_root = if self.spa_disabled {
439 None
440 } else {
441 self.spa_root.clone().or_else(|| {
442 let candidate = rust_webx_core::paths::app_base().join("wwwroot");
443 if candidate.is_dir() {
444 tracing::info!("[Host] Auto-detected SPA root: {}", candidate.display());
445 Some(candidate.to_string_lossy().into_owned())
446 } else {
447 None
448 }
449 })
450 };
451 if let Some(ref spa_root) = spa_root {
452 pipeline.add_middleware(Arc::new(SpaMiddleware::new(spa_root.clone())));
453 }
454
455 let pipeline = Arc::new(pipeline);
460
461 if self.mode == AppMode::Production
463 && self.auth_enabled
464 && is_weak_jwt_secret(&options.jwt.secret)
465 {
466 panic!(
467 "Production requires a strong JWT secret. Set APP__Jwt__Secret or JWT_SECRET env var (min 32 chars, not a placeholder)."
468 );
469 }
470
471 if self.mode == AppMode::Production && options.cors.origins.iter().any(|o| o == "*") {
473 panic!(
474 "Production forbids CORS origin '*'. Set explicit origins via Cors.Origins or APP__Cors__Origins."
475 );
476 }
477
478 let mut router = Router::new();
479 let mut route_count = 0usize;
480
481 crate::diagnostics::assert_route_configuration_valid();
482
483 let mut dispatch_map: std::collections::HashMap<
485 &'static str,
486 rust_webx_core::route::scan::RouteDispatchFn,
487 > = std::collections::HashMap::new();
488
489 for dispatch in inventory::iter::<rust_webx_core::route::scan::RouteDispatch> {
490 dispatch_map.insert(dispatch.handler_type, dispatch.dispatch);
491 }
492
493 let resource_policy: Option<Arc<crate::authz::ResourceAuthorization>> =
494 if self.resource_auth_enabled {
495 Some(Arc::new(build_resource_policy_from_routes()))
496 } else {
497 None
498 };
499
500 for entry in inventory::iter::<RouteEntry> {
501 route_count += 1;
502 let stub = Arc::new(StubEndpoint {
503 method: entry.method.as_str(),
504 path: entry.path,
505 handler_type: entry.handler_type,
506 dispatch_fn: dispatch_map.get(entry.handler_type).copied(),
507 auth_required_role: entry.required_role,
508 auth_required_permission: entry.required_permission,
509 resource_policy: resource_policy.clone(),
510 authorizers: collect_authorizers(provider.as_ref()),
511 });
512 router.register(entry.method, entry.path, stub);
513 }
514
515 let openapi_spec = generate_openapi_spec("Rust WebX API", env!("CARGO_PKG_VERSION"));
516 if self.mode == AppMode::Development {
517 let openapi_bytes = serde_json::to_vec(&openapi_spec).unwrap_or_default();
518 router.register(
519 HttpMethod::Get,
520 "/api/openapi.json",
521 Arc::new(StaticJsonEndpoint::new(openapi_bytes)),
522 );
523 router.register(
524 HttpMethod::Get,
525 "/api/openapi.html",
526 Arc::new(StaticHtmlEndpoint { body: APIUI_HTML }),
527 );
528 }
529
530 let health_registry = Arc::clone(&self.health_registry);
533 let health_endpoint: Arc<dyn IEndpoint> =
534 Arc::new(HealthCheckEndpoint::new(Arc::clone(&health_registry)));
535 router.register(HttpMethod::Get, "/health", Arc::clone(&health_endpoint));
536 router.register(HttpMethod::Get, "/healthz", health_endpoint);
537
538 let live_body =
540 serde_json::to_vec(&serde_json::json!({"status":"pass"})).unwrap_or_default();
541 router.register(
542 HttpMethod::Get,
543 "/health/live",
544 Arc::new(StaticJsonEndpoint {
545 body: live_body,
546 content_type: "application/health+json",
547 }),
548 );
549
550 router.register(
552 HttpMethod::Get,
553 "/health/ready",
554 Arc::new(HealthCheckEndpoint::new(health_registry)),
555 );
556
557 if options.metrics.enabled {
558 router.register(
559 HttpMethod::Get,
560 "/metrics",
561 Arc::new(MetricsEndpoint::new(http_metrics)),
562 );
563 }
564
565 if self.mode == AppMode::Development {
566 let version = env!("CARGO_PKG_VERSION");
567 tracing::info!("");
568 tracing::info!(" ----------------------------------------------------------------");
569 tracing::info!(" Rust WebX Framework v{}", version);
570 tracing::info!(" ----------------------------------------------------------------");
571 tracing::info!(" App: {}", options.app.name);
572 tracing::info!(" CORS: enabled");
573 if let Some(ref root) = self.spa_root {
574 tracing::info!(" SPA Root: {}", root);
575 }
576 if route_count > 0 {
577 tracing::info!(" Routes: {} registered", route_count);
578 }
579 crate::diagnostics::log_startup_diagnostics();
580 let banner_urls = if options.app.urls.is_empty() {
581 vec!["http://localhost:5000".to_string()]
582 } else {
583 options
584 .app
585 .urls
586 .iter()
587 .map(|u| u.replace("0.0.0.0", "localhost"))
588 .collect::<Vec<_>>()
589 };
590 for url in &banner_urls {
591 tracing::info!(" OpenAPI: {}/api/openapi.html", url);
592 tracing::info!(" OpenAPI: {}/api/openapi.json", url);
593 }
594 tracing::info!(" ----------------------------------------------------------------");
595 tracing::info!("");
596 } else if route_count > 0 {
597 tracing::info!("{} route(s) registered", route_count);
598 }
599
600 let router = Arc::new(router);
601 let router_handler = make_router_handler(Arc::clone(&router));
602
603 let hosted_services: Vec<Arc<dyn IHostedService>> =
606 provider.get_all::<dyn IHostedService>();
607
608 Host {
609 provider,
610 dispatch_runtime,
611 options,
612 body_limits,
613 pipeline,
614 router,
615 router_handler,
616 mode: self.mode,
617 spa_root: self.spa_root,
618 shutdown: Arc::new(tokio::sync::Notify::new()),
619 hosted_services,
620 }
621 }
622}
623
624pub struct ServerHandle {
626 shutdown: Arc<tokio::sync::Notify>,
627}
628
629impl ServerHandle {
630 pub fn shutdown(self) {
633 self.shutdown.notify_waiters();
634 }
635}
636
637pub struct Server {
649 host: Host,
650}
651
652impl Server {
653 pub async fn run(self) -> Result<()> {
655 self.host.run().await
656 }
657
658 pub async fn run_at(self, addr: &str) -> Result<()> {
660 self.host.run_at(addr).await
661 }
662
663 pub fn handle(&self) -> ServerHandle {
665 self.host.server_handle()
666 }
667}
668
669impl Default for HostBuilder {
670 fn default() -> Self {
671 Self::new()
672 }
673}
674
675impl Host {
676 pub fn builder() -> HostBuilder {
677 HostBuilder::new()
678 }
679
680 pub fn options(&self) -> &AppOptions {
681 &self.options
682 }
683
684 pub fn provider(&self) -> &Arc<ServiceProvider> {
686 &self.provider
687 }
688
689 pub fn dispatch_runtime(&self) -> &Arc<DispatchRuntime> {
691 &self.dispatch_runtime
692 }
693
694 pub async fn run(&self) -> Result<()> {
708 start_hosted_services(&self.hosted_services, &self.dispatch_runtime).await?;
709
710 let urls = if self.options.app.urls.is_empty() {
711 vec!["http://0.0.0.0:5000".to_string()]
712 } else {
713 self.options.app.urls.clone()
714 };
715
716 let mut http_addrs: Vec<String> = Vec::new();
717 let mut https_addrs: Vec<String> = Vec::new();
718
719 for url in &urls {
720 let (scheme, addr) = parse_url(url)?;
721 match scheme {
722 "http" => http_addrs.push(addr),
723 "https" => https_addrs.push(addr),
724 other => {
725 return Err(rust_webx_core::error::Error::Http(format!(
726 "Unsupported URL scheme '{}' in '{}'",
727 other, url
728 )))
729 }
730 }
731 }
732
733 let acceptor = if !https_addrs.is_empty() {
734 let tls = &self.options.tls;
735 if tls.cert_path.is_empty() || tls.key_path.is_empty() {
736 return Err(rust_webx_core::error::Error::Http(
737 "HTTPS URLs require Tls.CertPath and Tls.KeyPath".into(),
738 ));
739 }
740 Some(build_tls_acceptor(&tls.cert_path, &tls.key_path)?)
741 } else {
742 None
743 };
744
745 if self.mode == AppMode::Development {
747 tracing::info!("");
748 for url in &urls {
749 let display_url = url.replace("0.0.0.0", "localhost");
750 tracing::info!(" Listening on {}", display_url);
751 }
752 } else {
753 tracing::info!("Listening on {} url(s)", urls.len());
754 }
755
756 let notify = Arc::clone(&self.shutdown);
757 install_shutdown_handler(Arc::clone(¬ify));
758 spawn_async_shutdown_listener(Arc::clone(¬ify));
759
760 let mut handles = Vec::new();
761 let pipeline = Arc::clone(&self.pipeline);
762 let router_handler = self.router_handler.clone();
763 let dispatch_runtime = Arc::clone(&self.dispatch_runtime);
764 let mode = self.mode;
765 let body_limits = self.body_limits.clone();
766 let max_connections = self.options.app.max_connections;
767
768 for addr in &http_addrs {
769 let addr = addr.clone();
770 let n = std::sync::Arc::clone(¬ify);
771 let p = Arc::clone(&pipeline);
772 let rh = router_handler.clone();
773 let dr = Arc::clone(&dispatch_runtime);
774 handles.push(tokio::spawn(serve_http(
775 addr,
776 n,
777 p,
778 rh,
779 dr,
780 mode,
781 body_limits.clone(),
782 max_connections,
783 )));
784 }
785
786 if let Some(ref tls_acceptor) = acceptor {
787 for addr in &https_addrs {
788 let addr = addr.clone();
789 let n = std::sync::Arc::clone(¬ify);
790 let p = Arc::clone(&pipeline);
791 let rh = router_handler.clone();
792 let dr = Arc::clone(&dispatch_runtime);
793 let a = tls_acceptor.clone();
794 handles.push(tokio::spawn(serve_https(
795 addr,
796 a,
797 n,
798 p,
799 rh,
800 dr,
801 mode,
802 body_limits.clone(),
803 max_connections,
804 )));
805 }
806 }
807
808 for h in handles {
810 let _ = h.await;
811 }
812
813 stop_hosted_services(&self.hosted_services).await;
814
815 Ok(())
816 }
817
818 pub async fn run_at(&self, addr: &str) -> Result<()> {
820 start_hosted_services(&self.hosted_services, &self.dispatch_runtime).await?;
821
822 let notify = Arc::clone(&self.shutdown);
823 install_shutdown_handler(Arc::clone(¬ify));
824 spawn_async_shutdown_listener(Arc::clone(¬ify));
825 serve_http(
826 addr.to_string(),
827 notify,
828 Arc::clone(&self.pipeline),
829 self.router_handler.clone(),
830 Arc::clone(&self.dispatch_runtime),
831 self.mode,
832 self.body_limits.clone(),
833 self.options.app.max_connections,
834 )
835 .await;
836
837 stop_hosted_services(&self.hosted_services).await;
838 Ok(())
839 }
840
841 pub fn server_handle(&self) -> ServerHandle {
852 ServerHandle {
853 shutdown: Arc::clone(&self.shutdown),
854 }
855 }
856
857 pub fn into_server(self) -> Server {
862 Server { host: self }
863 }
864}
865
866#[async_trait::async_trait]
867impl IHost for Host {
868 async fn run(&self, addr: &str) -> Result<()> {
869 self.run_at(addr).await
870 }
871 async fn stop(&self) -> Result<()> {
872 tracing::info!("Stop requested.");
873 self.shutdown.notify_waiters();
874 Ok(())
875 }
876}
877
878fn make_router_handler(router: Arc<Router>) -> HandlerFn {
879 Arc::new(move |ctx: &mut dyn IHttpContext| {
880 let router = Arc::clone(&router);
881 Box::pin(async move {
882 match router.match_route(ctx).await? {
883 Some((endpoint, params, pattern)) => {
884 drop(router);
885 for (key, value) in params {
886 ctx.request_mut().route_params_mut().insert(key, value);
887 }
888 *ctx.request_mut().route_pattern_mut() = Some(pattern);
889 endpoint.handle(ctx).await
890 }
891 None => {
892 let path_exists = router.path_exists(ctx.request().path());
893 drop(router);
894 if !ctx.response().has_body() {
897 if path_exists {
898 write_error_response(ctx, 405, "Method Not Allowed").await;
899 } else {
900 write_error_response(ctx, 404, "Not Found").await;
901 }
902 }
903 Ok(())
904 }
905 }
906 })
907 })
908}
909
910async fn handle_request(
911 req: Request<Incoming>,
912 pipeline: Arc<MiddlewarePipeline>,
913 router_handler: HandlerFn,
914 dispatch_runtime: Arc<DispatchRuntime>,
915 body_limits: BodyLimits,
916) -> std::result::Result<hyper::Response<RespBody>, std::convert::Infallible> {
917 let mut ctx = HttpContext::new(req, body_limits);
918
919 if ctx.response().status() < 400 {
922 let result = dispatch_runtime
923 .run(async { pipeline.execute(&mut ctx, router_handler).await })
924 .await;
925 if let Err(e) = result {
926 let status = e.status_code();
927 write_error_response(&mut ctx, status, &e.to_string()).await;
928 }
929 }
930
931 Ok(ctx.into_response().await)
932}
933
934async fn write_error_response(ctx: &mut dyn IHttpContext, status: u16, message: &str) {
935 write_problem_response(ctx, build_problem(status, message)).await;
936}
937
938fn install_shutdown_handler(shutdown: Arc<tokio::sync::Notify>) {
948 use std::sync::atomic::{AtomicUsize, Ordering};
949
950 static CTRL_COUNT: AtomicUsize = AtomicUsize::new(0);
951
952 if let Err(e) = ctrlc::set_handler(move || {
953 let n = CTRL_COUNT.fetch_add(1, Ordering::SeqCst);
954 if n == 0 {
955 tracing::info!("Shutdown signal received, draining connections...");
956 shutdown.notify_waiters();
957 } else {
958 tracing::warn!("Second interrupt — force exiting.");
959 std::process::exit(130);
960 }
961 }) {
962 tracing::warn!("Could not install Ctrl+C handler: {}", e);
963 }
964}
965
966fn spawn_async_shutdown_listener(shutdown: Arc<tokio::sync::Notify>) {
968 tokio::spawn(async move {
969 wait_for_shutdown_signal().await;
970 tracing::info!("Async shutdown signal received, draining connections...");
971 shutdown.notify_waiters();
972 });
973}
974
975async fn wait_for_shutdown_signal() {
976 #[cfg(unix)]
977 {
978 use tokio::signal::unix::{signal, SignalKind};
979 let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
980 tokio::select! {
981 res = tokio::signal::ctrl_c() => {
982 if let Err(e) = res {
983 tracing::warn!("Ctrl+C listener error: {}", e);
984 }
985 }
986 _ = sigterm.recv() => {}
987 }
988 }
989 #[cfg(not(unix))]
990 {
991 if let Err(e) = tokio::signal::ctrl_c().await {
992 tracing::warn!("Ctrl+C listener error: {}", e);
993 }
994 }
995}
996
997async fn start_hosted_services(
998 services: &[Arc<dyn IHostedService>],
999 dispatch_runtime: &DispatchRuntime,
1000) -> Result<()> {
1001 if services.is_empty() {
1002 return Ok(());
1003 }
1004 tracing::info!("Starting {} hosted service(s)...", services.len());
1005 dispatch_runtime
1006 .run(async {
1007 for svc in services {
1008 svc.start().await?;
1009 }
1010 Ok::<(), rust_webx_core::error::Error>(())
1011 })
1012 .await?;
1013 tracing::info!("All hosted services started.");
1014 Ok(())
1015}
1016
1017async fn stop_hosted_services(services: &[Arc<dyn IHostedService>]) {
1018 if services.is_empty() {
1019 return;
1020 }
1021 tracing::info!("Stopping {} hosted service(s)...", services.len());
1022 for svc in services {
1023 if let Err(e) = svc.stop().await {
1024 tracing::warn!("Hosted service stop error: {}", e);
1025 }
1026 }
1027 tracing::info!("All hosted services stopped.");
1028}
1029
1030async fn drain_connections(join_set: &mut JoinSet<()>, label: &str, mode: AppMode) {
1031 let timeout_secs = if mode == AppMode::Development { 5 } else { 30 };
1032 let drain_future = async {
1033 while let Some(result) = join_set.join_next().await {
1034 if let Err(e) = result {
1035 tracing::error!("Connection task panicked: {:?}", e);
1036 }
1037 }
1038 };
1039
1040 match tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), drain_future).await {
1041 Ok(_) => tracing::info!("{}: drained.", label),
1042 Err(_) => {
1043 tracing::warn!("{}: drain timeout, aborting connections.", label);
1044 join_set.abort_all();
1045 while join_set.join_next().await.is_some() {}
1046 }
1047 }
1048}
1049
1050fn parse_url(url: &str) -> Result<(&str, String)> {
1057 if let Some(rest) = url.strip_prefix("https://") {
1058 Ok(("https", rest.to_string()))
1059 } else if let Some(rest) = url.strip_prefix("http://") {
1060 Ok(("http", rest.to_string()))
1061 } else {
1062 Err(rust_webx_core::error::Error::Http(format!(
1063 "Invalid URL '{}'. Use http://host:port or https://host:port",
1064 url
1065 )))
1066 }
1067}
1068
1069#[allow(clippy::too_many_arguments)]
1071async fn serve_http(
1072 addr: String,
1073 shutdown: std::sync::Arc<tokio::sync::Notify>,
1074 pipeline: Arc<MiddlewarePipeline>,
1075 router_handler: HandlerFn,
1076 dispatch_runtime: Arc<DispatchRuntime>,
1077 mode: AppMode,
1078 body_limits: BodyLimits,
1079 max_connections: usize,
1080) {
1081 let listener = match tokio::net::TcpListener::bind(&addr).await {
1082 Ok(l) => l,
1083 Err(e) => {
1084 tracing::error!("Failed to bind HTTP on {}: {}", addr, e);
1085 return;
1086 }
1087 };
1088
1089 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1090 let mut join_set = JoinSet::new();
1091
1092 loop {
1093 tokio::select! {
1094 accept_result = listener.accept() => {
1095 let (stream, _) = match accept_result {
1096 Ok(v) => v,
1097 Err(e) => {
1098 tracing::error!("Accept error (will retry): {}", e);
1099 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1100 continue;
1101 }
1102 };
1103
1104 while join_set.try_join_next().is_some() {}
1105
1106 let permit = match sem.clone().acquire_owned().await {
1107 Ok(p) => p,
1108 Err(_) => continue,
1109 };
1110
1111 let io = TokioIo::new(stream);
1112 let pipeline = Arc::clone(&pipeline);
1113 let router_handler = router_handler.clone();
1114 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1115 let body_limits = body_limits.clone();
1116
1117 join_set.spawn(async move {
1118 let _permit = permit;
1119 let svc_fn = service_fn(move |req: Request<Incoming>| {
1120 let pipeline = Arc::clone(&pipeline);
1121 let router_handler = router_handler.clone();
1122 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1123 let body_limits = body_limits.clone();
1124 let mode = mode;
1125 async move {
1126 let start = Instant::now();
1127 let method = req.method().to_string();
1128 let path = req.uri().path().to_string();
1129 let result = handle_request(
1130 req,
1131 pipeline,
1132 router_handler,
1133 dispatch_runtime,
1134 body_limits,
1135 )
1136 .await;
1137 let elapsed = start.elapsed();
1138 if mode == AppMode::Development {
1139 let status =
1140 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1141 tracing::info!(
1142 "[{}] {} -> {} ({:.0}ms)",
1143 method,
1144 path,
1145 status,
1146 elapsed.as_secs_f64() * 1000.0
1147 );
1148 }
1149 result
1150 }
1151 });
1152
1153 if let Err(err) = hyper::server::conn::http1::Builder::new()
1154 .serve_connection(io, svc_fn)
1155 .await
1156 {
1157 tracing::error!("Connection error: {}", err);
1158 }
1159 });
1160 }
1161 _ = shutdown.notified() => {
1162 tracing::info!("HTTP {}: stop accepting, draining...", addr);
1163 break;
1164 }
1165 }
1166 }
1167
1168 drain_connections(&mut join_set, &format!("HTTP {}", addr), mode).await;
1169}
1170
1171#[allow(clippy::too_many_arguments)]
1173async fn serve_https(
1174 addr: String,
1175 acceptor: TlsAcceptor,
1176 shutdown: std::sync::Arc<tokio::sync::Notify>,
1177 pipeline: Arc<MiddlewarePipeline>,
1178 router_handler: HandlerFn,
1179 dispatch_runtime: Arc<DispatchRuntime>,
1180 mode: AppMode,
1181 body_limits: BodyLimits,
1182 max_connections: usize,
1183) {
1184 let listener = match tokio::net::TcpListener::bind(&addr).await {
1185 Ok(l) => l,
1186 Err(e) => {
1187 tracing::error!("Failed to bind HTTPS on {}: {}", addr, e);
1188 return;
1189 }
1190 };
1191
1192 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1193 let mut join_set = JoinSet::new();
1194
1195 loop {
1196 tokio::select! {
1197 accept_result = listener.accept() => {
1198 let (stream, _) = match accept_result {
1199 Ok(v) => v,
1200 Err(e) => {
1201 tracing::error!("Accept error (will retry): {}", e);
1202 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1203 continue;
1204 }
1205 };
1206
1207 while join_set.try_join_next().is_some() {}
1208
1209 let permit = match sem.clone().acquire_owned().await {
1210 Ok(p) => p,
1211 Err(_) => continue,
1212 };
1213
1214 let acceptor = acceptor.clone();
1215 let pipeline = Arc::clone(&pipeline);
1216 let router_handler = router_handler.clone();
1217 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1218 let body_limits = body_limits.clone();
1219
1220 join_set.spawn(async move {
1221 let _permit = permit;
1222 match acceptor.accept(stream).await {
1223 Ok(tls_stream) => {
1224 let io = TokioIo::new(tls_stream);
1225 let svc_fn = service_fn(move |req: Request<Incoming>| {
1226 let pipeline = Arc::clone(&pipeline);
1227 let router_handler = router_handler.clone();
1228 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1229 let body_limits = body_limits.clone();
1230 let mode = mode;
1231 async move {
1232 let start = Instant::now();
1233 let method = req.method().to_string();
1234 let path = req.uri().path().to_string();
1235 let result = handle_request(
1236 req,
1237 pipeline,
1238 router_handler,
1239 dispatch_runtime,
1240 body_limits,
1241 )
1242 .await;
1243 let elapsed = start.elapsed();
1244 if mode == AppMode::Development {
1245 let status =
1246 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1247 tracing::info!(
1248 "[{}] {} -> {} ({:.0}ms)",
1249 method,
1250 path,
1251 status,
1252 elapsed.as_secs_f64() * 1000.0
1253 );
1254 }
1255 result
1256 }
1257 });
1258
1259 if let Err(err) = hyper::server::conn::http1::Builder::new()
1260 .serve_connection(io, svc_fn)
1261 .await
1262 {
1263 tracing::error!("TLS connection error: {}", err);
1264 }
1265 }
1266 Err(e) => {
1267 tracing::error!("TLS handshake error: {}", e);
1268 }
1269 }
1270 });
1271 }
1272 _ = shutdown.notified() => {
1273 tracing::info!("HTTPS {}: stop accepting, draining...", addr);
1274 break;
1275 }
1276 }
1277 }
1278
1279 drain_connections(&mut join_set, &format!("HTTPS {}", addr), mode).await;
1280}
1281
1282fn build_tls_acceptor(cert_path: &str, key_path: &str) -> Result<TlsAcceptor> {
1288 use std::fs::File;
1289 use std::io::BufReader;
1290
1291 if cert_path.is_empty() || key_path.is_empty() {
1292 return Err(rust_webx_core::error::Error::Http(
1293 "TLS certificate or key path not configured.".into(),
1294 ));
1295 }
1296
1297 let cert_file = File::open(cert_path).map_err(|e| {
1298 rust_webx_core::error::Error::Http(format!("Cannot open cert '{}': {}", cert_path, e))
1299 })?;
1300 let mut cert_reader = BufReader::new(cert_file);
1301 let certs: Vec<CertificateDer> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
1302 if certs.is_empty() {
1303 return Err(rust_webx_core::error::Error::Http(format!(
1304 "No valid certs in '{}'",
1305 cert_path
1306 )));
1307 }
1308
1309 let key_file = File::open(key_path).map_err(|e| {
1310 rust_webx_core::error::Error::Http(format!("Cannot open key '{}': {}", key_path, e))
1311 })?;
1312 let mut key_reader = BufReader::new(key_file);
1313 let key = pkcs8_private_keys(&mut key_reader)
1314 .filter_map(|r| r.ok())
1315 .map(PrivateKeyDer::from)
1316 .next()
1317 .or_else(|| {
1318 let key_file2 = File::open(key_path).map(BufReader::new).ok()?;
1319 let mut kr2 = key_file2;
1320 let rsa_keys: Vec<PrivateKeyDer> = rsa_private_keys(&mut kr2)
1321 .filter_map(|r| r.ok())
1322 .map(PrivateKeyDer::from)
1323 .collect();
1324 rsa_keys.into_iter().next()
1325 })
1326 .ok_or_else(|| {
1327 rust_webx_core::error::Error::Http(format!("No valid private key in '{}'", key_path))
1328 })?;
1329
1330 let config = ServerConfig::builder()
1331 .with_no_client_auth()
1332 .with_single_cert(certs, key)
1333 .map_err(|e| rust_webx_core::error::Error::Http(format!("TLS config error: {}", e)))?;
1334
1335 Ok(TlsAcceptor::from(std::sync::Arc::new(config)))
1336}