diff --git a/src/channels.rs b/src/channels.rs index 2ae18d3..81d14da 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -405,6 +405,17 @@ impl Server { { return; // unknown user, or already joined } + // CBAN — a forbidden channel name (opers bypass) + if !self.users.get(&uid).map(|u| u.flags.oper).unwrap_or(false) { + if let Some(reason) = self.matched_cban(&key) { + self.numeric( + uid, + ERR_BADCHANNEL, + &format!("{name} :Channel is CBAN'd: {reason}"), + ); + return; + } + } // an existing channel can refuse the join (+k / +b / +i / +l) if let Some(ch) = self.channels.get(&key) { if let Some(k) = &ch.modes.key { diff --git a/src/coremods/core_info.rs b/src/coremods/core_info.rs index 5f1d1e1..de447dc 100644 --- a/src/coremods/core_info.rs +++ b/src/coremods/core_info.rs @@ -14,9 +14,56 @@ pub fn commands() -> Vec> { Box::new(Motd), Box::new(VersionCmd), Box::new(Links), + Box::new(SslInfo), ] } +/// SSLINFO — report a user's TLS status and client-cert fingerprint (InspIRCd +/// `m_sslinfo`). You may query yourself; querying another user requires oper. +struct SslInfo; +impl Command for SslInfo { + fn name(&self) -> &'static str { + "SSLINFO" + } + fn min_params(&self) -> usize { + 1 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + let target = params[0].clone(); + let Some(tuid) = s.find_nick(&target) else { + s.numeric( + uid, + ERR_NOSUCHNICK, + &format!("{target} :No such nick/channel"), + ); + return CmdResult::Fail; + }; + if tuid != uid && !s.is_oper(uid) { + s.numeric(uid, ERR_NOPRIVILEGES, ":You may only SSLINFO yourself"); + return CmdResult::Fail; + } + let asker = s + .users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_else(|| "*".to_string()); + let (nick, secure, certfp) = { + let u = &s.users[&tuid]; + (u.nick.clone(), u.secure, u.certfp.clone()) + }; + let tls = if secure { "yes" } else { "no" }; + let fp = certfp.unwrap_or_else(|| "none".to_string()); + s.send( + uid, + format!( + ":{} NOTICE {asker} :SSLINFO {nick}: TLS={tls} certfp={fp}", + s.name + ), + ); + CmdResult::Ok + } +} + /// LINKS — the servers this one knows about (itself + every linked peer). struct Links; impl Command for Links { diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index 6db5873..d6fc482 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -34,6 +34,7 @@ pub fn commands() -> Vec> { Box::new(Eline), Box::new(Shun), Box::new(Qline), + Box::new(Cban), Box::new(Connect), Box::new(ChgHost), Box::new(ChgIdent), @@ -683,6 +684,21 @@ impl Command for Qline { } } +/// CBAN — forbid a channel-name glob (opers bypass it). Mask alone removes; a +/// mask + duration adds. InspIRCd `m_cban`. +struct Cban; +impl Command for Cban { + fn name(&self) -> &'static str { + "CBAN" + } + fn min_params(&self) -> usize { + 1 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + do_xline(s, uid, params, XKind::Cban) + } +} + /// CONNECT — dial a configured server link on demand. `CONNECT `. struct Connect; impl Command for Connect { diff --git a/src/numeric.rs b/src/numeric.rs index 28d77e5..6845e8a 100644 --- a/src/numeric.rs +++ b/src/numeric.rs @@ -31,6 +31,7 @@ pub const ERR_UNAVAILRESOURCE: u16 = 437; // channel temporarily unavailable (+j pub const ERR_LINKCHANNEL: u16 = 470; // +L — you were redirected to another channel pub const ERR_DELAYREJOIN: u16 = 495; // +J — must wait before rejoining after a kick pub const ERR_CANTSENDTOUSER: u16 = 531; // +c — no shared channel with the target +pub const ERR_BADCHANNEL: u16 = 926; // CBAN — this channel name is forbidden pub const RPL_ENDOFSPAMFILTER: u16 = 940; // end of the +g word-filter list pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry pub const RPL_KNOCK: u16 = 710; // channel gets the knock diff --git a/src/xline.rs b/src/xline.rs index 5b3a2ef..4887e3f 100644 --- a/src/xline.rs +++ b/src/xline.rs @@ -15,6 +15,7 @@ pub enum XKind { Eline, // user@host / ip EXEMPT from K/G/Z-lines Shun, // user@host allowed to connect but whose commands are dropped Qline, // a reserved/forbidden nick glob + Cban, // a forbidden channel-name glob } impl XKind { @@ -26,6 +27,7 @@ impl XKind { XKind::Eline => "E", XKind::Shun => "SHUN", XKind::Qline => "Q", + XKind::Cban => "CBAN", } } } @@ -109,6 +111,20 @@ impl Server { .map(|x| x.reason.clone()) } + /// The reason channel `chan` is CBAN'd (forbidden), if any. Case-insensitive. + pub fn matched_cban(&self, chan: &str) -> Option { + let n = now(); + let c = chan.to_ascii_lowercase(); + self.xlines + .iter() + .find(|x| { + x.kind == XKind::Cban + && (x.expires == 0 || x.expires > n) + && glob_match(&x.mask.to_ascii_lowercase(), &c) + }) + .map(|x| x.reason.clone()) + } + /// The reason a `user@host` / `ip` is banned by an active x-line, if any. /// An E-line (exemption) overrides every K/G/Z-line. pub fn matched_xline(&self, ident: &str, host: &str, ip: &str) -> Option {