Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
0

Configure Feed

Select the types of activity you want to include in your feed.

ircd.rs / leveva / src / registry.rs
68 kB 1560 lines
1//! The shared client registry: the authority on **client identity** across every 2//! connection. Held inside the `Arc<ServerContext>` so all per-connection tokio 3//! tasks see one table. 4//! 5//! Each registered client has a stable [`Uid`] (assigned at registration, never 6//! changing) and a current nick. The registry keeps two views in lockstep under one 7//! lock: 8//! 9//! - `by_nick`: RFC 1459 case-fold of the nick → [`Uid`]. This enforces nick 10//! uniqueness (`alice`/`ALICE`/`Alice[]` are one slot) and resolves a nick target 11//! (`PRIVMSG bob`) to its owner. 12//! - `by_uid`: [`Uid`] → the client's [`ClientRecord`] + its **mailbox** (an `mpsc` 13//! sender to that connection, so one connection can deliver a line to another). 14//! 15//! Keying delivery and channel membership on the UID rather than the nick is the 16//! point: a `NICK`-change moves only the `by_nick` entry, so nothing downstream 17//! (channels, in-flight fan-out targets) has to be re-keyed. 18 19use std::collections::HashMap; 20use std::sync::{Arc, Mutex}; 21 22use tokio::sync::mpsc::UnboundedSender; 23 24use crate::casemap; 25use crate::connstats::{ConnStats, ConnStatsSnapshot, SendqVerdict}; 26use crate::ident::Uid; 27 28/// A frame on a connection's mailbox. Most are plain wire bytes; an [`Envelope::Eject`] 29/// is the cross-connection **control plane** — one connection (an operator running 30/// `KILL`) telling another connection's serve loop to write a final line and then close. 31/// Riding the same `mpsc` as wire bytes guarantees the eject can never overtake wire 32/// frames already queued ahead of it (mailbox order is preserved). 33#[derive(Debug, Clone, PartialEq, Eq)] 34pub enum Envelope { 35 /// Pre-encoded wire bytes; write verbatim to the socket. 36 Wire(Vec<u8>), 37 /// Forced disconnect (a `KILL`): write `wire` to this connection, then close it, 38 /// relaying `QUIT :reason` to its channel co-members (via [`Session::force_close`]). 39 Eject { wire: Vec<u8>, reason: String }, 40 /// Forced nick change (a nick-collision `SAVE` that the **local** client lost on TS — P11 41 /// slice 133): write `wire` (the `:<oldmask> NICK <new>` line) to this connection **and** 42 /// update the session's cached nick to `new`, so its self-mask stays consistent with the 43 /// registry after the server renamed it to its UID. Rides the same mailbox so it cannot 44 /// overtake queued wire frames. 45 ForceNick { wire: Vec<u8>, new: String }, 46} 47 48impl Envelope { 49 /// The number of wire bytes this frame will write to the socket — the unit of SendQ 50 /// accounting (P11 slice 147). The `Eject` `reason` is the QUIT text relayed to co-members, 51 /// not written to this socket, so it does not count. 52 pub fn wire_len(&self) -> u64 { 53 match self { 54 Envelope::Wire(b) => b.len() as u64, 55 Envelope::Eject { wire, .. } | Envelope::ForceNick { wire, .. } => wire.len() as u64, 56 } 57 } 58} 59 60/// A per-connection mailbox: [`Envelope`]s pushed here are processed by that client's 61/// serve loop. Unbounded so the synchronous sender path (`Session::feed`) never blocks or 62/// `.await`s; the unboundedness is bounded by per-connection **SendQ** accounting (P11 slice 63/// 147 — [`ConnStats::sendq_admit`]), which kills a connection whose queue overruns its 64/// ceiling rather than letting the mailbox grow without limit. 65pub type Mailbox = UnboundedSender<Envelope>; 66 67/// The stored identity of one registered local client. The public snapshot 68/// ([`Registry::record_of`]/[`Registry::get`]) — the mailbox lives beside it in the 69/// private [`Entry`], so identity stays cheaply `Clone`/`Eq` for WHO/WHOIS. 70#[derive(Debug, Clone, PartialEq, Eq)] 71pub struct ClientRecord { 72 pub uid: Uid, 73 pub nick: String, 74 pub user: String, 75 pub host: String, 76 /// The client's **original** host as seen at connect (the socket-derived IP, before any 77 /// cloak or `CHGHOST` spoof). Set once at [`Registry::try_claim`] and never touched by 78 /// [`Registry::set_host`], so the `+x` IP-cloak ([`crate::cloak`]) is always computed 79 /// from — and reverts to — the real connect host, independent of later host changes 80 /// (the C `orighost`). Read by [`Registry::orighost`]. 81 pub orighost: String, 82 /// The free-text realname (gecos) from `USER`'s trailing field. Surfaced by 83 /// WHO/WHOIS (`311`/`352`); never used for routing. 84 pub realname: String, 85 /// The client's user-mode `FLAGS_*` bitmask (the `UserMode::bit()` set). The 86 /// connection owns the authoritative copy ([`crate::command::Registered::modes`]); 87 /// this snapshot is mirrored here by [`Registry::set_modes`] so other connections' 88 /// `WHO` sweeps can honour `+i` (invisible). The `+a` ([`crate::UserMode::Away`]) bit 89 /// is the exception — it is server-managed and toggled here in lockstep with `away` 90 /// by [`Registry::set_away`], never by `MODE`. 91 pub modes: u32, 92 /// The server-notice **snomask** — the category bitset ([`crate::snomask::SnoMask`]) this 93 /// client subscribes to while `+s` ([`crate::UserMode::ServerNotice`]) is set (P11 slice 94 /// 257). `0` until `+s` is set; a bare `+s` seeds [`crate::snomask::SnoMask::ALL`] (every 95 /// category, the slice-184 behaviour). The fan ([`crate::snotice::server_notice_cat`]) 96 /// delivers a notice only to a record whose `+s` bit **and** matching category bit are set. 97 /// Local-only — never mirrored from a remote server (the fan iterates local records only). 98 pub snomask: u32, 99 /// The away message, set by `AWAY :msg` and cleared by a bare `AWAY`. `Some` exactly 100 /// when the `+a` bit is set in `modes`. Read by another client's PRIVMSG (the `301` 101 /// auto-reply), WHO (`G`/`H`), and WHOIS (`301`) — which is why it lives in the shared 102 /// record rather than on the connection. 103 pub away: Option<String>, 104 /// The behavior-bearing IRCv3 capabilities this client negotiated 105 /// ([`crate::cap::ClientCaps`]). The connection owns the authoritative copy 106 /// ([`crate::command::Registered::caps`]); this snapshot is mirrored here by 107 /// [`Registry::set_caps`] so the **delivery plane** — running on another connection's 108 /// fan-out — can gate client-only (`+`) message tags and `TAGMSG` per recipient 109 /// (`message-tags`), exactly as `set_modes` mirrors `+i` for WHO. 110 pub caps: crate::cap::ClientCaps, 111 /// The client's TLS certificate fingerprint (CertFP, P11 slice 78), or `None` if it 112 /// presented no client certificate / connected in the clear. Set at registration for a 113 /// local client ([`crate::certfp`]) and by inbound `ENCAP CERTFP` for a remote one 114 /// ([`Registry::set_certfp`]); read by WHOIS (`276`) and the `operator` certfp gate. 115 pub certfp: Option<String>, 116 /// The services account this client is logged in to (P11 slice 139), or `None` if not 117 /// logged in. leveva keeps no account database of its own — the value is set entirely by 118 /// a remote server (services) broadcasting `:<sid> ENCAP * SU <uid> :<account>` 119 /// ([`Registry::set_account`], from [`crate::s2s::su`]). Read by WHOIS (`330 120 /// RPL_WHOISACCOUNT`) and the WHO/WHOX `%a` field. 121 pub account: Option<String>, 122 /// The matched `allow → class` connection-class **name** (P11 slice 187), or `None` when 123 /// no class applies — an open server (`class == None`), a remote/S2S-mirrored user (whose 124 /// home-server class leveva never learns over the link), or the unit-test [`try_claim`] 125 /// path. Set once at [`finalize_registration`](crate::session::Session::finalize_registration) 126 /// from the resolved I-line admission. Read by `TRACE` (`204`/`205` class field), falling 127 /// back to the default configured class when `None`. 128 pub class: Option<String>, 129 /// Whether the matched I-line set `kline-exempt` (the C `CFLAG_KEXEMPT` → the persistent 130 /// client `FLAGS_EXEMPT`). Set once at 131 /// [`finalize_registration`](crate::session::Session::finalize_registration) from the 132 /// resolved admission; `false` for a remote/S2S-mirrored user (never a local K-line 133 /// target) and for the unit-test [`try_claim`] path. Read by the `TKLINE` runtime reap 134 /// (`crate::command::tkline`) so an already-registered exempt client survives an 135 /// operator's later matching ban, mirroring the registration-time gate in 136 /// `Session::finalize_registration`. 137 pub kline_exempt: bool, 138 /// Whether the matched I-line set `xline-exempt` (the C `CFLAG_XEXEMPT`). Set once at 139 /// [`finalize_registration`](crate::session::Session::finalize_registration) from the resolved 140 /// admission (P11 slice 289); `false` for a remote/S2S-mirrored user (never a local X-line 141 /// target) and for the unit-test [`try_claim`] path. Read by the `XLINE` runtime reap 142 /// (`crate::command::xline`) so an already-registered exempt client survives an operator's later 143 /// matching realname ban, mirroring the registration-time X-line gate. 144 pub xline_exempt: bool, 145 /// The client's sign-on instant, in unix **nanoseconds** ([`crate::clock::unixnano`]). 146 /// Seeded once at [`Registry::try_claim`] and **never** moved thereafter. Rendered as 147 /// unix seconds in the WHOIS `317 RPL_WHOISIDLE` signon-time field (slice 97). 148 pub signon: u64, 149 /// The client's last-activity instant, in unix **nanoseconds**. Seeded equal to 150 /// [`Self::signon`] at claim and advanced to "now" **only** when this client *sends* a 151 /// `PRIVMSG` ([`Registry::bump_activity`], driven from `dispatch`) — faithful to the 152 /// oracle's `IDLE_FROM_MSG` model (only `m_private` bumps `user.last`). NOTICE and every 153 /// other command leave it untouched. The WHOIS `317` idle field is `now - last_activity`. 154 pub last_activity: u64, 155} 156 157/// One table slot: a client's identity plus the mailbox to reach its connection and the 158/// live per-connection traffic counters (P11 slice 145). The counters are an `Arc` shared 159/// with the connection's serve loop (the writer); `STATS l` reads them here. 160#[derive(Debug, Clone)] 161struct Entry { 162 record: ClientRecord, 163 mailbox: Mailbox, 164 stats: Arc<ConnStats>, 165 /// The SendQ ceiling in bytes (P11 slice 147). `0` disables the kill (account-only) — for 166 /// remote-user / test claims whose mailbox is never drained by a serve loop. A live local 167 /// client is claimed with a non-zero ceiling (its class `sendq`, or `DEFAULT_SENDQ`). 168 sendq_max: u64, 169} 170 171impl Entry { 172 /// Push a normal (droppable) wire frame onto this connection's mailbox with SendQ 173 /// accounting (P11 slice 147). On overflow the frame is replaced by a single terminal 174 /// `ERROR :Closing Link: … (Max SendQ exceeded)` eject; once dead, further frames are 175 /// dropped — bounding the mailbox under a non-draining peer. 176 fn enqueue(&self, env: Envelope) { 177 match self.stats.sendq_admit(env.wire_len(), self.sendq_max) { 178 SendqVerdict::Send => { 179 let _ = self.mailbox.send(env); 180 } 181 SendqVerdict::Overflow => { 182 let wire = format!( 183 "ERROR :Closing Link: {} (Max SendQ exceeded)\r\n", 184 self.record.host 185 ) 186 .into_bytes(); 187 self.stats.sendq_force_add(wire.len() as u64); 188 let _ = self.mailbox.send(Envelope::Eject { 189 wire, 190 reason: "Max SendQ exceeded".to_string(), 191 }); 192 } 193 SendqVerdict::Dropped => {} 194 } 195 } 196 197 /// Push a control-plane frame (a KILL eject or a collision SAVE) that **bypasses** the 198 /// SendQ ceiling — always delivered, always accounted (P11 slice 147). 199 fn enqueue_control(&self, env: Envelope) { 200 self.stats.sendq_force_add(env.wire_len()); 201 let _ = self.mailbox.send(env); 202 } 203} 204 205/// One enumerated link for `STATS l` / `211 RPL_STATSLINKINFO`: a registered local 206/// client's display nick plus a snapshot of its traffic counters. Produced by 207/// [`Registry::link_info`]. 208#[derive(Debug, Clone, PartialEq, Eq)] 209pub struct LinkInfo { 210 pub nick: String, 211 pub stats: ConnStatsSnapshot, 212} 213 214/// One enumerated connection for `STATS f` / `211` — the richer per-connection view backing 215/// the oracle's `report_fd` fd/socket table (P11 slice 154). A tokio daemon has no `select()` 216/// fd array, so the connection's [`Uid`] stands in as the stable per-connection handle (rendered 217/// where the oracle prints a raw fd); `last_activity` drives the idle field. Produced by 218/// [`Registry::connections`]. 219#[derive(Debug, Clone, PartialEq, Eq)] 220pub struct ConnInfo { 221 /// The connection's UID, rendered (the leveva analogue of the oracle's raw fd). 222 pub uid: String, 223 pub nick: String, 224 pub user: String, 225 pub host: String, 226 /// Last-activity stamp (unix-nanos) — the same field WHOIS `317` idle reads. 227 pub last_activity: u64, 228} 229 230/// Why a nick could not be claimed or renamed-to. 231#[derive(Debug, Clone, Copy, PartialEq, Eq)] 232pub enum ClaimError { 233 /// The (folded) nick is already held by another client → `433`. 234 InUse, 235} 236 237/// The process-wide client table. Cheap to lock — every operation holds the mutex 238/// only for a couple of map accesses and never across an `.await`, so a plain 239/// [`std::sync::Mutex`] is correct. 240#[derive(Debug, Default)] 241pub struct Registry { 242 inner: Mutex<Inner>, 243 /// In-flight (pre-registration) SASL exchanges, keyed by the client's early-assigned UID 244 /// (P11 slice 159). Lives here — rather than as a `ServerContext` field — because the 245 /// registry is already the per-UID connection directory, and an authenticating client is a 246 /// proto-connection whose verdict must reach it cross-task. See [`crate::sasl`]. 247 sasl: crate::sasl::SaslSessions, 248 /// IRCv3 metadata store (P11 slice 163): the key:value metadata attached to users/channels 249 /// plus each connection's key-subscription set. Lives here for the same reason as `sasl` — the 250 /// registry is the per-UID connection directory, and metadata is keyed on the same UIDs (and on 251 /// channel names). See [`crate::metadata`]. 252 metadata: crate::metadata::MetadataStore, 253 /// Caller-id (`+g`) accept lists (P11 slice 216): per `+g` owner, the senders it accepts plus 254 /// the senders it has already been notified about. Lives here for the same reason as `metadata` 255 /// — keyed on the same connection UIDs. See [`crate::callerid`]. 256 callerid: crate::callerid::CallerId, 257} 258 259#[derive(Debug, Default)] 260struct Inner { 261 /// Folded nick → owning UID (the uniqueness/lookup index). 262 by_nick: HashMap<String, Uid>, 263 /// UID → identity + mailbox (the delivery/identity store). 264 by_uid: HashMap<Uid, Entry>, 265} 266 267impl Registry { 268 pub fn new() -> Self { 269 Self::default() 270 } 271 272 /// The in-flight SASL session table (P11 slice 159) — the cross-task rendezvous between an 273 /// authenticating client's serve loop and the services peer's serve loop. See [`crate::sasl`]. 274 pub fn sasl(&self) -> &crate::sasl::SaslSessions { 275 &self.sasl 276 } 277 278 /// The IRCv3 metadata store (P11 slice 163) — the per-user/per-channel key:value metadata and 279 /// each connection's key subscriptions. See [`crate::metadata`]. 280 pub fn metadata(&self) -> &crate::metadata::MetadataStore { 281 &self.metadata 282 } 283 284 /// The caller-id (`+g`) accept-list store (P11 slice 216) — the per-owner accept set and the 285 /// once-per-pair `717`/`718` notify gate. See [`crate::callerid`]. 286 pub fn callerid(&self) -> &crate::callerid::CallerId { 287 &self.callerid 288 } 289 290 /// The user-mode bitmask currently mirrored for `uid` (`0` if the client is unknown — an 291 /// unknown target simply has no modes). Used by the caller-id (`+g`) delivery gate; mirrors 292 /// [`caps_of`](Self::caps_of). 293 pub fn modes_of(&self, uid: &Uid) -> u32 { 294 self.inner 295 .lock() 296 .expect("registry mutex poisoned") 297 .by_uid 298 .get(uid) 299 .map_or(0, |e| e.record.modes) 300 } 301 302 /// Claim `nick` for a newly-registered client identified by `uid`, recording the 303 /// `mailbox` that reaches its connection. `Err(InUse)` if the folded nick is 304 /// already held (the caller emits `433` and leaves the client unregistered; the 305 /// `uid` it generated simply goes unused). 306 /// 307 /// This convenience variant constructs a **fresh** (all-zero) [`ConnStats`] for the 308 /// link — used by every caller that doesn't carry one (the test paths, NICK-driven 309 /// reclaims, S2S-introduced locals). The live registration finalize path uses 310 /// [`Registry::try_claim_with_stats`] instead so the connection's serve loop and the 311 /// registry share the *same* counters. 312 pub fn try_claim( 313 &self, 314 uid: &Uid, 315 nick: &str, 316 user: &str, 317 host: &str, 318 realname: &str, 319 mailbox: Mailbox, 320 ) -> Result<(), ClaimError> { 321 let stats = Arc::new(ConnStats::new(crate::clock::unixnano())); 322 // `sendq_max = 0`: a `try_claim` link (remote user / S2S-introduced local / test) has no 323 // serve loop draining its mailbox, so SendQ is accounted but never used to kill it. 324 self.try_claim_with_stats(uid, nick, user, host, realname, mailbox, stats, 0) 325 } 326 327 /// Like [`Registry::try_claim`], but the caller supplies the link's [`ConnStats`] — the 328 /// `Arc` its serve loop already holds and increments at every socket read/write — so 329 /// `STATS l` reports live traffic for the connection (P11 slice 145) — plus its `sendq_max` 330 /// ceiling (P11 slice 147; `0` = account-only, never kill). 331 #[allow(clippy::too_many_arguments)] 332 pub fn try_claim_with_stats( 333 &self, 334 uid: &Uid, 335 nick: &str, 336 user: &str, 337 host: &str, 338 realname: &str, 339 mailbox: Mailbox, 340 stats: Arc<ConnStats>, 341 sendq_max: u64, 342 ) -> Result<(), ClaimError> { 343 let key = casemap::fold(nick); 344 let mut g = self.inner.lock().expect("registry mutex poisoned"); 345 if g.by_nick.contains_key(&key) { 346 return Err(ClaimError::InUse); 347 } 348 g.by_nick.insert(key, uid.clone()); 349 // Seed both signon and last_activity to the same claim-time stamp: a freshly 350 // registered client that has sent no PRIVMSG reports idle ≈ 0. (The unit-test path 351 // calls `try_claim` directly and never runs `finalize_registration`, so the seed 352 // must live here.) 353 let now = crate::clock::unixnano(); 354 g.by_uid.insert( 355 uid.clone(), 356 Entry { 357 record: ClientRecord { 358 uid: uid.clone(), 359 nick: nick.to_string(), 360 user: user.to_string(), 361 host: host.to_string(), 362 orighost: host.to_string(), 363 realname: realname.to_string(), 364 modes: 0, 365 snomask: 0, 366 away: None, 367 caps: crate::cap::ClientCaps::default(), 368 certfp: None, 369 account: None, 370 class: None, 371 kline_exempt: false, 372 xline_exempt: false, 373 signon: now, 374 last_activity: now, 375 }, 376 mailbox, 377 stats, 378 sendq_max, 379 }, 380 ); 381 Ok(()) 382 } 383 384 /// Snapshot every registered local client as a [`LinkInfo`] for `STATS l` / 385 /// `211 RPL_STATSLINKINFO`. Order is unspecified (a `HashMap` walk); the handler does 386 /// not promise a stable link order. Holds the lock only for the snapshot. 387 pub fn link_info(&self) -> Vec<LinkInfo> { 388 let g = self.inner.lock().expect("registry mutex poisoned"); 389 g.by_uid 390 .values() 391 .map(|e| LinkInfo { 392 nick: e.record.nick.clone(), 393 stats: e.stats.snapshot(), 394 }) 395 .collect() 396 } 397 398 /// Snapshot every registered connection as a [`ConnInfo`] for `STATS f` / `211` — the 399 /// oracle's `report_fd` fd/connection table (P11 slice 154). Order is unspecified (a 400 /// `HashMap` walk); the handler does not promise a stable order. Holds the lock only for 401 /// the snapshot. 402 pub fn connections(&self) -> Vec<ConnInfo> { 403 let g = self.inner.lock().expect("registry mutex poisoned"); 404 g.by_uid 405 .values() 406 .map(|e| ConnInfo { 407 uid: e.record.uid.to_string(), 408 nick: e.record.nick.clone(), 409 user: e.record.user.clone(), 410 host: e.record.host.clone(), 411 last_activity: e.record.last_activity, 412 }) 413 .collect() 414 } 415 416 /// Network-wide connection counts sharing a connect host, for the global per-host caps 417 /// (P11 slice 150). Returns `(per_host, per_user_host)`: the number of registered users — 418 /// **local and S2S-mirrored** (remote users are `try_claim`ed into this same registry by the 419 /// burst, so the count is the true network total) — whose `orighost` equals `host`, and the 420 /// subset of those whose `user` also equals `user`. Compares `orighost` (the real connect 421 /// host, left intact by `+x` cloaking) so the basis matches the local per-host cap's pre-cloak 422 /// host. One lock acquisition. Host/user compared by exact string equality, like the local 423 /// per-host cap's `HashMap` keys. 424 pub fn global_host_counts(&self, user: &str, host: &str) -> (u32, u32) { 425 let g = self.inner.lock().expect("registry mutex poisoned"); 426 let mut per_host = 0u32; 427 let mut per_user_host = 0u32; 428 for e in g.by_uid.values() { 429 if e.record.orighost == host { 430 per_host += 1; 431 if e.record.user == user { 432 per_user_host += 1; 433 } 434 } 435 } 436 (per_host, per_user_host) 437 } 438 439 /// Rename the client `uid` to display nick `new`. A **case-only** change 440 /// (folded-equal old/new, e.g. `alice`→`Alice`) always succeeds and just updates 441 /// the stored display nick. Otherwise `Err(InUse)` if another client holds the new 442 /// folded nick. Atomic: the freeing of the old fold-key and the taking of the new 443 /// one happen under one lock. The UID — and therefore channel membership and any 444 /// in-flight delivery target — is untouched. 445 /// 446 /// A no-op success if `uid` is unknown (an invariant violation for a registered 447 /// client): there is no slot to move. 448 pub fn try_rename(&self, uid: &Uid, new: &str) -> Result<(), ClaimError> { 449 let new_key = casemap::fold(new); 450 let mut g = self.inner.lock().expect("registry mutex poisoned"); 451 let Some(old_nick) = g.by_uid.get(uid).map(|e| e.record.nick.clone()) else { 452 return Ok(()); 453 }; 454 let old_key = casemap::fold(&old_nick); 455 if new_key != old_key && g.by_nick.contains_key(&new_key) { 456 return Err(ClaimError::InUse); 457 } 458 g.by_nick.remove(&old_key); 459 g.by_nick.insert(new_key, uid.clone()); 460 if let Some(e) = g.by_uid.get_mut(uid) { 461 e.record.nick = new.to_string(); 462 } 463 Ok(()) 464 } 465 466 /// Deliver pre-encoded wire `bytes` to the connection owning `uid`. Returns `false` 467 /// if no such client is registered. A closed mailbox (recipient task gone but the 468 /// registry not yet released) counts as delivered — the record is about to be freed. 469 /// This is the channel-fan-out / co-member-relay primitive. 470 pub fn deliver_uid(&self, uid: &Uid, bytes: Vec<u8>) -> bool { 471 let g = self.inner.lock().expect("registry mutex poisoned"); 472 match g.by_uid.get(uid) { 473 Some(entry) => { 474 entry.enqueue(Envelope::Wire(bytes)); 475 true 476 } 477 None => false, 478 } 479 } 480 481 /// Batched fan-out (the performance path for [`crate::command::message`]'s channel 482 /// delivery): deliver a wire line to each UID in `members` under a **single** lock 483 /// acquisition, replacing the per-member `modes_of` + `caps_of` + `deliver_uid` trio (three 484 /// independent lock cycles each) with one pass over the table. 485 /// 486 /// For every member still registered, `wire_for` is called with that member's mirrored 487 /// user-modes and negotiated caps — snapshotted from the record while the lock is held — and 488 /// decides delivery: `None` skips the member (e.g. a `+D` deaf receiver), `Some(bytes)` 489 /// enqueues `bytes` on its mailbox with the usual SendQ accounting (identical to 490 /// [`deliver_uid`](Self::deliver_uid)). The closure is expected to **memoize** its rendering 491 /// by capability-combination so the wire is serialized once per distinct combo rather than 492 /// once per member; it must not call back into the registry (the lock is held across the whole 493 /// pass). A member not in the table is silently skipped, exactly as `deliver_uid` returns 494 /// `false` for an unknown UID. Members are visited in slice order, so delivery order is 495 /// preserved. 496 pub fn deliver_batch<F>(&self, members: &[Uid], mut wire_for: F) 497 where 498 F: FnMut(u32, crate::cap::ClientCaps) -> Option<Vec<u8>>, 499 { 500 let g = self.inner.lock().expect("registry mutex poisoned"); 501 for uid in members { 502 if let Some(entry) = g.by_uid.get(uid) { 503 if let Some(bytes) = wire_for(entry.record.modes, entry.record.caps) { 504 entry.enqueue(Envelope::Wire(bytes)); 505 } 506 } 507 } 508 } 509 510 /// Forcibly disconnect the connection owning `uid` (a `KILL`): write `wire` to it, 511 /// then close it, relaying `QUIT :reason` to its channel co-members. Returns `false` 512 /// if no such client is registered. Delivered in-band on the mailbox so it lands 513 /// after any wire frames already queued for that connection. 514 pub fn eject_uid(&self, uid: &Uid, wire: Vec<u8>, reason: String) -> bool { 515 let g = self.inner.lock().expect("registry mutex poisoned"); 516 match g.by_uid.get(uid) { 517 Some(entry) => { 518 entry.enqueue_control(Envelope::Eject { wire, reason }); 519 true 520 } 521 None => false, 522 } 523 } 524 525 /// Deliver pre-encoded wire `bytes` to the live connection holding the nick 526 /// `target` (matched by fold). Returns `false` when **no client holds that folded 527 /// nick** (the caller emits `401`). The `PRIVMSG`/`NOTICE`-to-a-nick primitive. 528 pub fn deliver_nick(&self, target: &str, bytes: Vec<u8>) -> bool { 529 let g = self.inner.lock().expect("registry mutex poisoned"); 530 let Some(uid) = g.by_nick.get(&casemap::fold(target)) else { 531 return false; 532 }; 533 match g.by_uid.get(uid) { 534 Some(entry) => { 535 entry.enqueue(Envelope::Wire(bytes)); 536 true 537 } 538 None => false, 539 } 540 } 541 542 /// Release a client on disconnect, by UID. A no-op if it is not registered 543 /// (idempotent). Frees both the UID slot and its current fold-key. 544 pub fn release(&self, uid: &Uid) { 545 let mut g = self.inner.lock().expect("registry mutex poisoned"); 546 if let Some(entry) = g.by_uid.remove(uid) { 547 let key = casemap::fold(&entry.record.nick); 548 // Only drop the fold-key if it still points at this UID (it always should, 549 // but guard against a stale entry rather than evicting a new claimant). 550 if g.by_nick.get(&key) == Some(uid) { 551 g.by_nick.remove(&key); 552 } 553 } 554 drop(g); 555 // Drop this connection's caller-id (`+g`) accept list + notify state (slice 216) so the 556 // store never leaks. (Senders it appeared on *other* owners' lists are filtered on read.) 557 self.callerid.forget(uid); 558 // Drop this connection's METADATA rate-limit clock (slice 228) — keep the stored 559 // values/subscriptions (cleaned by their own paths), only the transient budget. 560 self.metadata.forget_rate(uid); 561 } 562 563 /// The UID currently holding the folded `nick`, if any. Resolves a nick target to 564 /// its owner (`PRIVMSG bob`, a `433` pre-check). 565 pub fn uid_of(&self, nick: &str) -> Option<Uid> { 566 self.inner 567 .lock() 568 .expect("registry mutex poisoned") 569 .by_nick 570 .get(&casemap::fold(nick)) 571 .cloned() 572 } 573 574 /// The current display nick of `uid` (for resolving channel members → NAMES). 575 pub fn nick_of(&self, uid: &Uid) -> Option<String> { 576 self.inner 577 .lock() 578 .expect("registry mutex poisoned") 579 .by_uid 580 .get(uid) 581 .map(|e| e.record.nick.clone()) 582 } 583 584 /// A snapshot of the stored record for `uid`. 585 pub fn record_of(&self, uid: &Uid) -> Option<ClientRecord> { 586 self.inner 587 .lock() 588 .expect("registry mutex poisoned") 589 .by_uid 590 .get(uid) 591 .map(|e| e.record.clone()) 592 } 593 594 /// Read the stored signon (the nanosecond claim/registration timestamp) of `uid` — the 595 /// **user timestamp** the nick-collision arbitration compares (P11 slice 133). `None` if 596 /// `uid` is unknown. 597 pub fn signon_of(&self, uid: &Uid) -> Option<u64> { 598 self.inner 599 .lock() 600 .expect("registry mutex poisoned") 601 .by_uid 602 .get(uid) 603 .map(|e| e.record.signon) 604 } 605 606 /// Overwrite the stored signon (user timestamp) of `uid`. Used when a remote user's real 607 /// registration timestamp arrives over `USERTS`, so a later nick-collision compares 608 /// against the true value rather than the moment we learned of the user. No-op if unknown. 609 pub fn set_signon(&self, uid: &Uid, ts: u64) { 610 if let Some(e) = self 611 .inner 612 .lock() 613 .expect("registry mutex poisoned") 614 .by_uid 615 .get_mut(uid) 616 { 617 e.record.signon = ts; 618 } 619 } 620 621 /// Deliver an [`Envelope::ForceNick`] to the connection owning `uid`: the local client lost 622 /// a nick-collision `SAVE` on timestamp (P11 slice 133), so write the `:<oldmask> NICK 623 /// <new>` line and update its session nick. Returns `false` if no such client is registered 624 /// (e.g. the loser was a remote user — handled by a `SAVE` to its uplink instead). 625 pub fn force_nick_uid(&self, uid: &Uid, wire: Vec<u8>, new: String) -> bool { 626 let g = self.inner.lock().expect("registry mutex poisoned"); 627 match g.by_uid.get(uid) { 628 Some(entry) => { 629 entry.enqueue_control(Envelope::ForceNick { wire, new }); 630 true 631 } 632 None => false, 633 } 634 } 635 636 /// Mirror a client's user-mode bitmask into its stored record. The connection 637 /// owns the authoritative copy; this keeps the WHO-visible snapshot in step after 638 /// a `MODE <nick>` change. A no-op if `uid` is unknown. 639 pub fn set_modes(&self, uid: &Uid, modes: u32) { 640 if let Some(e) = self 641 .inner 642 .lock() 643 .expect("registry mutex poisoned") 644 .by_uid 645 .get_mut(uid) 646 { 647 e.record.modes = modes; 648 } 649 } 650 651 /// Set a client's server-notice [`snomask`](ClientRecord::snomask) — the category bitset 652 /// the `+s` fan consults (P11 slice 257). Mirrors [`set_modes`](Self::set_modes); a no-op 653 /// if `uid` is unknown. 654 pub fn set_snomask(&self, uid: &Uid, snomask: u32) { 655 if let Some(e) = self 656 .inner 657 .lock() 658 .expect("registry mutex poisoned") 659 .by_uid 660 .get_mut(uid) 661 { 662 e.record.snomask = snomask; 663 } 664 } 665 666 /// The snomask currently stored for `uid` (`0` if unknown — an unknown client subscribes 667 /// to nothing). Read by [`crate::command::mode`] to compute a `+s` delta. 668 pub fn snomask_of(&self, uid: &Uid) -> u32 { 669 self.inner 670 .lock() 671 .expect("registry mutex poisoned") 672 .by_uid 673 .get(uid) 674 .map(|e| e.record.snomask) 675 .unwrap_or(0) 676 } 677 678 /// Mirror a client's negotiated IRCv3 capabilities into its stored record so the 679 /// cross-connection delivery plane can gate client-only message tags / `TAGMSG` per 680 /// recipient. Called at registration and on every post-registration `CAP REQ`/`CLEAR`. 681 /// A no-op if `uid` is unknown. 682 pub fn set_caps(&self, uid: &Uid, caps: crate::cap::ClientCaps) { 683 if let Some(e) = self 684 .inner 685 .lock() 686 .expect("registry mutex poisoned") 687 .by_uid 688 .get_mut(uid) 689 { 690 e.record.caps = caps; 691 } 692 } 693 694 /// The capabilities currently mirrored for `uid` (all-`false` default if the client is 695 /// unknown — an unknown recipient simply gets no tags). The delivery plane's gate. 696 pub fn caps_of(&self, uid: &Uid) -> crate::cap::ClientCaps { 697 self.inner 698 .lock() 699 .expect("registry mutex poisoned") 700 .by_uid 701 .get(uid) 702 .map(|e| e.record.caps) 703 .unwrap_or_default() 704 } 705 706 /// Set or clear a client's away message, keeping the snapshot's `+a` 707 /// ([`crate::UserMode::Away`]) bit in lockstep: `Some` sets both the message and the 708 /// bit, `None` clears both. The away bit is server-managed (only `AWAY` touches it), so 709 /// unlike the rest of `modes` it is owned here rather than mirrored from the connection. 710 /// A no-op if `uid` is unknown. 711 pub fn set_away(&self, uid: &Uid, away: Option<String>) { 712 if let Some(e) = self 713 .inner 714 .lock() 715 .expect("registry mutex poisoned") 716 .by_uid 717 .get_mut(uid) 718 { 719 let bit = crate::UserMode::Away.bit(); 720 if away.is_some() { 721 e.record.modes |= bit; 722 } else { 723 e.record.modes &= !bit; 724 } 725 e.record.away = away; 726 } 727 } 728 729 /// Advance a client's last-activity stamp to `now` (unix nanoseconds) — the WHOIS idle 730 /// clock. Called from `dispatch` when this client *sends* a `PRIVMSG` (and only then, 731 /// mirroring the oracle's `IDLE_FROM_MSG`); never moves [`ClientRecord::signon`]. A 732 /// silent no-op if `uid` is unknown. 733 pub fn bump_activity(&self, uid: &Uid, now: u64) { 734 if let Some(e) = self 735 .inner 736 .lock() 737 .expect("registry mutex poisoned") 738 .by_uid 739 .get_mut(uid) 740 { 741 e.record.last_activity = now; 742 } 743 } 744 745 /// Mirror a client's `user`+`host` into its stored record after an IRCv3 746 /// [`CHGHOST`](crate::command::chghost) (the operator self-CHGHOST locally, or an inbound 747 /// `ENCAP CHGHOST` for a remote user). Keeps the WHO/WHOIS/`userhost-in-names`-visible 748 /// mask in step. The folded-nick index is unaffected (the nick does not change). A no-op 749 /// if `uid` is unknown. 750 pub fn set_host(&self, uid: &Uid, user: &str, host: &str) { 751 if let Some(e) = self 752 .inner 753 .lock() 754 .expect("registry mutex poisoned") 755 .by_uid 756 .get_mut(uid) 757 { 758 e.record.user = user.to_string(); 759 e.record.host = host.to_string(); 760 } 761 } 762 763 /// Update a client's stored realname (GECOS) — the IRCv3 `setname` mutation (slice 90). 764 /// Called by the [`SETNAME`](crate::command::setname) command for a local client and by 765 /// inbound `ENCAP SETNAME` for a remote one. Leaves nick/user/host/modes intact. A no-op if 766 /// `uid` is unknown. Read back by WHOIS (`311`). 767 pub fn set_realname(&self, uid: &Uid, realname: &str) { 768 if let Some(e) = self 769 .inner 770 .lock() 771 .expect("registry mutex poisoned") 772 .by_uid 773 .get_mut(uid) 774 { 775 e.record.realname = realname.to_string(); 776 } 777 } 778 779 /// Mirror a client's TLS certificate fingerprint (CertFP, P11 slice 78) into its stored 780 /// record. Called at registration for a local client (from the value [`crate::certfp`] 781 /// computed at the TLS handshake) and by inbound `ENCAP CERTFP` for a remote one. `None` 782 /// clears it. Read back by WHOIS (`276`) and the `operator` certfp gate. A no-op if 783 /// `uid` is unknown. 784 pub fn set_certfp(&self, uid: &Uid, certfp: Option<&str>) { 785 if let Some(e) = self 786 .inner 787 .lock() 788 .expect("registry mutex poisoned") 789 .by_uid 790 .get_mut(uid) 791 { 792 e.record.certfp = certfp.map(str::to_string); 793 } 794 } 795 796 /// Set or clear a client's services account (P11 slice 139). `Some(name)` logs the client in 797 /// to `name`; `None` logs it out. The value is authored entirely by a remote server 798 /// (services) via `ENCAP * SU` ([`crate::s2s::su`]) — leveva has no account database. Read 799 /// back by WHOIS (`330`) and the WHO/WHOX `%a` field. A no-op if `uid` is unknown. 800 pub fn set_account(&self, uid: &Uid, account: Option<&str>) { 801 if let Some(e) = self 802 .inner 803 .lock() 804 .expect("registry mutex poisoned") 805 .by_uid 806 .get_mut(uid) 807 { 808 e.record.account = account.map(str::to_string); 809 } 810 } 811 812 /// Record the matched `allow → class` connection-class name for `uid` (P11 slice 187), 813 /// or `None` to clear it. Called once from 814 /// [`finalize_registration`](crate::session::Session::finalize_registration) after the nick 815 /// is claimed; read by `TRACE`. A no-op if `uid` is not registered. 816 pub fn set_class(&self, uid: &Uid, class: Option<String>) { 817 if let Some(e) = self 818 .inner 819 .lock() 820 .expect("registry mutex poisoned") 821 .by_uid 822 .get_mut(uid) 823 { 824 e.record.class = class; 825 } 826 } 827 828 /// Record whether `uid`'s matched I-line was `kline-exempt` (the C `CFLAG_KEXEMPT`). Called 829 /// once from [`finalize_registration`](crate::session::Session::finalize_registration) after 830 /// the nick is claimed; read by the `TKLINE` runtime reap so an exempt client survives an 831 /// operator's later matching ban. A no-op if `uid` is not registered. 832 pub fn set_kline_exempt(&self, uid: &Uid, exempt: bool) { 833 if let Some(e) = self 834 .inner 835 .lock() 836 .expect("registry mutex poisoned") 837 .by_uid 838 .get_mut(uid) 839 { 840 e.record.kline_exempt = exempt; 841 } 842 } 843 844 /// Record whether `uid`'s matched I-line was `xline-exempt` (the C `CFLAG_XEXEMPT`, P11 slice 845 /// 289). Called once from [`finalize_registration`](crate::session::Session::finalize_registration) 846 /// after the nick is claimed; read by the `XLINE` runtime reap so an exempt client survives an 847 /// operator's later matching realname ban. A no-op if `uid` is not registered. 848 pub fn set_xline_exempt(&self, uid: &Uid, exempt: bool) { 849 if let Some(e) = self 850 .inner 851 .lock() 852 .expect("registry mutex poisoned") 853 .by_uid 854 .get_mut(uid) 855 { 856 e.record.xline_exempt = exempt; 857 } 858 } 859 860 /// The original connect host of `uid` (the IP-cloak source), or `None` if unknown. 861 /// Unlike [`Self::set_host`]-mutated `host`, this never changes after `try_claim`. 862 pub fn orighost(&self, uid: &Uid) -> Option<String> { 863 self.inner 864 .lock() 865 .expect("registry mutex poisoned") 866 .by_uid 867 .get(uid) 868 .map(|e| e.record.orighost.clone()) 869 } 870 871 /// Whether the folded `nick` is currently held. 872 pub fn contains(&self, nick: &str) -> bool { 873 self.inner 874 .lock() 875 .expect("registry mutex poisoned") 876 .by_nick 877 .contains_key(&casemap::fold(nick)) 878 } 879 880 /// A snapshot of the stored record for `nick` (the seed for WHO/WHOIS). 881 pub fn get(&self, nick: &str) -> Option<ClientRecord> { 882 let g = self.inner.lock().expect("registry mutex poisoned"); 883 let uid = g.by_nick.get(&casemap::fold(nick))?; 884 g.by_uid.get(uid).map(|e| e.record.clone()) 885 } 886 887 /// A snapshot of every registered client record, sorted by folded nick for 888 /// deterministic ordering. The seed for a `WHO` mask scan / no-argument `WHO`. 889 pub fn records(&self) -> Vec<ClientRecord> { 890 let g = self.inner.lock().expect("registry mutex poisoned"); 891 let mut out: Vec<ClientRecord> = g.by_uid.values().map(|e| e.record.clone()).collect(); 892 out.sort_by_key(|r| casemap::fold(&r.nick)); 893 out 894 } 895 896 /// Number of registered clients currently in the table. 897 pub fn len(&self) -> usize { 898 self.inner 899 .lock() 900 .expect("registry mutex poisoned") 901 .by_uid 902 .len() 903 } 904 905 pub fn is_empty(&self) -> bool { 906 self.len() == 0 907 } 908} 909 910#[cfg(test)] 911mod tests { 912 use super::*; 913 use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; 914 915 fn uid(s: &str) -> Uid { 916 Uid::try_from(s).unwrap() 917 } 918 919 /// A throwaway mailbox whose receiver is dropped — for the uniqueness tests that 920 /// never deliver. 921 fn mb() -> Mailbox { 922 unbounded_channel().0 923 } 924 925 /// A mailbox plus the receiver to observe what was delivered to it. 926 fn mailbox() -> (Mailbox, UnboundedReceiver<Envelope>) { 927 unbounded_channel() 928 } 929 930 /// Unwrap the next delivered frame as wire bytes (panics on a control frame). 931 fn wire(rx: &mut UnboundedReceiver<Envelope>) -> Vec<u8> { 932 match rx.try_recv().expect("a frame was delivered") { 933 Envelope::Wire(b) => b, 934 other => panic!("expected wire bytes, got {other:?}"), 935 } 936 } 937 938 #[test] 939 fn global_host_counts_over_the_whole_registry() { 940 let r = Registry::new(); 941 // Empty registry → zero on any query. 942 assert_eq!(r.global_host_counts("u", "1.2.3.4"), (0, 0)); 943 944 // Two users (modelling a local + an S2S-mirrored remote) share host 1.2.3.4, same user. 945 r.try_claim(&uid("0ABCAAAAA"), "alice", "bob", "1.2.3.4", "A", mb()) 946 .unwrap(); 947 r.try_claim(&uid("0ABCAAAAB"), "alice2", "bob", "1.2.3.4", "A", mb()) 948 .unwrap(); 949 // A third on the same host but a *different* user. 950 r.try_claim(&uid("0ABCAAAAC"), "carol", "eve", "1.2.3.4", "C", mb()) 951 .unwrap(); 952 // A fourth on a *different* host entirely. 953 r.try_claim(&uid("0ABCAAAAD"), "dave", "bob", "5.6.7.8", "D", mb()) 954 .unwrap(); 955 956 // per_host counts all three on 1.2.3.4; per_user_host only the two bob@1.2.3.4. 957 assert_eq!(r.global_host_counts("bob", "1.2.3.4"), (3, 2)); 958 // eve@1.2.3.4: one host-mate set of 3, one matching user. 959 assert_eq!(r.global_host_counts("eve", "1.2.3.4"), (3, 1)); 960 // The other host is independent. 961 assert_eq!(r.global_host_counts("bob", "5.6.7.8"), (1, 1)); 962 // A host nobody is on. 963 assert_eq!(r.global_host_counts("bob", "9.9.9.9"), (0, 0)); 964 965 // INVERSE: releasing one bob@1.2.3.4 drops both counts by exactly one. 966 r.release(&uid("0ABCAAAAA")); 967 assert_eq!(r.global_host_counts("bob", "1.2.3.4"), (2, 1)); 968 } 969 970 #[test] 971 fn claim_then_reclaim_is_in_use_until_released() { 972 let r = Registry::new(); 973 let ua = uid("0ABCAAAAA"); 974 assert!(r.try_claim(&ua, "alice", "u", "h", "real", mb()).is_ok()); 975 assert!(r.contains("alice")); 976 assert_eq!(r.len(), 1); 977 assert_eq!(r.uid_of("alice"), Some(ua.clone())); 978 // A second claim of the same nick (even a different UID) is refused. 979 assert_eq!( 980 r.try_claim(&uid("0ABCAAAAB"), "alice", "u2", "h2", "real", mb()), 981 Err(ClaimError::InUse) 982 ); 983 // Inverse: release frees the slot and it can be reclaimed. 984 r.release(&ua); 985 assert!(!r.contains("alice")); 986 assert_eq!(r.len(), 0); 987 let uc = uid("0ABCAAAAC"); 988 assert!(r.try_claim(&uc, "alice", "u3", "h3", "real", mb()).is_ok()); 989 assert_eq!(r.get("alice").unwrap().user, "u3"); 990 assert_eq!(r.get("alice").unwrap().uid, uc); 991 } 992 993 #[test] 994 fn folding_collapses_case_and_scandinavian_pairs() { 995 let r = Registry::new(); 996 assert!(r 997 .try_claim(&uid("0ABCAAAAA"), "Alice[]", "u", "h", "real", mb()) 998 .is_ok()); 999 for (i, variant) in ["alice[]", "ALICE[]", "Alice{}", "alice{}"] 1000 .iter() 1001 .enumerate() 1002 { 1003 let u = uid(&format!("0ABCAAAA{}", (b'A' + i as u8) as char)); 1004 assert_eq!( 1005 r.try_claim(&u, variant, "u", "h", "real", mb()), 1006 Err(ClaimError::InUse), 1007 "{variant} must collide with Alice[]" 1008 ); 1009 } 1010 assert_eq!(r.len(), 1); 1011 } 1012 1013 #[test] 1014 fn case_only_rename_keeps_one_slot_and_updates_display() { 1015 let r = Registry::new(); 1016 let ua = uid("0ABCAAAAA"); 1017 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1018 assert!(r.try_rename(&ua, "Alice").is_ok()); 1019 assert_eq!(r.len(), 1, "case-only rename must not duplicate the slot"); 1020 assert_eq!(r.nick_of(&ua).unwrap(), "Alice"); 1021 assert!(r.contains("ALICE")); 1022 // The UID is unchanged by a rename. 1023 assert_eq!(r.uid_of("alice"), Some(ua)); 1024 } 1025 1026 #[test] 1027 fn rename_to_taken_nick_is_refused_and_changes_nothing() { 1028 let r = Registry::new(); 1029 let ua = uid("0ABCAAAAA"); 1030 let ub = uid("0ABCAAAAB"); 1031 r.try_claim(&ua, "alice", "ua", "ha", "real", mb()).unwrap(); 1032 r.try_claim(&ub, "bob", "ub", "hb", "real", mb()).unwrap(); 1033 assert_eq!(r.try_rename(&ua, "bob"), Err(ClaimError::InUse)); 1034 // Inverse: neither record moved. 1035 assert_eq!(r.nick_of(&ua).unwrap(), "alice"); 1036 assert_eq!(r.get("bob").unwrap().user, "ub"); 1037 assert_eq!(r.len(), 2); 1038 } 1039 1040 #[test] 1041 fn rename_to_free_nick_keeps_the_uid_and_moves_the_nick() { 1042 let r = Registry::new(); 1043 let ua = uid("0ABCAAAAA"); 1044 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1045 assert!(r.try_rename(&ua, "alicia").is_ok()); 1046 assert!(!r.contains("alice"), "old nick freed"); 1047 assert_eq!(r.get("alicia").unwrap().user, "u", "record carried over"); 1048 assert_eq!(r.uid_of("alicia"), Some(ua), "UID stable across rename"); 1049 assert_eq!(r.len(), 1); 1050 } 1051 1052 #[test] 1053 fn rename_keeps_the_mailbox_reachable_by_uid_and_new_nick() { 1054 let r = Registry::new(); 1055 let ua = uid("0ABCAAAAA"); 1056 let (tx, mut rx) = mailbox(); 1057 r.try_claim(&ua, "alice", "u", "h", "real", tx).unwrap(); 1058 r.try_rename(&ua, "alicia").unwrap(); 1059 // Both the UID and the new nick reach the same connection. 1060 assert!(r.deliver_uid(&ua, b"u\r\n".to_vec())); 1061 assert_eq!(wire(&mut rx), b"u\r\n"); 1062 assert!(r.deliver_nick("alicia", b"hi\r\n".to_vec())); 1063 assert_eq!(wire(&mut rx), b"hi\r\n"); 1064 // The old nick no longer delivers anywhere. 1065 assert!(!r.deliver_nick("alice", b"x\r\n".to_vec())); 1066 } 1067 1068 #[test] 1069 fn set_host_mirrors_user_and_host_leaving_identity_intact() { 1070 let r = Registry::new(); 1071 let ua = uid("0ABCAAAAA"); 1072 r.try_claim(&ua, "alice", "oldu", "old.host", "Real Name", mb()) 1073 .unwrap(); 1074 r.set_modes(&ua, crate::UserMode::Oper.bit()); 1075 r.set_host(&ua, "newu", "new.cloak"); 1076 let rec = r.record_of(&ua).unwrap(); 1077 assert_eq!(rec.user, "newu"); 1078 assert_eq!(rec.host, "new.cloak"); 1079 // Inverse: nothing else moved — nick, modes, realname, and the nick index hold. 1080 assert_eq!(rec.nick, "alice"); 1081 assert_eq!(rec.realname, "Real Name"); 1082 assert_eq!(rec.modes, crate::UserMode::Oper.bit()); 1083 assert_eq!(r.uid_of("alice"), Some(ua.clone()), "nick index unchanged"); 1084 // The original connect host is preserved (the cloak source) — `set_host` never 1085 // touches it, so `orighost()` still reports the claim-time host. 1086 assert_eq!(rec.orighost, "old.host"); 1087 assert_eq!(r.orighost(&ua).as_deref(), Some("old.host")); 1088 // An unknown UID is a silent no-op (never panics). 1089 r.set_host(&uid("0ABCZZZZZ"), "x", "y"); 1090 assert!(r.record_of(&uid("0ABCZZZZZ")).is_none()); 1091 } 1092 1093 #[test] 1094 fn caps_default_to_off_and_round_trip_through_set_caps() { 1095 let r = Registry::new(); 1096 let ua = uid("0ABCAAAAA"); 1097 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1098 // Default: every cap off. 1099 assert_eq!(r.caps_of(&ua), crate::cap::ClientCaps::default()); 1100 assert!(!r.caps_of(&ua).message_tags); 1101 // Mirror an enable; it reads back. 1102 r.set_caps( 1103 &ua, 1104 crate::cap::ClientCaps { 1105 message_tags: true, 1106 ..Default::default() 1107 }, 1108 ); 1109 assert!(r.caps_of(&ua).message_tags); 1110 // Inverse: mirroring the default clears it again. 1111 r.set_caps(&ua, crate::cap::ClientCaps::default()); 1112 assert!(!r.caps_of(&ua).message_tags); 1113 // An unknown UID reports the all-off default (never panics). 1114 assert_eq!( 1115 r.caps_of(&uid("0ABCZZZZZ")), 1116 crate::cap::ClientCaps::default() 1117 ); 1118 // set_caps on an unknown UID is a silent no-op. 1119 r.set_caps( 1120 &uid("0ABCZZZZZ"), 1121 crate::cap::ClientCaps { 1122 message_tags: true, 1123 ..Default::default() 1124 }, 1125 ); 1126 assert!(!r.caps_of(&uid("0ABCZZZZZ")).message_tags); 1127 } 1128 1129 #[test] 1130 fn link_info_reports_each_claimed_client_with_its_live_stats() { 1131 let r = Registry::new(); 1132 let ua = uid("0ABCAAAAA"); 1133 let stats = Arc::new(ConnStats::new(1_000)); 1134 r.try_claim_with_stats(&ua, "alice", "u", "h", "real", mb(), stats.clone(), 0) 1135 .unwrap(); 1136 r.try_claim(&uid("0ABCAAAAB"), "bob", "u", "h", "real", mb()) 1137 .unwrap(); 1138 // alice's serve loop accumulates traffic on the shared Arc. 1139 stats.record_sent(b"PRIVMSG bob :hi\r\n"); 1140 stats.record_recv(b"PRIVMSG bob :hi\r\nPONG :x\r\n"); 1141 1142 let mut links = r.link_info(); 1143 links.sort_by(|a, b| a.nick.cmp(&b.nick)); 1144 assert_eq!(links.len(), 2); 1145 assert_eq!(links[0].nick, "alice"); 1146 assert_eq!(links[0].stats.sent_messages, 1); 1147 assert_eq!(links[0].stats.sent_bytes, 17); 1148 assert_eq!(links[0].stats.recv_messages, 2); 1149 assert_eq!(links[0].stats.connected_at, 1_000); 1150 // bob's default (try_claim) stats are all zero. 1151 assert_eq!(links[1].nick, "bob"); 1152 assert_eq!(links[1].stats.sent_messages, 0); 1153 assert_eq!(links[1].stats.recv_bytes, 0); 1154 } 1155 1156 #[test] 1157 fn link_info_drops_a_released_client_and_a_reclaim_starts_at_zero() { 1158 // Inverse: a released link disappears from `link_info`, and reclaiming the freed 1159 // nick (default stats) reports zero counters — the old traffic does not linger. 1160 let r = Registry::new(); 1161 let ua = uid("0ABCAAAAA"); 1162 let stats = Arc::new(ConnStats::new(0)); 1163 r.try_claim_with_stats(&ua, "alice", "u", "h", "real", mb(), stats.clone(), 0) 1164 .unwrap(); 1165 stats.record_sent(b"x\r\n"); 1166 assert_eq!(r.link_info().len(), 1); 1167 1168 r.release(&ua); 1169 assert!(r.link_info().is_empty()); 1170 1171 let uc = uid("0ABCAAAAC"); 1172 r.try_claim(&uc, "alice", "u", "h", "real", mb()).unwrap(); 1173 let links = r.link_info(); 1174 assert_eq!(links.len(), 1); 1175 assert_eq!(links[0].nick, "alice"); 1176 assert_eq!( 1177 links[0].stats.sent_bytes, 0, 1178 "reclaim must not inherit old traffic" 1179 ); 1180 } 1181 1182 #[test] 1183 fn connections_snapshots_each_registered_client() { 1184 // The richer per-connection view that backs `STATS f` (the fd/connection table): 1185 // uid + identity + last_activity for every registered local connection. 1186 let r = Registry::new(); 1187 r.try_claim(&uid("0ABCAAAAA"), "alice", "al", "h.one", "real", mb()) 1188 .unwrap(); 1189 r.try_claim(&uid("0ABCAAAAB"), "bob", "bo", "h.two", "real", mb()) 1190 .unwrap(); 1191 let mut c = r.connections(); 1192 c.sort_by(|a, b| a.nick.cmp(&b.nick)); 1193 assert_eq!(c.len(), 2); 1194 assert_eq!(c[0].nick, "alice"); 1195 assert_eq!(c[0].uid, "0ABCAAAAA"); 1196 assert_eq!(c[0].user, "al"); 1197 assert_eq!(c[0].host, "h.one"); 1198 assert_eq!(c[1].nick, "bob"); 1199 assert_eq!(c[1].uid, "0ABCAAAAB"); 1200 // last_activity was seeded at claim time → a sane non-zero stamp. 1201 assert!(c[0].last_activity > 0); 1202 } 1203 1204 #[test] 1205 fn connections_drops_a_released_client() { 1206 // Inverse: a released connection disappears from the fd-table snapshot. 1207 let r = Registry::new(); 1208 let ua = uid("0ABCAAAAA"); 1209 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1210 assert_eq!(r.connections().len(), 1); 1211 r.release(&ua); 1212 assert!(r.connections().is_empty()); 1213 } 1214 1215 #[test] 1216 fn release_clears_mirrored_caps() { 1217 let r = Registry::new(); 1218 let ua = uid("0ABCAAAAA"); 1219 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1220 r.set_caps( 1221 &ua, 1222 crate::cap::ClientCaps { 1223 message_tags: true, 1224 ..Default::default() 1225 }, 1226 ); 1227 r.release(&ua); 1228 // The slot is gone → caps read back as the all-off default. 1229 assert!(!r.caps_of(&ua).message_tags); 1230 } 1231 1232 #[test] 1233 fn releasing_absent_uid_is_a_noop() { 1234 let r = Registry::new(); 1235 r.release(&uid("0ABCZZZZZ")); // must not panic 1236 assert_eq!(r.len(), 0); 1237 let ua = uid("0ABCAAAAA"); 1238 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1239 r.release(&uid("0ABCZZZZZ")); 1240 assert_eq!(r.len(), 1, "unrelated release leaves others intact"); 1241 } 1242 1243 #[test] 1244 fn set_account_logs_in_then_out() { 1245 let r = Registry::new(); 1246 let ua = uid("0ABCAAAAA"); 1247 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1248 // Fresh client: not logged in. 1249 assert_eq!(r.record_of(&ua).unwrap().account, None); 1250 // Login. 1251 r.set_account(&ua, Some("AliceAcct")); 1252 assert_eq!( 1253 r.record_of(&ua).unwrap().account.as_deref(), 1254 Some("AliceAcct") 1255 ); 1256 // Inverse: logout clears it. 1257 r.set_account(&ua, None); 1258 assert_eq!(r.record_of(&ua).unwrap().account, None); 1259 } 1260 1261 #[test] 1262 fn set_account_on_unknown_uid_is_a_noop() { 1263 let r = Registry::new(); 1264 r.set_account(&uid("0ABCZZZZZ"), Some("ghost")); // must not panic 1265 assert_eq!(r.len(), 0); 1266 } 1267 1268 #[test] 1269 fn set_class_assigns_then_clears() { 1270 let r = Registry::new(); 1271 let ua = uid("0ABCAAAAA"); 1272 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1273 // Inverse baseline: a fresh claim carries no matched class. 1274 assert_eq!(r.record_of(&ua).unwrap().class, None); 1275 // Assign the matched I-line class. 1276 r.set_class(&ua, Some("premium".to_string())); 1277 assert_eq!(r.record_of(&ua).unwrap().class.as_deref(), Some("premium")); 1278 // Overwrite with a different class. 1279 r.set_class(&ua, Some("staff".to_string())); 1280 assert_eq!(r.record_of(&ua).unwrap().class.as_deref(), Some("staff")); 1281 // Inverse: clearing it returns to None. 1282 r.set_class(&ua, None); 1283 assert_eq!(r.record_of(&ua).unwrap().class, None); 1284 } 1285 1286 #[test] 1287 fn set_class_on_unknown_uid_is_a_noop() { 1288 let r = Registry::new(); 1289 r.set_class(&uid("0ABCZZZZZ"), Some("ghost".to_string())); // must not panic 1290 assert_eq!(r.len(), 0); 1291 } 1292 1293 #[test] 1294 fn set_kline_exempt_toggles_then_unknown_uid_is_a_noop() { 1295 let r = Registry::new(); 1296 let ua = uid("0ABCAAAAA"); 1297 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1298 // Inverse baseline: a fresh claim is not exempt. 1299 assert!(!r.record_of(&ua).unwrap().kline_exempt); 1300 // Mark exempt, then clear it again. 1301 r.set_kline_exempt(&ua, true); 1302 assert!(r.record_of(&ua).unwrap().kline_exempt); 1303 r.set_kline_exempt(&ua, false); 1304 assert!(!r.record_of(&ua).unwrap().kline_exempt); 1305 // A bogus UID is a silent no-op (no panic, no spurious record). 1306 r.set_kline_exempt(&uid("0ABCZZZZZ"), true); 1307 assert_eq!(r.len(), 1); 1308 } 1309 1310 #[test] 1311 fn set_away_toggles_message_and_the_a_bit_together() { 1312 let r = Registry::new(); 1313 let ua = uid("0ABCAAAAA"); 1314 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1315 let away_bit = crate::UserMode::Away.bit(); 1316 // Fresh client: no message, no +a. 1317 let rec = r.record_of(&ua).unwrap(); 1318 assert_eq!(rec.away, None); 1319 assert_eq!(rec.modes & away_bit, 0); 1320 // Set away → message stored and +a set. 1321 r.set_away(&ua, Some("brb".into())); 1322 let rec = r.record_of(&ua).unwrap(); 1323 assert_eq!(rec.away.as_deref(), Some("brb")); 1324 assert_ne!(rec.modes & away_bit, 0, "+a set alongside the message"); 1325 // Inverse: clear away → message gone and +a cleared. 1326 r.set_away(&ua, None); 1327 let rec = r.record_of(&ua).unwrap(); 1328 assert_eq!(rec.away, None); 1329 assert_eq!(rec.modes & away_bit, 0, "+a cleared alongside the message"); 1330 } 1331 1332 #[test] 1333 fn set_away_preserves_other_modes() { 1334 let r = Registry::new(); 1335 let ua = uid("0ABCAAAAA"); 1336 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1337 let inv = crate::UserMode::Invisible.bit(); 1338 r.set_modes(&ua, inv); 1339 // Toggling away must leave +i untouched. 1340 r.set_away(&ua, Some("gone".into())); 1341 assert_ne!( 1342 r.record_of(&ua).unwrap().modes & inv, 1343 0, 1344 "+i kept when away set" 1345 ); 1346 r.set_away(&ua, None); 1347 assert_ne!( 1348 r.record_of(&ua).unwrap().modes & inv, 1349 0, 1350 "+i kept when away cleared" 1351 ); 1352 } 1353 1354 #[test] 1355 fn set_away_on_unknown_uid_is_a_noop() { 1356 let r = Registry::new(); 1357 r.set_away(&uid("0ABCZZZZZ"), Some("x".into())); // must not panic 1358 assert!(r.is_empty()); 1359 } 1360 1361 #[test] 1362 fn try_claim_seeds_signon_and_last_activity_together() { 1363 let r = Registry::new(); 1364 let ua = uid("0ABCAAAAA"); 1365 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1366 let rec = r.record_of(&ua).unwrap(); 1367 // Both stamps are seeded to a real (nonzero) unixnano at claim, equal to each other. 1368 assert!(rec.signon > 0, "signon seeded to a real clock value"); 1369 assert_eq!(rec.signon, rec.last_activity, "seeded equal at claim"); 1370 } 1371 1372 #[test] 1373 fn bump_activity_advances_last_activity_but_never_signon() { 1374 let r = Registry::new(); 1375 let ua = uid("0ABCAAAAA"); 1376 r.try_claim(&ua, "alice", "u", "h", "real", mb()).unwrap(); 1377 let before = r.record_of(&ua).unwrap(); 1378 // Bump to a far-future instant; last_activity moves, signon is pinned. 1379 let future = before.last_activity + 5_000_000_000; 1380 r.bump_activity(&ua, future); 1381 let after = r.record_of(&ua).unwrap(); 1382 assert_eq!(after.last_activity, future, "last_activity advanced"); 1383 assert_eq!(after.signon, before.signon, "signon never moves"); 1384 // An unknown UID is a silent no-op (never panics). 1385 r.bump_activity(&uid("0ABCZZZZZ"), future); 1386 assert!(r.record_of(&uid("0ABCZZZZZ")).is_none()); 1387 } 1388 1389 // --- delivery (the mailbox) --- 1390 1391 #[test] 1392 fn deliver_nick_reaches_the_claimed_mailbox() { 1393 let r = Registry::new(); 1394 let (tx, mut rx) = mailbox(); 1395 r.try_claim(&uid("0ABCAAAAB"), "bob", "u", "h", "real", tx) 1396 .unwrap(); 1397 assert!(r.deliver_nick("bob", b":src PRIVMSG bob :hi\r\n".to_vec())); 1398 assert_eq!(wire(&mut rx), b":src PRIVMSG bob :hi\r\n"); 1399 assert!(rx.try_recv().is_err()); 1400 } 1401 1402 #[test] 1403 fn deliver_nick_is_fold_aware() { 1404 let r = Registry::new(); 1405 let (tx, mut rx) = mailbox(); 1406 r.try_claim(&uid("0ABCAAAAB"), "bob", "u", "h", "real", tx) 1407 .unwrap(); 1408 assert!(r.deliver_nick("BOB", b"x\r\n".to_vec())); 1409 assert_eq!(wire(&mut rx), b"x\r\n"); 1410 } 1411 1412 #[test] 1413 fn deliver_to_absent_target_is_false() { 1414 let r = Registry::new(); 1415 assert!(!r.deliver_nick("ghost", b"x\r\n".to_vec())); 1416 assert!(!r.deliver_uid(&uid("0ABCZZZZZ"), b"x\r\n".to_vec())); 1417 } 1418 1419 #[test] 1420 fn deliver_after_release_is_false() { 1421 let r = Registry::new(); 1422 let ub = uid("0ABCAAAAB"); 1423 let (tx, mut rx) = mailbox(); 1424 r.try_claim(&ub, "bob", "u", "h", "real", tx).unwrap(); 1425 assert!(r.deliver_nick("bob", b"first\r\n".to_vec())); 1426 assert_eq!(wire(&mut rx), b"first\r\n"); 1427 r.release(&ub); 1428 assert!(!r.deliver_nick("bob", b"second\r\n".to_vec())); 1429 assert!(!r.deliver_uid(&ub, b"second\r\n".to_vec())); 1430 assert!(rx.try_recv().is_err(), "nothing delivered after release"); 1431 } 1432 1433 /// Claim `uid` carrying a shared `ConnStats` and a SendQ ceiling — the live-local path 1434 /// (the one that can be killed by an overrun). Returns the receiver + the stats `Arc`. 1435 fn claim_with_sendq( 1436 r: &Registry, 1437 u: &Uid, 1438 nick: &str, 1439 sendq_max: u64, 1440 ) -> (UnboundedReceiver<Envelope>, Arc<ConnStats>) { 1441 let (tx, rx) = mailbox(); 1442 let stats = Arc::new(ConnStats::new(0)); 1443 r.try_claim_with_stats(u, nick, "u", "h.host", "real", tx, stats.clone(), sendq_max) 1444 .unwrap(); 1445 (rx, stats) 1446 } 1447 1448 #[test] 1449 fn deliver_over_sendq_limit_ejects_then_drops_further_frames() { 1450 // P11 slice 147: a non-draining peer whose mailbox overruns its SendQ ceiling is killed 1451 // with a terminal eject, and everything after it is dropped (the memory bound). 1452 let r = Registry::new(); 1453 let ua = uid("0ABCAAAAA"); 1454 let (mut rx, stats) = claim_with_sendq(&r, &ua, "alice", 100); 1455 1456 // Two 40-byte frames fit (depth 40, then 80). 1457 assert!(r.deliver_uid(&ua, vec![b'x'; 40])); 1458 assert!(r.deliver_uid(&ua, vec![b'x'; 40])); 1459 assert_eq!(stats.sendq_len(), 80); 1460 assert!(!stats.sendq_is_dead()); 1461 1462 // The third 40-byte frame would push depth to 120 > 100 → overflow: the frame is 1463 // dropped and a terminal eject is queued in its place. 1464 assert!(r.deliver_uid(&ua, vec![b'x'; 40])); 1465 assert!(stats.sendq_is_dead()); 1466 assert_eq!( 1467 stats.sendq_len(), 1468 80 + "ERROR :Closing Link: h.host (Max SendQ exceeded)\r\n".len() as u64 1469 ); 1470 1471 // A fourth delivery is dropped outright — nothing more is queued. 1472 assert!(r.deliver_uid(&ua, vec![b'x'; 40])); 1473 1474 // Mailbox: the two fitting wire frames, then exactly one Max-SendQ eject, then nothing. 1475 assert_eq!(wire(&mut rx).len(), 40); 1476 assert_eq!(wire(&mut rx).len(), 40); 1477 match rx.try_recv().expect("a terminal eject was queued") { 1478 Envelope::Eject { wire, reason } => { 1479 assert_eq!(reason, "Max SendQ exceeded"); 1480 assert_eq!( 1481 wire, 1482 b"ERROR :Closing Link: h.host (Max SendQ exceeded)\r\n" 1483 ); 1484 } 1485 other => panic!("expected the Max-SendQ eject, got {other:?}"), 1486 } 1487 assert!( 1488 rx.try_recv().is_err(), 1489 "no frame survives past the terminal eject" 1490 ); 1491 } 1492 1493 #[test] 1494 fn kill_eject_and_force_nick_bypass_the_sendq_ceiling() { 1495 // Control-plane frames (a KILL eject, a collision SAVE) are always delivered, even past 1496 // the ceiling — they are how the connection is torn down / corrected. 1497 let r = Registry::new(); 1498 let ua = uid("0ABCAAAAA"); 1499 let (mut rx, stats) = claim_with_sendq(&r, &ua, "alice", 10); 1500 1501 // A KILL whose wire far exceeds the 10-byte ceiling still goes through. 1502 let big = vec![b'k'; 500]; 1503 assert!(r.eject_uid(&ua, big.clone(), "killed".to_string())); 1504 match rx.try_recv().expect("the KILL was delivered") { 1505 Envelope::Eject { wire, reason } => { 1506 assert_eq!(wire, big); 1507 assert_eq!(reason, "killed"); 1508 } 1509 other => panic!("expected the KILL eject, got {other:?}"), 1510 } 1511 // It is still accounted (force-add), and never trips the kill latch. 1512 assert!(!stats.sendq_is_dead()); 1513 assert_eq!(stats.sendq_len(), 500); 1514 1515 // A forced-nick SAVE likewise bypasses the ceiling. 1516 assert!(r.force_nick_uid(&ua, vec![b'n'; 50], "0ABCAAAAA".to_string())); 1517 assert!(matches!(rx.try_recv(), Ok(Envelope::ForceNick { .. }))); 1518 assert!(!stats.sendq_is_dead()); 1519 } 1520 1521 #[test] 1522 fn sendq_depth_is_reported_and_falls_as_the_consumer_drains() { 1523 // STATS l reads the live depth; draining (what `serve_client` does) returns it to zero. 1524 let r = Registry::new(); 1525 let ua = uid("0ABCAAAAA"); 1526 let (_rx, stats) = claim_with_sendq(&r, &ua, "alice", 0); // uncapped: account-only 1527 1528 r.deliver_uid(&ua, vec![b'x'; 30]); 1529 r.deliver_uid(&ua, vec![b'x'; 70]); 1530 let info = r.link_info(); 1531 assert_eq!(info.len(), 1); 1532 assert_eq!(info[0].stats.sendq, 100, "STATS l shows the queued depth"); 1533 1534 // Inverse: the serve loop draining both frames returns the depth to zero. 1535 stats.sendq_drain(30); 1536 stats.sendq_drain(70); 1537 assert_eq!(r.link_info()[0].stats.sendq, 0); 1538 } 1539 1540 #[test] 1541 fn try_claim_link_is_accounted_but_never_killed() { 1542 // A `try_claim` (remote-user / test) link has `sendq_max == 0`: its depth grows under a 1543 // flood (no serve loop drains it) but it is never falsely ejected. 1544 let r = Registry::new(); 1545 let ua = uid("0ABCAAAAA"); 1546 let (tx, mut rx) = mailbox(); 1547 r.try_claim(&ua, "alice", "u", "h", "real", tx).unwrap(); 1548 for _ in 0..1000 { 1549 assert!(r.deliver_uid(&ua, vec![b'x'; 1000])); 1550 } 1551 // 1000 frames all queued (none dropped); the link is alive. 1552 let info = r.link_info(); 1553 assert_eq!(info[0].stats.sendq, 1_000_000); 1554 // Every frame is a plain Wire — no terminal eject was ever synthesized. 1555 for _ in 0..1000 { 1556 assert!(matches!(rx.try_recv(), Ok(Envelope::Wire(_)))); 1557 } 1558 assert!(rx.try_recv().is_err()); 1559 } 1560}