diff --git a/operserv/src/lib.rs b/operserv/src/lib.rs index 878ba74..0eb588c 100644 --- a/operserv/src/lib.rs +++ b/operserv/src/lib.rs @@ -63,6 +63,7 @@ impl Service for OperServ { Some(cmd) if cmd.eq_ignore_ascii_case("SQLINE") => xline::SQLINE.handle(me, from, args, ctx, db), Some(cmd) if cmd.eq_ignore_ascii_case("SNLINE") => xline::SNLINE.handle(me, from, args, ctx, db), Some(cmd) if cmd.eq_ignore_ascii_case("SHUN") => xline::SHUN.handle(me, from, args, ctx, db), + Some(cmd) if cmd.eq_ignore_ascii_case("CBAN") => xline::CBAN.handle(me, from, args, ctx, db), Some(cmd) if cmd.eq_ignore_ascii_case("GLOBAL") => global::handle(me, from, args, ctx), Some(cmd) if cmd.eq_ignore_ascii_case("KILL") => kill::handle(me, from, args, ctx, net), Some(cmd) if cmd.eq_ignore_ascii_case("KICK") => kick::handle(me, from, args, ctx, net), @@ -87,5 +88,5 @@ impl Service for OperServ { } fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx) { - ctx.notice(me, from.uid, "OperServ holds network operator tools (Priv::Admin): \x02AKILL\x02 ADD|DEL|LIST (user@host bans), \x02SQLINE\x02 ADD|DEL|LIST (nick bans), \x02SNLINE\x02 ADD|DEL|LIST (realname bans), \x02SHUN\x02 ADD|DEL|LIST (silence a host), \x02CHANKILL\x02 <#chan> (clear a channel), \x02GLOBAL\x02 (announce to everyone), \x02KILL\x02 [reason] (disconnect a user), \x02KICK\x02 <#chan> [reason], \x02MODE\x02 <#chan> [params], \x02IGNORE\x02 ADD|DEL|LIST (silence a user from services), \x02STATS\x02 (enforcement overview), \x02SVSNICK\x02 , \x02SVSJOIN\x02 <#chan> [key], \x02INFO\x02 ADD|DEL (staff notes), \x02NEWS\x02 ADD|DEL|LIST (announcements), \x02OPER\x02 ADD|DEL|LIST (runtime operators), \x02SESSION\x02 LIST|VIEW + \x02EXCEPTION\x02 ADD|DEL|LIST (per-IP session limits), \x02JUPE\x02 | DEL | LIST, \x02LOGSEARCH\x02 [pattern] (search the action log)."); + ctx.notice(me, from.uid, "OperServ holds network operator tools (Priv::Admin): \x02AKILL\x02 ADD|DEL|LIST (user@host bans), \x02SQLINE\x02 ADD|DEL|LIST (nick bans), \x02SNLINE\x02 ADD|DEL|LIST (realname bans), \x02SHUN\x02 ADD|DEL|LIST (silence a host), \x02CBAN\x02 ADD|DEL|LIST (ban a channel name), \x02CHANKILL\x02 <#chan> (clear a channel), \x02GLOBAL\x02 (announce to everyone), \x02KILL\x02 [reason] (disconnect a user), \x02KICK\x02 <#chan> [reason], \x02MODE\x02 <#chan> [params], \x02IGNORE\x02 ADD|DEL|LIST (silence a user from services), \x02STATS\x02 (enforcement overview), \x02SVSNICK\x02 , \x02SVSJOIN\x02 <#chan> [key], \x02INFO\x02 ADD|DEL (staff notes), \x02NEWS\x02 ADD|DEL|LIST (announcements), \x02OPER\x02 ADD|DEL|LIST (runtime operators), \x02SESSION\x02 LIST|VIEW + \x02EXCEPTION\x02 ADD|DEL|LIST (per-IP session limits), \x02JUPE\x02 | DEL | LIST, \x02LOGSEARCH\x02 [pattern] (search the action log)."); } diff --git a/operserv/src/xline.rs b/operserv/src/xline.rs index 9e0fe16..fdb15e4 100644 --- a/operserv/src/xline.rs +++ b/operserv/src/xline.rs @@ -127,6 +127,10 @@ pub const SNLINE: Xline = Xline { kind: "R", name: "SNLINE", target: "realname-r // drops their commands — a quieter alternative to an AKILL. pub const SHUN: Xline = Xline { kind: "SHUN", name: "SHUN", target: "user@host", normalize: norm_userhost }; +// CBAN: a channel-name ban. Users can't join (or create) a channel matching the +// mask, which is a channel glob like `#warez*`. +pub const CBAN: Xline = Xline { kind: "CBAN", name: "CBAN", target: "#channel", normalize: norm_channel }; + fn norm_userhost(input: &str) -> Option { let body = input.rsplit('!').next().unwrap_or(input); let (user, host) = body.split_once('@')?; @@ -151,10 +155,19 @@ fn norm_realname(input: &str) -> Option { (!input.is_empty()).then(|| input.to_string()) } +fn norm_channel(input: &str) -> Option { + // A channel-name glob: must carry a channel prefix and a non-empty body. + if (!input.starts_with('#') && !input.starts_with('&')) || input.len() < 2 || input.contains(|c: char| c.is_whitespace() || c == ',') { + return None; + } + Some(input.to_ascii_lowercase()) +} + // A mask whose every meaningful character is a wildcard would match nearly all. +// A leading channel prefix is ignored so `#*` counts as too wide. fn too_wide(mask: &str) -> bool { - let trivial = |s: &str| s.chars().all(|c| c == '*' || c == '?' || c == '.' || c == '@'); - trivial(mask) + let body = mask.strip_prefix('#').or_else(|| mask.strip_prefix('&')).unwrap_or(mask); + body.is_empty() || body.chars().all(|c| c == '*' || c == '?' || c == '.' || c == '@') } fn human_secs(secs: u64) -> String { diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 714807e..1419d2c 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1593,6 +1593,7 @@ fn ban_kind_label(kind: &str) -> &'static str { "Q" => "nick ban", "R" => "realname ban", "SHUN" => "shun", + "CBAN" => "channel ban", _ => "network ban", } } @@ -4053,6 +4054,9 @@ mod tests { assert!(os(&mut e, "000AAAAAS", "SNLINE ADD .*free.money.* spambot").iter().any(|a| matches!(a, NetAction::AddLine { kind, mask, .. } if kind == "R" && mask == ".*free.money.*")), "R-line added"); // SHUN drives a SHUN X-line on a user@host mask. assert!(os(&mut e, "000AAAAAS", "SHUN ADD *@noisy.host quiet down").iter().any(|a| matches!(a, NetAction::AddLine { kind, mask, .. } if kind == "SHUN" && mask == "*@noisy.host")), "shun added"); + // CBAN drives a channel-name ban; an all-wildcard channel mask is refused. + assert!(os(&mut e, "000AAAAAS", "CBAN ADD #warez* piracy").iter().any(|a| matches!(a, NetAction::AddLine { kind, mask, .. } if kind == "CBAN" && mask == "#warez*")), "cban added"); + assert!(os(&mut e, "000AAAAAS", "CBAN ADD #* everything").iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("too wide"))), "wildcard channel refused"); // GLOBAL fans out to every user via the $* server glob. let out = os(&mut e, "000AAAAAS", "GLOBAL rebooting in 5");