services: SVSHOLD nick reservation, SVSTOPIC, SVSOPER, SVSCMODE over s2s (+ stats S)
This commit is contained in:
parent
f371ed0a18
commit
b76fbfdb79
4 changed files with 166 additions and 6 deletions
|
|
@ -243,13 +243,14 @@ impl Command for Stats {
|
|||
s.numeric(uid, RPL_STATSOLINE, &format!("O * * {n} :oper"));
|
||||
}
|
||||
}
|
||||
'k' | 'g' | 'z' | 'e' | 'q' | 's' => {
|
||||
'k' | 'g' | 'z' | 'e' | 'q' | 's' | 'S' => {
|
||||
let kind = match letter {
|
||||
'k' => XKind::Kline,
|
||||
'g' => XKind::Gline,
|
||||
'z' => XKind::Zline,
|
||||
'e' => XKind::Eline,
|
||||
'q' => XKind::Qline,
|
||||
'S' => XKind::Svshold,
|
||||
_ => XKind::Shun,
|
||||
};
|
||||
let rows: Vec<String> = s
|
||||
|
|
|
|||
|
|
@ -422,9 +422,9 @@ impl Command for Nick {
|
|||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
// Q-line: a reserved nick is refused (opers and services bypass)
|
||||
// Q-line / SVSHOLD: a reserved nick is refused (opers and services bypass)
|
||||
if !s.is_oper(uid) {
|
||||
if let Some(reason) = s.matched_qline(newnick) {
|
||||
if let Some(reason) = s.nick_reserved(newnick) {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_ERRONEUSNICKNAME,
|
||||
|
|
|
|||
138
src/link.rs
138
src/link.rs
|
|
@ -167,6 +167,10 @@ impl Server {
|
|||
"SVSMODE" if registered => self.link_svsmode(uid, msg),
|
||||
"SVSLOGIN" if registered => self.link_svslogin(uid, msg),
|
||||
"SVSLOGOUT" if registered => self.link_svslogout(uid, msg),
|
||||
"SVSHOLD" if registered => self.link_svshold(uid, msg),
|
||||
"SVSTOPIC" if registered => self.link_svstopic(uid, msg),
|
||||
"SVSOPER" if registered => self.link_svsoper(uid, msg),
|
||||
"SVSCMODE" if registered => self.link_svscmode(uid, msg),
|
||||
"ENCAP" if registered => self.link_encap(uid, msg),
|
||||
"METADATA" if registered => self.link_metadata(uid, msg),
|
||||
"SASL" if registered => self.link_sasl(uid, msg),
|
||||
|
|
@ -523,6 +527,140 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
/// A display name for whoever sourced a services command: the source uuid's
|
||||
/// nick if we know it, else the raw source, else "services".
|
||||
fn link_setter(&self, msg: &Message) -> String {
|
||||
let Some(src) = &msg.source else {
|
||||
return "services".to_string();
|
||||
};
|
||||
if let Some(ru) = self.remote_users.get(src) {
|
||||
return ru.nick.clone();
|
||||
}
|
||||
if let Some(&uid) = self.uuid_local.get(src) {
|
||||
if let Some(u) = self.users.get(&uid) {
|
||||
return u.nick.clone();
|
||||
}
|
||||
}
|
||||
src.clone()
|
||||
}
|
||||
|
||||
/// `:src SVSHOLD <nick> [<duration> :<reason>]` — services reserve a nick (added
|
||||
/// as an SVSHOLD x-line, so NICK to it is refused) or, with just the nick,
|
||||
/// release it. Broadcast across the network.
|
||||
fn link_svshold(&mut self, from: Uid, msg: &Message) {
|
||||
let Some(nick) = msg.params.first().cloned() else {
|
||||
return;
|
||||
};
|
||||
if msg.params.len() == 1 {
|
||||
self.remove_xline(crate::xline::XKind::Svshold, &nick);
|
||||
} else if msg.params.len() >= 3 {
|
||||
let Some(dur) = crate::xline::parse_duration(&msg.params[1]) else {
|
||||
return;
|
||||
};
|
||||
let setter = self.link_setter(msg);
|
||||
self.add_xline(
|
||||
crate::xline::XKind::Svshold,
|
||||
&nick,
|
||||
dur,
|
||||
&setter,
|
||||
&msg.params[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
self.propagate(&msg.to_wire(), Some(from)); // spanning-tree broadcast
|
||||
}
|
||||
|
||||
/// `:src SVSTOPIC <chan> [<topicts> <setter> :<topic>]` — services set (4-param)
|
||||
/// or clear (1-param) a channel's topic, overriding +t and op checks.
|
||||
fn link_svstopic(&mut self, from: Uid, msg: &Message) {
|
||||
let Some(chan) = msg.params.first().cloned() else {
|
||||
return;
|
||||
};
|
||||
let key = chan.to_ascii_lowercase();
|
||||
if !self.channels.contains_key(&key) {
|
||||
return;
|
||||
}
|
||||
let (text, setter, ts) = if msg.params.len() >= 4 {
|
||||
let ts = msg.params[1].parse::<u64>().unwrap_or_else(|_| now());
|
||||
(msg.params[3].clone(), msg.params[2].clone(), ts)
|
||||
} else {
|
||||
(String::new(), String::new(), 0) // clear
|
||||
};
|
||||
if let Some(ch) = self.channels.get_mut(&key) {
|
||||
ch.topic = if text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Topic {
|
||||
text: text.clone(),
|
||||
setter,
|
||||
ts,
|
||||
})
|
||||
};
|
||||
}
|
||||
let src = self.link_setter(msg);
|
||||
self.to_channel(&key, &format!(":{src} TOPIC {chan} :{text}"), None);
|
||||
self.propagate(&msg.to_wire(), Some(from));
|
||||
}
|
||||
|
||||
/// `:src SVSOPER <target> <opertype>` — services grant IRC-operator status to a
|
||||
/// local user (echo's opers are flat, so the type is accepted but not stored).
|
||||
fn link_svsoper(&mut self, from: Uid, msg: &Message) {
|
||||
if msg.params.len() < 2 {
|
||||
return;
|
||||
}
|
||||
match self.link_local_target(&msg.params[0]) {
|
||||
Some(tuid) => {
|
||||
if !self.is_oper(tuid) {
|
||||
self.oper_up(tuid);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.forward_to_target(&msg.params[0], msg, from);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `:src SVSCMODE <target> <chan> <listmodes>` — services clear the target user's
|
||||
/// matching entries from the named channel list modes (e.g. `b` to unban them,
|
||||
/// `be` bans + exceptions).
|
||||
fn link_svscmode(&mut self, from: Uid, msg: &Message) {
|
||||
if msg.params.len() < 3 {
|
||||
return;
|
||||
}
|
||||
let Some(tuid) = self.link_local_target(&msg.params[0]) else {
|
||||
self.forward_to_target(&msg.params[0], msg, from);
|
||||
return;
|
||||
};
|
||||
let key = msg.params[1].to_ascii_lowercase();
|
||||
let mut removals: Vec<(char, String)> = Vec::new();
|
||||
if let Some(ch) = self.channels.get(&key) {
|
||||
for mc in msg.params[2].chars() {
|
||||
let list = match mc {
|
||||
'b' => &ch.bans,
|
||||
'e' => &ch.excepts,
|
||||
'I' => &ch.invex,
|
||||
_ => continue,
|
||||
};
|
||||
for ban in list {
|
||||
if self.ban_list_hit(tuid, std::slice::from_ref(ban)) {
|
||||
removals.push((mc, ban.mask.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
for (mc, mask) in removals {
|
||||
crate::coremods::core_mode::svs_set_chan_modes(
|
||||
self,
|
||||
&msg.params[1],
|
||||
&format!("-{mc}"),
|
||||
std::slice::from_ref(&mask),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `:src ENCAP <servermask> <subcommand> [params...]` — a command encapsulated
|
||||
/// for specific server(s); services wrap SVS*/SASL this way. If the mask
|
||||
/// matches us we unwrap and dispatch the subcommand; a `*` mask is also flooded
|
||||
|
|
|
|||
21
src/xline.rs
21
src/xline.rs
|
|
@ -16,6 +16,7 @@ pub enum XKind {
|
|||
Shun, // user@host allowed to connect but whose commands are dropped
|
||||
Qline, // a reserved/forbidden nick glob
|
||||
Cban, // a forbidden channel-name glob
|
||||
Svshold, // a services-reserved nick glob (like Qline, but services-owned)
|
||||
}
|
||||
|
||||
impl XKind {
|
||||
|
|
@ -28,6 +29,7 @@ impl XKind {
|
|||
XKind::Shun => "SHUN",
|
||||
XKind::Qline => "Q",
|
||||
XKind::Cban => "CBAN",
|
||||
XKind::Svshold => "SVSHOLD",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +43,7 @@ impl XKind {
|
|||
"SHUN" => XKind::Shun,
|
||||
"Q" => XKind::Qline,
|
||||
"CBAN" => XKind::Cban,
|
||||
"SVSHOLD" => XKind::Svshold,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
@ -125,6 +128,24 @@ impl Server {
|
|||
.map(|x| x.reason.clone())
|
||||
}
|
||||
|
||||
/// The reason nick `nick` is held by services (SVSHOLD), if any.
|
||||
pub fn matched_svshold(&self, nick: &str) -> Option<String> {
|
||||
let n = now();
|
||||
self.xlines
|
||||
.iter()
|
||||
.find(|x| {
|
||||
x.kind == XKind::Svshold
|
||||
&& (x.expires == 0 || x.expires > n)
|
||||
&& glob_match(&x.mask, nick)
|
||||
})
|
||||
.map(|x| x.reason.clone())
|
||||
}
|
||||
|
||||
/// The reason nick `nick` may not be used — a Q-line or a services SVSHOLD.
|
||||
pub fn nick_reserved(&self, nick: &str) -> Option<String> {
|
||||
self.matched_qline(nick).or_else(|| self.matched_svshold(nick))
|
||||
}
|
||||
|
||||
/// The reason channel `chan` is CBAN'd (forbidden), if any. Case-insensitive.
|
||||
pub fn matched_cban(&self, chan: &str) -> Option<String> {
|
||||
let n = now();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue