use std::sync::Arc;
use praxis_core::{
PingoraServerRuntime, ProxyError,
config::{Config, ProtocolKind},
};
use crate::{ListenerPipelines, Protocol};
pub mod context;
pub(crate) mod convert;
pub(crate) mod grpc_trailers;
pub mod handler;
pub mod health;
#[cfg(feature = "admin-api")]
pub(crate) mod json;
#[cfg(feature = "admin-api")]
pub mod kv;
pub mod listener;
pub mod metrics;
pub struct PingoraHttp;
impl Protocol for PingoraHttp {
fn register(
self: Box<Self>,
server: &mut PingoraServerRuntime,
config: &Config,
pipelines: &ListenerPipelines,
) -> Result<Vec<tokio::sync::watch::Sender<bool>>, ProxyError> {
let http_listeners: Vec<_> = config
.listeners
.iter()
.filter(|l| l.protocol == ProtocolKind::Http)
.collect();
if http_listeners.is_empty() {
return Ok(Vec::new());
}
let mut cert_watcher_shutdowns = Vec::new();
for listener in &http_listeners {
let pipeline = pipelines.get(&listener.name).map(Arc::clone).ok_or_else(|| {
ProxyError::Config(format!("no pipeline for listener '{name}'", name = listener.name))
})?;
handler::load_http_handler(server.server_mut(), listener, pipeline, &mut cert_watcher_shutdowns)?;
}
Ok(cert_watcher_shutdowns)
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
clippy::too_many_lines,
clippy::use_self,
clippy::missing_panics_doc,
reason = "tests"
)]
mod tests {
use std::collections::HashMap;
use praxis_core::config::{
AdminConfig, BodyLimitsConfig, InsecureOptions, Listener, MetricsConfig, RuntimeConfig, TelemetryConfig,
};
use praxis_filter::{FilterPipeline, FilterRegistry};
use super::*;
fn make_config(listeners: Vec<Listener>) -> Config {
Config {
admin: AdminConfig::default(),
body_limits: BodyLimitsConfig::default(),
clusters: vec![],
filter_chains: vec![],
insecure_options: InsecureOptions::default(),
listeners,
metrics: MetricsConfig::default(),
runtime: RuntimeConfig::default(),
shutdown_timeout_secs: 30,
telemetry: TelemetryConfig::default(),
}
}
fn make_listener(name: &str, protocol: ProtocolKind) -> Listener {
Listener {
name: name.to_owned(),
address: "127.0.0.1:8080".to_owned(),
protocol,
cluster: None,
downstream_keepalive_timeout_ms: None,
downstream_read_timeout_ms: None,
filter_chains: vec![],
max_connections: None,
tcp_max_duration_secs: None,
tcp_session_timeout_ms: None,
tls: None,
upstream: None,
}
}
fn make_pipelines(names: &[&str]) -> ListenerPipelines {
let registry = FilterRegistry::with_builtins();
let mut map = HashMap::new();
for name in names {
let pipeline = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
map.insert((*name).to_owned(), pipeline);
}
ListenerPipelines::new(map)
}
#[test]
fn register_returns_empty_vec_when_no_http_listeners() {
let listeners = vec![
make_listener("tcp1", ProtocolKind::Tcp),
make_listener("tcp2", ProtocolKind::Tcp),
];
let config = make_config(listeners);
let http_listeners: Vec<_> = config
.listeners
.iter()
.filter(|l| l.protocol == ProtocolKind::Http)
.collect();
assert!(
http_listeners.is_empty(),
"should have no HTTP listeners when all are TCP"
);
}
#[test]
fn register_returns_empty_vec_when_no_listeners_at_all() {
let config = make_config(vec![]);
let http_listeners: Vec<_> = config
.listeners
.iter()
.filter(|l| l.protocol == ProtocolKind::Http)
.collect();
assert!(
http_listeners.is_empty(),
"should have no HTTP listeners in empty config"
);
}
#[test]
fn register_filters_http_listeners_correctly() {
let listeners = vec![
make_listener("http1", ProtocolKind::Http),
make_listener("tcp1", ProtocolKind::Tcp),
make_listener("http2", ProtocolKind::Http),
];
let config = make_config(listeners);
let http_listeners: Vec<_> = config
.listeners
.iter()
.filter(|l| l.protocol == ProtocolKind::Http)
.collect();
assert_eq!(http_listeners.len(), 2, "should have exactly 2 HTTP listeners");
assert_eq!(http_listeners[0].name, "http1");
assert_eq!(http_listeners[1].name, "http2");
}
#[test]
fn register_fails_when_pipeline_missing_for_listener() {
let listeners = vec![
make_listener("http1", ProtocolKind::Http),
make_listener("http2", ProtocolKind::Http),
];
let config = make_config(listeners);
let pipelines = make_pipelines(&["http1"]);
let listener = &config.listeners[1]; let result = pipelines
.get(&listener.name)
.ok_or_else(|| ProxyError::Config(format!("no pipeline for listener '{name}'", name = listener.name)));
assert!(result.is_err(), "should fail when pipeline is missing");
if let Err(ProxyError::Config(msg)) = result {
assert!(
msg.contains("no pipeline for listener"),
"error should mention missing pipeline"
);
assert!(msg.contains("http2"), "error should include listener name");
} else {
panic!("expected ProxyError::Config");
}
}
#[test]
fn register_finds_pipeline_for_matching_listener() {
let listeners = vec![make_listener("web", ProtocolKind::Http)];
let config = make_config(listeners);
let pipelines = make_pipelines(&["web"]);
let listener = &config.listeners[0];
let result = pipelines.get(&listener.name);
assert!(result.is_some(), "should find pipeline for matching listener");
}
#[test]
fn register_processes_multiple_http_listeners() {
let listeners = vec![
make_listener("http1", ProtocolKind::Http),
make_listener("http2", ProtocolKind::Http),
make_listener("http3", ProtocolKind::Http),
];
let config = make_config(listeners);
let pipelines = make_pipelines(&["http1", "http2", "http3"]);
for listener in &config.listeners {
assert!(
pipelines.get(&listener.name).is_some(),
"should have pipeline for {name}",
name = listener.name
);
}
}
#[test]
fn register_error_message_format() {
let listener_name = "my-http-listener";
let error = ProxyError::Config(format!("no pipeline for listener '{listener_name}'"));
match error {
ProxyError::Config(msg) => {
assert_eq!(msg, "no pipeline for listener 'my-http-listener'");
assert!(msg.starts_with("no pipeline"));
},
}
}
#[test]
fn protocol_kind_http_equality() {
assert_eq!(ProtocolKind::Http, ProtocolKind::Http);
assert_ne!(ProtocolKind::Http, ProtocolKind::Tcp);
}
#[test]
fn pingora_http_can_be_boxed() {
let protocol: Box<dyn Protocol> = Box::new(PingoraHttp);
drop(protocol);
}
#[test]
fn listener_name_used_for_pipeline_lookup() {
let listener1 = make_listener("listener-one", ProtocolKind::Http);
let listener2 = make_listener("listener-two", ProtocolKind::Http);
let pipelines = make_pipelines(&["listener-one"]);
assert!(
pipelines.get(&listener1.name).is_some(),
"should find pipeline by exact name match"
);
assert!(
pipelines.get(&listener2.name).is_none(),
"should not find pipeline for non-existent name"
);
}
#[test]
fn empty_pipeline_lookup_returns_none() {
let pipelines = make_pipelines(&[]);
assert!(
pipelines.get("any-name").is_none(),
"empty pipelines should return None"
);
}
#[test]
fn protocol_error_config_variant() {
let error = ProxyError::Config("test error".to_owned());
match error {
ProxyError::Config(msg) => assert_eq!(msg, "test error"),
}
}
#[test]
fn http_listener_filtering_with_mixed_protocols() {
let listeners = vec![
make_listener("http1", ProtocolKind::Http),
make_listener("tcp1", ProtocolKind::Tcp),
make_listener("http2", ProtocolKind::Http),
make_listener("tcp2", ProtocolKind::Tcp),
make_listener("http3", ProtocolKind::Http),
];
let config = make_config(listeners);
let http_listeners: Vec<_> = config
.listeners
.iter()
.filter(|l| l.protocol == ProtocolKind::Http)
.collect();
assert_eq!(http_listeners.len(), 3);
assert!(http_listeners.iter().all(|l| l.protocol == ProtocolKind::Http));
}
}