use core::fmt;
use crate::config::ProfileId;
use crate::net::CloseReason;
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RuleId {
PublicBind,
ForwardedOnDirect,
FastCgiTrust,
TrustWildcard,
TrustTooBroad,
TwoClientSources,
TrustEmpty,
TrustParse,
WorldWritableSocket,
PeerCredentialsUnavailable,
DuplicateName,
BadName,
ProfileIdReserved,
ProfileGap,
UnknownProfile,
DeadlineOrder,
DeadlineRange,
ZeroCap,
DuplicateBind,
DriverUnavailable,
ReloadWorkers,
DrainRange,
AlpnWithoutTls,
TlsOnFastCgi,
SchemeTrustWithoutSource,
AddressResolve,
TlsWithoutAcceptor,
BadHeaderName,
IngressNeedsHttp,
ReserveOverflow,
TooMany,
DeclaredSchemeHttp,
MinRateOff,
PanicCatchInAbortBuild,
OffloadOnBlocking,
ShortIdleBehindProxy,
LockdownWithoutCritical,
NoListeners,
}
impl RuleId {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
RuleId::PublicBind => "public_bind",
RuleId::ForwardedOnDirect => "forwarded_on_direct",
RuleId::FastCgiTrust => "fastcgi_trust",
RuleId::TrustWildcard => "trust_wildcard",
RuleId::TrustTooBroad => "trust_too_broad",
RuleId::TwoClientSources => "two_client_sources",
RuleId::TrustEmpty => "trust_empty",
RuleId::TrustParse => "trust_parse",
RuleId::WorldWritableSocket => "world_writable_socket",
RuleId::PeerCredentialsUnavailable => "peer_credentials_unavailable",
RuleId::DuplicateName => "duplicate_name",
RuleId::BadName => "bad_name",
RuleId::ProfileIdReserved => "profile_id_reserved",
RuleId::ProfileGap => "profile_gap",
RuleId::UnknownProfile => "unknown_profile",
RuleId::DeadlineOrder => "deadline_order",
RuleId::DeadlineRange => "deadline_range",
RuleId::ZeroCap => "zero_cap",
RuleId::DuplicateBind => "duplicate_bind",
RuleId::DriverUnavailable => "driver_unavailable",
RuleId::ReloadWorkers => "reload_workers",
RuleId::DrainRange => "drain_range",
RuleId::AlpnWithoutTls => "alpn_without_tls",
RuleId::TlsOnFastCgi => "tls_on_fastcgi",
RuleId::SchemeTrustWithoutSource => "scheme_trust_without_source",
RuleId::AddressResolve => "address_resolve",
RuleId::TlsWithoutAcceptor => "tls_without_acceptor",
RuleId::BadHeaderName => "bad_header_name",
RuleId::IngressNeedsHttp => "ingress_needs_http",
RuleId::ReserveOverflow => "reserve_overflow",
RuleId::TooMany => "too_many",
RuleId::DeclaredSchemeHttp => "declared_scheme_http",
RuleId::MinRateOff => "min_rate_off",
RuleId::PanicCatchInAbortBuild => "panic_catch_in_abort_build",
RuleId::OffloadOnBlocking => "offload_on_blocking",
RuleId::ShortIdleBehindProxy => "short_idle_behind_proxy",
RuleId::LockdownWithoutCritical => "lockdown_without_critical",
RuleId::NoListeners => "no_listeners",
}
}
}
impl fmt::Display for RuleId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Subject {
Server,
Listener(String),
Profile(ProfileId),
}
impl fmt::Display for Subject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Subject::Server => f.write_str("server"),
Subject::Listener(name) => write!(f, "listener {name}"),
Subject::Profile(id) => write!(f, "profile {}", id.0),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConfigError {
pub rule: RuleId,
pub subject: Subject,
pub message: &'static str,
pub hint: &'static str,
}
impl ConfigError {
pub(crate) fn new(
rule: RuleId,
subject: Subject,
message: &'static str,
hint: &'static str,
) -> Self {
ConfigError {
rule,
subject,
message,
hint,
}
}
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {} ({}; rule {})",
self.subject, self.message, self.hint, self.rule
)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConfigWarning {
pub rule: RuleId,
pub subject: Subject,
pub message: &'static str,
}
impl ConfigWarning {
pub(crate) fn new(rule: RuleId, subject: Subject, message: &'static str) -> Self {
ConfigWarning {
rule,
subject,
message,
}
}
}
impl fmt::Display for ConfigWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {} (rule {})", self.subject, self.message, self.rule)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConfigErrors {
errors: Vec<ConfigError>,
}
impl ConfigErrors {
pub(crate) fn from_vec(mut errors: Vec<ConfigError>) -> Option<ConfigErrors> {
if errors.is_empty() {
return None;
}
errors.sort_by(|a, b| a.subject.cmp(&b.subject));
Some(ConfigErrors { errors })
}
pub fn iter(&self) -> impl Iterator<Item = &ConfigError> {
self.errors.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.errors.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
#[must_use]
pub fn has(&self, rule: RuleId) -> bool {
self.errors.iter().any(|e| e.rule == rule)
}
}
impl IntoIterator for ConfigErrors {
type Item = ConfigError;
type IntoIter = std::vec::IntoIter<ConfigError>;
fn into_iter(self) -> Self::IntoIter {
self.errors.into_iter()
}
}
impl fmt::Display for ConfigErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let n = self.errors.len();
write!(
f,
"{n} configuration error{}:",
if n == 1 { "" } else { "s" }
)?;
for e in &self.errors {
write!(f, "\n {e}")?;
}
Ok(())
}
}
impl std::error::Error for ConfigErrors {}
#[non_exhaustive]
#[derive(Debug)]
pub enum ServeError {
Config(ConfigErrors),
Bind {
listener: String,
source: net_io::NetError,
},
Start(net_io::NetError),
Draining,
}
impl fmt::Display for ServeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ServeError::Config(errors) => write!(f, "{errors}"),
ServeError::Bind { listener, source } => {
write!(f, "listener `{listener}` could not be bound: {source}")
}
ServeError::Start(source) => write!(f, "the server could not start: {source}"),
ServeError::Draining => {
f.write_str("the server is draining; start a new server instead")
}
}
}
}
impl std::error::Error for ServeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ServeError::Config(errors) => Some(errors),
ServeError::Bind { source, .. } | ServeError::Start(source) => Some(source),
ServeError::Draining => None,
}
}
}
impl From<ConfigErrors> for ServeError {
fn from(errors: ConfigErrors) -> Self {
ServeError::Config(errors)
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EndReason {
Transport(CloseReason),
HeadTimeout,
RequestTimeout,
ReplyTimeout,
ClientClose,
RequestLimit,
MaxAge,
Drained,
ProtocolError,
IngressRefused,
CapacityRefused,
Shed,
Denied,
CheckpointKill,
ServiceClosed,
ServicePanic,
ReplyDropped,
NoEngine,
Refused,
}
impl EndReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
EndReason::Transport(_) => "transport",
EndReason::HeadTimeout => "head_timeout",
EndReason::RequestTimeout => "request_timeout",
EndReason::ReplyTimeout => "reply_timeout",
EndReason::ClientClose => "client_close",
EndReason::RequestLimit => "request_limit",
EndReason::MaxAge => "max_age",
EndReason::Drained => "drained",
EndReason::ProtocolError => "protocol_error",
EndReason::IngressRefused => "ingress_refused",
EndReason::CapacityRefused => "capacity_refused",
EndReason::Shed => "shed",
EndReason::Denied => "denied",
EndReason::CheckpointKill => "checkpoint_kill",
EndReason::ServiceClosed => "service_closed",
EndReason::ServicePanic => "service_panic",
EndReason::ReplyDropped => "reply_dropped",
EndReason::NoEngine => "no_engine",
EndReason::Refused => "refused",
}
}
#[must_use]
pub fn is_abortive(self) -> bool {
match self {
EndReason::CheckpointKill | EndReason::ServicePanic => true,
EndReason::Transport(reason) => reason.is_abortive(),
_ => false,
}
}
}
impl fmt::Display for EndReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn err(rule: RuleId, subject: Subject) -> ConfigError {
ConfigError::new(rule, subject, "m", "h")
}
#[test]
fn test_config_errors_empty_is_none() {
assert!(ConfigErrors::from_vec(Vec::new()).is_none());
}
#[test]
fn test_config_errors_sorted_by_subject_stable() {
let errors = ConfigErrors::from_vec(vec![
err(RuleId::BadName, Subject::Listener("b".into())),
err(RuleId::ZeroCap, Subject::Server),
err(RuleId::PublicBind, Subject::Listener("a".into())),
err(RuleId::DuplicateName, Subject::Listener("b".into())),
])
.unwrap();
let rules: Vec<RuleId> = errors.iter().map(|e| e.rule).collect();
assert_eq!(
rules,
[
RuleId::ZeroCap,
RuleId::PublicBind,
RuleId::BadName,
RuleId::DuplicateName
]
);
assert!(errors.has(RuleId::ZeroCap));
assert!(!errors.has(RuleId::TrustEmpty));
assert_eq!(errors.len(), 4);
}
#[test]
fn test_config_errors_display_lists_every_error() {
let errors = ConfigErrors::from_vec(vec![
ConfigError::new(
RuleId::ZeroCap,
Subject::Server,
"a cap is 0",
"set it to 1 or more",
),
ConfigError::new(
RuleId::BadName,
Subject::Listener("X".into()),
"bad name",
"use lowercase",
),
])
.unwrap();
let text = errors.to_string();
assert!(text.starts_with("2 configuration errors:"));
assert!(text.contains("server: a cap is 0 (set it to 1 or more; rule zero_cap)"));
assert!(text.contains("listener X: bad name"));
}
#[test]
fn test_serve_error_config_has_source() {
let errors = ConfigErrors::from_vec(vec![err(RuleId::ZeroCap, Subject::Server)]).unwrap();
let e = ServeError::from(errors);
assert!(std::error::Error::source(&e).is_some());
assert!(std::error::Error::source(&ServeError::Draining).is_none());
}
#[test]
fn test_end_reason_names_unique() {
let all = [
EndReason::Transport(CloseReason::PeerClosed),
EndReason::HeadTimeout,
EndReason::RequestTimeout,
EndReason::ReplyTimeout,
EndReason::ClientClose,
EndReason::RequestLimit,
EndReason::MaxAge,
EndReason::Drained,
EndReason::ProtocolError,
EndReason::IngressRefused,
EndReason::CapacityRefused,
EndReason::Shed,
EndReason::Denied,
EndReason::CheckpointKill,
EndReason::ServiceClosed,
EndReason::ServicePanic,
EndReason::ReplyDropped,
EndReason::NoEngine,
EndReason::Refused,
];
let mut names: Vec<&str> = all.iter().map(|r| r.as_str()).collect();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), all.len());
}
#[test]
fn test_subject_display() {
assert_eq!(Subject::Server.to_string(), "server");
assert_eq!(Subject::Listener("web".into()).to_string(), "listener web");
assert_eq!(Subject::Profile(ProfileId(1)).to_string(), "profile 1");
}
}