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 allow_credentials: cs.allow_credentials,
388 max_age: cs.max_age,
389 }
390 });
391 pipeline.add_middleware(Arc::new(CorsMiddleware::new(cors)));
392
393 let spa_root = if self.spa_disabled {
398 None
399 } else {
400 self.spa_root.clone().or_else(|| {
401 let candidate = rust_webx_core::paths::app_base().join("wwwroot");
402 if candidate.is_dir() {
403 tracing::info!("[Host] Auto-detected SPA root: {}", candidate.display());
404 Some(candidate.to_string_lossy().into_owned())
405 } else {
406 None
407 }
408 })
409 };
410 if let Some(ref spa_root) = spa_root {
411 pipeline.add_middleware(Arc::new(SpaMiddleware::new(spa_root.clone())));
412 }
413
414 if self.auth_enabled {
415 let secret = options.jwt.secret.clone();
416 if !secret.is_empty() {
417 let jwt_auth = Arc::new(JwtAuth::new(
418 DecodingKey::from_secret(secret.as_bytes()),
419 Validation::default(),
420 ));
421 pipeline.add_middleware(Arc::new(jwt_middleware(jwt_auth)));
422 init_jwt_secret(&secret);
423 } else {
424 tracing::warn!(
425 "add_authentication() enabled but no JWT secret configured. Set Jwt.Secret in appsettings.json, JWT_SECRET, or APP__Jwt__Secret env var."
426 );
427 }
428 }
429
430 let pipeline = Arc::new(pipeline);
435
436 if self.mode == AppMode::Production && self.auth_enabled && is_weak_jwt_secret(&options.jwt.secret)
438 {
439 panic!(
440 "Production requires a strong JWT secret. Set APP__Jwt__Secret or JWT_SECRET env var (min 32 chars, not a placeholder)."
441 );
442 }
443
444 if self.mode == AppMode::Production && options.cors.origins.iter().any(|o| o == "*") {
446 panic!(
447 "Production forbids CORS origin '*'. Set explicit origins via Cors.Origins or APP__Cors__Origins."
448 );
449 }
450
451 let mut router = Router::new();
452 let mut route_count = 0usize;
453
454 #[allow(clippy::type_complexity)]
456 let mut dispatch_map: std::collections::HashMap<
457 &'static str,
458 fn(
459 Vec<u8>,
460 std::collections::HashMap<String, String>,
461 std::collections::HashMap<String, String>,
462 Option<Box<dyn rust_webx_core::auth::IClaims>>,
463 ) -> std::pin::Pin<
464 Box<
465 dyn std::future::Future<
466 Output = rust_webx_core::error::Result<rust_webx_core::route::scan::ResponseData>,
467 > + Send,
468 >,
469 >,
470 > = std::collections::HashMap::new();
471
472 for dispatch in inventory::iter::<rust_webx_core::route::scan::RouteDispatch> {
473 dispatch_map.insert(dispatch.handler_type, dispatch.dispatch);
474 }
475
476 for entry in inventory::iter::<RouteEntry> {
477 route_count += 1;
478 let stub = Arc::new(StubEndpoint {
479 method: entry.method.as_str(),
480 path: entry.path,
481 handler_type: entry.handler_type,
482 dispatch_fn: dispatch_map.get(entry.handler_type).copied(),
483 auth_required_role: entry.required_role,
484 authorizers: collect_authorizers(provider.as_ref()),
485 });
486 router.register(entry.method, entry.path, stub);
487 }
488
489 let openapi_spec = generate_openapi_spec("Rust WebX API", env!("CARGO_PKG_VERSION"));
490 if self.mode == AppMode::Development {
491 let openapi_bytes = serde_json::to_vec(&openapi_spec).unwrap_or_default();
492 router.register(
493 HttpMethod::Get,
494 "/api/openapi.json",
495 Arc::new(StaticJsonEndpoint::new(openapi_bytes)),
496 );
497 router.register(
498 HttpMethod::Get,
499 "/api/openapi.html",
500 Arc::new(StaticHtmlEndpoint { body: APIUI_HTML }),
501 );
502 }
503
504 let health_registry = Arc::clone(&self.health_registry);
507 let health_endpoint: Arc<dyn IEndpoint> =
508 Arc::new(HealthCheckEndpoint::new(Arc::clone(&health_registry)));
509 router.register(HttpMethod::Get, "/health", Arc::clone(&health_endpoint));
510 router.register(HttpMethod::Get, "/healthz", health_endpoint);
511
512 let live_body = serde_json::to_vec(&serde_json::json!({"status":"pass"}))
514 .unwrap_or_default();
515 router.register(
516 HttpMethod::Get,
517 "/health/live",
518 Arc::new(StaticJsonEndpoint {
519 body: live_body,
520 content_type: "application/health+json",
521 }),
522 );
523
524 router.register(
526 HttpMethod::Get,
527 "/health/ready",
528 Arc::new(HealthCheckEndpoint::new(health_registry)),
529 );
530
531 if options.metrics.enabled {
532 router.register(
533 HttpMethod::Get,
534 "/metrics",
535 Arc::new(MetricsEndpoint::new(http_metrics)),
536 );
537 }
538
539 if self.mode == AppMode::Development {
540 let version = env!("CARGO_PKG_VERSION");
541 tracing::info!("");
542 tracing::info!(" ----------------------------------------------------------------");
543 tracing::info!(" Rust WebX Framework v{}", version);
544 tracing::info!(" ----------------------------------------------------------------");
545 tracing::info!(" App: {}", options.app.name);
546 tracing::info!(" CORS: enabled");
547 if let Some(ref root) = self.spa_root {
548 tracing::info!(" SPA Root: {}", root);
549 }
550 if route_count > 0 {
551 tracing::info!(" Routes: {} registered", route_count);
552 }
553 crate::diagnostics::log_startup_diagnostics();
554 let banner_urls = if options.app.urls.is_empty() {
555 vec!["http://localhost:5000".to_string()]
556 } else {
557 options
558 .app
559 .urls
560 .iter()
561 .map(|u| u.replace("0.0.0.0", "localhost"))
562 .collect::<Vec<_>>()
563 };
564 for url in &banner_urls {
565 tracing::info!(" OpenAPI: {}/api/openapi.html", url);
566 tracing::info!(" OpenAPI: {}/api/openapi.json", url);
567 }
568 tracing::info!(" ----------------------------------------------------------------");
569 tracing::info!("");
570 } else if route_count > 0 {
571 tracing::info!("{} route(s) registered", route_count);
572 }
573
574 let router = Arc::new(router);
575 let router_handler = make_router_handler(Arc::clone(&router));
576
577 let hosted_services: Vec<Arc<dyn IHostedService>> =
580 provider.get_all::<dyn IHostedService>();
581
582 Host {
583 provider,
584 options,
585 pipeline,
586 router,
587 router_handler,
588 mode: self.mode,
589 spa_root: self.spa_root,
590 shutdown: Arc::new(tokio::sync::Notify::new()),
591 hosted_services,
592 }
593 }
594}
595
596pub struct ServerHandle {
598 shutdown: Arc<tokio::sync::Notify>,
599}
600
601impl ServerHandle {
602 pub fn shutdown(self) {
605 self.shutdown.notify_waiters();
606 }
607}
608
609pub struct Server {
621 host: Host,
622}
623
624impl Server {
625 pub async fn run(self) -> Result<()> {
627 self.host.run().await
628 }
629
630 pub async fn run_at(self, addr: &str) -> Result<()> {
632 self.host.run_at(addr).await
633 }
634
635 pub fn handle(&self) -> ServerHandle {
637 self.host.server_handle()
638 }
639}
640
641impl Default for HostBuilder {
642 fn default() -> Self {
643 Self::new()
644 }
645}
646
647impl Host {
648 pub fn builder() -> HostBuilder {
649 HostBuilder::new()
650 }
651
652 pub fn options(&self) -> &AppOptions {
653 &self.options
654 }
655
656 pub async fn run(&self) -> Result<()> {
670 start_hosted_services(&self.hosted_services).await?;
671
672 let urls = if self.options.app.urls.is_empty() {
673 vec!["http://0.0.0.0:5000".to_string()]
674 } else {
675 self.options.app.urls.clone()
676 };
677
678 let mut http_addrs: Vec<String> = Vec::new();
679 let mut https_addrs: Vec<String> = Vec::new();
680
681 for url in &urls {
682 let (scheme, addr) = parse_url(url)?;
683 match scheme {
684 "http" => http_addrs.push(addr),
685 "https" => https_addrs.push(addr),
686 other => {
687 return Err(rust_webx_core::error::Error::Http(format!(
688 "Unsupported URL scheme '{}' in '{}'",
689 other, url
690 )))
691 }
692 }
693 }
694
695 let acceptor = if !https_addrs.is_empty() {
696 let tls = &self.options.tls;
697 if tls.cert_path.is_empty() || tls.key_path.is_empty() {
698 return Err(rust_webx_core::error::Error::Http(
699 "HTTPS URLs require Tls.CertPath and Tls.KeyPath".into(),
700 ));
701 }
702 Some(build_tls_acceptor(&tls.cert_path, &tls.key_path)?)
703 } else {
704 None
705 };
706
707 if self.mode == AppMode::Development {
709 tracing::info!("");
710 for url in &urls {
711 let display_url = url.replace("0.0.0.0", "localhost");
712 tracing::info!(" Listening on {}", display_url);
713 }
714 } else {
715 tracing::info!("Listening on {} url(s)", urls.len());
716 }
717
718 let notify = Arc::clone(&self.shutdown);
719 install_shutdown_handler(Arc::clone(¬ify));
720 spawn_async_shutdown_listener(Arc::clone(¬ify));
721
722 let mut handles = Vec::new();
723 let pipeline = Arc::clone(&self.pipeline);
724 let router_handler = self.router_handler.clone();
725 let mode = self.mode;
726 let max_body_size = self.options.app.max_body_size;
727
728 for addr in &http_addrs {
729 let addr = addr.clone();
730 let n = std::sync::Arc::clone(¬ify);
731 let p = Arc::clone(&pipeline);
732 let rh = router_handler.clone();
733 handles.push(tokio::spawn(serve_http(
734 addr,
735 n,
736 p,
737 rh,
738 mode,
739 max_body_size,
740 )));
741 }
742
743 if let Some(ref tls_acceptor) = acceptor {
744 for addr in &https_addrs {
745 let addr = addr.clone();
746 let n = std::sync::Arc::clone(¬ify);
747 let p = Arc::clone(&pipeline);
748 let rh = router_handler.clone();
749 let a = tls_acceptor.clone();
750 handles.push(tokio::spawn(serve_https(
751 addr,
752 a,
753 n,
754 p,
755 rh,
756 mode,
757 max_body_size,
758 )));
759 }
760 }
761
762 for h in handles {
764 let _ = h.await;
765 }
766
767 stop_hosted_services(&self.hosted_services).await;
768
769 Ok(())
770 }
771
772 pub async fn run_at(&self, addr: &str) -> Result<()> {
774 start_hosted_services(&self.hosted_services).await?;
775
776 let notify = Arc::clone(&self.shutdown);
777 install_shutdown_handler(Arc::clone(¬ify));
778 spawn_async_shutdown_listener(Arc::clone(¬ify));
779 serve_http(
780 addr.to_string(),
781 notify,
782 Arc::clone(&self.pipeline),
783 self.router_handler.clone(),
784 self.mode,
785 self.options.app.max_body_size,
786 )
787 .await;
788
789 stop_hosted_services(&self.hosted_services).await;
790 Ok(())
791 }
792
793 pub fn server_handle(&self) -> ServerHandle {
804 ServerHandle {
805 shutdown: Arc::clone(&self.shutdown),
806 }
807 }
808
809 pub fn into_server(self) -> Server {
814 Server { host: self }
815 }
816}
817
818#[async_trait::async_trait]
819impl IHost for Host {
820 async fn run(&self, addr: &str) -> Result<()> {
821 self.run_at(addr).await
822 }
823 async fn stop(&self) -> Result<()> {
824 tracing::info!("Stop requested.");
825 self.shutdown.notify_waiters();
826 Ok(())
827 }
828}
829
830fn make_router_handler(router: Arc<Router>) -> HandlerFn {
831 Arc::new(move |ctx: &mut dyn IHttpContext| {
832 let router = Arc::clone(&router);
833 Box::pin(async move {
834 match router.match_route(ctx).await? {
835 Some((endpoint, params, pattern)) => {
836 drop(router);
837 for (key, value) in params {
838 ctx.request_mut().route_params_mut().insert(key, value);
839 }
840 *ctx.request_mut().route_pattern_mut() = Some(pattern);
841 endpoint.handle(ctx).await
842 }
843 None => {
844 drop(router);
845 if !ctx.response().has_body() {
848 write_error_response(ctx, 404, "Not Found").await;
849 }
850 Ok(())
851 }
852 }
853 })
854 })
855}
856
857async fn handle_request(
858 req: Request<Incoming>,
859 pipeline: Arc<MiddlewarePipeline>,
860 router_handler: HandlerFn,
861 max_body_size: usize,
862) -> std::result::Result<hyper::Response<Full<Bytes>>, std::convert::Infallible> {
863 let mut ctx = HttpContext::new(req, max_body_size).await;
864 let result = pipeline.execute(&mut ctx, router_handler).await;
865
866 if let Err(e) = result {
867 let status = e.status_code();
868 write_error_response(&mut ctx, status, &e.to_string()).await;
869 }
870
871 Ok(ctx.into_response())
872}
873
874async fn write_error_response(ctx: &mut dyn IHttpContext, status: u16, message: &str) {
875 write_problem_response(ctx, build_problem(status, message)).await;
876}
877
878fn install_shutdown_handler(shutdown: Arc<tokio::sync::Notify>) {
888 use std::sync::atomic::{AtomicUsize, Ordering};
889
890 static CTRL_COUNT: AtomicUsize = AtomicUsize::new(0);
891
892 if let Err(e) = ctrlc::set_handler(move || {
893 let n = CTRL_COUNT.fetch_add(1, Ordering::SeqCst);
894 if n == 0 {
895 tracing::info!("Shutdown signal received, draining connections...");
896 shutdown.notify_waiters();
897 } else {
898 tracing::warn!("Second interrupt — force exiting.");
899 std::process::exit(130);
900 }
901 }) {
902 tracing::warn!("Could not install Ctrl+C handler: {}", e);
903 }
904}
905
906fn spawn_async_shutdown_listener(shutdown: Arc<tokio::sync::Notify>) {
908 tokio::spawn(async move {
909 wait_for_shutdown_signal().await;
910 tracing::info!("Async shutdown signal received, draining connections...");
911 shutdown.notify_waiters();
912 });
913}
914
915async fn wait_for_shutdown_signal() {
916 #[cfg(unix)]
917 {
918 use tokio::signal::unix::{signal, SignalKind};
919 let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM handler");
920 tokio::select! {
921 res = tokio::signal::ctrl_c() => {
922 if let Err(e) = res {
923 tracing::warn!("Ctrl+C listener error: {}", e);
924 }
925 }
926 _ = sigterm.recv() => {}
927 }
928 }
929 #[cfg(not(unix))]
930 {
931 if let Err(e) = tokio::signal::ctrl_c().await {
932 tracing::warn!("Ctrl+C listener error: {}", e);
933 }
934 }
935}
936
937async fn start_hosted_services(services: &[Arc<dyn IHostedService>]) -> Result<()> {
938 if services.is_empty() {
939 return Ok(());
940 }
941 tracing::info!("Starting {} hosted service(s)...", services.len());
942 for svc in services {
943 svc.start().await?;
944 }
945 tracing::info!("All hosted services started.");
946 Ok(())
947}
948
949async fn stop_hosted_services(services: &[Arc<dyn IHostedService>]) {
950 if services.is_empty() {
951 return;
952 }
953 tracing::info!("Stopping {} hosted service(s)...", services.len());
954 for svc in services {
955 if let Err(e) = svc.stop().await {
956 tracing::warn!("Hosted service stop error: {}", e);
957 }
958 }
959 tracing::info!("All hosted services stopped.");
960}
961
962async fn drain_connections(
963 join_set: &mut JoinSet<()>,
964 label: &str,
965 mode: AppMode,
966) {
967 let timeout_secs = if mode == AppMode::Development { 5 } else { 30 };
968 let drain_future = async {
969 while let Some(result) = join_set.join_next().await {
970 if let Err(e) = result {
971 tracing::error!("Connection task panicked: {:?}", e);
972 }
973 }
974 };
975
976 match tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), drain_future).await {
977 Ok(_) => tracing::info!("{}: drained.", label),
978 Err(_) => {
979 tracing::warn!("{}: drain timeout, aborting connections.", label);
980 join_set.abort_all();
981 while join_set.join_next().await.is_some() {}
982 }
983 }
984}
985
986fn parse_url(url: &str) -> Result<(&str, String)> {
993 if let Some(rest) = url.strip_prefix("https://") {
994 Ok(("https", rest.to_string()))
995 } else if let Some(rest) = url.strip_prefix("http://") {
996 Ok(("http", rest.to_string()))
997 } else {
998 Err(rust_webx_core::error::Error::Http(format!(
999 "Invalid URL '{}'. Use http://host:port or https://host:port",
1000 url
1001 )))
1002 }
1003}
1004
1005async fn serve_http(
1007 addr: String,
1008 shutdown: std::sync::Arc<tokio::sync::Notify>,
1009 pipeline: Arc<MiddlewarePipeline>,
1010 router_handler: HandlerFn,
1011 mode: AppMode,
1012 max_body_size: usize,
1013) {
1014 let listener = match tokio::net::TcpListener::bind(&addr).await {
1015 Ok(l) => l,
1016 Err(e) => {
1017 tracing::error!("Failed to bind HTTP on {}: {}", addr, e);
1018 return;
1019 }
1020 };
1021
1022 let mut join_set = JoinSet::new();
1023
1024 loop {
1025 tokio::select! {
1026 accept_result = listener.accept() => {
1027 let (stream, _) = match accept_result {
1028 Ok(v) => v,
1029 Err(e) => {
1030 tracing::error!("Accept error (will retry): {}", e);
1031 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1032 continue;
1033 }
1034 };
1035
1036 while join_set.try_join_next().is_some() {}
1037
1038 let io = TokioIo::new(stream);
1039 let pipeline = Arc::clone(&pipeline);
1040 let router_handler = router_handler.clone();
1041
1042 join_set.spawn(async move {
1043 let svc_fn = service_fn(move |req: Request<Incoming>| {
1044 let pipeline = Arc::clone(&pipeline);
1045 let router_handler = router_handler.clone();
1046 let mode = mode;
1047 async move {
1048 let start = Instant::now();
1049 let method = req.method().to_string();
1050 let path = req.uri().path().to_string();
1051 let result =
1052 handle_request(req, pipeline, router_handler, max_body_size).await;
1053 let elapsed = start.elapsed();
1054 if mode == AppMode::Development {
1055 let status =
1056 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1057 tracing::info!(
1058 "[{}] {} -> {} ({:.0}ms)",
1059 method,
1060 path,
1061 status,
1062 elapsed.as_secs_f64() * 1000.0
1063 );
1064 }
1065 result
1066 }
1067 });
1068
1069 if let Err(err) = hyper::server::conn::http1::Builder::new()
1070 .serve_connection(io, svc_fn)
1071 .await
1072 {
1073 tracing::error!("Connection error: {}", err);
1074 }
1075 });
1076 }
1077 _ = shutdown.notified() => {
1078 tracing::info!("HTTP {}: stop accepting, draining...", addr);
1079 break;
1080 }
1081 }
1082 }
1083
1084 drain_connections(&mut join_set, &format!("HTTP {}", addr), mode).await;
1085}
1086
1087async fn serve_https(
1089 addr: String,
1090 acceptor: TlsAcceptor,
1091 shutdown: std::sync::Arc<tokio::sync::Notify>,
1092 pipeline: Arc<MiddlewarePipeline>,
1093 router_handler: HandlerFn,
1094 mode: AppMode,
1095 max_body_size: usize,
1096) {
1097 let listener = match tokio::net::TcpListener::bind(&addr).await {
1098 Ok(l) => l,
1099 Err(e) => {
1100 tracing::error!("Failed to bind HTTPS on {}: {}", addr, e);
1101 return;
1102 }
1103 };
1104
1105 let mut join_set = JoinSet::new();
1106
1107 loop {
1108 tokio::select! {
1109 accept_result = listener.accept() => {
1110 let (stream, _) = match accept_result {
1111 Ok(v) => v,
1112 Err(e) => {
1113 tracing::error!("Accept error (will retry): {}", e);
1114 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1115 continue;
1116 }
1117 };
1118
1119 while join_set.try_join_next().is_some() {}
1120
1121 let acceptor = acceptor.clone();
1122 let pipeline = Arc::clone(&pipeline);
1123 let router_handler = router_handler.clone();
1124
1125 join_set.spawn(async move {
1126 match acceptor.accept(stream).await {
1127 Ok(tls_stream) => {
1128 let io = TokioIo::new(tls_stream);
1129 let svc_fn = service_fn(move |req: Request<Incoming>| {
1130 let pipeline = Arc::clone(&pipeline);
1131 let router_handler = router_handler.clone();
1132 let mode = mode;
1133 async move {
1134 let start = Instant::now();
1135 let method = req.method().to_string();
1136 let path = req.uri().path().to_string();
1137 let result =
1138 handle_request(req, pipeline, router_handler, max_body_size)
1139 .await;
1140 let elapsed = start.elapsed();
1141 if mode == AppMode::Development {
1142 let status =
1143 result.as_ref().map(|r| r.status().as_u16()).unwrap_or(500);
1144 tracing::info!(
1145 "[{}] {} -> {} ({:.0}ms)",
1146 method,
1147 path,
1148 status,
1149 elapsed.as_secs_f64() * 1000.0
1150 );
1151 }
1152 result
1153 }
1154 });
1155
1156 if let Err(err) = hyper::server::conn::http1::Builder::new()
1157 .serve_connection(io, svc_fn)
1158 .await
1159 {
1160 tracing::error!("TLS connection error: {}", err);
1161 }
1162 }
1163 Err(e) => {
1164 tracing::error!("TLS handshake error: {}", e);
1165 }
1166 }
1167 });
1168 }
1169 _ = shutdown.notified() => {
1170 tracing::info!("HTTPS {}: stop accepting, draining...", addr);
1171 break;
1172 }
1173 }
1174 }
1175
1176 drain_connections(&mut join_set, &format!("HTTPS {}", addr), mode).await;
1177}
1178
1179fn build_tls_acceptor(cert_path: &str, key_path: &str) -> Result<TlsAcceptor> {
1185 use std::fs::File;
1186 use std::io::BufReader;
1187
1188 if cert_path.is_empty() || key_path.is_empty() {
1189 return Err(rust_webx_core::error::Error::Http(
1190 "TLS certificate or key path not configured.".into(),
1191 ));
1192 }
1193
1194 let cert_file = File::open(cert_path).map_err(|e| {
1195 rust_webx_core::error::Error::Http(format!("Cannot open cert '{}': {}", cert_path, e))
1196 })?;
1197 let mut cert_reader = BufReader::new(cert_file);
1198 let certs: Vec<CertificateDer> = certs(&mut cert_reader).filter_map(|r| r.ok()).collect();
1199 if certs.is_empty() {
1200 return Err(rust_webx_core::error::Error::Http(format!(
1201 "No valid certs in '{}'",
1202 cert_path
1203 )));
1204 }
1205
1206 let key_file = File::open(key_path).map_err(|e| {
1207 rust_webx_core::error::Error::Http(format!("Cannot open key '{}': {}", key_path, e))
1208 })?;
1209 let mut key_reader = BufReader::new(key_file);
1210 let key = pkcs8_private_keys(&mut key_reader)
1211 .filter_map(|r| r.ok())
1212 .map(PrivateKeyDer::from)
1213 .next()
1214 .or_else(|| {
1215 let key_file2 = File::open(key_path).map(BufReader::new).ok()?;
1216 let mut kr2 = key_file2;
1217 let rsa_keys: Vec<PrivateKeyDer> = rsa_private_keys(&mut kr2)
1218 .filter_map(|r| r.ok())
1219 .map(PrivateKeyDer::from)
1220 .collect();
1221 rsa_keys.into_iter().next()
1222 })
1223 .ok_or_else(|| {
1224 rust_webx_core::error::Error::Http(format!("No valid private key in '{}'", key_path))
1225 })?;
1226
1227 let config = ServerConfig::builder()
1228 .with_no_client_auth()
1229 .with_single_cert(certs, key)
1230 .map_err(|e| rust_webx_core::error::Error::Http(format!("TLS config error: {}", e)))?;
1231
1232 Ok(TlsAcceptor::from(std::sync::Arc::new(config)))
1233}