All checks were successful
CI / check (push) Successful in 3m47s
Both ran scram verify_plain (~1s PBKDF2 at 1.2M iters) inline under the engine lock, freezing the whole daemon per login. Add NetAction::DeferAuthenticate + AuthThen continuation + ctx.defer_authenticate, mirroring DeferPassword: the module/SASL path fetches the verifier cheaply and defers; the link layer runs verify_plain on spawn_blocking, then Engine::complete_authenticate finishes the login (IDENTIFY reuses the same ctx helpers, so its login/AJOIN/vhost/memo side-effects are unchanged). Tests resolve the defer inline (cfg(test)). The gRPC web-login path was already fixed; no login path stalls services now. (GHOST/DROP/CERT/GROUP still verify inline but are rare account ops, not logins.)
69 lines
3.3 KiB
Rust
69 lines
3.3 KiB
Rust
use echo_api::{human_time, AuthThen, Store};
|
|
use echo_api::{Sender, ServiceCtx};
|
|
|
|
// IDENTIFY [account] <password>: log in. The account defaults to the current
|
|
// nick, so both the bare-password and account+password forms work.
|
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
|
let (account_name, password) = match (args.get(1), args.get(2)) {
|
|
(Some(account), Some(password)) => (*account, *password),
|
|
(Some(password), None) => (from.nick, *password),
|
|
_ => {
|
|
ctx.notice(me, from.uid, "Syntax: IDENTIFY [account] <password>");
|
|
return;
|
|
}
|
|
};
|
|
// Distinguish an unregistered account from a wrong password.
|
|
if !db.exists(account_name) {
|
|
ctx.notice(me, from.uid, format!("\x02{account_name}\x02 isn't registered."));
|
|
return;
|
|
}
|
|
// A suspended account can't be logged into (checked before the password so it
|
|
// doesn't reveal whether the password was right). Tell them who, when and why,
|
|
// and when it lifts, so they know what happened and who to reach.
|
|
if let Some(acc) = db.resolve_account(account_name).map(str::to_string) {
|
|
if db.is_suspended(&acc) {
|
|
if let Some(s) = db.suspension(&acc) {
|
|
let mut msg = format!("\x02{acc}\x02 is suspended, so you can't log in to it right now. It was suspended by \x02{}\x02 on {}", s.by, human_time(s.ts));
|
|
if s.reason.trim().is_empty() {
|
|
msg.push('.');
|
|
} else {
|
|
msg.push_str(&format!(" — reason: {}", s.reason));
|
|
}
|
|
if let Some(exp) = s.expires {
|
|
msg.push_str(&format!(" The suspension is due to lift on {}.", human_time(exp)));
|
|
}
|
|
msg.push_str(" If you think this is a mistake, please contact the network staff.");
|
|
ctx.notice(me, from.uid, msg);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
// Refuse while throttled, so a password can't be brute-forced.
|
|
if let Some(secs) = db.auth_lockout(account_name) {
|
|
ctx.notice(me, from.uid, format!("Too many failed attempts. Please wait {secs}s and try again."));
|
|
return;
|
|
}
|
|
// Fetch the verifier cheaply and hand the (~1s) PBKDF2 verify to the engine to
|
|
// run off the reactor — never verify under the engine lock. The login finish
|
|
// happens in Engine::complete_authenticate.
|
|
match db.scram_verifier(account_name) {
|
|
None => {
|
|
// Exists but has no verifier (e.g. cert-only) — a password can't match.
|
|
db.note_auth(account_name, false);
|
|
ctx.count("nickserv.identify_fail");
|
|
ctx.notice(me, from.uid, "Invalid password. Please try again.");
|
|
}
|
|
Some((account, verifier)) => {
|
|
// Already identified to this account: skip the (wasted) verify.
|
|
if from.account == Some(account.as_str()) {
|
|
ctx.notice(me, from.uid, format!("You're already identified as \x02{}\x02.", account));
|
|
return;
|
|
}
|
|
ctx.defer_authenticate(
|
|
verifier,
|
|
password,
|
|
AuthThen::Identify { uid: from.uid.to_string(), agent: me.to_string(), name: account_name.to_string(), account },
|
|
);
|
|
}
|
|
}
|
|
}
|