use std::{
borrow::Cow,
collections::{BTreeSet, HashMap},
net::{SocketAddr, ToSocketAddrs},
sync::Arc,
};
use http::{HeaderName, HeaderValue};
use path_tree::PathTree;
use pingora::lb::{discovery::Static, selection::RoundRobin, Backends, LoadBalancer};
use crate::config::{RouteCache, RoutePlugin, RouteUpstream};
#[derive(Debug, Default, Clone)]
pub struct RouteStorePathMatcher {
pub pattern: Option<PathTree<usize>>,
}
impl RouteStorePathMatcher {
pub fn new() -> Self {
RouteStorePathMatcher::default()
}
pub fn with_pattern(&mut self, pattern: &[Cow<'_, str>]) -> &mut Self {
if pattern.is_empty() {
return self;
}
let mut path_tree = PathTree::new();
for (index, value) in pattern.iter().enumerate() {
let _ = path_tree.insert(value, index);
}
self.pattern = Some(path_tree);
self
}
}
#[derive(Clone)]
pub struct RouteStoreContainer {
pub load_balancer: Arc<LoadBalancer<RoundRobin>>,
pub path_matcher: RouteStorePathMatcher,
pub host_header_remove: Vec<String>,
pub host_header_add: Vec<(HeaderName, HeaderValue)>,
pub upstreams: Vec<RouteUpstream>,
upstream_addrs: HashMap<SocketAddr, usize>,
pub self_signed_certificate: bool,
pub plugins: HashMap<String, RoutePlugin>,
pub cache: Option<RouteCache>,
}
impl Default for RouteStoreContainer {
fn default() -> Self {
RouteStoreContainer {
load_balancer: Arc::new(LoadBalancer::<RoundRobin>::from_backends(Backends::new(
Static::new(BTreeSet::new()),
))),
path_matcher: RouteStorePathMatcher::default(),
host_header_remove: Vec::new(),
host_header_add: Vec::new(),
self_signed_certificate: false,
plugins: HashMap::new(),
upstreams: Vec::new(),
upstream_addrs: HashMap::new(),
cache: None,
}
}
}
impl RouteStoreContainer {
pub fn new(load_balancer: LoadBalancer<RoundRobin>) -> Self {
RouteStoreContainer {
load_balancer: Arc::new(load_balancer),
path_matcher: RouteStorePathMatcher::new(),
host_header_remove: Vec::with_capacity(5),
host_header_add: Vec::with_capacity(5),
self_signed_certificate: false,
plugins: HashMap::new(),
upstreams: Vec::with_capacity(5),
upstream_addrs: HashMap::new(),
cache: None,
}
}
pub fn set_upstreams(&mut self, upstreams: Vec<RouteUpstream>) {
let mut upstream_addrs = HashMap::new();
for (index, upstream) in upstreams.iter().enumerate() {
let Ok(addrs) = format!("{}:{}", upstream.ip, upstream.port).to_socket_addrs() else {
tracing::warn!(
"could not resolve upstream {}:{}",
upstream.ip,
upstream.port
);
continue;
};
for addr in addrs {
upstream_addrs.entry(addr).or_insert(index);
}
}
self.upstream_addrs = upstream_addrs;
self.upstreams = upstreams;
}
pub fn upstream_for(&self, addr: &SocketAddr) -> Option<&RouteUpstream> {
self.upstream_addrs
.get(addr)
.and_then(|index| self.upstreams.get(*index))
}
}
pub type RouteStore = papaya::HashMap<String, RouteStoreContainer>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_router_container_defaults_empty_pattern() {
let load_balancer = LoadBalancer::<RoundRobin>::try_from_iter(vec!["1.1.1.1:80"]).unwrap();
let route_store = RouteStoreContainer::new(load_balancer);
assert!(route_store.path_matcher.pattern.is_none());
}
#[test]
fn test_router_container_works_with_valid_and_invalid_pattern() {
let load_balancer = LoadBalancer::<RoundRobin>::try_from_iter(vec!["1.1.1.1:80"]).unwrap();
let mut route_store = RouteStoreContainer::new(load_balancer);
route_store
.path_matcher
.with_pattern(&[Cow::Borrowed("/auth")]);
assert!(route_store.path_matcher.pattern.is_some());
let pattern = route_store.path_matcher.pattern.as_ref().unwrap();
assert!(pattern.find("/auth").is_some());
let (h, p) = pattern.find("/auth").unwrap();
assert_eq!(h, &0);
assert_eq!(p.pattern(), "/auth");
assert!(pattern.find("/invalid").is_none());
}
#[test]
fn test_router_container_finds_upstream_by_resolved_addr() {
let load_balancer = LoadBalancer::<RoundRobin>::try_from_iter(vec!["1.1.1.1:80"]).unwrap();
let mut route_store = RouteStoreContainer::new(load_balancer);
route_store.set_upstreams(vec![
RouteUpstream {
ip: Cow::Borrowed("127.0.0.1"),
port: 8080,
sni: Some("ip.test".to_string()),
..RouteUpstream::default()
},
RouteUpstream {
ip: Cow::Borrowed("localhost"),
port: 9090,
sni: Some("hostname.test".to_string()),
..RouteUpstream::default()
},
RouteUpstream {
ip: Cow::Borrowed("does-not-resolve.invalid"),
port: 80,
..RouteUpstream::default()
},
]);
let by_ip = route_store.upstream_for(&"127.0.0.1:8080".parse().unwrap());
assert_eq!(by_ip.unwrap().sni.as_deref(), Some("ip.test"));
let by_hostname = route_store.upstream_for(&"127.0.0.1:9090".parse().unwrap());
assert_eq!(by_hostname.unwrap().sni.as_deref(), Some("hostname.test"));
assert!(route_store
.upstream_for(&"127.0.0.1:1234".parse().unwrap())
.is_none());
assert_eq!(route_store.upstreams.len(), 3);
}
}