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  #[serde(flatten)]
96  /// Tracing options
97  pub tracing_options: TracingOptions,
98}
99
100/// Server state.
101#[derive(Clone)]
102pub struct GenericServerState {
103  /// Converted startup variant, ready to launch.
104  pub startup_variant: StartupVariant,
105  /// File log guard; needed to be handled the entire time the application is running.
106  pub _guards: Arc<TracingGuards>,
107}
108
109/// Loads the config from YAML file (`{app_name}.yaml`).
110pub async fn load_generic_config<T: DeserializeOwned + GenericSetup + Default>(app_name: &str) -> MResult<T> {
111  let mut file = std::fs::File::open(format!("{app_name}.yaml"));
112  if file.is_err() {
113    file = std::fs::File::open(format!("/etc/{app_name}.yaml"));
114  }
115  let mut file = file.map_err(|e| {
116    ServerError::from_private(e)
117      .with_public("The server configuration could not be found.")
118      .with_500()
119  })?;
120
121  let mut buffer = String::new();
122  file.read_to_string(&mut buffer).map_err(|e| {
123    ServerError::from_private(e)
124      .with_public("Failed to read the contents of the server configuration file.")
125      .with_500()
126  })?;
127  let mut config: T = serde_pretty_yaml::from_str(&buffer).map_err(|e| {
128    ServerError::from_private(e)
129      .with_public("Failed to parse the contents of the server configuration file.")
130      .with_500()
131  })?;
132
133  let data = config.generic_values_mut();
134  data.app_name = app_name.to_string();
135
136  #[cfg(feature = "oapi")]
137  if data.allow_oapi_access.is_some_and(|v| v) {
138    if data.oapi_name.is_none() {
139      ServerError::from_public("The API name for OAPI is not specified.")
140        .with_500()
141        .bail()?;
142    }
143    if data.oapi_ver.is_none() {
144      ServerError::from_public("The API version for OAPI is not specified.")
145        .with_500()
146        .bail()?;
147    }
148    if data.oapi_api_addr.is_none() {
149      ServerError::from_public("The path to OAPI was not specified.")
150        .with_500()
151        .bail()?;
152    }
153  }
154
155  if let Some(achiever) = &data.server_port_achiever {
156    let port = port_file_watcher(achiever.as_path()).await?;
157    data.server_port = Some(port);
158  }
159
160  Ok(config)
161}
162
163/// Loads the server's state: initializes the logging and checks YAML config for misconfigurations and errors.
164///
165/// You should call this function only once at startup because of logging setup.
166pub async fn load_generic_state<T: GenericSetup>(setup: &T, init_logging: bool) -> MResult<GenericServerState> {
167  let data = setup.generic_values();
168
169  let guards = if init_logging {
170    data.tracing_options.init(&data.app_name)?
171  } else {
172    Default::default()
173  };
174
175  let state = GenericServerState {
176    startup_variant: match &*data.startup_type {
177      "http_localhost" => {
178        if data.server_host.is_some() {
179          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()?;
180        }
181        StartupVariant::HttpLocalhost
182      }
183      "unsafe_http" => {
184        if data.server_host.is_none() {
185          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
186            .with_500()
187            .bail()?;
188        }
189        StartupVariant::UnsafeHttp
190      }
191      #[cfg(feature = "acme")]
192      "https_acme" => {
193        if data.server_host.is_none() {
194          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
195            .with_500()
196            .bail()?;
197        }
198        if data.acme_domain.is_none() {
199          ServerError::from_public("Choose ACME's domain!").with_500().bail()?;
200        }
201        StartupVariant::HttpsAcme
202      }
203      "https_only" => {
204        if data.server_host.is_none() {
205          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
206            .with_500()
207            .bail()?;
208        }
209        if data.ssl_key_path.is_none() {
210          ServerError::from_public("Choose SSL key path.").with_500().bail()?;
211        }
212        if data.ssl_crt_path.is_none() {
213          ServerError::from_public("Choose SSL cert path.").with_500().bail()?;
214        }
215        StartupVariant::HttpsOnly
216      }
217      #[cfg(all(feature = "http3", feature = "acme"))]
218      "quinn_acme" => {
219        if data.server_host.is_none() {
220          ServerError::from_public("Choose server's host, e.g. `0.0.0.0`.")
221            .with_500()
222            .bail()?;
223        }
224        if data.acme_domain.is_none() {
225          ServerError::from_public("Choose ACME's domain!").with_500().bail()?;
226        }
227        StartupVariant::QuinnAcme
228      }
229      #[cfg(feature = "http3")]
230      "quinn" => {
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::Quinn
243      }
244      #[cfg(feature = "http3")]
245      "quinn_only" => {
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.ssl_key_path.is_none() {
252          ServerError::from_public("Choose SSL key path.").with_500().bail()?;
253        }
254        if data.ssl_crt_path.is_none() {
255          ServerError::from_public("Choose SSL cert path.").with_500().bail()?;
256        }
257        StartupVariant::QuinnOnly
258      }
259      _ => ServerError::from_public(
260        "The server deployment method could not be determined. Read the documentation on the `startup_variant` field.",
261      )
262      .with_500()
263      .bail()?,
264    },
265    _guards: Arc::new(guards),
266  };
267  Ok(state)
268}