From 140554719bbaaf9bb732d8cef71c09fedb9c67b7 Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 20:03:47 +0000 Subject: [PATCH] modules: port randquote, disable, restrictchans, channames --- src/channels.rs | 8 +++++++ src/modules/channames.rs | 33 ++++++++++++++++++++++++++ src/modules/disable.rs | 45 ++++++++++++++++++++++++++++++++++++ src/modules/mod.rs | 6 +++++ src/modules/randquote.rs | 44 +++++++++++++++++++++++++++++++++++ src/modules/restrictchans.rs | 36 +++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+) create mode 100644 src/modules/channames.rs create mode 100644 src/modules/disable.rs create mode 100644 src/modules/randquote.rs create mode 100644 src/modules/restrictchans.rs diff --git a/src/channels.rs b/src/channels.rs index 4a49419..f2f295b 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -533,6 +533,14 @@ impl Server { if crate::modules::denychans::intercept(self, uid, name, is_oper) { return; } + // restrictchans — only opers may create new channels (unless whitelisted) + if crate::modules::restrictchans::intercept(self, uid, name, is_oper) { + return; + } + // channames — forbidden characters in new channel names + if crate::modules::channames::intercept(self, uid, name) { + return; + } // an existing channel can refuse the join (+k / +b / +i / +z / +R / +J) if let Some(ch) = self.channels.get(&key) { if let Some(k) = &ch.modes.key { diff --git a/src/modules/channames.rs b/src/modules/channames.rs new file mode 100644 index 0000000..dbf4589 --- /dev/null +++ b/src/modules/channames.rs @@ -0,0 +1,33 @@ +//! channames — restrict which characters may appear in *new* channel names, beyond +//! the protocol minimum. `channames_deny = ` lists forbidden characters (e.g. +//! control codes or fancy Unicode an admin doesn't want in channel names). Existing +//! channels are unaffected. Off unless `channames_deny` is set. Dispatched from +//! `Server::join`. +//! +//! Behaviour reference: InspIRCd's `m_channames`. Original native Rust. + +use crate::numeric::ERR_BADCHANNEL; +use crate::server::Server; +use crate::Uid; + +/// Called from `Server::join`. Returns true when `name` uses a forbidden character +/// (the caller returns without joining). Only creation of new channels is checked. +pub fn intercept(s: &mut Server, uid: Uid, name: &str) -> bool { + let Some(deny) = s.conf("channames_deny").filter(|d| !d.is_empty()) else { + return false; + }; + let deny: Vec = deny.chars().collect(); + // joining an existing channel is always allowed + if s.channels.contains_key(&name.to_ascii_lowercase()) { + return false; + } + if name.chars().any(|c| deny.contains(&c)) { + s.numeric( + uid, + ERR_BADCHANNEL, + &format!("{name} :Channel name contains characters not permitted here"), + ); + return true; + } + false +} diff --git a/src/modules/disable.rs b/src/modules/disable.rs new file mode 100644 index 0000000..f69a03a --- /dev/null +++ b/src/modules/disable.rs @@ -0,0 +1,45 @@ +//! disable — refuse a configured set of commands to ordinary users (opers bypass). +//! `disabled_commands = LIST WHO KNOCK` (space-separated; repeatable). A disabled +//! command replies with `421` as if it didn't exist. Off unless configured. +//! +//! Behaviour reference: InspIRCd's `m_disable`. Original native Rust. + +use crate::module::{ModResult, Module}; +use crate::numeric::ERR_UNKNOWNCOMMAND; +use crate::server::Server; +use crate::Uid; + +pub struct Disable; + +impl Module for Disable { + fn name(&self) -> &'static str { + "disable" + } + + fn on_pre_command( + &mut self, + srv: &mut Server, + uid: Uid, + cmd: &str, + _params: &[String], + ) -> ModResult { + // opers are never restricted + if srv.is_oper(uid) { + return ModResult::Passthru; + } + let disabled = srv + .conf_all("disabled_commands") + .iter() + .flat_map(|line| line.split_whitespace()) + .any(|c| c.eq_ignore_ascii_case(cmd)); + if disabled { + srv.numeric( + uid, + ERR_UNKNOWNCOMMAND, + &format!("{cmd} :This command has been disabled by the administrator."), + ); + return ModResult::Deny; + } + ModResult::Passthru + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 960ae54..aa5601c 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -7,6 +7,7 @@ pub mod account_registration; pub mod antimixedutf8; pub mod antirandom; pub mod blockamsg; +pub mod channames; pub mod channelban; pub mod chathistory; pub mod cloak; @@ -14,6 +15,7 @@ pub mod cloudflare_challenge; pub mod connectban; pub mod connflood; pub mod denychans; +pub mod disable; pub mod dnsbl; pub mod extended_isupport; pub mod extjwt; @@ -31,9 +33,11 @@ pub mod multiline; pub mod network_icon; pub mod password_hash; pub mod profilelink; +pub mod randquote; pub mod realnameban; pub mod recaptcha; pub mod reputation; +pub mod restrictchans; pub mod restrictcommands; pub mod restrictmsg; pub mod rpc; @@ -70,6 +74,8 @@ pub fn default_modules() -> Vec> { Box::new(cloudflare_challenge::CloudflareChallenge), Box::new(filehost::FileHost), Box::new(irccloudtags::IrcCloudTags), + Box::new(randquote::RandQuote), + Box::new(disable::Disable), ] } diff --git a/src/modules/randquote.rs b/src/modules/randquote.rs new file mode 100644 index 0000000..3ae93f6 --- /dev/null +++ b/src/modules/randquote.rs @@ -0,0 +1,44 @@ +//! randquote — greet each connecting user with a random line from a configured +//! set of quotes. Off unless one or more `randquote = ` are configured. +//! +//! Behaviour reference: InspIRCd's `m_randquote`. Original native Rust. + +use openssl::rand::rand_bytes; + +use crate::module::Module; +use crate::server::Server; +use crate::Uid; + +/// A random index in `0..n` from the CSPRNG (0 if `n == 0` or on error). +fn rand_below(n: usize) -> usize { + if n == 0 { + return 0; + } + let mut b = [0u8; 8]; + if rand_bytes(&mut b).is_err() { + return 0; + } + (u64::from_le_bytes(b) % n as u64) as usize +} + +pub struct RandQuote; + +impl Module for RandQuote { + fn name(&self) -> &'static str { + "randquote" + } + + fn on_user_connect(&mut self, srv: &mut Server, uid: Uid) { + let quotes = srv.conf_all("randquote"); + if quotes.is_empty() { + return; + } + let quote = quotes[rand_below(quotes.len())].clone(); + let nick = srv + .users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_default(); + srv.send(uid, format!(":{} NOTICE {nick} :{quote}", srv.name)); + } +} diff --git a/src/modules/restrictchans.rs b/src/modules/restrictchans.rs new file mode 100644 index 0000000..7f052be --- /dev/null +++ b/src/modules/restrictchans.rs @@ -0,0 +1,36 @@ +//! restrictchans — only opers may *create* new channels; everyone can still join +//! existing ones. A `restrictchan = ` whitelist lets ordinary users create +//! channels whose name matches (e.g. `restrictchan = #public-*`). Off unless +//! `restrictchans = yes`. Dispatched from `Server::join` (like denychans). +//! +//! Behaviour reference: InspIRCd's `m_restrictchans`. Original native Rust. + +use crate::channels::glob_match; +use crate::numeric::ERR_BADCHANNEL; +use crate::server::Server; +use crate::Uid; + +/// Called from `Server::join`. Returns true when creating `name` should be blocked +/// (the caller returns without joining). Opers and joins to *existing* channels pass. +pub fn intercept(s: &mut Server, uid: Uid, name: &str, is_oper: bool) -> bool { + if is_oper || !s.conf_bool("restrictchans", false) { + return false; + } + // joining a channel that already exists is always fine + if s.channels.contains_key(&name.to_ascii_lowercase()) { + return false; + } + // creating a new one: allowed only if it matches a whitelist glob + if s.conf_all("restrictchan") + .iter() + .any(|g| glob_match(g, name)) + { + return false; + } + s.numeric( + uid, + ERR_BADCHANNEL, + &format!("{name} :Only IRC operators may create new channels here"), + ); + true +}