OperServ: JUPE — hold a server name against a rogue link

JUPE <server.name> [reason] introduces a fake server holding the name so
the real one can't link; JUPE DEL <name> squits it; JUPE LIST shows them.
Node-local (the introducing node owns the jupe, so it isn't federated —
that would collide SIDs across nodes) and re-asserted at burst. A fake
server id is allocated per jupe. New JupeServer/Squit actions serialise to
the mid-link SERVER introduction and SQUIT. Admin-only.
This commit is contained in:
Jean Chevronnet 2026-07-14 01:51:07 +00:00
parent 76a7162227
commit 1daebc94a5
No known key found for this signature in database
6 changed files with 179 additions and 2 deletions

49
operserv/src/jupe.rs Normal file
View file

@ -0,0 +1,49 @@
use fedserv_api::{Priv, Sender, ServiceCtx, Store};
// JUPE <server.name> [reason] | JUPE DEL <server.name> | JUPE LIST: hold a
// server name with a fake server so a rogue one can't link (or lift it). Admin-
// only. Node-local: the introducing node owns the jupe.
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
if !from.privs.has(Priv::Admin) {
ctx.notice(me, from.uid, "Access denied — JUPE needs the \x02admin\x02 privilege.");
return;
}
match args.get(1) {
Some(&sub) if sub.eq_ignore_ascii_case("DEL") || sub.eq_ignore_ascii_case("REMOVE") => del(me, from, args.get(2).copied(), ctx, db),
Some(&sub) if sub.eq_ignore_ascii_case("LIST") => list(me, from, ctx, db),
Some(&name) if name.contains('.') => {
let reason = if args.len() > 2 { args[2..].join(" ") } else { "Juped by services".to_string() };
let by = from.account.unwrap_or(from.nick);
let sid = db.jupe_add(name, &format!("({by}) {reason}"));
ctx.jupe(name, &sid, &format!("({by}) {reason}"));
ctx.notice(me, from.uid, format!("\x02{name}\x02 is now juped."));
}
_ => ctx.notice(me, from.uid, "Syntax: JUPE <server.name> [reason] | JUPE DEL <server.name> | JUPE LIST"),
}
}
fn del(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
let Some(name) = name else {
ctx.notice(me, from.uid, "Syntax: JUPE DEL <server.name>");
return;
};
match db.jupe_del(name) {
Some(sid) => {
ctx.squit(&sid, "Jupe lifted");
ctx.notice(me, from.uid, format!("The jupe on \x02{name}\x02 has been lifted."));
}
None => ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't juped.")),
}
}
fn list(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store) {
let jupes = db.jupes();
if jupes.is_empty() {
ctx.notice(me, from.uid, "No servers are juped.");
return;
}
for (name, sid, reason) in &jupes {
ctx.notice(me, from.uid, format!(" \x02{name}\x02 ({sid}) — {reason}"));
}
ctx.notice(me, from.uid, format!("End of jupe list ({} shown).", jupes.len()));
}

View file

@ -29,6 +29,8 @@ mod news;
mod oper;
#[path = "session.rs"]
mod session;
#[path = "jupe.rs"]
mod jupe;
pub struct OperServ {
pub uid: String,
@ -69,6 +71,7 @@ impl Service for OperServ {
Some(cmd) if cmd.eq_ignore_ascii_case("OPER") => oper::handle(me, from, args, ctx, db),
Some(cmd) if cmd.eq_ignore_ascii_case("SESSION") => session::handle_session(me, from, args, ctx, net),
Some(cmd) if cmd.eq_ignore_ascii_case("EXCEPTION") => session::handle_exception(me, from, args, ctx, db),
Some(cmd) if cmd.eq_ignore_ascii_case("JUPE") => jupe::handle(me, from, args, ctx, db),
Some(cmd) if cmd.eq_ignore_ascii_case("HELP") => help(me, from, ctx),
None => help(me, from, ctx),
Some(other) => ctx.notice(me, from.uid, format!("I don't know \x02{other}\x02. Try \x02HELP\x02.")),
@ -77,5 +80,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), \x02GLOBAL\x02 <message> (announce to everyone), \x02KILL\x02 <nick> [reason] (disconnect a user), \x02KICK\x02 <#chan> <nick> [reason], \x02MODE\x02 <#chan> <modes> [params], \x02IGNORE\x02 ADD|DEL|LIST (silence a user from services), \x02STATS\x02 (enforcement overview), \x02SVSNICK\x02 <nick> <newnick>, \x02SVSJOIN\x02 <nick> <#chan> [key], \x02INFO\x02 ADD|DEL <target> (staff notes), \x02NEWS\x02 ADD|DEL|LIST <LOGON|OPER> (announcements), \x02OPER\x02 ADD|DEL|LIST (runtime operators), \x02SESSION\x02 LIST|VIEW + \x02EXCEPTION\x02 ADD|DEL|LIST (per-IP session limits).");
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), \x02GLOBAL\x02 <message> (announce to everyone), \x02KILL\x02 <nick> [reason] (disconnect a user), \x02KICK\x02 <#chan> <nick> [reason], \x02MODE\x02 <#chan> <modes> [params], \x02IGNORE\x02 ADD|DEL|LIST (silence a user from services), \x02STATS\x02 (enforcement overview), \x02SVSNICK\x02 <nick> <newnick>, \x02SVSJOIN\x02 <nick> <#chan> [key], \x02INFO\x02 ADD|DEL <target> (staff notes), \x02NEWS\x02 ADD|DEL|LIST <LOGON|OPER> (announcements), \x02OPER\x02 ADD|DEL|LIST (runtime operators), \x02SESSION\x02 LIST|VIEW + \x02EXCEPTION\x02 ADD|DEL|LIST (per-IP session limits), \x02JUPE\x02 <server> | DEL | LIST.");
}