Derive registration credentials off-thread; rate-limit REGISTER; constant-time SCRAM compare
This commit is contained in:
parent
496c0f99a6
commit
9a918839fe
10 changed files with 214 additions and 36 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -119,6 +119,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"subtle",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ base64 = "0.22"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
|
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
|
||||||
|
subtle = "2"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,15 @@ pub enum RegError {
|
||||||
Internal,
|
Internal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The expensive, password-derived half of an account, computed once at
|
||||||
|
// registration. Split out from `register` so the derivation (argon2 + two SCRAM
|
||||||
|
// verifiers, ~1s at the default cost) can run off the reactor via spawn_blocking.
|
||||||
|
pub struct Credentials {
|
||||||
|
password_hash: String,
|
||||||
|
scram256: String,
|
||||||
|
scram512: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Db {
|
pub struct Db {
|
||||||
accounts: HashMap<String, Account>, // keyed by casefolded name
|
accounts: HashMap<String, Account>, // keyed by casefolded name
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
@ -75,24 +84,43 @@ impl Db {
|
||||||
self.accounts.contains_key(&key(name))
|
self.accounts.contains_key(&key(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(&mut self, name: &str, password: &str, email: Option<String>) -> Result<(), RegError> {
|
/// Derive the password-bound half of an account. Pure and CPU-heavy (no
|
||||||
|
/// `&self`), so a caller can run it on a blocking thread; the cheap
|
||||||
|
/// `register_prepared` then commits the result.
|
||||||
|
pub fn derive_credentials(password: &str, iterations: u32) -> Option<Credentials> {
|
||||||
|
Some(Credentials {
|
||||||
|
password_hash: hash_password(password)?,
|
||||||
|
scram256: scram::make_verifier(Hash::Sha256, password, iterations),
|
||||||
|
scram512: scram::make_verifier(Hash::Sha512, password, iterations),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit pre-derived credentials as a new account. Cheap: no key stretching.
|
||||||
|
pub fn register_prepared(&mut self, name: &str, creds: Credentials, email: Option<String>) -> Result<(), RegError> {
|
||||||
if self.exists(name) {
|
if self.exists(name) {
|
||||||
return Err(RegError::Exists);
|
return Err(RegError::Exists);
|
||||||
}
|
}
|
||||||
let password_hash = hash_password(password).ok_or(RegError::Internal)?;
|
|
||||||
let account = Account {
|
let account = Account {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
password_hash,
|
password_hash: creds.password_hash,
|
||||||
email,
|
email,
|
||||||
ts: now(),
|
ts: now(),
|
||||||
scram256: Some(scram::make_verifier(Hash::Sha256, password, self.scram_iterations)),
|
scram256: Some(creds.scram256),
|
||||||
scram512: Some(scram::make_verifier(Hash::Sha512, password, self.scram_iterations)),
|
scram512: Some(creds.scram512),
|
||||||
};
|
};
|
||||||
self.append(&Event::AccountRegistered(account.clone())).map_err(|_| RegError::Internal)?;
|
self.append(&Event::AccountRegistered(account.clone())).map_err(|_| RegError::Internal)?;
|
||||||
self.accounts.insert(key(name), account);
|
self.accounts.insert(key(name), account);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register synchronously, deriving and committing in one call. A test helper;
|
||||||
|
/// the live paths derive off-thread via `derive_credentials` + `register_prepared`.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn register(&mut self, name: &str, password: &str, email: Option<String>) -> Result<(), RegError> {
|
||||||
|
let creds = Self::derive_credentials(password, self.scram_iterations).ok_or(RegError::Internal)?;
|
||||||
|
self.register_prepared(name, creds, email)
|
||||||
|
}
|
||||||
|
|
||||||
/// Check credentials; on success return the account's canonical name (its
|
/// Check credentials; on success return the account's canonical name (its
|
||||||
/// stored casing), else None.
|
/// stored casing), else None.
|
||||||
pub fn authenticate(&self, name: &str, password: &str) -> Option<&str> {
|
pub fn authenticate(&self, name: &str, password: &str) -> Option<&str> {
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ pub mod service;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
|
|
||||||
use crate::proto::{NetAction, NetEvent};
|
use crate::proto::{NetAction, NetEvent, RegReply};
|
||||||
use db::{Db, RegError};
|
use db::{Db, RegError};
|
||||||
use scram::Verifier;
|
use scram::Verifier;
|
||||||
use service::{Sender, Service, ServiceCtx};
|
use service::{Sender, Service, ServiceCtx};
|
||||||
|
|
@ -53,11 +54,23 @@ pub struct Engine {
|
||||||
network: Network,
|
network: Network,
|
||||||
db: Db,
|
db: Db,
|
||||||
sasl_sessions: HashMap<String, SaslSession>, // client uid -> in-progress exchange
|
sasl_sessions: HashMap<String, SaslSession>, // client uid -> in-progress exchange
|
||||||
|
reg_limiter: RegLimiter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
||||||
Self { services, network: Network::default(), db, sasl_sessions: HashMap::new() }
|
Self {
|
||||||
|
services,
|
||||||
|
network: Network::default(),
|
||||||
|
db,
|
||||||
|
sasl_sessions: HashMap::new(),
|
||||||
|
reg_limiter: RegLimiter::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cost of a fresh SCRAM verifier; read by the link layer for off-thread derivation.
|
||||||
|
pub fn scram_iterations(&self) -> u32 {
|
||||||
|
self.db.scram_iterations
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
||||||
|
|
@ -215,26 +228,40 @@ impl Engine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authority side of the IRCv3 account-registration relay: create the account
|
// Authority side of the IRCv3 account-registration relay. Hands the work to
|
||||||
// (same store as classic NickServ REGISTER) and answer the requesting ircd.
|
// the link layer (which derives the password off-thread) via DeferRegister.
|
||||||
fn account_request(&mut self, reqid: String, kind: String, account: String, p2: String, p3: String) -> Vec<NetAction> {
|
fn account_request(&mut self, reqid: String, kind: String, account: String, p2: String, p3: String) -> Vec<NetAction> {
|
||||||
if !kind.eq_ignore_ascii_case("REGISTER") {
|
if !kind.eq_ignore_ascii_case("REGISTER") {
|
||||||
return Vec::new(); // VERIFY / RESEND / STATUS: later
|
return Vec::new(); // VERIFY / RESEND / STATUS: later
|
||||||
}
|
}
|
||||||
let email = if p2.is_empty() || p2 == "*" { None } else { Some(p2) };
|
let email = if p2.is_empty() || p2 == "*" { None } else { Some(p2) };
|
||||||
let (status, code, message) = match self.db.register(&account, &p3, email) {
|
vec![NetAction::DeferRegister { account, password: p3, email, reply: RegReply::Relay { reqid, kind } }]
|
||||||
Ok(()) => ("success", "*", "Account registered."),
|
}
|
||||||
Err(RegError::Exists) => ("error", "ACCOUNT_EXISTS", "That account name is already registered."),
|
|
||||||
Err(RegError::Internal) => ("error", "TEMPORARILY_UNAVAILABLE", "Registration is unavailable, try again later."),
|
// Cheap gate run before the expensive derivation: reject an already-taken name
|
||||||
|
// outright, and rate-limit the rest so a REGISTER flood can't pin CPU. Returns
|
||||||
|
// the rejection response if refused, or None to proceed (spending a token).
|
||||||
|
pub fn pre_register_check(&mut self, account: &str, reply: &RegReply) -> Option<Vec<NetAction>> {
|
||||||
|
if self.db.exists(account) {
|
||||||
|
return Some(reg_reply(reply, RegOutcome::Exists, account));
|
||||||
|
}
|
||||||
|
if !self.reg_limiter.allow() {
|
||||||
|
return Some(reg_reply(reply, RegOutcome::RateLimited, account));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit credentials the link layer derived off-thread, then answer `reply`.
|
||||||
|
pub fn complete_register(&mut self, account: &str, creds: Option<db::Credentials>, email: Option<String>, reply: RegReply) -> Vec<NetAction> {
|
||||||
|
let Some(creds) = creds else {
|
||||||
|
return reg_reply(&reply, RegOutcome::Internal, account);
|
||||||
};
|
};
|
||||||
vec![NetAction::AccountResponse {
|
let outcome = match self.db.register_prepared(account, creds, email) {
|
||||||
reqid,
|
Ok(()) => RegOutcome::Ok,
|
||||||
kind,
|
Err(RegError::Exists) => RegOutcome::Exists,
|
||||||
account,
|
Err(RegError::Internal) => RegOutcome::Internal,
|
||||||
status: status.to_string(),
|
};
|
||||||
code: code.to_string(),
|
reg_reply(&reply, outcome, account)
|
||||||
message: message.to_string(),
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route a PRIVMSG addressed to a service (by uid or nick) into that service,
|
// Route a PRIVMSG addressed to a service (by uid or nick) into that service,
|
||||||
|
|
@ -277,6 +304,79 @@ fn sasl_success(agent: &str, client: &str, account: String) -> Vec<NetAction> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Outcome of a registration attempt, rendered to the right wire response below.
|
||||||
|
enum RegOutcome {
|
||||||
|
Ok,
|
||||||
|
Exists,
|
||||||
|
RateLimited,
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render a registration outcome for whichever origin asked (relay or NickServ),
|
||||||
|
// so both the pre-check rejection and the post-derivation result share one place.
|
||||||
|
fn reg_reply(reply: &RegReply, outcome: RegOutcome, account: &str) -> Vec<NetAction> {
|
||||||
|
match reply {
|
||||||
|
RegReply::Relay { reqid, kind } => {
|
||||||
|
let (status, code, message) = match outcome {
|
||||||
|
RegOutcome::Ok => ("success", "*", "Account registered."),
|
||||||
|
RegOutcome::Exists => ("error", "ACCOUNT_EXISTS", "That account name is already registered."),
|
||||||
|
RegOutcome::RateLimited => ("error", "TEMPORARILY_UNAVAILABLE", "Too many registrations, please wait a moment."),
|
||||||
|
RegOutcome::Internal => ("error", "TEMPORARILY_UNAVAILABLE", "Registration is unavailable, try again later."),
|
||||||
|
};
|
||||||
|
vec![NetAction::AccountResponse {
|
||||||
|
reqid: reqid.clone(),
|
||||||
|
kind: kind.clone(),
|
||||||
|
account: account.to_string(),
|
||||||
|
status: status.to_string(),
|
||||||
|
code: code.to_string(),
|
||||||
|
message: message.to_string(),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
RegReply::NickServ { agent, uid, nick } => {
|
||||||
|
let notice = |text: String| NetAction::Notice { from: agent.clone(), to: uid.clone(), text };
|
||||||
|
match outcome {
|
||||||
|
RegOutcome::Ok => vec![
|
||||||
|
// Registering identifies you to the nick right away (drives 900).
|
||||||
|
NetAction::Metadata { target: uid.clone(), key: "accountname".to_string(), value: nick.clone() },
|
||||||
|
notice(format!("Nickname \x02{nick}\x02 is now registered.")),
|
||||||
|
],
|
||||||
|
RegOutcome::Exists => vec![notice(format!("Nickname \x02{nick}\x02 is already registered."))],
|
||||||
|
RegOutcome::RateLimited => vec![notice("Too many registrations, please wait a moment.".to_string())],
|
||||||
|
RegOutcome::Internal => vec![notice("Registration failed, please try again later.".to_string())],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token bucket bounding how often the expensive registration derivation may run,
|
||||||
|
// so a REGISTER flood degrades to a trickle instead of pinning a core. Global on
|
||||||
|
// purpose: it caps total work regardless of which user or server relayed it.
|
||||||
|
struct RegLimiter {
|
||||||
|
tokens: f64,
|
||||||
|
last: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
const REG_BURST: f64 = 8.0; // registrations allowed back-to-back from a full bucket
|
||||||
|
const REG_REFILL_PER_SEC: f64 = 0.5; // steady-state: one every two seconds
|
||||||
|
|
||||||
|
impl RegLimiter {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { tokens: REG_BURST, last: Instant::now() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allow(&mut self) -> bool {
|
||||||
|
let now = Instant::now();
|
||||||
|
self.tokens = (self.tokens + now.duration_since(self.last).as_secs_f64() * REG_REFILL_PER_SEC).min(REG_BURST);
|
||||||
|
self.last = now;
|
||||||
|
if self.tokens >= 1.0 {
|
||||||
|
self.tokens -= 1.0;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use argon2::password_hash::rand_core::{OsRng, RngCore};
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use sha2::{Digest, Sha256, Sha512};
|
use sha2::{Digest, Sha256, Sha512};
|
||||||
|
use subtle::ConstantTimeEq;
|
||||||
|
|
||||||
// Provisioning cost. High on purpose: the client bears the Hi() work each login,
|
// Provisioning cost. High on purpose: the client bears the Hi() work each login,
|
||||||
// the server only at REGISTER, and the verifier must not be a weaker
|
// the server only at REGISTER, and the verifier must not be a weaker
|
||||||
|
|
@ -227,7 +228,8 @@ pub fn verify_final(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let client_key = xor(&proof, &client_signature);
|
let client_key = xor(&proof, &client_signature);
|
||||||
if h(hash, &client_key) != v.stored_key {
|
// Constant-time: StoredKey is a secret, so leak nothing through compare timing.
|
||||||
|
if h(hash, &client_key).ct_eq(&v.stored_key).unwrap_u8() != 1 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let server_signature = hmac(hash, &v.server_key, auth_message.as_bytes());
|
let server_signature = hmac(hash, &v.server_key, auth_message.as_bytes());
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::engine::db::Db;
|
use crate::engine::db::Db;
|
||||||
use crate::proto::NetAction;
|
use crate::proto::{NetAction, RegReply};
|
||||||
|
|
||||||
// Who sent the command, resolved by the engine (UID + current nick).
|
// Who sent the command, resolved by the engine (UID + current nick).
|
||||||
pub struct Sender<'a> {
|
pub struct Sender<'a> {
|
||||||
|
|
@ -33,6 +33,17 @@ impl ServiceCtx {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hand a registration to the engine to finish: its password derivation runs
|
||||||
|
// off the reactor, then the engine commits it and answers `reply`.
|
||||||
|
pub fn defer_register(&mut self, account: impl Into<String>, password: impl Into<String>, email: Option<String>, reply: RegReply) {
|
||||||
|
self.actions.push(NetAction::DeferRegister {
|
||||||
|
account: account.into(),
|
||||||
|
password: password.into(),
|
||||||
|
email,
|
||||||
|
reply,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Log a user into an account: sets the accountname the ircd turns into
|
// Log a user into an account: sets the accountname the ircd turns into
|
||||||
// RPL_LOGGEDIN (900) and exposes to account-tag / WHOX, the same login the
|
// RPL_LOGGEDIN (900) and exposes to account-tag / WHOX, the same login the
|
||||||
// SASL agent applies. Used after a successful REGISTER / IDENTIFY.
|
// SASL agent applies. Used after a successful REGISTER / IDENTIFY.
|
||||||
|
|
|
||||||
28
src/link.rs
28
src/link.rs
|
|
@ -2,8 +2,9 @@ use anyhow::Result;
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
|
use crate::engine::db::Db;
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::proto::Protocol;
|
use crate::proto::{NetAction, Protocol};
|
||||||
|
|
||||||
// One uplink session: connect, handshake + burst, then translate lines forever.
|
// One uplink session: connect, handshake + burst, then translate lines forever.
|
||||||
pub async fn run(mut proto: Box<dyn Protocol>, mut engine: Engine, addr: &str) -> Result<()> {
|
pub async fn run(mut proto: Box<dyn Protocol>, mut engine: Engine, addr: &str) -> Result<()> {
|
||||||
|
|
@ -24,8 +25,29 @@ pub async fn run(mut proto: Box<dyn Protocol>, mut engine: Engine, addr: &str) -
|
||||||
tracing::debug!(dir = "<<", %line);
|
tracing::debug!(dir = "<<", %line);
|
||||||
for event in proto.parse(&line) {
|
for event in proto.parse(&line) {
|
||||||
for action in engine.handle(event) {
|
for action in engine.handle(event) {
|
||||||
for out in proto.serialize(&action) {
|
let outs = match action {
|
||||||
send(&mut write, &out).await?;
|
// Registration: derive the password off the reactor so the
|
||||||
|
// ~1s of key stretching can't stall the link, after a cheap
|
||||||
|
// gate that rejects taken names and rate-limits floods.
|
||||||
|
NetAction::DeferRegister { account, password, email, reply } => {
|
||||||
|
match engine.pre_register_check(&account, &reply) {
|
||||||
|
Some(rejection) => rejection,
|
||||||
|
None => {
|
||||||
|
let iterations = engine.scram_iterations();
|
||||||
|
let creds = tokio::task::spawn_blocking(move || {
|
||||||
|
Db::derive_credentials(&password, iterations)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
engine.complete_register(&account, creds, email, reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action => vec![action],
|
||||||
|
};
|
||||||
|
for act in outs {
|
||||||
|
for out in proto.serialize(&act) {
|
||||||
|
send(&mut write, &out).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,8 @@ impl Protocol for InspIrcd {
|
||||||
vec![self.from_us(format!("METADATA {} {} :{}", target, key, value))]
|
vec![self.from_us(format!("METADATA {} {} :{}", target, key, value))]
|
||||||
}
|
}
|
||||||
NetAction::Raw(s) => vec![s.clone()],
|
NetAction::Raw(s) => vec![s.clone()],
|
||||||
|
// Internal: the link layer handles this before serialization.
|
||||||
|
NetAction::DeferRegister { .. } => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,19 @@ pub enum NetAction {
|
||||||
// advertised SASL mechanism list), otherwise a user uid (e.g. their account).
|
// advertised SASL mechanism list), otherwise a user uid (e.g. their account).
|
||||||
Metadata { target: String, key: String, value: String },
|
Metadata { target: String, key: String, value: String },
|
||||||
Raw(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
|
||||||
|
// derivation off-thread, then calls Engine::complete_register.
|
||||||
|
DeferRegister { account: String, password: String, email: Option<String>, reply: RegReply },
|
||||||
|
}
|
||||||
|
|
||||||
|
// How to answer a registration once its credentials have been derived.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum RegReply {
|
||||||
|
// IRCv3 account-registration relay: answer the requesting ircd.
|
||||||
|
Relay { reqid: String, kind: String },
|
||||||
|
// NickServ REGISTER: NOTICE the requesting user, logging them in on success.
|
||||||
|
NickServ { agent: String, uid: String, nick: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Protocol: Send {
|
pub trait Protocol: Send {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::engine::db::{Db, RegError};
|
use crate::engine::db::Db;
|
||||||
use crate::engine::service::{Sender, Service, ServiceCtx};
|
use crate::engine::service::{Sender, Service, ServiceCtx};
|
||||||
|
use crate::proto::RegReply;
|
||||||
|
|
||||||
pub struct NickServ {
|
pub struct NickServ {
|
||||||
pub uid: String,
|
pub uid: String,
|
||||||
|
|
@ -26,15 +27,12 @@ impl Service for NickServ {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let email = args.get(2).map(|s| s.to_string());
|
let email = args.get(2).map(|s| s.to_string());
|
||||||
match db.register(from.nick, password, email) {
|
// The engine derives the password off-thread, commits, and answers.
|
||||||
Ok(()) => {
|
ctx.defer_register(from.nick, *password, email, RegReply::NickServ {
|
||||||
// Registering identifies you to the nick right away (drives 900).
|
agent: me.to_string(),
|
||||||
ctx.login(from.uid, from.nick);
|
uid: from.uid.to_string(),
|
||||||
ctx.notice(me, from.uid, format!("Nickname \x02{}\x02 is now registered.", from.nick));
|
nick: from.nick.to_string(),
|
||||||
}
|
});
|
||||||
Err(RegError::Exists) => ctx.notice(me, from.uid, format!("Nickname \x02{}\x02 is already registered.", from.nick)),
|
|
||||||
Err(RegError::Internal) => ctx.notice(me, from.uid, "Registration failed, please try again later."),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some("IDENTIFY") => {
|
Some("IDENTIFY") => {
|
||||||
let Some(password) = args.get(1) else {
|
let Some(password) = args.get(1) else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue