inspircd: restore logins from replayed accountname metadata
All checks were successful
CI / check (push) Successful in 3m45s
All checks were successful
CI / check (push) Successful in 3m45s
The inbound parser ignored METADATA entirely, so on a services netburst (after a restart/relink) the ircd's replayed 'accountname' metadata was dropped — every logged-in user looked logged out to services until they re-identified, losing ChanServ access. Parse METADATA <uid> accountname into a new AccountLogin event that restores account_of (empty = logout); our own echoes and other keys are filtered.
This commit is contained in:
parent
99df7baf10
commit
7e868babc3
4 changed files with 58 additions and 0 deletions
|
|
@ -18,6 +18,10 @@ pub enum NetEvent {
|
||||||
Privmsg { from: String, to: String, text: String },
|
Privmsg { from: String, to: String, text: String },
|
||||||
UserConnect { uid: String, nick: String, host: String, ip: String },
|
UserConnect { uid: String, nick: String, host: String, ip: String },
|
||||||
NickChange { uid: String, nick: String },
|
NickChange { uid: String, nick: String },
|
||||||
|
// The ircd tells us a user's logged-in account (METADATA accountname), e.g.
|
||||||
|
// replayed on a services netburst so we can restore who was identified. An
|
||||||
|
// empty `account` means they logged out.
|
||||||
|
AccountLogin { uid: String, account: String },
|
||||||
// A channel was created or bursted (an FJOIN). Subsequent single joins arrive
|
// A channel was created or bursted (an FJOIN). Subsequent single joins arrive
|
||||||
// as IJOIN and are not surfaced.
|
// as IJOIN and are not surfaced.
|
||||||
ChannelCreate { channel: String },
|
ChannelCreate { channel: String },
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,22 @@ impl Protocol for InspIrcd {
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// :<src> METADATA <target> <key> :<value> — the ircd sharing user
|
||||||
|
// state. We care about `accountname` on a uid (e.g. replayed on our
|
||||||
|
// netburst) to restore who is logged in; an empty value is a logout.
|
||||||
|
"METADATA" => {
|
||||||
|
let a: Vec<&str> = tokens.collect();
|
||||||
|
match (source.as_deref(), a.first(), a.get(1)) {
|
||||||
|
(Some(src), Some(target), Some(key))
|
||||||
|
if !src.starts_with(self.sid.as_str())
|
||||||
|
&& key.eq_ignore_ascii_case("accountname")
|
||||||
|
&& *target != "*" =>
|
||||||
|
{
|
||||||
|
vec![NetEvent::AccountLogin { uid: target.to_string(), account: trailing(rest) }]
|
||||||
|
}
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
"QUIT" => vec![NetEvent::Quit { uid: source.unwrap_or_default() }],
|
"QUIT" => vec![NetEvent::Quit { uid: source.unwrap_or_default() }],
|
||||||
// ENCAP <target> <subcmd> … — we care about relayed SASL and the
|
// ENCAP <target> <subcmd> … — we care about relayed SASL and the
|
||||||
// account-registration relay (the ircd's account module forwards a
|
// account-registration relay (the ircd's account module forwards a
|
||||||
|
|
@ -436,6 +452,19 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The ircd replaying `accountname` metadata (e.g. on our netburst) restores a
|
||||||
|
// login; an empty value is a logout, and our own echoes and other keys are ignored.
|
||||||
|
#[test]
|
||||||
|
fn parses_accountname_metadata() {
|
||||||
|
let mut p = proto();
|
||||||
|
assert!(matches!(p.parse(":0IR METADATA 0IRAAAAAB accountname :alice").as_slice(),
|
||||||
|
[NetEvent::AccountLogin { uid, account }] if uid == "0IRAAAAAB" && account == "alice"), "login restored");
|
||||||
|
assert!(matches!(p.parse(":0IR METADATA 0IRAAAAAB accountname :").as_slice(),
|
||||||
|
[NetEvent::AccountLogin { uid, account }] if uid == "0IRAAAAAB" && account.is_empty()), "empty value = logout");
|
||||||
|
assert!(p.parse(":42S METADATA 0IRAAAAAB accountname :bob").is_empty(), "our own echo is ignored");
|
||||||
|
assert!(p.parse(":0IR METADATA 0IRAAAAAB ssl_cert :deadbeef").is_empty(), "non-account keys ignored");
|
||||||
|
}
|
||||||
|
|
||||||
// A UID burst introduces the user under their current nick.
|
// A UID burst introduces the user under their current nick.
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_uid_burst() {
|
fn parses_uid_burst() {
|
||||||
|
|
|
||||||
|
|
@ -850,6 +850,18 @@ impl Engine {
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
NetEvent::AccountLogin { uid, account } => {
|
||||||
|
// The ircd told us who a user is logged in as (e.g. replayed on our
|
||||||
|
// netburst). Restore the mapping so services recognise the login;
|
||||||
|
// no side-effects — the burst already put them in their channels.
|
||||||
|
if account.is_empty() {
|
||||||
|
self.network.clear_account(&uid);
|
||||||
|
} else {
|
||||||
|
self.network.set_account(&uid, &account);
|
||||||
|
self.pending_enforce.retain(|p| p.uid != uid);
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
NetEvent::NickChange { uid, nick } => {
|
NetEvent::NickChange { uid, nick } => {
|
||||||
let new_nick = nick.clone();
|
let new_nick = nick.clone();
|
||||||
self.network.user_nick_change(&uid, nick);
|
self.network.user_nick_change(&uid, nick);
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,19 @@
|
||||||
assert!(parted, "the assigned bot parts the released channel");
|
assert!(parted, "the assigned bot parts the released channel");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A netburst that replays a user's accountname restores their services login
|
||||||
|
// (so a services restart doesn't silently log everyone out); empty = logout.
|
||||||
|
#[test]
|
||||||
|
fn account_login_event_restores_session() {
|
||||||
|
let mut e = engine_with("acctlogin", "alice", "sesame");
|
||||||
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "alice".into(), host: "h".into() , ip: "0.0.0.0".into() });
|
||||||
|
assert_eq!(e.network.account_of("000AAAAAB"), None, "not recognised before the metadata");
|
||||||
|
e.handle(NetEvent::AccountLogin { uid: "000AAAAAB".into(), account: "alice".into() });
|
||||||
|
assert_eq!(e.network.account_of("000AAAAAB"), Some("alice"), "login restored from the ircd");
|
||||||
|
e.handle(NetEvent::AccountLogin { uid: "000AAAAAB".into(), account: String::new() });
|
||||||
|
assert_eq!(e.network.account_of("000AAAAAB"), None, "empty account is a logout");
|
||||||
|
}
|
||||||
|
|
||||||
// A suspension that arrives by gossip must end local sessions on that account,
|
// A suspension that arrives by gossip must end local sessions on that account,
|
||||||
// just as a local SUSPEND does — the account and its channels stay put.
|
// just as a local SUSPEND does — the account and its channels stay put.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue