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