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
116 FileDescriptor {
122 #[serde(default)]
126 fd: usize,
127
128 #[serde(default = "UnixOrTcp::tcp")]
131 kind: UnixOrTcp,
132 },
133}
134
135#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
137pub struct TlsConfig {
138 #[serde(skip_serializing_if = "Option::is_none")]
142 pub certificate: Option<String>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
148 #[schemars(with = "Option<String>")]
149 pub certificate_file: Option<Utf8PathBuf>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
155 pub key: Option<String>,
156
157 #[serde(skip_serializing_if = "Option::is_none")]
161 #[schemars(with = "Option<String>")]
162 pub key_file: Option<Utf8PathBuf>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
169 pub password: Option<String>,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
176 #[schemars(with = "Option<String>")]
177 pub password_file: Option<Utf8PathBuf>,
178}
179
180impl TlsConfig {
181 pub fn load(
193 &self,
194 ) -> Result<(PrivateKeyDer<'static>, Vec<CertificateDer<'static>>), anyhow::Error> {
195 let password = match (&self.password, &self.password_file) {
196 (None, None) => None,
197 (Some(_), Some(_)) => {
198 bail!("Only one of `password` or `password_file` can be set at a time")
199 }
200 (Some(password), None) => Some(Cow::Borrowed(password)),
201 (None, Some(path)) => Some(Cow::Owned(std::fs::read_to_string(path)?)),
202 };
203
204 let key = match (&self.key, &self.key_file) {
206 (None, None) => bail!("Either `key` or `key_file` must be set"),
207 (Some(_), Some(_)) => bail!("Only one of `key` or `key_file` can be set at a time"),
208 (Some(key), None) => {
209 if let Some(password) = password {
211 PrivateKey::load_encrypted_pem(key, password.as_bytes())?
212 } else {
213 PrivateKey::load_pem(key)?
214 }
215 }
216 (None, Some(path)) => {
217 let key = std::fs::read(path)?;
220 if let Some(password) = password {
221 PrivateKey::load_encrypted(&key, password.as_bytes())?
222 } else {
223 PrivateKey::load(&key)?
224 }
225 }
226 };
227
228 let key = key.to_pkcs8_der()?;
230 let key = PrivatePkcs8KeyDer::from(key.to_vec()).into();
231
232 let certificate_chain_pem = match (&self.certificate, &self.certificate_file) {
233 (None, None) => bail!("Either `certificate` or `certificate_file` must be set"),
234 (Some(_), Some(_)) => {
235 bail!("Only one of `certificate` or `certificate_file` can be set at a time")
236 }
237 (Some(certificate), None) => Cow::Borrowed(certificate),
238 (None, Some(path)) => Cow::Owned(std::fs::read_to_string(path)?),
239 };
240
241 let certificate_chain = CertificateDer::pem_slice_iter(certificate_chain_pem.as_bytes())
242 .collect::<Result<Vec<_>, _>>()?;
243
244 if certificate_chain.is_empty() {
245 bail!("TLS certificate chain is empty (or invalid)")
246 }
247
248 Ok((key, certificate_chain))
249 }
250}
251
252#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
254#[serde(tag = "name", rename_all = "lowercase")]
255pub enum Resource {
256 Health,
258
259 Prometheus,
261
262 Discovery,
264
265 Human,
267
268 GraphQL {
270 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
272 playground: bool,
273
274 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
276 undocumented_oauth2_access: bool,
277 },
278
279 OAuth,
281
282 Compat,
284
285 Assets {
287 #[serde(
289 default = "http_listener_assets_path_default",
290 skip_serializing_if = "is_default_http_listener_assets_path"
291 )]
292 #[schemars(with = "String")]
293 path: Utf8PathBuf,
294 },
295
296 AdminApi,
298
299 #[serde(rename = "connection-info")]
302 ConnectionInfo,
303}
304
305#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
307pub struct ListenerConfig {
308 #[serde(skip_serializing_if = "Option::is_none")]
311 pub name: Option<String>,
312
313 pub resources: Vec<Resource>,
315
316 #[serde(skip_serializing_if = "Option::is_none")]
318 pub prefix: Option<String>,
319
320 pub binds: Vec<BindConfig>,
322
323 #[serde(default)]
325 pub proxy_protocol: bool,
326
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub tls: Option<TlsConfig>,
330}
331
332#[derive(Debug, Serialize, Deserialize, JsonSchema)]
334pub struct HttpConfig {
335 #[serde(default)]
337 pub listeners: Vec<ListenerConfig>,
338
339 #[serde(default = "default_trusted_proxies")]
342 #[schemars(with = "Vec<String>", inner(ip))]
343 pub trusted_proxies: Vec<IpNetwork>,
344
345 pub public_base: Url,
347
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub issuer: Option<Url>,
351}
352
353impl Default for HttpConfig {
354 fn default() -> Self {
355 Self {
356 listeners: vec![
357 ListenerConfig {
358 name: Some("web".to_owned()),
359 resources: vec![
360 Resource::Discovery,
361 Resource::Human,
362 Resource::OAuth,
363 Resource::Compat,
364 Resource::GraphQL {
365 playground: false,
366 undocumented_oauth2_access: false,
367 },
368 Resource::Assets {
369 path: http_listener_assets_path_default(),
370 },
371 ],
372 prefix: None,
373 tls: None,
374 proxy_protocol: false,
375 binds: vec![BindConfig::Address {
376 address: "[::]:8080".into(),
377 }],
378 },
379 ListenerConfig {
380 name: Some("internal".to_owned()),
381 resources: vec![Resource::Health],
382 prefix: None,
383 tls: None,
384 proxy_protocol: false,
385 binds: vec![BindConfig::Listen {
386 host: Some("localhost".to_owned()),
387 port: 8081,
388 }],
389 },
390 ],
391 trusted_proxies: default_trusted_proxies(),
392 issuer: Some(default_public_base()),
393 public_base: default_public_base(),
394 }
395 }
396}
397
398impl ConfigurationSection for HttpConfig {
399 const PATH: Option<&'static str> = Some("http");
400
401 fn validate(
402 &self,
403 figment: &figment::Figment,
404 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
405 for (index, listener) in self.listeners.iter().enumerate() {
406 let annotate = |mut error: figment::Error| {
407 error.metadata = figment
408 .find_metadata(&format!("{root}.listeners", root = Self::PATH.unwrap()))
409 .cloned();
410 error.profile = Some(figment::Profile::Default);
411 error.path = vec![
412 Self::PATH.unwrap().to_owned(),
413 "listeners".to_owned(),
414 index.to_string(),
415 ];
416 error
417 };
418
419 if listener.resources.is_empty() {
420 return Err(
421 annotate(figment::Error::from("listener has no resources".to_owned())).into(),
422 );
423 }
424
425 if listener.binds.is_empty() {
426 return Err(annotate(figment::Error::from(
427 "listener does not bind to any address".to_owned(),
428 ))
429 .into());
430 }
431
432 if let Some(tls_config) = &listener.tls {
433 if tls_config.certificate.is_some() && tls_config.certificate_file.is_some() {
434 return Err(annotate(figment::Error::from(
435 "Only one of `certificate` or `certificate_file` can be set at a time"
436 .to_owned(),
437 ))
438 .into());
439 }
440
441 if tls_config.certificate.is_none() && tls_config.certificate_file.is_none() {
442 return Err(annotate(figment::Error::from(
443 "TLS configuration is missing a certificate".to_owned(),
444 ))
445 .into());
446 }
447
448 if tls_config.key.is_some() && tls_config.key_file.is_some() {
449 return Err(annotate(figment::Error::from(
450 "Only one of `key` or `key_file` can be set at a time".to_owned(),
451 ))
452 .into());
453 }
454
455 if tls_config.key.is_none() && tls_config.key_file.is_none() {
456 return Err(annotate(figment::Error::from(
457 "TLS configuration is missing a private key".to_owned(),
458 ))
459 .into());
460 }
461
462 if tls_config.password.is_some() && tls_config.password_file.is_some() {
463 return Err(annotate(figment::Error::from(
464 "Only one of `password` or `password_file` can be set at a time".to_owned(),
465 ))
466 .into());
467 }
468 }
469 }
470
471 Ok(())
472 }
473}