dccallow: block unwanted DCC sends/chat + DCCALLOW +/-/LIST allow-list command
This commit is contained in:
parent
61954f6c5c
commit
0bf6992d8a
3 changed files with 226 additions and 0 deletions
|
|
@ -117,6 +117,13 @@ amu_target = both
|
||||||
# hidewhois_hide_server = yes # hide 312
|
# hidewhois_hide_server = yes # hide 312
|
||||||
# hidewhois_hide_idle = yes # hide 317
|
# hidewhois_hide_idle = yes # hide 317
|
||||||
# hidewhois_hide_secure = yes # hide 671
|
# hidewhois_hide_secure = yes # hide 671
|
||||||
|
# --- dccallow (m_dccallow): block unwanted DCC transfers unless the recipient
|
||||||
|
# ran /DCCALLOW +<nick>. Blocked file globs are repeatable; blockchat also
|
||||||
|
# gates DCC CHAT. Recipients manage their allow-list with DCCALLOW +/-/LIST.
|
||||||
|
# dccallow_blockfile = *.exe
|
||||||
|
# dccallow_blockfile = *.scr
|
||||||
|
# dccallow_blockchat = yes
|
||||||
|
# dccallow_maxentries = 20
|
||||||
# --- conn_waitpong (m_conn_waitpong): hold registration until the client answers
|
# --- conn_waitpong (m_conn_waitpong): hold registration until the client answers
|
||||||
# a server PING with the exact cookie — filters bots that never PONG. Real
|
# a server PING with the exact cookie — filters bots that never PONG. Real
|
||||||
# clients auto-reply, so it's transparent to them.
|
# clients auto-reply, so it's transparent to them.
|
||||||
|
|
|
||||||
216
src/modules/dccallow.rs
Normal file
216
src/modules/dccallow.rs
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
//! dccallow — block unwanted DCC file transfers (and optionally DCC CHAT) unless
|
||||||
|
//! the recipient has explicitly allowed the sender with `/DCCALLOW +<nick>`.
|
||||||
|
//! Mirrors InspIRCd's `m_dccallow`. Config (all optional, all runtime-read):
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! dccallow_blockfile = *.exe # repeatable: filename globs to block on DCC SEND
|
||||||
|
//! dccallow_blockfile = *.scr
|
||||||
|
//! dccallow_blockchat = yes # also block DCC CHAT (default: no)
|
||||||
|
//! dccallow_maxentries = 20 # per-user allow-list cap (default 20)
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! A user's allow-list ("nicks I permit to DCC me") lives on their `User.ext`, so
|
||||||
|
//! it vanishes cleanly when they quit. Original native Rust.
|
||||||
|
|
||||||
|
use crate::channels::glob_match;
|
||||||
|
use crate::command::{CmdResult, Command};
|
||||||
|
use crate::module::{ModResult, Module};
|
||||||
|
use crate::server::Server;
|
||||||
|
use crate::Uid;
|
||||||
|
|
||||||
|
/// Nicks (lowercased) a user permits to DCC them. Stored on the recipient's ext.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Allowed(Vec<String>);
|
||||||
|
|
||||||
|
/// A parsed DCC CTCP request.
|
||||||
|
enum Dcc {
|
||||||
|
Send(String), // SEND <filename> ...
|
||||||
|
Chat, // CHAT chat ...
|
||||||
|
Other, // RESUME/ACCEPT/etc. — not gated
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a `\x01DCC …\x01` CTCP body; `None` if the text isn't a DCC request.
|
||||||
|
/// Handles a quoted filename that contains spaces (`"my file.exe"`).
|
||||||
|
fn parse_dcc(text: &str) -> Option<Dcc> {
|
||||||
|
let inner = text.strip_prefix('\u{1}')?;
|
||||||
|
let inner = inner.strip_suffix('\u{1}').unwrap_or(inner);
|
||||||
|
let mut it = inner.split_whitespace();
|
||||||
|
if !it.next()?.eq_ignore_ascii_case("DCC") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let sub = it.next()?;
|
||||||
|
if sub.eq_ignore_ascii_case("SEND") {
|
||||||
|
let first = it.next()?;
|
||||||
|
let fname = if let Some(stripped) = first.strip_prefix('"') {
|
||||||
|
match stripped.strip_suffix('"') {
|
||||||
|
Some(one) => one.to_string(), // "name" — single token
|
||||||
|
None => {
|
||||||
|
let mut acc = stripped.to_string();
|
||||||
|
for tok in it.by_ref() {
|
||||||
|
acc.push(' ');
|
||||||
|
if let Some(end) = tok.strip_suffix('"') {
|
||||||
|
acc.push_str(end);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
acc.push_str(tok);
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
first.to_string()
|
||||||
|
};
|
||||||
|
Some(Dcc::Send(fname))
|
||||||
|
} else if sub.eq_ignore_ascii_case("CHAT") {
|
||||||
|
Some(Dcc::Chat)
|
||||||
|
} else {
|
||||||
|
Some(Dcc::Other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DccAllow;
|
||||||
|
|
||||||
|
impl Module for DccAllow {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"dccallow"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_pre_message(
|
||||||
|
&mut self,
|
||||||
|
s: &mut Server,
|
||||||
|
uid: Uid,
|
||||||
|
target: &str,
|
||||||
|
text: &str,
|
||||||
|
) -> ModResult {
|
||||||
|
let Some(dcc) = parse_dcc(text) else {
|
||||||
|
return ModResult::Passthru; // not a DCC request
|
||||||
|
};
|
||||||
|
let Some(tuid) = s.find_nick(target) else {
|
||||||
|
return ModResult::Passthru; // DCC only makes sense user-to-user
|
||||||
|
};
|
||||||
|
if tuid == uid {
|
||||||
|
return ModResult::Passthru;
|
||||||
|
}
|
||||||
|
let sender = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
|
||||||
|
let low = sender.to_ascii_lowercase();
|
||||||
|
// recipient already whitelisted this sender?
|
||||||
|
let allowed = s
|
||||||
|
.users
|
||||||
|
.get(&tuid)
|
||||||
|
.and_then(|u| u.ext.get::<Allowed>())
|
||||||
|
.map(|a| a.0.contains(&low))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if allowed {
|
||||||
|
return ModResult::Passthru;
|
||||||
|
}
|
||||||
|
let (block, what) = match &dcc {
|
||||||
|
Dcc::Send(fname) => {
|
||||||
|
let hit = s
|
||||||
|
.conf_all("dccallow_blockfile")
|
||||||
|
.iter()
|
||||||
|
.any(|p| glob_match(p, fname));
|
||||||
|
(hit, format!("the file \"{fname}\""))
|
||||||
|
}
|
||||||
|
Dcc::Chat => (s.conf_bool("dccallow_blockchat", false), "a DCC CHAT".to_string()),
|
||||||
|
Dcc::Other => (false, String::new()),
|
||||||
|
};
|
||||||
|
if !block {
|
||||||
|
return ModResult::Passthru;
|
||||||
|
}
|
||||||
|
let tnick = s.users.get(&tuid).map(|u| u.nick.clone()).unwrap_or_default();
|
||||||
|
s.send(
|
||||||
|
uid,
|
||||||
|
format!(
|
||||||
|
":{} NOTICE {sender} :*** Your DCC to {tnick} was blocked; they must /DCCALLOW +{sender} first.",
|
||||||
|
s.name
|
||||||
|
),
|
||||||
|
);
|
||||||
|
s.send(
|
||||||
|
tuid,
|
||||||
|
format!(
|
||||||
|
":{} NOTICE {tnick} :*** {sender} tried to send you {what} — blocked. /DCCALLOW +{sender} to allow it, then ask them to resend.",
|
||||||
|
s.name
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ModResult::Deny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
|
vec![Box::new(DccAllowCmd)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DCCALLOW `+nick` / `-nick` / `LIST` / `HELP` — manage the nicks you let DCC you.
|
||||||
|
struct DccAllowCmd;
|
||||||
|
impl Command for DccAllowCmd {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"DCCALLOW"
|
||||||
|
}
|
||||||
|
fn min_params(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||||
|
let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
|
||||||
|
let note = |s: &Server, msg: String| s.send(uid, format!(":{} NOTICE {nick} :{msg}", s.name));
|
||||||
|
let arg = ¶ms[0];
|
||||||
|
|
||||||
|
if arg.eq_ignore_ascii_case("LIST") {
|
||||||
|
let list = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.and_then(|u| u.ext.get::<Allowed>())
|
||||||
|
.map(|a| a.0.join(", "))
|
||||||
|
.unwrap_or_default();
|
||||||
|
note(
|
||||||
|
s,
|
||||||
|
if list.is_empty() {
|
||||||
|
"*** Your DCCALLOW list is empty.".to_string()
|
||||||
|
} else {
|
||||||
|
format!("*** DCCALLOW list: {list}")
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return CmdResult::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (add, name) = match arg.strip_prefix('+') {
|
||||||
|
Some(n) => (true, n),
|
||||||
|
None => match arg.strip_prefix('-') {
|
||||||
|
Some(n) => (false, n),
|
||||||
|
None => {
|
||||||
|
note(s, "*** Usage: DCCALLOW +<nick> | -<nick> | LIST".to_string());
|
||||||
|
return CmdResult::Ok;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if name.is_empty() {
|
||||||
|
note(s, "*** DCCALLOW: no nick given.".to_string());
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
let low = name.to_ascii_lowercase();
|
||||||
|
let max = s.conf_num("dccallow_maxentries", 20usize);
|
||||||
|
// mutate under the borrow, then drop it before sending the notice
|
||||||
|
let msg = {
|
||||||
|
let Some(u) = s.users.get_mut(&uid) else {
|
||||||
|
return CmdResult::Fail;
|
||||||
|
};
|
||||||
|
let a = u.ext.get_or_insert_with(Allowed::default);
|
||||||
|
if add {
|
||||||
|
if a.0.contains(&low) {
|
||||||
|
format!("*** {name} is already on your DCCALLOW list.")
|
||||||
|
} else if a.0.len() >= max {
|
||||||
|
format!("*** Your DCCALLOW list is full ({max}).")
|
||||||
|
} else {
|
||||||
|
a.0.push(low);
|
||||||
|
format!("*** Added {name} to your DCCALLOW list.")
|
||||||
|
}
|
||||||
|
} else if let Some(pos) = a.0.iter().position(|n| n == &low) {
|
||||||
|
a.0.remove(pos);
|
||||||
|
format!("*** Removed {name} from your DCCALLOW list.")
|
||||||
|
} else {
|
||||||
|
format!("*** {name} was not on your DCCALLOW list.")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
note(s, msg);
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ pub mod conn_waitpong;
|
||||||
pub mod connectban;
|
pub mod connectban;
|
||||||
pub mod connflood;
|
pub mod connflood;
|
||||||
pub mod customtitle;
|
pub mod customtitle;
|
||||||
|
pub mod dccallow;
|
||||||
pub mod denychans;
|
pub mod denychans;
|
||||||
pub mod disable;
|
pub mod disable;
|
||||||
pub mod dnsbl;
|
pub mod dnsbl;
|
||||||
|
|
@ -84,6 +85,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
|
||||||
Box::new(randquote::RandQuote),
|
Box::new(randquote::RandQuote),
|
||||||
Box::new(disable::Disable),
|
Box::new(disable::Disable),
|
||||||
Box::new(maphide::MapHide),
|
Box::new(maphide::MapHide),
|
||||||
|
Box::new(dccallow::DccAllow),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,6 +118,7 @@ pub fn module_commands() -> Vec<Box<dyn Command>> {
|
||||||
.chain(tline::commands())
|
.chain(tline::commands())
|
||||||
.chain(rmode::commands())
|
.chain(rmode::commands())
|
||||||
.chain(customtitle::commands())
|
.chain(customtitle::commands())
|
||||||
|
.chain(dccallow::commands())
|
||||||
.chain(geoip::commands())
|
.chain(geoip::commands())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue