From 7fa7831390054c91e3fccee93b8b86e5ee496c46 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 20 Jul 2026 21:08:42 +0000 Subject: [PATCH] LOGIN from an already-identified session reclaims the nick without a needless re-verify --- modules/nickserv/src/lib.rs | 2 +- modules/nickserv/src/login.rs | 13 ++++++++++--- src/engine/tests.rs | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/modules/nickserv/src/lib.rs b/modules/nickserv/src/lib.rs index fc79103..0a69202 100644 --- a/modules/nickserv/src/lib.rs +++ b/modules/nickserv/src/lib.rs @@ -114,7 +114,7 @@ impl Service for NickServ { match cmd.as_deref() { Some("REGISTER") => register::handle(me, from, args, ctx, db), Some("IDENTIFY") | Some("ID") => identify::handle(me, from, args, ctx, db), - Some("LOGIN") => login::handle(me, from, args, ctx, db), + Some("LOGIN") => login::handle(me, &self.guest_nick, &mut self.guest_seq, from, args, ctx, net, db), Some("LOGOUT") | Some("LOGOFF") => logout::handle(me, &self.guest_nick, &mut self.guest_seq, from, ctx, net, db), Some("CERT") => cert::handle(me, from, args, ctx, db), Some("INFO") => info::handle(me, from, args, ctx, net, db), diff --git a/modules/nickserv/src/login.rs b/modules/nickserv/src/login.rs index 31aa5e3..1734b89 100644 --- a/modules/nickserv/src/login.rs +++ b/modules/nickserv/src/login.rs @@ -1,15 +1,22 @@ -use echo_api::{AuthThen, Sender, ServiceCtx, Store}; +use echo_api::{AuthThen, NetView, Sender, ServiceCtx, Store}; -use super::identify; +use super::{ghost, identify}; // LOGIN : identify to the account owning and, on success, // reclaim the nick — freeing any ghost holding it and moving you onto it. A // one-shot IDENTIFY + RECOVER for someone who connected under a guest nick. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { +#[allow(clippy::too_many_arguments)] +pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) { let (Some(&nick), Some(&password)) = (args.get(1), args.get(2)) else { ctx.notice(me, from.uid, "Syntax: LOGIN "); return; }; + // Already identified to this account: don't burn a needless ~1s re-verify — just + // reclaim the nick, exactly like RECOVER (which needs no password for an owner). + if from.account.is_some() && db.resolve_account(nick) == from.account { + ghost::handle(me, guest_nick, guest_seq, from, args, ctx, net, db, true); + return; + } let Some((account, verifier)) = identify::precheck(me, from, "LOGIN", nick, ctx, db) else { return; }; diff --git a/src/engine/tests.rs b/src/engine/tests.rs index 69c984d..aafd99d 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -868,6 +868,20 @@ assert!(out.iter().any(|a| matches!(a, NetAction::ForceNick { uid, nick } if uid == "000AAAAAB" && nick == "alice")), "caller takes the nick: {out:?}"); } + // LOGIN from a session already identified to that account skips the (wasteful) + // re-verify and just reclaims the nick, like RECOVER. + #[test] + fn login_while_identified_reclaims_without_reverify() { + let mut e = engine_with("loginid", "alice", "sesame"); + e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "Guest1".into(), host: "h".into(), ip: "0.0.0.0".into() }); + e.complete_authenticate(true, echo_api::AuthThen::Identify { uid: "000AAAAAB".into(), agent: "42SAAAAAA".into(), name: "alice".into(), account: "alice".into() }); + e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "alice".into(), host: "h".into(), ip: "0.0.0.0".into() }); + let out = e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "LOGIN alice sesame".into() }); + assert!(!out.iter().any(|a| matches!(a, NetAction::DeferAuthenticate { .. })), "no re-verify when already identified: {out:?}"); + assert!(out.iter().any(|a| matches!(a, NetAction::ForceNick { uid, nick } if uid == "000AAAAAC" && nick != "alice")), "ghost freed: {out:?}"); + assert!(out.iter().any(|a| matches!(a, NetAction::ForceNick { uid, nick } if uid == "000AAAAAB" && nick == "alice")), "caller regains the nick: {out:?}"); + } + // 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. #[test]