nickserv: split into one file per command
Mirrors the chanserv layout: nickserv.rs keeps the service and dispatch, each command (register, identify, logout, cert) lives in its own file with its subcommands and parameters. Behaviour is unchanged.
This commit is contained in:
parent
921088bfca
commit
9d6cf3bb08
5 changed files with 138 additions and 127 deletions
59
modules/nickserv/cert.rs
Normal file
59
modules/nickserv/cert.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
use crate::engine::db::{CertError, Db};
|
||||||
|
use crate::engine::service::{Sender, ServiceCtx};
|
||||||
|
|
||||||
|
// CERT ADD|DEL|LIST <password> [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 <password>");
|
||||||
|
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 <password> <fingerprint>");
|
||||||
|
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 <password> <fingerprint>");
|
||||||
|
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 <password> [fingerprint]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
33
modules/nickserv/identify.rs
Normal file
33
modules/nickserv/identify.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use crate::engine::db::Db;
|
||||||
|
use crate::engine::service::{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: &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] <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;
|
||||||
|
}
|
||||||
|
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."),
|
||||||
|
}
|
||||||
|
}
|
||||||
14
modules/nickserv/logout.rs
Normal file
14
modules/nickserv/logout.rs
Normal file
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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::service::{Sender, Service, ServiceCtx};
|
||||||
use crate::engine::state::Network;
|
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 struct NickServ {
|
||||||
pub uid: String,
|
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) {
|
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, _net: &Network, db: &mut Db) {
|
||||||
let me = self.uid.as_str();
|
let me = self.uid.as_str();
|
||||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
Some("REGISTER") => {
|
Some("REGISTER") => register::handle(me, from, args, ctx),
|
||||||
// REGISTER <password> [email] — registers the sender's current nick.
|
Some("IDENTIFY") | Some("ID") => identify::handle(me, from, args, ctx, db),
|
||||||
let Some(password) = args.get(1) else {
|
Some("LOGOUT") | Some("LOGOFF") => logout::handle(me, &self.guest_nick, &mut self.guest_seq, from, ctx),
|
||||||
ctx.notice(me, from.uid, "Syntax: REGISTER <password> [email]");
|
Some("CERT") => cert::handle(me, from, args, ctx, db),
|
||||||
return;
|
Some("HELP") => ctx.notice(me, from.uid, "NickServ looks after your nickname. Commands: \x02REGISTER\x02 <password> [email], \x02IDENTIFY\x02 [account] <password>, \x02LOGOUT\x02, \x02CERT\x02 ADD|DEL|LIST <password> [fingerprint]."),
|
||||||
};
|
|
||||||
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] <password> — 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] <password>");
|
|
||||||
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 <password> [email], \x02IDENTIFY\x02 [account] <password>, \x02LOGOUT\x02, \x02CERT\x02 ADD|DEL|LIST <password> [fingerprint].",
|
|
||||||
),
|
|
||||||
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")),
|
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")),
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NickServ {
|
|
||||||
// CERT ADD|DEL|LIST <password> [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 <password>");
|
|
||||||
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 <password> <fingerprint>");
|
|
||||||
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 <password> <fingerprint>");
|
|
||||||
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 <password> [fingerprint]"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
17
modules/nickserv/register.rs
Normal file
17
modules/nickserv/register.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
use crate::engine::service::{Sender, ServiceCtx};
|
||||||
|
use crate::proto::RegReply;
|
||||||
|
|
||||||
|
// REGISTER <password> [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 <password> [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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue