filter (m_filter) as a self-contained module: Server.ext typemap for module state + module_commands() wiring; FILTER command + on_pre_message hook all in modules/filter.rs
This commit is contained in:
parent
7bf9e4c0b8
commit
ee98fade40
4 changed files with 197 additions and 0 deletions
|
|
@ -29,6 +29,7 @@ pub fn command_table() -> HashMap<&'static str, Box<dyn Command>> {
|
||||||
.chain(core_info::commands())
|
.chain(core_info::commands())
|
||||||
.chain(core_extra::commands())
|
.chain(core_extra::commands())
|
||||||
.chain(core_watch::commands())
|
.chain(core_watch::commands())
|
||||||
|
.chain(crate::modules::module_commands())
|
||||||
{
|
{
|
||||||
m.insert(c.name(), c);
|
m.insert(c.name(), c);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
182
src/modules/filter.rs
Normal file
182
src/modules/filter.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
//! filter — InspIRCd's `m_filter`: oper-configured spam/word filters. A glob is
|
||||||
|
//! matched against PRIVMSG/NOTICE text and, on a hit, an action is taken. Fully
|
||||||
|
//! self-contained: the rule set lives in `Server.ext` (the module-owned typemap),
|
||||||
|
//! the `FILTER` command manages it, and the `on_pre_message` hook enforces it —
|
||||||
|
//! nothing leaks into server.rs or config.rs.
|
||||||
|
|
||||||
|
use crate::channels::glob_match;
|
||||||
|
use crate::command::{CmdResult, Command};
|
||||||
|
use crate::module::{ModResult, Module};
|
||||||
|
use crate::numeric::ERR_NOPRIVILEGES;
|
||||||
|
use crate::server::Server;
|
||||||
|
use crate::xline::{parse_duration, XKind};
|
||||||
|
use crate::Uid;
|
||||||
|
|
||||||
|
/// One filter rule.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SpamFilter {
|
||||||
|
pub pattern: String, // glob matched against message text
|
||||||
|
pub action: String, // block | silent | kill | kline | gline | zline
|
||||||
|
pub duration: u64, // ban length for the *line actions (seconds; 0 = permanent)
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rule set — stored in `Server.ext`, so it never touches the core struct.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Filters(pub Vec<SpamFilter>);
|
||||||
|
|
||||||
|
impl Filters {
|
||||||
|
/// The (action, reason, duration) of the first rule whose glob matches `text`.
|
||||||
|
fn hit(&self, text: &str) -> Option<(String, String, u64)> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.find(|f| glob_match(&f.pattern, text))
|
||||||
|
.map(|f| (f.action.clone(), f.reason.clone(), f.duration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The enforcement hook.
|
||||||
|
pub struct Filter;
|
||||||
|
impl Module for Filter {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"filter"
|
||||||
|
}
|
||||||
|
fn on_pre_message(&mut self, s: &mut Server, uid: Uid, _target: &str, text: &str) -> ModResult {
|
||||||
|
let Some((action, reason, duration)) = s.ext.get::<Filters>().and_then(|f| f.hit(text))
|
||||||
|
else {
|
||||||
|
return ModResult::Passthru;
|
||||||
|
};
|
||||||
|
let (mask, ip) = match s.users.get(&uid) {
|
||||||
|
Some(u) => (u.prefix(), u.addr.ip().to_string()),
|
||||||
|
None => return ModResult::Deny,
|
||||||
|
};
|
||||||
|
s.snotice(&format!(
|
||||||
|
"FILTER: {mask} matched a filter (action={action}): {reason}"
|
||||||
|
));
|
||||||
|
match action.as_str() {
|
||||||
|
"block" => s.notice_star(uid, &format!("Your message was blocked: {reason}")),
|
||||||
|
"silent" => {}
|
||||||
|
"kill" => {
|
||||||
|
s.send(uid, format!("ERROR :Closing link: ({reason})"));
|
||||||
|
s.remove_user(uid, &reason);
|
||||||
|
}
|
||||||
|
"kline" => {
|
||||||
|
s.add_xline(
|
||||||
|
XKind::Kline,
|
||||||
|
&format!("*@{ip}"),
|
||||||
|
duration,
|
||||||
|
"filter",
|
||||||
|
&reason,
|
||||||
|
);
|
||||||
|
s.remove_user(uid, &reason);
|
||||||
|
}
|
||||||
|
"gline" => {
|
||||||
|
s.add_xline(
|
||||||
|
XKind::Gline,
|
||||||
|
&format!("*@{ip}"),
|
||||||
|
duration,
|
||||||
|
"filter",
|
||||||
|
&reason,
|
||||||
|
);
|
||||||
|
s.remove_user(uid, &reason);
|
||||||
|
}
|
||||||
|
"zline" => {
|
||||||
|
s.add_xline(XKind::Zline, &ip, duration, "filter", &reason);
|
||||||
|
s.remove_user(uid, &reason);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
ModResult::Deny // the message never reaches the channel/user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
|
vec![Box::new(FilterCmd)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FILTER — manage spam filters (oper). No args = list; `<pattern>` = remove;
|
||||||
|
/// `<pattern> <action> [duration] :<reason>` = add/replace.
|
||||||
|
struct FilterCmd;
|
||||||
|
impl Command for FilterCmd {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"FILTER"
|
||||||
|
}
|
||||||
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||||
|
if !s.is_oper(uid) {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_NOPRIVILEGES,
|
||||||
|
":Permission Denied- You're not an IRC operator",
|
||||||
|
);
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
let nick = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| u.nick.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
match params.first() {
|
||||||
|
None => {
|
||||||
|
let list: Vec<String> = s
|
||||||
|
.ext
|
||||||
|
.get::<Filters>()
|
||||||
|
.map(|f| {
|
||||||
|
f.0.iter()
|
||||||
|
.map(|r| {
|
||||||
|
format!("{} {} {} :{}", r.pattern, r.action, r.duration, r.reason)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
for r in list {
|
||||||
|
s.send(uid, format!(":{} NOTICE {nick} :FILTER {r}", s.name));
|
||||||
|
}
|
||||||
|
s.send(
|
||||||
|
uid,
|
||||||
|
format!(":{} NOTICE {nick} :End of FILTER list", s.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(pattern) => {
|
||||||
|
let pattern = pattern.clone();
|
||||||
|
if params.len() < 2 {
|
||||||
|
let removed = s
|
||||||
|
.ext
|
||||||
|
.get_mut::<Filters>()
|
||||||
|
.map(|f| {
|
||||||
|
let before = f.0.len();
|
||||||
|
f.0.retain(|r| r.pattern != pattern);
|
||||||
|
before != f.0.len()
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
let word = if removed { "removed" } else { "not found" };
|
||||||
|
s.send(
|
||||||
|
uid,
|
||||||
|
format!(":{} NOTICE {nick} :FILTER {word}: {pattern}", s.name),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let action = params[1].clone();
|
||||||
|
let (duration, reason) = match params.get(2).and_then(|p| parse_duration(p)) {
|
||||||
|
Some(d) if params.len() > 3 => (d, params[3].clone()),
|
||||||
|
_ => (
|
||||||
|
0,
|
||||||
|
params
|
||||||
|
.get(2)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "Filtered".to_string()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let f = s.ext.get_or_insert_with::<Filters>(Filters::default);
|
||||||
|
f.0.retain(|r| r.pattern != pattern);
|
||||||
|
f.0.push(SpamFilter {
|
||||||
|
pattern: pattern.clone(),
|
||||||
|
action: action.clone(),
|
||||||
|
duration,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
s.snotice(&format!("{nick} added FILTER {pattern} (action={action})"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,11 @@
|
||||||
pub mod antimixedutf8;
|
pub mod antimixedutf8;
|
||||||
pub mod cloak;
|
pub mod cloak;
|
||||||
pub mod dnsbl;
|
pub mod dnsbl;
|
||||||
|
pub mod filter;
|
||||||
pub mod flood;
|
pub mod flood;
|
||||||
pub mod snoop;
|
pub mod snoop;
|
||||||
|
|
||||||
|
use crate::command::Command;
|
||||||
use crate::module::Module;
|
use crate::module::Module;
|
||||||
|
|
||||||
/// The modules loaded at boot. (Later: load by name from the config.)
|
/// The modules loaded at boot. (Later: load by name from the config.)
|
||||||
|
|
@ -18,5 +20,12 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
|
||||||
Box::new(flood::Flood),
|
Box::new(flood::Flood),
|
||||||
Box::new(cloak::Cloak),
|
Box::new(cloak::Cloak),
|
||||||
Box::new(antimixedutf8::AntiMixedUtf8),
|
Box::new(antimixedutf8::AntiMixedUtf8),
|
||||||
|
Box::new(filter::Filter),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Commands contributed by modules (chained into the core command table), so a
|
||||||
|
/// module that adds a command keeps it in its own file, InspIRCd-style.
|
||||||
|
pub fn module_commands() -> Vec<Box<dyn Command>> {
|
||||||
|
filter::commands().into_iter().collect()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,10 @@ pub struct Server {
|
||||||
pub mline: HashMap<Uid, MlineBatch>, // in-progress inbound multiline batches
|
pub mline: HashMap<Uid, MlineBatch>, // in-progress inbound multiline batches
|
||||||
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
||||||
pub conn_counter: Arc<AtomicU64>, // mints connection uids (for CONNECT dials)
|
pub conn_counter: Arc<AtomicU64>, // mints connection uids (for CONNECT dials)
|
||||||
|
/// Module-owned server state, keyed by type — the InspIRCd `ExtensionItem`
|
||||||
|
/// equivalent. Each `modules/*.rs` stores its own struct here so features live
|
||||||
|
/// in their own file instead of bloating this one.
|
||||||
|
pub ext: Extensible,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
|
|
@ -216,6 +220,7 @@ impl Server {
|
||||||
mline: HashMap::new(),
|
mline: HashMap::new(),
|
||||||
event_tx,
|
event_tx,
|
||||||
conn_counter,
|
conn_counter,
|
||||||
|
ext: Extensible::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue