diff --git a/modules/nickserv/cert.rs b/modules/nickserv/cert.rs new file mode 100644 index 0000000..85b6849 --- /dev/null +++ b/modules/nickserv/cert.rs @@ -0,0 +1,59 @@ +use crate::engine::db::{CertError, Db}; +use crate::engine::service::{Sender, ServiceCtx}; + +// CERT ADD|DEL|LIST [fingerprint]: manage the TLS certificate +// fingerprints that may log in to your account via SASL EXTERNAL. Each +// subcommand is password-gated, so it needs no identified-session state. +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { + let auth = |db: &Db, password: &str| db.authenticate(from.nick, password).map(str::to_string); + match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() { + Some("LIST") => { + let Some(password) = args.get(2) else { + ctx.notice(me, from.uid, "Syntax: CERT LIST "); + return; + }; + let Some(account) = auth(db, password) else { + ctx.notice(me, from.uid, "Invalid password."); + return; + }; + let fps = db.certfps(&account); + if fps.is_empty() { + ctx.notice(me, from.uid, "No certificate fingerprints are registered to your account."); + } else { + ctx.notice(me, from.uid, format!("Registered fingerprints: {}", fps.join(", "))); + } + } + Some("ADD") => { + let (Some(password), Some(fp)) = (args.get(2), args.get(3)) else { + ctx.notice(me, from.uid, "Syntax: CERT ADD "); + return; + }; + let Some(account) = auth(db, password) else { + ctx.notice(me, from.uid, "Invalid password."); + return; + }; + match db.certfp_add(&account, fp) { + Ok(()) => ctx.notice(me, from.uid, format!("Added fingerprint \x02{fp}\x02. You can now log in with SASL EXTERNAL.")), + Err(CertError::InUse) => ctx.notice(me, from.uid, "That fingerprint is already registered."), + Err(CertError::Invalid) => ctx.notice(me, from.uid, "That does not look like a certificate fingerprint."), + Err(CertError::NoAccount | CertError::Internal) => ctx.notice(me, from.uid, "Could not add the fingerprint, please try again later."), + } + } + Some("DEL") | Some("REMOVE") => { + let (Some(password), Some(fp)) = (args.get(2), args.get(3)) else { + ctx.notice(me, from.uid, "Syntax: CERT DEL "); + return; + }; + let Some(account) = auth(db, password) else { + ctx.notice(me, from.uid, "Invalid password."); + return; + }; + match db.certfp_del(&account, fp) { + Ok(true) => ctx.notice(me, from.uid, format!("Removed fingerprint \x02{fp}\x02.")), + Ok(false) => ctx.notice(me, from.uid, "No such fingerprint is registered to your account."), + Err(_) => ctx.notice(me, from.uid, "Could not remove the fingerprint, please try again later."), + } + } + _ => ctx.notice(me, from.uid, "Syntax: CERT ADD|DEL|LIST [fingerprint]"), + } +} diff --git a/modules/nickserv/identify.rs b/modules/nickserv/identify.rs new file mode 100644 index 0000000..f191ae2 --- /dev/null +++ b/modules/nickserv/identify.rs @@ -0,0 +1,33 @@ +use crate::engine::db::Db; +use crate::engine::service::{Sender, ServiceCtx}; + +// IDENTIFY [account] : 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: &Db) { + 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] "); + 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; + } + match db.authenticate(account_name, password) { + Some(account) => { + // Already identified to this account: don't re-fire the login. + if from.account == Some(account) { + ctx.notice(me, from.uid, format!("You're already identified as \x02{}\x02.", account)); + return; + } + let account = account.to_string(); + ctx.login(from.uid, &account); + ctx.notice(me, from.uid, format!("You're now identified as \x02{}\x02. Welcome back!", account)); + } + None => ctx.notice(me, from.uid, "Invalid password. Please try again."), + } +} diff --git a/modules/nickserv/logout.rs b/modules/nickserv/logout.rs new file mode 100644 index 0000000..4d9fde0 --- /dev/null +++ b/modules/nickserv/logout.rs @@ -0,0 +1,14 @@ +use crate::engine::service::{Sender, ServiceCtx}; + +// LOGOUT: log out and rename to a guest nick (prefix + a per-logout sequence). +pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ctx: &mut ServiceCtx) { + if from.account.is_none() { + ctx.notice(me, from.uid, "You're not logged in."); + return; + } + let guest = format!("{guest_nick}{guest_seq}"); + *guest_seq = guest_seq.wrapping_add(1); + ctx.logout(from.uid); + ctx.force_nick(from.uid, &guest); + ctx.notice(me, from.uid, format!("You're now logged out. Your nick is now \x02{}\x02.", guest)); +} diff --git a/modules/nickserv/nickserv.rs b/modules/nickserv/nickserv.rs index 7cbf5b5..31ce4cb 100644 --- a/modules/nickserv/nickserv.rs +++ b/modules/nickserv/nickserv.rs @@ -1,7 +1,15 @@ -use crate::engine::db::{CertError, Db}; +use crate::engine::db::Db; use crate::engine::service::{Sender, Service, ServiceCtx}; use crate::engine::state::Network; -use crate::proto::RegReply; + +#[path = "register.rs"] +mod register; +#[path = "identify.rs"] +mod identify; +#[path = "logout.rs"] +mod logout; +#[path = "cert.rs"] +mod cert; pub struct NickServ { pub uid: String, @@ -28,133 +36,13 @@ impl Service for NickServ { fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, _net: &Network, db: &mut Db) { let me = self.uid.as_str(); match args.first().map(|s| s.to_ascii_uppercase()).as_deref() { - Some("REGISTER") => { - // REGISTER [email] — registers the sender's current nick. - let Some(password) = args.get(1) else { - ctx.notice(me, from.uid, "Syntax: REGISTER [email]"); - return; - }; - let email = args.get(2).map(|s| s.to_string()); - // The engine derives the password off-thread, commits, and answers. - ctx.defer_register(from.nick, *password, email, RegReply::NickServ { - agent: me.to_string(), - uid: from.uid.to_string(), - nick: from.nick.to_string(), - }); - } - Some("IDENTIFY") | Some("ID") => { - // IDENTIFY [account] — the account defaults to the - // current nick, so both the bare-password and account+password forms work. - 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] "); - return; - } - }; - // Distinguish an unregistered account from a wrong password, so a - // failed login says which it was. - if !db.exists(account_name) { - ctx.notice(me, from.uid, format!("\x02{account_name}\x02 isn't registered.")); - return; - } - match db.authenticate(account_name, password) { - Some(account) => { - // Already identified to this account: don't re-fire the login. - if from.account == Some(account) { - ctx.notice(me, from.uid, format!("You're already identified as \x02{}\x02.", account)); - return; - } - let account = account.to_string(); - ctx.login(from.uid, &account); - ctx.notice(me, from.uid, format!("You're now identified as \x02{}\x02. Welcome back!", account)); - } - None => ctx.notice(me, from.uid, "Invalid password. Please try again."), - } - } - Some("LOGOUT") | Some("LOGOFF") => { - if from.account.is_none() { - ctx.notice(me, from.uid, "You're not logged in."); - return; - } - // Guest nick = prefix + sequence (seeded from the link TS, bumped - // per logout). Inlined field access so it stays disjoint from `me`. - let guest = format!("{}{}", self.guest_nick, self.guest_seq); - self.guest_seq = self.guest_seq.wrapping_add(1); - ctx.logout(from.uid); - ctx.force_nick(from.uid, &guest); - ctx.notice(me, from.uid, format!("You're now logged out. Your nick is now \x02{}\x02.", guest)); - } - Some("CERT") => self.cert(from, &args, ctx, db), - Some("HELP") => ctx.notice( - me, - from.uid, - "NickServ looks after your nickname. Commands: \x02REGISTER\x02 [email], \x02IDENTIFY\x02 [account] , \x02LOGOUT\x02, \x02CERT\x02 ADD|DEL|LIST [fingerprint].", - ), + Some("REGISTER") => register::handle(me, from, args, ctx), + Some("IDENTIFY") | Some("ID") => identify::handle(me, from, args, ctx, db), + Some("LOGOUT") | Some("LOGOFF") => logout::handle(me, &self.guest_nick, &mut self.guest_seq, from, ctx), + Some("CERT") => cert::handle(me, from, args, ctx, db), + Some("HELP") => ctx.notice(me, from.uid, "NickServ looks after your nickname. Commands: \x02REGISTER\x02 [email], \x02IDENTIFY\x02 [account] , \x02LOGOUT\x02, \x02CERT\x02 ADD|DEL|LIST [fingerprint]."), Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")), None => {} } } } - -impl NickServ { - // CERT ADD|DEL|LIST [fingerprint]: manage the TLS certificate - // fingerprints that may log in to your account via SASL EXTERNAL. Gated by - // the account password, so it needs no separate identified-session state. - fn cert(&self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { - let me = self.uid.as_str(); - // All subcommands authenticate the sender's nick's account by password. - let auth = |db: &Db, password: &str| db.authenticate(from.nick, password).map(str::to_string); - match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() { - Some("LIST") => { - let Some(password) = args.get(2) else { - ctx.notice(me, from.uid, "Syntax: CERT LIST "); - return; - }; - let Some(account) = auth(db, password) else { - ctx.notice(me, from.uid, "Invalid password."); - return; - }; - let fps = db.certfps(&account); - if fps.is_empty() { - ctx.notice(me, from.uid, "No certificate fingerprints are registered to your account."); - } else { - ctx.notice(me, from.uid, format!("Registered fingerprints: {}", fps.join(", "))); - } - } - Some("ADD") => { - let (Some(password), Some(fp)) = (args.get(2), args.get(3)) else { - ctx.notice(me, from.uid, "Syntax: CERT ADD "); - return; - }; - let Some(account) = auth(db, password) else { - ctx.notice(me, from.uid, "Invalid password."); - return; - }; - match db.certfp_add(&account, fp) { - Ok(()) => ctx.notice(me, from.uid, format!("Added fingerprint \x02{fp}\x02. You can now log in with SASL EXTERNAL.")), - Err(CertError::InUse) => ctx.notice(me, from.uid, "That fingerprint is already registered."), - Err(CertError::Invalid) => ctx.notice(me, from.uid, "That does not look like a certificate fingerprint."), - Err(CertError::NoAccount | CertError::Internal) => ctx.notice(me, from.uid, "Could not add the fingerprint, please try again later."), - } - } - Some("DEL") | Some("REMOVE") => { - let (Some(password), Some(fp)) = (args.get(2), args.get(3)) else { - ctx.notice(me, from.uid, "Syntax: CERT DEL "); - return; - }; - let Some(account) = auth(db, password) else { - ctx.notice(me, from.uid, "Invalid password."); - return; - }; - match db.certfp_del(&account, fp) { - Ok(true) => ctx.notice(me, from.uid, format!("Removed fingerprint \x02{fp}\x02.")), - Ok(false) => ctx.notice(me, from.uid, "No such fingerprint is registered to your account."), - Err(_) => ctx.notice(me, from.uid, "Could not remove the fingerprint, please try again later."), - } - } - _ => ctx.notice(me, from.uid, "Syntax: CERT ADD|DEL|LIST [fingerprint]"), - } - } -} diff --git a/modules/nickserv/register.rs b/modules/nickserv/register.rs new file mode 100644 index 0000000..be9dbd8 --- /dev/null +++ b/modules/nickserv/register.rs @@ -0,0 +1,17 @@ +use crate::engine::service::{Sender, ServiceCtx}; +use crate::proto::RegReply; + +// REGISTER [email]: register the sender's current nick. The engine +// derives the password off-thread, commits, and answers. +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx) { + let Some(password) = args.get(1) else { + ctx.notice(me, from.uid, "Syntax: REGISTER [email]"); + return; + }; + let email = args.get(2).map(|s| s.to_string()); + ctx.defer_register(from.nick, *password, email, RegReply::NickServ { + agent: me.to_string(), + uid: from.uid.to_string(), + nick: from.nick.to_string(), + }); +}