Add persisted account store with NickServ REGISTER/IDENTIFY
Accounts are an append-only JSONL event log (state is the fold of the log) with argon2id-hashed passwords. NickServ gains REGISTER <password> [email] and IDENTIFY <password>; the engine tracks UID->nick from the burst so a command resolves to the sender's current nick. Classic and ircd-agnostic — the cap-based account-registration forward is the next step.
This commit is contained in:
parent
7929f5f9f4
commit
9285105afd
11 changed files with 348 additions and 41 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
||||||
/target
|
/target
|
||||||
config.toml
|
config.toml
|
||||||
|
|
||||||
|
fedserv.db.jsonl
|
||||||
|
|
|
||||||
152
Cargo.lock
generated
152
Cargo.lock
generated
|
|
@ -17,6 +17,42 @@ version = "1.0.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"cpufeatures",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.12.1"
|
version = "1.12.1"
|
||||||
|
|
@ -29,6 +65,36 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -40,13 +106,36 @@ name = "fedserv"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"argon2",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
|
|
@ -63,6 +152,12 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -122,6 +217,17 @@ version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
|
@ -146,6 +252,15 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
|
|
@ -193,6 +308,19 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.9"
|
version = "0.6.9"
|
||||||
|
|
@ -227,6 +355,12 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.118"
|
version = "2.0.118"
|
||||||
|
|
@ -375,6 +509,12 @@ dependencies = [
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|
@ -387,6 +527,12 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
|
@ -416,3 +562,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ description = "Federated IRC services daemon (protocol-agnostic core, InspIRCd l
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "macros", "time"] }
|
tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "macros", "time"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
argon2 = { version = "0.5", features = ["std"] }
|
||||||
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"] }
|
||||||
|
|
|
||||||
101
src/engine/db.rs
Normal file
101
src/engine/db.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use argon2::password_hash::rand_core::OsRng;
|
||||||
|
use argon2::password_hash::SaltString;
|
||||||
|
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Account {
|
||||||
|
pub name: String,
|
||||||
|
pub password_hash: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub ts: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-sourced persistence: every change is an Event appended to a JSONL log,
|
||||||
|
// and account state is the fold of that log. Replicating this log across nodes
|
||||||
|
// is what turns the store federated later, without changing the services.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "event")]
|
||||||
|
pub enum Event {
|
||||||
|
AccountRegistered(Account),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RegError {
|
||||||
|
Exists,
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Db {
|
||||||
|
accounts: HashMap<String, Account>, // keyed by casefolded name
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(name: &str) -> String {
|
||||||
|
name.to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now() -> u64 {
|
||||||
|
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Db {
|
||||||
|
pub fn open(path: impl Into<PathBuf>) -> Self {
|
||||||
|
let path = path.into();
|
||||||
|
let mut accounts = HashMap::new();
|
||||||
|
if let Ok(data) = std::fs::read_to_string(&path) {
|
||||||
|
for line in data.lines().filter(|l| !l.trim().is_empty()) {
|
||||||
|
match serde_json::from_str::<Event>(line) {
|
||||||
|
Ok(Event::AccountRegistered(a)) => {
|
||||||
|
accounts.insert(key(&a.name), a);
|
||||||
|
}
|
||||||
|
Err(e) => tracing::warn!(%e, "skipping malformed event log line"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::info!(accounts = accounts.len(), ?path, "account store loaded");
|
||||||
|
Self { accounts, path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(&self, name: &str) -> bool {
|
||||||
|
self.accounts.contains_key(&key(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(&mut self, name: &str, password: &str, email: Option<String>) -> Result<(), RegError> {
|
||||||
|
if self.exists(name) {
|
||||||
|
return Err(RegError::Exists);
|
||||||
|
}
|
||||||
|
let password_hash = hash_password(password).ok_or(RegError::Internal)?;
|
||||||
|
let account = Account { name: name.to_string(), password_hash, email, ts: now() };
|
||||||
|
self.append(&Event::AccountRegistered(account.clone())).map_err(|_| RegError::Internal)?;
|
||||||
|
self.accounts.insert(key(name), account);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self, name: &str, password: &str) -> bool {
|
||||||
|
match self.accounts.get(&key(name)) {
|
||||||
|
Some(a) => verify_password(password, &a.password_hash),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append(&self, event: &Event) -> std::io::Result<()> {
|
||||||
|
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
|
||||||
|
writeln!(f, "{}", serde_json::to_string(event).unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_password(password: &str) -> Option<String> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
Argon2::default().hash_password(password.as_bytes(), &salt).ok().map(|h| h.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_password(password: &str, hash: &str) -> bool {
|
||||||
|
PasswordHash::new(hash)
|
||||||
|
.map(|parsed| Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,21 @@
|
||||||
|
pub mod db;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
use crate::proto::{NetAction, NetEvent};
|
use crate::proto::{NetAction, NetEvent};
|
||||||
use service::{Service, ServiceCtx};
|
use db::Db;
|
||||||
use state::{EventLog, Network};
|
use service::{Sender, Service, ServiceCtx};
|
||||||
|
use state::Network;
|
||||||
|
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
services: Vec<Box<dyn Service>>,
|
services: Vec<Box<dyn Service>>,
|
||||||
#[allow(dead_code)]
|
|
||||||
network: Network,
|
network: Network,
|
||||||
#[allow(dead_code)]
|
db: Db,
|
||||||
log: EventLog,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
pub fn new(services: Vec<Box<dyn Service>>) -> Self {
|
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
||||||
Self {
|
Self { services, network: Network::default(), db }
|
||||||
services,
|
|
||||||
network: Network::default(),
|
|
||||||
log: EventLog::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
||||||
|
|
@ -41,18 +37,30 @@ impl Engine {
|
||||||
pub fn handle(&mut self, event: NetEvent) -> Vec<NetAction> {
|
pub fn handle(&mut self, event: NetEvent) -> Vec<NetAction> {
|
||||||
match event {
|
match event {
|
||||||
NetEvent::Ping { token, from } => vec![NetAction::Pong { token, from }],
|
NetEvent::Ping { token, from } => vec![NetAction::Pong { token, from }],
|
||||||
|
NetEvent::UserConnect { uid, nick } => {
|
||||||
|
self.network.user_connect(uid, nick);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
NetEvent::Quit { uid } => {
|
||||||
|
self.network.user_quit(&uid);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
NetEvent::Privmsg { from, to, text } => self.dispatch(&from, &to, &text),
|
NetEvent::Privmsg { from, to, text } => self.dispatch(&from, &to, &text),
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
||||||
|
// handing it the sender's resolved nick and the account store.
|
||||||
fn dispatch(&mut self, from: &str, to: &str, text: &str) -> Vec<NetAction> {
|
fn dispatch(&mut self, from: &str, to: &str, text: &str) -> Vec<NetAction> {
|
||||||
|
let nick = self.network.nick_of(from).unwrap_or(from).to_string();
|
||||||
let mut ctx = ServiceCtx::default();
|
let mut ctx = ServiceCtx::default();
|
||||||
for svc in self.services.iter_mut() {
|
let sender = Sender { uid: from, nick: &nick };
|
||||||
|
let Self { services, db, .. } = self;
|
||||||
|
for svc in services.iter_mut() {
|
||||||
if to.eq_ignore_ascii_case(svc.uid()) || to.eq_ignore_ascii_case(svc.nick()) {
|
if to.eq_ignore_ascii_case(svc.uid()) || to.eq_ignore_ascii_case(svc.nick()) {
|
||||||
let args: Vec<&str> = text.split_whitespace().collect();
|
let args: Vec<&str> = text.split_whitespace().collect();
|
||||||
svc.on_command(from, &args, &mut ctx);
|
svc.on_command(&sender, &args, &mut ctx, db);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
|
use crate::engine::db::Db;
|
||||||
use crate::proto::NetAction;
|
use crate::proto::NetAction;
|
||||||
|
|
||||||
// A pseudo-client (NickServ, ChanServ, OperServ, ...). Introduced at burst,
|
// Who sent the command, resolved by the engine (UID + current nick).
|
||||||
// receives the commands users message it, and pushes actions onto the ctx.
|
pub struct Sender<'a> {
|
||||||
|
pub uid: &'a str,
|
||||||
|
pub nick: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pseudo-client (NickServ, ChanServ, ...). Introduced at burst, receives the
|
||||||
|
// commands users message it, reads/writes the account store, and pushes actions.
|
||||||
pub trait Service: Send {
|
pub trait Service: Send {
|
||||||
fn nick(&self) -> &str;
|
fn nick(&self) -> &str;
|
||||||
fn uid(&self) -> &str;
|
fn uid(&self) -> &str;
|
||||||
|
|
@ -9,10 +16,9 @@ pub trait Service: Send {
|
||||||
"services.local"
|
"services.local"
|
||||||
}
|
}
|
||||||
fn gecos(&self) -> &str;
|
fn gecos(&self) -> &str;
|
||||||
fn on_command(&mut self, from: &str, args: &[&str], ctx: &mut ServiceCtx);
|
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db);
|
||||||
}
|
}
|
||||||
|
|
||||||
// What a service can do in response to a command, collected for the link to flush.
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ServiceCtx {
|
pub struct ServiceCtx {
|
||||||
pub actions: Vec<NetAction>,
|
pub actions: Vec<NetAction>,
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,34 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// Live network view the services reason about.
|
// Live network view, rebuilt from the uplink's burst each connect (ephemeral —
|
||||||
|
// unlike the account store, which persists).
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Network {
|
pub struct Network {
|
||||||
pub users: HashMap<String, User>, // keyed by UID
|
pub users: HashMap<String, User>, // keyed by UID
|
||||||
pub channels: HashMap<String, Channel>,
|
pub channels: HashMap<String, Channel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub uid: String,
|
pub uid: String,
|
||||||
pub nick: String,
|
pub nick: String,
|
||||||
pub account: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub ts: u64,
|
pub ts: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Sable-inspired core: every persistent change is an Event, and state is a
|
impl Network {
|
||||||
// fold over the log. Single-node today; replicating this log across service
|
pub fn user_connect(&mut self, uid: String, nick: String) {
|
||||||
// nodes is what turns it federated later, without rewriting the services.
|
self.users.insert(uid.clone(), User { uid, nick });
|
||||||
#[derive(Debug, Clone)]
|
}
|
||||||
pub enum Event {
|
|
||||||
AccountRegistered { account: String, uid: String },
|
|
||||||
ChannelRegistered { channel: String, founder: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
pub fn user_quit(&mut self, uid: &str) {
|
||||||
pub struct EventLog {
|
self.users.remove(uid);
|
||||||
pub events: Vec<Event>,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl EventLog {
|
pub fn nick_of(&self, uid: &str) -> Option<&str> {
|
||||||
pub fn append(&mut self, event: Event) {
|
self.users.get(uid).map(|u| u.nick.as_str())
|
||||||
self.events.push(event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ async fn main() -> Result<()> {
|
||||||
let services: Vec<Box<dyn engine::service::Service>> = vec![Box::new(NickServ {
|
let services: Vec<Box<dyn engine::service::Service>> = vec![Box::new(NickServ {
|
||||||
uid: format!("{}AAAAAA", cfg.server.sid),
|
uid: format!("{}AAAAAA", cfg.server.sid),
|
||||||
})];
|
})];
|
||||||
let engine = Engine::new(services);
|
let db = engine::db::Db::open("fedserv.db.jsonl");
|
||||||
|
let engine = Engine::new(services, db);
|
||||||
|
|
||||||
let addr = format!("{}:{}", cfg.uplink.host, cfg.uplink.port);
|
let addr = format!("{}:{}", cfg.uplink.host, cfg.uplink.port);
|
||||||
tracing::info!(server = %cfg.server.name, %addr, "linking to uplink");
|
tracing::info!(server = %cfg.server.name, %addr, "linking to uplink");
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,17 @@ impl Protocol for InspIrcd {
|
||||||
text: trailing(rest),
|
text: trailing(rest),
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
// UID <uuid> <nickts> <nick> … — track who's online so services can
|
||||||
|
// resolve a sender's current nick.
|
||||||
|
"UID" => {
|
||||||
|
let a: Vec<&str> = tokens.collect();
|
||||||
|
match (a.first(), a.get(2)) {
|
||||||
|
(Some(uid), Some(nick)) => {
|
||||||
|
vec![NetEvent::UserConnect { uid: uid.to_string(), nick: nick.to_string() }]
|
||||||
|
}
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
"QUIT" => vec![NetEvent::Quit { uid: source.unwrap_or_default() }],
|
"QUIT" => vec![NetEvent::Quit { uid: source.unwrap_or_default() }],
|
||||||
_ => vec![NetEvent::Unknown { line: line.to_string() }],
|
_ => vec![NetEvent::Unknown { line: line.to_string() }],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub enum NetEvent {
|
||||||
EndBurst,
|
EndBurst,
|
||||||
Ping { token: String, from: Option<String> },
|
Ping { token: String, from: Option<String> },
|
||||||
Privmsg { from: String, to: String, text: String },
|
Privmsg { from: String, to: String, text: String },
|
||||||
|
UserConnect { uid: String, nick: String },
|
||||||
Quit { uid: String },
|
Quit { uid: String },
|
||||||
Unknown { line: String },
|
Unknown { line: String },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::engine::service::{Service, ServiceCtx};
|
use crate::engine::db::{Db, RegError};
|
||||||
|
use crate::engine::service::{Sender, Service, ServiceCtx};
|
||||||
|
|
||||||
pub struct NickServ {
|
pub struct NickServ {
|
||||||
pub uid: String,
|
pub uid: String,
|
||||||
|
|
@ -15,10 +16,37 @@ impl Service for NickServ {
|
||||||
"Nickname Services"
|
"Nickname Services"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_command(&mut self, from: &str, args: &[&str], ctx: &mut ServiceCtx) {
|
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) {
|
||||||
|
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("HELP") => ctx.notice(self.uid(), from, "Commands: REGISTER, IDENTIFY (coming soon)."),
|
Some("REGISTER") => {
|
||||||
Some(other) => ctx.notice(self.uid(), from, format!("Unknown command: {}. Try HELP.", other)),
|
// REGISTER <password> [email] — registers the sender's current nick.
|
||||||
|
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());
|
||||||
|
match db.register(from.nick, password, email) {
|
||||||
|
Ok(()) => ctx.notice(me, from.uid, format!("Nickname \x02{}\x02 is now registered.", from.nick)),
|
||||||
|
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") => {
|
||||||
|
let Some(password) = args.get(1) else {
|
||||||
|
ctx.notice(me, from.uid, "Syntax: IDENTIFY <password>");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if db.verify(from.nick, password) {
|
||||||
|
ctx.notice(me, from.uid, format!("You are now identified for \x02{}\x02.", from.nick));
|
||||||
|
} else {
|
||||||
|
ctx.notice(me, from.uid, "Invalid password.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("HELP") => {
|
||||||
|
ctx.notice(me, from.uid, "Commands: REGISTER <password> [email], IDENTIFY <password>.")
|
||||||
|
}
|
||||||
|
Some(other) => ctx.notice(me, from.uid, format!("Unknown command: {}. Try HELP.", other)),
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue