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::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_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 pub options: AppOptions,
61 pipeline: Arc<MiddlewarePipeline>,
62 #[allow(dead_code)]
64 router: Arc<Router>,
65 router_handler: HandlerFn,
67 mode: AppMode,
68 #[allow(dead_code)]
69 spa_root: Option<String>,
70 shutdown: Arc<tokio::sync::Notify>,
71 hosted_services: Vec<Arc<dyn IHostedService>>,
74}
75
76#[allow(clippy::type_complexity)]
77pub struct HostBuilder {
78 service_configs: Vec<Box<dyn FnOnce(ServiceCollection) -> ServiceCollection + Send>>,
79 mode: AppMode,
80 spa_root: Option<String>,
81 spa_disabled: bool,
82 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
83 cors_config: Option<CorsConfig>,
84 auth_enabled: bool,
85 health_registry: Arc<HealthCheckRegistry>,
86}
87
88#[allow(clippy::type_complexity)]
89pub struct HostAppBuilder {
90 options_modifiers: Vec<Box<dyn FnOnce(&mut AppOptions) + Send>>,
91}
92
93impl HostAppBuilder {
94 fn new() -> Self {
95 Self {
96 options_modifiers: Vec::new(),
97 }
98 }
99
100 #[allow(non_snake_case)]
101 pub fn useOptions<F>(&mut self, f: F)
102 where
103 F: FnOnce(&mut AppOptions) + Send + 'static,
104 {
105 self.options_modifiers.push(Box::new(f));
106 }
107}
108
109impl HostBuilder {
110 pub fn new() -> Self {
111 Self {
112 service_configs: Vec::new(),
113 mode: AppMode::from_env(),
116 spa_root: None,
117 spa_disabled: false,
118 options_modifiers: Vec::new(),
119 cors_config: None,
120 auth_enabled: false,
121 health_registry: HealthCheckRegistry::new(),
122 }
123 }
124
125 pub fn register<F>(mut self, f: F) -> Self
126 where
127 F: FnOnce(ServiceCollection) -> ServiceCollection + Send + 'static,
128 {
129 self.service_configs.push(Box::new(f));
130 self
131 }
132
133 pub fn configure<F>(mut self, f: F) -> Self
134 where
135 F: FnOnce(&mut HostAppBuilder) + Send + 'static,
136 {
137 let mut builder = HostAppBuilder::new();
138 f(&mut builder);
139 self.options_modifiers
140 .append(&mut builder.options_modifiers);
141 self
142 }
143
144 pub fn mode(mut self, mode: AppMode) -> Self {
145 self.mode = mode;
146 self
147 }
148
149 pub fn add_options<T>(self, section: &str) -> Self
161 where
162 T: for<'de> serde::Deserialize<'de> + Default + Send + Sync + 'static,
163 {
164 let mode = self.mode;
165 let section = section.to_string();
166 self.register(move |svc| {
167 let appsettings = config::load_appsettings(mode).unwrap_or_default();
168 let bound: T = config::bind_config(&appsettings, §ion);
169 tracing::info!(
170 "[Host] add_options<{}>(\"{}\") registered",
171 std::any::type_name::<T>(),
172 section
173 );
174 svc.instance(Arc::new(bound))
175 })
176 }
177
178 pub fn use_spa(mut self, root: impl Into<String>) -> Self {
179 self.spa_root = Some(root.into());
180 self
181 }
182
183 pub fn no_spa(mut self) -> Self {
189 self.spa_disabled = true;
190 self
191 }
192
193 pub fn use_cors(mut self, config: CorsConfig) -> Self {
194 self.cors_config = Some(config);
195 self
196 }
197
198 pub fn add_authentication(mut self) -> Self {
210 self.auth_enabled = true;
211 self
212 }
213
214 pub fn add_memory_cache(mut self) -> Self {
226 let cache = Arc::new(MemoryCache::new());
227 self.service_configs
228 .push(Box::new(move |svc| svc.instance(cache)));
229 self
230 }
231
232 pub fn add_memory_cache_with(mut self, max_entries: usize) -> Self {
234 let cache = Arc::new(MemoryCache::new().with_max_entries(max_entries));
235 self.service_configs
236 .push(Box::new(move |svc| svc.instance(Arc::clone(&cache))));
237 self
238 }
239
240 pub fn use_middleware<T: IMiddleware + Default + 'static>(mut self) -> Self {
252 self.service_configs.push(Box::new(|svc| {
253 svc.singleton::<dyn IMiddleware>(|_| Arc::new(T::default()) as Arc<dyn IMiddleware>)
254 }));
255 self
256 }
257
258 pub fn use_middleware_with<F>(mut self, factory: F) -> Self
270 where
271 F: Fn() -> Arc<dyn IMiddleware> + Send + Sync + 'static,
272 {
273 self.service_configs.push(Box::new(move |svc| {
274 svc.singleton::<dyn IMiddleware>(move |_| factory())
275 }));
276 self
277 }
278
279 pub fn add_health_check<F>(self, name: impl Into<String>, check: F) -> Self
292 where
293 F: Fn() -> HealthStatus + Send + Sync + 'static,
294 {
295 self.health_registry.register(name, Arc::new(check));
296 self
297 }
298
299 pub fn build(self) -> Host {
300 let env_filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into());
303 if self.mode == AppMode::Development {
304 let _ = tracing_subscriber::fmt()
305 .with_env_filter(env_filter)
306 .without_time()
307 .with_target(false)
308 .try_init();
309 } else {
310 let _ = tracing_subscriber::fmt()
311 .json()
312 .with_env_filter(env_filter)
313 .try_init();
314 }
315
316 let mut svc = ServiceCollection::from_injected();
317 for cfg in self.service_configs {
318 svc = cfg(svc);
319 }
320
321 let health_registry = Arc::clone(&self.health_registry);
323 svc = svc.instance(health_registry);
324
325 let provider = svc.build().unwrap_or_else(|e| {
326 panic!(
327 "Failed to build ServiceProvider: {}. Check your DI registrations.",
328 e
329 )
330 });
331
332 rust_webx_core::route::scan::set_global_provider(Arc::clone(&provider));
334
335 rust_webx_core::route::scan::HandlerCache::init_global();
340
341 let appsettings =
342 config::load_appsettings(self.mode).unwrap_or_else(|| serde_json::json!({}));
343 let mut options: AppOptions = config::bind_root(&appsettings);
344 for modifier in self.options_modifiers {
345 modifier(&mut options);
346 }
347
348 let http_metrics = HttpMetrics::new();
349
350 let mut pipeline = MiddlewarePipeline::new();
351
352 pipeline.add_middleware(Arc::new(crate::security_headers::SecurityHeadersMiddleware::new()));
355 pipeline.add_middleware(Arc::new(crate::request_id::RequestIdMiddleware::new()));
356
357 if options.rate_limit.enabled {
358 tracing::info!(
359 "[Host] RateLimit enabled: {} req/s, burst {}, max_ips {}",
360 options.rate_limit.requests_per_second,
361 options.rate_limit.burst_size,
362 options.rate_limit.max_tracked_ips
363 );
364 pipeline.add_middleware(Arc::new(RateLimitMiddleware::from_config(
365 &options.rate_limit,
366 )));
367 }
368
369 if options.metrics.enabled {
370 tracing::info!("[Host] Metrics enabled at GET /metrics");
371 pipeline.add_middleware(Arc::new(MetricsMiddleware::new(Arc::clone(
372 &http_metrics,
373 ))));
374 }
375
376 let middlewares: Vec<Arc<dyn IMiddleware>> = provider.get_all::<dyn IMiddleware>();
377 for mw in middlewares {
378 pipeline.add_middleware(mw);
379 }
380
381 let cors = self.cors_config.unwrap_or_else(|| {
382 let cs = &options.cors;
383 CorsConfig {
384 origins: cs.origins.clone(),
385 methods: cs.methods.clone(),
386 headers: cs.headers.clone(),
387 expose_headers: cs.expose_headers.clone(),
388 allow_credentials: cs.allow_credentials,
389 max_age: cs.max_age,
390 }
391 });
392 pipeline.add_middleware(Arc::new(CorsMiddleware::new(cors)));
393
394 let spa_root = if self.spa_disabled {
399 None
400 } else {
401 self.spa_root.clone().or_else(|| {
402 let candidate = rust_webx_core::paths::app_base().join("wwwroot");
403 if candidate.is_dir() {
404 tracing::info!("[Host] Auto-detected SPA root: {}", candidate.display());
405 Some(candidate.to_string_lossy().into_owned())
406 } else {
407 None
408 }
409 })
410 };
411 if let Some(ref spa_root) = spa_root {
412 pipeline.add_middleware(Arc::new(SpaMiddleware::new(spa_root.clone())));
413 }
414
415 if self.auth_enabled {
416 let secret = options.jwt.secret.clone();
417 if !secret.is_empty() {
418 let jwt_auth = Arc::new(JwtAuth::new(
419 DecodingKey::from_secret(secret.as_bytes()),
420 Validation::default(),
421 ));
422 pipeline.add_middleware(Arc::new(jwt_middleware(jwt_auth)));
423 init_jwt_secret(&secret);
424 } else {
425 tracing::warn!(
426 "add_authentication() enabled but no JWT secret configured. Set Jwt.Secret in appsettings.json, JWT_SECRET, or APP__Jwt__Secret env var."
427 );
428 }
429 }
430
431 let pipeline = Arc::new(pipeline);
436
437 if self.mode == AppMode::Production && self.auth_enabled && is_weak_jwt_secret(&options.jwt.secret)
439 {
440 panic!(
441 "Production requires a strong JWT secret. Set APP__Jwt__Secret or JWT_SECRET env var (min 32 chars, not a placeholder)."
442 );
443 }
444
445 if self.mode == AppMode::Production && options.cors.origins.iter().any(|o| o == "*") {
447 panic!(
448 "Production forbids CORS origin '*'. Set explicit origins via Cors.Origins or APP__Cors__Origins."
449 );
450 }
451
452 let mut router = Router::new();
453 let mut route_count = 0usize;
454
455 #[allow(clippy::type_complexity)]
457 let mut dispatch_map: std::collections::HashMap<
458 &'static str,
459 fn(
460 Vec<u8>,
461 std::collections::HashMap<String, String>,
462 std::collections::HashMap<String, String>,
463 Option<Box<dyn rust_webx_core::auth::IClaims>>,
464 ) -> std::pin::Pin<
465 Box<
466 dyn std::future::Future<
467 Output = rust_webx_core::error::Result<rust_webx_core::route::scan::ResponseData>,
468 > + Send,
469 >,
470 >,
471 > = std::collections::HashMap::new();
472
473 for dispatch in inventory::iter::<rust_webx_core::route::scan::RouteDispatch> {
474 dispatch_map.insert(dispatch.handler_type, dispatch.dispatch);
475 }
476
477 for entry in inventory::iter::<RouteEntry> {
478 route_count += 1;
479 let stub = Arc::new(StubEndpoint {
480 method: entry.method.as_str(),
481 path: entry.path,
482 handler_type: entry.handler_type,
483 dispatch_fn: dispatch_map.get(entry.handler_type).copied(),
484 auth_required_role: entry.required_role,
485 authorizers: collect_authorizers(provider.as_ref()),
486 });
487 router.register(entry.method, entry.path, stub);
488 }
489
490 let openapi_spec = generate_openapi_spec("Rust WebX API", env!("CARGO_PKG_VERSION"));
491 if self.mode == AppMode::Development {
492 let openapi_bytes = serde_json::to_vec(&openapi_spec).unwrap_or_default();
493 router.register(
494 HttpMethod::Get,
495 "/api/openapi.json",
496 Arc::new(StaticJsonEndpoint::new(openapi_bytes)),
497 );
498 router.register(
499 HttpMethod::Get,
500 "/api/openapi.html",
501 Arc::new(StaticHtmlEndpoint { body: APIUI_HTML }),
502 );
503 }
504
505 let health_registry = Arc::clone(&self.health_registry);
508 let health_endpoint: Arc<dyn IEndpoint> =
509 Arc::new(HealthCheckEndpoint::new(Arc::clone(&health_registry)));
510 router.register(HttpMethod::Get, "/health", Arc::clone(&health_endpoint));
511 router.register(HttpMethod::Get, "/healthz", health_endpoint);
512
513 let live_body = serde_json::to_vec(&serde_json::json!({"status":"pass"}))
515 .unwrap_or_default();
516 router.register(
517 HttpMethod::Get,
518 "/health/live",
519 Arc::new(StaticJsonEndpoint {
520 body: live_body,
521 content_type: "application/health+json",
522 }),
523 );
524
525 router.register(
527 HttpMethod::Get,
528 "/health/ready",
529 Arc::new(HealthCheckEndpoint::new(health_registry)),
530 );
531
532 if options.metrics.enabled {
533 router.register(
534 HttpMethod::Get,
535 "/metrics",
536 Arc::new(MetricsEndpoint::new(http_metrics)),
537 );
538 }
539
540 if self.mode == AppMode::Development {
541 let version = env!("CARGO_PKG_VERSION");
542 tracing::info!("");
543 tracing::info!(" ----------------------------------------------------------------");
544 tracing::info!(" Rust WebX Framework v{}", version);
545 tracing::info!(" ----------------------------------------------------------------");
546 tracing::info!(" App: {}", options.app.name);
547 tracing::info!(" CORS: enabled");
548 if let Some(ref root) = self.spa_root {
549 tracing::info!(" SPA Root: {}", root);
550 }
551 if route_count > 0 {
552 tracing::info!(" Routes: {} registered", route_count);
553 }
554 crate::diagnostics::log_startup_diagnostics();
555 let banner_urls = if options.app.urls.is_empty() {
556 vec!["http://localhost:5000".to_string()]
557 } else {
558 options
559 .app
560 .urls
561 .iter()
562 .map(|u| u.replace("0.0.0.0", "localhost"))
563 .collect::<Vec<_>>()
564 };
565 for url in &banner_urls {
566 tracing::info!(" OpenAPI: {}/api/openapi.html", url);
567 tracing::info!(" OpenAPI: {}/api/openapi.json", url);
568 }
569 tracing::info!(" ----------------------------------------------------------------");
570 tracing::info!("");
571 } else if route_count > 0 {
572 tracing::info!("{} route(s) registered", route_count);
573 }
574
575 let router = Arc::new(router);
576 let router_handler = make_router_handler(Arc::clone(&router));
577
578 let hosted_services: Vec<Arc<dyn IHostedService>> =
581 provider.get_all::<dyn IHostedService>();
582
583 Host {
584 provider,
585 options,
586 pipeline,
587 router,
588 router_handler,
589 mode: self.mode,
590 spa_root: self.spa_root,
591 shutdown: Arc::new(tokio::sync::Notify::new()),
592 hosted_services,
593 }
594 }
595}
596
597pub struct ServerHandle {
599 shutdown: Arc<tokio::sync::Notify>,
600}
601
602impl ServerHandle {
603 pub fn shutdown(self) {
606 self.shutdown.notify_waiters();
607 }
608}
609
610pub struct Server {
622 host: Host,
623}
624
625impl Server {
626 pub async fn run(self) -> Result<()> {
628 self.host.run().await
629 }
630
631 pub async fn run_at(self, addr: &str) -> Result<()> {
633 self.host.run_at(addr).await
634 }
635
636 pub fn handle(&self) -> ServerHandle {
638 self.host.server_handle()
639 }
640}
641
642impl Default for HostBuilder {
643 fn default() -> Self {
644 Self::new()
645 }
646}
647
648impl Host {
649 pub fn builder() -> HostBuilder {
650 HostBuilder::new()
651 }
652
653 pub fn options(&self) -> &AppOptions {
654 &self.options
655 }
656
657 pub async fn run(&self) -> Result<()> {
671 start_hosted_services(&self.hosted_services).await?;
672
673 let urls = if self.options.app.urls.is_empty() {
674 vec!["http://0.0.0.0:5000".to_string()]
675 } else {
676 self.options.app.urls.clone()
677 };
678
679 let mut http_addrs: Vec<String> = Vec::new();
680 let mut https_addrs: Vec<String> = Vec::new();
681
682 for url in &urls {
683 let (scheme, addr) = parse_url(url)?;
684 match scheme {
685 "http" => http_addrs.push(addr),
686 "https" => https_addrs.push(addr),
687 other => {
688 return Err(rust_webx_core::error::Error::Http(format!(
689 "Unsupported URL scheme '{}' in '{}'",
690 other, url
691 )))
692 }
693 }
694 }
695
696 let acceptor = if !https_addrs.is_empty() {
697 let tls = &self.options.tls;
698 if tls.cert_path.is_empty() || tls.key_path.is_empty() {
699 return Err(rust_webx_core::error::Error::Http(
700 "HTTPS URLs require Tls.CertPath and Tls.KeyPath".into(),
701 ));
702 }
703 Some(build_tls_acceptor(&tls.cert_path, &tls.key_path)?)
704 } else {
705 None
706 };
707
708 if self.mode == AppMode::Development {
710 tracing::info!("");
711 for url in &urls {
712 let display_url = url.replace("0.0.0.0", "localhost");
713 tracing::info!(" Listening on {}", display_url);
714 }
715 } else {
716 tracing::info!("Listening on {} url(s)", urls.len());
717 }
718
719 let notify = Arc::clone(&self.shutdown);
720 install_shutdown_handler(Arc::clone(¬ify));
721 spawn_async_shutdown_listener(Arc::clone(¬ify));
722
723 let mut handles = Vec::new();
724 let pipeline = Arc::clone(&self.pipeline);
725 let router_handler = self.router_handler.clone();
726 let mode = self.mode;
727 let max_body_size = self.options.app.max_body_size;
728 let max_connections = self.options.app.max_connections;
729
730 for addr in &http_addrs {
731 let addr = addr.clone();
732 let n = std::sync::Arc::clone(¬ify);
733 let p = Arc::clone(&pipeline);
734 let rh = router_handler.clone();
735 handles.push(tokio::spawn(serve_http(
736 addr,
737 n,
738 p,
739 rh,
740 mode,
741 max_body_size,
742 max_connections,
743 )));
744 }
745
746 if let Some(ref tls_acceptor) = acceptor {
747 for addr in &https_addrs {
748 let addr = addr.clone();
749 let n = std::sync::Arc::clone(¬ify);
750 let p = Arc::clone(&pipeline);
751 let rh = router_handler.clone();
752 let a = tls_acceptor.clone();
753 handles.push(tokio::spawn(serve_https(
754 addr,
755 a,
756 n,
757 p,
758 rh,
759 mode,
760 max_body_size,
761 max_connections,
762 )));
763 }
764 }
765
766 for h in handles {
768 let _ = h.await;
769 }
770
771 stop_hosted_services(&self.hosted_services).await;
772
773 Ok(())
774 }
775
776 pub async fn run_at(&self, addr: &str) -> Result<()> {
778 start_hosted_services(&self.hosted_services).await?;
779
780 let notify = Arc::clone(&self.shutdown);
781 install_shutdown_handler(Arc::clone(¬ify));
782 spawn_async_shutdown_listener(Arc::clone(¬ify));
783 serve_http(
784 addr.to_string(),
785 notify,
786 Arc::clone(&self.pipeline),
787 self.router_handler.clone(),
788 self.mode,
789 self.options.app.max_body_size,
790 self.options.app.max_connections,
791 )
792 .await;
793
794 stop_hosted_services(&self.hosted_services).await;
795 Ok(())
796 }
797
798 pub fn server_handle(&self) -> ServerHandle {
809 ServerHandle {
810 shutdown: Arc::clone(&self.shutdown),
811 }
812 }
813
814 pub fn into_server(self) -> Server {
819 Server { host: self }
820 }
821}
822
823#[async_trait::async_trait]
824impl IHost for Host {
825 async fn run(&self, addr: &str) -> Result<()> {
826 self.run_at(addr).await
827 }
828 async fn stop(&self) -> Result<()> {
829 tracing::info!("Stop requested.");
830 self.shutdown.notify_waiters();
831 Ok(())
832 }
833}
834
835fn make_router_handler(router: Arc<Router>) -> HandlerFn {
836 Arc::new(move |ctx: &mut dyn IHttpContext| {
837 let router = Arc::clone(&router);
838 Box::pin(async move {
839 match router.match_route(ctx).await? {
840 Some((endpoint, params, pattern)) => {
841 drop(router);
842 for (key, value) in params {
843 ctx.request_mut().route_params_mut().insert(key, value);
844 }
845 *ctx.request_mut().route_pattern_mut() = Some(pattern);
846 endpoint.handle(ctx).await
847 }
848 None => {
849 let path_exists = router.path_exists(ctx.request().path());
850 drop(router);
851 if !ctx.response().has_body() {
854 if path_exists {
855 write_error_response(ctx, 405, "Method Not Allowed").await;
856 } else {
857 write_error_response(ctx, 404, "Not Found").await;
858 }
859 }
860 Ok(())
861 }
862 }
863 })
864 })
865}
866
867async fn handle_request(
868 req: Request<Incoming>,
869 pipeline: Arc<MiddlewarePipeline>,
870 router_handler: HandlerFn,
871 max_body_size: usize,
872) -> std::result::Result<hyper::Response<Full<Bytes>>, std::convert::Infallible> {
873 let mut ctx = HttpContext::new(req, max_body_size).await;
874
875 if ctx.response().status() < 400 {
878 let result = pipeline.execute(&mut ctx, router_handler).await;
879 if let Err(e) = result {
880 let status = e.status_code();
881 write_error_response(&mut ctx, status, &e.to_string()).await;
882 }
883 }
884
885 Ok(ctx.into_response())
886}
887
888async fn write_error_response(ctx: &mut dyn IHttpContext, status: u16, message: &str) {
889 write_problem_response(ctx, build_problem(status, message)).await;
890}
891
892fn install_shutdown_handler(shutdown: Arc<tokio::sync::Notify>) {
902 use std::sync::atomic::{AtomicUsize, Ordering};
903
904 static CTRL_COUNT: AtomicUsize = AtomicUsize::new(0);
905
906 if let Err(e) = ctrlc::set_handler(move || {
907 let n = CTRL_COUNT.fetch_add(1, Ordering::SeqCst);
908 if n == 0 {
909 tracing::info!("Shutdown signal received, draining connections...");
910 shutdown.notify_waiters();
911 } else {
912 tracing::warn!("Second interrupt — force exiting.");
913 std::process::exit(130);
914 }
915 }) {
916 tracing::warn!("Could not install Ctrl+C handler: {}", e);
917 }
918}
919
920fn spawn_async_shutdown_listener(shutdown: Arc<tokio::sync::Notify>) {
922 tokio::spawn(async move {
923 wait_for_shutdown_signal().await;
924 tracing::info!("Async shutdown signal received, draining connections...");
925 shutdown.notify_waiters();
926 });
927}
928
929async fn wait_for_shutdown_signal() {
930 #[cfg(unix)]
931 {
932 use tokio::signal::unix::{signal, SignalKind};
933 let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
934 tokio::select! {
935 res = tokio::signal::ctrl_c() => {
936 if let Err(e) = res {
937 tracing::warn!("Ctrl+C listener error: {}", e);
938 }
939 }
940 _ = sigterm.recv() => {}
941 }
942 }
943 #[cfg(not(unix))]
944 {
945 if let Err(e) = tokio::signal::ctrl_c().await {
946 tracing::warn!("Ctrl+C listener error: {}", e);
947 }
948 }
949}
950
951async fn start_hosted_services(services: &[Arc<dyn IHostedService>]) -> Result<()> {
952 if services.is_empty() {
953 return Ok(());
954 }
955 tracing::info!("Starting {} hosted service(s)...", services.len());
956 for svc in services {
957 svc.start().await?;
958 }
959 tracing::info!("All hosted services started.");
960 Ok(())
961}
962
963async fn stop_hosted_services(services: &[Arc<dyn IHostedService>]) {
964 if services.is_empty() {
965 return;
966 }
967 tracing::info!("Stopping {} hosted service(s)...", services.len());
968 for svc in services {
969 if let Err(e) = svc.stop().await {
970 tracing::warn!("Hosted service stop error: {}", e);
971 }
972 }
973 tracing::info!("All hosted services stopped.");
974}
975
976async fn drain_connections(
977 join_set: &mut JoinSet<()>,
978 label: &str,
979 mode: AppMode,
980) {
981 let timeout_secs = if mode == AppMode::Development { 5 } else { 30 };
982 let drain_future = async {
983 while let Some(result) = join_set.join_next().await {
984 if let Err(e) = result {
985 tracing::error!("Connection task panicked: {:?}", e);
986 }
987 }
988 };
989
990 match tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), drain_future).await {
991 Ok(_) => tracing::info!("{}: drained.", label),
992 Err(_) => {
993 tracing::warn!("{}: drain timeout, aborting connections.", label);
994 join_set.abort_all();
995 while join_set.join_next().await.is_some() {}
996 }
997 }
998}
999
1000fn parse_url(url: &str) -> Result<(&str, String)> {
1007 if let Some(rest) = url.strip_prefix("https://") {
1008 Ok(("https", rest.to_string()))
1009 } else if let Some(rest) = url.strip_prefix("http://") {
1010 Ok(("http", rest.to_string()))
1011 } else {
1012 Err(rust_webx_core::error::Error::Http(format!(
1013 "Invalid URL '{}'. Use http://host:port or https://host:port",
1014 url
1015 )))
1016 }
1017}
1018
1019async fn serve_http(
1021 addr: String,
1022 shutdown: std::sync::Arc<tokio::sync::Notify>,
1023 pipeline: Arc<MiddlewarePipeline>,
1024 router_handler: HandlerFn,
1025 mode: AppMode,
1026 max_body_size: usize,
1027 max_connections: usize,
1028) {
1029 let listener = match tokio::net::TcpListener::bind(&addr).await {
1030 Ok(l) => l,
1031 Err(e) => {
1032 tracing::error!("Failed to bind HTTP on {}: {}", addr, e);
1033 return;
1034 }
1035 };
1036
1037 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1038 let mut join_set = JoinSet::new();
1039
1040 loop {
1041 tokio::select! {
1042 accept_result = listener.accept() => {
1043 let (stream, _) = match accept_result {
1044 Ok(v) => v,
1045 Err(e) => {
1046 tracing::error!("Accept error (will retry): {}", e);
1047 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1048 continue;
1049 }
1050 };
1051
1052 while join_set.try_join_next().is_some() {}
1053
1054 let permit = match sem.clone().acquire_owned().await {
1055 Ok(p) => p,
1056 Err(_) => continue,
1057 };
1058
1059 let io = TokioIo::new(stream);
1060 let pipeline = Arc::clone(&pipeline);
1061 let router_handler = router_handler.clone();
1062
1063 join_set.spawn(async move {
1064 let _permit = permit;
1065 let svc_fn = service_fn(move |req: Request<Incoming>| {
1066 let pipeline = Arc::clone(&pipeline);
1067 let router_handler = router_handler.clone();
1068 let mode = mode;
1069 async move {
1070 let start = Instant::now();
1071 let method = req.method().to_string();
1072 let path = req.uri().path().to_string();
1073 let result =
1074 handle_request(req, pipeline, router_handler, max_body_size).await;
1075 let elapsed = start.elapsed();
1076 if mode == AppMode::Development {
1077 let status =
1078 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1079 tracing::info!(
1080 "[{}] {} -> {} ({:.0}ms)",
1081 method,
1082 path,
1083 status,
1084 elapsed.as_secs_f64() * 1000.0
1085 );
1086 }
1087 result
1088 }
1089 });
1090
1091 if let Err(err) = hyper::server::conn::http1::Builder::new()
1092 .serve_connection(io, svc_fn)
1093 .await
1094 {
1095 tracing::error!("Connection error: {}", err);
1096 }
1097 });
1098 }
1099 _ = shutdown.notified() => {
1100 tracing::info!("HTTP {}: stop accepting, draining...", addr);
1101 break;
1102 }
1103 }
1104 }
1105
1106 drain_connections(&mut join_set, &format!("HTTP {}", addr), mode).await;
1107}
1108
1109#[allow(clippy::too_many_arguments)]
1111async fn serve_https(
1112 addr: String,
1113 acceptor: TlsAcceptor,
1114 shutdown: std::sync::Arc<tokio::sync::Notify>,
1115 pipeline: Arc<MiddlewarePipeline>,
1116 router_handler: HandlerFn,
1117 mode: AppMode,
1118 max_body_size: usize,
1119 max_connections: usize,
1120) {
1121 let listener = match tokio::net::TcpListener::bind(&addr).await {
1122 Ok(l) => l,
1123 Err(e) => {
1124 tracing::error!("Failed to bind HTTPS on {}: {}", addr, e);
1125 return;
1126 }
1127 };
1128
1129 let sem = Arc::new(tokio::sync::Semaphore::new(max_connections));
1130 let mut join_set = JoinSet::new();
1131
1132 loop {
1133 tokio::select! {
1134 accept_result = listener.accept() => {
1135 let (stream, _) = match accept_result {
1136 Ok(v) => v,
1137 Err(e) => {
1138 tracing::error!("Accept error (will retry): {}", e);
1139 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1140 continue;
1141 }
1142 };
1143
1144 while join_set.try_join_next().is_some() {}
1145
1146 let permit = match sem.clone().acquire_owned().await {
1147 Ok(p) => p,
1148 Err(_) => continue,
1149 };
1150
1151 let acceptor = acceptor.clone();
1152 let pipeline = Arc::clone(&pipeline);
1153 let router_handler = router_handler.clone();
1154
1155 join_set.spawn(async move {
1156 let _permit = permit;
1157 match acceptor.accept(stream).await {
1158 Ok(tls_stream) => {
1159 let io = TokioIo::new(tls_stream);
1160 let svc_fn = service_fn(move |req: Request<Incoming>| {
1161 let pipeline = Arc::clone(&pipeline);
1162 let router_handler = router_handler.clone();
1163 let mode = mode;
1164 async move {
1165 let start = Instant::now();
1166 let method = req.method().to_string();
1167 let path = req.uri().path().to_string();
1168 let result =
1169 handle_request(req, pipeline, router_handler, max_body_size)
1170 .await;
1171 let elapsed = start.elapsed();
1172 if mode == AppMode::Development {
1173 let status =
1174 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1175 tracing::info!(
1176 "[{}] {} -> {} ({:.0}ms)",
1177 method,
1178 path,
1179 status,
1180 elapsed.as_secs_f64() * 1000.0
1181 );
1182 }
1183 result
1184 }
1185 });
1186
1187 if let Err(err) = hyper::server::conn::http1::Builder::new()
1188 .serve_connection(io, svc_fn)
1189 .await
1190 {
1191 tracing::error!("TLS connection error: {}", err);
1192 }
1193 }
1194 Err(e) => {
1195 tracing::error!("TLS handshake error: {}", e);
1196 }
1197 }
1198 });
1199 }
1200 _ = shutdown.notified() => {
1201 tracing::info!("HTTPS {}: stop accepting, draining...", addr);
1202 break;
1203 }
1204 }
1205 }
1206
1207 drain_connections(&mut join_set, &format!("HTTPS {}", addr), mode).await;
1208}
1209
1210fn build_tls_acceptor(cert_path: &str, key_path: &str) -> Result<TlsAcceptor> {
1216 use std::fs::File;
1217 use std::io::BufReader;
1218
1219 if cert_path.is_empty() || key_path.is_empty() {
1220 return Err(rust_webx_core::error::Error::Http(
1221 "TLS certificate or key path not configured.".into(),
1222 ));
1223 }
1224
1225 let cert_file = File::open(cert_path).map_err(|e| {
1226 rust_webx_core::error::Error::Http(format!("Cannot open cert '{}': {}", cert_path, e))
1227 })?;
1228 let mut cert_reader = BufReader::new(cert_file);
1229 let certs: Vec<CertificateDer> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
1230 if certs.is_empty() {
1231 return Err(rust_webx_core::error::Error::Http(format!(
1232 "No valid certs in '{}'",
1233 cert_path
1234 )));
1235 }
1236
1237 let key_file = File::open(key_path).map_err(|e| {
1238 rust_webx_core::error::Error::Http(format!("Cannot open key '{}': {}", key_path, e))
1239 })?;
1240 let mut key_reader = BufReader::new(key_file);
1241 let key = pkcs8_private_keys(&mut key_reader)
1242 .filter_map(|r| r.ok())
1243 .map(PrivateKeyDer::from)
1244 .next()
1245 .or_else(|| {
1246 let key_file2 = File::open(key_path).map(BufReader::new).ok()?;
1247 let mut kr2 = key_file2;
1248 let rsa_keys: Vec<PrivateKeyDer> = rsa_private_keys(&mut kr2)
1249 .filter_map(|r| r.ok())
1250 .map(PrivateKeyDer::from)
1251 .collect();
1252 rsa_keys.into_iter().next()
1253 })
1254 .ok_or_else(|| {
1255 rust_webx_core::error::Error::Http(format!("No valid private key in '{}'", key_path))
1256 })?;
1257
1258 let config = ServerConfig::builder()
1259 .with_no_client_auth()
1260 .with_single_cert(certs, key)
1261 .map_err(|e| rust_webx_core::error::Error::Http(format!("TLS config error: {}", e)))?;
1262
1263 Ok(TlsAcceptor::from(std::sync::Arc::new(config)))
1264}