customtitle: verify a KDF /TITLE password off the core thread (Event::TitleAuth) — /TITLE spam can't freeze the server

This commit is contained in:
Jean Chevronnet 2026-08-12 13:07:28 +00:00
parent 7cb58586b4
commit 2b3495be65
2 changed files with 78 additions and 32 deletions

View file

@ -66,6 +66,13 @@ pub enum Event {
algo: String, algo: String,
hash: Option<String>, hash: Option<String>,
}, },
/// A background `/TITLE` password verify finished (see `crate::modules::customtitle`).
TitleAuth {
uid: Uid,
ok: bool,
title: String,
vhost: String,
},
/// A module's async HTTP request finished. `tag` is `"<module>:<detail>"` /// A module's async HTTP request finished. `tag` is `"<module>:<detail>"`
/// so the core can route the reply back to the module that issued it (e.g. /// so the core can route the reply back to the module that issued it (e.g.
/// account registration, captcha verification). `status` is 0 on transport /// account registration, captcha verification). `status` is 0 on transport
@ -241,6 +248,18 @@ impl Ircd {
}; };
self.server.send(uid, line); self.server.send(uid, line);
} }
Event::TitleAuth {
uid,
ok,
title,
vhost,
} => {
if ok {
crate::modules::customtitle::grant(&mut self.server, uid, &title, &vhost);
} else {
crate::modules::customtitle::deny(&self.server, uid);
}
}
Event::HttpResult { Event::HttpResult {
uid, uid,
tag, tag,

View file

@ -29,6 +29,37 @@ pub fn commands() -> Vec<Box<dyn Command>> {
vec![Box::new(TitleCmd)] vec![Box::new(TitleCmd)]
} }
fn nick(s: &Server, uid: Uid) -> String {
s.users
.get(&uid)
.map(|u| u.nick.clone())
.unwrap_or_default()
}
/// Apply a verified title: store it, apply the vhost (unless `*`), and confirm.
pub fn grant(s: &mut Server, uid: Uid, title: &str, vhost: &str) {
if let Some(u) = s.users.get_mut(&uid) {
u.ext.set(Title(title.to_string()));
}
if vhost != "*" && !vhost.is_empty() {
s.change_host_ident(uid, None, Some(vhost));
}
let nick = nick(s, uid);
s.send(
uid,
format!(":{} NOTICE {nick} :*** TITLE: you are now known as \"{title}\".", s.name),
);
}
/// Reject a TITLE attempt (bad name or password).
pub fn deny(s: &Server, uid: Uid) {
let nick = nick(s, uid);
s.send(
uid,
format!(":{} NOTICE {nick} :*** TITLE: invalid title name or password.", s.name),
);
}
struct TitleCmd; struct TitleCmd;
impl Command for TitleCmd { impl Command for TitleCmd {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
@ -39,47 +70,43 @@ impl Command for TitleCmd {
} }
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
let (name, pass) = (params[0].clone(), params[1].clone()); let (name, pass) = (params[0].clone(), params[1].clone());
// find a matching <name> <password> <vhost> <title…> block // find the <name> <password> <vhost> <title…> block by name (first wins)
let found = s.conf_all("customtitle").iter().find_map(|line| { let block = s.conf_all("customtitle").iter().find_map(|line| {
let mut it = line.split_whitespace(); let mut it = line.split_whitespace();
let cname = it.next()?; let cname = it.next()?;
let cpass = it.next()?; let cpass = it.next()?;
let cvhost = it.next()?; let cvhost = it.next()?;
let title = it.collect::<Vec<_>>().join(" "); let title = it.collect::<Vec<_>>().join(" ");
if cname == name && !title.is_empty() && password_hash::verify(cpass, &pass) { (cname == name && !title.is_empty())
Some((title, cvhost.to_string())) .then(|| (cpass.to_string(), cvhost.to_string(), title))
} else {
None
}
}); });
let nick = s let Some((cpass, cvhost, title)) = block else {
.users deny(s, uid);
.get(&uid)
.map(|u| u.nick.clone())
.unwrap_or_default();
let Some((title, vhost)) = found else {
s.send(
uid,
format!(
":{} NOTICE {nick} :*** TITLE: invalid title name or password.",
s.name
),
);
return CmdResult::Fail; return CmdResult::Fail;
}; };
if let Some(u) = s.users.get_mut(&uid) { // a KDF title password is slow — verify it off the core thread (result comes
u.ext.set(Title(title.clone())); // back as TitleAuth) so /TITLE spam can't freeze the server.
if password_hash::is_slow(&cpass) {
let started = s.spawn_crypto(move || {
let ok = password_hash::verify(&cpass, &pass);
crate::ircd::Event::TitleAuth {
uid,
ok,
title,
vhost: cvhost,
}
});
if !started {
deny(s, uid);
}
return CmdResult::Ok;
} }
if vhost != "*" && !vhost.is_empty() { if password_hash::verify(&cpass, &pass) {
s.change_host_ident(uid, None, Some(&vhost)); grant(s, uid, &title, &cvhost);
CmdResult::Ok
} else {
deny(s, uid);
CmdResult::Fail
} }
s.send(
uid,
format!(
":{} NOTICE {nick} :*** TITLE: you are now known as \"{title}\".",
s.name
),
);
CmdResult::Ok
} }
} }