1use http_body_util::Full;
8use hyper::body::{Bytes, Incoming};
9use hyper::service::service_fn;
10use hyper::Request;
11use hyper_util::rt::TokioIo;
12use rust_dix::{ServiceCollection, ServiceProvider};
13use rust_webx_core::app::IHost;
14use rust_webx_core::config::{self, AppOptions};
15use rust_webx_core::error::Result;
16use rust_webx_core::handler::IHostedService;
17use rust_webx_core::http::IHttpContext;
18use rust_webx_core::middleware::IMiddleware;
19use rust_webx_core::mode::AppMode;
20use rust_webx_core::routing::{HttpMethod, IEndpoint, IRouter};
21
22use crate::auth_jwt::{init_jwt_secret, jwt_middleware, JwtAuth};
23use crate::authz::{build_resource_policy_from_routes, collect_authorizers};
24use crate::context::HttpContext;
25use crate::cors::{CorsConfig, CorsMiddleware};
26use crate::endpoint::{StaticHtmlEndpoint, StaticJsonEndpoint, StubEndpoint};
27use crate::health::{HealthCheckEndpoint, HealthCheckRegistry, HealthStatus};
28use crate::memory_cache::MemoryCache;
29use crate::metrics::{HttpMetrics, MetricsEndpoint, MetricsMiddleware};
30use crate::problem_response::{build_problem, write_problem_response};
31use crate::rate_limit::RateLimitMiddleware;
32use crate::pipeline::{HandlerFn, MiddlewarePipeline};
33use crate::router::Router;
34use jsonwebtoken::{DecodingKey, Validation};
35use rust_webx_core::route::scan::RouteEntry;
36use rust_webx_core::DispatchRuntime;
37use rust_webx_openapi::{generate_openapi_spec, APIUI_HTML};
38use rust_webx_spa::SpaMiddleware;
39
40use rustls::pki_types::{CertificateDer, PrivateKeyDer};
41use rustls::ServerConfig;
42use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
43use std::sync::Arc;
44use std::time::Instant;
45use tokio::task::JoinSet;
46use tokio_rustls::TlsAcceptor;
47
48fn is_weak_jwt_secret(secret: &str) -> bool {
50 secret.is_empty()
51 || secret.len() < 32
52 || secret.contains("change-in-production")
53 || secret.contains("please-change")
54 || secret.contains("demo-secret")
55 || secret.contains("dev-secret")
56}
57
58pub struct Host {
59 #[allow(dead_code)]
60 provider: Arc<ServiceProvider>,
61 dispatch_runtime: Arc<DispatchRuntime>,
62 pub options: AppOptions,
63 pipeline: Arc<MiddlewarePipeline>,
64 #[allow(dead_code)]
66 router: Arc<Router>,
67 router_handler: HandlerFn,
69 mode: AppMode,
70 #[allow(dead_code)]
71 spa_root: Option<String>,
72 shutdown: Arc<tokio::sync::Notify>,
73 hosted_services: Vec<Arc<dyn IHostedService>>,
76}
77
78#[allow(clippy::type_complexity)]
79pub struct HostBuilder {
80 service_configs: Vec<Box<dyn FnOnce(ServiceCollection) -> ServiceCollection + Send>>,
81 mode: AppMode,
82 spa_root: Option<String>,
83 spa_disabled: bool,
84 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
85 cors_config: Option<CorsConfig>,
86 auth_enabled: bool,
87 resource_auth_enabled: bool,
88 health_registry: Arc<HealthCheckRegistry>,
89}
90
91#[allow(clippy::type_complexity)]
92pub struct HostAppBuilder {
93 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
94}
95
96impl HostAppBuilder {
97 fn new() -> Self {
98 Self {
99 options_modifiers: Vec::new(),
100 }
101 }
102
103 #[allow(non_snake_case)]
104 pub fn useOptions<F>(&mut self, f: F)
105 where
106 F: FnOnce(&mut AppOptions) + Send + 'static,
107 {
108 self.options_modifiers.push(Box::new(f));
109 }
110}
111
112impl HostBuilder {
113 pub fn new() -> Self {
114 Self {
115 service_configs: Vec::new(),
116 mode: AppMode::from_env(),
119 spa_root: None,
120 spa_disabled: false,
121 options_modifiers: Vec::new(),
122 cors_config: None,
123 auth_enabled: false,
124 resource_auth_enabled: false,
125 health_registry: HealthCheckRegistry::new(),
126 }
127 }
128
129 pub fn register<F>(mut self, f: F) -> Self
130 where
131 F: FnOnce(ServiceCollection) -> ServiceCollection + Send + 'static,
132 {
133 self.service_configs.push(Box::new(f));
134 self
135 }
136
137 pub fn configure<F>(mut self, f: F) -> Self
138 where
139 F: FnOnce(&mut HostAppBuilder) + Send + 'static,
140 {
141 let mut builder = HostAppBuilder::new();
142 f(&mut builder);
143 self.options_modifiers
144 .append(&mut builder.options_modifiers);
145 self
146 }
147
148 pub fn mode(mut self, mode: AppMode) -> Self {
149 self.mode = mode;
150 self
151 }
152
153 pub fn add_options<T>(self, section: &str) -> Self
165 where
166 T: for<'de> serde::Deserialize<'de> + Default + Send + Sync + 'static,
167 {
168 let mode = self.mode;
169 let section = section.to_string();
170 self.register(move |svc| {
171 let appsettings = config::load_appsettings(mode).unwrap_or_default();
172 let bound: T = config::bind_config(&appsettings, §ion);
173 tracing::info!(
174 "[Host] add_options<{}>(\"{}\") registered",
175 std::any::type_name::<T>(),
176 section
177 );
178 svc.instance(Arc::new(bound))
179 })
180 }
181
182 pub fn use_spa(mut self, root: impl Into<String>) -> Self {
183 self.spa_root = Some(root.into());
184 self
185 }
186
187 pub fn no_spa(mut self) -> Self {
193 self.spa_disabled = true;
194 self
195 }
196
197 pub fn use_cors(mut self, config: CorsConfig) -> Self {
198 self.cors_config = Some(config);
199 self
200 }
201
202 pub fn add_authentication(mut self) -> Self {
214 self.auth_enabled = true;
215 self
216 }
217
218 pub fn use_resource_authorization(mut self) -> Self {
224 self.resource_auth_enabled = true;
225 self
226 }
227
228 pub fn add_memory_cache(mut self) -> Self {
240 let cache = Arc::new(MemoryCache::new());
241 self.service_configs
242 .push(Box::new(move |svc| svc.instance(cache)));
243 self
244 }
245
246 pub fn add_memory_cache_with(mut self, max_entries: usize) -> Self {
248 let cache = Arc::new(MemoryCache::new().with_max_entries(max_entries));
249 self.service_configs
250 .push(Box::new(move |svc| svc.instance(Arc::clone(&cache))));
251 self
252 }
253
254 pub fn use_middleware<T: IMiddleware + Default + 'static>(mut self) -> Self {
266 self.service_configs.push(Box::new(|svc| {
267 svc.singleton::<dyn IMiddleware>(|_| Arc::new(T::default()) as Arc<dyn IMiddleware>)
268 }));
269 self
270 }
271
272 pub fn use_middleware_with<F>(mut self, factory: F) -> Self
284 where
285 F: Fn() -> Arc<dyn IMiddleware> + Send + Sync + 'static,
286 {
287 self.service_configs.push(Box::new(move |svc| {
288 svc.singleton::<dyn IMiddleware>(move |_| factory())
289 }));
290 self
291 }
292
293 pub fn add_health_check<F>(self, name: impl Into<String>, check: F) -> Self
306 where
307 F: Fn() -> HealthStatus + Send + Sync + 'static,
308 {
309 self.health_registry.register(name, Arc::new(check));
310 self
311 }
312
313 pub fn build(self) -> Host {
314 let env_filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into());
317 if self.mode == AppMode::Development {
318 let _ = tracing_subscriber::fmt()
319 .with_env_filter(env_filter)
320 .without_time()
321 .with_target(false)
322 .try_init();
323 } else {
324 let _ = tracing_subscriber::fmt()
325 .json()
326 .with_env_filter(env_filter)
327 .try_init();
328 }
329
330 let mut svc = ServiceCollection::from_injected();
331 for cfg in self.service_configs {
332 svc = cfg(svc);
333 }
334
335 let health_registry = Arc::clone(&self.health_registry);
337 svc = svc.instance(health_registry);
338
339 let provider = svc.build().unwrap_or_else(|e| {
340 panic!(
341 "Failed to build ServiceProvider: {}. Check your DI registrations.",
342 e
343 )
344 });
345
346 let handler_cache = Arc::new(rust_webx_core::route::scan::HandlerCache::build());
348 let dispatch_runtime = Arc::new(DispatchRuntime::new(
349 Arc::clone(&provider),
350 handler_cache,
351 ));
352
353 let appsettings =
354 config::load_appsettings(self.mode).unwrap_or_else(|| serde_json::json!({}));
355 let mut options: AppOptions = config::bind_root(&appsettings);
356 for modifier in self.options_modifiers {
357 modifier(&mut options);
358 }
359
360 let http_metrics = HttpMetrics::new();
361
362 let mut pipeline = MiddlewarePipeline::new();
363
364 pipeline.add_middleware(Arc::new(crate::security_headers::SecurityHeadersMiddleware::new()));
367 pipeline.add_middleware(Arc::new(crate::request_id::RequestIdMiddleware::new()));
368
369 if options.rate_limit.enabled {
370 tracing::info!(
371 "[Host] RateLimit enabled: {} req/s, burst {}, max_ips {}",
372 options.rate_limit.requests_per_second,
373 options.rate_limit.burst_size,
374 options.rate_limit.max_tracked_ips
375 );
376 pipeline.add_middleware(Arc::new(RateLimitMiddleware::from_config(
377 &options.rate_limit,
378 )));
379 }
380
381 if options.metrics.enabled {
382 tracing::info!("[Host] Metrics enabled at GET /metrics");
383 pipeline.add_middleware(Arc::new(MetricsMiddleware::new(Arc::clone(
384 &http_metrics,
385 ))));
386 }
387
388 let middlewares: Vec<Arc<dyn IMiddleware>> = provider.get_all::<dyn IMiddleware>();
389 for mw in middlewares {
390 pipeline.add_middleware(mw);
391 }
392
393 let cors = self.cors_config.unwrap_or_else(|| {
394 let cs = &options.cors;
395 CorsConfig {
396 origins: cs.origins.clone(),
397 methods: cs.methods.clone(),
398 headers: cs.headers.clone(),
399 expose_headers: cs.expose_headers.clone(),
400 allow_credentials: cs.allow_credentials,
401 max_age: cs.max_age,
402 }
403 });
404 pipeline.add_middleware(Arc::new(CorsMiddleware::new(cors)));
405
406 if self.auth_enabled {
407 let secret = options.jwt.secret.clone();
408 if !secret.is_empty() {
409 let jwt_auth = Arc::new(JwtAuth::new(
410 DecodingKey::from_secret(secret.as_bytes()),
411 Validation::default(),
412 ));
413 pipeline.add_middleware(Arc::new(jwt_middleware(jwt_auth)));
414 init_jwt_secret(&secret);
415 } else {
416 tracing::warn!(
417 "add_authentication() enabled but no JWT secret configured. Set Jwt.Secret in appsettings.json, JWT_SECRET, or APP__Jwt__Secret env var."
418 );
419 }
420 }
421
422 let spa_root = if self.spa_disabled {
429 None
430 } else {
431 self.spa_root.clone().or_else(|| {
432 let candidate = rust_webx_core::paths::app_base().join("wwwroot");
433 if candidate.is_dir() {
434 tracing::info!("[Host] Auto-detected SPA root: {}", candidate.display());
435 Some(candidate.to_string_lossy().into_owned())
436 } else {
437 None
438 }
439 })
440 };
441 if let Some(ref spa_root) = spa_root {
442 pipeline.add_middleware(Arc::new(SpaMiddleware::new(spa_root.clone())));
443 }
444
445 let pipeline = Arc::new(pipeline);
450
451 if self.mode == AppMode::Production && self.auth_enabled && is_weak_jwt_secret(&options.jwt.secret)
453 {
454 panic!(
455 "Production requires a strong JWT secret. Set APP__Jwt__Secret or JWT_SECRET env var (min 32 chars, not a placeholder)."
456 );
457 }
458
459 if self.mode == AppMode::Production && options.cors.origins.iter().any(|o| o == "*") {
461 panic!(
462 "Production forbids CORS origin '*'. Set explicit origins via Cors.Origins or APP__Cors__Origins."
463 );
464 }
465
466 let mut router = Router::new();
467 let mut route_count = 0usize;
468
469 crate::diagnostics::assert_route_configuration_valid();
470
471 #[allow(clippy::type_complexity)]
473 let mut dispatch_map: std::collections::HashMap<
474 &'static str,
475 fn(
476 Vec<u8>,
477 std::collections::HashMap<String, String>,
478 std::collections::HashMap<String, String>,
479 Option<Box<dyn rust_webx_core::auth::IClaims>>,
480 ) -> std::pin::Pin<
481 Box<
482 dyn std::future::Future<
483 Output = rust_webx_core::error::Result<rust_webx_core::route::scan::ResponseData>,
484 > + Send,
485 >,
486 >,
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 = serde_json::to_vec(&serde_json::json!({"status":"pass"}))
540 .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 pipeline,
613 router,
614 router_handler,
615 mode: self.mode,
616 spa_root: self.spa_root,
617 shutdown: Arc::new(tokio::sync::Notify::new()),
618 hosted_services,
619 }
620 }
621}
622
623pub struct ServerHandle {
625 shutdown: Arc<tokio::sync::Notify>,
626}
627
628impl ServerHandle {
629 pub fn shutdown(self) {
632 self.shutdown.notify_waiters();
633 }
634}
635
636pub struct Server {
648 host: Host,
649}
650
651impl Server {
652 pub async fn run(self) -> Result<()> {
654 self.host.run().await
655 }
656
657 pub async fn run_at(self, addr: &str) -> Result<()> {
659 self.host.run_at(addr).await
660 }
661
662 pub fn handle(&self) -> ServerHandle {
664 self.host.server_handle()
665 }
666}
667
668impl Default for HostBuilder {
669 fn default() -> Self {
670 Self::new()
671 }
672}
673
674impl Host {
675 pub fn builder() -> HostBuilder {
676 HostBuilder::new()
677 }
678
679 pub fn options(&self) -> &AppOptions {
680 &self.options
681 }
682
683 pub fn provider(&self) -> &Arc<ServiceProvider> {
685 &self.provider
686 }
687
688 pub fn dispatch_runtime(&self) -> &Arc<DispatchRuntime> {
690 &self.dispatch_runtime
691 }
692
693 pub async fn run(&self) -> Result<()> {
707 start_hosted_services(&self.hosted_services, &self.dispatch_runtime).await?;
708
709 let urls = if self.options.app.urls.is_empty() {
710 vec!["http://0.0.0.0:5000".to_string()]
711 } else {
712 self.options.app.urls.clone()
713 };
714
715 let mut http_addrs: Vec<String> = Vec::new();
716 let mut https_addrs: Vec<String> = Vec::new();
717
718 for url in &urls {
719 let (scheme, addr) = parse_url(url)?;
720 match scheme {
721 "http" => http_addrs.push(addr),
722 "https" => https_addrs.push(addr),
723 other => {
724 return Err(rust_webx_core::error::Error::Http(format!(
725 "Unsupported URL scheme '{}' in '{}'",
726 other, url
727 )))
728 }
729 }
730 }
731
732 let acceptor = if !https_addrs.is_empty() {
733 let tls = &self.options.tls;
734 if tls.cert_path.is_empty() || tls.key_path.is_empty() {
735 return Err(rust_webx_core::error::Error::Http(
736 "HTTPS URLs require Tls.CertPath and Tls.KeyPath".into(),
737 ));
738 }
739 Some(build_tls_acceptor(&tls.cert_path, &tls.key_path)?)
740 } else {
741 None
742 };
743
744 if self.mode == AppMode::Development {
746 tracing::info!("");
747 for url in &urls {
748 let display_url = url.replace("0.0.0.0", "localhost");
749 tracing::info!(" Listening on {}", display_url);
750 }
751 } else {
752 tracing::info!("Listening on {} url(s)", urls.len());
753 }
754
755 let notify = Arc::clone(&self.shutdown);
756 install_shutdown_handler(Arc::clone(¬ify));
757 spawn_async_shutdown_listener(Arc::clone(¬ify));
758
759 let mut handles = Vec::new();
760 let pipeline = Arc::clone(&self.pipeline);
761 let router_handler = self.router_handler.clone();
762 let dispatch_runtime = Arc::clone(&self.dispatch_runtime);
763 let mode = self.mode;
764 let max_body_size = self.options.app.max_body_size;
765 let max_connections = self.options.app.max_connections;
766
767 for addr in &http_addrs {
768 let addr = addr.clone();
769 let n = std::sync::Arc::clone(¬ify);
770 let p = Arc::clone(&pipeline);
771 let rh = router_handler.clone();
772 let dr = Arc::clone(&dispatch_runtime);
773 handles.push(tokio::spawn(serve_http(
774 addr,
775 n,
776 p,
777 rh,
778 dr,
779 mode,
780 max_body_size,
781 max_connections,
782 )));
783 }
784
785 if let Some(ref tls_acceptor) = acceptor {
786 for addr in &https_addrs {
787 let addr = addr.clone();
788 let n = std::sync::Arc::clone(¬ify);
789 let p = Arc::clone(&pipeline);
790 let rh = router_handler.clone();
791 let dr = Arc::clone(&dispatch_runtime);
792 let a = tls_acceptor.clone();
793 handles.push(tokio::spawn(serve_https(
794 addr,
795 a,
796 n,
797 p,
798 rh,
799 dr,
800 mode,
801 max_body_size,
802 max_connections,
803 )));
804 }
805 }
806
807 for h in handles {
809 let _ = h.await;
810 }
811
812 stop_hosted_services(&self.hosted_services).await;
813
814 Ok(())
815 }
816
817 pub async fn run_at(&self, addr: &str) -> Result<()> {
819 start_hosted_services(&self.hosted_services, &self.dispatch_runtime).await?;
820
821 let notify = Arc::clone(&self.shutdown);
822 install_shutdown_handler(Arc::clone(¬ify));
823 spawn_async_shutdown_listener(Arc::clone(¬ify));
824 serve_http(
825 addr.to_string(),
826 notify,
827 Arc::clone(&self.pipeline),
828 self.router_handler.clone(),
829 Arc::clone(&self.dispatch_runtime),
830 self.mode,
831 self.options.app.max_body_size,
832 self.options.app.max_connections,
833 )
834 .await;
835
836 stop_hosted_services(&self.hosted_services).await;
837 Ok(())
838 }
839
840 pub fn server_handle(&self) -> ServerHandle {
851 ServerHandle {
852 shutdown: Arc::clone(&self.shutdown),
853 }
854 }
855
856 pub fn into_server(self) -> Server {
861 Server { host: self }
862 }
863}
864
865#[async_trait::async_trait]
866impl IHost for Host {
867 async fn run(&self, addr: &str) -> Result<()> {
868 self.run_at(addr).await
869 }
870 async fn stop(&self) -> Result<()> {
871 tracing::info!("Stop requested.");
872 self.shutdown.notify_waiters();
873 Ok(())
874 }
875}
876
877fn make_router_handler(router: Arc<Router>) -> HandlerFn {
878 Arc::new(move |ctx: &mut dyn IHttpContext| {
879 let router = Arc::clone(&router);
880 Box::pin(async move {
881 match router.match_route(ctx).await? {
882 Some((endpoint, params, pattern)) => {
883 drop(router);
884 for (key, value) in params {
885 ctx.request_mut().route_params_mut().insert(key, value);
886 }
887 *ctx.request_mut().route_pattern_mut() = Some(pattern);
888 endpoint.handle(ctx).await
889 }
890 None => {
891 let path_exists = router.path_exists(ctx.request().path());
892 drop(router);
893 if !ctx.response().has_body() {
896 if path_exists {
897 write_error_response(ctx, 405, "Method Not Allowed").await;
898 } else {
899 write_error_response(ctx, 404, "Not Found").await;
900 }
901 }
902 Ok(())
903 }
904 }
905 })
906 })
907}
908
909async fn handle_request(
910 req: Request<Incoming>,
911 pipeline: Arc<MiddlewarePipeline>,
912 router_handler: HandlerFn,
913 dispatch_runtime: Arc<DispatchRuntime>,
914 max_body_size: usize,
915) -> std::result::Result<hyper::Response<Full<Bytes>>, std::convert::Infallible> {
916 let mut ctx = HttpContext::new(req, max_body_size).await;
917
918 if ctx.response().status() < 400 {
921 let result = dispatch_runtime
922 .run(async {
923 pipeline.execute(&mut ctx, router_handler).await
924 })
925 .await;
926 if let Err(e) = result {
927 let status = e.status_code();
928 write_error_response(&mut ctx, status, &e.to_string()).await;
929 }
930 }
931
932 Ok(ctx.into_response())
933}
934
935async fn write_error_response(ctx: &mut dyn IHttpContext, status: u16, message: &str) {
936 write_problem_response(ctx, build_problem(status, message)).await;
937}
938
939fn install_shutdown_handler(shutdown: Arc<tokio::sync::Notify>) {
949 use std::sync::atomic::{AtomicUsize, Ordering};
950
951 static CTRL_COUNT: AtomicUsize = AtomicUsize::new(0);
952
953 if let Err(e) = ctrlc::set_handler(move || {
954 let n = CTRL_COUNT.fetch_add(1, Ordering::SeqCst);
955 if n == 0 {
956 tracing::info!("Shutdown signal received, draining connections...");
957 shutdown.notify_waiters();
958 } else {
959 tracing::warn!("Second interrupt — force exiting.");
960 std::process::exit(130);
961 }
962 }) {
963 tracing::warn!("Could not install Ctrl+C handler: {}", e);
964 }
965}
966
967fn spawn_async_shutdown_listener(shutdown: Arc<tokio::sync::Notify>) {
969 tokio::spawn(async move {
970 wait_for_shutdown_signal().await;
971 tracing::info!("Async shutdown signal received, draining connections...");
972 shutdown.notify_waiters();
973 });
974}
975
976async fn wait_for_shutdown_signal() {
977 #[cfg(unix)]
978 {
979 use tokio::signal::unix::{signal, SignalKind};
980 let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
981 tokio::select! {
982 res = tokio::signal::ctrl_c() => {
983 if let Err(e) = res {
984 tracing::warn!("Ctrl+C listener error: {}", e);
985 }
986 }
987 _ = sigterm.recv() => {}
988 }
989 }
990 #[cfg(not(unix))]
991 {
992 if let Err(e) = tokio::signal::ctrl_c().await {
993 tracing::warn!("Ctrl+C listener error: {}", e);
994 }
995 }
996}
997
998async fn start_hosted_services(
999 services: &[Arc<dyn IHostedService>],
1000 dispatch_runtime: &DispatchRuntime,
1001) -> Result<()> {
1002 if services.is_empty() {
1003 return Ok(());
1004 }
1005 tracing::info!("Starting {} hosted service(s)...", services.len());
1006 dispatch_runtime
1007 .run(async {
1008 for svc in services {
1009 svc.start().await?;
1010 }
1011 Ok::<(), rust_webx_core::error::Error>(())
1012 })
1013 .await?;
1014 tracing::info!("All hosted services started.");
1015 Ok(())
1016}
1017
1018async fn stop_hosted_services(services: &[Arc<dyn IHostedService>]) {
1019 if services.is_empty() {
1020 return;
1021 }
1022 tracing::info!("Stopping {} hosted service(s)...", services.len());
1023 for svc in services {
1024 if let Err(e) = svc.stop().await {
1025 tracing::warn!("Hosted service stop error: {}", e);
1026 }
1027 }
1028 tracing::info!("All hosted services stopped.");
1029}
1030
1031async fn drain_connections(
1032 join_set: &mut JoinSet<()>,
1033 label: &str,
1034 mode: AppMode,
1035) {
1036 let timeout_secs = if mode == AppMode::Development { 5 } else { 30 };
1037 let drain_future = async {
1038 while let Some(result) = join_set.join_next().await {
1039 if let Err(e) = result {
1040 tracing::error!("Connection task panicked: {:?}", e);
1041 }
1042 }
1043 };
1044
1045 match tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), drain_future).await {
1046 Ok(_) => tracing::info!("{}: drained.", label),
1047 Err(_) => {
1048 tracing::warn!("{}: drain timeout, aborting connections.", label);
1049 join_set.abort_all();
1050 while join_set.join_next().await.is_some() {}
1051 }
1052 }
1053}
1054
1055fn parse_url(url: &str) -> Result<(&str, String)> {
1062 if let Some(rest) = url.strip_prefix("https://") {
1063 Ok(("https", rest.to_string()))
1064 } else if let Some(rest) = url.strip_prefix("http://") {
1065 Ok(("http", rest.to_string()))
1066 } else {
1067 Err(rust_webx_core::error::Error::Http(format!(
1068 "Invalid URL '{}'. Use http://host:port or https://host:port",
1069 url
1070 )))
1071 }
1072}
1073
1074async fn serve_http(
1076 addr: String,
1077 shutdown: std::sync::Arc<tokio::sync::Notify>,
1078 pipeline: Arc<MiddlewarePipeline>,
1079 router_handler: HandlerFn,
1080 dispatch_runtime: Arc<DispatchRuntime>,
1081 mode: AppMode,
1082 max_body_size: usize,
1083 max_connections: usize,
1084) {
1085 let listener = match tokio::net::TcpListener::bind(&addr).await {
1086 Ok(l) => l,
1087 Err(e) => {
1088 tracing::error!("Failed to bind HTTP on {}: {}", addr, e);
1089 return;
1090 }
1091 };
1092
1093 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1094 let mut join_set = JoinSet::new();
1095
1096 loop {
1097 tokio::select! {
1098 accept_result = listener.accept() => {
1099 let (stream, _) = match accept_result {
1100 Ok(v) => v,
1101 Err(e) => {
1102 tracing::error!("Accept error (will retry): {}", e);
1103 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1104 continue;
1105 }
1106 };
1107
1108 while join_set.try_join_next().is_some() {}
1109
1110 let permit = match sem.clone().acquire_owned().await {
1111 Ok(p) => p,
1112 Err(_) => continue,
1113 };
1114
1115 let io = TokioIo::new(stream);
1116 let pipeline = Arc::clone(&pipeline);
1117 let router_handler = router_handler.clone();
1118 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1119
1120 join_set.spawn(async move {
1121 let _permit = permit;
1122 let svc_fn = service_fn(move |req: Request<Incoming>| {
1123 let pipeline = Arc::clone(&pipeline);
1124 let router_handler = router_handler.clone();
1125 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1126 let mode = mode;
1127 async move {
1128 let start = Instant::now();
1129 let method = req.method().to_string();
1130 let path = req.uri().path().to_string();
1131 let result = handle_request(
1132 req,
1133 pipeline,
1134 router_handler,
1135 dispatch_runtime,
1136 max_body_size,
1137 )
1138 .await;
1139 let elapsed = start.elapsed();
1140 if mode == AppMode::Development {
1141 let status =
1142 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1143 tracing::info!(
1144 "[{}] {} -> {} ({:.0}ms)",
1145 method,
1146 path,
1147 status,
1148 elapsed.as_secs_f64() * 1000.0
1149 );
1150 }
1151 result
1152 }
1153 });
1154
1155 if let Err(err) = hyper::server::conn::http1::Builder::new()
1156 .serve_connection(io, svc_fn)
1157 .await
1158 {
1159 tracing::error!("Connection error: {}", err);
1160 }
1161 });
1162 }
1163 _ = shutdown.notified() => {
1164 tracing::info!("HTTP {}: stop accepting, draining...", addr);
1165 break;
1166 }
1167 }
1168 }
1169
1170 drain_connections(&mut join_set, &format!("HTTP {}", addr), mode).await;
1171}
1172
1173#[allow(clippy::too_many_arguments)]
1175async fn serve_https(
1176 addr: String,
1177 acceptor: TlsAcceptor,
1178 shutdown: std::sync::Arc<tokio::sync::Notify>,
1179 pipeline: Arc<MiddlewarePipeline>,
1180 router_handler: HandlerFn,
1181 dispatch_runtime: Arc<DispatchRuntime>,
1182 mode: AppMode,
1183 max_body_size: usize,
1184 max_connections: usize,
1185) {
1186 let listener = match tokio::net::TcpListener::bind(&addr).await {
1187 Ok(l) => l,
1188 Err(e) => {
1189 tracing::error!("Failed to bind HTTPS on {}: {}", addr, e);
1190 return;
1191 }
1192 };
1193
1194 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1195 let mut join_set = JoinSet::new();
1196
1197 loop {
1198 tokio::select! {
1199 accept_result = listener.accept() => {
1200 let (stream, _) = match accept_result {
1201 Ok(v) => v,
1202 Err(e) => {
1203 tracing::error!("Accept error (will retry): {}", e);
1204 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1205 continue;
1206 }
1207 };
1208
1209 while join_set.try_join_next().is_some() {}
1210
1211 let permit = match sem.clone().acquire_owned().await {
1212 Ok(p) => p,
1213 Err(_) => continue,
1214 };
1215
1216 let acceptor = acceptor.clone();
1217 let pipeline = Arc::clone(&pipeline);
1218 let router_handler = router_handler.clone();
1219 let dispatch_runtime = Arc::clone(&dispatch_runtime);
1220
1221 join_set.spawn(async move {
1222 let _permit = permit;
1223 match acceptor.accept(stream).await {
1224 Ok(tls_stream) => {
1225 let io = TokioIo::new(tls_stream);
1226 let svc_fn = service_fn(move |req: Request<Incoming>| {
1227 let pipeline = Arc::clone(&pipeline);
1228 let router_handler = router_handler.clone();
1229 let dispatch_runtime = Arc::clone(&dispatch_runtime);
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 max_body_size,
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}