mas_config/sections/
http.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7#![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/// Kind of socket
70#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy)]
71#[serde(rename_all = "lowercase")]
72pub enum UnixOrTcp {
73    /// UNIX domain socket
74    Unix,
75
76    /// TCP socket
77    Tcp,
78}
79
80impl UnixOrTcp {
81    /// UNIX domain socket
82    #[must_use]
83    pub const fn unix() -> Self {
84        Self::Unix
85    }
86
87    /// TCP socket
88    #[must_use]
89    pub const fn tcp() -> Self {
90        Self::Tcp
91    }
92}
93
94/// Configuration of a single listener
95#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
96#[serde(untagged)]
97pub enum BindConfig {
98    /// Listen on the specified host and port
99    Listen {
100        /// Host on which to listen.
101        ///
102        /// Defaults to listening on all addresses
103        #[serde(skip_serializing_if = "Option::is_none")]
104        host: Option<String>,
105
106        /// Port on which to listen.
107        port: u16,
108    },
109
110    /// Listen on the specified address
111    Address {
112        /// Host and port on which to listen
113        #[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    /// Listen on a UNIX domain socket
123    Unix {
124        /// Path to the socket
125        #[schemars(with = "String")]
126        socket: Utf8PathBuf,
127    },
128
129    /// Accept connections on file descriptors passed by the parent process.
130    ///
131    /// This is useful for grabbing sockets passed by systemd.
132    ///
133    /// See <https://www.freedesktop.org/software/systemd/man/sd_listen_fds.html>
134    FileDescriptor {
135        /// Index of the file descriptor. Note that this is offseted by 3
136        /// because of the standard input/output sockets, so setting
137        /// here a value of `0` will grab the file descriptor `3`
138        #[serde(default)]
139        fd: usize,
140
141        /// Whether the socket is a TCP socket or a UNIX domain socket. Defaults
142        /// to TCP.
143        #[serde(default = "UnixOrTcp::tcp")]
144        kind: UnixOrTcp,
145    },
146}
147
148/// Configuration related to TLS on a listener
149#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
150pub struct TlsConfig {
151    /// PEM-encoded X509 certificate chain
152    ///
153    /// Exactly one of `certificate` or `certificate_file` must be set.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub certificate: Option<String>,
156
157    /// File containing the PEM-encoded X509 certificate chain
158    ///
159    /// Exactly one of `certificate` or `certificate_file` must be set.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    #[schemars(with = "Option<String>")]
162    pub certificate_file: Option<Utf8PathBuf>,
163
164    /// PEM-encoded private key
165    ///
166    /// Exactly one of `key` or `key_file` must be set.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub key: Option<String>,
169
170    /// File containing a PEM or DER-encoded private key
171    ///
172    /// Exactly one of `key` or `key_file` must be set.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    #[schemars(with = "Option<String>")]
175    pub key_file: Option<Utf8PathBuf>,
176
177    /// Password used to decode the private key
178    ///
179    /// One of `password` or `password_file` must be set if the key is
180    /// encrypted.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub password: Option<String>,
183
184    /// Password file used to decode the private key
185    ///
186    /// One of `password` or `password_file` must be set if the key is
187    /// encrypted.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    #[schemars(with = "Option<String>")]
190    pub password_file: Option<Utf8PathBuf>,
191}
192
193impl TlsConfig {
194    /// Load the TLS certificate chain and key file from disk
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if an error was encountered either while:
199    ///   - reading the certificate, key or password files
200    ///   - decoding the key as PEM or DER
201    ///   - decrypting the key if encrypted
202    ///   - a password was provided but the key was not encrypted
203    ///   - decoding the certificate chain as PEM
204    ///   - the certificate chain is empty
205    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        // Read the key either embedded in the config file or on disk
218        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 the key was embedded in the config file, assume it is formatted as PEM
223                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                // When reading from disk, it might be either PEM or DER. `PrivateKey::load*`
231                // will try both.
232                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        // Re-serialize the key to PKCS#8 DER, so rustls can consume it
242        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/// HTTP resources to mount
268#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
269#[serde(tag = "name", rename_all = "lowercase")]
270pub enum Resource {
271    /// Healthcheck endpoint (/health)
272    Health,
273
274    /// Prometheus metrics endpoint (/metrics)
275    Prometheus,
276
277    /// OIDC discovery endpoints
278    Discovery,
279
280    /// Pages destined to be viewed by humans
281    Human,
282
283    /// GraphQL endpoint
284    GraphQL {
285        /// Enabled the GraphQL playground
286        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
287        playground: bool,
288
289        /// Allow access for OAuth 2.0 clients (undocumented)
290        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
291        undocumented_oauth2_access: bool,
292    },
293
294    /// OAuth-related APIs
295    OAuth,
296
297    /// Matrix compatibility API
298    Compat,
299
300    /// Static files
301    Assets {
302        /// Path to the directory to serve.
303        #[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    /// Admin API, served at `/api/admin/v1`
312    AdminApi,
313
314    /// Mount a "/connection-info" handler which helps debugging informations on
315    /// the upstream connection
316    #[serde(rename = "connection-info")]
317    ConnectionInfo,
318}
319
320/// Configuration of a listener
321#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
322pub struct ListenerConfig {
323    /// A unique name for this listener which will be shown in traces and in
324    /// metrics labels
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub name: Option<String>,
327
328    /// List of resources to mount
329    pub resources: Vec<Resource>,
330
331    /// HTTP prefix to mount the resources on
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub prefix: Option<String>,
334
335    /// List of sockets to bind
336    pub binds: Vec<BindConfig>,
337
338    /// Accept `HAProxy`'s Proxy Protocol V1
339    #[serde(default)]
340    pub proxy_protocol: bool,
341
342    /// If set, makes the listener use TLS with the provided certificate and key
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub tls: Option<TlsConfig>,
345}
346
347/// Configuration related to the web server
348#[derive(Debug, Serialize, Deserialize, JsonSchema)]
349pub struct HttpConfig {
350    /// List of listeners to run
351    #[serde(default)]
352    pub listeners: Vec<ListenerConfig>,
353
354    /// List of trusted reverse proxies that can set the `X-Forwarded-For`
355    /// header
356    #[serde(default = "default_trusted_proxies")]
357    pub trusted_proxies: Vec<IpNetwork>,
358
359    /// Public URL base from where the authentication service is reachable
360    pub public_base: Url,
361
362    /// OIDC issuer URL. Defaults to `public_base` if not set.
363    #[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}