Add NickServ LOGOUT command
Clears the account (RPL_LOGGEDOUT) and renames the user to a configurable guest nick (default Guest) via SVSNICK.
This commit is contained in:
parent
a382ac8ad3
commit
60fc687162
7 changed files with 77 additions and 2 deletions
|
|
@ -25,12 +25,21 @@ pub struct Server {
|
|||
// on the single-threaded link matters more than verifier strength.
|
||||
#[serde(default = "default_scram_iterations")]
|
||||
pub scram_iterations: u32,
|
||||
// Nick prefix a user is renamed to on NickServ LOGOUT (they keep the ircd's
|
||||
// guest number appended, e.g. Guest12345). Must start with a letter — the
|
||||
// ircd rejects a digit-leading SVSNICK and falls back to the raw uuid.
|
||||
#[serde(default = "default_guest_nick")]
|
||||
pub guest_nick: String,
|
||||
}
|
||||
|
||||
fn default_protocol() -> u32 {
|
||||
1206 // InspIRCd 4 spanning-tree protocol (1205 = insp3)
|
||||
}
|
||||
|
||||
fn default_guest_nick() -> String {
|
||||
"Guest".to_string()
|
||||
}
|
||||
|
||||
fn default_scram_iterations() -> u32 {
|
||||
crate::engine::scram::DEFAULT_ITERATIONS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -468,7 +468,7 @@ mod tests {
|
|||
let mut db = Db::open(&path);
|
||||
db.scram_iterations = 4096; // keep the debug-build verifier cheap in tests
|
||||
assert!(db.register(account, password, None).is_ok());
|
||||
Engine::new(vec![Box::new(NickServ { uid: "42SAAAAAA".to_string() })], db)
|
||||
Engine::new(vec![Box::new(NickServ { uid: "42SAAAAAA".to_string(), guest_nick: "Guest".to_string(), guest_seq: 12345 })], db)
|
||||
}
|
||||
|
||||
fn sasl(engine: &mut Engine, mode: &str, chunk: &str) -> Vec<NetAction> {
|
||||
|
|
@ -721,4 +721,27 @@ mod tests {
|
|||
e.db.register("bar", "sesame", None).unwrap();
|
||||
assert!(matches!(e.db.certfp_add("bar", fp), Err(db::CertError::InUse)), "a fingerprint maps to one account");
|
||||
}
|
||||
|
||||
// IDENTIFY logs in, LOGOUT clears the accountname (ircd emits RPL_LOGGEDOUT).
|
||||
#[test]
|
||||
fn nickserv_logout_clears_account() {
|
||||
let mut e = engine_with("nslogout", "foo", "sesame");
|
||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "foo".into() });
|
||||
|
||||
let login = e.handle(NetEvent::Privmsg {
|
||||
from: "000AAAAAB".into(),
|
||||
to: "42SAAAAAA".into(),
|
||||
text: "IDENTIFY sesame".into(),
|
||||
});
|
||||
assert!(login.iter().any(|a| matches!(a, NetAction::Metadata { key, value, .. } if key == "accountname" && value == "foo")), "{login:?}");
|
||||
|
||||
let out = e.handle(NetEvent::Privmsg {
|
||||
from: "000AAAAAB".into(),
|
||||
to: "42SAAAAAA".into(),
|
||||
text: "LOGOUT".into(),
|
||||
});
|
||||
assert!(out.iter().any(|a| matches!(a, NetAction::Metadata { key, value, .. } if key == "accountname" && value.is_empty())), "logout clears accountname: {out:?}");
|
||||
assert!(out.iter().any(|a| matches!(a, NetAction::ForceNick { uid, nick } if uid == "000AAAAAB" && nick == "Guest12345")), "logout renames to guest nick: {out:?}");
|
||||
assert!(out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("logged out"))), "{out:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,4 +54,22 @@ impl ServiceCtx {
|
|||
value: account.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Log a user out: clearing the accountname the ircd turns into RPL_LOGGEDOUT
|
||||
// (901) and drops from account-tag / WHOX. The inverse of `login`.
|
||||
pub fn logout(&mut self, uid: &str) {
|
||||
self.actions.push(NetAction::Metadata {
|
||||
target: uid.to_string(),
|
||||
key: "accountname".to_string(),
|
||||
value: String::new(),
|
||||
});
|
||||
}
|
||||
|
||||
// Force a user's nick (SVSNICK), e.g. to a guest nick after logout.
|
||||
pub fn force_nick(&mut self, uid: &str, nick: &str) {
|
||||
self.actions.push(NetAction::ForceNick {
|
||||
uid: uid.to_string(),
|
||||
nick: nick.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ async fn main() -> Result<()> {
|
|||
|
||||
let services: Vec<Box<dyn engine::service::Service>> = vec![Box::new(NickServ {
|
||||
uid: format!("{}AAAAAA", cfg.server.sid),
|
||||
guest_nick: cfg.server.guest_nick.clone(),
|
||||
guest_seq: (ts % 100_000) as u32,
|
||||
})];
|
||||
let mut db = engine::db::Db::open("fedserv.db.jsonl");
|
||||
db.scram_iterations = cfg.server.scram_iterations;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// sequence in Network-Links (protocols/inspircd.py); mode/burst details get
|
||||
// firmed up against a live insp4 uplink.
|
||||
use super::{NetAction, NetEvent, Protocol};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub struct InspIrcd {
|
||||
sid: String,
|
||||
|
|
@ -163,6 +164,12 @@ impl Protocol for InspIrcd {
|
|||
NetAction::Metadata { target, key, value } => {
|
||||
vec![self.from_us(format!("METADATA {} {} :{}", target, key, value))]
|
||||
}
|
||||
// SVSNICK <uid> <newnick> <nickts> — the new nick takes the current
|
||||
// time as its TS so it wins any collision resolution.
|
||||
NetAction::ForceNick { uid, nick } => {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(self.ts);
|
||||
vec![self.from_us(format!("SVSNICK {} {} {}", uid, nick, now))]
|
||||
}
|
||||
NetAction::Raw(s) => vec![s.clone()],
|
||||
// Internal: the link layer handles this before serialization.
|
||||
NetAction::DeferRegister { .. } => vec![],
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ pub enum NetAction {
|
|||
// Publish network state to the uplink: target "*" is server-global (e.g. the
|
||||
// advertised SASL mechanism list), otherwise a user uid (e.g. their account).
|
||||
Metadata { target: String, key: String, value: String },
|
||||
// Force a user's nick (SVSNICK), e.g. renaming to a guest nick on logout.
|
||||
// The protocol stamps the new nick's timestamp.
|
||||
ForceNick { uid: String, nick: String },
|
||||
Raw(String),
|
||||
// Internal only, never serialized to the wire: a registration whose password
|
||||
// still needs its (expensive) key derivation. The link layer runs the
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ use crate::proto::RegReply;
|
|||
|
||||
pub struct NickServ {
|
||||
pub uid: String,
|
||||
// Nick prefix assigned on LOGOUT (default "Guest"); a per-session sequence is
|
||||
// appended so successive guests don't collide within a run.
|
||||
pub guest_nick: String,
|
||||
pub guest_seq: u32,
|
||||
}
|
||||
|
||||
impl Service for NickServ {
|
||||
|
|
@ -48,11 +52,20 @@ impl Service for NickServ {
|
|||
None => ctx.notice(me, from.uid, "Invalid password."),
|
||||
}
|
||||
}
|
||||
Some("LOGOUT") | Some("LOGOFF") => {
|
||||
// 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 are 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,
|
||||
"Commands: REGISTER <password> [email], IDENTIFY <password>, CERT ADD|DEL|LIST <password> [fingerprint].",
|
||||
"Commands: REGISTER <password> [email], IDENTIFY <password>, LOGOUT, CERT ADD|DEL|LIST <password> [fingerprint].",
|
||||
),
|
||||
Some(other) => ctx.notice(me, from.uid, format!("Unknown command: {}. Try HELP.", other)),
|
||||
None => {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue