1#![allow(deprecated)]
9
10use std::borrow::Cow;
11
12use anyhow::bail;
13use camino::Utf8PathBuf;
14use ipnetwork::IpNetwork;
15use mas_keystore::PrivateKey;
16use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, pem::PemObject};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use url::Url;
20
21use super::ConfigurationSection;
22
23fn default_public_base() -> Url {
24 "http://[::]:8080".parse().unwrap()
25}
26
27#[cfg(not(any(feature = "docker", feature = "dist")))]
28fn http_listener_assets_path_default() -> Utf8PathBuf {
29 "./frontend/dist/".into()
30}
31
32#[cfg(feature = "docker")]
33fn http_listener_assets_path_default() -> Utf8PathBuf {
34 "/usr/local/share/mas-cli/assets/".into()
35}
36
37#[cfg(feature = "dist")]
38fn http_listener_assets_path_default() -> Utf8PathBuf {
39 "./share/assets/".into()
40}
41
42fn is_default_http_listener_assets_path(value: &Utf8PathBuf) -> bool {
43 *value == http_listener_assets_path_default()
44}
45
46fn default_trusted_proxies() -> Vec<IpNetwork> {
47 vec![
48 IpNetwork::new([192, 168, 0, 0].into(), 16).unwrap(),
49 IpNetwork::new([172, 16, 0, 0].into(), 12).unwrap(),
50 IpNetwork::new([10, 0, 0, 0].into(), 10).unwrap(),
51 IpNetwork::new(std::net::Ipv4Addr::LOCALHOST.into(), 8).unwrap(),
52 IpNetwork::new([0xfd00, 0, 0, 0, 0, 0, 0, 0].into(), 8).unwrap(),
53 IpNetwork::new(std::net::Ipv6Addr::LOCALHOST.into(), 128).unwrap(),
54 ]
55}
56
57#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy)]
59#[serde(rename_all = "lowercase")]
60pub enum UnixOrTcp {
61 Unix,
63
64 Tcp,
66}
67
68impl UnixOrTcp {
69 #[must_use]
71 pub const fn unix() -> Self {
72 Self::Unix
73 }
74
75 #[must_use]
77 pub const fn tcp() -> Self {
78 Self::Tcp
79 }
80}
81
82#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
84#[serde(untagged)]
85pub enum BindConfig {
86 Listen {
88 #[serde(skip_serializing_if = "Option::is_none")]
92 host: Option<String>,
93
94 port: u16,
96 },
97
98 Address {
100 #[schemars(
102 example = &"[::1]:8080",
103 example = &"[::]:8080",
104 example = &"127.0.0.1:8080",
105 example = &"0.0.0.0:8080",
106 )]
107 address: String,
108 },
109
110 Unix {
112 #[schemars(with = "String")]
114 socket: Utf8PathBuf,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 #[schemars(example = &"600")]
119 mode: Option<String>,
120 },
121
122 FileDescriptor {
128 #[serde(default)]
132 fd: usize,
133
134 #[serde(default = "UnixOrTcp::tcp")]
137 kind: UnixOrTcp,
138 },
139}
140
141#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
143pub struct TlsConfig {
144 #[serde(skip_serializing_if = "Option::is_none")]
148 pub certificate: Option<String>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
154 #[schemars(with = "Option<String>")]
155 pub certificate_file: Option<Utf8PathBuf>,
156
157 #[serde(skip_serializing_if = "Option::is_none")]
161 pub key: Option<String>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
167 #[schemars(with = "Option<String>")]
168 pub key_file: Option<Utf8PathBuf>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
175 pub password: Option<String>,
176
177 #[serde(skip_serializing_if = "Option::is_none")]
182 #[schemars(with = "Option<String>")]
183 pub password_file: Option<Utf8PathBuf>,
184}
185
186impl TlsConfig {
187 pub fn load(
199 &self,
200 ) -> Result<(PrivateKeyDer<'static>, Vec<CertificateDer<'static>>), anyhow::Error> {
201 let password = match (&self.password, &self.password_file) {
202 (None, None) => None,
203 (Some(_), Some(_)) => {
204 bail!("Only one of `password` or `password_file` can be set at a time")
205 }
206 (Some(password), None) => Some(Cow::Borrowed(password)),
207 (None, Some(path)) => Some(Cow::Owned(std::fs::read_to_string(path)?)),
208 };
209
210 let key = match (&self.key, &self.key_file) {
212 (None, None) => bail!("Either `key` or `key_file` must be set"),
213 (Some(_), Some(_)) => bail!("Only one of `key` or `key_file` can be set at a time"),
214 (Some(key), None) => {
215 if let Some(password) = password {
217 PrivateKey::load_encrypted_pem(key, password.as_bytes())?
218 } else {
219 PrivateKey::load_pem(key)?
220 }
221 }
222 (None, Some(path)) => {
223 let key = std::fs::read(path)?;
226 if let Some(password) = password {
227 PrivateKey::load_encrypted(&key, password.as_bytes())?
228 } else {
229 PrivateKey::load(&key)?
230 }
231 }
232 };
233
234 let key = key.to_pkcs8_der()?;
236 let key = PrivatePkcs8KeyDer::from(key.to_vec()).into();
237
238 let certificate_chain_pem = match (&self.certificate, &self.certificate_file) {
239 (None, None) => bail!("Either `certificate` or `certificate_file` must be set"),
240 (Some(_), Some(_)) => {
241 bail!("Only one of `certificate` or `certificate_file` can be set at a time")
242 }
243 (Some(certificate), None) => Cow::Borrowed(certificate),
244 (None, Some(path)) => Cow::Owned(std::fs::read_to_string(path)?),
245 };
246
247 let certificate_chain = CertificateDer::pem_slice_iter(certificate_chain_pem.as_bytes())
248 .collect::<Result<Vec<_>, _>>()?;
249
250 if certificate_chain.is_empty() {
251 bail!("TLS certificate chain is empty (or invalid)")
252 }
253
254 Ok((key, certificate_chain))
255 }
256}
257
258#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
260#[serde(tag = "name", rename_all = "lowercase")]
261pub enum Resource {
262 Health,
264
265 Prometheus,
267
268 Discovery,
270
271 Human,
273
274 GraphQL {
276 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
278 undocumented_oauth2_access: bool,
279 },
280
281 OAuth,
283
284 Compat,
286
287 Assets {
289 #[serde(
291 default = "http_listener_assets_path_default",
292 skip_serializing_if = "is_default_http_listener_assets_path"
293 )]
294 #[schemars(with = "String")]
295 path: Utf8PathBuf,
296 },
297
298 AdminApi,
300
301 #[serde(rename = "connection-info")]
304 ConnectionInfo,
305}
306
307#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
309pub struct ListenerConfig {
310 #[serde(skip_serializing_if = "Option::is_none")]
313 pub name: Option<String>,
314
315 pub resources: Vec<Resource>,
317
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub prefix: Option<String>,
321
322 pub binds: Vec<BindConfig>,
324
325 #[serde(default)]
327 pub proxy_protocol: bool,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub tls: Option<TlsConfig>,
332}
333
334#[derive(Debug, Serialize, Deserialize, JsonSchema)]
336pub struct HttpConfig {
337 #[serde(default)]
339 pub listeners: Vec<ListenerConfig>,
340
341 #[serde(default = "default_trusted_proxies")]
344 #[schemars(with = "Vec<String>", inner(ip))]
345 pub trusted_proxies: Vec<IpNetwork>,
346
347 pub public_base: Url,
349
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub issuer: Option<Url>,
353}
354
355impl Default for HttpConfig {
356 fn default() -> Self {
357 Self {
358 listeners: vec![
359 ListenerConfig {
360 name: Some("web".to_owned()),
361 resources: vec![
362 Resource::Discovery,
363 Resource::Human,
364 Resource::OAuth,
365 Resource::Compat,
366 Resource::GraphQL {
367 undocumented_oauth2_access: false,
368 },
369 Resource::Assets {
370 path: http_listener_assets_path_default(),
371 },
372 ],
373 prefix: None,
374 tls: None,
375 proxy_protocol: false,
376 binds: vec![BindConfig::Address {
377 address: "[::]:8080".into(),
378 }],
379 },
380 ListenerConfig {
381 name: Some("internal".to_owned()),
382 resources: vec![Resource::Health],
383 prefix: None,
384 tls: None,
385 proxy_protocol: false,
386 binds: vec![BindConfig::Listen {
387 host: Some("localhost".to_owned()),
388 port: 8081,
389 }],
390 },
391 ],
392 trusted_proxies: default_trusted_proxies(),
393 issuer: Some(default_public_base()),
394 public_base: default_public_base(),
395 }
396 }
397}
398
399impl ConfigurationSection for HttpConfig {
400 const PATH: Option<&'static str> = Some("http");
401
402 fn validate(
403 &self,
404 figment: &figment::Figment,
405 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
406 for (index, listener) in self.listeners.iter().enumerate() {
407 let annotate = |mut error: figment::Error| {
408 error.metadata = figment
409 .find_metadata(&format!("{root}.listeners", root = Self::PATH.unwrap()))
410 .cloned();
411 error.profile = Some(figment::Profile::Default);
412 error.path = vec![
413 Self::PATH.unwrap().to_owned(),
414 "listeners".to_owned(),
415 index.to_string(),
416 ];
417 error
418 };
419
420 if listener.resources.is_empty() {
421 return Err(
422 annotate(figment::Error::from("listener has no resources".to_owned())).into(),
423 );
424 }
425
426 if listener.binds.is_empty() {
427 return Err(annotate(figment::Error::from(
428 "listener does not bind to any address".to_owned(),
429 ))
430 .into());
431 }
432
433 if let Some(tls_config) = &listener.tls {
434 if tls_config.certificate.is_some() && tls_config.certificate_file.is_some() {
435 return Err(annotate(figment::Error::from(
436 "Only one of `certificate` or `certificate_file` can be set at a time"
437 .to_owned(),
438 ))
439 .into());
440 }
441
442 if tls_config.certificate.is_none() && tls_config.certificate_file.is_none() {
443 return Err(annotate(figment::Error::from(
444 "TLS configuration is missing a certificate".to_owned(),
445 ))
446 .into());
447 }
448
449 if tls_config.key.is_some() && tls_config.key_file.is_some() {
450 return Err(annotate(figment::Error::from(
451 "Only one of `key` or `key_file` can be set at a time".to_owned(),
452 ))
453 .into());
454 }
455
456 if tls_config.key.is_none() && tls_config.key_file.is_none() {
457 return Err(annotate(figment::Error::from(
458 "TLS configuration is missing a private key".to_owned(),
459 ))
460 .into());
461 }
462
463 if tls_config.password.is_some() && tls_config.password_file.is_some() {
464 return Err(annotate(figment::Error::from(
465 "Only one of `password` or `password_file` can be set at a time".to_owned(),
466 ))
467 .into());
468 }
469 }
470 }
471
472 Ok(())
473 }
474}