use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::config::validation::{require_nonempty, require_nonzero};
use crate::errors::OrionError;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub shutdown_drain_secs: u64,
pub shutdown_force_timeout_secs: u64,
pub tls: TlsConfig,
pub compression: CompressionConfig,
pub docs: DocsConfig,
pub verbose_errors: Option<bool>,
pub max_admin_body_size: usize,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 8080,
shutdown_drain_secs: 30,
shutdown_force_timeout_secs: 30,
tls: TlsConfig::default(),
compression: CompressionConfig::default(),
docs: DocsConfig::default(),
verbose_errors: None,
max_admin_body_size: 8 * 1_048_576,
}
}
}
impl ServerConfig {
pub(crate) fn validate(&self, is_prod: bool) -> Result<(), OrionError> {
if is_prod && self.verbose_errors == Some(true) {
return Err(OrionError::Config {
message: "server.verbose_errors = true is refused in production: raw \
task errors can carry upstream URLs, connector names and \
driver detail, and the data plane is unauthenticated. Leave \
it unset (verbose outside production, sanitized in it) and \
read full messages from the trace"
.to_string(),
});
}
require_nonzero(u64::from(self.port), "server.port")?;
require_nonzero(
self.max_admin_body_size as u64,
"server.max_admin_body_size",
)?;
if self.tls.enabled {
require_nonempty(
&self.tls.cert_path,
"server.tls.cert_path (required when TLS is enabled)",
)?;
require_nonempty(
&self.tls.key_path,
"server.tls.key_path (required when TLS is enabled)",
)?;
if !Path::new(&self.tls.cert_path).exists() {
return Err(OrionError::Config {
message: format!("TLS certificate file not found: '{}'", self.tls.cert_path),
});
}
if !Path::new(&self.tls.key_path).exists() {
return Err(OrionError::Config {
message: format!("TLS private key file not found: '{}'", self.tls.key_path),
});
}
}
Ok(())
}
}
impl IngestConfig {
pub(crate) fn validate(&self) -> Result<(), OrionError> {
require_nonzero(self.max_payload_size as u64, "ingest.max_payload_size")
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct CompressionConfig {
pub enabled: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct DocsConfig {
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct TlsConfig {
pub enabled: bool,
pub cert_path: String,
pub key_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct IngestConfig {
pub max_payload_size: usize,
}
impl Default for IngestConfig {
fn default() -> Self {
Self {
max_payload_size: 1_048_576, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verbose_errors_unset_is_allowed_in_production() {
let config = ServerConfig::default();
assert_eq!(config.verbose_errors, None);
assert!(config.validate(true).is_ok());
}
#[test]
fn verbose_errors_false_is_allowed_in_production() {
let config = ServerConfig {
verbose_errors: Some(false),
..ServerConfig::default()
};
assert!(config.validate(true).is_ok());
}
#[test]
fn verbose_errors_true_is_allowed_outside_production() {
let config = ServerConfig {
verbose_errors: Some(true),
..ServerConfig::default()
};
assert!(config.validate(false).is_ok());
}
#[test]
fn verbose_errors_true_is_refused_in_production() {
let config = ServerConfig {
verbose_errors: Some(true),
..ServerConfig::default()
};
let err = config
.validate(true)
.expect_err("verbose errors in production must not start");
let message = err.to_string();
assert!(
message.contains("server.verbose_errors"),
"the error must name the setting to change: {message}"
);
}
}