mod commands;
mod response;
use kdl::KdlNode;
use crate::bus::BusModule;
use crate::filter::{FilterParseError, FilterStore, policy_from_lists};
pub struct FilterModule {
store: FilterStore,
}
impl FilterModule {
#[must_use]
pub fn new(store: FilterStore) -> Self {
Self { store }
}
fn command_node<'a>(
module_node: &'a KdlNode,
command_nodes: &[&'a KdlNode],
) -> Option<&'a KdlNode> {
if let Some(cmd) = command_nodes.first() {
return Some(cmd);
}
module_node.children()?.nodes().first()
}
}
impl BusModule for FilterModule {
fn name(&self) -> &'static str {
"filter"
}
fn handle(&self, module_node: &KdlNode, command_nodes: &[&KdlNode]) -> String {
let Some(cmd) = Self::command_node(module_node, command_nodes) else {
return response::err("filter commands: ping, status, reload { … }");
};
handle_node(&self.store, cmd)
}
}
pub fn handle_node(store: &FilterStore, node: &KdlNode) -> String {
match commands::dispatch_node(store, node) {
Ok(body) => body,
Err(e) => response::err(&e.to_string()),
}
}
pub fn handle_legacy(store: &FilterStore, request: &str) -> String {
if request.eq_ignore_ascii_case("PING") {
return response::ok("pong");
}
if request.eq_ignore_ascii_case("STATUS") {
return response::status(store);
}
if let Some(rest) = request.strip_prefix("RELOAD") {
return reload_from_text(store, rest.trim());
}
response::err(&format!("unknown command: {request}"))
}
fn reload_from_text(store: &FilterStore, rest: &str) -> String {
let mut deny_raw = String::new();
let mut allow_raw = String::new();
for part in rest.split_whitespace() {
if let Some(v) = part.strip_prefix("deny=") {
deny_raw = v.to_string();
} else if let Some(v) = part.strip_prefix("allow=") {
allow_raw = v.to_string();
}
}
if deny_raw.is_empty() && allow_raw.is_empty() {
return response::err("filter reload requires at least one deny or allow entry");
}
match policy_from_lists(&deny_raw, &allow_raw) {
Ok(policy) => match store.replace(policy) {
Ok(()) => response::ok("reloaded"),
Err(e) => response::err(&e.to_string()),
},
Err(FilterParseError::InvalidEntry(e) | FilterParseError::InvalidCidr(e)) => {
response::err(&format!("parse error: {e}"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filter::{FilterRule, policy_allows};
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn ping_pong_legacy() {
let store = FilterStore::default();
assert_eq!(handle_legacy(&store, "PING"), "ok \"pong\"");
}
#[test]
fn reload_rejects_empty_legacy() {
let store = FilterStore::default();
let out = handle_legacy(&store, "RELOAD");
assert!(out.starts_with("err "));
assert!(out.contains("filter reload requires at least one deny or allow entry"));
}
#[test]
fn reload_deny_legacy() {
let store = FilterStore::default();
assert_eq!(
handle_legacy(&store, "RELOAD deny=203.0.113.50"),
"ok \"reloaded\""
);
let denied = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 50));
assert!(store.read(|p| p.deny.contains(&FilterRule::Ip(denied))));
assert!(!policy_allows(&store.snapshot(), Some(denied), "/health"));
}
}