diff --git a/echoircd.conf.example b/echoircd.conf.example index af0dc79..4741162 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -117,6 +117,10 @@ amu_target = both # hidewhois_hide_server = yes # hide 312 # hidewhois_hide_idle = yes # hide 317 # hidewhois_hide_secure = yes # hide 671 +# --- banredirect (m_banredirect): no config needed — it extends ban syntax. A +# ban `+b $<#channel>` bounces a matching user into #channel instead of +# refusing them, e.g. /MODE #main +b *!*@*.spammer.net$#quarantine +# The redirect fires at most once (never loops). # --- solvemsg (m_solvemsg): an un-vouched user must answer one arithmetic # question before their private messages are delivered (opers & logged-in # accounts are exempt). Cheap anti-spam-bot gate. diff --git a/src/channels.rs b/src/channels.rs index 89fca91..a46fe22 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -560,6 +560,21 @@ impl Server { // (both honour the g: security-group extban) if self.ban_list_hit(uid, &ch.bans) && !self.ban_list_hit(uid, &ch.excepts) { if !is_oper { + // banredirect: `+b mask$#chan` bounces the user into #chan (once) + if let Some(t) = crate::modules::banredirect::redirect_target(self, uid, &key) { + let tl = t.to_ascii_lowercase(); + if !self.in_redirect && tl != key && !self.is_member(uid, &tl) { + self.numeric( + uid, + ERR_LINKCHANNEL, + &format!("{name} {t} :Cannot join channel (+b), redirecting"), + ); + self.in_redirect = true; + self.join(uid, &t, None); + self.in_redirect = false; + return; + } + } self.numeric( uid, ERR_BANNEDFROMCHAN, @@ -925,7 +940,8 @@ impl Server { _ => false, } } else { - glob_match(&b.mask, &who) + // strip any `$#chan` banredirect suffix before matching the mask + glob_match(crate::modules::banredirect::mask_part(&b.mask), &who) } }) } diff --git a/src/modules/banredirect.rs b/src/modules/banredirect.rs new file mode 100644 index 0000000..c502553 --- /dev/null +++ b/src/modules/banredirect.rs @@ -0,0 +1,31 @@ +//! banredirect — a ban of the form `+b $<#channel>` bounces a matching, +//! banned user into `#channel` instead of refusing them outright. The redirect +//! fires at most once, guarded by `Server.in_redirect` (shared with the `+L` +//! full-channel redirect), so it can never loop. Reference: InspIRCd's +//! `m_banredirect`. Original native Rust. + +use crate::channels::glob_match; +use crate::server::Server; +use crate::Uid; + +/// The mask part of a ban, without any `$#chan` redirect suffix — used both for +/// matching (a redirect ban must still *block*) and for reading the target. +pub fn mask_part(mask: &str) -> &str { + mask.split('$').next().unwrap_or(mask) +} + +/// If `uid` is caught by a redirect ban (`mask$#chan`) in `key` with no matching +/// exception, return the target channel. The caller has already established the +/// user is banned; this extracts the `$#chan` destination. +pub fn redirect_target(s: &Server, uid: Uid, key: &str) -> Option { + let ch = s.channels.get(key)?; + let who = s.users.get(&uid)?.prefix(); + // a matching +e exception cancels the ban, hence the redirect + if ch.excepts.iter().any(|e| glob_match(mask_part(&e.mask), &who)) { + return None; + } + ch.bans.iter().find_map(|b| { + let (mask, redir) = b.mask.split_once('$')?; + (redir.starts_with('#') && glob_match(mask, &who)).then(|| redir.to_string()) + }) +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index d3842d1..adee0cb 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -6,6 +6,7 @@ pub mod account_registration; pub mod antimixedutf8; pub mod antirandom; +pub mod banredirect; pub mod blockamsg; pub mod channames; pub mod channelban;