v0.24.5
This commit is contained in:
commit
5e23e9af02
240 files changed
+99957
No files matched your search
+658
@@ -0,0 +1,658 @@
|
||||
//! Persisted listener intent and response controls. Listener changes require restart.
|
||||
use anyhow::{ensure, Result};
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
/// Listener addresses explicitly supplied by the service manager/CLI. These
|
||||
/// are process-level contracts and cannot be removed or rewritten through the
|
||||
/// persisted control plane while the process is started with those values.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct StartupLocks {
|
||||
pub http: Option<SocketAddr>,
|
||||
pub https: Option<SocketAddr>,
|
||||
pub administration: Option<SocketAddr>,
|
||||
}
|
||||
impl StartupLocks {
|
||||
pub fn ensure_config(&self, config: &mut crate::config::Config) {
|
||||
if self.administration.is_some() {
|
||||
// Explicit startup administration bind remains authoritative. `None`
|
||||
// means "use startup value" and also preserves the plain-HTTP safety
|
||||
// gate handled by main.
|
||||
config.administration.listen = None;
|
||||
}
|
||||
if let Some(address) = self.http {
|
||||
ensure_locked_listener(config, address, false);
|
||||
}
|
||||
if let Some(address) = self.https {
|
||||
ensure_locked_listener(config, address, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate(
|
||||
&self,
|
||||
config: &crate::config::Config,
|
||||
startup_admin: SocketAddr,
|
||||
) -> Result<()> {
|
||||
if let Some(required) = self.http {
|
||||
ensure!(
|
||||
config.listeners.endpoints.iter().any(|endpoint| {
|
||||
endpoint.enabled
|
||||
&& endpoint.bindings().iter().any(|binding| binding.address == required && !binding.tls)
|
||||
}),
|
||||
"HTTP listener {required} is fixed by systemd/startup and cannot be removed, disabled, or changed"
|
||||
);
|
||||
}
|
||||
if let Some(required) = self.https {
|
||||
ensure!(
|
||||
config.listeners.endpoints.iter().any(|endpoint| {
|
||||
endpoint.enabled
|
||||
&& endpoint.bindings().iter().any(|binding| binding.address == required && binding.tls)
|
||||
}),
|
||||
"HTTPS listener {required} is fixed by systemd/startup and cannot be removed, disabled, or changed"
|
||||
);
|
||||
}
|
||||
if let Some(required) = self.administration {
|
||||
ensure!(
|
||||
config.administration.listen.is_none()
|
||||
&& config.administration.resolved_listen(startup_admin) == required,
|
||||
"administration listener {required} is fixed by systemd/startup and cannot be changed"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_locked_listener(config: &mut crate::config::Config, address: SocketAddr, tls: bool) {
|
||||
if let Some(endpoint) = config.listeners.endpoints.iter_mut().find(|endpoint| {
|
||||
endpoint
|
||||
.bindings()
|
||||
.iter()
|
||||
.any(|binding| binding.address == address && binding.tls == tls)
|
||||
}) {
|
||||
endpoint.enabled = true;
|
||||
return;
|
||||
}
|
||||
let base = if tls { "systemd-https" } else { "systemd-http" };
|
||||
let mut id = base.to_owned();
|
||||
let mut suffix = 2usize;
|
||||
while config
|
||||
.listeners
|
||||
.endpoints
|
||||
.iter()
|
||||
.any(|endpoint| endpoint.id == id)
|
||||
{
|
||||
id = format!("{base}-{suffix}");
|
||||
suffix += 1;
|
||||
}
|
||||
config.listeners.endpoints.push(NamedListener {
|
||||
id,
|
||||
name: if tls {
|
||||
"HTTPS (startup locked)".into()
|
||||
} else {
|
||||
"HTTP (startup locked)".into()
|
||||
},
|
||||
scope: if address.ip().is_loopback() {
|
||||
ListenerScope::Internal
|
||||
} else {
|
||||
ListenerScope::Public
|
||||
},
|
||||
enabled: true,
|
||||
binding: crate::vhost::Listener {
|
||||
address,
|
||||
tls,
|
||||
http2: tls,
|
||||
http2_settings: tls.then(Http2Connections::default),
|
||||
h2c: false,
|
||||
http3: tls.then(crate::http3::Settings::default),
|
||||
},
|
||||
additional_addresses: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Http2Connections {
|
||||
/// Close an otherwise unused HTTP/2 connection after this inactivity period.
|
||||
pub idle_timeout_seconds: u64,
|
||||
/// Absolute connection age limit. Zero disables the lifetime limit.
|
||||
pub max_connection_lifetime_seconds: u64,
|
||||
/// Maximum time after GOAWAY for active streams to finish before TCP is closed.
|
||||
pub drain_timeout_seconds: u64,
|
||||
}
|
||||
impl Default for Http2Connections {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
idle_timeout_seconds: 300,
|
||||
max_connection_lifetime_seconds: 0,
|
||||
drain_timeout_seconds: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Http2Connections {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
ensure!(
|
||||
(1..=86_400).contains(&self.idle_timeout_seconds),
|
||||
"HTTP/2 idle_timeout_seconds must be 1..86400"
|
||||
);
|
||||
ensure!(
|
||||
self.max_connection_lifetime_seconds == 0
|
||||
|| (1..=2_592_000).contains(&self.max_connection_lifetime_seconds),
|
||||
"HTTP/2 max_connection_lifetime_seconds must be 0..2592000 (0 disables the limit)"
|
||||
);
|
||||
ensure!(
|
||||
(1..=3_600).contains(&self.drain_timeout_seconds),
|
||||
"HTTP/2 drain_timeout_seconds must be 1..3600"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
pub fn idle_timeout(self) -> Duration {
|
||||
Duration::from_secs(self.idle_timeout_seconds)
|
||||
}
|
||||
pub fn max_connection_lifetime(self) -> Option<Duration> {
|
||||
(self.max_connection_lifetime_seconds > 0)
|
||||
.then(|| Duration::from_secs(self.max_connection_lifetime_seconds))
|
||||
}
|
||||
pub fn drain_timeout(self) -> Duration {
|
||||
Duration::from_secs(self.drain_timeout_seconds)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Administration {
|
||||
/// None keeps the startup/CLI administration address for backward compatibility.
|
||||
/// A persisted address is applied on the next process start.
|
||||
pub listen: Option<SocketAddr>,
|
||||
pub tls: bool,
|
||||
/// None uses the application-managed default certificate pair.
|
||||
pub certificate_id: Option<String>,
|
||||
/// Optional strict DNS Host/Origin allowlist. Empty HTTPS lists accept valid DNS names.
|
||||
pub allowed_hosts: Vec<String>,
|
||||
/// Absolute browser-session lifetime. Existing sessions are bounded by the current value.
|
||||
pub session_days: u16,
|
||||
}
|
||||
impl Default for Administration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
listen: None,
|
||||
tls: false,
|
||||
certificate_id: None,
|
||||
allowed_hosts: Vec::new(),
|
||||
session_days: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Administration {
|
||||
pub fn resolved_listen(&self, startup: SocketAddr) -> SocketAddr {
|
||||
self.listen.unwrap_or(startup)
|
||||
}
|
||||
pub fn session_seconds(&self) -> i64 {
|
||||
i64::from(self.session_days) * 24 * 60 * 60
|
||||
}
|
||||
pub fn restart_differs(&self, active: &Self) -> bool {
|
||||
self.listen != active.listen
|
||||
|| self.tls != active.tls
|
||||
|| self.certificate_id != active.certificate_id
|
||||
|| self.allowed_hosts != active.allowed_hosts
|
||||
}
|
||||
pub fn validate(&mut self, tls: &crate::tls::Settings) -> Result<()> {
|
||||
if let Some(addr) = self.listen {
|
||||
ensure!(
|
||||
addr.port() > 0,
|
||||
"administration listener port must be nonzero"
|
||||
);
|
||||
ensure!(
|
||||
!addr.ip().is_multicast(),
|
||||
"administration listener must use a unicast/wildcard IP"
|
||||
);
|
||||
}
|
||||
if let Some(id) = &self.certificate_id {
|
||||
ensure!(
|
||||
self.tls,
|
||||
"administration certificate selection requires administration TLS"
|
||||
);
|
||||
ensure!(
|
||||
tls.certificates
|
||||
.iter()
|
||||
.any(|certificate| &certificate.id == id),
|
||||
"administration certificate references an unknown certificate id"
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
(7..=30).contains(&self.session_days),
|
||||
"administration session_days must be 7..30"
|
||||
);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for host in &mut self.allowed_hosts {
|
||||
if host.parse::<std::net::IpAddr>().is_err() {
|
||||
*host = crate::config::normalize_host(host)?;
|
||||
}
|
||||
ensure!(
|
||||
seen.insert(host.clone()),
|
||||
"duplicate administration hostname"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Listeners {
|
||||
/// All proxy sockets are configured here and referenced by vhosts by ID.
|
||||
pub endpoints: Vec<NamedListener>,
|
||||
}
|
||||
impl Listeners {
|
||||
pub fn from_startup(http: SocketAddr, https: Option<SocketAddr>) -> Self {
|
||||
let mut endpoints = vec![NamedListener {
|
||||
id: "http".into(),
|
||||
name: "HTTP".into(),
|
||||
scope: ListenerScope::Internal,
|
||||
enabled: true,
|
||||
binding: crate::vhost::Listener {
|
||||
address: http,
|
||||
tls: false,
|
||||
http2: false,
|
||||
http2_settings: None,
|
||||
h2c: false,
|
||||
http3: None,
|
||||
},
|
||||
additional_addresses: Vec::new(),
|
||||
}];
|
||||
if let Some(address) = https {
|
||||
endpoints.push(NamedListener {
|
||||
id: "https".into(),
|
||||
name: "HTTPS".into(),
|
||||
scope: ListenerScope::Internal,
|
||||
enabled: true,
|
||||
binding: crate::vhost::Listener {
|
||||
address,
|
||||
tls: true,
|
||||
http2: true,
|
||||
http2_settings: Some(Http2Connections::default()),
|
||||
h2c: false,
|
||||
http3: Some(crate::http3::Settings::default()),
|
||||
},
|
||||
additional_addresses: Vec::new(),
|
||||
});
|
||||
}
|
||||
Self { endpoints }
|
||||
}
|
||||
|
||||
pub fn validate(&self, policy: &crate::config::ValidationPolicy) -> Result<()> {
|
||||
ensure!(self.endpoints.len() <= 64, "at most 64 named listeners");
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
for endpoint in &self.endpoints {
|
||||
ensure!(
|
||||
crate::config::valid_id(&endpoint.id) && ids.insert(&endpoint.id),
|
||||
"invalid/duplicate listener id"
|
||||
);
|
||||
ensure!(
|
||||
endpoint.name.len() <= 96 && !endpoint.name.chars().any(char::is_control),
|
||||
"listener name must be at most 96 bytes without controls"
|
||||
);
|
||||
ensure!(
|
||||
endpoint.additional_addresses.len() <= 15,
|
||||
"listener allows at most 16 bind addresses"
|
||||
);
|
||||
let bindings = endpoint.bindings();
|
||||
let mut addresses = std::collections::HashSet::new();
|
||||
for binding in &bindings {
|
||||
crate::vhost::validate_listener(*binding)?;
|
||||
ensure!(
|
||||
binding.address.port() != policy.admin_addr.port(),
|
||||
"listener cannot share administration port"
|
||||
);
|
||||
ensure!(
|
||||
addresses.insert(binding.address),
|
||||
"duplicate address in named listener"
|
||||
);
|
||||
}
|
||||
if bindings.len() > 1 {
|
||||
ensure!(
|
||||
bindings.iter().all(|binding| !binding.address.ip().is_unspecified()),
|
||||
"multi-address listeners require explicit IPs; do not combine 0.0.0.0/:: with selected addresses"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Labels describe operator intent, never firewall or access-control enforcement.
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ListenerScope {
|
||||
#[default]
|
||||
Internal,
|
||||
Public,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NamedListener {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub scope: ListenerScope,
|
||||
#[serde(default = "enabled")]
|
||||
pub enabled: bool,
|
||||
pub binding: crate::vhost::Listener,
|
||||
/// Extra explicit local addresses that share the same transport/protocol policy.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub additional_addresses: Vec<SocketAddr>,
|
||||
}
|
||||
impl NamedListener {
|
||||
pub fn bindings(&self) -> Vec<crate::vhost::Listener> {
|
||||
let mut out = Vec::with_capacity(1 + self.additional_addresses.len());
|
||||
out.push(self.binding);
|
||||
out.extend(self.additional_addresses.iter().map(|address| {
|
||||
let mut binding = self.binding;
|
||||
binding.address = *address;
|
||||
binding
|
||||
}));
|
||||
out
|
||||
}
|
||||
}
|
||||
fn enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct DiagnosticHeaders {
|
||||
pub request_id: bool,
|
||||
}
|
||||
impl DiagnosticHeaders {
|
||||
pub fn apply(&self, headers: &mut HeaderMap, id: u64) {
|
||||
headers.remove("x-request-id");
|
||||
if self.request_id {
|
||||
if let Ok(value) = id.to_string().parse() {
|
||||
headers.insert("x-request-id", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ServerMode {
|
||||
#[default]
|
||||
Preserve,
|
||||
Hide,
|
||||
Custom,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct ServerHeader {
|
||||
pub mode: ServerMode,
|
||||
pub value: String,
|
||||
}
|
||||
impl Default for ServerHeader {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: ServerMode::Preserve,
|
||||
value: format!("RustProxy/v{}", env!("CARGO_PKG_VERSION")),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ServerHeader {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
ensure!(
|
||||
self.value.len() <= 256,
|
||||
"Server header value exceeds 256 bytes"
|
||||
);
|
||||
if matches!(self.mode, ServerMode::Custom) {
|
||||
ensure!(
|
||||
!self.value.trim().is_empty(),
|
||||
"custom Server header cannot be empty"
|
||||
);
|
||||
self.value.parse::<HeaderValue>()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn apply(&self, headers: &mut HeaderMap) {
|
||||
match self.mode {
|
||||
ServerMode::Preserve => {}
|
||||
ServerMode::Hide => {
|
||||
headers.remove("server");
|
||||
}
|
||||
ServerMode::Custom => {
|
||||
if let Ok(value) = self.value.parse() {
|
||||
headers.insert("server", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn http2_connection_limits_default_and_validate_independently() {
|
||||
let defaults: Http2Connections = serde_json::from_str("{}").unwrap();
|
||||
assert_eq!(defaults.idle_timeout_seconds, 300);
|
||||
assert_eq!(defaults.max_connection_lifetime_seconds, 0);
|
||||
assert_eq!(defaults.drain_timeout_seconds, 10);
|
||||
defaults.validate().unwrap();
|
||||
|
||||
let configured = Http2Connections {
|
||||
idle_timeout_seconds: 45,
|
||||
max_connection_lifetime_seconds: 3_600,
|
||||
drain_timeout_seconds: 20,
|
||||
};
|
||||
configured.validate().unwrap();
|
||||
assert_eq!(configured.idle_timeout(), Duration::from_secs(45));
|
||||
assert_eq!(configured.max_connection_lifetime(), Some(Duration::from_secs(3_600)));
|
||||
assert_eq!(configured.drain_timeout(), Duration::from_secs(20));
|
||||
assert!(Http2Connections { idle_timeout_seconds: 0, ..defaults }.validate().is_err());
|
||||
assert!(Http2Connections { max_connection_lifetime_seconds: 2_592_001, ..defaults }.validate().is_err());
|
||||
assert!(Http2Connections { drain_timeout_seconds: 0, ..defaults }.validate().is_err());
|
||||
}
|
||||
#[test]
|
||||
fn server_policy_and_injection() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("server", "origin".parse().unwrap());
|
||||
ServerHeader {
|
||||
mode: ServerMode::Hide,
|
||||
..Default::default()
|
||||
}
|
||||
.apply(&mut headers);
|
||||
assert!(!headers.contains_key("server"));
|
||||
let custom = ServerHeader {
|
||||
mode: ServerMode::Custom,
|
||||
value: "RustProxy/v0.1.0".into(),
|
||||
};
|
||||
custom.validate().unwrap();
|
||||
custom.apply(&mut headers);
|
||||
assert_eq!(headers["server"], "RustProxy/v0.1.0");
|
||||
assert!(ServerHeader {
|
||||
mode: ServerMode::Custom,
|
||||
value: "bad\r\nx: y".into()
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn explicit_startup_binds_are_injected_and_cannot_be_removed() {
|
||||
let locks = StartupLocks {
|
||||
http: Some("127.0.0.1:18080".parse().unwrap()),
|
||||
https: Some("127.0.0.1:18443".parse().unwrap()),
|
||||
administration: Some("127.0.0.1:19090".parse().unwrap()),
|
||||
};
|
||||
let mut config = crate::config::Config::default();
|
||||
config.administration.listen = Some("127.0.0.1:19999".parse().unwrap());
|
||||
locks.ensure_config(&mut config);
|
||||
assert!(config.administration.listen.is_none());
|
||||
locks
|
||||
.validate(&config, "127.0.0.1:19090".parse().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let http = config
|
||||
.listeners
|
||||
.endpoints
|
||||
.iter_mut()
|
||||
.find(|endpoint| {
|
||||
endpoint.bindings().iter().any(|binding| {
|
||||
binding.address == "127.0.0.1:18080".parse().unwrap() && !binding.tls
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
http.enabled = false;
|
||||
locks.ensure_config(&mut config);
|
||||
assert!(config.listeners.endpoints.iter().any(|endpoint| {
|
||||
endpoint.enabled
|
||||
&& endpoint.bindings().iter().any(|binding| {
|
||||
binding.address == "127.0.0.1:18080".parse().unwrap() && !binding.tls
|
||||
})
|
||||
}));
|
||||
|
||||
config.listeners.endpoints.retain(|endpoint| {
|
||||
!endpoint.bindings().iter().any(|binding| {
|
||||
binding.address == "127.0.0.1:18080".parse().unwrap() && !binding.tls
|
||||
})
|
||||
});
|
||||
assert!(locks
|
||||
.validate(&config, "127.0.0.1:19090".parse().unwrap())
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_defaults_become_named_listeners() {
|
||||
let saved = Listeners::from_startup(
|
||||
"0.0.0.0:80".parse().unwrap(),
|
||||
Some("0.0.0.0:443".parse().unwrap()),
|
||||
);
|
||||
assert_eq!(saved.endpoints.len(), 2);
|
||||
assert_eq!(saved.endpoints[0].id, "http");
|
||||
assert_eq!(saved.endpoints[0].binding.address.port(), 80);
|
||||
assert_eq!(saved.endpoints[1].id, "https");
|
||||
assert_eq!(saved.endpoints[1].binding.address.port(), 443);
|
||||
assert_eq!(
|
||||
saved.endpoints[1].binding.http2_settings,
|
||||
Some(Http2Connections::default())
|
||||
);
|
||||
assert!(saved.endpoints[1].binding.http3.is_some());
|
||||
}
|
||||
#[test]
|
||||
fn named_listener_can_bind_multiple_explicit_addresses() {
|
||||
let endpoint = NamedListener {
|
||||
id: "web".into(),
|
||||
name: "Web".into(),
|
||||
scope: ListenerScope::Public,
|
||||
enabled: true,
|
||||
binding: crate::vhost::Listener {
|
||||
address: "192.0.2.10:443".parse().unwrap(),
|
||||
tls: true,
|
||||
http2: true,
|
||||
http2_settings: Some(Http2Connections::default()),
|
||||
h2c: false,
|
||||
http3: None,
|
||||
},
|
||||
additional_addresses: vec!["192.0.2.11:443".parse().unwrap()],
|
||||
};
|
||||
let bindings = endpoint.bindings();
|
||||
assert_eq!(bindings.len(), 2);
|
||||
assert_eq!(bindings[0].address, "192.0.2.10:443".parse().unwrap());
|
||||
assert_eq!(bindings[1].address, "192.0.2.11:443".parse().unwrap());
|
||||
assert_eq!(bindings[0].tls, bindings[1].tls);
|
||||
assert_eq!(bindings[0].http2, bindings[1].http2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_address_listener_rejects_unspecified_or_duplicate_addresses() {
|
||||
let policy = crate::config::ValidationPolicy {
|
||||
tls_addr: None,
|
||||
cert_dir: None,
|
||||
data_dir: None,
|
||||
proxy_addr: "127.0.0.1:18080".parse().unwrap(),
|
||||
admin_addr: "127.0.0.1:19090".parse().unwrap(),
|
||||
};
|
||||
let mut listeners = Listeners {
|
||||
endpoints: vec![NamedListener {
|
||||
id: "web".into(),
|
||||
name: "Web".into(),
|
||||
scope: ListenerScope::Public,
|
||||
enabled: true,
|
||||
binding: crate::vhost::Listener {
|
||||
address: "0.0.0.0:8080".parse().unwrap(),
|
||||
tls: false,
|
||||
http2: false,
|
||||
http2_settings: None,
|
||||
h2c: false,
|
||||
http3: None,
|
||||
},
|
||||
additional_addresses: vec!["192.0.2.10:8080".parse().unwrap()],
|
||||
}],
|
||||
};
|
||||
assert!(listeners.validate(&policy).is_err());
|
||||
listeners.endpoints[0].binding.address = "192.0.2.10:8080".parse().unwrap();
|
||||
assert!(listeners.validate(&policy).is_err());
|
||||
listeners.endpoints[0].additional_addresses = vec!["192.0.2.11:8080".parse().unwrap()];
|
||||
assert!(listeners.validate(&policy).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn administration_certificate_requires_tls_and_known_inventory_id() {
|
||||
let inventory = crate::tls::Settings {
|
||||
certificates: vec![crate::tls::Certificate {
|
||||
issuer: None,
|
||||
id: "panel".into(),
|
||||
covered_hosts: Vec::new(),
|
||||
hosts: vec![],
|
||||
cert_file: "panel.pem".into(),
|
||||
key_file: "panel.key".into(),
|
||||
}],
|
||||
..crate::tls::Settings::default()
|
||||
};
|
||||
let mut admin = Administration {
|
||||
tls: false,
|
||||
certificate_id: Some("panel".into()),
|
||||
..Administration::default()
|
||||
};
|
||||
assert!(admin.validate(&inventory).is_err());
|
||||
admin.tls = true;
|
||||
assert!(admin.validate(&inventory).is_ok());
|
||||
admin.certificate_id = Some("missing".into());
|
||||
assert!(admin.validate(&inventory).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn administration_session_lifetime_is_7_to_30_days_and_hot_apply_only() {
|
||||
let tls = crate::tls::Settings::default();
|
||||
let mut admin = Administration::default();
|
||||
assert_eq!(admin.session_days, 30);
|
||||
assert_eq!(admin.session_seconds(), 30 * 24 * 60 * 60);
|
||||
assert!(admin.validate(&tls).is_ok());
|
||||
let active = admin.clone();
|
||||
admin.session_days = 7;
|
||||
assert!(admin.validate(&tls).is_ok());
|
||||
assert!(!admin.restart_differs(&active));
|
||||
admin.session_days = 6;
|
||||
assert!(admin.validate(&tls).is_err());
|
||||
admin.session_days = 31;
|
||||
assert!(admin.validate(&tls).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn administration_hostnames_are_normalized_and_unique() {
|
||||
let mut admin = Administration {
|
||||
tls: true,
|
||||
allowed_hosts: vec!["Panel.Example.COM.".into()],
|
||||
..Administration::default()
|
||||
};
|
||||
admin.validate(&crate::tls::Settings::default()).unwrap();
|
||||
assert_eq!(admin.allowed_hosts, vec!["panel.example.com".to_owned()]);
|
||||
admin.allowed_hosts = vec!["panel.example.com".into(), "PANEL.EXAMPLE.COM.".into()];
|
||||
assert!(admin.validate(&crate::tls::Settings::default()).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn administration_hostname_count_is_not_artificially_capped() {
|
||||
let mut admin = Administration {
|
||||
tls: true,
|
||||
allowed_hosts: (0..40)
|
||||
.map(|index| format!("admin-{index}.example.test"))
|
||||
.collect(),
|
||||
..Administration::default()
|
||||
};
|
||||
assert!(admin.validate(&crate::tls::Settings::default()).is_ok());
|
||||
assert_eq!(admin.allowed_hosts.len(), 40);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user