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