Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit 8bc35f6

Browse files
committed
Flatten the http config
Also properly remove the `spa` resource
1 parent 6d77d0e commit 8bc35f6

File tree

3 files changed

+146
-97
lines changed

3 files changed

+146
-97
lines changed

crates/cli/src/server.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ use rustls::ServerConfig;
4848
use sentry_tower::{NewSentryLayer, SentryHttpLayer};
4949
use tower::Layer;
5050
use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer};
51-
use tracing::{warn, Span};
51+
use tracing::Span;
5252
use tracing_opentelemetry::OpenTelemetrySpanExt;
5353

5454
use crate::app_state::AppState;
@@ -243,12 +243,6 @@ where
243243
format!("{connection:?}")
244244
}),
245245
),
246-
247-
#[allow(deprecated)]
248-
mas_config::HttpResource::Spa { .. } => {
249-
warn!("The SPA HTTP resource is deprecated");
250-
router
251-
}
252246
}
253247
}
254248

crates/config/src/sections/http.rs

Lines changed: 121 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,9 @@ use rand::Rng;
2525
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
2626
use schemars::JsonSchema;
2727
use serde::{Deserialize, Serialize};
28-
use serde_with::skip_serializing_none;
2928
use url::Url;
3029

31-
use super::{secrets::PasswordOrFile, ConfigurationSection};
30+
use super::ConfigurationSection;
3231

