Skip to main content

impulse_server_kit/
startup.rs

1//! Startup module.
2//!
3//! In most cases, you just need to use `start` function:
4//!
5//! ```rust,ignore
6//! let (server, _) = start(app_state, app_config, router).await.unwrap();
7//! server.await
8//! ```
9
10use impulse_utils::errors::ServerError;
11use impulse_utils::prelude::MResult;
12use salvo::prelude::*;
13
14use salvo::conn::rustls::{Keycert, RustlsConfig};
15use salvo::server::ServerHandle;
16use std::future::Future;
17use std::pin::Pin;
18use std::process::Command;
19
20#[cfg(feature = "http3")]
21use salvo::http::HeaderValue;
22#[cfg(feature = "http3")]
23use salvo::http::header::ALT_SVC;
24
25use crate::setup::{GenericServerState, GenericSetup, StartupVariant};
26
27static TLS13: &[&rustls::SupportedProtocolVersion] = &[&rustls::version::TLS13];
28
29#[cfg(feature = "http3")]
30#[handler]
31/// HTTP2-to-HTTP3 switching header.
32///
33/// Usage is `router.hoop(h3_header)`.
34pub async fn h3_header(depot: &mut Depot, res: &mut Response) {
35  use crate::setup::GenericValues;
36
37  let server_port = match depot.obtain::<GenericValues>() {
38    Ok(app_config) => app_config.server_port.unwrap(),
39    Err(_) => 443,
40  };
41
42  res
43    .headers_mut()
44    .insert(
45      ALT_SVC,
46      HeaderValue::from_str(&format!(r##"h3=":{server_port}"; ma=2592000"##)).unwrap(),
47    )
48    .unwrap();
49}
50
51fn tlsv13(certpath: impl AsRef<str>, keypath: impl AsRef<str>) -> MResult<RustlsConfig> {
52  Ok(
53    RustlsConfig::new(
54      Keycert::new()
55        .cert_from_path(certpath.as_ref())
56        .map_err(|e| ServerError::from_private(e).with_500())?
57        .key_from_path(keypath.as_ref())
58        .map_err(|e| ServerError::from_private(e).with_500())?,
59    )
60    .tls_versions(TLS13),
61  )
62}
63
64#[cfg(feature = "otel")]
65#[handler]
66/// Default Server Kit OpenTelemetry metrics.
67///
68/// Installed by default with `get_root_router_autoinject` method.
69pub async fn sk_default_metrics(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
70  let meter = crate::otel::api::global::meter("sk_metrics");
71
72  let request_counter = meter
73    .u64_counter("sk_requests")
74    .with_unit("1")
75    .with_description("Total number of requests")
76    .build();
77
78  let request_duration = meter
79    .f64_histogram("sk_request_duration")
80    .with_unit("s")
81    .with_description("HTTP request duration in seconds")
82    .build();
83
84  let active_connections = meter
85    .i64_up_down_counter("sk_active_connections")
86    .with_unit("1")
87    .with_description("Number of active HTTP connections")
88    .build();
89
90  let host = req.uri().host().map(String::from);
91  let path = req.uri().path().to_string();
92  let method = req.method().as_str().to_string();
93
94  let attributes = vec![
95    opentelemetry::KeyValue::new("host", host.unwrap_or(String::from("unknown"))),
96    opentelemetry::KeyValue::new("path", path),
97    opentelemetry::KeyValue::new("method", method),
98    opentelemetry::KeyValue::new("user_agent", req.header("user-agent").unwrap_or("unknown").to_string()),
99  ];
100
101  active_connections.add(1, &[]);
102  active_connections.add(1, &attributes);
103  let start = tokio::time::Instant::now();
104
105  ctrl.call_next(req, depot, res).await;
106
107  let duration = start.elapsed().as_secs_f64();
108
109  let mut result_attributes = attributes.clone();
110  let status = res.status_code.unwrap_or(StatusCode::OK).as_u16().to_string();
111  result_attributes.push(opentelemetry::KeyValue::new("status", status));
112
113  request_counter.add(1, &[]);
114  request_counter.add(1, &result_attributes);
115  request_duration.record(duration, &result_attributes);
116
117  active_connections.add(-1, &attributes);
118}
119
120/// Returns preconfigured router with app state and OpenTelemetry metrics injected.
121///
122/// To get your `app_config` inside handler/endpoint, call
123/// `depot.obtain::<YourAppConfigType>().unwrap()`.
124pub fn get_root_router_autoinject<T: GenericSetup + Send + Sync + Clone + 'static>(
125  app_state: &GenericServerState,
126  app_config: T,
127) -> Router {
128  #[allow(unused_mut)]
129  let mut router = Router::new().hoop(affix_state::inject(app_state.clone()).inject(app_config.clone()));
130
131  let sec = &app_config.generic_values().security_headers;
132  if sec.enabled {
133    router = router.hoop(crate::security_headers::SecurityHeaders::new(sec));
134  }
135
136  #[cfg(all(feature = "http3", feature = "acme"))]
137  if app_state.startup_variant == StartupVariant::QuinnAcme {
138    router = router.hoop(h3_header);
139  }
140
141  #[cfg(feature = "http3")]
142  if app_state.startup_variant == StartupVariant::Quinn || app_state.startup_variant == StartupVariant::QuinnOnly {
143    router = router.hoop(h3_header);
144  }
145
146  #[cfg(feature = "otel")]
147  if app_config.generic_values().tracing_options.otel_http_endpoint.is_some() {
148    router = router.hoop(sk_default_metrics);
149  }
150
151  router
152}
153
154/// Returns preconfigured root router to use.
155///
156/// Usually it installs application config and state in `affix_state` and installs `h3_header` for switching protocol to QUIC, if used.
157#[allow(unused_variables)]
158pub fn get_root_router(app_state: &GenericServerState) -> Router {
159  #[allow(unused_mut)]
160  let mut router = Router::new();
161
162  #[cfg(all(feature = "http3", feature = "acme"))]
163  if app_state.startup_variant == StartupVariant::QuinnAcme {
164    router = router.hoop(h3_header);
165  }
166
167  #[cfg(feature = "http3")]
168  if app_state.startup_variant == StartupVariant::Quinn || app_state.startup_variant == StartupVariant::QuinnOnly {
169    router = router.hoop(h3_header);
170  }
171
172  router
173}
174
175#[cfg(any(feature = "oapi", feature = "acme"))]
176#[allow(clippy::mut_from_ref, invalid_reference_casting)]
177unsafe fn make_mut<T>(reference: &T) -> &mut T {
178  let const_ptr = reference as *const T;
179  let mut_ptr = const_ptr as *mut T;
180  unsafe { &mut *mut_ptr }
181}
182
183/// Starts up HTTPS redirect server.
184///
185/// Example:
186///
187/// ```rust,ignore
188/// let (server, _) = start(app_state, app_config, router).await.unwrap();
189/// let (redirect, _) = start_force_https_redirect(80, 443).await.unwrap();
190///
191/// tracing::info!("Server is booted.");
192///
193/// tokio::select! {
194///   _ = server   => tracing::info!("Server is shutdowned."),
195///   _ = redirect => tracing::info!("Redirect is shutdowned."),
196/// }
197/// ```
198#[cfg(feature = "force-https")]
199pub async fn start_force_https_redirect(
200  listen_port: u16,
201  redirect_port: u16,
202) -> MResult<(Pin<Box<dyn Future<Output = ()> + Send>>, ServerHandle)> {
203  let service = Service::new(Router::new()).hoop(ForceHttps::new().https_port(redirect_port));
204  let acceptor = TcpListener::new(format!("0.0.0.0:{listen_port}")).bind().await;
205  let server = Server::new(acceptor);
206  let handle = server.handle();
207  let server = Box::pin(server.serve(service));
208  Ok((server, handle))
209}
210
211/// Starts your application with provided service, if you predefined one by yourself.
212///
213/// For example, you can setup service with error catcher or any other middleware that
214/// `salvo` provides.
215pub async fn start_with_service(
216  app_state: GenericServerState,
217  app_config: &impl GenericSetup,
218  #[allow(unused_mut)] mut service: Service,
219) -> MResult<(Pin<Box<dyn Future<Output = ()> + Send>>, ServerHandle)> {
220  tracing::info!("Server is starting...");
221
222  rustls::crypto::aws_lc_rs::default_provider()
223    .install_default()
224    .map_err(|_| ServerError::from_private_str("Can't install default crypto provider!").with_500())?;
225  let app_config = app_config.generic_values();
226
227  if let Some(bin) = app_config.auto_migrate_bin.as_ref() {
228    Command::new(bin)
229      .spawn()
230      .map_err(|e| ServerError::from_private(e).with_500())?;
231  }
232
233  #[cfg(feature = "oapi")]
234  if app_config.allow_oapi_access.is_some_and(|v| v) {
235    let doc = OpenApi::new(
236      app_config.oapi_name.as_ref().unwrap(),
237      app_config.oapi_ver.as_ref().unwrap(),
238    )
239    .merge_router(&service.router);
240
241    let oapi_endpoint = if let Some(ftype) = app_config.oapi_frontend_type.as_ref() {
242      match ftype.as_str() {
243        "Scalar" => Some(
244          Scalar::new(format!("{}/openapi.json", app_config.oapi_api_addr.as_ref().unwrap()))
245            .title(format!(
246              "{} - API @ Scalar",
247              app_config.oapi_name.as_ref().unwrap_or(&app_config.app_name)
248            ))
249            .description(format!(
250              "{} - API",
251              app_config.oapi_name.as_ref().unwrap_or(&app_config.app_name)
252            ))
253            .into_router(app_config.oapi_api_addr.as_ref().unwrap()),
254        ),
255        "SwaggerUI" => Some(
256          SwaggerUi::new(format!("{}/openapi.json", app_config.oapi_api_addr.as_ref().unwrap()))
257            .title(format!(
258              "{} - API @ SwaggerUI",
259              app_config.oapi_name.as_ref().unwrap_or(&app_config.app_name)
260            ))
261            .description(format!(
262              "{} - API",
263              app_config.oapi_name.as_ref().unwrap_or(&app_config.app_name)
264            ))
265            .into_router(app_config.oapi_api_addr.as_ref().unwrap()),
266        ),
267        _ => None,
268      }
269    } else {
270      None
271    };
272
273    let mut router = Router::new();
274    router = router.push(doc.into_router(format!("{}/openapi.json", app_config.oapi_api_addr.as_ref().unwrap())));
275    if let Some(oapi) = oapi_endpoint {
276      router = router.push(oapi);
277    }
278
279    unsafe {
280      let service_router = make_mut(service.router.as_ref());
281      service_router.routers_mut().insert(0, router);
282    }
283
284    tracing::info!("API is available on {}", app_config.oapi_api_addr.as_ref().unwrap());
285  }
286
287  #[cfg(feature = "cors")]
288  if let Some(domain) = &app_config.allow_cors_domain {
289    let cors = salvo::cors::Cors::new()
290      .allow_origin(domain)
291      .allow_credentials(domain.as_str() != "*")
292      .allow_headers(vec![
293        "Authorization",
294        "Accept",
295        "Access-Control-Allow-Headers",
296        "Content-Type",
297        "Origin",
298        "X-Requested-With",
299        "Cookie",
300      ])
301      .expose_headers(vec!["Set-Cookie"])
302      .allow_methods(vec![
303        salvo::http::Method::GET,
304        salvo::http::Method::POST,
305        salvo::http::Method::PUT,
306        salvo::http::Method::PATCH,
307        salvo::http::Method::DELETE,
308        salvo::http::Method::OPTIONS,
309      ])
310      .into_handler();
311
312    service = service.hoop(cors);
313  }
314
315  let handle;
316
317  let server = match app_state.startup_variant {
318    StartupVariant::HttpLocalhost => {
319      let acceptor = TcpListener::new(format!("127.0.0.1:{}", app_config.server_port.unwrap()))
320        .bind()
321        .await;
322      let server = Server::new(acceptor);
323      handle = server.handle();
324      Box::pin(server.serve(service)) as Pin<Box<dyn Future<Output = ()> + Send>>
325    }
326    StartupVariant::UnsafeHttp => {
327      let acceptor = TcpListener::new(format!(
328        "{}:{}",
329        app_config.server_host.as_ref().unwrap(),
330        app_config.server_port.unwrap()
331      ))
332      .bind()
333      .await;
334      let server = Server::new(acceptor);
335      handle = server.handle();
336      Box::pin(server.serve(service))
337    }
338    #[cfg(feature = "acme")]
339    StartupVariant::HttpsAcme => {
340      let acceptor = TcpListener::new(format!(
341        "{}:{}",
342        app_config.server_host.as_ref().unwrap(),
343        app_config.server_port.unwrap()
344      ))
345      .acme()
346      .cache_path("tmp/letsencrypt")
347      .add_domain(app_config.acme_domain.as_ref().unwrap())
348      .bind()
349      .await;
350      let server = Server::new(acceptor);
351      handle = server.handle();
352      Box::pin(server.serve(service))
353    }
354    StartupVariant::HttpsOnly => {
355      let rustls_config = tlsv13(
356        app_config.ssl_crt_path.as_ref().unwrap(),
357        app_config.ssl_key_path.as_ref().unwrap(),
358      )?;
359      let listener = TcpListener::new(format!(
360        "{}:{}",
361        app_config.server_host.as_ref().unwrap(),
362        app_config.server_port.unwrap()
363      ))
364      .rustls(rustls_config)
365      .bind()
366      .await;
367
368      let server = Server::new(listener);
369      handle = server.handle();
370      Box::pin(server.serve(service))
371    }
372    #[cfg(all(feature = "http3", feature = "acme"))]
373    StartupVariant::QuinnAcme => {
374      let acceptor = TcpListener::new(format!(
375        "{}:{}",
376        app_config.server_host.as_ref().unwrap(),
377        app_config.server_port.unwrap()
378      ))
379      .acme()
380      .cache_path("tmp/letsencrypt")
381      .add_domain(app_config.acme_domain.as_ref().unwrap())
382      .quinn(format!(
383        "{}:{}",
384        app_config.server_host.as_ref().unwrap(),
385        app_config.server_port.unwrap()
386      ))
387      .bind()
388      .await;
389      let server = Server::new(acceptor);
390      handle = server.handle();
391      Box::pin(server.serve(service))
392    }
393    #[cfg(feature = "http3")]
394    StartupVariant::Quinn => {
395      let rustls_config = tlsv13(
396        app_config.ssl_crt_path.as_ref().unwrap(),
397        app_config.ssl_key_path.as_ref().unwrap(),
398      )?;
399      let listener = TcpListener::new(format!(
400        "{}:{}",
401        app_config.server_host.as_ref().unwrap(),
402        app_config.server_port.unwrap()
403      ))
404      .rustls(rustls_config.clone());
405
406      let quinn_config = rustls_config
407        .build_quinn_config()
408        .map_err(|e| ServerError::from_private(e).with_500())?;
409      let acceptor = QuinnListener::new(
410        quinn_config,
411        format!(
412          "{}:{}",
413          app_config.server_host.as_ref().unwrap(),
414          app_config.server_port.unwrap()
415        ),
416      )
417      .join(listener)
418      .bind()
419      .await;
420
421      let server = Server::new(acceptor);
422      handle = server.handle();
423      Box::pin(server.serve(service))
424    }
425    #[cfg(feature = "http3")]
426    StartupVariant::QuinnOnly => {
427      let quinn_config = tlsv13(
428        app_config.ssl_crt_path.as_ref().unwrap(),
429        app_config.ssl_key_path.as_ref().unwrap(),
430      )?
431      .build_quinn_config()
432      .map_err(|e| ServerError::from_private(e).with_500())?;
433      let acceptor = QuinnListener::new(
434        quinn_config,
435        format!(
436          "{}:{}",
437          app_config.server_host.as_ref().unwrap(),
438          app_config.server_port.unwrap()
439        ),
440      )
441      .bind()
442      .await;
443
444      let server = Server::new(acceptor);
445      handle = server.handle();
446      Box::pin(server.serve(service))
447    }
448  };
449
450  Ok((server, handle))
451}
452
453/// Starts the server according to the startup variant provided with the custom shutdown.
454pub async fn start_clean(
455  app_state: GenericServerState,
456  app_config: &impl GenericSetup,
457  router: Router,
458) -> MResult<(Pin<Box<dyn Future<Output = ()> + Send>>, ServerHandle)> {
459  start_with_service(app_state, app_config, Service::new(router)).await
460}
461
462/// Starts the server according to the startup variant provided.
463pub async fn start(
464  app_state: GenericServerState,
465  app_config: &impl GenericSetup,
466  router: Router,
467) -> MResult<(Pin<Box<dyn Future<Output = ()> + Send>>, ServerHandle)> {
468  let (fut, handle) = start_clean(app_state, app_config, router).await?;
469  let ctrl_c_handle = handle.clone();
470  tokio::spawn(async move { shutdown_signal(ctrl_c_handle).await });
471  Ok((fut, handle))
472}
473
474/// Signal to graceful shutdown.
475///
476/// Required to be manually awaited, if you start server with `start_clean`/`start_with_service` functions. Example:
477///
478/// ```rust,ignore
479/// let (server, handle) = start_clean(app_state, app_config, router).await.unwrap();
480/// let default_handle = tokio::spawn(async move { shutdown_signal(handle).await });
481///
482/// tracing::info!("Server is booted.");
483///
484/// tokio::select! {
485///   _ = server         => tracing::info!("Server is shutdowned."),
486///   _ = default_handle => std::process::exit(0),
487/// }
488/// ```
489///
490/// Graceful coroutine starts automatically with `start` function.
491pub async fn shutdown_signal(handle: ServerHandle) {
492  tokio::signal::ctrl_c().await.unwrap();
493  tracing::info!("Shutdown with Ctrl+C requested.");
494  handle.stop_graceful(None);
495}