Skip to main content

impulse_server_kit/setup/
mod.rs

1//! Setup module.
2
3use serde::Deserialize;
4use serde::de::DeserializeOwned;
5use std::io::Read;
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use impulse_utils::prelude::*;
10
11pub mod port_achiever;
12pub mod tracing_init;
13
14use crate::setup::port_achiever::port_file_watcher;
15use crate::setup::tracing_init::{TracingGuards, TracingOptions};
16
17/// Provides at least values needed by Server Kit to start.
18pub trait GenericSetup {
19  /// Provides generic values; see `GenericValues`.
20  fn generic_values(&self) -> &GenericValues;
21  /// Provides mutable generic values; see `GenericValues`.
22  fn generic_values_mut(&mut self) -> &mut GenericValues;
23}
24
25/// Server startup variants.
26///
27/// These are the hardcoded variants; by default, `salvo` can much more than this.
28#[derive(Clone, Eq, PartialEq)]
29pub enum StartupVariant {
30  /// Will listen `http://127.0.0.1:{port}` only.
31  HttpLocalhost,
32  /// Will listen `http://{host}:{port}`. Not recommended.
33  UnsafeHttp,
34  #[cfg(feature = "acme")]
35  /// Will listen `https://{host}:{port}` with automatic SSL certificate acquiring.
36  HttpsAcme,
37  #[cfg(all(feature = "http3", feature = "acme"))]
38  /// Will listen `https|quic://{host}:{port}` with automatic SSL certificate acquiring.
39  QuinnAcme,
40  /// Will listen `https://{host}:{port}` with your SSL cert and key.
41  HttpsOnly,
42  #[cfg(feature = "http3")]
43  /// Will listen `https|quic://{host}:{port}` with your SSL cert and key.
44  Quinn,
45  #[cfg(feature = "http3")]
46  /// Will listen `quic://{host}:{port}` only, with your SSL cert and key.
47  QuinnOnly,
48}
49
50/// Server generic configuration.
51#[derive(Clone, Deserialize, Default)]
52pub struct GenericValues {
53  /// Application name.
54  ///
55  /// You're not needed to write it in YAML configuration, instead you should send it to `load_generic_config` function.
56  #[serde(skip)]
57  pub app_name: String,
58  /// Startup variant. Converts to `StartupVariant`.
59  pub startup_type: String,
60  /// Server host.
61  pub server_host: Option<String>,
62  /// Server port. For no reverse proxy and Internet usage, set to `80` for HTTP and `443` for HTTPS/QUIC.
63  pub server_port: Option<u16>,
64  /// ACME origin; see [`salvo/conn/acme` docs](https://docs.rs/salvo/latest/salvo/conn/acme/index.html).
65  pub acme_domain: Option<String>,
66  /// Path to SSL key.
67  pub ssl_key_path: Option<String>,
68  /// Path to SSL certificate.
69  pub ssl_crt_path: Option<String>,
70  /// If you want to run any migration or anything else just before server's start, set to path to binary.
71  pub auto_migrate_bin: Option<String>,
72  /// Use text file to find out which port to listen to.
73  pub server_port_achiever: Option<PathBuf>,
74
75  #[cfg(feature = "cors")]
76  /// CORS allowed domains
77  pub allow_cors_domain: Option<String>,
78
79  #[cfg(feature = "oapi")]
80  /// Set this to `true` to enable OpenAPI endpoint.
81  pub allow_oapi_access: Option<bool>,
82  #[cfg(feature = "oapi")]
83  /// Select `Scalar` or `SwaggerUI`.
84  pub oapi_frontend_type: Option<String>,
85  #[cfg(feature = "oapi")]
86  /// By default, equals `app_name`; consider give expanded API name.
87  pub oapi_name: Option<String>,
88  #[cfg(feature = "oapi")]
89  /// API version.
90  pub oapi_ver: Option<String>,
91  #[cfg(feature = "oapi")]
92  /// API endpoint (with slash), e.g. `/api` or `/swagger`.
93  pub oapi_api_addr: Option<String>,
94
95  #[cfg(feature = "leptos-ssr")]
96  /// Path to the front-end dist directory (the folder produced by
97  /// `cargo-leptos` / `trunk`). The SSR handler serves every file under this
98  /// directory at the URL that mirrors its on-disk path; unknown paths fall
99  /// through to the SSR renderer.
100  ///
101  /// Resolution order (first hit wins): `IMPULSE_FRONTEND_DIST` env var,
102  /// this field, `./dist`, `/usr/local/frontend-dist`.
103  pub frontend_dist_path: Option<PathBuf>,
104  #[cfg(feature = "leptos-ssr")]
105  /// Output bundle name (matches `cargo-leptos`'s `output-name`). Used to
106  /// build URLs for the wasm/JS/CSS bundle, e.g. `/pkg/<name>.js`.
107  pub leptos_output_name: Option<String>,
108  #[cfg(feature = "leptos-ssr")]
109  /// Reserved for the upcoming server-functions support. Defaults to
110  /// `/api/leptos`.
111  pub leptos_server_fn_prefix: Option<String>,
112  #[cfg(feature = "leptos-ssr")]
113  /// SEO defaults injected as a Leptos context during SSR.
114  pub leptos_seo: Option<crate::leptos_ssr::SeoDefaults>,
115
116  /// Security response headers applied to every response when the
117  /// router is built via `get_root_router_autoinject`. See
118  /// [`crate::security_headers::SecurityHeadersOptions`].
119  #[serde(default)]
120  pub security_headers: crate::security_headers::SecurityHeadersOptions,
121
122  #[serde(flatten)]
123  /// Tracing options
124  pub tracing_options: TracingOptions,
125}
126
127/// Server state.
128#[derive(Clone)]
129pub struct GenericServerState {
130  /// Converted startup variant, ready to launch.
131  pub startup_variant: StartupVariant,
132  /// File log guard; needed to be handled the entire time the application is running.
133  pub _guards: Arc<TracingGuards>,
134}
135
136/// Loads the config from YAML file (`{app_name}.yaml`).
137pub async fn load_generic_config<T: DeserializeOwned + GenericSetup + Default>(app_name: &str) -> MResult<T> {
138  let mut file = std::fs::File::open(format!("{app_name}.yaml"));
139  if file.is_err() {
140    file = std::fs::File::open(format!("/etc/{app_name}.yaml"));
141  }
142  let mut file = file.map_err(|e| {
143    ServerError::from_private(e)
144      .with_public("The server configuration could not be found.")
145      .with_500()
146  })?;
147
148  let mut buffer = String::new();
149  file.read_to_string(&mut buffer).map_err(|e| {
150    ServerError::from_private(e)
151      .with_public("Failed to read the contents of the server configuration file.")
152      .with_500()
153  })?;
154  let mut config: T = serde_pretty_yaml::from_str(&buffer).map_err(|e| {
155    ServerError::from_private(e)
156      .with_public("Failed to parse the contents of the server configuration file.")
157      .with_500()
158  })?;
159
160  let data = config.generic_values_mut();
161  data.app_name = app_name.to_string();
162
163  #[cfg(feature = "oapi")]
164  if data.allow_oapi_access.is_some_and(|v| v) {
165    if data.oapi_name.is_none() {
166      ServerError::from_public("The API name for OAPI is not specified.")
167        .with_500()
168        .bail()?;
169    }
170    if data.oapi_ver.is_none() {
171      ServerError::from_public("The API version for OAPI is not specified.")
172        .with_500()
173        .bail()?;
174    }
175    if data.oapi_api_addr.is_none() {
176      ServerError::from_public("The path to OAPI was not specified.")
177        .with_500()
178        .bail()?;
179    }
180  }
181
182  if let Some(achiever) = &data.server_port_achiever {
183    let port = port_file_watcher(achiever.as_path()).await?;
184    data.server_port = Some(port);
185  }
186
187  Ok(config)
188}
189
190/// Loads the server's state: initializes the logging and checks YAML config for misconfigurations and errors.
191///
192/// You should call this function only once at startup because of logging setup.
193pub async fn load_generic_state<T: GenericSetup>(setup: &T, init_logging: bool) -> MResult<GenericServerState> {
194  let data = setup.generic_values();
195
196  let guards = if init_logging {
197    data.tracing_options.init(&data.app_name)?
198  } else {
199    Default::default()
200  };
201
202  let state = GenericServerState {
203    startup_variant: match &*data.startup_type {
204      "http_localhost" => {
205        if data.server_host.is_some() {
206          ServerError::from_public("Server will only listen `127.0.0.1` address because of `http_localhost` startup variant. Consider to move to `https_only` or `quinn`.").with_500().bail()?;
207        }
208        StartupVariant::HttpLocalhost
209      }
210      "unsafe_http" => {
211        if data.server_host.is_none() {
212          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
213            .with_500()
214            .bail()?;
215        }
216        StartupVariant::UnsafeHttp
217      }
218      #[cfg(feature = "acme")]
219      "https_acme" => {
220        if data.server_host.is_none() {
221          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
222            .with_500()
223            .bail()?;
224        }
225        if data.acme_domain.is_none() {
226          ServerError::from_public("Choose ACME's domain!").with_500().bail()?;
227        }
228        StartupVariant::HttpsAcme
229      }
230      "https_only" => {
231        if data.server_host.is_none() {
232          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
233            .with_500()
234            .bail()?;
235        }
236        if data.ssl_key_path.is_none() {
237          ServerError::from_public("Choose SSL key path.").with_500().bail()?;
238        }
239        if data.ssl_crt_path.is_none() {
240          ServerError::from_public("Choose SSL cert path.").with_500().bail()?;
241        }
242        StartupVariant::HttpsOnly
243      }
244      #[cfg(all(feature = "http3", feature = "acme"))]
245      "quinn_acme" => {
246        if data.server_host.is_none() {
247          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
248            .with_500()
249            .bail()?;
250        }
251        if data.acme_domain.is_none() {
252          ServerError::from_public("Choose ACME's domain!").with_500().bail()?;
253        }
254        StartupVariant::QuinnAcme
255      }
256      #[cfg(feature = "http3")]
257      "quinn" => {
258        if data.server_host.is_none() {
259          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
260            .with_500()
261            .bail()?;
262        }
263        if data.ssl_key_path.is_none() {
264          ServerError::from_public("Choose SSL key path.").with_500().bail()?;
265        }
266        if data.ssl_crt_path.is_none() {
267          ServerError::from_public("Choose SSL cert path.").with_500().bail()?;
268        }
269        StartupVariant::Quinn
270      }
271      #[cfg(feature = "http3")]
272      "quinn_only" => {
273        if data.server_host.is_none() {
274          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
275            .with_500()
276            .bail()?;
277        }
278        if data.ssl_key_path.is_none() {
279          ServerError::from_public("Choose SSL key path.").with_500().bail()?;
280        }
281        if data.ssl_crt_path.is_none() {
282          ServerError::from_public("Choose SSL cert path.").with_500().bail()?;
283        }
284        StartupVariant::QuinnOnly
285      }
286      _ => ServerError::from_public(
287        "The server deployment method could not be determined. Read the documentation on the `startup_variant` field.",
288      )
289      .with_500()
290      .bail()?,
291    },
292    _guards: Arc::new(guards),
293  };
294  Ok(state)
295}