ban actions + error handling + refactoring
parent
bf93109f96
commit
7952cb0b76
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {})));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("/It’s");
|
||||
let files = (
|
||||
DataFile::try_from((&parent, "a.json")),
|
||||
DataFile::try_from((&parent, "a.yaml")),
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
Ok(DataFile::Json(OsString::from_str("/It’s/a.json").unwrap())),
|
||||
Ok(DataFile::Yaml(OsString::from_str("/It’s/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 don’t exist 😱 ←.yaml";
|
||||
let datafile = DataFile::try_from(filename).unwrap();
|
||||
let exists = datafile.exists();
|
||||
assert_eq!(false, exists);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
68
src/main.rs
68
src/main.rs
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue