ban actions + error handling + refactoring

rust-rewrite
Yves G 2021-04-24 18:20:22 +02:00
parent bf93109f96
commit 7952cb0b76
32 changed files with 1604 additions and 366 deletions

View File

@ -16,3 +16,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
systemd = "0.8"
[dev-dependencies]
lazy_static = "1.4"

View File

@ -1,5 +1,5 @@
use super::CounterAction;
use crate::domain::{Action, Counters, CountersPort, ModuleArgs, Record, Singleton, Value};
use crate::domain::{Action, Counters, CountersPort, Error, ModuleArgs, Record, Singleton, Value};
use crate::singleton_borrow;
use chrono::Utc;
@ -19,9 +19,10 @@ impl<C: CountersPort> CounterRaise<C> {
}
impl<C: CountersPort> Action for CounterRaise<C> {
fn act(&mut self, record: &mut Record) -> Result<(), ()> {
match record.get(&self.act.counter_key) {
None => Err(()),
fn act(&mut self, record: &mut Record) -> Result<(), Error> {
let k = &self.act.counter_key;
match record.get(k) {
None => Err(format!("Key {} not found in the log entry", k).into()),
Some(v) => {
let count = singleton_borrow!(self.act.counters).augment(
(self.act.counter_name.as_ref(), v),
@ -50,7 +51,7 @@ mod tests {
fn when_non_existing_then_raise_to_1() {
let (_, mut action) = get_counters_action();
let mut record = HashMap::with_capacity(1);
record.insert("k".to_string(), Value::Str("raise#1".to_string()));
record.insert("k".into(), Value::Str("raise#1".into()));
action.act(&mut record).unwrap();
assert_eq!(Some(&Value::Int(1)), record.get("raise"));
@ -60,9 +61,9 @@ mod tests {
fn when_different_key_then_different_counter() {
let (_, mut action) = get_counters_action();
let mut record1 = HashMap::with_capacity(1);
record1.insert("k".to_string(), Value::Str("raise#3".to_string()));
record1.insert("k".into(), Value::Str("raise#3".into()));
let mut record2 = HashMap::with_capacity(1);
record2.insert("k".to_string(), Value::Str("raise#4".to_string()));
record2.insert("k".into(), Value::Str("raise#4".into()));
action.act(&mut record1).unwrap();
assert_eq!(Some(&Value::Int(1)), record1.get("raise"));
@ -80,9 +81,9 @@ mod tests {
fn when_grace_time_then_count_is_0() {
let (counters, mut action) = get_counters_action();
let mut record = HashMap::with_capacity(1);
record.insert("k".to_string(), Value::Str("raise#5".to_string()));
record.insert("k".into(), Value::Str("raise#5".into()));
singleton_borrow!(counters).insert(
("test".to_string(), Value::Str("raise#5".to_string())),
("test".into(), Value::Str("raise#5".into())),
(0, Some(Utc::now() + Duration::seconds(1))),
);
@ -103,9 +104,9 @@ mod tests {
counters: singleton_share!(counters)
}));
let mut args = HashMap::with_capacity(3);
args.insert("counter".to_string(), Value::Str("test".to_string()));
args.insert("for".to_string(), Value::Str("k".to_string()));
args.insert("save".to_string(), Value::Str("raise".to_string()));
args.insert("counter".into(), Value::Str("test".into()));
args.insert("for".into(), Value::Str("k".into()));
args.insert("save".into(), Value::Str("raise".into()));
let action = CounterRaise::<FakeCountersAdapter>::from_args(args, counters_backend);
(counters, action)
}

View File

@ -1,5 +1,5 @@
use super::CounterAction;
use crate::domain::{Action, Counters, CountersPort, ModuleArgs, Record, Singleton, Value};
use crate::domain::{Action, Counters, CountersPort, Error, ModuleArgs, Record, Singleton, Value};
use crate::singleton_borrow;
use chrono::Utc;
@ -19,9 +19,10 @@ impl<C: CountersPort> CounterReset<C> {
}
impl<C: CountersPort> Action for CounterReset<C> {
fn act(&mut self, record: &mut Record) -> Result<(), ()> {
match record.get(&self.act.counter_key) {
None => Err(()),
fn act(&mut self, record: &mut Record) -> Result<(), Error> {
let k = &self.act.counter_key;
match record.get(k) {
None => Err(format!("Key {} not found in the log entry", k).into()),
Some(v) => {
let count = singleton_borrow!(self.act.counters).reset(
(self.act.counter_name.as_ref(), v),
@ -49,11 +50,8 @@ mod tests {
fn when_reset_without_gracetime_then_count_is_0_and_counter_removed() {
let (counters, mut action) = get_counters_action(None);
let mut record = HashMap::with_capacity(1);
record.insert("k".to_string(), Value::Str("reset#1".to_string()));
singleton_borrow!(counters).insert(
("test".to_string(), Value::Str("reset#1".to_string())),
(5, None),
);
record.insert("k".into(), Value::Str("reset#1".into()));
singleton_borrow!(counters).insert(("test".into(), Value::Str("reset#1".into())), (5, None));
action.act(&mut record).unwrap();
assert_eq!(Some(&Value::Int(0)), record.get("reset"));
@ -64,14 +62,14 @@ mod tests {
fn when_reset_with_gracetime_then_count_is_0_and_gracetime_is_stored() {
let (counters, mut action) = get_counters_action(Some(5));
let mut record = HashMap::with_capacity(1);
record.insert("k".to_string(), Value::Str("reset#2".to_string()));
record.insert("k".into(), Value::Str("reset#2".into()));
let almost = Utc::now() + Duration::seconds(5);
let after = almost + Duration::seconds(1);
action.act(&mut record).unwrap();
assert_eq!(Some(&Value::Int(0)), record.get("reset"));
let (c, od) = *(singleton_borrow!(counters)
.get(&("test".to_string(), Value::Str("reset#2".to_string())))
.get(&("test".into(), Value::Str("reset#2".into())))
.unwrap());
let d = od.unwrap();
assert!(d >= almost);
@ -91,11 +89,11 @@ mod tests {
counters: singleton_share!(counters)
}));
let mut args = HashMap::with_capacity(grace_time.map(|_| 4).unwrap_or(3));
args.insert("counter".to_string(), Value::Str("test".to_string()));
args.insert("for".to_string(), Value::Str("k".to_string()));
args.insert("save".to_string(), Value::Str("reset".to_string()));
args.insert("counter".into(), Value::Str("test".into()));
args.insert("for".into(), Value::Str("k".into()));
args.insert("save".into(), Value::Str("reset".into()));
if let Some(sec) = grace_time {
args.insert("graceSeconds".to_string(), Value::Int(sec));
args.insert("graceSeconds".into(), Value::Int(sec));
}
let action = CounterReset::<FakeCountersAdapter>::from_args(args, counters_backend);
(counters, action)

View File

@ -1,5 +1,7 @@
use super::{get_acceptable_key, remove_acceptable_key};
use crate::domain::{Action, DnatMapping, DnatMappingsPort, ModuleArgs, Record, Singleton, Value};
use crate::domain::{
Action, DnatMapping, DnatMappingsPort, Error, ModuleArgs, Record, Singleton, Value,
};
use crate::singleton_borrow;
use chrono::{Duration, Utc};
@ -69,7 +71,7 @@ impl DnatCapture {
}
impl Action for DnatCapture {
fn act(&mut self, record: &mut Record) -> Result<(), ()> {
fn act(&mut self, record: &mut Record) -> Result<(), Error> {
let src_addr = value_for(&self.specs.src_addr, record);
let internal_addr = value_for(&self.specs.internal_addr, record);
if src_addr == None || internal_addr == None {
@ -117,7 +119,7 @@ mod tests {
let mappings = singleton_new!(FakeDnatMappings {
mappings: Vec::new()
});
args.insert("addr".to_string(), Value::Str("int_ip".to_string()));
args.insert("addr".into(), Value::Str("int_ip".into()));
let _ = DnatCapture::from_args(args, singleton_share!(mappings));
}
@ -130,7 +132,7 @@ mod tests {
let mappings = singleton_new!(FakeDnatMappings {
mappings: Vec::new()
});
args.insert("saddr".to_string(), Value::Str("src_ip".to_string()));
args.insert("saddr".into(), Value::Str("src_ip".into()));
let _ = DnatCapture::from_args(args, singleton_share!(mappings));
}
@ -140,8 +142,8 @@ mod tests {
let mappings = singleton_new!(FakeDnatMappings {
mappings: Vec::new()
});
args.insert("saddr".to_string(), Value::Str("src_ip".to_string()));
args.insert("addrValue".to_string(), Value::Str("1.2.3.4".to_string()));
args.insert("saddr".into(), Value::Str("src_ip".into()));
args.insert("addrValue".into(), Value::Str("1.2.3.4".into()));
let _ = DnatCapture::from_args(args, singleton_share!(mappings));
assert!(true);
}
@ -152,8 +154,8 @@ mod tests {
let mappings = singleton_new!(FakeDnatMappings {
mappings: Vec::new()
});
args.insert("saddr".to_string(), Value::Str("src_ip".to_string()));
args.insert("addr".to_string(), Value::Str("int_ip".to_string()));
args.insert("saddr".into(), Value::Str("src_ip".into()));
args.insert("addr".into(), Value::Str("int_ip".into()));
let _ = DnatCapture::from_args(args, singleton_share!(mappings));
assert!(true);
}
@ -164,8 +166,8 @@ mod tests {
let mappings = singleton_new!(FakeDnatMappings {
mappings: Vec::new()
});
args.insert("saddr".to_string(), Value::Str("src_ip".to_string()));
args.insert("addr".to_string(), Value::Str("int_ip".to_string()));
args.insert("saddr".into(), Value::Str("src_ip".into()));
args.insert("addr".into(), Value::Str("int_ip".into()));
let action = DnatCapture::from_args(args, singleton_share!(mappings));
assert_eq!(Duration::seconds(63), action.specs.keep_duration);
}
@ -176,8 +178,8 @@ mod tests {
let mappings = singleton_new!(FakeDnatMappings {
mappings: Vec::new()
});
args.insert("saddr".to_string(), Value::Str("src_ip".to_string()));
args.insert("addr".to_string(), Value::Str("int_ip".to_string()));
args.insert("saddr".into(), Value::Str("src_ip".into()));
args.insert("addr".into(), Value::Str("int_ip".into()));
let mut action = DnatCapture::from_args(args, singleton_share!(mappings));
action.act(&mut HashMap::new()).unwrap();
assert_eq!(0, singleton_borrow!(mappings).mappings.len());
@ -194,19 +196,19 @@ mod tests {
});
// specify the Action
args.insert("saddr".to_string(), Value::Str("sa".to_string()));
args.insert("saddr".into(), Value::Str("sa".into()));
// prepare the entry
let mut entry: Record = HashMap::with_capacity(6);
entry.insert("sa".to_string(), Value::Str("vsa".to_string()));
entry.insert("sp".to_string(), Value::Str("vsp".to_string()));
entry.insert("sa".into(), Value::Str("vsa".into()));
entry.insert("sp".into(), Value::Str("vsp".into()));
if entry_with_addr {
entry.insert("a".to_string(), Value::Str("va".to_string()));
entry.insert("p".to_string(), Value::Str("vp".to_string()));
entry.insert("a".into(), Value::Str("va".into()));
entry.insert("p".into(), Value::Str("vp".into()));
}
if entry_with_daddr {
entry.insert("da".to_string(), Value::Str("vda".to_string()));
entry.insert("dp".to_string(), Value::Str("vdp".to_string()));
entry.insert("da".into(), Value::Str("vda".into()));
entry.insert("dp".into(), Value::Str("vdp".into()));
}
// run
@ -301,7 +303,7 @@ mod tests {
dest_port: Option<&str>,
) -> DnatMapping {
DnatMapping {
src_addr: Some("vsa".to_string()),
src_addr: Some("vsa".into()),
src_port: src_port.map(|s| s.to_string()),
internal_addr: internal_addr.map(|s| s.to_string()),
internal_port: internal_port.map(|s| s.to_string()),

View File

@ -1,5 +1,7 @@
use super::{get_acceptable_key, remove_acceptable_key};
use crate::domain::{Action, DnatMapping, DnatMappingsPort, ModuleArgs, Record, Singleton, Value};
use crate::domain::{
Action, DnatMapping, DnatMappingsPort, Error, ModuleArgs, Record, Singleton, Value,
};
use crate::singleton_borrow;
type MappingGetter = fn(&DnatMapping) -> &Option<String>;
@ -54,7 +56,7 @@ impl DnatReplace {
}
impl Action for DnatReplace {
fn act(&mut self, record: &mut Record) -> Result<(), ()> {
fn act(&mut self, record: &mut Record) -> Result<(), Error> {
for (field, _) in self.matchers.iter() {
if !record.contains_key(field) {
return Ok(()); // not applicable
@ -97,7 +99,7 @@ mod tests {
let mappings = singleton_new!(FakeDnatMappings {
mappings: Vec::new()
});
args.insert("addr".to_string(), Value::Str("int_ip".to_string()));
args.insert("addr".into(), Value::Str("int_ip".into()));
let _ = DnatReplace::from_args(args, singleton_share!(mappings));
}
@ -110,7 +112,7 @@ mod tests {
mappings: Vec::new()
});
let mut args = HashMap::with_capacity(1);
args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string()));
args.insert("saddrInto".into(), Value::Str("src_ip".into()));
let _ = DnatReplace::from_args(args, singleton_share!(mappings));
}
@ -120,11 +122,11 @@ mod tests {
mappings: Vec::new()
});
let mut args = HashMap::with_capacity(2);
args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string()));
args.insert("dport".to_string(), Value::Int(1234));
args.insert("saddrInto".into(), Value::Str("src_ip".into()));
args.insert("dport".into(), Value::Int(1234));
let action = DnatReplace::from_args(args, singleton_share!(mappings));
assert_eq!(
vec!(("1234".to_string(), Some("dp".to_string()))),
vec!(("1234".into(), Some("dp".into()))),
action
.matchers
.iter()
@ -132,7 +134,7 @@ mod tests {
.collect::<Vec<(String, Option<String>)>>()
);
assert_eq!(
vec!(("src_ip".to_string(), Some("sa".to_string()))),
vec!(("src_ip".into(), Some("sa".into()))),
action
.updaters
.iter()
@ -147,11 +149,11 @@ mod tests {
mappings: vec!(sample_dnat_mapping()),
});
let mut args = HashMap::with_capacity(2);
args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string()));
args.insert("port".to_string(), Value::Str("src_port".to_string()));
args.insert("saddrInto".into(), Value::Str("src_ip".into()));
args.insert("port".into(), Value::Str("src_port".into()));
let mut record = HashMap::new();
record.insert("src_ip".to_string(), Value::Str("prox".to_string()));
record.insert("dest_ip".to_string(), Value::Str("serv".to_string()));
record.insert("src_ip".into(), Value::Str("prox".into()));
record.insert("dest_ip".into(), Value::Str("serv".into()));
let expected = record.clone();
let mut action = DnatReplace::from_args(args, singleton_share!(mappings));
action.act(&mut record).unwrap();
@ -164,12 +166,12 @@ mod tests {
mappings: vec!(sample_dnat_mapping()),
});
let mut args = HashMap::with_capacity(2);
args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string()));
args.insert("port".to_string(), Value::Str("src_port".to_string()));
args.insert("saddrInto".into(), Value::Str("src_ip".into()));
args.insert("port".into(), Value::Str("src_port".into()));
let mut record = HashMap::with_capacity(3);
record.insert("src_ip".to_string(), Value::Str("prox".to_string()));
record.insert("src_port".to_string(), Value::Str("1234".to_string()));
record.insert("dest_ip".to_string(), Value::Str("serv".to_string()));
record.insert("src_ip".into(), Value::Str("prox".into()));
record.insert("src_port".into(), Value::Str("1234".into()));
record.insert("dest_ip".into(), Value::Str("serv".into()));
let expected = record.clone();
let mut action = DnatReplace::from_args(args, singleton_share!(mappings));
action.act(&mut record).unwrap();
@ -182,39 +184,39 @@ mod tests {
mappings: vec!(sample_dnat_mapping()),
});
let mut args = HashMap::with_capacity(2);
args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string()));
args.insert("port".to_string(), Value::Str("src_port".to_string()));
args.insert("saddrInto".into(), Value::Str("src_ip".into()));
args.insert("port".into(), Value::Str("src_port".into()));
let mut record = HashMap::with_capacity(3);
record.insert("src_ip".to_string(), Value::Str("prox".to_string()));
record.insert("src_port".to_string(), Value::Int(12345));
record.insert("dest_ip".to_string(), Value::Str("serv".to_string()));
record.insert("src_ip".into(), Value::Str("prox".into()));
record.insert("src_port".into(), Value::Int(12345));
record.insert("dest_ip".into(), Value::Str("serv".into()));
let mut action = DnatReplace::from_args(args, singleton_share!(mappings));
action.act(&mut record).unwrap();
assert_eq!(3, record.len());
assert_eq!(Some(&Value::Str("bad".to_string())), record.get("src_ip"));
assert_eq!(Some(&Value::Str("bad".into())), record.get("src_ip"));
assert_eq!(Some(&Value::Int(12345)), record.get("src_port"));
assert_eq!(Some(&Value::Str("serv".to_string())), record.get("dest_ip"));
assert_eq!(Some(&Value::Str("serv".into())), record.get("dest_ip"));
}
fn mapping_getter_identification() -> DnatMapping {
DnatMapping {
src_addr: Some("sa".to_string()),
src_port: Some("sp".to_string()),
internal_addr: Some("ia".to_string()),
internal_port: Some("ip".to_string()),
dest_addr: Some("da".to_string()),
dest_port: Some("dp".to_string()),
src_addr: Some("sa".into()),
src_port: Some("sp".into()),
internal_addr: Some("ia".into()),
internal_port: Some("ip".into()),
dest_addr: Some("da".into()),
dest_port: Some("dp".into()),
keep_until: Utc::now(),
}
}
fn sample_dnat_mapping() -> DnatMapping {
DnatMapping {
src_addr: Some("bad".to_string()),
src_addr: Some("bad".into()),
src_port: None,
internal_addr: Some("prox".to_string()),
internal_port: Some("12345".to_string()),
dest_addr: Some("serv".to_string()),
internal_addr: Some("prox".into()),
internal_port: Some("12345".into()),
dest_addr: Some("serv".into()),
dest_port: None,
keep_until: Utc::now() + Duration::hours(1),
}

View File

@ -1,4 +1,6 @@
use crate::domain::{Action, EmailData, EmailPort, ModuleArgs, Record, Singleton, Template, Value};
use crate::domain::{
Action, EmailData, EmailPort, Error, ModuleArgs, Record, Singleton, Template, Value,
};
use crate::singleton_borrow;
pub struct Email {
@ -11,7 +13,7 @@ impl Email {
pub fn from_args(mut args: ModuleArgs, mailer: Singleton<dyn EmailPort>) -> Email {
let subject = match args.remove("subject") {
Some(Value::Str(s)) => s,
_ => "Pyruse Notification".to_string(),
_ => "Pyruse Notification".into(),
};
let email = EmailData::new(subject);
let template = match args.remove("message") {
@ -33,7 +35,7 @@ impl Email {
}
impl Action for Email {
fn act(&mut self, record: &mut Record) -> Result<(), ()> {
fn act(&mut self, record: &mut Record) -> Result<(), Error> {
singleton_borrow!(self.mailer).send(self.clone_email(record))
}
}

View File

@ -1,4 +1,6 @@
use crate::domain::{Action, LogMessage, LogPort, ModuleArgs, Record, Singleton, Template, Value};
use crate::domain::{
Action, Error, LogMessage, LogPort, ModuleArgs, Record, Singleton, Template, Value,
};
use crate::singleton_borrow;
type LogFormat = fn(&str) -> LogMessage;
@ -50,7 +52,7 @@ impl Log {
}
impl Action for Log {
fn act(&mut self, record: &mut Record) -> Result<(), ()> {
fn act(&mut self, record: &mut Record) -> Result<(), Error> {
let message = self.template.format(record);
singleton_borrow!(self.logger).write((self.log_format)(&message))
}
@ -60,7 +62,7 @@ impl Action for Log {
mod tests {
use super::Log;
use crate::domain::test_util::FakeLog;
use crate::domain::{Action, ModuleArgs, Record, Singleton, Value};
use crate::domain::{Action, Error, ModuleArgs, Record, Singleton, Value};
use crate::{assert_log_match, singleton_new, singleton_share};
use core::panic;
use std::collections::HashMap;
@ -131,13 +133,13 @@ mod tests {
fn create_log_logger_record(
template: &str,
level: Option<&str>,
logs: Vec<Result<Record, ()>>,
logs: Vec<Result<Record, Error>>,
vars: Vec<(&str, &str)>,
) -> (Log, Singleton<FakeLog>, Record) {
let mut args: ModuleArgs = HashMap::new();
args.insert("message".to_string(), Value::Str(template.to_string()));
args.insert("message".into(), Value::Str(template.to_string()));
if let Some(l) = level {
args.insert("level".to_string(), Value::Str(l.to_string()));
args.insert("level".into(), Value::Str(l.to_string()));
}
let logger = singleton_new!(FakeLog::new(logs));
let log = Log::from_args(args, singleton_share!(logger));

View File

@ -10,6 +10,8 @@ mod email;
pub use self::email::*;
mod log;
pub use self::log::*;
mod netfilter_ban;
pub use self::netfilter_ban::*;
mod noop;
pub use self::noop::*;

View File

@ -0,0 +1,104 @@
use crate::domain::{Action, Error, ModuleArgs, Record, Singleton, Value};
use chrono::{DateTime, Duration, Utc};
pub trait NetfilterBackendPort {
fn set_ban<'s, 'r>(
&'s mut self,
nf_set: &'s str,
ip: &'r str,
ban_until: &'s Option<DateTime<Utc>>,
) -> Result<(), Error>;
fn cancel_ban<'s, 'r>(&'s mut self, nf_set: &'s str, ip: &'r str) -> Result<(), Error>;
}
pub trait NetfilterStoragePort {
fn store_and_remove_obsoletes<'s, 'r>(
&'s mut self,
nf_set: &'s str,
ip: &'r str,
ban_until: &'s Option<DateTime<Utc>>,
) -> Result<bool, Error>;
}
pub struct NetfilterBan {
backend: Singleton<dyn NetfilterBackendPort>,
storage: Singleton<dyn NetfilterStoragePort>,
ipv4_set: String,
ipv6_set: String,
field: String,
ban_seconds: Option<usize>,
}
impl NetfilterBan {
pub fn from_args(
mut args: ModuleArgs,
module_alias: &str,
ipv4_arg_name: &str,
ipv6_arg_name: &str,
backend: Singleton<dyn NetfilterBackendPort>,
storage: Singleton<dyn NetfilterStoragePort>,
) -> NetfilterBan {
let ipv4_set = match args.remove(ipv4_arg_name) {
Some(Value::Str(s)) => s,
_ => panic!(
"The {} action needs an IPv4 set name in “{}”",
module_alias, ipv4_arg_name
),
};
let ipv6_set = match args.remove(ipv6_arg_name) {
Some(Value::Str(s)) => s,
_ => panic!(
"The {} action needs an IPv6 set name in “{}”",
module_alias, ipv4_arg_name
),
};
let field = match args.remove("IP") {
Some(Value::Str(s)) => s,
_ => panic!(
"The {} action needs a field to read the IP address from, in “IP”",
module_alias
),
};
let ban_seconds = match args.remove("banSeconds") {
Some(Value::Int(i)) => Some(i as usize),
_ => None,
};
NetfilterBan {
backend,
storage,
ipv4_set,
ipv6_set,
field,
ban_seconds,
}
}
}
impl Action for NetfilterBan {
fn act(&mut self, record: &mut Record) -> Result<(), Error> {
if let Some(Value::Str(ip)) = record.get(&self.field) {
let set = if ip.contains(':') {
&self.ipv6_set
} else {
&self.ipv4_set
};
let ban_until = self
.ban_seconds
.map(|s| Utc::now() + Duration::seconds(s as i64));
if self
.storage
.borrow_mut()
.store_and_remove_obsoletes(set, ip, &ban_until)?
{
// should not happen, since the IP is banned…
self
.backend
.borrow_mut()
.cancel_ban(set, ip)
.unwrap_or_default(); // if too late: not a problem
}
self.backend.borrow_mut().set_ban(set, ip, &ban_until)
} else {
Ok(())
}
}
}

View File

@ -1,4 +1,4 @@
use crate::domain::{Action, ModuleArgs, Record};
use crate::domain::{Action, Error, ModuleArgs, Record};
pub struct Noop {}
@ -9,7 +9,7 @@ impl Noop {
}
impl Action for Noop {
fn act(&mut self, _record: &mut Record) -> Result<(), ()> {
fn act(&mut self, _record: &mut Record) -> Result<(), Error> {
Ok(())
}
}

View File

@ -143,7 +143,7 @@ mod tests {
#[test]
fn augment_raises_a_counter_by_its_amount() {
let (_, mut counters) = get_store_counters();
let str_value = Value::Str("string".to_string());
let str_value = Value::Str("string".into());
counters.set(("test", &str_value), (4, None));
let value = counters.augment(("test", &str_value), (3, None));
assert_eq!(value, 7);

View File

@ -1,4 +1,6 @@
use super::Error;
use std::convert::TryFrom;
use std::string::ToString;
#[derive(Clone)]
pub struct EmailData {
@ -17,15 +19,16 @@ impl EmailData {
}
pub trait EmailPort {
fn send(&mut self, email: EmailData) -> Result<(), ()>;
fn send(&mut self, email: EmailData) -> Result<(), Error>;
}
#[derive(Debug, PartialEq, Eq)]
pub struct EmailAddress {
as_string: String,
}
impl EmailAddress {
pub fn to_string(&self) -> String {
impl ToString for EmailAddress {
fn to_string(&self) -> String {
self.as_string.clone()
}
}
@ -42,12 +45,12 @@ impl Clone for EmailAddress {
}
impl TryFrom<String> for EmailAddress {
type Error = ();
fn try_from(as_string: String) -> Result<Self, Self::Error> {
type Error = Error;
fn try_from(as_string: String) -> Result<Self, Error> {
if is_address_valid(&as_string) {
Ok(EmailAddress { as_string })
} else {
Err(())
Err(format!("Email {} is invalid", as_string).into())
}
}
}
@ -447,6 +450,9 @@ mod tests {
}
fn assert_invalid(addr: &str) {
assert_eq!(Err(()), EmailAddress::try_from(addr.to_string()));
assert_eq!(
Err(format!("Email {} is invalid", addr).into()),
EmailAddress::try_from(addr.to_string())
);
}
}

View File

@ -1,4 +1,4 @@
use crate::domain::Record;
use crate::domain::{Error, Record};
pub enum LogMessage<'t> {
EMERG(&'t str),
@ -12,6 +12,6 @@ pub enum LogMessage<'t> {
}
pub trait LogPort {
fn read_next(&mut self) -> Result<Record, ()>;
fn write(&mut self, message: LogMessage) -> Result<(), ()>;
fn read_next(&mut self) -> Result<Record, Error>;
fn write(&mut self, message: LogMessage) -> Result<(), Error>;
}

View File

@ -20,7 +20,9 @@ pub use self::workflow::*;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::convert::From;
use std::hash::{Hash, Hasher};
use std::string::ToString;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Value {
@ -45,6 +47,18 @@ impl Hash for Value {
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Error {
pub message: String,
}
impl<T: ToString> From<T> for Error {
fn from(err: T) -> Self {
Error {
message: err.to_string(),
}
}
}
pub type Record = HashMap<String, Value>;
pub type Singleton<T> = std::rc::Rc<std::cell::RefCell<T>>;
@ -70,4 +84,4 @@ macro_rules! singleton_borrow {
}
#[cfg(test)]
mod test_util;
pub mod test_util;

View File

@ -1,6 +1,5 @@
use crate::domain::{Record, Value};
use crate::domain::{Error, Record, Value};
use std::collections::HashMap;
pub type ActionConstructor = Box<dyn Fn(ModuleArgs) -> Box<dyn Action>>;
pub type FilterConstructor = Box<dyn Fn(ModuleArgs) -> Box<dyn Filter>>;
@ -32,29 +31,26 @@ pub enum Module {
}
impl Module {
pub fn new(name: String, args: ModuleArgs, available: &Modules) -> Result<Module, ()> {
if let Some(a) = available.available_actions.get(&name) {
pub fn new(name: &str, args: ModuleArgs, available: &Modules) -> Result<Module, Error> {
if let Some(a) = available.available_actions.get(name) {
Ok(Module::Action(a(args)))
} else if let Some(f) = available.available_filters.get(&name) {
} else if let Some(f) = available.available_filters.get(name) {
Ok(Module::Filter(f(args)))
} else {
Err(())
Err(format!("Module {} does not exist", name).into())
}
}
pub fn run(&mut self, record: &mut Record) -> Result<bool, ()> {
pub fn run(&mut self, record: &mut Record) -> Result<bool, Error> {
match self {
Module::Action(a) => match a.act(record) {
Ok(()) => Ok(true),
Err(()) => Err(()),
},
Module::Action(a) => a.act(record).map(|_| true),
Module::Filter(f) => Ok(f.filter(record)),
}
}
}
pub trait Action {
fn act(&mut self, record: &mut Record) -> Result<(), ()>;
fn act(&mut self, record: &mut Record) -> Result<(), Error>;
}
pub trait Filter {
@ -77,10 +73,10 @@ mod tests {
let mut record: Record = HashMap::new();
// When
let mut module = Module::new(ACT_NAME.to_string(), HashMap::new(), &mods).unwrap();
let mut module = Module::new(ACT_NAME, HashMap::new(), &mods).unwrap();
// Then
assert_eq!(module.run(&mut record), Ok(true));
assert_eq!(Ok(true), module.run(&mut record));
assert!(record.contains_key(ACT_NAME));
assert_eq!(record[ACT_NAME], Value::Int(1));
}
@ -93,10 +89,10 @@ mod tests {
let mut record: Record = HashMap::new();
// When
let mut module = Module::new(FLT_NAME.to_string(), HashMap::new(), &mods).unwrap();
let mut module = Module::new(FLT_NAME, HashMap::new(), &mods).unwrap();
// Then
assert_eq!(module.run(&mut record), Ok(false));
assert_eq!(Ok(false), module.run(&mut record));
assert!(record.contains_key(FLT_NAME));
assert_eq!(record[FLT_NAME], Value::Int(1));
}

View File

@ -51,34 +51,34 @@ mod tests {
#[test]
fn template_without_placeholder_is_rendered_as_is() {
let template = Template::new("x".to_string());
let template = Template::new("x".into());
let result = template.format(&HashMap::new());
assert_eq!("x", &result);
}
#[test]
fn placeholder_without_variable_is_rendered_as_is() {
let template = Template::new("x{y}z".to_string());
let template = Template::new("x{y}z".into());
let result = template.format(&HashMap::new());
assert_eq!("x{y}z", &result);
}
#[test]
fn placeholder_with_variable_is_replaced() {
let template = Template::new("x{y}z".to_string());
let template = Template::new("x{y}z".into());
let mut record = HashMap::new();
record.insert("y".to_string(), Value::Str("y".to_string()));
record.insert("y".into(), Value::Str("y".into()));
let result = template.format(&record);
assert_eq!("xyz", &result);
}
#[test]
fn all_variables_are_replaced() {
let template = Template::new("{x}{a}{yy}-{zzz}".to_string());
let template = Template::new("{x}{a}{yy}-{zzz}".into());
let mut record = HashMap::new();
record.insert("x".to_string(), Value::Str("x".to_string()));
record.insert("yy".to_string(), Value::Str("y".to_string()));
record.insert("zzz".to_string(), Value::Str("z".to_string()));
record.insert("x".into(), Value::Str("x".into()));
record.insert("yy".into(), Value::Str("y".into()));
record.insert("zzz".into(), Value::Str("z".into()));
let result = template.format(&record);
assert_eq!("x{a}y-z", &result);
}

View File

@ -1,17 +1,30 @@
use crate::domain::{
Action, CounterData, CounterRef, CountersPort, DnatMapping, DnatMappingsPort, Filter, LogMessage,
LogPort, Record, Singleton, Value,
Action, Chain, Config, CounterData, CounterRef, CountersPort, DnatMapping, DnatMappingsPort,
Error, Filter, LogMessage, LogPort, Record, Singleton, Value,
};
use std::collections::HashMap;
use indexmap::IndexMap;
use std::{collections::HashMap, io::Write};
pub const ACT_NAME: &str = "fake_action";
pub const FLT_NAME: &str = "fake_filter";
impl Config {
pub fn new(
actions: Option<IndexMap<String, Chain>>,
options: Option<HashMap<String, Value>>,
) -> Config {
Config {
actions: actions.unwrap_or(IndexMap::new()),
options: options.unwrap_or(HashMap::new()),
}
}
}
pub struct FakeAction {}
impl Action for FakeAction {
fn act(&mut self, record: &mut Record) -> Result<(), ()> {
let v = record.get(ACT_NAME).unwrap_or(&Value::Int(0));
fn act(&mut self, record: &mut Record) -> Result<(), Error> {
let v = record.get(ACT_NAME).unwrap_or(&Value::Int(0)).clone();
match v {
Value::Int(i) => record.insert(String::from(ACT_NAME), Value::Int(i + 1)),
_ => panic!("The record did not contain the expected value."),
@ -24,7 +37,7 @@ pub struct FakeFilter {}
impl Filter for FakeFilter {
fn filter(&mut self, record: &mut Record) -> bool {
let v = record.get(FLT_NAME).unwrap_or(&Value::Int(0));
let v = record.get(FLT_NAME).unwrap_or(&Value::Int(0)).clone();
match v {
Value::Int(i) => record.insert(String::from(FLT_NAME), Value::Int(i + 1)),
_ => panic!("The record did not contain the expected value."),
@ -34,12 +47,12 @@ impl Filter for FakeFilter {
}
pub struct FakeLog {
pub wanted_next: Vec<Result<Record, ()>>,
pub wanted_next: Vec<Result<Record, Error>>,
pub last_write: Option<(String, String)>,
}
impl FakeLog {
pub fn new(wanted_next: Vec<Result<Record, ()>>) -> FakeLog {
pub fn new(wanted_next: Vec<Result<Record, Error>>) -> FakeLog {
FakeLog {
wanted_next,
last_write: None,
@ -48,24 +61,24 @@ impl FakeLog {
}
impl LogPort for FakeLog {
fn read_next(&mut self) -> Result<Record, ()> {
fn read_next(&mut self) -> Result<Record, Error> {
if self.wanted_next.is_empty() {
Err(())
Err("ERROR!".into())
} else {
self.wanted_next.remove(0)
}
}
fn write(&mut self, message: LogMessage) -> Result<(), ()> {
fn write(&mut self, message: LogMessage) -> Result<(), Error> {
self.last_write = match message {
LogMessage::EMERG(m) => Some(("EMERG".to_string(), m.to_string())),
LogMessage::ALERT(m) => Some(("ALERT".to_string(), m.to_string())),
LogMessage::CRIT(m) => Some(("CRIT".to_string(), m.to_string())),
LogMessage::ERR(m) => Some(("ERR".to_string(), m.to_string())),
LogMessage::WARNING(m) => Some(("WARNING".to_string(), m.to_string())),
LogMessage::NOTICE(m) => Some(("NOTICE".to_string(), m.to_string())),
LogMessage::INFO(m) => Some(("INFO".to_string(), m.to_string())),
LogMessage::DEBUG(m) => Some(("DEBUG".to_string(), m.to_string())),
LogMessage::EMERG(m) => Some(("EMERG".into(), m.into())),
LogMessage::ALERT(m) => Some(("ALERT".into(), m.into())),
LogMessage::CRIT(m) => Some(("CRIT".into(), m.into())),
LogMessage::ERR(m) => Some(("ERR".into(), m.into())),
LogMessage::WARNING(m) => Some(("WARNING".into(), m.into())),
LogMessage::NOTICE(m) => Some(("NOTICE".into(), m.into())),
LogMessage::INFO(m) => Some(("INFO".into(), m.into())),
LogMessage::DEBUG(m) => Some(("DEBUG".into(), m.into())),
};
Ok(())
}
@ -107,3 +120,21 @@ impl DnatMappingsPort for FakeDnatMappings {
self.mappings.iter().collect()
}
}
pub struct WriteProxy<'t, W: Write> {
inner: &'t mut W,
}
impl<'t, W: Write> WriteProxy<'t, W> {
pub fn new<'x>(inner: &'x mut W) -> WriteProxy<'x, W> {
WriteProxy { inner }
}
}
impl<'t, W: Write> Write for WriteProxy<'t, W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}

View File

@ -111,7 +111,7 @@ fn build_chain(
let name = chain_name
.clone()
.add(&format!("[{}]:{}", index, &step.module));
let module = Module::new(step.module, step.args, available)
let module = Module::new(&step.module, step.args, available)
.expect(&format!("Module {} could not be created.", &name));
let then_dest = step
.then_dest
@ -172,10 +172,7 @@ mod tests {
#[should_panic(expected = "A configuration must have at least one module.")]
fn empty_config_results_in_panic() {
// Given
let mut conf = Config {
actions: IndexMap::new(),
options: HashMap::new(),
};
let mut conf = Config::new(None, None);
let mods = Modules::new();
// When
@ -187,7 +184,7 @@ mod tests {
// Given
let mut actions: IndexMap<String, Chain> = IndexMap::new();
actions.insert(
"chain1".to_string(),
"chain1".into(),
vec![Step {
module: ACT_NAME.to_string(),
args: HashMap::new(),
@ -195,10 +192,7 @@ mod tests {
else_dest: None,
}],
);
let mut conf = Config {
actions,
options: HashMap::new(),
};
let mut conf = Config::new(Some(actions), None);
let mut mods = Modules::new();
mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {})));
let mut record: Record = HashMap::new();
@ -221,16 +215,16 @@ mod tests {
// Given
let mut actions: IndexMap<String, Chain> = IndexMap::new();
actions.insert(
"chain1".to_string(),
"chain1".into(),
vec![Step {
module: FLT_NAME.to_string(),
args: HashMap::new(),
then_dest: None,
else_dest: Some("chain2".to_string()),
else_dest: Some("chain2".into()),
}],
);
actions.insert(
"chain2".to_string(),
"chain2".into(),
vec![Step {
module: ACT_NAME.to_string(),
args: HashMap::new(),
@ -238,10 +232,7 @@ mod tests {
else_dest: None,
}],
);
let mut conf = Config {
actions,
options: HashMap::new(),
};
let mut conf = Config::new(Some(actions), None);
let mut mods = Modules::new();
mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {})));
mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {})));
@ -268,7 +259,7 @@ mod tests {
// Given
let mut actions: IndexMap<String, Chain> = IndexMap::new();
actions.insert(
"chain1".to_string(),
"chain1".into(),
vec![Step {
module: FLT_NAME.to_string(),
args: HashMap::new(),
@ -277,7 +268,7 @@ mod tests {
}],
);
actions.insert(
"chain2".to_string(),
"chain2".into(),
vec![Step {
module: ACT_NAME.to_string(),
args: HashMap::new(),
@ -285,10 +276,7 @@ mod tests {
else_dest: None,
}],
);
let mut conf = Config {
actions,
options: HashMap::new(),
};
let mut conf = Config::new(Some(actions), None);
let mut mods = Modules::new();
mods.register_action(ACT_NAME.to_string(), Box::new(|_| Box::new(FakeAction {})));
mods.register_filter(FLT_NAME.to_string(), Box::new(|_| Box::new(FakeFilter {})));

View File

@ -1,80 +1,102 @@
use super::SerdeConfigAdapter;
use crate::infra::file::DataFile;
use std::convert::TryFrom;
use std::env;
use std::ffi::OsString;
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
const ENV_VARIABLE: &'static str = "PYRUSE_CONF";
const ETC_PATH: &'static str = "/etc/pyruse";
pub const ETC_PATH: &'static str = "/etc/pyruse";
pub enum ConfFile {
Json(OsString),
Yaml(OsString),
pub fn configuration_from_filesystem(conf_dir: &str) -> DataFile {
find_file(find_candidates(conf_dir))
}
impl ConfFile {
pub fn from_filesystem() -> ConfFile {
find_file(find_candidates())
}
pub fn to_config(self) -> SerdeConfigAdapter {
match self {
ConfFile::Json(path) => {
SerdeConfigAdapter::from_json(BufReader::new(File::open(path).expect("Read error")))
}
ConfFile::Yaml(path) => {
SerdeConfigAdapter::from_yaml(BufReader::new(File::open(path).expect("Read error")))
}
}
}
}
fn find_candidates() -> Vec<ConfFile> {
fn find_candidates(conf_dir: &str) -> Vec<DataFile> {
match env::var_os(ENV_VARIABLE) {
Some(path) => {
let s = Path::new(&path)
.extension()
.and_then(|e| Some(e.to_string_lossy()))
.and_then(|s| Some(s.to_ascii_lowercase()))
.unwrap_or_default();
match s.as_ref() {
"json" => vec![ConfFile::Json(path)],
"yaml" | "yml" => vec![ConfFile::Yaml(path)],
_ => panic!(
"Cannot determine file format from file name: {}",
path.to_string_lossy()
),
}
}
Some(path) => vec![DataFile::try_from(path).unwrap()],
None => {
let cwd = env::current_dir().expect("Error accessing the current working directory");
let add_file: fn(&PathBuf, &str) -> OsString = |c, f| {
let mut c2 = c.clone();
c2.push(f); // not a fluent API…
c2.into_os_string()
};
vec![
ConfFile::Json(add_file(&cwd, "pyruse.yml")),
ConfFile::Yaml(add_file(&cwd, "pyruse.yaml")),
ConfFile::Yaml(add_file(&cwd, "pyruse.yml")),
ConfFile::Json(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.json"))),
ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yaml"))),
ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yml"))),
DataFile::try_from((&cwd, "pyruse.json")).unwrap(),
DataFile::try_from((&cwd, "pyruse.yaml")).unwrap(),
DataFile::try_from((&cwd, "pyruse.yml")).unwrap(),
DataFile::try_from(format!("{}/{}", conf_dir, "pyruse.json").as_ref()).unwrap(),
DataFile::try_from(format!("{}/{}", conf_dir, "pyruse.yaml").as_ref()).unwrap(),
DataFile::try_from(format!("{}/{}", conf_dir, "pyruse.yml").as_ref()).unwrap(),
]
}
}
}
fn find_file(conf_candidates: Vec<ConfFile>) -> ConfFile {
fn find_file(conf_candidates: Vec<DataFile>) -> DataFile {
for name in conf_candidates {
match name {
ConfFile::Json(ref path) | ConfFile::Yaml(ref path) => {
if Path::new(&path).exists() {
return name;
}
}
if name.exists() {
return name;
}
}
panic!("No configuration found. Consider setting ${}, or creating one of these in $PWD: pyruse.json, pyruse.yaml or pyruse.yml", ENV_VARIABLE)
}
#[macro_use]
#[cfg(test)]
mod tests {
use super::{configuration_from_filesystem, ENV_VARIABLE};
use crate::infra::file::DataFile;
use crate::test_with_exclusive_env_access;
use lazy_static::lazy_static;
use std::convert::TryFrom;
use std::env;
use std::fs;
use std::panic;
use std::sync::Mutex;
lazy_static! {
static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
}
test_with_exclusive_env_access! {
fn if_envvar_is_set_then_the_configuration_comes_from_there() {
let mut temp = env::temp_dir();
temp.push("pyruse-conf.yaml");
let filename = temp.to_str().unwrap();
env::set_var(ENV_VARIABLE, filename);
fs::File::create(filename).unwrap();
assert_eq!(
DataFile::try_from(filename).unwrap(),
configuration_from_filesystem("")
);
fs::remove_file(filename).unwrap();
}
}
test_with_exclusive_env_access! {
fn if_envvar_is_unset_then_the_configuration_is_found_in_search_dir() {
env::set_current_dir("/").unwrap();
env::remove_var(ENV_VARIABLE);
let tempdir = env::temp_dir();
let tempfile = tempdir.join("pyruse.yml");
let dirname = tempdir.to_str().unwrap();
let filename = tempfile.to_str().unwrap();
fs::File::create(filename).unwrap();
assert_eq!(
DataFile::try_from(filename).unwrap(),
configuration_from_filesystem(dirname)
);
fs::remove_file(filename).unwrap();
}
}
// https://github.com/rust-lang/rust/issues/43155#issuecomment-315543432
// Environment variables and current working directory are for all tests “simultaneously”.
#[macro_export]
macro_rules! test_with_exclusive_env_access {
(fn $name:ident() $body:block) => {
#[test]
fn $name() {
let guard = ENV_MUTEX.lock().unwrap();
if let Err(e) = panic::catch_unwind(|| $body) {
drop(guard);
panic::resume_unwind(e);
}
}
};
}
}

View File

@ -1,9 +1,9 @@
use crate::domain::{Chain, Config, ConfigPort, ModuleArgs, Step, Value};
use crate::infra::file::DataFile;
use crate::infra::serde::Input;
use indexmap::IndexMap;
use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
use serde::Deserialize;
use serde_json;
use serde_yaml;
use std::collections::HashMap;
use std::fmt;
use std::io::Read;
@ -15,45 +15,48 @@ pub struct SerdeConfigAdapter {
config: Config,
}
impl SerdeConfigAdapter {
pub fn from_json(data: impl Read) -> SerdeConfigAdapter {
let serde_config: SerdeConfig =
serde_json::from_reader(data).expect("Failed to parse configuration");
SerdeConfigAdapter::from_serde(serde_config)
}
pub fn from_yaml(data: impl Read) -> SerdeConfigAdapter {
let serde_config: SerdeConfig =
serde_yaml::from_reader(data).expect("Failed to parse configuration");
SerdeConfigAdapter::from_serde(serde_config)
impl From<DataFile> for SerdeConfigAdapter {
fn from(datafile: DataFile) -> Self {
datafile
.open_r()
.expect("Failed to read the configuration file")
.into()
}
}
fn from_serde(mut data: SerdeConfig) -> SerdeConfigAdapter {
SerdeConfigAdapter {
config: Config {
actions: data
.actions
.drain(..)
.map(|(name, mut serde_chain)| {
(
name,
serde_chain
.drain(..)
.map(|step| Step {
module: match step.module {
StepType::Action(s) => s,
StepType::Filter(s) => s,
},
args: step.args,
then_dest: step.then_dest,
else_dest: step.else_dest,
})
.collect::<Chain>(),
)
})
.collect::<IndexMap<String, Chain>>(),
options: data.options,
},
}
impl<R: Read> From<Input<R>> for SerdeConfigAdapter {
fn from(data: Input<R>) -> Self {
let serde_conf = data.parse().expect("Failed to parse configuration");
from_serde(serde_conf)
}
}
fn from_serde(mut data: SerdeConfig) -> SerdeConfigAdapter {
SerdeConfigAdapter {
config: Config {
actions: data
.actions
.drain(..)
.map(|(name, mut serde_chain)| {
(
name,
serde_chain
.drain(..)
.map(|step| Step {
module: match step.module {
StepType::Action(s) => s,
StepType::Filter(s) => s,
},
args: step.args,
then_dest: step.then_dest,
else_dest: step.else_dest,
})
.collect::<Chain>(),
)
})
.collect::<IndexMap<String, Chain>>(),
options: data.options,
},
}
}
@ -198,7 +201,7 @@ impl<'de> Visitor<'de> for ValueVisitor {
where
E: de::Error,
{
Ok(Value::Str(v.to_string()))
Ok(Value::Str(v.into()))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
@ -229,7 +232,7 @@ impl<'de> Visitor<'de> for ValueVisitor {
Ok(Value::Str(
std::str::from_utf8(v)
.expect("Strings in the configuration must be UTF-8")
.to_string(),
.into(),
))
}
@ -240,7 +243,7 @@ impl<'de> Visitor<'de> for ValueVisitor {
Ok(Value::Str(
std::str::from_utf8(v)
.expect("Strings in the configuration must be UTF-8")
.to_string(),
.into(),
))
}
@ -290,7 +293,8 @@ impl<'de> Deserialize<'de> for Value {
#[cfg(test)]
mod tests {
use super::SerdeConfigAdapter;
use crate::domain::Value;
use crate::domain::{Config, Value};
use crate::infra::serde::Input;
#[test]
fn parse_json_works() {
@ -321,9 +325,48 @@ mod tests {
"#.as_bytes();
// When
let conf = SerdeConfigAdapter::from_json(json).config;
let conf = SerdeConfigAdapter::from(Input::Json(json)).config;
// Then
assert_right_contents(conf);
}
#[test]
fn parse_yaml_works() {
// Given
let yaml = r#"---
actions:
Detect request errors with Nextcloud:
- filter: filter_equals
args:
field: SYSLOG_IDENTIFIER
value: uwsgi
- filter: filter_pcre
args:
field: MESSAGE
re: "^\\[[^]]+\\] ([^ ]+) .*\\] ([A-Z]+ /[^?]*)(?:\\?.*)? => .*\\(HTTP/1.1 5..\\)"
save:
- thatIP
- HTTPrequest
else: "… Report insufficient buffer-size for Nextcloud QUERY_STRING"
- action: action_dailyReport
args:
level: INFO
message: IP {thatIP} failed to {HTTPrequest} on Nextcloud
details: FIRSTLAST
"… Report insufficient buffer-size for Nextcloud QUERY_STRING": []
debug: false
"#
.as_bytes();
// When
let conf = SerdeConfigAdapter::from(Input::Yaml(yaml)).config;
// Then
assert_right_contents(conf);
}
fn assert_right_contents(conf: Config) {
assert_eq!(conf.actions.len(), 2);
assert_eq!(
conf.actions.get_index(0).unwrap().0,

View File

@ -64,7 +64,7 @@ mod tests {
fn modify_allows_modifying_an_entry_and_returns_the_new_value() {
let mut counters: HashMap<String, CounterKeys> = HashMap::new();
let counter = "counter";
let key = Value::Str("1.2.3.4".to_string());
let key = Value::Str("1.2.3.4".into());
let new_data = (2, None);
counters.insert(counter.to_string(), HashMap::new());
counters
@ -87,7 +87,7 @@ mod tests {
fn after_remove_the_entry_is_not_there() {
let mut counters: HashMap<String, CounterKeys> = HashMap::new();
let counter = "counter";
let key1 = Value::Str("1.2.3.4".to_string());
let key1 = Value::Str("1.2.3.4".into());
let key2 = Value::Bool(true);
let data1 = (2, None);
let data2 = (5, None);
@ -109,7 +109,7 @@ mod tests {
fn remove_on_unexisting_entry_does_nothing_and_returns_none() {
let mut counters: HashMap<String, CounterKeys> = HashMap::new();
let counter = "counter";
let key1 = Value::Str("1.2.3.4".to_string());
let key1 = Value::Str("1.2.3.4".into());
let key2 = Value::Bool(true);
let data1 = (2, None);
counters.insert(counter.to_string(), HashMap::new());
@ -129,7 +129,7 @@ mod tests {
fn after_last_key_is_removed_by_remove_counter_is_also_removed() {
let mut counters: HashMap<String, CounterKeys> = HashMap::new();
let counter = "counter";
let key1 = Value::Str("1.2.3.4".to_string());
let key1 = Value::Str("1.2.3.4".into());
let key2 = Value::Bool(true);
let data1 = (2, None);
let data2 = (5, None);
@ -147,7 +147,7 @@ mod tests {
fn removeif_removes_entries_that_match_the_predicate() {
let mut counters: HashMap<String, CounterKeys> = HashMap::new();
let counter = "counter";
let key1 = Value::Str("1.2.3.4".to_string());
let key1 = Value::Str("1.2.3.4".into());
let key2 = Value::Bool(true);
let data1 = (2, None);
let data2 = (5, None);
@ -168,7 +168,7 @@ mod tests {
fn after_last_key_is_removed_by_removeif_counter_is_also_removed() {
let mut counters: HashMap<String, CounterKeys> = HashMap::new();
let counter = "counter";
let key1 = Value::Str("1.2.3.4".to_string());
let key1 = Value::Str("1.2.3.4".into());
let data1 = (2, None);
counters.insert(counter.to_string(), HashMap::new());
let map = counters.get_mut(counter).unwrap();

View File

@ -27,3 +27,27 @@ impl DnatMappingsPort for InMemoryDnatMappingsAdapter {
self.mappings.iter().collect()
}
}
#[cfg(test)]
mod tests {
use crate::domain::{DnatMapping, DnatMappingsPort};
use crate::infra::dnat::InMemoryDnatMappingsAdapter;
use chrono::Utc;
#[test]
fn keep_until_is_taken_into_account() {
let already_past = Utc::now();
let mut mappings = InMemoryDnatMappingsAdapter::new();
let mapping = DnatMapping {
src_addr: None,
src_port: None,
internal_addr: None,
internal_port: None,
dest_addr: None,
dest_port: None,
keep_until: already_past,
};
mappings.put(mapping);
assert_eq!(Vec::<&DnatMapping>::new(), mappings.get_all());
}
}

View File

@ -1,4 +1,4 @@
use crate::domain::{Config, EmailAddress, EmailData, EmailPort, Value};
use crate::domain::{Config, EmailAddress, EmailData, EmailPort, Error, Value};
use chrono::Utc;
use lettre_email::{mime::Mime, Header, MimeMultipartType, PartBuilder};
use std::{
@ -25,13 +25,13 @@ pub struct ProcessEmailAdapter {
impl ProcessEmailAdapter {
pub fn new(conf: &Config) -> ProcessEmailAdapter {
let mut default_options = HashMap::new();
default_options.insert("from".to_string(), Value::Str(DEFAULT_FROM.to_string()));
default_options.insert("from".into(), Value::Str(DEFAULT_FROM.to_string()));
default_options.insert(
"to".to_string(),
"to".into(),
Value::List(vec![Value::Str(DEFAULT_TO.to_string())]),
);
default_options.insert(
"sendmail".to_string(),
"sendmail".into(),
Value::List(
DEFAULT_SENDMAIL
.iter()
@ -88,16 +88,13 @@ impl ProcessEmailAdapter {
}
impl EmailPort for ProcessEmailAdapter {
fn send(&mut self, email: EmailData) -> Result<(), ()> {
fn send(&mut self, email: EmailData) -> Result<(), Error> {
let mut main_part = PartBuilder::new()
.header(Header::new("From".to_string(), self.from.to_string()))
.header(Header::new("From".into(), self.from.to_string()))
.header(Header::new("Return-Path".into(), self.from.to_string()))
.header(Header::new("Date".into(), Utc::now().to_rfc2822()))
.header(Header::new(
"Return-Path".to_string(),
self.from.to_string(),
))
.header(Header::new("Date".to_string(), Utc::now().to_rfc2822()))
.header(Header::new(
"To".to_string(),
"To".into(),
self
.to
.iter()
@ -106,7 +103,7 @@ impl EmailPort for ProcessEmailAdapter {
.join(","),
))
.header(Header::new(
"Subject".to_string(),
"Subject".into(),
format!(
"=?utf-8?Q?{}?=",
quoted_printable::encode_to_str(email.subject)
@ -117,8 +114,8 @@ impl EmailPort for ProcessEmailAdapter {
main_part = main_part.child(
PartBuilder::new()
.header(Header::new(
"Content-Transfer-Encoding".to_string(),
"QUOTED-PRINTABLE".to_string(),
"Content-Transfer-Encoding".into(),
"QUOTED-PRINTABLE".into(),
))
.body(quoted_printable::encode_to_str(text))
.content_type(&Mime::from_str("text/plain; charset=UTF-8").unwrap())
@ -129,8 +126,8 @@ impl EmailPort for ProcessEmailAdapter {
main_part = main_part.child(
PartBuilder::new()
.header(Header::new(
"Content-Transfer-Encoding".to_string(),
"QUOTED-PRINTABLE".to_string(),
"Content-Transfer-Encoding".into(),
"QUOTED-PRINTABLE".into(),
))
.body(quoted_printable::encode_to_str(html))
.content_type(&Mime::from_str("text/html; charset=UTF-8").unwrap())
@ -142,16 +139,14 @@ impl EmailPort for ProcessEmailAdapter {
for p in &self.sendmail_params {
sendmail.arg(p);
}
match sendmail.stdin(Stdio::piped()).spawn() {
Ok(mut sendmail_process) => match sendmail_process.stdin.as_mut() {
Some(stdin) => match stdin.write_all(mime_message.as_bytes()) {
Ok(_) => Ok(()),
Err(_) => Err(()),
},
None => Err(()),
},
Err(_) => Err(()),
}
sendmail
.stdin(Stdio::piped())
.spawn()?
.stdin
.as_mut()
.ok_or(Error::from("Cannot write to sendmail process"))?
.write_all(mime_message.as_bytes())
.map_err(|e| e.into())
}
}
@ -159,7 +154,6 @@ impl EmailPort for ProcessEmailAdapter {
mod tests {
use super::{ProcessEmailAdapter, DEFAULT_FROM, DEFAULT_SENDMAIL, DEFAULT_TO};
use crate::domain::{Config, EmailData, EmailPort, Value};
use indexmap::IndexMap;
use regex::Regex;
use std::collections::HashMap;
use std::env::temp_dir;
@ -167,10 +161,7 @@ mod tests {
#[test]
fn if_no_email_options_then_defaults_are_used() {
let conf = Config {
actions: IndexMap::new(),
options: HashMap::new(),
};
let conf = Config::new(None, None);
let proc = ProcessEmailAdapter::new(&conf);
let from: String = proc.from.clone().into();
let to: String = proc.to[0].clone().into();
@ -196,7 +187,7 @@ mod tests {
#[test]
fn from_email_must_be_a_string_or_default_from_is_used() {
let conf = test_config("", |o| {
o.insert("from".to_string(), Value::Int(33));
o.insert("from".into(), Value::Int(33));
});
let proc = ProcessEmailAdapter::new(&conf);
let from: String = proc.from.clone().into();
@ -207,7 +198,7 @@ mod tests {
#[should_panic(expected = "Invalid address: wrong 😱")]
fn an_email_address_must_be_valid() {
let conf = test_config("", |o| {
o.insert("from".to_string(), Value::Str("wrong 😱".to_string()));
o.insert("from".into(), Value::Str("wrong 😱".into()));
});
ProcessEmailAdapter::new(&conf);
}
@ -229,11 +220,8 @@ mod tests {
fn to_emails_must_be_strings() {
let conf = test_config("", |o| {
o.insert(
"to".to_string(),
Value::List(vec![
Value::Str("ok@example.org".to_string()),
Value::Bool(true),
]),
"to".into(),
Value::List(vec![Value::Str("ok@example.org".into()), Value::Bool(true)]),
);
});
ProcessEmailAdapter::new(&conf);
@ -242,7 +230,7 @@ mod tests {
#[test]
fn if_empty_to_list_then_default_to_is_used() {
let conf = test_config("", |o| {
o.insert("to".to_string(), Value::List(Vec::new()));
o.insert("to".into(), Value::List(Vec::new()));
});
let proc = ProcessEmailAdapter::new(&conf);
let to: String = proc.to[0].clone().into();
@ -265,7 +253,7 @@ mod tests {
#[test]
fn sendmail_command_cannot_be_an_empty_list_or_default_sendmail_is_used() {
let conf = test_config("", |o| {
o.insert("sendmail".to_string(), Value::List(Vec::new()));
o.insert("sendmail".into(), Value::List(Vec::new()));
});
let proc = ProcessEmailAdapter::new(&conf);
let sendmail = &[
@ -289,7 +277,7 @@ mod tests {
let conf = test_config(filename, |_| {});
let mut proc = ProcessEmailAdapter::new(&conf);
let data = EmailData {
text: Some("= Flags\n\n« 🇫🇷🇨🇦🇯🇵 »".to_string()),
text: Some("= Flags\n\n« 🇫🇷🇨🇦🇯🇵 »".into()),
html: Some(
r#"<html>
<body>
@ -297,9 +285,9 @@ mod tests {
<p>« 🇫🇷🇨🇦🇯🇵 »</p>
</body>
</html>"#
.to_string(),
.into(),
),
subject: "Ého! Ça va? … 😼".to_string(),
subject: "Ého! Ça va? … 😼".into(),
};
proc.send(data).unwrap();
@ -355,12 +343,9 @@ mod tests {
fn test_config(test_file: &str, alter_email_options: fn(&mut HashMap<String, Value>)) -> Config {
let mut sendmail_opts = HashMap::new();
sendmail_opts.insert("from".into(), Value::Str("pyruse@localhost".into()));
sendmail_opts.insert(
"from".to_string(),
Value::Str("pyruse@localhost".to_string()),
);
sendmail_opts.insert(
"to".to_string(),
"to".into(),
Value::List(
vec!["root@localhost", "abuse@localhost"]
.iter()
@ -369,7 +354,7 @@ mod tests {
),
);
sendmail_opts.insert(
"sendmail".to_string(),
"sendmail".into(),
Value::List(
//vec!["bash", "-c", "tee test.eml | sendmail -t"]
vec!["bash", "-c", &format!(r#"cat >"{}""#, test_file)]
@ -380,10 +365,7 @@ mod tests {
);
alter_email_options(&mut sendmail_opts);
let mut options = HashMap::new();
options.insert("email".to_string(), Value::Map(sendmail_opts));
Config {
actions: IndexMap::new(),
options,
}
options.insert("email".into(), Value::Map(sendmail_opts));
Config::new(None, Some(options))
}
}

147
src/infra/file.rs Normal file
View File

@ -0,0 +1,147 @@
use crate::domain::Error;
use crate::infra::serde::{Input, Output};
use std::{
convert::TryFrom,
ffi::OsString,
fs::{File, OpenOptions},
io::BufReader,
path::{Path, PathBuf},
str::FromStr,
};
#[derive(Debug, PartialEq, Eq)]
pub enum DataFile {
Json(OsString),
Yaml(OsString),
}
impl DataFile {
pub fn exists(&self) -> bool {
match &self {
DataFile::Json(ref path) | DataFile::Yaml(ref path) => Path::new(path).exists(),
}
}
pub fn open_r(&self) -> Result<Input<BufReader<File>>, Error> {
Ok(match self {
DataFile::Json(path) => Input::Json(BufReader::new(File::open(path)?)),
DataFile::Yaml(path) => Input::Yaml(BufReader::new(File::open(path)?)),
})
}
pub fn open_w(&self) -> Result<Output<File>, Error> {
let mut opt = OpenOptions::new();
let wopt = opt.create(true).write(true).truncate(true);
Ok(match self {
DataFile::Json(path) => Output::Json(wopt.open(path)?),
DataFile::Yaml(path) => Output::Yaml(wopt.open(path)?),
})
}
}
impl TryFrom<(&PathBuf, &str)> for DataFile {
type Error = Error;
fn try_from((parent, file): (&PathBuf, &str)) -> Result<Self, Error> {
let mut o = parent.clone();
o.push(file);
DataFile::try_from(o.into_os_string())
}
}
impl TryFrom<&str> for DataFile {
type Error = Error;
fn try_from(path: &str) -> Result<Self, Error> {
DataFile::try_from(OsString::from_str(path)?)
}
}
impl TryFrom<OsString> for DataFile {
type Error = Error;
fn try_from(path: OsString) -> Result<Self, Error> {
let ext = Path::new(&path)
.extension()
.and_then(|e| Some(e.to_string_lossy()))
.and_then(|s| Some(s.to_ascii_lowercase()))
.unwrap_or_default();
match ext.as_ref() {
"json" => Ok(DataFile::Json(path)),
"yaml" | "yml" => Ok(DataFile::Yaml(path)),
_ => Err(format!("File {:?} does not have extension .json or .yaml", path).into()),
}
}
}
#[cfg(test)]
mod tests {
use crate::infra::file::DataFile;
use std::{convert::TryFrom, env, ffi::OsString, fs, path::PathBuf, str::FromStr};
#[test]
fn json_path_becomes_a_json_variant() {
if let Ok(DataFile::Json(_)) = DataFile::try_from("/Some/File name.Json") {
()
} else {
panic!("Expected Json variant")
}
}
#[test]
fn yaml_path_becomes_a_yaml_variant() {
if let Ok(DataFile::Yaml(_)) = DataFile::try_from("/Some/File name.Yaml") {
()
} else {
panic!("Expected Yaml variant")
}
}
#[test]
fn yml_path_becomes_a_yaml_variant() {
if let Ok(DataFile::Yaml(_)) = DataFile::try_from("/Some/File name.Yml") {
()
} else {
panic!("Expected Yaml variant")
}
}
#[test]
fn when_neither_yaml_nor_json_then_unknown_file() {
if let Ok(_) = DataFile::try_from("/Some/File name") {
panic!("Expected an error")
}
}
#[test]
fn pathbuf_and_str_can_be_combined_and_converted() {
let parent = PathBuf::from("/Its");
let files = (
DataFile::try_from((&parent, "a.json")),
DataFile::try_from((&parent, "a.yaml")),
);
assert_eq!(
(
Ok(DataFile::Json(OsString::from_str("/Its/a.json").unwrap())),
Ok(DataFile::Yaml(OsString::from_str("/Its/a.yaml").unwrap()))
),
files
);
}
#[test]
fn exists_returns_true_for_existing_file() {
let tempdir = env::temp_dir();
let tempfile = tempdir.join("test-exists.yaml");
let filename = tempfile.to_str().unwrap();
let datafile = DataFile::try_from(filename).unwrap();
fs::File::create(filename).unwrap();
let exists = datafile.exists();
fs::remove_file(filename).unwrap();
assert_eq!(true, exists);
}
#[test]
fn exists_returns_false_for_absent_file() {
let filename = "→ I dont exist 😱 ←.yaml";
let datafile = DataFile::try_from(filename).unwrap();
let exists = datafile.exists();
assert_eq!(false, exists);
}
}

View File

@ -1,4 +1,4 @@
use crate::domain::{LogMessage, LogPort, Record, Value};
use crate::domain::{Error, LogMessage, LogPort, Record, Value};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::iter::FromIterator;
@ -24,28 +24,23 @@ pub struct SystemdLogAdapter {
}
impl SystemdLogAdapter {
pub fn open() -> Result<Self, ()> {
let mappers = create_mappers();
if let Ok(mut journal) = OpenOptions::default()
pub fn open() -> Result<Self, Error> {
let mut journal = OpenOptions::default()
.system(true)
.current_user(false)
.local_only(false)
.open()
{
if let Ok(_) = journal.seek_tail() {
return Ok(SystemdLogAdapter { journal, mappers });
}
}
Err(())
.open()?;
journal.seek_tail()?;
let mappers = create_mappers();
Ok(SystemdLogAdapter { journal, mappers })
}
}
impl LogPort for SystemdLogAdapter {
fn read_next(&mut self) -> Result<Record, ()> {
fn read_next(&mut self) -> Result<Record, Error> {
loop {
match self.journal.await_next_entry(None) {
Err(_) => return Err(()),
Ok(Some(mut entry)) => {
match self.journal.await_next_entry(None)? {
Some(mut entry) => {
let mut record: Record = HashMap::with_capacity(entry.len());
let all_keys = entry.keys().map(|s| s.clone()).collect::<Vec<String>>();
for k in all_keys {
@ -58,12 +53,12 @@ impl LogPort for SystemdLogAdapter {
}
return Ok(record);
}
Ok(None) => continue,
None => continue,
};
}
}
fn write(&mut self, message: LogMessage) -> Result<(), ()> {
fn write(&mut self, message: LogMessage) -> Result<(), Error> {
let unix_status = match message {
LogMessage::EMERG(m) => print(0, m),
LogMessage::ALERT(m) => print(1, m),
@ -76,7 +71,7 @@ impl LogPort for SystemdLogAdapter {
};
match unix_status {
0 => Ok(()),
_ => Err(()),
_ => Err("Writing the systemd log resulted in a non-zero status".into()),
}
}
}
@ -151,7 +146,7 @@ fn create_mappers<'t>() -> HashMap<String, JournalFieldMapper> {
("__MONOTONIC_TIMESTAMP", DATE_MAPPER),
]
.iter()
.map(|(s, m)| (s.to_string(), m.to_owned())),
.map(|(s, m)| ((*s).into(), m.to_owned())),
);
map
}

View File

@ -2,4 +2,7 @@ pub mod config;
pub mod counter;
pub mod dnat;
pub mod email;
pub mod file;
pub mod log;
pub mod netfilter;
pub mod serde;

392
src/infra/netfilter/data.rs Normal file
View File

@ -0,0 +1,392 @@
use crate::domain::action::NetfilterStoragePort;
use crate::domain::{Config, Error, Value};
use crate::infra::file::DataFile;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, path::PathBuf};
pub struct FilesystemNetfilterStorageAdapter {
file: DataFile,
}
impl FilesystemNetfilterStorageAdapter {
pub fn new(conf: &Config, basename: &str) -> FilesystemNetfilterStorageAdapter {
let dirname = PathBuf::from(if let Some(Value::Str(s)) = conf.options.get("storage") {
s.as_ref()
} else {
"/var/lib/pyruse"
});
FilesystemNetfilterStorageAdapter {
file: DataFile::try_from((&dirname, basename)).unwrap(),
}
}
}
impl NetfilterStoragePort for FilesystemNetfilterStorageAdapter {
fn store_and_remove_obsoletes<'s, 'r>(
&'s mut self,
nf_set: &'s str,
ip: &'r str,
ban_until: &'s Option<DateTime<Utc>>,
) -> Result<bool, Error> {
let now = Utc::now();
let mut found = false;
let mut bans = (if self.file.exists() {
self.file.open_r()?.parse::<Vec<Ban>>()?
} else {
Vec::new()
})
.drain(..)
.filter(|b| b.ban_until.map(|d| d > now).unwrap_or(true))
.filter(|b| {
if (&b.ip.as_ref(), &b.nf_set.as_ref()) == (&ip, &nf_set) {
found = true;
false
} else {
true
}
})
.collect::<Vec<Ban>>();
bans.push(Ban {
ip: ip.to_string(),
nf_set: nf_set.to_string(),
ban_until: ban_until.clone(),
});
self.file.open_w()?.format(bans)?;
Ok(found)
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct Ban {
#[serde(rename = "IP")]
ip: String,
#[serde(rename = "nfSet")]
nf_set: String,
#[serde(rename = "timestamp", with = "timestamp_serde")]
ban_until: Option<DateTime<Utc>>,
}
// Lossy de·serializer which does not care about sub-seconds
mod timestamp_serde {
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::de::{self, Visitor};
use serde::{Deserializer, Serializer};
use std::fmt;
struct TimestampVisitor;
impl<'de> Visitor<'de> for TimestampVisitor {
type Value = i64;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a number")
}
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v)
}
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v as i64)
}
}
pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i64(date.map(|d| d.timestamp()).unwrap_or(0))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
match deserializer.deserialize_any(TimestampVisitor) {
Ok(i) if i == 0 => Ok(None),
Ok(i) => Ok(Some(DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(i, 0),
Utc,
))),
Err(x) => Err(x),
}
}
}
#[cfg(test)]
mod tests {
use super::{timestamp_serde, Ban, FilesystemNetfilterStorageAdapter};
use crate::domain::action::NetfilterStoragePort;
use crate::domain::test_util::WriteProxy;
use crate::domain::{Config, Value};
use crate::infra::file::DataFile;
use crate::infra::serde::{Input, Output};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::time::{Duration, UNIX_EPOCH};
#[derive(Serialize, Deserialize)]
struct TimestampHolder {
#[serde(with = "timestamp_serde")]
ts: Option<DateTime<Utc>>,
}
#[test]
fn timestamp_subsecond_is_discarded_when_it_is_read() {
let yaml = "ts: 1617864646.521".as_bytes();
let value: TimestampHolder = Input::Yaml(yaml).parse().unwrap();
assert_eq!(1617864646, value.ts.unwrap().timestamp());
assert_eq!(0, value.ts.unwrap().timestamp_subsec_micros());
}
#[test]
fn timestamp_subsecond_is_not_written() {
let value: TimestampHolder = TimestampHolder {
ts: Some(DateTime::from(
UNIX_EPOCH + Duration::from_millis(1618655010521),
)),
};
let mut bytes = Vec::<u8>::new();
let writer = WriteProxy::new(&mut bytes);
Output::Yaml(writer).format(value).unwrap();
assert_eq!("---\nts: 1618655010\n", String::from_utf8(bytes).unwrap());
}
#[test]
fn zero_timestamp_is_read_as_an_empty_datetime() {
let yaml = "ts: 0".as_bytes();
let value: TimestampHolder = Input::Yaml(yaml).parse().unwrap();
assert_eq!(None, value.ts);
}
#[test]
fn empty_timestamp_is_written_as_zero() {
let value: TimestampHolder = TimestampHolder { ts: None };
let mut bytes = Vec::<u8>::new();
let writer = WriteProxy::new(&mut bytes);
Output::Yaml(writer).format(value).unwrap();
assert_eq!("---\nts: 0\n", String::from_utf8(bytes).unwrap());
}
#[test]
fn ban_is_correctly_read() {
let json =
r#"{"IP": "164.52.24.168", "nfSet": "ip Inet4 mail_ban", "timestamp": 1614263304.356016}"#
.as_bytes();
let value: Ban = Input::Json(json).parse().unwrap();
let expected = Ban {
ip: "164.52.24.168".into(),
nf_set: "ip Inet4 mail_ban".into(),
ban_until: Some(DateTime::from(UNIX_EPOCH + Duration::from_secs(1614263304))),
};
assert_eq!(expected, value);
}
#[test]
fn ban_is_correctly_written() {
let ban = Ban {
ip: "164.52.24.168".into(),
nf_set: "ip Inet4 mail_ban".into(),
ban_until: Some(DateTime::from(UNIX_EPOCH + Duration::from_secs(1614263304))),
};
let mut bytes = Vec::<u8>::new();
let writer = WriteProxy::new(&mut bytes);
Output::Json(writer).format(ban).unwrap();
assert_eq!(
r#"{"IP":"164.52.24.168","nfSet":"ip Inet4 mail_ban","timestamp":1614263304}"#,
String::from_utf8(bytes).unwrap()
);
}
#[test]
fn ban_file_is_correctly_read() {
let mut path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
path.push("tests/netfilter-data.yaml");
let file = fs::File::open(path).unwrap();
let bans: Vec<Ban> = Input::Yaml(file).parse().unwrap();
assert_eq!(5, bans.len());
}
#[test]
fn ban_file_is_correctly_filtered_and_new_ban_added_to_file() {
let mut srcpath = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
srcpath.push("tests/netfilter-data.yaml");
let mut destpath = env::temp_dir();
destpath.push("netfilter-data-copy-for-new.yaml");
fs::copy(&srcpath, &destpath).unwrap();
let mut storage = FilesystemNetfilterStorageAdapter {
file: DataFile::Yaml(destpath.clone().into()),
};
let found = storage
.store_and_remove_obsoletes("NEW SET", "NEW.IP", &None)
.unwrap();
let file = fs::File::open(&destpath).unwrap();
let bans: Vec<Ban> = Input::Yaml(file).parse().unwrap();
let expected = vec![
Ban {
ip: "121.66.35.37".into(),
nf_set: "ip Inet4 mail_ban".into(),
ban_until: Some(DateTime::from(UNIX_EPOCH + Duration::from_secs(2614199470))),
},
Ban {
ip: "59.39.183.34".into(),
nf_set: "ip Inet4 mail_ban".into(),
ban_until: Some(DateTime::from(UNIX_EPOCH + Duration::from_secs(2614201649))),
},
Ban {
ip: "51.11.240.49".into(),
nf_set: "ip Inet4 https_ban".into(),
ban_until: None,
},
Ban {
ip: "NEW.IP".into(),
nf_set: "NEW SET".into(),
ban_until: None,
},
];
fs::remove_file(&destpath).unwrap();
assert_eq!(false, found);
assert_eq!(expected, bans);
}
#[test]
fn existing_ban_is_updated_in_file() {
let mut srcpath = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
srcpath.push("tests/netfilter-data.yaml");
let mut destpath = env::temp_dir();
destpath.push("netfilter-data-copy-for-existing.yaml");
fs::copy(&srcpath, &destpath).unwrap();
let mut storage = FilesystemNetfilterStorageAdapter {
file: DataFile::Yaml(destpath.clone().into()),
};
let found = storage
.store_and_remove_obsoletes(
"ip Inet4 mail_ban",
"121.66.35.37",
&Some(DateTime::from(UNIX_EPOCH + Duration::from_secs(2614199400))),
)
.unwrap();
let file = fs::File::open(&destpath).unwrap();
let bans: Vec<Ban> = Input::Yaml(file).parse().unwrap();
let expected = vec![
Ban {
ip: "59.39.183.34".into(),
nf_set: "ip Inet4 mail_ban".into(),
ban_until: Some(DateTime::from(UNIX_EPOCH + Duration::from_secs(2614201649))),
},
Ban {
ip: "51.11.240.49".into(),
nf_set: "ip Inet4 https_ban".into(),
ban_until: None,
},
Ban {
ip: "121.66.35.37".into(),
nf_set: "ip Inet4 mail_ban".into(),
ban_until: Some(DateTime::from(UNIX_EPOCH + Duration::from_secs(2614199400))),
},
];
fs::remove_file(&destpath).unwrap();
assert_eq!(true, found);
assert_eq!(expected, bans);
}
#[test]
fn default_data_dir_is_in_var() {
let conf = Config::new(None, None);
let adapter = FilesystemNetfilterStorageAdapter::new(&conf, "test.yaml");
assert_eq!(
DataFile::try_from("/var/lib/pyruse/test.yaml").unwrap(),
adapter.file
);
}
#[test]
fn data_dir_is_read_from_config() {
let mut conf = Config::new(None, None);
conf
.options
.insert("storage".into(), Value::Str("/tmp".into()));
let adapter = FilesystemNetfilterStorageAdapter::new(&conf, "test.yaml");
assert_eq!(DataFile::try_from("/tmp/test.yaml").unwrap(), adapter.file);
}
}

View File

@ -0,0 +1,150 @@
use crate::domain::{action::NetfilterBackendPort, Config, Error};
use chrono::{DateTime, Utc};
use std::ffi::OsString;
use std::process::Command;
const DEFAULT_IPSET: &[&str] = &["/usr/bin/ipset", "-exist", "-quiet"];
pub struct IpsetNetfilterBackendAdapter {
ipset_cmd: OsString,
ipset_params: Vec<OsString>,
}
impl IpsetNetfilterBackendAdapter {
pub fn new(conf: &Config) -> IpsetNetfilterBackendAdapter {
let (ipset_cmd, ipset_params) =
super::read_command_from_options(conf, "ipsetBan", "ipset", DEFAULT_IPSET).unwrap();
IpsetNetfilterBackendAdapter {
ipset_cmd,
ipset_params,
}
}
}
impl NetfilterBackendPort for IpsetNetfilterBackendAdapter {
fn set_ban<'s, 'r>(
&'s mut self,
nf_set: &'s str,
ip: &'r str,
ban_until: &'s Option<DateTime<Utc>>,
) -> Result<(), Error> {
let mut ipset = Command::new(&self.ipset_cmd);
for p in &self.ipset_params {
ipset.arg(p);
}
ipset.arg("add");
ipset.arg(nf_set);
ipset.arg(ip);
let debug_seconds = if let Some(d) = ban_until {
let seconds = (d.timestamp() - Utc::now().timestamp()).to_string();
ipset.arg("timeout");
ipset.arg(&seconds);
seconds
} else {
"indefinitely".into()
};
let exit_status = ipset.spawn()?.wait()?;
if exit_status.success() {
Ok(())
} else {
Err(Error::from(format!(
"Ipset ban {:?} failed with code {:?}",
&["add", nf_set, ip, &debug_seconds],
exit_status.code()
)))
}
}
fn cancel_ban<'s, 'r>(&'s mut self, nf_set: &'s str, ip: &'r str) -> Result<(), Error> {
let mut ipset = Command::new(&self.ipset_cmd);
for p in &self.ipset_params {
ipset.arg(p);
}
ipset.arg("del");
ipset.arg(nf_set);
ipset.arg(ip);
let exit_status = ipset.spawn()?.wait()?;
if exit_status.success() {
Ok(())
} else {
Err(Error::from(format!(
"Ipset unban {:?} failed with code {:?}",
&["del", nf_set, ip],
exit_status.code()
)))
}
}
}
#[cfg(test)]
mod tests {
use super::IpsetNetfilterBackendAdapter;
use crate::domain::action::NetfilterBackendPort;
use crate::domain::{Config, Value};
use chrono::{Duration, Utc};
use std::collections::HashMap;
use std::{env, fs};
#[test]
fn ban_with_enddate_has_a_timeout() {
let mut temp = env::temp_dir();
temp.push("ipset-ban-test-with-time.cmd");
let filename = temp.to_str().unwrap();
let conf = test_config(filename);
let mut adapter = IpsetNetfilterBackendAdapter::new(&conf);
adapter
.set_ban(
"an iptables set",
"::1",
&Some(Utc::now() + Duration::seconds(9)),
)
.unwrap();
let file = fs::read_to_string(filename).unwrap();
let prefix = "add\nan iptables set\n::1\ntimeout\n";
assert!(file.starts_with(prefix));
assert!(file[prefix.len()..(file.len() - 1)].parse::<i32>().unwrap() < 10); // ignore trailing "\n"
fs::remove_file(filename).unwrap();
}
#[test]
fn ban_without_enddate_has_no_timeout() {
let mut temp = env::temp_dir();
temp.push("ipset-ban-test-without-time.cmd");
let filename = temp.to_str().unwrap();
let conf = test_config(filename);
let mut adapter = IpsetNetfilterBackendAdapter::new(&conf);
adapter.set_ban("an iptables set", "::1", &None).unwrap();
let file = fs::read_to_string(filename).unwrap();
assert_eq!(&file, "add\nan iptables set\n::1\n");
fs::remove_file(filename).unwrap();
}
#[test]
fn unban_works() {
let mut temp = env::temp_dir();
temp.push("ipset-unban-test.cmd");
let filename = temp.to_str().unwrap();
let conf = test_config(filename);
let mut adapter = IpsetNetfilterBackendAdapter::new(&conf);
adapter.cancel_ban("an iptables set", "1.2.3.4").unwrap();
let file = fs::read_to_string(filename).unwrap();
assert_eq!(&file, "del\nan iptables set\n1.2.3.4\n");
fs::remove_file(filename).unwrap();
}
fn test_config(filename: &str) -> Config {
let ipset = &[
"bash",
"-c",
&format!(r#"printf '%s\n' "$@" >"{}""#, filename),
"-",
];
let mut options = HashMap::new();
let mut ipset_ban = HashMap::new();
ipset_ban.insert(
"ipset".into(),
Value::List(ipset.iter().map(|s| Value::Str((*s).into())).collect()),
);
options.insert("ipsetBan".into(), Value::Map(ipset_ban));
Config::new(None, Some(options))
}
}

View File

@ -0,0 +1,77 @@
mod data;
pub use self::data::*;
mod ipset;
pub use self::ipset::*;
mod nftables;
pub use self::nftables::*;
use crate::domain::{Config, Error, Value};
use std::ffi::OsString;
use std::str::FromStr;
fn read_command_from_options(
conf: &Config,
option_name: &str,
command_entry: &str,
default_command: &[&str],
) -> Result<(OsString, Vec<OsString>), Error> {
let mut params = conf.options.get(option_name).and_then(|o| match o {
Value::Map(m) => m.get(command_entry),
_ => None,
})
.and_then(|n| match n {
Value::Str(s) => Some(vec!(OsString::from_str(s).expect(&format!("{}” could not be read", s)))),
Value::List(l) => Some(l.iter().map(|v| match v {
Value::Str(s) => OsString::from_str(s).expect(&format!("{}” could not be read", s)),
_ => panic!(format!("One item in the “{}” command-line components of {} options is not a shell command or a parameter", command_entry, option_name)),
}).collect()),
_ => None,
})
.filter(|v| !v.is_empty())
.unwrap_or(default_command.iter().map(|s| OsString::from_str(s).unwrap()).collect());
let cmd = params.remove(0);
Ok((cmd, params))
}
#[cfg(test)]
mod tests {
use super::read_command_from_options;
use crate::domain::{Config, Value};
use std::collections::HashMap;
use std::ffi::OsString;
#[test]
fn default_command_is_used_if_unspecified() {
let conf = Config::new(None, None);
let (cmd, params) = read_command_from_options(&conf, "o", "c", &["cmd", "p1", "p2"]).unwrap();
assert_eq!(OsString::from("cmd"), cmd);
assert_eq!(vec![OsString::from("p1"), OsString::from("p2")], params);
}
#[test]
fn command_cannot_be_an_empty_list_or_default_command_is_used() {
let conf = test_config(&[]);
let (cmd, params) = read_command_from_options(&conf, "o", "c", &["cmd"]).unwrap();
assert_eq!(OsString::from("cmd"), cmd);
assert_eq!(Vec::<OsString>::new(), params);
}
#[test]
fn custom_commands_are_accepted() {
let conf = test_config(&["a", "b"]);
let (cmd, params) = read_command_from_options(&conf, "o", "c", &["cmd", "p"]).unwrap();
assert_eq!(OsString::from("a"), cmd);
assert_eq!(vec![OsString::from("b")], params);
}
fn test_config(command: &[&str]) -> Config {
let mut options = HashMap::new();
let mut cmd_opt = HashMap::new();
cmd_opt.insert(
"c".into(),
Value::List(command.iter().map(|s| Value::Str(s.to_string())).collect()),
);
options.insert("o".into(), Value::Map(cmd_opt));
Config::new(None, Some(options))
}
}

View File

@ -0,0 +1,139 @@
use crate::domain::{action::NetfilterBackendPort, Config, Error};
use chrono::{DateTime, Utc};
use std::ffi::OsString;
use std::process::Command;
const DEFAULT_NFT: &[&str] = &["/usr/bin/nft"];
pub struct NftablesNetfilterBackendAdapter {
nft_cmd: OsString,
nft_params: Vec<OsString>,
}
impl NftablesNetfilterBackendAdapter {
pub fn new(conf: &Config) -> NftablesNetfilterBackendAdapter {
let (nft_cmd, nft_params) =
super::read_command_from_options(conf, "nftBan", "nft", DEFAULT_NFT).unwrap();
NftablesNetfilterBackendAdapter {
nft_cmd,
nft_params,
}
}
}
impl NetfilterBackendPort for NftablesNetfilterBackendAdapter {
fn set_ban<'s, 'r>(
&'s mut self,
nf_set: &'s str,
ip: &'r str,
ban_until: &'s Option<DateTime<Utc>>,
) -> Result<(), Error> {
let mut nft = Command::new(&self.nft_cmd);
for p in &self.nft_params {
nft.arg(p);
}
let timeout = ban_until
.map(|d| format!(" timeout {}s", d.timestamp() - Utc::now().timestamp()))
.unwrap_or(String::new());
let ban = format!("add element {} {{{}{}}}", nf_set, ip, timeout);
nft.arg(&ban);
let exit_status = nft.spawn()?.wait()?;
if exit_status.success() {
Ok(())
} else {
Err(Error::from(format!(
"Nftables ban [{}] failed with code {:?}",
ban,
exit_status.code()
)))
}
}
fn cancel_ban<'s, 'r>(&'s mut self, nf_set: &'s str, ip: &'r str) -> Result<(), Error> {
let mut nft = Command::new(&self.nft_cmd);
for p in &self.nft_params {
nft.arg(p);
}
let unban = format!("delete element {} {{{}}}", nf_set, ip);
nft.arg(&unban);
let exit_status = nft.spawn()?.wait()?;
if exit_status.success() {
Ok(())
} else {
Err(Error::from(format!(
"Nftables unban [{}] failed with code {:?}",
unban,
exit_status.code()
)))
}
}
}
#[cfg(test)]
mod tests {
use super::NftablesNetfilterBackendAdapter;
use crate::domain::action::NetfilterBackendPort;
use crate::domain::{Config, Value};
use chrono::{Duration, Utc};
use std::collections::HashMap;
use std::{env, fs};
#[test]
fn ban_with_enddate_has_a_timeout() {
let mut temp = env::temp_dir();
temp.push("nft-ban-test-with-time.cmd");
let filename = temp.to_str().unwrap();
let conf = test_config(filename);
let mut adapter = NftablesNetfilterBackendAdapter::new(&conf);
adapter
.set_ban("a nft set", "::1", &Some(Utc::now() + Duration::seconds(9)))
.unwrap();
let file = fs::read_to_string(filename).unwrap();
let prefix = "add element a nft set {::1 timeout ";
assert!(file.starts_with(prefix));
assert!(file[prefix.len()..(file.len() - 3)].parse::<i32>().unwrap() < 10); // ignore trailing "s}\n"
fs::remove_file(filename).unwrap();
}
#[test]
fn ban_without_enddate_has_no_timeout() {
let mut temp = env::temp_dir();
temp.push("nft-ban-test-without-time.cmd");
let filename = temp.to_str().unwrap();
let conf = test_config(filename);
let mut adapter = NftablesNetfilterBackendAdapter::new(&conf);
adapter.set_ban("a nft set", "::1", &None).unwrap();
let file = fs::read_to_string(filename).unwrap();
assert_eq!(&file, "add element a nft set {::1}\n");
fs::remove_file(filename).unwrap();
}
#[test]
fn unban_works() {
let mut temp = env::temp_dir();
temp.push("nft-unban-test.cmd");
let filename = temp.to_str().unwrap();
let conf = test_config(filename);
let mut adapter = NftablesNetfilterBackendAdapter::new(&conf);
adapter.cancel_ban("a nft set", "1.2.3.4").unwrap();
let file = fs::read_to_string(filename).unwrap();
assert_eq!(&file, "delete element a nft set {1.2.3.4}\n");
fs::remove_file(filename).unwrap();
}
fn test_config(filename: &str) -> Config {
let nft = &[
"bash",
"-c",
&format!(r#"printf '%s\n' "$@" >"{}""#, filename),
"-",
];
let mut options = HashMap::new();
let mut nft_ban = HashMap::new();
nft_ban.insert(
"nft".into(),
Value::List(nft.iter().map(|s| Value::Str((*s).into())).collect()),
);
options.insert("nftBan".into(), Value::Map(nft_ban));
Config::new(None, Some(options))
}
}

69
src/infra/serde.rs Normal file
View File

@ -0,0 +1,69 @@
use crate::domain::Error;
use serde::{de::DeserializeOwned, Serialize};
use std::io::{Read, Write};
pub enum Input<R: Read> {
Json(R),
Yaml(R),
}
impl<R: Read> Input<R> {
pub fn parse<T: DeserializeOwned>(self) -> Result<T, Error> {
match self {
Input::Json(r) => Ok(serde_json::from_reader(r)?),
Input::Yaml(r) => Ok(serde_yaml::from_reader(r)?),
}
}
}
pub enum Output<W: Write> {
Json(W),
Yaml(W),
}
impl<W: Write> Output<W> {
pub fn format<T: Serialize>(self, data: T) -> Result<(), Error> {
match self {
Output::Json(w) => Ok(serde_json::to_writer(w, &data)?),
Output::Yaml(w) => Ok(serde_yaml::to_writer(w, &data)?),
}
}
}
#[cfg(test)]
mod tests {
use super::{Input, Output};
use crate::domain::test_util::WriteProxy;
#[test]
fn parse_on_json_input_gives_data_if_syntax_is_correct() {
let raw = "[1,3,5]";
let input = Input::Json(raw.as_bytes());
let data: Vec<u64> = input.parse().unwrap();
assert_eq!(vec![1, 3, 5], data);
}
#[test]
fn parse_on_yaml_input_gives_data_if_syntax_is_correct() {
let raw = "- 1\n- 3\n- 5\n";
let input = Input::Yaml(raw.as_bytes());
let data: Vec<u64> = input.parse().unwrap();
assert_eq!(vec![1, 3, 5], data);
}
#[test]
fn format_on_json_gives_json_output() {
let data = vec![1, 3, 5];
let mut bytes = Vec::<u8>::new();
let writer = WriteProxy::new(&mut bytes);
Output::Json(writer).format(data).unwrap();
assert_eq!("[1,3,5]", String::from_utf8(bytes).unwrap());
}
#[test]
fn format_on_yaml_gives_yaml_output() {
let data = vec![1, 3, 5];
let mut bytes = Vec::<u8>::new();
let writer = WriteProxy::new(&mut bytes);
Output::Yaml(writer).format(data).unwrap();
assert_eq!("---\n- 1\n- 3\n- 5\n", String::from_utf8(bytes).unwrap());
}
}

View File

@ -3,29 +3,43 @@ mod domain;
mod infra;
mod service;
use domain::action::{CounterRaise, CounterReset, DnatCapture, DnatReplace, Email, Log, Noop};
use domain::action::{
CounterRaise, CounterReset, DnatCapture, DnatReplace, Email, Log, NetfilterBan, Noop,
};
use domain::filter::Equals;
use domain::{ConfigPort, Counters, Modules, Workflow};
use infra::config::{configuration_from_filesystem, SerdeConfigAdapter, ETC_PATH};
use infra::counter::InMemoryCounterAdapter;
use infra::dnat::InMemoryDnatMappingsAdapter;
use infra::email::ProcessEmailAdapter;
use infra::log::SystemdLogAdapter;
use infra::{config::ConfFile, email::ProcessEmailAdapter};
use infra::netfilter::{
FilesystemNetfilterStorageAdapter, IpsetNetfilterBackendAdapter, NftablesNetfilterBackendAdapter,
};
type CountersImpl = InMemoryCounterAdapter;
type DnatImpl = InMemoryDnatMappingsAdapter;
type EmailImpl = ProcessEmailAdapter;
type IpsetBackendImpl = IpsetNetfilterBackendAdapter;
type IpsetStorageImpl = FilesystemNetfilterStorageAdapter;
type LogImpl = SystemdLogAdapter;
type NftablesBackendImpl = NftablesNetfilterBackendAdapter;
type NftablesStorageImpl = FilesystemNetfilterStorageAdapter;
fn main() {
let mut conf = ConfFile::from_filesystem().to_config();
let mut conf: SerdeConfigAdapter = configuration_from_filesystem(ETC_PATH).into();
let email = singleton_new!(EmailImpl::new(conf.get()));
let log = singleton_new!(LogImpl::open().expect("Error initializing systemd"));
let mut modules = Modules::new();
let counters = singleton_new!(Counters::<CountersImpl>::new(CountersImpl::new()));
let dnat = singleton_new!(DnatImpl::new());
let gets_moved_into_closure = singleton_share!(counters);
let ipset_backend = singleton_new!(IpsetBackendImpl::new(conf.get()));
let ipset_storage = singleton_new!(IpsetStorageImpl::new(conf.get(), "action_ipsetBan.json"));
let nftables_backend = singleton_new!(NftablesBackendImpl::new(conf.get()));
let nftables_storage = singleton_new!(NftablesStorageImpl::new(conf.get(), "action_nftBan.json"));
modules.register_action(
"action_counterRaise".to_string(),
"action_counterRaise".into(),
Box::new(move |a| {
Box::new(CounterRaise::<CountersImpl>::from_args(
a,
@ -35,7 +49,7 @@ fn main() {
);
let gets_moved_into_closure = singleton_share!(counters);
modules.register_action(
"action_counterReset".to_string(),
"action_counterReset".into(),
Box::new(move |a| {
Box::new(CounterReset::<CountersImpl>::from_args(
a,
@ -45,7 +59,7 @@ fn main() {
);
let gets_moved_into_closure = singleton_share!(dnat);
modules.register_action(
"action_dnatCapture".to_string(),
"action_dnatCapture".into(),
Box::new(move |a| {
Box::new(DnatCapture::from_args(
a,
@ -55,7 +69,7 @@ fn main() {
);
let gets_moved_into_closure = singleton_share!(dnat);
modules.register_action(
"action_dnatReplace".to_string(),
"action_dnatReplace".into(),
Box::new(move |a| {
Box::new(DnatReplace::from_args(
a,
@ -65,7 +79,7 @@ fn main() {
);
let gets_moved_into_closure = singleton_share!(email);
modules.register_action(
"action_email".to_string(),
"action_email".into(),
Box::new(move |a| {
Box::new(Email::from_args(
a,
@ -73,16 +87,46 @@ fn main() {
))
}),
);
let gets_moved_into_closure = singleton_share!(ipset_backend);
let gets_moved_into_closure_2 = singleton_share!(ipset_storage);
modules.register_action(
"action_log".to_string(),
Box::new(move |a| Box::new(Log::from_args(a, singleton_share!(log)))),
"action_ipsetBan".into(),
Box::new(move |a| {
Box::new(NetfilterBan::from_args(
a,
"action_ipsetBan",
"ipSetIPv4",
"ipSetIPv6",
singleton_share!(gets_moved_into_closure), // clone for each call of the constructor
singleton_share!(gets_moved_into_closure_2), // clone for each call of the constructor
))
}),
);
modules.register_action(
"action_noop".to_string(),
"action_log".into(),
Box::new(move |a| Box::new(Log::from_args(a, singleton_share!(log)))),
);
let gets_moved_into_closure = singleton_share!(nftables_backend);
let gets_moved_into_closure_2 = singleton_share!(nftables_storage);
modules.register_action(
"action_nftBan".into(),
Box::new(move |a| {
Box::new(NetfilterBan::from_args(
a,
"action_nftBan",
"nftSetIPv4",
"nftSetIPv6",
singleton_share!(gets_moved_into_closure), // clone for each call of the constructor
singleton_share!(gets_moved_into_closure_2), // clone for each call of the constructor
))
}),
);
modules.register_action(
"action_noop".into(),
Box::new(move |a| Box::new(Noop::from_args(a))),
);
modules.register_filter(
"filter_equals".to_string(),
"filter_equals".into(),
Box::new(move |a| Box::new(Equals::from_args(a))),
);
let _workflow = Workflow::build(conf.get(), &modules);