3332
fn default_public_base() -> Url {
3433
"http://[::]:8080".parse().unwrap()
@@ -99,7 +98,6 @@ impl UnixOrTcp {
9998
}
10099

101100
/// Configuration of a single listener
102-
#[skip_serializing_none]
103101
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
104102
#[serde(untagged)]
105103
pub enum BindConfig {
@@ -108,7 +106,7 @@ pub enum BindConfig {
108106
/// Host on which to listen.
109107
///
110108
/// Defaults to listening on all addresses
111-
#[serde(default)]
109+
#[serde(skip_serializing_if = "Option::is_none")]
112110
host: Option<String>,
113111

114112
/// Port on which to listen.
@@ -153,37 +151,49 @@ pub enum BindConfig {
153151
},
154152
}
155153

156-
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
157-
#[serde(rename_all = "snake_case")]
158-
pub enum KeyOrFile {
159-
Key(String),
160-
#[schemars(with = "String")]
161-
KeyFile(Utf8PathBuf),
162-
}
163-
164-
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
165-
#[serde(rename_all = "snake_case")]
166-
pub enum CertificateOrFile {
167-
Certificate(String),
168-
#[schemars(with = "String")]
169-
CertificateFile(Utf8PathBuf),
170-
}
171-
172154
/// Configuration related to TLS on a listener
173-
#[skip_serializing_none]
174155
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
175156
pub struct TlsConfig {
176157
/// PEM-encoded X509 certificate chain
177-
#[serde(flatten)]
178-
pub certificate: CertificateOrFile,
158+
///
159+
/// Exactly one of `certificate` or `certificate_file` must be set.
160+
#[serde(skip_serializing_if = "Option::is_none")]
161+
pub certificate: Option<String>,
179162

180-
/// Private key
181-
#[serde(flatten)]
182-
pub key: KeyOrFile,
163+
/// File containing the PEM-encoded X509 certificate chain
164+
///
165+
/// Exactly one of `certificate` or `certificate_file` must be set.
166+
#[serde(skip_serializing_if = "Option::is_none")]
167+
#[schemars(with = "Option<String>")]
168+
pub certificate_file: Option<Utf8PathBuf>,
169+
170+
/// PEM-encoded private key
171+
///
172+
/// Exactly one of `key` or `key_file` must be set.
173+
#[serde(skip_serializing_if = "Option::is_none")]
174+
pub key: Option<String>,
175+
176+
/// File containing a PEM or DER-encoded private key
177+
///
178+
/// Exactly one of `key` or `key_file` must be set.
179+
#[serde(skip_serializing_if = "Option::is_none")]
180+
#[schemars(with = "Option<String>")]
181+
pub key_file: Option<Utf8PathBuf>,
183182

184183
/// Password used to decode the private key
185-
#[serde(flatten)]
186-
pub password: Option<PasswordOrFile>,
184+
///
185+
/// One of `password` or `password_file` must be set if the key is
186+
/// encrypted.
187+
#[serde(skip_serializing_if = "Option::is_none")]
188+
pub password: Option<String>,
189+
190+
/// Password file used to decode the private key
191+
///
192+
/// One of `password` or `password_file` must be set if the key is
193+
/// encrypted.
194+
#[serde(skip_serializing_if = "Option::is_none")]
195+
#[schemars(with = "Option<String>")]
196+
pub password_file: Option<Utf8PathBuf>,
187197
}
188198

189199
impl TlsConfig {
@@ -201,25 +211,28 @@ impl TlsConfig {
201211
pub fn load(
202212
&self,
203213
) -> Result<(PrivateKeyDer<'static>, Vec<CertificateDer<'static>>), anyhow::Error> {
204-
let password = match &self.password {
205-
Some(PasswordOrFile::Password(password)) => Some(Cow::Borrowed(password.as_str())),
206-
Some(PasswordOrFile::PasswordFile(path)) => {
207-
Some(Cow::Owned(std::fs::read_to_string(path)?))
214+
let password = match (&self.password, &self.password_file) {
215+
(None, None) => None,
216+
(Some(_), Some(_)) => {
217+
bail!("Only one of `password` or `password_file` can be set at a time")
208218
}
209-
None => None,
219+
(Some(password), None) => Some(Cow::Borrowed(password)),
220+
(None, Some(path)) => Some(Cow::Owned(std::fs::read_to_string(path)?)),
210221
};
211222

212223
// Read the key either embedded in the config file or on disk
213-
let key = match &self.key {
214-
KeyOrFile::Key(key) => {
224+
let key = match (&self.key, &self.key_file) {
225+
(None, None) => bail!("Either `key` or `key_file` must be set"),
226+
(Some(_), Some(_)) => bail!("Only one of `key` or `key_file` can be set at a time"),
227+
(Some(key), None) => {
215228
// If the key was embedded in the config file, assume it is formatted as PEM
216229
if let Some(password) = password {
217230
PrivateKey::load_encrypted_pem(key, password.as_bytes())?
218231
} else {
219232
PrivateKey::load_pem(key)?
220233
}
221234
}
222-
KeyOrFile::KeyFile(path) => {
235+
(None, Some(path)) => {
223236
// When reading from disk, it might be either PEM or DER. `PrivateKey::load*`
224237
// will try both.
225238
let key = std::fs::read(path)?;
@@ -235,9 +248,13 @@ impl TlsConfig {
235248
let key = key.to_pkcs8_der()?;
236249
let key = PrivatePkcs8KeyDer::from(key.to_vec()).into();
237250

238-
let certificate_chain_pem = match &self.certificate {
239-
CertificateOrFile::Certificate(pem) => Cow::Borrowed(pem.as_str()),
240-
CertificateOrFile::CertificateFile(path) => Cow::Owned(std::fs::read_to_string(path)?),
251+
let certificate_chain_pem = match (&self.certificate, &self.certificate_file) {
252+
(None, None) => bail!("Either `certificate` or `certificate_file` must be set"),
253+
(Some(_), Some(_)) => {
254+
bail!("Only one of `certificate` or `certificate_file` can be set at a time")
255+
}
256+
(Some(certificate), None) => Cow::Borrowed(certificate),
257+
(None, Some(path)) => Cow::Owned(std::fs::read_to_string(path)?),
241258
};
242259

243260
let mut certificate_chain_reader = Cursor::new(certificate_chain_pem.as_bytes());
@@ -254,7 +271,6 @@ impl TlsConfig {
254271
}
255272

256273
/// HTTP resources to mount
257-
#[skip_serializing_none]
258274
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
259275
#[serde(tag = "name", rename_all = "lowercase")]
260276
pub enum Resource {
@@ -295,28 +311,21 @@ pub enum Resource {
295311
/// the upstream connection
296312
#[serde(rename = "connection-info")]
297313
ConnectionInfo,
298-
299-
/// Mount the single page app
300-
///
301-
/// This is deprecated and will be removed in a future release.
302-
#[deprecated = "This resource is deprecated and will be removed in a future release"]
303-
Spa,
304314
}
305315

306316
/// Configuration of a listener
307-
#[skip_serializing_none]
308317
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
309318
pub struct ListenerConfig {
310319
/// A unique name for this listener which will be shown in traces and in
311320
/// metrics labels
312-
#[serde(default)]
321+
#[serde(skip_serializing_if = "Option::is_none")]
313322
pub name: Option<String>,
314323

315324
/// List of resources to mount
316325
pub resources: Vec<Resource>,
317326

318327
/// HTTP prefix to mount the resources on
319-
#[serde(default)]
328+
#[serde(skip_serializing_if = "Option::is_none")]
320329
pub prefix: Option<String>,
321330

322331
/// List of sockets to bind
@@ -327,7 +336,7 @@ pub struct ListenerConfig {
327336
pub proxy_protocol: bool,
328337

329338
/// If set, makes the listener use TLS with the provided certificate and key
330-
#[serde(default)]
339+
#[serde(skip_serializing_if = "Option::is_none")]
331340
pub tls: Option<TlsConfig>,
332341
}
333342

@@ -403,6 +412,68 @@ impl ConfigurationSection for HttpConfig {
403412
Ok(Self::default())
404413
}
405414

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+
406477
fn test() -> Self {
407478
Self::default()
408479
}

docs/config.schema.json

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -836,22 +836,6 @@
836836
]
837837
}
838838
}
839-
},
840-
{
841-
"description": "Mount the single page app\n\nThis is deprecated and will be removed in a future release.",
842-
"deprecated": true,
843-
"type": "object",
844-
"required": [
845-
"name"
846-
],
847-
"properties": {
848-
"name": {
849-
"type": "string",
850-
"enum": [
851-
"spa"
852-
]
853-
}
854-
}
855839
}
856840
]
857841
},
@@ -955,32 +939,32 @@
955939
"TlsConfig": {
956940
"description": "Configuration related to TLS on a listener",
957941
"type": "object",
958-
"oneOf": [
959-
{
960-
"type": "object",
961-
"required": [
962-
"certificate"
963-
],
964-
"properties": {
965-
"certificate": {
966-
"type": "string"
967-
}
968-
},
969-
"additionalProperties": false
942+
"properties": {
943+
"certificate": {
944+
"description": "PEM-encoded X509 certificate chain\n\nExactly one of `certificate` or `certificate_file` must be set.",
945+
"type": "string"
970946
},
971-
{
972-
"type": "object",
973-
"required": [
974-
"certificate_file"
975-
],
976-
"properties": {
977-
"certificate_file": {
978-
"type": "string"
979-
}
980-
},
981-
"additionalProperties": false
947+
"certificate_file": {
948+
"description": "File containing the PEM-encoded X509 certificate chain\n\nExactly one of `certificate` or `certificate_file` must be set.",
949+
"type": "string"
950+
},
951+
"key": {
952+
"description": "PEM-encoded private key\n\nExactly one of `key` or `key_file` must be set.",
953+
"type": "string"
954+
},
955+
"key_file": {
956+
"description": "File containing a PEM or DER-encoded private key\n\nExactly one of `key` or `key_file` must be set.",
957+
"type": "string"
958+
},
959+
"password": {
960+
"description": "Password used to decode the private key\n\nOne of `password` or `password_file` must be set if the key is encrypted.",
961+
"type": "string"
962+
},
963+
"password_file": {
964+
"description": "Password file used to decode the private key\n\nOne of `password` or `password_file` must be set if the key is encrypted.",
965+
"type": "string"
982966
}
983-
]
967+
}
984968
},
985969
"IpNetwork": {
986970
"oneOf": [

0 commit comments

Comments
 (0)