diff --git a/src/channels.rs b/src/channels.rs index 8e3070e..a45845e 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -906,6 +906,9 @@ impl Server { Some(b'y') => { crate::modules::reputation::score_ban_match(self, uid, &b.mask[2..]) } + Some(b'r') => crate::modules::realnameban::matches(self, uid, &b.mask[2..]), + Some(b'j') => crate::modules::channelban::matches(self, uid, &b.mask[2..]), + Some(b's') => crate::modules::serverban::matches(self, uid, &b.mask[2..]), _ => false, } } else { @@ -1114,8 +1117,10 @@ pub fn normalize_mask(m: &str) -> String { pub fn normalize_ban_mask(m: &str) -> String { let b = m.as_bytes(); if b.len() >= 2 && b[1] == b':' && (b[0] as char).is_ascii_alphabetic() { - // g: (security-group name) and y: (reputation score spec) aren't host masks - if b[0] == b'g' || b[0] == b'y' { + // These extbans carry a name / spec / channel / server, not a host mask, + // so they must not be host-normalised: g: (security group), y: (reputation + // score), r: (realname), j: (channel), s: (server name). + if matches!(b[0], b'g' | b'y' | b'r' | b'j' | b's') { return m.to_string(); } return format!("{}:{}", &m[..1], normalize_mask(&m[2..])); diff --git a/src/modules/channelban.rs b/src/modules/channelban.rs new file mode 100644 index 0000000..d07f324 --- /dev/null +++ b/src/modules/channelban.rs @@ -0,0 +1,47 @@ +//! channelban — the `j:` matching extban: match a user by another channel they +//! are in. `+b j:#lobby` bans everyone who is also in `#lobby`; an optional status +//! prefix narrows it to members at/above that rank, e.g. `+b j:@#staff` matches +//! only ops-or-higher in `#staff`. The channel part is a glob. Dispatched from the +//! channel ban matcher; the logic lives here in its own file. +//! +//! Behaviour reference: InspIRCd's `m_channelban`. Original native Rust. + +use crate::channels::{glob_match, RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE}; +use crate::server::Server; +use crate::Uid; + +/// Map a leading status prefix to the minimum rank it requires; `None` if the +/// first char isn't a prefix (so the whole string is the channel glob). +fn split_prefix(mask: &str) -> (u8, &str) { + match mask.chars().next() { + Some('~') => (RANK_OWNER, &mask[1..]), + Some('&') => (RANK_ADMIN, &mask[1..]), + Some('@') => (RANK_OP, &mask[1..]), + Some('%') => (RANK_HALFOP, &mask[1..]), + Some('+') => (RANK_VOICE, &mask[1..]), + _ => (0, mask), + } +} + +/// Is `uid` a member (at/above the required rank) of a channel matching the glob +/// in `mask` (the part after `j:`)? +pub fn matches(s: &Server, uid: Uid, mask: &str) -> bool { + let (min_rank, changlob) = split_prefix(mask); + let changlob = changlob.to_ascii_lowercase(); + let Some(u) = s.users.get(&uid) else { + return false; + }; + u.channels.iter().any(|key| { + if !glob_match(&changlob, key) { + return false; + } + if min_rank == 0 { + return true; + } + s.channels + .get(key) + .and_then(|ch| ch.members.get(&uid)) + .map(|m| m.rank() >= min_rank) + .unwrap_or(false) + }) +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 47c5c13..527fb01 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -6,6 +6,7 @@ pub mod antimixedutf8; pub mod antirandom; pub mod blockamsg; +pub mod channelban; pub mod chathistory; pub mod cloak; pub mod connectban; @@ -19,10 +20,12 @@ pub mod metadata; pub mod multiline; pub mod network_icon; pub mod profilelink; +pub mod realnameban; pub mod reputation; pub mod restrictcommands; pub mod restrictmsg; pub mod securitygroups; +pub mod serverban; pub mod snoop; pub mod whoisport; diff --git a/src/modules/realnameban.rs b/src/modules/realnameban.rs new file mode 100644 index 0000000..5dc6046 --- /dev/null +++ b/src/modules/realnameban.rs @@ -0,0 +1,18 @@ +//! realnameban — the `r:` matching extban: match a user by their real name +//! (GECOS) instead of their host. `+b r:*some spammer*` bans everyone whose +//! realname matches the glob. Dispatched from the channel ban matcher; the logic +//! lives here in its own file. +//! +//! Behaviour reference: InspIRCd's `m_realnameban`. Original native Rust. + +use crate::channels::glob_match; +use crate::server::Server; +use crate::Uid; + +/// Does `uid`'s realname match the glob `mask` (the part after `r:`)? +pub fn matches(s: &Server, uid: Uid, mask: &str) -> bool { + s.users + .get(&uid) + .map(|u| glob_match(mask, &u.realname)) + .unwrap_or(false) +} diff --git a/src/modules/serverban.rs b/src/modules/serverban.rs new file mode 100644 index 0000000..ef5418d --- /dev/null +++ b/src/modules/serverban.rs @@ -0,0 +1,20 @@ +//! serverban — the `s:` matching extban: match a user by the name of the server +//! they are connected to. `+b s:irc.example.net` bans everyone on that server. +//! Ban matching only ever runs against local users (join happens locally), so a +//! matched user is on this server — we glob the mask against our own name. +//! Dispatched from the channel ban matcher; the logic lives here. +//! +//! Behaviour reference: InspIRCd's `m_serverban`. Original native Rust. + +use crate::channels::glob_match; +use crate::server::Server; +use crate::Uid; + +/// Does `uid`'s server name match the glob `mask` (the part after `s:`)? +pub fn matches(s: &Server, uid: Uid, mask: &str) -> bool { + // local users are on this server; guard on the user still existing + if !s.users.contains_key(&uid) { + return false; + } + glob_match(mask, &s.name) +} diff --git a/src/users.rs b/src/users.rs index 7a23cfe..0af54d9 100644 --- a/src/users.rs +++ b/src/users.rs @@ -449,7 +449,7 @@ impl Server { uid, RPL_ISUPPORT, &format!( - "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgX,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,cgmny WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server", + "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgX,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,cgjmnrsy WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server", self.network ), );