Source channel mode changes from ChanServ
Channel modes (+r, mode lock, auto-op, enforcement) are sent from the ChanServ pseudoclient instead of the services server, so users see ChanServ as the setter. The engine locates the channel-managing service; the FMODE filter now ignores anything under our SID prefix (server and pseudoclients) so enforcement can't loop on our own changes.
This commit is contained in:
parent
e785c9d8ac
commit
f15c9998be
5 changed files with 47 additions and 22 deletions
|
|
@ -73,16 +73,19 @@ pub struct Engine {
|
||||||
db: Db,
|
db: Db,
|
||||||
sasl_sessions: HashMap<String, TimedSession>, // client uid -> in-progress exchange
|
sasl_sessions: HashMap<String, TimedSession>, // client uid -> in-progress exchange
|
||||||
reg_limiter: RegLimiter,
|
reg_limiter: RegLimiter,
|
||||||
|
chan_service: Option<String>, // uid to source channel modes from (ChanServ)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
||||||
|
let chan_service = services.iter().find(|s| s.manages_channels()).map(|s| s.uid().to_string());
|
||||||
Self {
|
Self {
|
||||||
services,
|
services,
|
||||||
network: Network::default(),
|
network: Network::default(),
|
||||||
db,
|
db,
|
||||||
sasl_sessions: HashMap::new(),
|
sasl_sessions: HashMap::new(),
|
||||||
reg_limiter: RegLimiter::new(),
|
reg_limiter: RegLimiter::new(),
|
||||||
|
chan_service,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,15 +177,17 @@ impl Engine {
|
||||||
// creation and on the uplink burst, so registered channels regain the
|
// creation and on the uplink burst, so registered channels regain the
|
||||||
// mode after emptying, and after a services relink.
|
// mode after emptying, and after a services relink.
|
||||||
NetEvent::ChannelCreate { channel } => {
|
NetEvent::ChannelCreate { channel } => {
|
||||||
|
let from = self.chan_service.clone().unwrap_or_default();
|
||||||
match self.db.channel(&channel) {
|
match self.db.channel(&channel) {
|
||||||
Some(info) => vec![NetAction::ChannelMode { channel, modes: info.lock_modes() }],
|
Some(info) => vec![NetAction::ChannelMode { from, channel, modes: info.lock_modes() }],
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Enforce the mode lock: revert any change that broke it.
|
// Enforce the mode lock: revert any change that broke it.
|
||||||
NetEvent::ChannelModeChange { channel, modes } => {
|
NetEvent::ChannelModeChange { channel, modes } => {
|
||||||
|
let from = self.chan_service.clone().unwrap_or_default();
|
||||||
match self.db.channel(&channel).and_then(|info| info.enforce(&modes)) {
|
match self.db.channel(&channel).and_then(|info| info.enforce(&modes)) {
|
||||||
Some(revert) => vec![NetAction::ChannelMode { channel, modes: revert }],
|
Some(revert) => vec![NetAction::ChannelMode { from, channel, modes: revert }],
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +196,10 @@ impl Engine {
|
||||||
let founder = self.db.channel(&channel).map(|c| c.founder.clone());
|
let founder = self.db.channel(&channel).map(|c| c.founder.clone());
|
||||||
let account = self.network.account_of(&uid).map(str::to_string);
|
let account = self.network.account_of(&uid).map(str::to_string);
|
||||||
match (founder, account) {
|
match (founder, account) {
|
||||||
(Some(f), Some(a)) if f == a => vec![NetAction::ChannelMode { channel, modes: format!("+o {uid}") }],
|
(Some(f), Some(a)) if f == a => {
|
||||||
|
let from = self.chan_service.clone().unwrap_or_default();
|
||||||
|
vec![NetAction::ChannelMode { from, channel, modes: format!("+o {uid}") }]
|
||||||
|
}
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -951,13 +959,13 @@ mod tests {
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
||||||
let out = to_cs(&mut e, "000AAAAAB", "REGISTER #room");
|
let out = to_cs(&mut e, "000AAAAAB", "REGISTER #room");
|
||||||
assert!(notice(&out, "now registered"));
|
assert!(notice(&out, "now registered"));
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#room" && modes == "+r")), "register sets +r: {out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#room" && modes == "+r")), "register sets +r: {out:?}");
|
||||||
assert!(notice(&to_cs(&mut e, "000AAAAAB", "INFO #room"), "alice"));
|
assert!(notice(&to_cs(&mut e, "000AAAAAB", "INFO #room"), "alice"));
|
||||||
|
|
||||||
// Founder sets a mode lock: it applies immediately and shows on view.
|
// Founder sets a mode lock: it applies immediately and shows on view.
|
||||||
let out = to_cs(&mut e, "000AAAAAB", "MLOCK #room +nt-s");
|
let out = to_cs(&mut e, "000AAAAAB", "MLOCK #room +nt-s");
|
||||||
assert!(notice(&out, "updated"));
|
assert!(notice(&out, "updated"));
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#room" && modes == "+rnt-s")), "mlock applied: {out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#room" && modes == "+rnt-s")), "mlock applied: {out:?}");
|
||||||
assert!(notice(&to_cs(&mut e, "000AAAAAB", "MLOCK #room"), "+nt-s"));
|
assert!(notice(&to_cs(&mut e, "000AAAAAB", "MLOCK #room"), "+nt-s"));
|
||||||
|
|
||||||
// A different, unidentified user cannot set the lock or drop it.
|
// A different, unidentified user cannot set the lock or drop it.
|
||||||
|
|
@ -968,7 +976,7 @@ mod tests {
|
||||||
// The founder can, which also clears +r.
|
// The founder can, which also clears +r.
|
||||||
let out = to_cs(&mut e, "000AAAAAB", "DROP #room");
|
let out = to_cs(&mut e, "000AAAAAB", "DROP #room");
|
||||||
assert!(notice(&out, "dropped"));
|
assert!(notice(&out, "dropped"));
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#room" && modes == "-r")), "drop clears +r: {out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#room" && modes == "-r")), "drop clears +r: {out:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// A registered channel (re)appearing re-asserts +r; an unregistered one does not.
|
// A registered channel (re)appearing re-asserts +r; an unregistered one does not.
|
||||||
|
|
@ -982,7 +990,7 @@ mod tests {
|
||||||
let mut e = Engine::new(vec![Box::new(ns)], db);
|
let mut e = Engine::new(vec![Box::new(ns)], db);
|
||||||
|
|
||||||
let out = e.handle(NetEvent::ChannelCreate { channel: "#reg".into() });
|
let out = e.handle(NetEvent::ChannelCreate { channel: "#reg".into() });
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#reg" && modes == "+r")), "registered channel regains +r: {out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#reg" && modes == "+r")), "registered channel regains +r: {out:?}");
|
||||||
|
|
||||||
let out = e.handle(NetEvent::ChannelCreate { channel: "#other".into() });
|
let out = e.handle(NetEvent::ChannelCreate { channel: "#other".into() });
|
||||||
assert!(out.is_empty(), "unregistered channel is left alone: {out:?}");
|
assert!(out.is_empty(), "unregistered channel is left alone: {out:?}");
|
||||||
|
|
@ -1001,11 +1009,11 @@ mod tests {
|
||||||
|
|
||||||
// Creation applies the full lock.
|
// Creation applies the full lock.
|
||||||
let out = e.handle(NetEvent::ChannelCreate { channel: "#lk".into() });
|
let out = e.handle(NetEvent::ChannelCreate { channel: "#lk".into() });
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#lk" && modes == "+rnt-s")), "{out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#lk" && modes == "+rnt-s")), "{out:?}");
|
||||||
|
|
||||||
// Setting a locked-off mode is reverted.
|
// Setting a locked-off mode is reverted.
|
||||||
let out = e.handle(NetEvent::ChannelModeChange { channel: "#lk".into(), modes: "+s".into() });
|
let out = e.handle(NetEvent::ChannelModeChange { channel: "#lk".into(), modes: "+s".into() });
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#lk" && modes == "-s")), "{out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#lk" && modes == "-s")), "{out:?}");
|
||||||
|
|
||||||
// An unrelated mode change is left alone.
|
// An unrelated mode change is left alone.
|
||||||
assert!(e.handle(NetEvent::ChannelModeChange { channel: "#lk".into(), modes: "+m".into() }).is_empty());
|
assert!(e.handle(NetEvent::ChannelModeChange { channel: "#lk".into(), modes: "+m".into() }).is_empty());
|
||||||
|
|
@ -1026,7 +1034,7 @@ mod tests {
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
||||||
|
|
||||||
let out = e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#x".into() });
|
let out = e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#x".into() });
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#x" && modes == "+o 000AAAAAB")), "founder opped: {out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#x" && modes == "+o 000AAAAAB")), "founder opped: {out:?}");
|
||||||
|
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into() });
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into() });
|
||||||
assert!(e.handle(NetEvent::Join { uid: "000AAAAAC".into(), channel: "#x".into() }).is_empty(), "non-founder not opped");
|
assert!(e.handle(NetEvent::Join { uid: "000AAAAAC".into(), channel: "#x".into() }).is_empty(), "non-founder not opped");
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ pub trait Service: Send {
|
||||||
"services.local"
|
"services.local"
|
||||||
}
|
}
|
||||||
fn gecos(&self) -> &str;
|
fn gecos(&self) -> &str;
|
||||||
|
// Whether this service owns channel modes (ChanServ), so the engine can source
|
||||||
|
// channel mode changes from it.
|
||||||
|
fn manages_channels(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db);
|
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,9 +80,10 @@ impl ServiceCtx {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set channel modes from services, e.g. "+r" on a registered channel.
|
// Set channel modes, sourced from pseudoclient `from` (e.g. ChanServ).
|
||||||
pub fn channel_mode(&mut self, channel: &str, modes: &str) {
|
pub fn channel_mode(&mut self, from: &str, channel: &str, modes: &str) {
|
||||||
self.actions.push(NetAction::ChannelMode {
|
self.actions.push(NetAction::ChannelMode {
|
||||||
|
from: from.to_string(),
|
||||||
channel: channel.to_string(),
|
channel: channel.to_string(),
|
||||||
modes: modes.to_string(),
|
modes: modes.to_string(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,8 @@ impl Protocol for InspIrcd {
|
||||||
"FMODE" => {
|
"FMODE" => {
|
||||||
let a: Vec<&str> = tokens.collect();
|
let a: Vec<&str> = tokens.collect();
|
||||||
match (source.as_deref(), a.first(), a.get(2)) {
|
match (source.as_deref(), a.first(), a.get(2)) {
|
||||||
(Some(src), Some(chan), Some(modes)) if src != self.sid && chan.starts_with('#') => {
|
// Our own uids (server SID + pseudoclients) share our SID prefix.
|
||||||
|
(Some(src), Some(chan), Some(modes)) if !src.starts_with(self.sid.as_str()) && chan.starts_with('#') => {
|
||||||
vec![NetEvent::ChannelModeChange { channel: chan.to_string(), modes: modes.to_string() }]
|
vec![NetEvent::ChannelModeChange { channel: chan.to_string(), modes: modes.to_string() }]
|
||||||
}
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
|
|
@ -218,10 +219,16 @@ impl Protocol for InspIrcd {
|
||||||
vec![self.from_us(format!("SVSNICK {} {} {}", uid, nick, now))]
|
vec![self.from_us(format!("SVSNICK {} {} {}", uid, nick, now))]
|
||||||
}
|
}
|
||||||
// FMODE <chan> <ts> <modes>. The ircd drops an FMODE whose TS is newer
|
// FMODE <chan> <ts> <modes>. The ircd drops an FMODE whose TS is newer
|
||||||
// than the channel's, so we send TS 1 to guarantee it applies to the
|
// than the channel's, so we send TS 1 to guarantee it applies. Sourced
|
||||||
// existing channel. Sourced from our server, which +r requires.
|
// from the given pseudoclient (e.g. ChanServ) so users see who set it,
|
||||||
NetAction::ChannelMode { channel, modes } => {
|
// or from the services server when `from` is empty.
|
||||||
vec![self.from_us(format!("FMODE {} 1 {}", channel, modes))]
|
NetAction::ChannelMode { from, channel, modes } => {
|
||||||
|
let cmd = format!("FMODE {} 1 {}", channel, modes);
|
||||||
|
if from.is_empty() {
|
||||||
|
vec![self.from_us(cmd)]
|
||||||
|
} else {
|
||||||
|
vec![format!(":{} {}", from, cmd)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NetAction::Raw(s) => vec![s.clone()],
|
NetAction::Raw(s) => vec![s.clone()],
|
||||||
// Internal: the link layer handles this before serialization.
|
// Internal: the link layer handles this before serialization.
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,10 @@ pub enum NetAction {
|
||||||
// Force a user's nick (SVSNICK), e.g. renaming to a guest nick on logout.
|
// Force a user's nick (SVSNICK), e.g. renaming to a guest nick on logout.
|
||||||
// The protocol stamps the new nick's timestamp.
|
// The protocol stamps the new nick's timestamp.
|
||||||
ForceNick { uid: String, nick: String },
|
ForceNick { uid: String, nick: String },
|
||||||
// Set channel modes from services, e.g. +r on a registered channel. The
|
// Set channel modes from services, e.g. +r on a registered channel. `from` is
|
||||||
|
// the pseudoclient uid to source it from (empty = the services server). The
|
||||||
// protocol stamps a timestamp the ircd will accept.
|
// protocol stamps a timestamp the ircd will accept.
|
||||||
ChannelMode { channel: String, modes: String },
|
ChannelMode { from: String, channel: String, modes: String },
|
||||||
Raw(String),
|
Raw(String),
|
||||||
// Internal only, never serialized to the wire: a registration whose password
|
// Internal only, never serialized to the wire: a registration whose password
|
||||||
// still needs its (expensive) key derivation. The link layer runs the
|
// still needs its (expensive) key derivation. The link layer runs the
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ impl Service for ChanServ {
|
||||||
fn gecos(&self) -> &str {
|
fn gecos(&self) -> &str {
|
||||||
"Channel Services"
|
"Channel Services"
|
||||||
}
|
}
|
||||||
|
fn manages_channels(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) {
|
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) {
|
||||||
let me = self.uid.as_str();
|
let me = self.uid.as_str();
|
||||||
|
|
@ -35,7 +38,7 @@ impl Service for ChanServ {
|
||||||
};
|
};
|
||||||
match db.register_channel(chan, account) {
|
match db.register_channel(chan, account) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
ctx.channel_mode(chan, "+r"); // mark the channel registered
|
ctx.channel_mode(me, chan, "+r"); // mark the channel registered
|
||||||
ctx.notice(me, from.uid, format!("\x02{chan}\x02 is now registered and you are its founder. Enjoy!"));
|
ctx.notice(me, from.uid, format!("\x02{chan}\x02 is now registered and you are its founder. Enjoy!"));
|
||||||
}
|
}
|
||||||
Err(ChanError::Exists) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 is already registered. Try \x02INFO {chan}\x02 to see who owns it.")),
|
Err(ChanError::Exists) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 is already registered. Try \x02INFO {chan}\x02 to see who owns it.")),
|
||||||
|
|
@ -75,7 +78,7 @@ impl Service for ChanServ {
|
||||||
}
|
}
|
||||||
match db.drop_channel(chan) {
|
match db.drop_channel(chan) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
ctx.channel_mode(chan, "-r"); // no longer registered
|
ctx.channel_mode(me, chan, "-r"); // no longer registered
|
||||||
ctx.notice(me, from.uid, format!("\x02{chan}\x02 has been dropped and is no longer registered."));
|
ctx.notice(me, from.uid, format!("\x02{chan}\x02 has been dropped and is no longer registered."));
|
||||||
}
|
}
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
|
|
@ -113,7 +116,7 @@ impl Service for ChanServ {
|
||||||
match db.set_mlock(chan, &on, &off) {
|
match db.set_mlock(chan, &on, &off) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Some(info) = db.channel(chan) {
|
if let Some(info) = db.channel(chan) {
|
||||||
ctx.channel_mode(chan, &info.lock_modes()); // apply it now
|
ctx.channel_mode(me, chan, &info.lock_modes()); // apply it now
|
||||||
}
|
}
|
||||||
ctx.notice(me, from.uid, format!("Mode lock for \x02{chan}\x02 updated."));
|
ctx.notice(me, from.uid, format!("Mode lock for \x02{chan}\x02 updated."));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue