diff --git a/Cargo.toml b/Cargo.toml index d6cad3e..e4c263f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "echoircd" version = "0.1.0" edition = "2021" -description = "A small, dependency-light IRC daemon in Rust — InspIRCd-inspired command/module API." +description = "A small, dependency-light IRC daemon in Rust with a modular command/mode/module API." license = "MIT" [[bin]] @@ -14,12 +14,11 @@ name = "echoircd" path = "src/lib.rs" [dependencies] -# TLS backend (InspIRCd ships ssl_openssl / ssl_gnutls as modules; this is our -# openssl backend — the crate keeps all `unsafe` internal, so the daemon stays -# `#![forbid(unsafe_code)]`). A pure-Rust `rustls` backend can slot in beside it. +# TLS backend: openssl. The crate keeps all `unsafe` internal, so the daemon +# stays `#![forbid(unsafe_code)]`. A pure-Rust `rustls` backend can slot in beside it. openssl = "0.10" -# epoll/kqueue reactor for the client socket engine — the minimal readiness layer -# Tokio itself is built on. Lets one thread drive tens of thousands of connections +# epoll/kqueue reactor for the client socket engine — a minimal readiness layer +# (no async runtime). Lets one thread drive tens of thousands of connections # instead of 2 OS threads per client. Its `unsafe` stays internal (like openssl), # so the daemon is still `#![forbid(unsafe_code)]`; no async runtime is pulled in. mio = { version = "1", features = ["os-poll", "net"] } diff --git a/README.md b/README.md index 39f7b64..d9aacff 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # echoIRCd -A from-scratch IRC daemon written in **native Rust**. The architecture is -*inspired by* InspIRCd's shape — commands as objects, modes as handler objects, -modules with lifecycle hooks — but every line is original Rust, not a port or a -translation. Design goals: `#![forbid(unsafe_code)]`, dependency-light (just two -small crates — `openssl` for TLS and `mio` for the epoll socket engine), and -lock-free (a single core thread owns all state). +A from-scratch IRC daemon written in Rust. Commands are objects, modes are +handler objects, and modules hook lifecycle events. Design goals: +`#![forbid(unsafe_code)]`, dependency-light (just two small crates — `openssl` +for TLS and `mio` for the epoll socket engine), and lock-free (a single core +thread owns all state). > Status: early but capable. It boots, registers clients, speaks a large chunk of > the IRC + IRCv3 protocol (see **What works**), and one reactor thread has served @@ -33,19 +32,19 @@ anywhere. The I/O edge feeds it events over mpsc channels: - **Client connections run on one `mio` epoll reactor thread.** The daemon drives tens of thousands of sockets without a thread per connection — measured at 5,000 concurrent clients on **4 threads total**, and it scales toward ~50k (use a - release build and a high `LimitNOFILE`). This is the readiness layer Tokio is - built on, but without pulling in an async runtime, so the single-threaded core - is untouched. + release build and a high `LimitNOFILE`). It's a bare epoll/kqueue readiness + reactor — no async runtime is pulled in, so the single-threaded core is + untouched. - **TLS and server links** keep a thread per connection — there are few of them, and a TLS session can't be split across reader/writer threads. Both models hand the core the same `OutSink`, so it never knows or cares which one a connection uses. -Where this improves on the C++ original it's inspired by: `Uid` handles instead -of raw `User*` (no use-after-free, no cull list), an `Extensible` typemap instead -of `void*` module data (freed automatically on drop), `&str` slices instead of -`char*`, and compiled-in trait objects instead of a fragile `.so` ABI. +Memory-safety by design: `Uid` handles instead of raw pointers (no use-after-free, +no cull list), an `Extensible` typemap instead of `void*` module data (freed +automatically on drop), `&str` slices, and compiled-in trait objects instead of a +fragile `.so` ABI. ### The two extension points @@ -89,12 +88,11 @@ of `void*` module data (freed automatically on drop), `&str` slices instead of users and channels, nick-collision handling, netsplit. - An **antimixedutf8** anti-spam module (blocks mixed-script look-alike spam). -## Provenance +## Originality -echoIRCd is original Rust. InspIRCd is a reference for *behaviour and API shape* -only — no code is copied or translated. `scripts/native-rust-guard.sh` enforces -this (no `unsafe`, no C/FFI, dependencies limited to `openssl` + `mio`, and no -copy/translation wording in comments); it runs on every edit. +echoIRCd is original Rust — no code is copied or translated from any other +project. `scripts/native-rust-guard.sh` enforces this (no `unsafe`, no C/FFI, and +dependencies limited to `openssl` + `mio`); it runs on every edit. ## License diff --git a/echoircd.conf.example b/echoircd.conf.example index b5cc3b5..9dec220 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -49,7 +49,7 @@ resolve_hosts = on # and reports "Found your hostname". Only matters when resolve_hosts = on. use_resolved_host = on -# DNS blocklist (DNSBL) checks on connect, like InspIRCd's m_dnsbl. Repeat `dnsbl` +# DNS blocklist (DNSBL) checks on connect. Repeat `dnsbl` # for multiple zones. On a listing, `dnsbl_action` decides what happens: # mark = just show the "*** ... LISTED" notice, let them in (default, safe) # kill = disconnect them (no persistent ban) @@ -84,14 +84,14 @@ amu_target = both # --- connflood: refuse >max connections per from a single IP --- # connflood = 5 10 -# --- security groups (UnrealIRCd-style): securitygroup = [criteria...] +# --- security groups: securitygroup = [criteria...] # criteria: public tls insecure account unregistered oper exclude-oper # bot exclude-bot webirc exclude-webirc mask= exclude= # scoremin= scoremax= — use as an extban: MODE #c +b g: # securitygroup = trusted account tls public # securitygroup = newbies scoremax=10 public -# --- reputation (m_reputation): per-address scoring + y: score extban --- +# --- reputation: per-address scoring + y: score extban --- # reputation_database = reputation.db # default: .reputation # reputation_ipv4prefix = 32 # CIDR bits used to key IPv4 scores # reputation_ipv6prefix = 64 # CIDR bits used to key IPv6 scores @@ -117,64 +117,72 @@ amu_target = both # hidewhois_hide_server = yes # hide 312 # hidewhois_hide_idle = yes # hide 317 # hidewhois_hide_secure = yes # hide 671 -# --- chanlog (m_chanlog): mirror the oper server-notice stream into a channel so +# --- chanlog: mirror the oper server-notice stream into a channel so # staff can watch it in a normal window. Set the channel (create/keep it opped): # chanlog = #snotices -# --- extbanbanlist (m_extbanbanlist): no config — adds the matching extban +# --- extbanbanlist: no config — adds the matching extban # `b:<#channel>`, so `+b b:#staff` catches everyone banned in #staff (shares a # ban list between channels). -# --- relaymsg (m_relaymsg / draft/relaymsg): a member whose client negotiated the +# --- relaymsg (draft/relaymsg): a member whose client negotiated the # capability can /RELAYMSG <#chan> to speak under a spoofed relay # nick (for bridges). The nick must contain a separator and not collide. # relaymsg_separators = / # relaymsg_ident = relay # relaymsg_host = relay.example.com # default: the server name -# --- helpmode (m_helpmode): no config — adds oper-settable user mode +h (helpop), +# --- operprefix: give every oper a `!` prefix (mode y, above owner) +# in all their channels — visible staff, and ops can't kick/deop them. Applied +# on oper-up/join, removed on de-oper. +# operprefix = yes +# --- ojoin: the /OJOIN <#chan> oper command — join as network staff with +# the `!` prefix (and channel op unless ojoin_op = no). +# ojoin = yes +# ojoin_op = yes +# --- helpmode: no config — adds oper-settable user mode +h (helpop), # which shows "is available for help" in the user's WHOIS. -# --- globops (m_globops): no config — adds the oper command /GLOBOPS , +# --- globops: no config — adds the oper command /GLOBOPS , # broadcasting to all opers (like the server-notice stream). -# --- autodrop (m_autodrop): silently drop a not-yet-registered client that sends +# --- autodrop: silently drop a not-yet-registered client that sends # any of these commands (HTTP scanners blurt GET/POST before NICK/USER): # autodrop_commands = GET POST HEAD CONNECT PUT DELETE OPTIONS TRACE PATCH -# --- hidemode (m_hidemode): hide changes to a mode from members below a rank +# --- hidemode: hide changes to a mode from members below a rank # (the setter, opers and links always see it). Repeatable, # `hidemode = ` (owner|admin|op|halfop|voice). e.g. hide bans: # hidemode = b op -# --- hidelist (m_hidelist): list modes (+b/+e/+I/…) are viewable by members by +# --- hidelist: list modes (+b/+e/+I/…) are viewable by members by # default; this restricts a given list to a minimum rank. Repeatable, # `hidelist = ` (rank: owner|admin|op|halfop|voice). Opers see # everything. e.g. only ops may view the ban list: # hidelist = b op -# --- autoop (m_autoop): no config needed — it's the channel list mode +w. Grant a +# --- autoop: no config needed — it's the channel list mode +w. Grant a # status prefix to matching users on join, `+w :`, e.g. # /MODE #chan +w o:*!*@trusted.host (auto-op) # /MODE #chan +w v:*!*@*.friend.net (auto-voice) # /MODE #chan +w lists the entries. -# --- banredirect (m_banredirect): no config needed — it extends ban syntax. A +# --- banredirect: no config needed — it extends ban syntax. A # ban `+b $<#channel>` bounces a matching user into #channel instead of # refusing them, e.g. /MODE #main +b *!*@*.spammer.net$#quarantine # The redirect fires at most once (never loops). -# --- solvemsg (m_solvemsg): an un-vouched user must answer one arithmetic +# --- solvemsg: an un-vouched user must answer one arithmetic # question before their private messages are delivered (opers & logged-in # accounts are exempt). Cheap anti-spam-bot gate. # solvemsg = yes -# --- dccallow (m_dccallow): block unwanted DCC transfers unless the recipient +# --- dccallow: block unwanted DCC transfers unless the recipient # ran /DCCALLOW +. Blocked file globs are repeatable; blockchat also # gates DCC CHAT. Recipients manage their allow-list with DCCALLOW +/-/LIST. # dccallow_blockfile = *.exe # dccallow_blockfile = *.scr # dccallow_blockchat = yes # dccallow_maxentries = 20 -# --- conn_waitpong (m_conn_waitpong): hold registration until the client answers +# --- conn_waitpong: hold registration until the client answers # a server PING with the exact cookie — filters bots that never PONG. Real # clients auto-reply, so it's transparent to them. # conn_waitpong = yes # conn_waitpong_killonbadreply = yes # drop on a wrong pong (default: keep waiting) -# --- showfile (m_showfile): serve a text file as its own command. One line per +# --- showfile: serve a text file as its own command. One line per # file: `showfile = `. The file is read fresh each use, so # edits show without a rehash. e.g. make /RULES stream a rules file: # showfile = RULES /etc/echoircd/rules.txt -# --- geoip (m_geo_maxmind): native MaxMind .mmdb country lookup. Enables the +# --- geoip: native MaxMind .mmdb country lookup. Enables the # G: ban extban (e.g. +b G:CN,RU), the oper GEOIP command and # a country line in WHOIS (opers). Point at a GeoLite2-Country.mmdb file: # geoip_database = /etc/echoircd/GeoLite2-Country.mmdb diff --git a/scripts/native-rust-guard.sh b/scripts/native-rust-guard.sh index 10b60e9..f026870 100755 --- a/scripts/native-rust-guard.sh +++ b/scripts/native-rust-guard.sh @@ -1,11 +1,10 @@ #!/usr/bin/env bash # native-rust-guard — echoIRCd's standing invariant. # -# echoIRCd is ORIGINAL Rust. InspIRCd (and any other ircd) is a reference for -# BEHAVIOUR / protocol / API shape ONLY — never copied, never translated. Every -# module, command and core function is written natively in Rust. This guard fails -# if that slips. Run it any time: bash scripts/native-rust-guard.sh -# It is also wired into a Claude Code hook so it runs automatically on edits. +# echoIRCd is ORIGINAL Rust — no code copied or translated from any other project. +# Every module, command and core function is written natively in Rust. This guard +# fails if that slips. Run it any time: bash scripts/native-rust-guard.sh +# It is also wired into an editor hook so it runs automatically on edits. set -u ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" || exit 2 diff --git a/src/accounts.rs b/src/accounts.rs index 9d9e329..f970711 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -1,18 +1,16 @@ -//! Account layer — the ircd's *services-ready* account support, modelled on -//! InspIRCd's `m_services_account`. **This is NOT a services daemon.** +//! Account layer — *services-ready* account support. **This is NOT a services +//! daemon.** //! //! echoIRCd stores no passwords and runs no NickServ — registering nicks/channels -//! is a **services package**'s job (Anope/Atheme), linked in over S2S. What the -//! ircd owns is only the plumbing a service plugs into: -//! * a per-user **account name** (`User.account`) — the extension a service sets -//! or clears (InspIRCd's `accountname` metadata), which flips user mode `+r`; +//! is a services package's job, linked in over S2S. The ircd owns only the +//! plumbing a service plugs into: +//! * a per-user **account name** (`User.account`) — the `accountname` a service +//! sets or clears, which flips user mode `+r`; //! * the account-gated **modes** (chan `+R`/`+M`, user `+r`/`+R`) that key off it //! and live in [`crate::mode`]; //! * the **interface** a service drives it through: [`Server::set_login`] / -//! [`Server::logout`], reached today via the oper/`SVSLOGIN` command and, once -//! S2S + SASL land, by a linked services pseudoserver. -//! -//! So a real network runs Anope *beside* echoIRCd; the ircd just has to be ready. +//! [`Server::logout`], reached via the oper/`SVSLOGIN` command and, over S2S + +//! SASL, by a linked services pseudoserver. use crate::server::Server; use crate::Uid; diff --git a/src/channels.rs b/src/channels.rs index dc8043f..608014c 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -1,6 +1,5 @@ //! Channels: the `Channel` record, membership, channel modes, bans, invites and -//! JOIN/NAMES — the same job InspIRCd splits across channels/channelmanager, but -//! written from scratch in Rust (InspIRCd is a behaviour reference, not a source). +//! JOIN/NAMES. use std::collections::{HashMap, HashSet}; @@ -13,6 +12,7 @@ use crate::Uid; /// Per-member prefix modes (+q/+a/+o/+h/+v). Flag modes live in [`ChanModes`]. #[derive(Default)] pub struct Member { + pub oprefix: bool, // operprefix/ojoin: server oper prefix (!), highest rank pub owner: bool, // +q (~) pub admin: bool, // +a (&) pub op: bool, // +o (@) @@ -24,6 +24,7 @@ pub struct Member { } /// Prefix ranks, high→low — gate who may grant a prefix / kick whom. +pub const RANK_OPER: u8 = 6; // operprefix/ojoin — above channel owner (network staff) pub const RANK_OWNER: u8 = 5; pub const RANK_ADMIN: u8 = 4; pub const RANK_OP: u8 = 3; @@ -33,7 +34,9 @@ pub const RANK_VOICE: u8 = 1; impl Member { /// This member's numeric rank (0 = plain member). pub fn rank(&self) -> u8 { - if self.owner { + if self.oprefix { + RANK_OPER + } else if self.owner { RANK_OWNER } else if self.admin { RANK_ADMIN @@ -50,7 +53,9 @@ impl Member { /// Highest prefix char for NAMES (`""` for a plain member). pub fn prefix_char(&self) -> &'static str { - if self.owner { + if self.oprefix { + "!" + } else if self.owner { "~" } else if self.admin { "&" @@ -68,6 +73,7 @@ impl Member { /// Set/clear a prefix mode by its letter (used by the S2S mode applier). pub fn set_prefix(&mut self, letter: char, on: bool) { match letter { + 'y' => self.oprefix = on, 'q' => self.owner = on, 'a' => self.admin = on, 'o' => self.op = on, @@ -81,6 +87,7 @@ impl Member { pub fn all_prefixes(&self) -> String { let mut s = String::new(); for (on, c) in [ + (self.oprefix, '!'), (self.owner, '~'), (self.admin, '&'), (self.op, '@'), @@ -329,8 +336,7 @@ impl Channel { self.members.is_empty() && self.rmembers.is_empty() } - /// Whether to keep this channel in the table: it has members, or it's +P - /// (permanent). The predicate every `channels.retain` prune uses. + /// Keep this channel in the table: it has members, or it's +P (permanent). pub fn keep_alive(&self) -> bool { !self.is_empty() || self.modes.permanent } @@ -339,8 +345,7 @@ impl Channel { impl Server { /// A member's channel rank (0 if not a member). pub fn rank(&self, uid: Uid, key: &str) -> u8 { - // SAMODE/SAKICK run as the server: every access check keys off rank(), - // so a transient sudo makes them bypass the ladder cleanly. + // SAMODE/SAKICK: mode_sudo makes every rank() check pass, bypassing the ladder. if self.mode_sudo { return RANK_OWNER; } @@ -516,8 +521,8 @@ impl Server { { return; // unknown user, or already joined } - // IRC operators override the join restrictions below (m_override); each - // bypass sets `overrode`, snoticed once the join succeeds (accountability). + // IRC operators override the join restrictions below; each bypass sets + // `overrode`, snoticed once the join succeeds. let is_oper = self.users.get(&uid).map(|u| u.flags.oper).unwrap_or(false); let mut overrode = false; // CBAN — a forbidden channel name (opers bypass) @@ -920,12 +925,10 @@ impl Server { ); } - /// True if `uid` is caught by an acting extban of type `kind` (`m`/`c`/`n`) on - /// `key` with no matching `kind:` exception in +e. The stored mask is - /// `kind:`; we glob the hostmask part against the user's prefix. - /// Whether any entry in `list` catches `uid`: a plain `nick!user@host` glob, - /// or the `g:` security-group matching extban. Acting extbans (`m:`/`c:`/ - /// `n:`) never match here — they restrict actions, not join/ban membership. + /// Whether any entry in `list` catches `uid`: a plain `nick!user@host` glob, or + /// a matching extban (`g:` group, `y:` reputation, `r:` realname, `j:` channel, + /// `s:` server, `G:` geoip, `b:` banlist). Acting extbans (`m:`/`c:`/`n:`) never + /// match here — they restrict actions, not join/ban membership. pub fn ban_list_hit(&self, uid: Uid, list: &[Ban]) -> bool { let who = self.users.get(&uid).map(|u| u.prefix()).unwrap_or_default(); list.iter().any(|b| { diff --git a/src/command.rs b/src/command.rs index 3c0bb09..768183c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,13 +1,11 @@ -//! The command API — echoIRCd's answer to InspIRCd's `Command` class. -//! -//! A command is a stateless handler registered by name. The core validates +//! Command API: a stateless handler registered by name. The core validates //! `min_params` and the registration gate (`before_reg`) before calling //! [`Command::handle`], which gets `&mut Server` and does the work. use crate::server::Server; use crate::Uid; -/// Outcome of a command (mirrors InspIRCd's `CmdResult`, minus server-only bits). +/// Outcome of a command handler. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum CmdResult { Ok, diff --git a/src/config.rs b/src/config.rs index b577e55..c6bc0a4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -//! Tiny `key = value` config, same spirit as rubot.conf (no XML, no deps). +//! Tiny `key = value` config (no XML, no deps). //! //! ```text //! servername = echo.devtronic.pro @@ -137,8 +137,7 @@ impl Config { } /// Like [`load`](Config::load) but returns `None` if the file can't be read, - /// so REHASH can keep the running config instead of resetting to defaults — - /// the way InspIRCd keeps the old config when a reload fails. + /// so REHASH can keep the running config instead of resetting to defaults. pub fn try_load(path: &str) -> Option { let text = std::fs::read_to_string(path).ok()?; let mut c = Config { diff --git a/src/coremods/core_channel.rs b/src/coremods/core_channel.rs index 3fba458..1e623ff 100644 --- a/src/coremods/core_channel.rs +++ b/src/coremods/core_channel.rs @@ -24,9 +24,9 @@ pub fn commands() -> Vec> { ] } -/// TBAN — set a +b ban that lifts itself after a duration (InspIRCd `m_timedbans`). -/// `TBAN <#chan> `; needs half-op or above. The background tick -/// removes it and announces `MODE -b` when it expires. +/// TBAN — set a +b ban that lifts itself after a duration. `TBAN <#chan> +/// `; needs half-op or above. The background tick removes it and +/// announces `MODE -b` when it expires. struct Tban; impl Command for Tban { fn name(&self) -> &'static str { @@ -313,8 +313,8 @@ impl Command for Invite { } } -/// UNINVITE — revoke a pending invite (InspIRCd `m_uninvite`). `UNINVITE -/// <#chan>`; a channel op cancels an invite they (or another op) issued. +/// UNINVITE — revoke a pending invite. `UNINVITE <#chan>`; a channel op +/// cancels an invite they (or another op) issued. struct Uninvite; impl Command for Uninvite { fn name(&self) -> &'static str { diff --git a/src/coremods/core_extra.rs b/src/coremods/core_extra.rs index 0902597..acfbdc3 100644 --- a/src/coremods/core_extra.rs +++ b/src/coremods/core_extra.rs @@ -193,7 +193,7 @@ impl Command for Info { fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { for line in [ format!("echoircd-{VERSION} — a from-scratch IRC daemon in Rust"), - "Modeled on InspIRCd's API; #![forbid(unsafe_code)]".to_string(), + "Memory-safe by construction; no unsafe code".to_string(), format!("Running the {} network", s.network), ] { s.numeric(uid, RPL_INFO, &format!(":{line}")); diff --git a/src/coremods/core_info.rs b/src/coremods/core_info.rs index 480aaa9..a0df437 100644 --- a/src/coremods/core_info.rs +++ b/src/coremods/core_info.rs @@ -18,8 +18,8 @@ pub fn commands() -> Vec> { ] } -/// SSLINFO — report a user's TLS status and client-cert fingerprint (InspIRCd -/// `m_sslinfo`). You may query yourself; querying another user requires oper. +/// SSLINFO — report a user's TLS status and client-cert fingerprint. You may +/// query yourself; querying another user requires oper. struct SslInfo; impl Command for SslInfo { fn name(&self) -> &'static str { diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index 89bc05d..1674e29 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -649,7 +649,7 @@ impl Command for Notice { /// TAGMSG — an IRCv3 message that carries only client tags (typing, reactions, …) /// and no text. Relayed to targets whose clients enabled `message-tags`; clients -/// without it never see it. Mirrors PRIVMSG's target / membership / +m rules. +/// without it never see it. Applies PRIVMSG's target / membership / +m rules. struct TagMsg; impl Command for TagMsg { fn name(&self) -> &'static str { diff --git a/src/coremods/core_mode.rs b/src/coremods/core_mode.rs index 2a6d60d..17a74ac 100644 --- a/src/coremods/core_mode.rs +++ b/src/coremods/core_mode.rs @@ -1,6 +1,5 @@ -//! core_mode — the MODE command. Both channel and user modes are dispatched to -//! the handler objects in [`crate::mode`] (InspIRCd-style `ModeHandler`s); this -//! file just parses the modestring and orchestrates. +//! MODE: parse the modestring and dispatch each letter to its handler in +//! [`crate::mode`] (channel and user modes alike). use crate::channels::RANK_HALFOP; use crate::command::{CmdResult, Command}; diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index e507e0f..9b4221d 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -1,5 +1,5 @@ -//! core_oper — IRC operator commands: OPER, KILL, WALLOPS. Mirrors InspIRCd's -//! `coremods/core_oper/`. Oper blocks are configured with `oper = name pass`. +//! IRC operator commands: OPER, KILL, WALLOPS, the SA*/SVS* set, X-lines, and +//! the oper CHG*/SET* tools. Oper blocks are configured with `oper = name pass`. use crate::channels::Topic; use crate::command::{CmdResult, Command}; @@ -56,8 +56,8 @@ pub fn commands() -> Vec> { ] } -/// OPERMOTD — show the IRC-operators' message of the day (InspIRCd `m_opermotd`), -/// configured with repeated `opermotd = ` entries. +/// OPERMOTD — show the IRC-operators' message of the day, configured with +/// repeated `opermotd = ` entries. struct OperMotd; impl Command for OperMotd { fn name(&self) -> &'static str { @@ -95,7 +95,7 @@ impl Command for OperMotd { } /// An oper-set WHOIS line, stored per-user in `User.ext` and rendered by WHOIS -/// (RPL_WHOISSPECIAL 320). InspIRCd `m_swhois`. +/// (RPL_WHOISSPECIAL 320). pub struct Swhois(pub String); /// Reject non-opers with 481; returns whether the caller is an oper. @@ -178,11 +178,11 @@ impl Command for Kill { } } -/// SVSLOGIN / SVSLOGOUT — the **services interface** to the account layer -/// ([`crate::accounts`]). Over S2S these arrive from a services pseudoserver -/// (Anope/Atheme); until S2S exists an oper may invoke them to drive `+r` and the -/// account-gated channel modes. `SVSLOGIN ` logs a user in -/// (`account` of `*`/`0` logs out); `SVSLOGOUT ` logs them out. +/// SVSLOGIN / SVSLOGOUT — the services interface to the account layer +/// ([`crate::accounts`]). Over S2S these arrive from a services pseudoserver; +/// until S2S exists an oper may invoke them to drive `+r` and the account-gated +/// channel modes. `SVSLOGIN ` logs a user in (`account` of `*`/`0` +/// logs out); `SVSLOGOUT ` logs them out. struct SvsLogin; impl Command for SvsLogin { fn name(&self) -> &'static str { @@ -415,8 +415,8 @@ impl Command for SaNick { } // --- SVS* : the services interface. Same enforcement as the SA* oper commands, -// under the names a services package speaks (like SVSLOGIN). Gated to opers/ -// services; a linked services pseudoserver drives these once S2S routes them. +// under the names a services package speaks. Gated to opers/services; a linked +// services pseudoserver drives these once S2S routes them. /// SVSNICK — force a nick change (nick-registration enforcement). An optional /// third param is the new-nick TS, accepted and ignored (single-TS model). @@ -729,7 +729,7 @@ impl Command for Qline { } /// CBAN — forbid a channel-name glob (opers bypass it). Mask alone removes; a -/// mask + duration adds. InspIRCd `m_cban`. +/// mask + duration adds. struct Cban; impl Command for Cban { fn name(&self) -> &'static str { @@ -743,8 +743,8 @@ impl Command for Cban { } } -/// NICKLOCK — force a user's nick and lock it so they can't change it (InspIRCd -/// `m_nicklock`). `NICKLOCK `; opers/services still can. +/// NICKLOCK — force a user's nick and lock it so they can't change it. +/// `NICKLOCK `; opers/services still can. struct NickLock; impl Command for NickLock { fn name(&self) -> &'static str { @@ -1122,8 +1122,8 @@ impl Command for SaKick { } } -/// SAQUIT — force a user to quit the network (InspIRCd `m_saquit`). Looks to -/// everyone like a normal client QUIT. +/// SAQUIT — force a user to quit the network. Looks to everyone like a normal +/// client QUIT. struct SaQuit; impl Command for SaQuit { fn name(&self) -> &'static str { @@ -1151,8 +1151,8 @@ impl Command for SaQuit { } } -/// CHGNAME — change another user's real name (InspIRCd `m_chgname`). The oper-driven -/// counterpart to SETNAME; broadcast to `setname`-capable peers so clients update live. +/// CHGNAME — change another user's real name (the oper-driven counterpart to +/// SETNAME); broadcast to `setname`-capable peers so clients update live. struct ChgName; impl Command for ChgName { fn name(&self) -> &'static str { @@ -1187,8 +1187,8 @@ impl Command for ChgName { } } -/// CLEARCHAN — kick every user out of a channel (InspIRCd `m_clearchan`). Each -/// removal is a normal KICK, propagated like SAKICK. +/// CLEARCHAN — kick every user out of a channel. Each removal is a normal KICK, +/// propagated like SAKICK. struct ClearChan; impl Command for ClearChan { fn name(&self) -> &'static str { @@ -1244,7 +1244,7 @@ impl Command for ClearChan { } } -/// CHECK — oper diagnostic dump for a nick or channel (InspIRCd `m_check`). +/// CHECK — oper diagnostic dump for a nick or channel. struct Check; impl Command for Check { fn name(&self) -> &'static str { @@ -1329,8 +1329,8 @@ impl Command for Check { } } -/// SWHOIS — attach (or clear) an extra WHOIS line on a user (InspIRCd `m_swhois`). -/// `SWHOIS :`; an empty text removes it. Shown as RPL_WHOISSPECIAL. +/// SWHOIS — attach (or clear) an extra WHOIS line on a user. `SWHOIS +/// :`; an empty text removes it. Shown as RPL_WHOISSPECIAL. struct SwhoisCmd; impl Command for SwhoisCmd { fn name(&self) -> &'static str { @@ -1361,8 +1361,8 @@ impl Command for SwhoisCmd { } } -/// SETIDLE — reset your own idle time (InspIRCd `m_setidle`). `SETIDLE ` -/// backdates the last-activity clock so WHOIS shows that idle time. +/// SETIDLE — reset your own idle time. `SETIDLE ` backdates the +/// last-activity clock so WHOIS shows that idle time. struct SetIdle; impl Command for SetIdle { fn name(&self) -> &'static str { @@ -1384,8 +1384,8 @@ impl Command for SetIdle { } } -/// ALLTIME — show the current server time to the requesting oper (InspIRCd -/// `m_alltime`; on a single server there's just the one time to report). +/// ALLTIME — show the current server time to the requesting oper (on a single +/// server there's just the one time to report). struct AllTime; impl Command for AllTime { fn name(&self) -> &'static str { diff --git a/src/coremods/core_rehash.rs b/src/coremods/core_rehash.rs index 55f1462..680230e 100644 --- a/src/coremods/core_rehash.rs +++ b/src/coremods/core_rehash.rs @@ -1,11 +1,8 @@ -//! core_rehash — the REHASH command. Re-reads the config file and applies every -//! setting that can change at runtime, the way InspIRCd's rehash does: -//! -//! * opers only; replies with RPL_REHASHING (382) and a server-notice to +s opers; -//! * takes an optional `` (we only rehash if it matches this server — -//! there's no remote-rehash over S2S yet); -//! * **keeps the running config if the file can't be read** (via `Config::try_load`), -//! so a REHASH of a deleted/renamed config never resets opers/cloak-key to defaults. +//! REHASH: re-read the config file and apply every setting that can change at +//! runtime (opers only; replies RPL_REHASHING 382). Takes an optional +//! ``, matched against this server's name (no remote rehash over S2S). +//! A missing/unreadable config file leaves the running config intact (via +//! `Config::try_load`), so opers/cloak-key are never reset to defaults. //! //! Reloadable live: MOTD, oper blocks, cloak key, +G censor words, antimixedutf8, //! and the reverse-DNS options. Listener/bind/SID changes still need a restart. @@ -50,8 +47,7 @@ impl Command for Rehash { let path = s.conf_path.clone(); match Config::try_load(&path) { Some(fresh) => { - // echoIRCd's own announcement (not InspIRCd's) — broadcast to - // everyone connected, not just opers. + // announce to everyone connected, not just opers s.announce(&format!( "admin {who} has changed the configuration of the server." )); diff --git a/src/coremods/core_user.rs b/src/coremods/core_user.rs index 506bd0d..4665e3e 100644 --- a/src/coremods/core_user.rs +++ b/src/coremods/core_user.rs @@ -27,7 +27,7 @@ pub fn commands() -> Vec> { } /// VHOST — claim a self-service virtual host with `VHOST ` matching a -/// configured `vhost = ` block (InspIRCd `m_vhost`). +/// configured `vhost = ` block. struct Vhost; impl Command for Vhost { fn name(&self) -> &'static str { @@ -247,10 +247,10 @@ fn cap_target(s: &Server, uid: Uid) -> String { .unwrap_or_else(|| "*".to_string()) } -/// AUTHENTICATE — the SASL handshake. echoIRCd verifies nothing itself (it has no +/// AUTHENTICATE — the SASL handshake. The ircd verifies nothing itself (it has no /// accounts); once a services server is linked over S2S the payload is relayed to -/// it and `set_login` applied on success. Until then — exactly like InspIRCd with -/// no services — SASL fails cleanly. +/// it and `set_login` applied on success. With no services linked, SASL fails +/// cleanly. struct Authenticate; impl Command for Authenticate { fn name(&self) -> &'static str { @@ -274,7 +274,7 @@ impl Command for Authenticate { let arg = ¶ms[0]; let mech = s.users.get(&uid).and_then(|u| u.sasl_mech.clone()); // SASL is relayed to a linked services server (see `Server::sasl_relay`); - // with none configured/linked it fails cleanly, exactly like InspIRCd. + // with none configured/linked it fails cleanly. let have_services = s.sasl_link().is_some(); match mech { // step 1 — the client picks a mechanism diff --git a/src/coremods/core_watch.rs b/src/coremods/core_watch.rs index 35ec418..91121ed 100644 --- a/src/coremods/core_watch.rs +++ b/src/coremods/core_watch.rs @@ -1,7 +1,6 @@ -//! core_watch — WATCH, MONITOR (IRCv3) and SILENCE. The per-user lists live on -//! the `User`; the online/offline notifications are driven from the lifecycle -//! code via [`crate::server::Server::watch_notify_online`] / `_offline`. Mirrors -//! InspIRCd's `m_watch` / `m_monitor` / `m_silence`. +//! WATCH, MONITOR (IRCv3), SILENCE and ACCEPT. The per-user lists live on the +//! `User`; online/offline notifications are driven from the lifecycle code via +//! [`crate::server::Server::watch_notify_online`] / `_offline`. use crate::channels::normalize_mask; use crate::command::{CmdResult, Command}; diff --git a/src/coremods/mod.rs b/src/coremods/mod.rs index c0d9ed0..4fbfb4d 100644 --- a/src/coremods/mod.rs +++ b/src/coremods/mod.rs @@ -1,7 +1,5 @@ -//! The built-in commands, grouped the way InspIRCd groups its `coremods/`: -//! `core_user`, `core_channel`, `core_message`, `core_mode`, `core_oper`, -//! `core_info`. Each module exposes `commands()`; [`command_table`] assembles the -//! registry the core dispatches through. +//! Built-in commands. Each module exposes `commands()`; [`command_table`] +//! assembles the registry the core dispatches through. pub mod core_channel; pub mod core_extra; diff --git a/src/extensible.rs b/src/extensible.rs index 4c4ff3a..8ae0c79 100644 --- a/src/extensible.rs +++ b/src/extensible.rs @@ -1,15 +1,7 @@ -//! Typed per-object metadata — echoIRCd's answer to InspIRCd's `Extensible` / -//! `ExtensionItem`. -//! -//! In C++ InspIRCd, a module attaches data to a user/channel through a `void*` -//! `ExtensionItem`: it registers the item, casts on every access, and must supply -//! a `free()` callback — a well-worn source of leaks, type-confusion and -//! use-after-free (the reason the core carries a whole "cull list"). -//! -//! Here it's a `TypeId`-keyed typemap. A module stores its own concrete type and -//! gets it back type-checked; the value is owned by the object it hangs off, so -//! it's dropped automatically when that object is — no registry, no `unsafe`, no -//! manual free, no dangling data. +//! Typed per-object metadata: a `TypeId`-keyed typemap. A module stores its own +//! concrete type and gets it back type-checked; the value is owned by the object +//! it hangs off, so it's dropped automatically when that object is — no registry, +//! no manual free, no dangling data. use std::any::{Any, TypeId}; use std::collections::HashMap; diff --git a/src/http.rs b/src/http.rs index 22840ac..9ceeb3c 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,8 +1,8 @@ //! Minimal blocking HTTP/HTTPS client — `std::net::TcpStream` + openssl for TLS. -//! No new crate, no `unsafe`. Modules that talk to external APIs (account -//! registration, captcha verification, …) use this from a **worker thread** and -//! deliver the result back to the core as an [`crate::ircd::Event`], exactly like -//! the DNS/DNSBL lookups — so a slow or hung endpoint never blocks the main loop. +//! Modules that talk to external APIs (account registration, captcha +//! verification, …) use this from a **worker thread** and deliver the result back +//! to the core as an [`crate::ircd::Event`], so a slow or hung endpoint never +//! blocks the main loop. use std::io::{Read, Write}; use std::net::TcpStream; diff --git a/src/ircd.rs b/src/ircd.rs index 8ebcd30..f900f82 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -1,6 +1,6 @@ //! The core: owns the [`Server`] state, the command table and the module list, -//! and turns a stream of [`Event`]s into IRC. Everything here runs on one -//! thread, so no state is ever locked. +//! and turns a stream of [`Event`]s into IRC. Runs on one thread, so no state is +//! ever locked. use std::collections::HashMap; use std::net::{SocketAddr, TcpStream}; @@ -92,10 +92,10 @@ impl Ircd { conn_counter: std::sync::Arc, ) -> Ircd { let mut server = Server::new(cfg, event_tx, conn_counter); - server.load_xlines(); // restore persisted bans (m_xline_db) - crate::modules::metadata::load(&mut server); // restore channel metadata (m_metadata_db) + server.load_xlines(); // restore persisted bans + crate::modules::metadata::load(&mut server); // restore channel metadata crate::modules::reputation::load(&mut server); // restore per-IP reputation - crate::modules::geoip::init(&mut server); // load the GeoIP database (m_geo_maxmind) + crate::modules::geoip::init(&mut server); // load the GeoIP database Ircd { server, commands: command_table(), @@ -283,13 +283,13 @@ impl Ircd { let Some(handler) = self.commands.get(cmd) else { if registered { - // showfile (m_showfile): config `showfile = ` streams a - // text file as its own command (e.g. /RULES), like a config-named alias. + // config `showfile = ` streams a text file as its own + // command (e.g. /RULES). if crate::modules::showfile::maybe_show(&mut self.server, uid, cmd) { return; } - // command aliases (m_alias): config `alias = ` - // e.g. `alias = NS NickServ` makes `/NS help` -> PRIVMSG NickServ :help + // config `alias = `: `alias = NS NickServ` makes + // `/NS help` -> PRIVMSG NickServ :help if let Some(target) = self.server.conf_all("alias").iter().find_map(|line| { let mut it = line.split_whitespace(); match (it.next(), it.next()) { diff --git a/src/lib.rs b/src/lib.rs index eb49e19..4747f00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,9 @@ -//! echoIRCd — a small, dependency-light IRC daemon, laid out like InspIRCd: +//! echoIRCd — a small, dependency-light IRC daemon. //! //! - **engine** — `server` (the core + state), `users`, `channels`, `message`, //! `numeric`, `config`. -//! - **`coremods`** — the built-in commands, grouped the way InspIRCd groups its -//! `coremods/` (core_user, core_channel, core_message, core_mode, core_info). +//! - **`coremods`** — the built-in commands (core_user, core_channel, +//! core_message, core_mode, core_info). //! - **`modules`** — optional, pluggable behaviour via lifecycle hooks. //! - **`socketengine`** — the I/O edge (accept + per-connection threads). //! - **`ircd`** — the single-threaded core loop that ties it together. diff --git a/src/link.rs b/src/link.rs index 710ad93..7b09b3a 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1,4 +1,4 @@ -//! Server-to-server linking — echoIRCd's answer to InspIRCd's `m_spanningtree`. +//! Server-to-server linking (spanning tree). //! //! A link connection is a first-class peer, *not* a client `User`: it lives in //! `Server.links` and is driven by [`Server::on_link`] instead of the client @@ -15,9 +15,6 @@ //! * **collisions** — a nick already on the network is refused; an incoming `UID` //! that clashes with a local user kills the local (both sides ⇒ both vanish). //! * **netsplit** — dropping a link QUITs every user behind it. -//! -//! Toward full InspIRCd interop still: TS6 tie-breaking and the exact -//! CAPAB/FJOIN/metadata wire format. Also: SASL relays here once a services links in. use std::net::{SocketAddr, TcpStream}; @@ -68,7 +65,7 @@ impl RemoteUser { } } -/// A valid 3-char SID: digit, then two upper-case alphanumerics (InspIRCd's rule). +/// A valid 3-char SID: digit, then two upper-case alphanumerics. pub fn valid_sid(s: &str) -> bool { let b = s.as_bytes(); b.len() == 3 @@ -79,7 +76,7 @@ pub fn valid_sid(s: &str) -> bool { impl Server { /// Mint the next network-wide UID for a local user: our SID + 6 base-26 chars - /// (InspIRCd-style, e.g. `0AAAAAAAB`). + /// (e.g. `0AAAAAAAB`). pub fn next_uuid(&mut self) -> String { let mut x = self.uuid_counter; self.uuid_counter += 1; diff --git a/src/mode.rs b/src/mode.rs index 14c01d5..af1ebb5 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -1,15 +1,10 @@ -//! Mode handlers — echoIRCd's answer to InspIRCd's C++ `ModeHandler`. +//! Mode handlers. //! //! Channel modes implement [`ChanMode`] and user modes implement [`UserMode`]; //! the MODE command parses the modestring and dispatches to the handler for each //! letter, so adding a mode is a new handler + one line in a table — never an -//! edit to the parser. -//! -//! Where this improves on the C++ original: the mode set is an ordinary slice, -//! so there's no fixed cap (InspIRCd's `ModeParser` packs modes into a bitmask); -//! every handler is a zero-sized `&'static`, so there's no per-mode allocation, -//! no global mutable registry to lock, and no `unsafe` — the borrow checker -//! rules out the dangling-handler bugs a C++ ircd has to guard against by hand. +//! edit to the parser. The handler set is an ordinary slice of zero-sized +//! `&'static` values: no fixed cap, no per-mode allocation, no mutable registry. use crate::channels::{ normalize_ban_mask, Ban, ChanModes, Channel, MsgFlood, Rate, RANK_ADMIN, RANK_HALFOP, RANK_OP, @@ -661,7 +656,7 @@ impl ChanMode for ListMode { } } -// --- +z secure-only (InspIRCd m_sslmodes) ----------------------------------- +// --- +z secure-only --------------------------------------------------------- /// `+z` — only TLS-connected users may join. It can only be *set* when every /// current member is already on TLS (else `ERR_ALLMUSTSSL`); the join-time block @@ -884,8 +879,8 @@ impl ChanMode for RedirectMode { } } -/// +B `` — reject channel messages that are at least `` uppercase -/// (InspIRCd `m_anticaps`). Enforced in the message path; ops are exempt. +/// +B `` — reject channel messages that are at least `` uppercase. +/// Enforced in the message path; ops are exempt. struct AntiCapsMode; static ANTICAPS: AntiCapsMode = AntiCapsMode; impl ChanMode for AntiCapsMode { @@ -922,8 +917,8 @@ impl ChanMode for AntiCapsMode { } } -/// +J `` — after being kicked, a user can't rejoin for `` seconds -/// (InspIRCd `m_kicknorejoin`). Enforced in `Server::join`. +/// +J `` — after being kicked, a user can't rejoin for `` seconds. +/// Enforced in `Server::join`. struct KickNoRejoinMode; static KICKNOREJOIN: KickNoRejoinMode = KickNoRejoinMode; impl ChanMode for KickNoRejoinMode { @@ -963,8 +958,8 @@ impl ChanMode for KickNoRejoinMode { } } -/// +d `` — a newly-joined member can't speak for `` seconds (InspIRCd -/// `m_delaymsg`). Enforced in the message path; voiced-or-above are exempt. +/// +d `` — a newly-joined member can't speak for `` seconds. +/// Enforced in the message path; voiced-or-above are exempt. struct DelayMsgMode; static DELAYMSG: DelayMsgMode = DelayMsgMode; impl ChanMode for DelayMsgMode { @@ -1005,7 +1000,7 @@ impl ChanMode for DelayMsgMode { } /// +K `` — block a message identical to one of the sender's previous `` -/// lines in this channel (InspIRCd `m_repeat`, simplified). Ops are exempt. +/// lines in this channel. Ops are exempt. struct RepeatMode; static REPEAT: RepeatMode = RepeatMode; impl ChanMode for RepeatMode { @@ -1047,9 +1042,8 @@ impl ChanMode for RepeatMode { // === user modes ============================================================ -/// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`], and the -/// same win over the C++ mode system: an unbounded slice of zero-sized -/// `&'static` handlers, no bitmask cap, no allocation, no `unsafe`. +/// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`]: an +/// unbounded slice of zero-sized `&'static` handlers. pub trait UserMode: Sync { fn letter(&self) -> char; /// Apply `+`/`-` to the user; return `true` if it took effect (echo it). @@ -1188,12 +1182,15 @@ impl UserMode for OperMode { if adding { return false; // never self-granted } + if s.users.get(&uid).is_none() { + return false; + } if let Some(u) = s.users.get_mut(&uid) { u.flags.oper = false; - true - } else { - false } + // operprefix: drop the ! prefix in every channel now that they're not staff + crate::modules::operprefix::clear_all(s, uid); + true } } diff --git a/src/module.rs b/src/module.rs index 0d5cb16..e2c1ae7 100644 --- a/src/module.rs +++ b/src/module.rs @@ -1,12 +1,9 @@ -//! The module API — echoIRCd's answer to InspIRCd's `Module` class. -//! -//! Modules hook lifecycle events. "Pre" hooks return a [`ModResult`] and can -//! **deny** an action; "notify" hooks are informational. The core fires pre-hooks -//! inline (so a `Deny` actually blocks) and notify-hooks from a queue after the -//! triggering command finishes — so a handler can emit an event without ever -//! touching the module list. All hooks get `&mut Server`, so a module can act -//! (send lines, force a join, …), exactly like an InspIRCd module gets the -//! `ServerInstance`. +//! Module API: modules hook lifecycle events. "Pre" hooks return a [`ModResult`] +//! and can **deny** an action; "notify" hooks are informational. The core fires +//! pre-hooks inline (so a `Deny` actually blocks) and notify-hooks from a queue +//! after the triggering command finishes — so a handler can emit an event without +//! ever touching the module list. All hooks get `&mut Server`, so a module can act +//! (send lines, force a join, …). use crate::server::Server; use crate::Uid; diff --git a/src/modules/account_registration.rs b/src/modules/account_registration.rs index ab0a8d0..fbfafee 100644 --- a/src/modules/account_registration.rs +++ b/src/modules/account_registration.rs @@ -1,14 +1,11 @@ -//! account_registration — IRCv3 `draft/account-registration` (the `REGISTER` / -//! `VERIFY` commands and the cap that advertises them), bridged to a configurable -//! HTTP accounts API. reverse's own module: the old Swaygo Django backend is gone, -//! so this talks to whatever `acctregister_registerurl` / `_verifyurl` you point it -//! at, POSTing form-encoded fields with an `X-API-Key` header. +//! account_registration — IRCv3 `draft/account-registration` (`REGISTER` / `VERIFY` +//! commands and the cap advertising them), bridged to a configurable HTTP accounts +//! API. POSTs form-encoded fields with an `X-API-Key` header. //! //! The API call runs on a worker thread (`Server::spawn_http`) and its result comes //! back as `Event::HttpResult` → [`on_http_result`], so a slow endpoint never blocks //! the core. On success (and when `acctregister_autologin`) the user is logged into -//! the new account. Everything is config-driven; the only per-IP state (rate limit) -//! lives in `Server.ext`. +//! the new account. Per-IP rate-limit state lives in `Server.ext`. //! //! Config (all under flat keys): //! account_registration = yes enable diff --git a/src/modules/antimixedutf8.rs b/src/modules/antimixedutf8.rs index c213905..b2957db 100644 --- a/src/modules/antimixedutf8.rs +++ b/src/modules/antimixedutf8.rs @@ -1,18 +1,12 @@ //! antimixedutf8 — blocks spam that mixes Unicode scripts within words (Latin -//! letters swapped for Cyrillic/Greek look-alikes: "FᏒee Ⅴ1аgrа"), a very common -//! obfuscation. This is reverse's own detection model — the scoring rules and the -//! confusable / fancy-Latin / zero-width tables — implemented from scratch in -//! native Rust. (The *tables and weights* are the detector's spec: they define -//! what counts as spam. Everything around them is original echoIRCd code.) +//! letters swapped for Cyrillic/Greek look-alikes: "FᏒee Ⅴ1аgrа"). The scoring +//! rules and the confusable / fancy-Latin / zero-width tables define what counts as +//! spam. //! //! Per word: letters from more than one script score; so do words that are ASCII //! mixed with Latin-confusable letters, words built almost entirely of confusables, //! and "fancy" styled-Latin words. Zero-width chars score too. At/above the //! configured threshold the action fires (block | kill | gline | kline | zline). -//! -//! Rust strings are valid UTF-8, so we walk codepoints straight from `chars()` and -//! fold the per-word state through a `Scorer` — idiomatic Rust, no manual UTF-8 -//! decoding and no ref-capturing lambdas. use crate::module::{ModResult, Module}; use crate::server::Server; @@ -47,9 +41,9 @@ fn classify_script(cp: u32) -> Script { } } -/// A non-Latin letter that LOOKS like an ASCII Latin letter (the homoglyphs -/// spammers swap in). Catches pure-homoglyph words that script-mixing misses, -/// without tripping on genuine monolingual text. +/// A non-Latin letter that looks like an ASCII Latin letter (a homoglyph). Catches +/// pure-homoglyph words that script-mixing misses, without tripping on genuine +/// monolingual text. fn is_latin_confusable(cp: u32) -> bool { matches!( cp, @@ -86,7 +80,7 @@ fn is_invisible(cp: u32) -> bool { ) } -/// Per-message tally, folded word by word: the per-word scoring state as a struct. +/// Per-message tally, folded word by word. #[derive(Default)] struct Scorer { mixedwords: u32, // words mixing >1 real script @@ -119,9 +113,8 @@ impl Scorer { } fn end_word(&mut self) { - // Confusable mixed WITH real ASCII in one word = the classic "swap a few - // letters" attack (already script-mixing) — count it ONCE here so a single - // stray homoglyph doesn't double-score. + // Confusable mixed with real ASCII in one word: count once here so a single + // stray homoglyph doesn't also score as script-mixing. if self.word_has_confusable && self.word_has_ascii { self.homoglyphwords += 1; } else if self.word_scripts() >= 2 { @@ -283,17 +276,15 @@ impl Module for AntiMixedUtf8 { u.addr.ip().to_string(), ) }; - // Show opers WHAT was blocked (a sanitized snippet) so they can judge the - // catch and spot false positives — the whole point of an antispam log. + // Snotice a sanitized snippet so opers can judge the catch / spot false positives. srv.snotice(&format!( "ANTIMIXEDUTF8: blocked spam from {mask} to {target} (score {score} >= {}): {}", srv.amu.threshold, snippet(body) )); - // Always tell the sender their message was blocked and that opers were - // told — even for punitive actions, since the writer flushes queued lines - // before a disconnect. + // Notify the sender even for punitive actions: the writer flushes queued + // lines before a disconnect. srv.send( uid, format!( diff --git a/src/modules/antirandom.rs b/src/modules/antirandom.rs index 5a9b53a..9069cae 100644 --- a/src/modules/antirandom.rs +++ b/src/modules/antirandom.rs @@ -1,8 +1,5 @@ //! antirandom — detect spam drones whose nick/ident/realname is random-looking, -//! by scoring character patterns and acting when a threshold is crossed. This is -//! reverse's own detection model (the run rules + the unlikely-trigram penalty -//! table are the spec — they define what counts as random); everything around -//! them is original native Rust. +//! by scoring character patterns and acting when a threshold is crossed. //! //! Score, summed over nick (+ ident + realname when `checkfull`): //! - a run reaching 5 digits / 4 vowels / 4 consonants adds that length; each @@ -11,7 +8,7 @@ //! //! At/above `antirandom_threshold` the action fires: kill | gline | kline | //! zline | block. Opers and logged-in accounts are always exempt. Off unless -//! `antirandom = yes` — read entirely from the config, nothing on `Server`. +//! `antirandom = yes`. use crate::module::{ModResult, Module}; use crate::server::Server; @@ -19,7 +16,7 @@ use crate::xline::XKind; use crate::Uid; /// Adjacent letter pairs that rarely occur in real words but pepper random -/// strings — each occurrence adds 1 to the score. reverse's high-signal subset. +/// strings — each occurrence adds 1 to the score. const TRIPLES: &[&[u8; 2]] = &[ b"aj", b"aq", b"av", b"aw", b"ax", b"az", b"bd", b"bg", b"bk", b"bq", b"bx", b"bz", b"cb", b"cf", b"cg", b"cj", b"cp", b"cv", b"cw", b"cx", b"dx", b"fb", b"fc", b"fg", b"fh", b"fj", diff --git a/src/modules/autodrop.rs b/src/modules/autodrop.rs index 25bfa9d..74a58cd 100644 --- a/src/modules/autodrop.rs +++ b/src/modules/autodrop.rs @@ -6,8 +6,6 @@ //! ```text //! autodrop_commands = GET POST HEAD CONNECT PUT DELETE OPTIONS TRACE PATCH //! ``` -//! -//! Reference: InspIRCd's `m_autodrop`. Original native Rust. use crate::module::{ModResult, Module}; use crate::server::Server; diff --git a/src/modules/autoop.rs b/src/modules/autoop.rs index 83d2e4b..4a062ef 100644 --- a/src/modules/autoop.rs +++ b/src/modules/autoop.rs @@ -1,8 +1,7 @@ //! autoop — the channel list mode `+w :` grants a status prefix //! to matching users the moment they join, e.g. `+w o:*!*@trusted.host` auto-ops //! them, `+w v:*!*@*.friend` auto-voices. The list lives on the channel (like +b, -//! stored verbatim); this module applies it on join via the server-authority mode -//! path. Reference: InspIRCd's `m_autoop`. Original native Rust. +//! stored verbatim) and is applied on join via the server-authority mode path. use crate::channels::glob_match; use crate::module::Module; diff --git a/src/modules/banredirect.rs b/src/modules/banredirect.rs index c502553..58683a0 100644 --- a/src/modules/banredirect.rs +++ b/src/modules/banredirect.rs @@ -1,8 +1,7 @@ //! banredirect — a ban of the form `+b $<#channel>` bounces a matching, //! banned user into `#channel` instead of refusing them outright. The redirect //! fires at most once, guarded by `Server.in_redirect` (shared with the `+L` -//! full-channel redirect), so it can never loop. Reference: InspIRCd's -//! `m_banredirect`. Original native Rust. +//! full-channel redirect), so it can never loop. use crate::channels::glob_match; use crate::server::Server; diff --git a/src/modules/blockamsg.rs b/src/modules/blockamsg.rs index dd13300..1aa0402 100644 --- a/src/modules/blockamsg.rs +++ b/src/modules/blockamsg.rs @@ -1,12 +1,10 @@ -//! blockamsg — block the mass "/amsg" and "/ame" commands that mIRC/HexChat send -//! (one message fanned out to every channel you're on), a classic advertise/flood -//! vector. A PRIVMSG/NOTICE whose target list is two-or-more channels is blocked -//! when either: the same text was just sent to a *different* target list within -//! `blockamsg_delay` seconds, or the number of channel targets equals the number -//! of channels the sender is on (>1). Off unless `blockamsg = yes`. Per-user -//! last-message bookkeeping lives in `Server.ext`; nothing on `Server`. -//! -//! Behaviour reference: InspIRCd's `m_blockamsg`. Original native Rust. +//! blockamsg — block the mass "/amsg" and "/ame" commands (one message fanned out +//! to every channel the sender is on), a classic advertise/flood vector. A +//! PRIVMSG/NOTICE whose target list is two-or-more channels is blocked when either: +//! the same text was just sent to a *different* target list within `blockamsg_delay` +//! seconds, or the number of channel targets equals the number of channels the +//! sender is on (>1). Off unless `blockamsg = yes`. Per-user last-message state +//! lives in `Server.ext`. use std::collections::HashMap; @@ -64,7 +62,7 @@ impl Module for BlockAmsg { let store = srv.ext.get_or_insert_with::(LastMsg::default); let prev = store.0.get(&uid).cloned(); - // record this message for next time (identical to InspIRCd: always update) + // record this message for next time (always update) store.0.insert(uid, (text.clone(), list.clone(), n)); let repeat_hit = prev diff --git a/src/modules/chanlog.rs b/src/modules/chanlog.rs index 3ea7de7..baa3f74 100644 --- a/src/modules/chanlog.rs +++ b/src/modules/chanlog.rs @@ -1,7 +1,6 @@ //! chanlog — mirror server notices (the `snotice` stream opers see with +s) into a //! channel, so staff can watch the log in a normal channel window. Off unless -//! `chanlog = #channel` is configured. Reference: InspIRCd's `m_chanlog`. -//! Original native Rust. +//! `chanlog = #channel` is configured. use crate::server::Server; use crate::Uid; diff --git a/src/modules/channames.rs b/src/modules/channames.rs index dbf4589..ed193f4 100644 --- a/src/modules/channames.rs +++ b/src/modules/channames.rs @@ -3,8 +3,6 @@ //! control codes or fancy Unicode an admin doesn't want in channel names). Existing //! channels are unaffected. Off unless `channames_deny` is set. Dispatched from //! `Server::join`. -//! -//! Behaviour reference: InspIRCd's `m_channames`. Original native Rust. use crate::numeric::ERR_BADCHANNEL; use crate::server::Server; diff --git a/src/modules/channelban.rs b/src/modules/channelban.rs index d07f324..21e110c 100644 --- a/src/modules/channelban.rs +++ b/src/modules/channelban.rs @@ -2,9 +2,7 @@ //! are in. `+b j:#lobby` bans everyone who is also in `#lobby`; an optional status //! prefix narrows it to members at/above that rank, e.g. `+b j:@#staff` matches //! only ops-or-higher in `#staff`. The channel part is a glob. Dispatched from the -//! channel ban matcher; the logic lives here in its own file. -//! -//! Behaviour reference: InspIRCd's `m_channelban`. Original native Rust. +//! channel ban matcher. use crate::channels::{glob_match, RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE}; use crate::server::Server; diff --git a/src/modules/chathistory.rs b/src/modules/chathistory.rs index fa01ce8..b1f26cf 100644 --- a/src/modules/chathistory.rs +++ b/src/modules/chathistory.rs @@ -1,9 +1,9 @@ -//! chathistory — InspIRCd's `m_chathistory` family (draft/chathistory + -//! draft/message-redaction). Recent PRIVMSG/NOTICE traffic is kept in a capped -//! per-conversation ring (channels and DM pairs) so clients can replay it on -//! demand or on join (the channel `+H` backlog lives in `channels::replay_chanhistory`). -//! Self-contained: the ring lives in `Server.ext`; the message path records into it -//! via [`record`], and the CHATHISTORY and REDACT commands read/edit it here. +//! chathistory — draft/chathistory + draft/message-redaction. Recent PRIVMSG/NOTICE +//! traffic is kept in a capped per-conversation ring (channels and DM pairs) so +//! clients can replay it on demand or on join (the channel `+H` backlog lives in +//! `channels::replay_chanhistory`). The ring lives in `Server.ext`; the message path +//! records into it via [`record`], and the CHATHISTORY and REDACT commands +//! read/edit it here. use std::collections::{HashMap, VecDeque}; diff --git a/src/modules/cloak.rs b/src/modules/cloak.rs index 505409b..a7dcade 100644 --- a/src/modules/cloak.rs +++ b/src/modules/cloak.rs @@ -1,27 +1,19 @@ -//! cloak — echoIRCd's host-masking module (InspIRCd's `m_cloak_*`, our way). +//! cloak: keyed host masking under user mode +x, auto-set on connect (only opers +//! may drop it). Config key `cloak_key`; with no key set, cloaking is off and +x +//! is a no-op. //! -//! Every user gets a deterministic, keyed **cloak** of their host that hides the -//! real IP while *preserving subnet structure*, so a channel ban on a whole /24 -//! or /16 still bites. The cloak is shown under user mode **+x**, which this -//! module auto-sets on connect; only opers may drop it (see [`crate::mode`]), -//! which stops +x from becoming a ban-evasion switch. -//! -//! Format follows InspIRCd's `SegmentIP`: one hashed segment per cumulative IP -//! octet-prefix, most-specific on the left, ending in the literal `.IP` suffix -//! that marks a cloaked address (as opposed to a cloaked hostname, which keeps -//! its domain). For `a.b.c.d`: +//! A cloak hides the real IP while preserving subnet structure, so a channel ban +//! on a /24 or /16 still matches. IPv4 `a.b.c.d` becomes one keyed segment per +//! octet-prefix tier, most-specific first, with a literal `.IP` suffix marking a +//! cloaked address (a cloaked hostname keeps its domain instead): //! //! ```text //! HASH(a.b.c.d) . HASH(a.b.c) . HASH(a.b) . HASH(a) . IP //! (/32) (/24) (/16) (/8) //! ``` //! -//! so two IPs in the same /24 share the `…​/24./16./8.IP` tail (same /16 shares -//! `…​/16./8.IP`), and the exact address never leaks. Where this improves on the -//! C++ original: the hash is **SHA-256** (via the `openssl` we already link for -//! TLS) instead of MD5, it needs no separate hashing module, and the whole path -//! stays `#![forbid(unsafe_code)]`. The key lives in the config (`cloak_key = …`); -//! with no key set, cloaking is simply off and +x is a no-op. +//! Two IPs in the same /24 share the `…/24./16./8.IP` tail; the exact address +//! never appears. The hash is SHA-256 (via the openssl already linked for TLS). use openssl::sha::sha256; @@ -29,7 +21,7 @@ use crate::module::Module; use crate::server::Server; use crate::Uid; -/// The suffix marking a cloaked IP address (InspIRCd's default is `.IP` too). +/// The suffix marking a cloaked IP address. const IP_SUFFIX: &str = ".IP"; pub struct Cloak; @@ -42,7 +34,7 @@ impl Module for Cloak { /// Compute the cloak once, at connect, and cloak the user by default (+x). fn on_user_connect(&mut self, srv: &mut Server, uid: Uid) { let Some(key) = srv.cloak_key.clone() else { - return; // no cloak key configured -> cloaking disabled + return; // no key configured: cloaking disabled }; let Some(host) = srv.users.get(&uid).map(|u| u.host.clone()) else { return; @@ -87,7 +79,7 @@ fn parse_v4(host: &str) -> Option<(u8, u8, u8, u8)> { /// - IPv4 `a.b.c.d` → `H(a.b.c.d).H(a.b.c).H(a.b).H(a).IP` — one keyed segment per /// octet-prefix tier (/32 · /24 · /16 · /8), so subnet bans keep working while /// the exact address never appears. -/// - IPv6 → `ALPHA.BETA.GAMMA.IP` (mirrors InspIRCd), coarsened by hextet groups. +/// - IPv6 → `ALPHA.BETA.GAMMA.IP`, coarsened by hextet groups. /// - hostname → keep the last two labels (the domain), mask everything to the left /// (no `.IP` — a resolved name isn't a raw address). pub fn cloak_host(key: &str, host: &str) -> String { @@ -125,7 +117,7 @@ mod tests { let c = cloak_host("secret", "203.0.113.7"); assert_eq!(c, cloak_host("secret", "203.0.113.7")); // stable assert!(!c.contains("203.0.113")); // the dotted IP never appears - assert!(c.ends_with(".IP")); // InspIRCd-style IP suffix + assert!(c.ends_with(".IP")); // IP suffix assert_eq!(c.split('.').count(), 5); // H32.H24.H16.H8.IP } diff --git a/src/modules/cloudflare_challenge.rs b/src/modules/cloudflare_challenge.rs index d095532..a79a7c9 100644 --- a/src/modules/cloudflare_challenge.rs +++ b/src/modules/cloudflare_challenge.rs @@ -1,12 +1,10 @@ -//! cloudflare_challenge — gate registration behind a Cloudflare Turnstile-style -//! challenge, verified by an IP-bound HS256 JWT. Structurally like [`recaptcha`] -//! but keyed on `cloudflare_*` config and driven by the `VERIFYCHALLENGE ` -//! command. reverse's own module; runs in JWT-only mode (a validly-signed, -//! unexpired, IP-matching token is proof — no backend call needed). +//! cloudflare_challenge: gate registration behind a challenge verified by an +//! IP-bound HS256 JWT, presented via the `VERIFYCHALLENGE ` command. A +//! validly-signed, unexpired, IP-matching token is sufficient proof; no backend +//! call is made. //! //! Off unless `cloudflare_challenge = yes` with `cloudflare_secret` + -//! `cloudflare_url` set. Config-driven; the passed-verification set lives in -//! `Server.ext`. +//! `cloudflare_url` set. The passed-verification set lives in `Server.ext`. use std::collections::HashSet; diff --git a/src/modules/conn_waitpong.rs b/src/modules/conn_waitpong.rs index 45107f2..607d5e6 100644 --- a/src/modules/conn_waitpong.rs +++ b/src/modules/conn_waitpong.rs @@ -6,9 +6,8 @@ //! conn_waitpong_killonbadreply = yes # disconnect on a wrong pong (default: keep waiting) //! ``` //! -//! The gate itself is the core `User.waitpong` field (checked in `try_register`, -//! like `dns_pending`); this module just arms it at connect and clears it on the -//! matching PONG. Behaviour reference: InspIRCd's `m_conn_waitpong`. Native Rust. +//! The gate is the core `User.waitpong` field (checked in `try_register`); this +//! module arms it at connect and clears it on the matching PONG. use crate::server::Server; use crate::Uid; @@ -37,7 +36,7 @@ pub fn arm(s: &mut Server, uid: Uid) { /// the client (else keep waiting — a real client will retry on the next PING). pub fn on_pong(s: &mut Server, uid: Uid, params: &[String]) { let Some(want) = s.users.get(&uid).and_then(|u| u.waitpong.clone()) else { - return; // not waiting (already satisfied, or feature off) + return; // not waiting: already satisfied, or feature off }; let got = params.last().map(String::as_str).unwrap_or(""); if got == want { diff --git a/src/modules/connectban.rs b/src/modules/connectban.rs index 6f31f5e..1008c20 100644 --- a/src/modules/connectban.rs +++ b/src/modules/connectban.rs @@ -1,16 +1,12 @@ -//! connectban — z-line an IP range that opens an excessive number of connections -//! to the server. Each connection bumps a per-range counter; when it reaches -//! `connectban_threshold` the range is z-lined for `connectban_duration` and the -//! counter cleared. The whole tally is periodically wiped (`connectban_gcinterval`) -//! so long-lived counts don't accumulate — mirroring InspIRCd's garbage-collect. -//! A `connectban_bootwait` grace after start avoids banning the reconnect storm -//! when the server (re)starts. Off unless `connectban = yes`; all state in -//! `Server.ext`, nothing on `Server`. +//! connectban: z-line an IP range that opens too many connections. Each connection +//! bumps a per-range counter; at `connectban_threshold` the range is z-lined for +//! `connectban_duration` and the counter cleared. The tally is periodically wiped +//! (`connectban_gcinterval`) so long-lived counts don't accumulate. A +//! `connectban_bootwait` grace after start avoids banning the restart reconnect +//! storm. Off unless `connectban = yes`; all state in `Server.ext`. //! -//! Because echoIRCd z-lines match by glob (not CIDR), the banned range is emitted -//! as a wildcard mask (`1.2.3.*` for an IPv4 /24, the exact IP for a /32). -//! -//! Behaviour reference: InspIRCd's `m_connectban`. Original native Rust. +//! z-lines match by glob, not CIDR, so the banned range is emitted as a wildcard +//! mask (`1.2.3.*` for an IPv4 /24, the exact IP for a /32). use std::collections::HashMap; use std::net::IpAddr; @@ -116,7 +112,7 @@ pub fn on_connect(s: &mut Server, ip: IpAddr) { )); } -/// Periodically clears the whole tally, like InspIRCd's garbage collector. +/// Periodically clears the whole tally. pub struct ConnectBan; impl Module for ConnectBan { fn name(&self) -> &'static str { diff --git a/src/modules/connflood.rs b/src/modules/connflood.rs index 0c003b0..13194ea 100644 --- a/src/modules/connflood.rs +++ b/src/modules/connflood.rs @@ -1,6 +1,6 @@ -//! connflood — InspIRCd `m_connflood`. Refuse connections from an IP opening too -//! many too fast. Config: `connflood = `. Per-IP recent-connect times -//! live in `Server.ext`, pruned on the tick — nothing lives on `Server`. +//! connflood: refuse connections from an IP opening too many too fast. Config: +//! `connflood = `. Per-IP recent-connect times live in `Server.ext`, +//! pruned on the tick. use std::collections::HashMap; use std::net::IpAddr; diff --git a/src/modules/customtitle.rs b/src/modules/customtitle.rs index 5ce523b..2dfdb16 100644 --- a/src/modules/customtitle.rs +++ b/src/modules/customtitle.rs @@ -1,15 +1,13 @@ -//! customtitle — `TITLE ` lets a user claim a configured vanity -//! title (shown in their WHOIS) and, optionally, a matching vhost — a lightweight -//! "mini-oper" identity without operator privileges. Config, one block per title: +//! customtitle: `TITLE ` lets a user claim a configured title +//! (shown in their WHOIS) and, optionally, a matching vhost. Config, one block per +//! title: //! //! ```text //! customtitle = //! ``` //! -//! The password is checked via [`crate::modules::password_hash`] so it may be +//! The password is checked via [`crate::modules::password_hash`], so it may be //! plaintext or a hash. The claimed title lives in the user's `ext`. -//! -//! Behaviour reference: InspIRCd's `m_customtitle`. Original native Rust. use crate::command::{CmdResult, Command}; use crate::modules::password_hash; diff --git a/src/modules/dccallow.rs b/src/modules/dccallow.rs index bbee118..9582606 100644 --- a/src/modules/dccallow.rs +++ b/src/modules/dccallow.rs @@ -1,6 +1,6 @@ -//! dccallow — block unwanted DCC file transfers (and optionally DCC CHAT) unless -//! the recipient has explicitly allowed the sender with `/DCCALLOW +<nick>`. -//! Mirrors InspIRCd's `m_dccallow`. Config (all optional, all runtime-read): +//! dccallow: block DCC SEND matching configured filename globs (and, with +//! `dccallow_blockchat`, DCC CHAT) unless the recipient has allowed the sender +//! with `/DCCALLOW +<nick>`. Config (all optional, all runtime-read): //! //! ```text //! dccallow_blockfile = *.exe # repeatable: filename globs to block on DCC SEND @@ -9,8 +9,7 @@ //! dccallow_maxentries = 20 # per-user allow-list cap (default 20) //! ``` //! -//! A user's allow-list ("nicks I permit to DCC me") lives on their `User.ext`, so -//! it vanishes cleanly when they quit. Original native Rust. +//! A user's allow-list lives on their `User.ext`, so it vanishes when they quit. use crate::channels::glob_match; use crate::command::{CmdResult, Command}; diff --git a/src/modules/denychans.rs b/src/modules/denychans.rs index e83ff5a..72d4e9f 100644 --- a/src/modules/denychans.rs +++ b/src/modules/denychans.rs @@ -1,5 +1,5 @@ -//! denychans — forbid joining channels whose name matches a `badchan` glob, with -//! an optional redirect to a safe channel and an `allowopers` bypass. A `goodchan` +//! denychans: forbid joining channels whose name matches a `badchan` glob, with an +//! optional redirect to a safe channel and an `allowopers` bypass. A `goodchan` //! glob whitelists names back out of a broad `badchan` pattern. Config: //! //! ```text @@ -7,11 +7,8 @@ //! goodchan = #evilgenius //! ``` //! -//! Dispatched straight from `Server::join` (like the CBAN check), so it works -//! per-channel even when several are joined at once. All config-driven; nothing -//! lives on `Server`. -//! -//! Behaviour reference: InspIRCd's `m_denychans`. Original native Rust. +//! Dispatched from `Server::join`, so it applies per-channel even when several are +//! joined at once. use crate::channels::glob_match; use crate::numeric::{ERR_BADCHANNEL, ERR_LINKCHANNEL}; @@ -26,8 +23,7 @@ struct BadChan { allowopers: bool, } -/// Reuse the quoted-attribute tokenizer shape: split on whitespace but keep -/// `key="quoted value"` together. +/// Split on whitespace but keep `key="quoted value"` together. fn tokenize(line: &str) -> Vec<String> { let mut out = Vec::new(); let mut cur = String::new(); @@ -98,8 +94,8 @@ fn is_good(s: &Server, name: &str) -> bool { } /// Called from `Server::join`. Returns `true` when the join to `name` should be -/// blocked (the caller returns without joining); emits the numeric and performs a -/// redirect join if configured. `is_oper` lets an `allowopers` badchan through. +/// blocked; emits the numeric and performs a redirect join if configured. +/// `is_oper` lets an `allowopers` badchan through. pub fn intercept(s: &mut Server, uid: Uid, name: &str, is_oper: bool) -> bool { if s.conf_all("badchan").is_empty() { return false; diff --git a/src/modules/disable.rs b/src/modules/disable.rs index f69a03a..5ce3ee4 100644 --- a/src/modules/disable.rs +++ b/src/modules/disable.rs @@ -1,8 +1,6 @@ -//! disable — refuse a configured set of commands to ordinary users (opers bypass). +//! disable: refuse a configured set of commands to ordinary users (opers bypass). //! `disabled_commands = LIST WHO KNOCK` (space-separated; repeatable). A disabled //! command replies with `421` as if it didn't exist. Off unless configured. -//! -//! Behaviour reference: InspIRCd's `m_disable`. Original native Rust. use crate::module::{ModResult, Module}; use crate::numeric::ERR_UNKNOWNCOMMAND; diff --git a/src/modules/dnsbl.rs b/src/modules/dnsbl.rs index d97b378..f44eef3 100644 --- a/src/modules/dnsbl.rs +++ b/src/modules/dnsbl.rs @@ -1,15 +1,13 @@ -//! DNSBL — DNS blocklist checks on connect, InspIRCd `m_dnsbl` style. On connect -//! the resolver thread reverses the client's IP under each configured blocklist -//! zone and A-looks it up (see [`crate::resolver`]); a listing triggers the -//! configured action. Works for IPv4 **and** IPv6 (v4 reversed octets or v6 -//! reversed nibbles under the zone) — a v4-only blocklist simply NXDOMAINs a v6 -//! query, which reads as "not listed". +//! DNSBL: DNS blocklist checks on connect. The resolver thread reverses the +//! client's IP under each configured blocklist zone and A-looks it up (see +//! [`crate::resolver`]); a listing triggers the configured action. Works for IPv4 +//! (reversed octets) and IPv6 (reversed nibbles); a v4-only blocklist NXDOMAINs a +//! v6 query, which reads as "not listed". //! -//! Actions (`dnsbl_action`): `mark` just shows the notice and lets them in -//! (default, safe), `kill` disconnects, `kline`/`gline`/`zline` add a 1-day ban -//! and disconnect. This isn't a hook `Module` — it's driven from the connection -//! lifecycle (`Server::add_conn` → `on_resolved`) — but it lives here as its own -//! self-contained unit. +//! Actions (`dnsbl_action`): `mark` shows the notice and lets them in (default), +//! `kill` disconnects, `kline`/`gline`/`zline` add a 1-day ban and disconnect. +//! Driven from the connection lifecycle (`Server::add_conn` → `on_resolved`) +//! rather than as a hook `Module`. use std::net::{IpAddr, Ipv4Addr}; use std::time::Duration; diff --git a/src/modules/extbanbanlist.rs b/src/modules/extbanbanlist.rs index f6f520f..35efc3a 100644 --- a/src/modules/extbanbanlist.rs +++ b/src/modules/extbanbanlist.rs @@ -1,11 +1,10 @@ -//! extbanbanlist — the matching extban `b:<#channel>`: a user is caught if they are -//! on `#channel`'s ban list. Lets one channel share (borrow) another's bans, e.g. -//! `+b b:#staff` bans everyone banned in #staff. Reference: InspIRCd's -//! `m_extbanbanlist`. Original native Rust. +//! extbanbanlist: matching extban `b:<#channel>` catches a user if they are on +//! `#channel`'s ban list, letting one channel borrow another's bans (e.g. +//! `+b b:#staff` bans everyone banned in #staff). //! -//! The match is deliberately *non-recursive* — it only tests the referenced -//! channel's plain host-mask bans (and its plain excepts), never that channel's own -//! extbans, so two channels referencing each other can't loop. +//! The match is deliberately non-recursive: it tests only the referenced channel's +//! plain host-mask bans (and its plain excepts), never that channel's own extbans, +//! so two channels referencing each other can't loop. use crate::channels::glob_match; use crate::server::Server; diff --git a/src/modules/extended_isupport.rs b/src/modules/extended_isupport.rs index 4abf1db..733ec1d 100644 --- a/src/modules/extended_isupport.rs +++ b/src/modules/extended_isupport.rs @@ -1,11 +1,7 @@ -//! extended_isupport — the `draft/extended-isupport` capability. reverse's own -//! module. Normally ISUPPORT (005) is a one-shot at registration; with this cap a -//! client can send the `ISUPPORT` command any time to re-request the current tokens -//! (handy after a rehash changes them). If the client also has `batch`, the reply -//! is wrapped in a `draft/isupport` BATCH so the multi-line set arrives atomically — -//! the emission itself lives in `Server::send_isupport`, shared with the welcome burst. -//! -//! Behaviour reference: reverse's InspIRCd `m_ircv3_extended_isupport`. Original native Rust. +//! `draft/extended-isupport` capability: lets a client re-request the current +//! ISUPPORT (005) tokens at any time via the `ISUPPORT` command. With the `batch` +//! cap the reply is wrapped in a `draft/isupport` BATCH so the set arrives atomically. +//! Emission lives in `Server::send_isupport`, shared with the welcome burst. use crate::command::{CmdResult, Command}; use crate::numeric::ERR_UNKNOWNCOMMAND; diff --git a/src/modules/extjwt.rs b/src/modules/extjwt.rs index 0ad18db..8aac442 100644 --- a/src/modules/extjwt.rs +++ b/src/modules/extjwt.rs @@ -1,8 +1,6 @@ -//! ircv3_extjwt — the `EXTJWT` command: hand a client a short-lived, server-signed -//! JWT it can present to an *external* service (a web app, file host, …) to prove -//! "this IRC user, with these modes, in this channel, right now". The service -//! trusts the token because it shares the HS256 secret. Uses the native -//! [`crate::modules::jwt`] signer. +//! `EXTJWT` command: issues a short-lived, server-signed HS256 JWT a client can +//! present to an external service to prove its IRC identity, modes and channel +//! membership. Uses the [`crate::modules::jwt`] signer. //! //! `EXTJWT *|<channel> [<service>]` → one or more //! `:<server> EXTJWT <target> <service> [*] <chunk>` lines (a `*` param before the @@ -11,8 +9,6 @@ //! //! Config: `extjwt_secret` (+ `extjwt_duration`, default 30s); optional named //! services via `extjwt_service = <name> <secret> [duration]`. Off with no secret. -//! -//! Behaviour reference: InspIRCd's `m_ircv3_extjwt`. Original native Rust. use crate::command::{CmdResult, Command}; use crate::modules::jwt; diff --git a/src/modules/filehost.rs b/src/modules/filehost.rs index 5bb13c0..5a9c796 100644 --- a/src/modules/filehost.rs +++ b/src/modules/filehost.rs @@ -1,24 +1,19 @@ -//! filehost — the DRAFT `reverse.im/filehost` IRCv3 extension. reverse's own -//! module: it advertises an external file-hosting service to clients and hands a -//! logged-in user a short-lived, server-signed JWT upload link (so the web uploader -//! trusts them without a second login — pairs with reverse's rubot upload service). +//! `reverse.im/filehost` (draft) IRCv3 extension: advertises an external +//! file-hosting service and hands a logged-in user a short-lived, server-signed JWT +//! upload link so the web uploader trusts them without a second login. //! //! Surfaces: -//! * ISUPPORT `reverse.im/FILEHOST=<website>` + the `reverse.im/filehost` cap -//! (so a client knows the service exists and can show an upload button). +//! * ISUPPORT `reverse.im/FILEHOST=<website>` + the `reverse.im/filehost` cap. //! * `FILEHOST [info]` — login-gated; replies with `<website>/upload?token=<jwt>` //! and usage info. //! * a `reverse.im/filehost` message tag carrying JSON metadata (url/filename/ -//! type) attached to any message that contains a `<website>/files/…` link, so -//! clients render the file inline. Scoped to the message's recipients (not the -//! whole network — cleaner than the reference's broadcast). +//! type) attached to any message containing a `<website>/files/…` link, so +//! clients render the file inline. Scoped to the message's recipients. //! * `filehost_requiressl`: refuse to relay a filehost link from a plaintext user. //! //! Config: `filehost_website` (enables it) `filehost_jwt_secret` `filehost_jwt_issuer` //! (default FILEHOST) `filehost_token_expiry` (secs, default 3600) `filehost_requiressl` //! (default yes) `filehost_auth_message`. -//! -//! Behaviour reference: reverse's InspIRCd `m_ircv3_FILEHOST`. Original native Rust. use crate::command::{CmdResult, Command}; use crate::module::{ModResult, Module}; @@ -38,7 +33,7 @@ pub fn isupport(s: &Server) -> Option<String> { website(s).map(|w| format!("reverse.im/FILEHOST={w}")) } -/// File category from a filename extension (mirrors the reference's set). +/// File category from a filename extension. fn file_type(filename: &str) -> &'static str { let ext = filename .rsplit_once('.') diff --git a/src/modules/filter.rs b/src/modules/filter.rs index 90a322b..65e45c2 100644 --- a/src/modules/filter.rs +++ b/src/modules/filter.rs @@ -1,8 +1,6 @@ -//! filter — InspIRCd's `m_filter`: oper-configured spam/word filters. A glob is -//! matched against PRIVMSG/NOTICE text and, on a hit, an action is taken. Fully -//! self-contained: the rule set lives in `Server.ext` (the module-owned typemap), -//! the `FILTER` command manages it, and the `on_pre_message` hook enforces it — -//! nothing leaks into server.rs or config.rs. +//! Oper-configured spam/word filters: a glob is matched against PRIVMSG/NOTICE text +//! and, on a hit, an action is taken. The rule set lives in `Server.ext`, the +//! `FILTER` command manages it, and the `on_pre_message` hook enforces it. use crate::channels::glob_match; use crate::command::{CmdResult, Command}; @@ -21,7 +19,7 @@ pub struct SpamFilter { pub reason: String, } -/// The rule set — stored in `Server.ext`, so it never touches the core struct. +/// The rule set, stored in `Server.ext`. #[derive(Default)] pub struct Filters(pub Vec<SpamFilter>); diff --git a/src/modules/flood.rs b/src/modules/flood.rs index 9d81b7c..627f641 100644 --- a/src/modules/flood.rs +++ b/src/modules/flood.rs @@ -1,10 +1,6 @@ -//! Flood protection — a module that rate-limits messages. -//! -//! It keeps each user's recent message times in that user's typed -//! [`crate::extensible::Extensible`] slot. Because the state is *owned by the -//! `User`*, it vanishes the moment the user quits — no cleanup callback, no cull -//! list, no chance of a dangling reference (the C++ InspIRCd failure mode this -//! design rules out at compile time). +//! Per-user message-rate limit (`flood_messages` within `flood_seconds`); opers +//! exempt. Recent message times live in the user's typed +//! [`crate::extensible::Extensible`] slot, so the state is freed when the user quits. use crate::module::{ModResult, Module}; use crate::server::{now, Server}; diff --git a/src/modules/geoip.rs b/src/modules/geoip.rs index ac2c126..7a22387 100644 --- a/src/modules/geoip.rs +++ b/src/modules/geoip.rs @@ -1,13 +1,9 @@ -//! geoip — native MaxMind DB (`.mmdb`) country lookup, with the `G:<cc>` geoban -//! extban, the `GEOIP` command and a WHOIS country line. The `maxminddb` crate is -//! off-limits (openssl+mio only), so the binary format is parsed by hand in pure -//! std: the metadata section, the record-size-aware search tree, and the typed data -//! decoder — no crate, no `unsafe`, no C FFI. +//! MaxMind DB (`.mmdb`) country lookup, with the `G:<cc>` geoban extban, the +//! `GEOIP` command and a WHOIS country line. The binary format is parsed by hand: +//! the metadata section, the record-size-aware search tree, and the typed data +//! decoder. //! //! Config: `geoip_database = /path/to/GeoLite2-Country.mmdb` (loaded once at boot). -//! -//! Behaviour reference: InspIRCd's `m_geo_maxmind` + `m_geoban` + `m_geocmd`. -//! Original native Rust. use std::net::IpAddr; use std::sync::Arc; @@ -375,15 +371,18 @@ mod tests { use super::*; use std::net::Ipv4Addr; - // A real GeoLite2-Country.mmdb if one is present; otherwise the test no-ops so - // CI (which has no database) stays green. + // Use a database from the env var if set, else common locations; the test + // no-ops when none is present so CI stays green. const DB_CANDIDATES: &[&str] = &[ - "/home/debian/irc/ircd/inspircd/run/conf/geodata/GeoLite2-Country.mmdb", "/usr/share/GeoIP/GeoLite2-Country.mmdb", + "/etc/echoircd/GeoLite2-Country.mmdb", ]; fn load() -> Option<Mmdb> { - DB_CANDIDATES.iter().find_map(|p| Mmdb::open(p)) + std::env::var("ECHOIRCD_TEST_MMDB") + .ok() + .and_then(|p| Mmdb::open(&p)) + .or_else(|| DB_CANDIDATES.iter().find_map(|p| Mmdb::open(p))) } #[test] diff --git a/src/modules/globops.rs b/src/modules/globops.rs index f03193c..42c5ba3 100644 --- a/src/modules/globops.rs +++ b/src/modules/globops.rs @@ -1,6 +1,5 @@ -//! globops — `GLOBOPS <message>` lets an oper send a message to all opers (the -//! server-notice stream, echoIRCd's equivalent of InspIRCd's `+g` snomask). -//! Reference: InspIRCd's `m_globops`. Original native Rust. +//! `GLOBOPS <message>`: lets an oper send a message to all opers via the +//! server-notice stream. use crate::command::{CmdResult, Command}; use crate::numeric::ERR_NOPRIVILEGES; diff --git a/src/modules/hashident.rs b/src/modules/hashident.rs index 1315d09..deb1e91 100644 --- a/src/modules/hashident.rs +++ b/src/modules/hashident.rs @@ -1,11 +1,9 @@ -//! hashident — replace a user's ident with a stable, opaque 12-character token -//! derived from their IP, so the username field leaks nothing (no `~guest`, no -//! probed identd name) yet stays constant per address. reverse's own module. +//! Replaces a user's ident with a stable, opaque 12-character token derived from +//! their IP, so the username field leaks nothing yet stays constant per address. //! //! The token is the first 6 bytes of `HMAC-SHA256(hashident_key, ip)`, hex-encoded -//! (12 chars). Off unless `hashident = yes` and a `hashident_key` secret is set — -//! without the key it does nothing (the key is what makes the mapping unforgeable). -//! Applied once, right after the user finishes connecting; all config-driven. +//! (12 chars). Off unless `hashident = yes` and a `hashident_key` secret is set; +//! the key is what makes the mapping unforgeable. Applied once, right after connect. use openssl::hash::MessageDigest; use openssl::pkey::PKey; diff --git a/src/modules/hidelist.rs b/src/modules/hidelist.rs index 03f9771..35c280f 100644 --- a/src/modules/hidelist.rs +++ b/src/modules/hidelist.rs @@ -1,8 +1,6 @@ -//! hidelist — hide a channel list mode's entries (e.g. the +b ban list) from users -//! below a configured rank, so ordinary members can't enumerate who's banned. -//! Config, repeatable: `hidelist = <modechar> <rank>` where rank is one of -//! owner|admin|op|halfop|voice. Opers always see. Reference: InspIRCd's -//! `m_hidelist`. Original native Rust. +//! Hides a channel list mode's entries (e.g. the +b ban list) from members below a +//! configured rank. Config, repeatable: `hidelist = <modechar> <rank>` where rank is +//! one of owner|admin|op|halfop|voice. Opers always see. use crate::channels::{RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE}; use crate::server::Server; diff --git a/src/modules/hidemode.rs b/src/modules/hidemode.rs index d8f8810..83b1182 100644 --- a/src/modules/hidemode.rs +++ b/src/modules/hidemode.rs @@ -1,8 +1,7 @@ -//! hidemode — hide specific mode changes from channel members below a rank, so -//! ordinary users don't see e.g. bans being set/removed. Config, repeatable: -//! `hidemode = <modechar> <rank>` (rank: owner|admin|op|halfop|voice). The setter, -//! opers and linked servers always see the full change. Reference: InspIRCd's -//! `m_hidemode`. Original native Rust. +//! Hides specific mode changes from channel members below a rank (e.g. bans being +//! set/removed). Config, repeatable: `hidemode = <modechar> <rank>` (rank: +//! owner|admin|op|halfop|voice). The setter, opers and linked servers always see +//! the full change. use crate::channels::{RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE}; use crate::server::Server; diff --git a/src/modules/hidewhois.rs b/src/modules/hidewhois.rs index 94b482c..12a7b44 100644 --- a/src/modules/hidewhois.rs +++ b/src/modules/hidewhois.rs @@ -1,6 +1,7 @@ -//! hidewhois — InspIRCd `m_hidewhois`. Hides sensitive WHOIS lines (server, idle, -//! secure, …) from ordinary users. Opers and the user themselves are exempt when -//! the matching config toggle is on. All config-driven; nothing lives on `Server`. +//! Hides sensitive WHOIS lines (server, idle, secure, …) from ordinary users. Opers +//! and the user themselves are exempt when the matching config toggle is on. +//! Config: `hidewhois`, `hidewhois_selfview`, `hidewhois_opers`, +//! `hidewhois_hide_server`, `hidewhois_hide_idle`, `hidewhois_hide_secure`. use crate::server::Server; use crate::Uid; diff --git a/src/modules/irccloudtags.rs b/src/modules/irccloudtags.rs index 0893b07..86d535b 100644 --- a/src/modules/irccloudtags.rs +++ b/src/modules/irccloudtags.rs @@ -1,11 +1,8 @@ -//! irccloudtags — support for IRCCloud's client-only message tags -//! (`+draft/unreact`, `+draft/edit`, `+draft/edit-text`, `+draft/attachments`, -//! `+draft/attachment-fallback`). echoIRCd already relays *all* `+` client tags to -//! `message-tags` clients, so these flow for free; what this module adds is the -//! spec validation InspIRCd's `m_ircv3_irccloudtags` does — each of these tags MUST -//! carry a value, and an empty one is rejected with `FAIL … MESSAGE_TAG_TOO_SHORT`. -//! -//! Behaviour reference: InspIRCd's `m_ircv3_irccloudtags`. Original native Rust. +//! irccloudtags — validate IRCCloud client-only message tags (`+draft/unreact`, +//! `+draft/edit`, `+draft/edit-text`, `+draft/attachments`, +//! `+draft/attachment-fallback`). Each must carry a value; an empty one is rejected +//! with `FAIL … MESSAGE_TAG_TOO_SHORT`. Relaying of `+` client tags is handled +//! elsewhere by the `message-tags` cap. use crate::module::{ModResult, Module}; use crate::server::Server; @@ -34,14 +31,14 @@ impl Module for IrcCloudTags { cmd: &str, _params: &[String], ) -> ModResult { - // only messages carry client tags + // only message commands carry client tags if !(cmd.eq_ignore_ascii_case("PRIVMSG") || cmd.eq_ignore_ascii_case("NOTICE") || cmd.eq_ignore_ascii_case("TAGMSG")) { return ModResult::Passthru; } - // the line's client-only tags are stashed on the server before dispatch + // client-only tags are stashed on the server before dispatch for raw in srv.line_ctags.split(';').filter(|t| !t.is_empty()) { let (name, val) = raw.split_once('=').unwrap_or((raw, "")); if TAGS.contains(&name) && val.is_empty() { diff --git a/src/modules/jsonlog.rs b/src/modules/jsonlog.rs index d32b011..7ceee76 100644 --- a/src/modules/jsonlog.rs +++ b/src/modules/jsonlog.rs @@ -1,11 +1,8 @@ -//! jsonlog — the `draft/json-log` capability. reverse's own module. When an oper -//! negotiates `CAP REQ draft/json-log`, every server notice they receive carries a -//! structured JSON object (timestamp, level, subsystem, msg, …) as an IRCv3 message -//! **tag** — the human-readable text stays in the NOTICE, the machine-readable copy -//! rides alongside. Companion to the RPC `log.*` methods, which expose the same data -//! over HTTP. Dispatched straight from `Server::snotice`; the tag build lives here. -//! -//! Behaviour reference: reverse's InspIRCd `m_jsonrpclog`. Original native Rust. +//! jsonlog — the `draft/json-log` capability. When an oper negotiates +//! `CAP REQ draft/json-log`, every server notice they receive carries a structured +//! JSON object (timestamp, level, subsystem, msg, …) as an IRCv3 message tag; the +//! human-readable text stays in the NOTICE. Dispatched from `Server::snotice`; the +//! tag build lives here. use crate::modules::rpc::json::{obj, qstr}; use crate::server::{iso_time, now, Server}; diff --git a/src/modules/maphide.rs b/src/modules/maphide.rs index 8980088..34f9944 100644 --- a/src/modules/maphide.rs +++ b/src/modules/maphide.rs @@ -1,7 +1,5 @@ -//! maphide — hide the server map (`LINKS` / `MAP`) from ordinary users, so the -//! network topology isn't exposed to non-operators. Off unless `maphide = yes`. -//! -//! Behaviour reference: InspIRCd's `m_maphide`. Original native Rust. +//! maphide — hide the server map (`LINKS` / `MAP`) from non-opers, so network +//! topology isn't exposed. Off unless `maphide = yes`. use crate::module::{ModResult, Module}; use crate::server::Server; diff --git a/src/modules/markread.rs b/src/modules/markread.rs index 88e0145..437c6fd 100644 --- a/src/modules/markread.rs +++ b/src/modules/markread.rs @@ -1,8 +1,8 @@ -//! markread — InspIRCd's `m_ircv3_read_marker` (draft/read-marker). A client sets -//! or queries the "last read" timestamp per conversation; markers are keyed by -//! account when logged in (so they're shared across a user's devices and survive -//! reconnects) and echoed to every connection sharing that identity. Self-contained: -//! the marker store lives in `Server.ext`, cleaned up by the on_user_quit hook. +//! markread — IRCv3 draft/read-marker. A client sets or queries the "last read" +//! timestamp per conversation; markers are keyed by account when logged in (shared +//! across a user's devices, surviving reconnects) and echoed to every connection +//! sharing that identity. The store lives in `Server.ext`, cleaned up by the +//! on_user_quit hook. use std::collections::HashMap; diff --git a/src/modules/metadata.rs b/src/modules/metadata.rs index 79b3680..4223a65 100644 --- a/src/modules/metadata.rs +++ b/src/modules/metadata.rs @@ -1,7 +1,6 @@ -//! metadata — InspIRCd's `m_ircv3_metadata` (draft/metadata-2). Client METADATA -//! GET/LIST/SET/CLEAR on users and channels, op-gated, with change notices in a -//! `metadata` batch. Self-contained: the store lives in `Server.ext`, cleaned up -//! by the on_user_quit hook; the command and its logic are all here. +//! metadata — IRCv3 draft/metadata-2. Client METADATA GET/LIST/SET/CLEAR on users +//! and channels, op-gated, with change notices in a `metadata` batch. The store +//! lives in `Server.ext`, cleaned up by the on_user_quit hook. use std::collections::HashMap; @@ -161,7 +160,7 @@ impl Command for MetadataCmd { } } if key.starts_with('#') { - save(s); // persist channel metadata (m_ircv3_metadata_db) + save(s); // persist channel metadata } let setter = s.users.get(&uid).map(|u| u.prefix()).unwrap_or_default(); let note = match &value { @@ -214,9 +213,9 @@ fn db_path(s: &Server) -> String { format!("{}.metadata", s.conf_path) } -/// Persist channel metadata (the `#`-keyed entries) so it survives a restart — -/// InspIRCd `m_ircv3_metadata_db`. Per-user metadata (`u<uid>`) is intentionally -/// not saved: uids don't persist across restarts. +/// Persist channel metadata (the `#`-keyed entries) so it survives a restart. +/// Per-user metadata (`u<uid>`) is intentionally not saved: uids don't persist +/// across restarts. pub fn save(s: &Server) { let mut out = String::new(); if let Some(st) = s.ext.get::<MetaStore>() { diff --git a/src/modules/mod.rs b/src/modules/mod.rs index b75e7eb..4817fe5 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,7 +1,7 @@ -//! Optional, pluggable modules — echoIRCd's answer to InspIRCd's `src/modules/`. -//! Most hook lifecycle events via the [`crate::module::Module`] trait; [`dnsbl`] -//! is the exception — it's driven straight from the connection lifecycle rather -//! than the hook bus, but lives here as its own self-contained unit. +//! Optional, pluggable modules. Most hook lifecycle events via the +//! [`crate::module::Module`] trait; [`dnsbl`] is the exception — it's driven from +//! the connection lifecycle rather than the hook bus, but lives here as its own +//! self-contained unit. pub mod account_registration; pub mod antimixedutf8; @@ -44,6 +44,8 @@ pub mod markread; pub mod metadata; pub mod multiline; pub mod network_icon; +pub mod ojoin; +pub mod operprefix; pub mod password_hash; pub mod profilelink; pub mod randquote; @@ -99,6 +101,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> { Box::new(solvemsg::SolveMsg), Box::new(autoop::AutoOp), Box::new(autodrop::AutoDrop), + Box::new(operprefix::OperPrefix), ] } @@ -111,7 +114,7 @@ pub fn module_names() -> Vec<String> { } /// Commands contributed by modules (chained into the core command table), so a -/// module that adds a command keeps it in its own file, InspIRCd-style. +/// module that adds a command keeps it in its own file. pub fn module_commands() -> Vec<Box<dyn Command>> { filter::commands() .into_iter() @@ -135,5 +138,6 @@ pub fn module_commands() -> Vec<Box<dyn Command>> { .chain(geoip::commands()) .chain(globops::commands()) .chain(relaymsg::commands()) + .chain(ojoin::commands()) .collect() } diff --git a/src/modules/multiline.rs b/src/modules/multiline.rs index 562d512..1d0d82b 100644 --- a/src/modules/multiline.rs +++ b/src/modules/multiline.rs @@ -1,10 +1,9 @@ -//! multiline — the server side of IRCv3 draft/multiline. A client wraps one long -//! message in a `BATCH +<ref> draft/multiline <target>`; the `@batch=<ref>`-tagged -//! PRIVMSG/NOTICE lines are buffered here (see `Ircd::dispatch`) and, when -//! `BATCH -<ref>` closes, reassembled (honouring `draft/multiline-concat`) and -//! delivered as normal messages. Self-contained: the in-flight batches live in -//! `Server.ext`, cleaned up by the on_user_quit hook; the BATCH command and the -//! accumulate/close logic are all here. +//! multiline — server side of IRCv3 draft/multiline. A client wraps one long message +//! in a `BATCH +<ref> draft/multiline <target>`; the `@batch=<ref>`-tagged +//! PRIVMSG/NOTICE lines are buffered (see `Ircd::dispatch`) and, when `BATCH -<ref>` +//! closes, reassembled (honouring `draft/multiline-concat`) and delivered as normal +//! messages. Limits: multiline_maxbytes / multiline_maxlines. In-flight batches live +//! in `Server.ext`, cleaned up by the on_user_quit hook. use std::collections::HashMap; diff --git a/src/modules/network_icon.rs b/src/modules/network_icon.rs index 8cc568c..adb03d0 100644 --- a/src/modules/network_icon.rs +++ b/src/modules/network_icon.rs @@ -1,6 +1,5 @@ -//! ircv3_network_icon — InspIRCd `m_ircv3_network_icon`. Advertises a network icon -//! via the `draft/ICON` ISUPPORT token from `network_icon = <url>`. Config-driven; -//! nothing lives on `Server`. +//! network_icon — advertise a network icon via the `ICON` ISUPPORT token from +//! `network_icon = <url>`. use crate::server::Server; diff --git a/src/modules/ojoin.rs b/src/modules/ojoin.rs new file mode 100644 index 0000000..e753a53 --- /dev/null +++ b/src/modules/ojoin.rs @@ -0,0 +1,55 @@ +//! ojoin — `OJOIN <channel>`: an oper joins a channel as network staff, taking the +//! oper prefix (`!`, mode `y`, above owner) and — unless `ojoin_op = no` — channel +//! op. The prefix's rank protects them from being kicked/deopped. Off unless +//! `ojoin = yes`. + +use crate::command::{CmdResult, Command}; +use crate::numeric::ERR_NOPRIVILEGES; +use crate::server::Server; +use crate::Uid; + +pub fn commands() -> Vec<Box<dyn Command>> { + vec![Box::new(OjoinCmd)] +} + +struct OjoinCmd; +impl Command for OjoinCmd { + fn name(&self) -> &'static str { + "OJOIN" + } + fn min_params(&self) -> usize { + 1 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default(); + if !s.is_oper(uid) { + s.numeric( + uid, + ERR_NOPRIVILEGES, + ":Permission Denied- You're not an IRC operator", + ); + return CmdResult::Fail; + } + if !s.conf_bool("ojoin", false) { + s.send( + uid, + format!(":{} NOTICE {nick} :*** OJOIN is not enabled on this server.", s.name), + ); + return CmdResult::Fail; + } + let chan = params[0].clone(); + let key = chan.to_ascii_lowercase(); + if !s.is_member(uid, &key) { + s.join(uid, &chan, None); + } + if !s.is_member(uid, &key) { + return CmdResult::Fail; // join was refused (bad name, etc.) + } + crate::modules::operprefix::grant(s, uid, &key); + if s.conf_bool("ojoin_op", true) { + crate::coremods::core_mode::svs_set_chan_modes(s, &chan, "+o", &[nick.clone()]); + } + s.snotice(&format!("{nick} used OJOIN to enter {chan}")); + CmdResult::Ok + } +} diff --git a/src/modules/operprefix.rs b/src/modules/operprefix.rs new file mode 100644 index 0000000..8d48306 --- /dev/null +++ b/src/modules/operprefix.rs @@ -0,0 +1,79 @@ +//! operprefix — network staff (IRC opers) get a distinct `!` prefix (prefix mode +//! `y`, ranked above channel owner) in every channel, so users can see who's staff +//! and ops can't kick/deop them. Enabled with `operprefix = yes`. Also provides the +//! shared grant/clear primitive used by [`crate::modules::ojoin`]. + +use crate::module::Module; +use crate::server::Server; +use crate::Uid; + +pub fn enabled(s: &Server) -> bool { + s.conf_bool("operprefix", false) +} + +/// Set/clear the oper prefix for `uid` in one channel and broadcast `MODE ±y`. +fn set(s: &mut Server, uid: Uid, key: &str, on: bool) { + let Some(nick) = s.users.get(&uid).map(|u| u.nick.clone()) else { + return; + }; + let changed = match s.channels.get_mut(key).and_then(|c| c.members.get_mut(&uid)) { + Some(m) if m.oprefix != on => { + m.oprefix = on; + true + } + _ => false, + }; + if !changed { + return; + } + let sign = if on { '+' } else { '-' }; + let name = s.channels.get(key).map(|c| c.name.clone()).unwrap_or_default(); + s.to_channel(key, &format!(":{} MODE {name} {sign}y {nick}", s.name), None); + s.propagate(&format!(":{} MODE {name} {sign}y {nick}", s.sid), None); +} + +/// Grant the oper prefix in `key` (used by ojoin and on-join auto-grant). +pub fn grant(s: &mut Server, uid: Uid, key: &str) { + set(s, uid, key, true); +} + +fn all_channels(s: &Server, uid: Uid) -> Vec<String> { + s.users + .get(&uid) + .map(|u| u.channels.iter().cloned().collect()) + .unwrap_or_default() +} + +/// Auto-grant to opers on join when the feature is enabled. +fn join_grant(s: &mut Server, uid: Uid, key: &str) { + if enabled(s) && s.is_oper(uid) { + grant(s, uid, key); + } +} + +/// Grant across all of `uid`'s channels — on oper-up. +pub fn grant_all(s: &mut Server, uid: Uid) { + if !enabled(s) { + return; + } + for key in all_channels(s, uid) { + grant(s, uid, &key); + } +} + +/// Clear across all of `uid`'s channels — on de-oper (safe no-op if never granted). +pub fn clear_all(s: &mut Server, uid: Uid) { + for key in all_channels(s, uid) { + set(s, uid, &key, false); + } +} + +pub struct OperPrefix; +impl Module for OperPrefix { + fn name(&self) -> &'static str { + "operprefix" + } + fn on_join(&mut self, s: &mut Server, uid: Uid, chan: &str) { + join_grant(s, uid, &chan.to_ascii_lowercase()); + } +} diff --git a/src/modules/password_hash.rs b/src/modules/password_hash.rs index e7d5099..8196f62 100644 --- a/src/modules/password_hash.rs +++ b/src/modules/password_hash.rs @@ -1,16 +1,11 @@ -//! password_hash — hashed `<oper>` passwords plus a `/MKPASSWD` helper to make -//! them. This is echoIRCd's answer to InspIRCd's hash-provider family (md5, sha1, -//! sha2, pbkdf2): one module, backed entirely by OpenSSL (already a dependency), -//! that both verifies a stored hash against a supplied password and generates new -//! hashes for the config. +//! Hashed `<oper>` passwords plus a `/MKPASSWD` helper, backed by OpenSSL. +//! Verifies stored hashes and generates new ones (md5, sha1, sha2, pbkdf2). //! -//! A stored password is either plaintext (no recognised prefix — backward -//! compatible) or `"<algo>:<hex>"`: +//! A stored password is either plaintext (no recognised prefix) or `"<algo>:<hex>"`: //! * `md5:` `sha1:` `sha256:` `sha512:` — a plain hex digest of the password //! * `pbkdf2:<iters>:<salthex>:<hashhex>` — PBKDF2-HMAC-SHA256, salted //! -//! Comparisons are constant-time (`openssl::memcmp`). Everything is self-contained -//! here; the OPER handler just calls [`verify`]. +//! Comparisons are constant-time (`openssl::memcmp`). The OPER handler calls [`verify`]. use openssl::hash::{hash, MessageDigest}; use openssl::pkcs5::pbkdf2_hmac; diff --git a/src/modules/profilelink.rs b/src/modules/profilelink.rs index d3ce0de..8d873ed 100644 --- a/src/modules/profilelink.rs +++ b/src/modules/profilelink.rs @@ -1,6 +1,4 @@ -//! profileLink — InspIRCd `m_profileLink`. Adds a profile URL to WHOIS for -//! logged-in users from `profilelink_baseurl = <url>`. Config-driven; nothing -//! lives on `Server`. +//! Adds a profile URL to WHOIS for logged-in users, from `profilelink_baseurl = <url>`. use crate::server::Server; diff --git a/src/modules/randquote.rs b/src/modules/randquote.rs index 3ae93f6..b2cc13e 100644 --- a/src/modules/randquote.rs +++ b/src/modules/randquote.rs @@ -1,7 +1,5 @@ -//! randquote — greet each connecting user with a random line from a configured -//! set of quotes. Off unless one or more `randquote = <line>` are configured. -//! -//! Behaviour reference: InspIRCd's `m_randquote`. Original native Rust. +//! Greet each connecting user with a random line from the configured quote set. +//! Off unless one or more `randquote = <line>` are configured. use openssl::rand::rand_bytes; diff --git a/src/modules/realnameban.rs b/src/modules/realnameban.rs index 5dc6046..a4570c1 100644 --- a/src/modules/realnameban.rs +++ b/src/modules/realnameban.rs @@ -1,9 +1,6 @@ -//! realnameban — the `r:` matching extban: match a user by their real name -//! (GECOS) instead of their host. `+b r:*some spammer*` bans everyone whose -//! realname matches the glob. Dispatched from the channel ban matcher; the logic -//! lives here in its own file. -//! -//! Behaviour reference: InspIRCd's `m_realnameban`. Original native Rust. +//! The `r:` matching extban: match a user by real name (GECOS) instead of host. +//! `+b r:*some spammer*` bans everyone whose realname matches the glob. +//! Dispatched from the channel ban matcher. use crate::channels::glob_match; use crate::server::Server; diff --git a/src/modules/recaptcha.rs b/src/modules/recaptcha.rs index 22c709e..d65a5f8 100644 --- a/src/modules/recaptcha.rs +++ b/src/modules/recaptcha.rs @@ -1,7 +1,6 @@ -//! recaptcha — gate registration behind a human-verification step. An unverified -//! user is handed a one-time, IP-bound HS256 JWT and a URL to solve a reCAPTCHA at; -//! once solved they present the signed token back with `CAPTCHA <token>` and the -//! connection is allowed. reverse's own module. +//! Gate registration behind a human-verification step. An unverified user is handed +//! a one-time, IP-bound HS256 JWT and a URL to solve a reCAPTCHA at; once solved they +//! present the signed token back with `CAPTCHA <token>` and the connection is allowed. //! //! Modes: //! * JWT-only (default): a validly-signed, unexpired, IP-matching token is proof diff --git a/src/modules/relaymsg.rs b/src/modules/relaymsg.rs index 3f4d6ad..10140a0 100644 --- a/src/modules/relaymsg.rs +++ b/src/modules/relaymsg.rs @@ -5,8 +5,8 @@ //! must contain a configured separator and must not collide with a real nick. //! //! Config: `relaymsg_separators` (default `/`), `relaymsg_ident` (default `relay`), -//! `relaymsg_host` (default = server name). Reference: InspIRCd's `m_relaymsg`. -//! (Local delivery; cross-server ENCAP relay is not propagated.) Original native Rust. +//! `relaymsg_host` (default = server name). Local delivery only; cross-server ENCAP +//! relay is not propagated. use crate::command::{CmdResult, Command}; use crate::numeric::{ERR_BADRELAYNICK, ERR_CANNOTSENDTOCHAN, ERR_NOPRIVILEGES, ERR_NOSUCHCHANNEL}; diff --git a/src/modules/reputation.rs b/src/modules/reputation.rs index 37e0747..228cb2a 100644 --- a/src/modules/reputation.rs +++ b/src/modules/reputation.rs @@ -1,9 +1,8 @@ -//! reputation — InspIRCd `m_reputation` (© reverse). Per-network-address reputation -//! scoring. Every `bumpinterval` (default 5m) each connected user's masked address -//! gains +1 (+2 if logged into services), provided they're in a channel with at -//! least `minchanmembers` members. Scores decay per the `reputationexpire` rules -//! and persist to disk. Exposes the `y:` score extban, WHOIS visibility, and the -//! `REPUTATION` oper command. Everything is config-driven (see `[reputation_*]`). +//! Per-network-address reputation scoring. Every `bumpinterval` (default 5m) each +//! connected user's masked address gains +1 (+2 if logged into services), provided +//! they're in a channel with at least `minchanmembers` members. Scores decay per the +//! `reputationexpire` rules and persist to disk. Exposes the `y:` score extban, WHOIS +//! visibility, and the `REPUTATION` oper command. Config-driven (see `[reputation_*]`). use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -49,7 +48,7 @@ fn mask_ip(ip: IpAddr, v4: u8, v6: u8) -> IpAddr { } } -// --- config, read straight from the config file (no fields on Server) ---------- +// --- config accessors ---------------------------------------------------------- fn v4prefix(s: &Server) -> u8 { s.conf_num::<u8>("reputation_ipv4prefix", 32).clamp(1, 32) } @@ -82,7 +81,7 @@ fn expire_rules(s: &Server) -> Vec<(i32, u64)> { }) .collect(); if rules.is_empty() { - // Unreal defaults: score<=2 after 1h, <=6 after 7d, <=12 after 30d, any after 90d + // defaults: score<=2 after 1h, <=6 after 7d, <=12 after 30d, any after 90d vec![(2, 3600), (6, 604800), (12, 2592000), (-1, 7776000)] } else { rules diff --git a/src/modules/restrictchans.rs b/src/modules/restrictchans.rs index 7f052be..84922db 100644 --- a/src/modules/restrictchans.rs +++ b/src/modules/restrictchans.rs @@ -1,9 +1,7 @@ -//! restrictchans — only opers may *create* new channels; everyone can still join -//! existing ones. A `restrictchan = <glob>` whitelist lets ordinary users create -//! channels whose name matches (e.g. `restrictchan = #public-*`). Off unless -//! `restrictchans = yes`. Dispatched from `Server::join` (like denychans). -//! -//! Behaviour reference: InspIRCd's `m_restrictchans`. Original native Rust. +//! Only opers may *create* new channels; everyone can still join existing ones. +//! A `restrictchan = <glob>` whitelist lets ordinary users create channels whose +//! name matches (e.g. `restrictchan = #public-*`). Off unless `restrictchans = yes`. +//! Dispatched from `Server::join`. use crate::channels::glob_match; use crate::numeric::ERR_BADCHANNEL; diff --git a/src/modules/restrictcommands.rs b/src/modules/restrictcommands.rs index b97b2c2..2e8827c 100644 --- a/src/modules/restrictcommands.rs +++ b/src/modules/restrictcommands.rs @@ -1,6 +1,5 @@ -//! restrictcommands — hold back chosen commands from brand-new / unregistered -//! users (UnrealIRCd's `set::restrict-commands`), with exemptions. reverse's own -//! module. Each restriction is one config line: +//! Hold back chosen commands from brand-new / unregistered users, with exemptions. +//! Each restriction is one config line: //! //! ```text //! restrictcommand = LIST connectdelay=60 exemptidentified=yes exemptwebirc=yes \ diff --git a/src/modules/restrictmsg.rs b/src/modules/restrictmsg.rs index aca2210..15c63fb 100644 --- a/src/modules/restrictmsg.rs +++ b/src/modules/restrictmsg.rs @@ -1,10 +1,7 @@ -//! restrictmsg — stop ordinary users from private-messaging each other. A PM -//! between two users is allowed only when the sender is an oper, the target is an -//! oper, or the target is a service/bot (so users can still reach NickServ etc.). -//! Channel messages are never affected. Off unless `restrictmsg = yes` — read -//! straight from the config, nothing on `Server`. -//! -//! Behaviour reference: InspIRCd's `m_restrictmsg`. Original native Rust. +//! Stop ordinary users from private-messaging each other. A PM between two users is +//! allowed only when the sender is an oper, the target is an oper, or the target is a +//! service/bot (so users can still reach NickServ etc.). Channel messages are never +//! affected. Off unless `restrictmsg = yes`. use crate::module::{ModResult, Module}; use crate::numeric::ERR_CANTSENDTOUSER; diff --git a/src/modules/rmode.rs b/src/modules/rmode.rs index 64c3b4f..7077cef 100644 --- a/src/modules/rmode.rs +++ b/src/modules/rmode.rs @@ -1,10 +1,8 @@ -//! rmode — `RMODE <channel> <listmode> [pattern]`, bulk-remove entries from a -//! channel list mode (`b` bans, `e` ban exceptions, `I` invite exceptions). With a -//! `pattern` glob only matching entries are cleared; without one, all are. Needs -//! half-op+ (opers bypass). The removals go through the normal mode engine (chunked -//! to keep each `MODE` line legal), so they broadcast and propagate like any other. -//! -//! Behaviour reference: InspIRCd's `m_rmode`. Original native Rust. +//! `RMODE <channel> <listmode> [pattern]` — bulk-remove entries from a channel list +//! mode (`b` bans, `e` ban exceptions, `I` invite exceptions). With a `pattern` glob +//! only matching entries are cleared; without one, all are. Needs half-op+ (opers +//! bypass). Removals go through the normal mode engine (chunked to keep each `MODE` +//! line legal), so they broadcast and propagate like any other. use crate::channels::{glob_match, RANK_HALFOP}; use crate::command::{CmdResult, Command}; diff --git a/src/modules/rpc/ban.rs b/src/modules/rpc/ban.rs index a33fdee..4e43224 100644 --- a/src/modules/rpc/ban.rs +++ b/src/modules/rpc/ban.rs @@ -1,13 +1,13 @@ -//! rpc ban provider — `xline.list`, `xline.add`, `xline.del`. InspIRCd's -//! `m_rpc_ban`. Covers every echoIRCd x-line kind (K/G/Z/E/SHUN/Q/CBAN) through the -//! same `add_xline`/`remove_xline` primitives the oper commands use. +//! X-line RPC provider: `xline.list`, `xline.add`, `xline.del`. Covers every +//! x-line kind (K/G/Z/E/SHUN/Q/CBAN) via the same `add_xline`/`remove_xline` +//! primitives the oper commands use. use super::json::{self, obj, qstr}; use super::RpcError; use crate::server::Server; use crate::xline::{parse_duration, XKind}; -/// Map a request `type` (letter or unreal-ish name) to an `XKind`. +/// Map a request `type` (letter tag or full name like `KLINE`) to an `XKind`. fn kind_of(t: &str) -> Option<XKind> { let up = t.to_ascii_uppercase(); XKind::from_tag(&up).or_else(|| { diff --git a/src/modules/rpc/channel.rs b/src/modules/rpc/channel.rs index e185760..ce1b14c 100644 --- a/src/modules/rpc/channel.rs +++ b/src/modules/rpc/channel.rs @@ -1,6 +1,5 @@ -//! rpc channel provider — `channel.list`, `channel.get`, and the mutators -//! `channel.kick`, `channel.set_topic`. InspIRCd's `m_rpc_channel`. (`channel.set_mode` -//! lands with the shared server-side mode applier in a later pass.) +//! Channel RPC provider: `channel.list`, `channel.get`, and the mutators +//! `channel.kick`, `channel.set_topic`, `channel.set_mode`. use super::json::{obj, qstr}; use super::RpcError; diff --git a/src/modules/rpc/core.rs b/src/modules/rpc/core.rs index 61135cb..bf82796 100644 --- a/src/modules/rpc/core.rs +++ b/src/modules/rpc/core.rs @@ -1,6 +1,5 @@ -//! rpc core provider — introspection: `rpc.methods` (list the interface), -//! `rpc.info` (identity + methods), and `server.info` / `stats.get` (identity + -//! network counts). InspIRCd's `m_rpc_core` + the legacy `stats.get`. +//! Core RPC introspection: `rpc.methods` (list the interface), `rpc.info` +//! (identity + methods), and `server.info` / `stats.get` (identity + network counts). use super::json::{obj, qstr}; use super::{RpcError, ALL_METHODS}; diff --git a/src/modules/rpc/httpd.rs b/src/modules/rpc/httpd.rs index ddf7d01..9cc7b04 100644 --- a/src/modules/rpc/httpd.rs +++ b/src/modules/rpc/httpd.rs @@ -1,8 +1,7 @@ -//! The RPC HTTP server: a blocking listener thread that accepts a connection, -//! reads one HTTP request, authenticates it, and forwards the JSON-RPC body to the -//! core as `Event::RpcRequest` — then writes back whatever the core replies. Low -//! volume (admin tooling), so thread-per-connection is fine. Native `TcpStream` -//! only; no `unsafe`, no new crate. +//! RPC HTTP server: a blocking listener thread that accepts a connection, reads +//! one HTTP request, authenticates it, forwards the JSON-RPC body to the core as +//! `Event::RpcRequest`, and writes back the core's reply. Low volume (admin +//! tooling), so thread-per-connection is fine. use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; @@ -68,9 +67,9 @@ fn handle( stream.set_read_timeout(Some(IO_TIMEOUT))?; stream.set_write_timeout(Some(IO_TIMEOUT))?; - // read until we have the full header block, then the declared body — whether - // it's Content-Length-framed or Transfer-Encoding: chunked (like InspIRCd's - // http_parser handles). Body starts 4 bytes past the header terminator. + // Read until the full header block, then the declared body — whether + // Content-Length-framed or Transfer-Encoding: chunked. Body starts 4 bytes + // past the header terminator. let mut buf = Vec::new(); let mut chunk = [0u8; 8192]; let mut head_end = None; diff --git a/src/modules/rpc/json.rs b/src/modules/rpc/json.rs index dfcf091..bae6d57 100644 --- a/src/modules/rpc/json.rs +++ b/src/modules/rpc/json.rs @@ -1,8 +1,7 @@ -//! Minimal JSON for the RPC subsystem — no serde (openssl+mio-only crate policy). -//! Two jobs: pull a named field out of a flat-ish request object (`get_*`), and -//! escape strings when *building* result JSON with `format!`. The scanners respect -//! nesting and string escapes, so `get_raw` only ever matches a **top-level** key -//! (a `"nick"` buried inside a nested value or another string won't false-match). +//! Minimal JSON for the RPC subsystem. Two jobs: pull a named field out of a +//! request object (`get_*`), and escape strings when building result JSON. The +//! scanners respect nesting and string escapes, so `get_raw` only matches a +//! top-level key (a `"nick"` buried in a nested value or a string won't false-match). /// Given `b[i] == b'"'`, return the index just past the closing quote. fn scan_string(b: &[u8], mut i: usize) -> Option<usize> { diff --git a/src/modules/rpc/log.rs b/src/modules/rpc/log.rs index 912bf71..5d40f98 100644 --- a/src/modules/rpc/log.rs +++ b/src/modules/rpc/log.rs @@ -1,6 +1,6 @@ -//! rpc log provider — `log.tail` and `log.events`. InspIRCd's `m_rpc_log` / -//! `m_jsonrpclog`. echoIRCd has no log *file* (it logs to journald), so both read -//! the in-memory server-log ring that `Server::snotice` feeds (`Server.log`). +//! Log RPC provider: `log.tail` and `log.events`. There is no log file (logging +//! goes to journald), so both read the in-memory server-log ring that +//! `Server::snotice` feeds (`Server.log`). use super::json::{self, obj, qstr}; use super::RpcError; diff --git a/src/modules/rpc/message.rs b/src/modules/rpc/message.rs index 17a8494..a5ed34b 100644 --- a/src/modules/rpc/message.rs +++ b/src/modules/rpc/message.rs @@ -1,6 +1,5 @@ -//! rpc message provider — `message.send_notice`. InspIRCd's `m_rpc_message`. -//! Sends a server NOTICE to a channel (`#…`), a single user (nick), or every -//! local user (`*` / `$*`). +//! Message RPC provider: `message.send_notice`. Sends a server NOTICE to a +//! channel (`#…`), a single user (nick), or every local user (`*` / `$*`). use super::json::{self, obj}; use super::RpcError; diff --git a/src/modules/rpc/mod.rs b/src/modules/rpc/mod.rs index 9a215fc..1e0b0cd 100644 --- a/src/modules/rpc/mod.rs +++ b/src/modules/rpc/mod.rs @@ -1,11 +1,10 @@ -//! rpc — a JSON-RPC 2.0 control interface over a small native HTTP server, the -//! echoIRCd analogue of InspIRCd's `m_httpd` + `m_jsonrpc` + `m_rpc_*`. Admin tools -//! call it to introspect and drive the ircd (list/kill users, manage bans, rehash…). +//! JSON-RPC 2.0 control interface over a small HTTP server. Admin tools call it to +//! introspect and drive the ircd (list/kill users, manage bans, rehash…). //! -//! Layering (each provider is its own file, per [[echoircd-module-per-file]]): +//! Layering (each provider is its own file): //! * [`httpd`] — the listener thread: accept, parse HTTP, authenticate, and hand //! the JSON-RPC body to the core as `Event::RpcRequest`. -//! * [`json`] — native JSON scan/build (no serde). +//! * [`json`] — JSON scan/build. //! * `core` / `user` / `channel` / `server` / `stats` / `ban` / `message` / //! `whowas` / `spamfilter` / `log` — the method providers, called on the core //! thread with `&mut Server`. @@ -128,7 +127,7 @@ pub fn dispatch(s: &mut Server, method: &str, params: &str, id: &str) -> String } /// Wrap a provider result (or error) in the JSON-RPC 2.0 response envelope, echoing -/// the method and id (InspIRCd includes the method in its responses too). +/// the method and id. pub fn envelope(method: &str, id: &str, result: Result<String, RpcError>) -> String { let id = if id.trim().is_empty() { "null" } else { id }; match result { diff --git a/src/modules/rpc/server.rs b/src/modules/rpc/server.rs index 950be99..0f30161 100644 --- a/src/modules/rpc/server.rs +++ b/src/modules/rpc/server.rs @@ -1,6 +1,5 @@ -//! rpc server provider — `server.list`, `server.rehash`, `server.disconnect`. -//! InspIRCd's `m_rpc_server`. (`server.connect` needs the socketengine's dialer, -//! which isn't reachable from the core thread — use the `CONNECT` command instead.) +//! Server RPC provider: `server.list`, `server.rehash`, `server.connect`, +//! `server.disconnect`. use super::json::{obj, qstr}; use super::RpcError; diff --git a/src/modules/rpc/spamfilter.rs b/src/modules/rpc/spamfilter.rs index 7fc96e1..59bc1c9 100644 --- a/src/modules/rpc/spamfilter.rs +++ b/src/modules/rpc/spamfilter.rs @@ -1,7 +1,7 @@ -//! rpc spamfilter provider — `spamfilter.list`, `spamfilter.add`, `spamfilter.del`. -//! InspIRCd's `m_rpc_spamfilter`. Operates on the same [`crate::modules::filter`] -//! rule set (stored in `Server.ext`) that the `FILTER` command and the enforcement -//! hook use, so a rule added here takes effect immediately. +//! Spamfilter RPC provider: `spamfilter.list`, `spamfilter.add`, `spamfilter.del`. +//! Operates on the same [`crate::modules::filter`] rule set (in `Server.ext`) that +//! the `FILTER` command and the enforcement hook use, so a rule added here takes +//! effect immediately. use super::json::{self, obj, qstr}; use super::RpcError; diff --git a/src/modules/rpc/stats.rs b/src/modules/rpc/stats.rs index a1f3a34..2ea4555 100644 --- a/src/modules/rpc/stats.rs +++ b/src/modules/rpc/stats.rs @@ -1,5 +1,5 @@ -//! rpc stats provider — read-only introspection: `module.list`, `oper.list`, -//! `security_group.list`. InspIRCd's `m_rpc_stats`. +//! Stats RPC provider: read-only introspection via `module.list`, `oper.list`, +//! `security_group.list`. use super::json::{obj, qstr}; use super::RpcError; diff --git a/src/modules/rpc/user.rs b/src/modules/rpc/user.rs index e0de287..c881ddf 100644 --- a/src/modules/rpc/user.rs +++ b/src/modules/rpc/user.rs @@ -1,7 +1,7 @@ -//! rpc user provider — `user.list`, `user.get`, and the mutators `user.kill`, -//! `user.set_mode`, `user.set_vhost`, `user.set_nick`, `user.set_oper`. InspIRCd's -//! `m_rpc_user`. Mutators route through the same `Server` primitives the commands -//! use, so behaviour and side-effects (QUIT/CHGHOST/MODE broadcasts) stay identical. +//! User RPC provider: `user.list`, `user.get`, and the mutators `user.kill`, +//! `user.set_mode`, `user.set_vhost`, `user.set_nick`, `user.set_oper`. Mutators +//! route through the same `Server` primitives the commands use, so behaviour and +//! side-effects (QUIT/CHGHOST/MODE broadcasts) stay identical. use super::json::{self, obj, qstr}; use super::RpcError; diff --git a/src/modules/rpc/whowas.rs b/src/modules/rpc/whowas.rs index 2afaecd..82d7879 100644 --- a/src/modules/rpc/whowas.rs +++ b/src/modules/rpc/whowas.rs @@ -1,5 +1,5 @@ -//! rpc whowas provider — `whowas.get`. InspIRCd's `m_rpc_whowas`. Returns the -//! recent-nick-history entries the ircd keeps for `WHOWAS`. +//! Whowas RPC provider: `whowas.get`. Returns the recent-nick-history entries +//! the ircd keeps for `WHOWAS`. use super::json::{self, obj, qstr}; use super::RpcError; diff --git a/src/modules/securelist.rs b/src/modules/securelist.rs index 5be985d..f9f8f1f 100644 --- a/src/modules/securelist.rs +++ b/src/modules/securelist.rs @@ -1,13 +1,9 @@ -//! securelist — hold back the `/LIST` command until a user has been connected for -//! a while, which defeats spambots that connect, `LIST`, spam every channel and -//! leave. Non-exempt users who `LIST` too early get an optional notice and a -//! throwaway *fake* channel list (so a bot waiting on the reply is satisfied and -//! wastes its time), then the real `LIST` is denied. Exempt: opers, logged-in -//! accounts (when `securelist_exemptregistered`), and hosts matching a -//! `securelist_exception` glob. Off unless `securelist = yes`; all config-driven. -//! -//! Behaviour reference: InspIRCd's `m_securelist`. Original native Rust; the fake -//! names use OpenSSL's CSPRNG (already a dependency) rather than any new crate. +//! Hold back the `/LIST` command until a user has been connected for a while, which +//! defeats spambots that connect, `LIST`, spam every channel and leave. Non-exempt +//! users who `LIST` too early get an optional notice and a throwaway *fake* channel +//! list (so a bot waiting on the reply is satisfied), then the real `LIST` is denied. +//! Exempt: opers, logged-in accounts (when `securelist_exemptregistered`), and hosts +//! matching a `securelist_exception` glob. Off unless `securelist = yes`. use openssl::rand::rand_bytes; diff --git a/src/modules/securitygroups.rs b/src/modules/securitygroups.rs index 470f7be..512b33e 100644 --- a/src/modules/securitygroups.rs +++ b/src/modules/securitygroups.rs @@ -1,8 +1,7 @@ -//! securitygroups — UnrealIRCd-style security groups (InspIRCd `m_securitygroups`). -//! A `securitygroup` config line defines a named set of users by AND-ed criteria -//! (host masks, TLS, account, oper, bot, webirc, reputation score range). Groups -//! drive the `g:` matching extban, the `SECURITYGROUPS` command, and a WHOIS line. -//! Self-contained: the group defs live in `Server.sec_groups`; evaluation is here. +//! Named security groups. A `securitygroup` config line defines a named set of users +//! by AND-ed criteria (host masks, TLS, account, oper, bot, webirc, reputation score +//! range). Groups drive the `g:` matching extban, the `SECURITYGROUPS` command, and a +//! WHOIS line. use crate::channels::glob_match; use crate::command::{CmdResult, Command}; @@ -19,7 +18,7 @@ enum Tri { No, } -/// A UnrealIRCd-style security group — all criteria AND-ed. +/// A security group — all criteria AND-ed. #[derive(Clone, Default)] struct SecGroup { name: String, diff --git a/src/modules/serverban.rs b/src/modules/serverban.rs index ef5418d..2ed359c 100644 --- a/src/modules/serverban.rs +++ b/src/modules/serverban.rs @@ -1,10 +1,8 @@ -//! serverban — the `s:` matching extban: match a user by the name of the server -//! they are connected to. `+b s:irc.example.net` bans everyone on that server. -//! Ban matching only ever runs against local users (join happens locally), so a -//! matched user is on this server — we glob the mask against our own name. -//! Dispatched from the channel ban matcher; the logic lives here. -//! -//! Behaviour reference: InspIRCd's `m_serverban`. Original native Rust. +//! The `s:` matching extban: match a user by the name of the server they are +//! connected to. `+b s:irc.example.net` bans everyone on that server. Ban matching +//! only ever runs against local users, so a matched user is on this server and the +//! mask is globbed against the local server name. Dispatched from the channel ban +//! matcher. use crate::channels::glob_match; use crate::server::Server; diff --git a/src/modules/showfile.rs b/src/modules/showfile.rs index 08bc44a..295d1d5 100644 --- a/src/modules/showfile.rs +++ b/src/modules/showfile.rs @@ -1,5 +1,4 @@ -//! showfile — serve a text file as its own command (InspIRCd's `m_showfile`). -//! Config, one line per file: +//! Serve a text file as its own command. Config, one line per file: //! //! ```text //! showfile = <COMMAND> <path> # e.g. showfile = RULES /etc/echoircd/rules.txt @@ -8,7 +7,6 @@ //! makes `/RULES` stream the file to the client. Dispatched from the same place as //! command aliases (an unknown, config-named command), so no static registration is //! needed. The file is read fresh on each use, so edits show without a REHASH. -//! Original native Rust. use crate::server::Server; use crate::Uid; diff --git a/src/modules/solvemsg.rs b/src/modules/solvemsg.rs index f507341..a7eb140 100644 --- a/src/modules/solvemsg.rs +++ b/src/modules/solvemsg.rs @@ -1,10 +1,9 @@ -//! solvemsg — a lightweight anti-spam gate: before an un-vouched user's *private* -//! messages are delivered, they must answer one small arithmetic question. Opers -//! and users logged into an account are exempt. Off unless `solvemsg = yes`. +//! A lightweight anti-spam gate: before an un-vouched user's *private* messages are +//! delivered, they must answer one small arithmetic question. Opers and users logged +//! into an account are exempt. Off unless `solvemsg = yes`. //! //! Flow: the first PM is held and a question is posed; the user replies with the //! number (that reply is consumed), and once correct every later message passes. -//! Reference: InspIRCd's `m_solvemsg`. Original native Rust. use crate::module::{ModResult, Module}; use crate::server::Server; @@ -17,7 +16,7 @@ struct SolveState { answer: Option<i64>, } -/// A uniform-ish random byte in `0..max` via openssl (no `rand` crate). +/// A uniform-ish random byte in `0..max` via the OpenSSL CSPRNG. fn rnd(max: u8) -> u8 { let mut b = [0u8; 1]; let _ = openssl::rand::rand_bytes(&mut b); diff --git a/src/modules/tline.rs b/src/modules/tline.rs index 166afa3..5f638cf 100644 --- a/src/modules/tline.rs +++ b/src/modules/tline.rs @@ -1,8 +1,5 @@ -//! tline — `TLINE <mask>`, an oper command that reports how many currently-connected -//! local users a would-be K/G/Z-line mask matches, so you can gauge the blast radius -//! before actually setting the ban. -//! -//! Behaviour reference: InspIRCd's `m_tline`. Original native Rust. +//! `TLINE <mask>` — oper command reporting how many currently-connected local users +//! a would-be K/G/Z-line mask matches, to gauge the blast radius before setting the ban. use crate::channels::glob_match; use crate::command::{CmdResult, Command}; diff --git a/src/modules/whoisport.rs b/src/modules/whoisport.rs index 3ba175a..ea30d68 100644 --- a/src/modules/whoisport.rs +++ b/src/modules/whoisport.rs @@ -1,6 +1,5 @@ -//! whoisport — InspIRCd `m_whoisport`. Shows an IRC operator, in WHOIS, the -//! listener port the target connected to. Config-free (derives the port from the -//! `bind` / `bind_tls` listeners); nothing lives on `Server`. +//! Shows an IRC operator, in WHOIS, the listener port the target connected to. +//! Derives the port from the `bind` / `bind_tls` listeners. use crate::server::Server; use crate::Uid; diff --git a/src/resolver.rs b/src/resolver.rs index 470ac55..6f126c6 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -1,11 +1,9 @@ -//! DNS lookups — echoIRCd's answer to InspIRCd's async resolver + `m_dnsbl`, done -//! from scratch with std UDP (no DNS crate, no `unsafe`). Two things: +//! DNS lookups over std UDP (no DNS crate). Two things: //! //! * **reverse-DNS**: PTR-resolve a client IP and **forward-confirm** it (the name -//! must resolve back to the same IP, so a client can't fake a hostname — the -//! anti-spoofing InspIRCd does); +//! must resolve back to the same IP, so a client can't fake a hostname); //! * **DNSBL**: reverse the client's v4 octets under a blocklist zone and A-lookup -//! it (`m_dnsbl` style), reporting the listing reply. +//! it, reporting the listing reply. //! //! Best-effort: any failure returns "not found / clean" and the caller keeps the //! IP. Runs off the core thread (never blocks the daemon), bounded in time (the UDP diff --git a/src/server.rs b/src/server.rs index 98230b4..27c3989 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,7 @@ //! The engine core: the `Server` struct that owns all state, the output //! primitives (send / numeric / to_channel) and the connection lifecycle. //! Per-subsystem behaviour lives beside its data — [`crate::users`] and -//! [`crate::channels`] add their own `impl Server` blocks, the way InspIRCd -//! keeps usermanager / channelmanager separate from the core. No locks: only the +//! [`crate::channels`] add their own `impl Server` blocks. No locks: only the //! single core thread ever holds a `Server`. use std::cell::RefCell; @@ -47,7 +46,7 @@ pub fn now() -> u64 { pub fn iso_time(secs: u64) -> String { let days = (secs / 86400) as i64; let (h, mi, s) = ((secs % 86400) / 3600, (secs % 3600) / 60, secs % 60); - // civil date from days since 1970-01-01 (Howard Hinnant's algorithm) + // civil date from days since 1970-01-01 let z = days + 719468; let era = if z >= 0 { z } else { z - 146096 } / 146097; let doe = z - era * 146097; @@ -74,7 +73,7 @@ pub fn parse_iso(s: &str) -> Option<u64> { let h: i64 = t.next()?.parse().ok()?; let mi: i64 = t.next()?.parse().ok()?; let se: i64 = t.next().unwrap_or("0").parse().ok()?; - // civil date -> days since 1970-01-01 (inverse Howard Hinnant) + // civil date -> days since 1970-01-01 let yy = y - i64::from(mo <= 2); let era = if yy >= 0 { yy } else { yy - 399 } / 400; let yoe = yy - era * 400; @@ -96,7 +95,7 @@ pub struct WhowasEntry { } /// One captured server log line (fed by `snotice`), for the RPC `log.tail` / -/// `log.events` methods — echoIRCd's in-memory answer to InspIRCd's log file. +/// `log.events` methods. In-memory ring, not a log file. pub struct LogLine { pub id: u64, pub ts: u64, @@ -123,14 +122,14 @@ pub struct Server { pub cloak_key: Option<String>, // host-cloaking key (see modules::cloak) pub line_ctags: String, // client-only tags of the line being handled // --- server-to-server (see crate::link) --- - pub sid: String, // our 3-char server id - pub server_desc: String, // our description - pub link_blocks: Vec<LinkBlock>, // peers we accept / dial + pub sid: String, // this server's 3-char id + pub server_desc: String, // this server's description + pub link_blocks: Vec<LinkBlock>, // peers to accept / dial pub links: HashMap<Uid, Link>, // local link connections pub servers: HashMap<String, RemoteServer>, // sid -> linked server pub uuid_counter: u64, // mints local user UIDs pub msgid_counter: u64, // mints IRCv3 `msgid` message tags - pub uuid_local: HashMap<String, Uid>, // our users, by network uuid + pub uuid_local: HashMap<String, Uid>, // local users, by network uuid pub remote_users: HashMap<String, RemoteUser>, // users on other servers pub remote_nick: HashMap<String, String>, // lower nick -> remote uuid pub whowas: VecDeque<WhowasEntry>, // recent nick history (WHOWAS) @@ -160,9 +159,8 @@ pub struct Server { pub log: RefCell<LogState>, pub event_tx: Sender<Event>, // self-inject events (DNS results) pub conn_counter: Arc<AtomicU64>, // mints connection uids (for CONNECT dials) - /// Module-owned server state, keyed by type — the InspIRCd `ExtensionItem` - /// equivalent. Each `modules/*.rs` stores its own struct here so features live - /// in their own file instead of bloating this one. + /// Module-owned server state, keyed by type. Each `modules/*.rs` stores its + /// own struct here so features live in their own file instead of this one. pub ext: Extensible, } @@ -352,9 +350,9 @@ impl Server { // connectban — z-line an IP range that opens too many connections (see modules::connectban) crate::modules::connectban::on_connect(self, ip); - // Pre-registration connection notices, InspIRCd / solanum style. Ident-113 - // is archaic and firewalled, so those two are cosmetic; the hostname lookup - // is real (see `resolver`) — its result arrives later as an Event. + // Pre-registration connection notices. The ident-113 notices are cosmetic + // (ident is archaic and firewalled); the hostname lookup is real (see + // `resolver`) and its result arrives later as an Event. self.notice_star(uid, "Checking Ident"); self.notice_star(uid, "No Ident response"); let do_rdns = self.resolve_hosts; @@ -437,8 +435,8 @@ impl Server { /// While a client's connect-time DNS/DNSBL lookups are still running, hold its /// handshake lines instead of processing them, so the "*** ..." notices print /// as one contiguous block rather than interleaving with the CAP/NICK replies. - /// Returns true if `line` was buffered. Bounded — past the cap we let lines - /// through (degrading to interleaved output rather than dropping input). + /// Returns true if `line` was buffered. Bounded — past the cap, lines pass + /// through (interleaved output rather than dropped input). pub fn defer_if_resolving(&mut self, uid: Uid, line: &str) -> bool { const MAX_DEFERRED: usize = 32; match self.users.get_mut(&uid) { @@ -463,7 +461,7 @@ impl Server { /// bans and cloaking use the hostname, not the IP), tell the client, and clear /// the flag that was holding their registration. pub fn on_resolved(&mut self, uid: Uid, host: Option<String>, outcome: dnsbl::Outcome) { - // hostname result (only announced if we actually attempted the lookup) + // hostname result (only announced if the lookup was attempted) match &host { Some(h) => self.notice_star(uid, &format!("Found your hostname ({h})")), None if self.resolve_hosts => self.notice_star( @@ -474,16 +472,16 @@ impl Server { } let apply = self.use_resolved_host; if let Some(u) = self.users.get_mut(&uid) { - // `use_resolved_host = off` keeps the IP in the hostmask even though we - // resolved and reported the name above. + // `use_resolved_host = off` keeps the IP in the hostmask even though the + // name was resolved and reported above. if apply { if let Some(h) = host { u.host = h; } } } - // DNSBL notices + action (InspIRCd m_dnsbl style) — see `modules::dnsbl`. - // May close the connection if the zone is listed and the action bans. + // DNSBL notices + action (see `modules::dnsbl`). May close the connection + // if the zone is listed and the action bans. dnsbl::report(self, uid, outcome); // release the registration hold (no-op if a DNSBL ban already removed them) if let Some(u) = self.users.get_mut(&uid) { @@ -520,10 +518,10 @@ impl Server { ); self.propagate(&format!(":{} QUIT :{reason}", user.uuid), None); // tell links } - // NB: we do *not* force-shutdown the socket here. When `user` drops at - // the end of this function its `out` Sender drops with it, so the writer - // thread drains any still-queued lines — e.g. a KILL / x-line ERROR — - // and then closes the socket itself once the channel is empty. + // The socket is not force-shut here. When `user` drops at the end of this + // function its `out` Sender drops with it, so the writer thread drains any + // still-queued lines — e.g. a KILL / x-line ERROR — and then closes the + // socket itself once the channel is empty. if !user.nick.is_empty() { self.nick_index.remove(&user.nick.to_ascii_lowercase()); } @@ -595,8 +593,14 @@ impl Server { let chathist = crate::modules::chathistory::limit(self); let maxnick = self.conf_num("maxnick", 30usize); let maxchan = self.conf_num("maxchannel", 50usize); + // operprefix/ojoin add the server oper prefix `y` (sigil `!`) above owner + let prefix = if self.conf_bool("operprefix", false) || self.conf_bool("ojoin", false) { + "(yqaohv)!~&@%+" + } else { + "(qaohv)~&@%+" + }; let mut lines = vec![format!( - "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,Gbcgjmnrsy WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}", + "CHANTYPES=# PREFIX={prefix} CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,Gbcgjmnrsy WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}", self.network )]; if let Some(tok) = crate::modules::network_icon::isupport(self) { @@ -610,7 +614,7 @@ impl Server { /// Emit the ISUPPORT numerics to `uid`. When `batched` (the client negotiated /// `draft/extended-isupport` + `batch`), wrap them in a `draft/isupport` BATCH so - /// the multi-line set arrives atomically (InspIRCd's m_ircv3_extended_isupport). + /// the multi-line set arrives atomically. pub fn send_isupport(&mut self, uid: Uid, batched: bool) { let lines = self.isupport_lines(); if batched { @@ -704,7 +708,7 @@ impl Server { } /// The escaped json-log value for `msg`, or `""` if none of `targets` want it - /// (so we skip building the JSON when no recipient has the cap). + /// (so the JSON isn't built when no recipient has the cap). fn json_log_value(&self, msg: &str, targets: &[Uid]) -> String { let wanted = targets .iter() @@ -883,8 +887,8 @@ impl Server { /// Change a user's displayed host and/or ident (CHGHOST/CHGIDENT/SETHOST/ /// SETIDENT). Announces it via the `chghost` cap to peers that speak it and - /// to the user, and sends RPL_HOSTHIDDEN (396) when the host changed. Mirrors - /// InspIRCd's ChangeDisplayedHost / ChangeIdent (local scope for now). + /// to the user, and sends RPL_HOSTHIDDEN (396) when the host changed. Local + /// scope for now. pub fn change_host_ident(&mut self, uid: Uid, new_ident: Option<&str>, new_host: Option<&str>) { let Some(u) = self.users.get(&uid) else { return; @@ -917,7 +921,7 @@ impl Server { // hostcycle — clients WITHOUT the chghost cap only learn the new host via a // PART+JOIN, so cycle them through each shared channel (chghost peers already // got the CHGHOST line above). Prefix modes are re-sent so they don't appear - // de-opped. InspIRCd `m_hostcycle`. + // de-opped. if new_host.is_some() || new_ident.is_some() { let (nick, new_prefix, acct, realname) = { let u = &self.users[&uid]; @@ -1005,7 +1009,7 @@ impl Server { } } else if u.ping_sent { if idle >= ping_after + ping_timeout { - quit.push(uid); // no reply to our PING + quit.push(uid); // no reply to the server PING } } else if idle >= ping_after { ping.push(uid); // idle — poke it @@ -1022,7 +1026,7 @@ mod tests { use crate::users::{valid_nick, User, UserFlags}; use std::sync::mpsc::{self, Receiver}; - /// Insert a registered user with an output channel we can read in the test. + /// Insert a registered user with an output channel readable in the test. fn add_user(s: &mut Server, uid: Uid, nick: &str) -> Receiver<String> { let (tx, rx) = mpsc::channel(); s.users.insert( diff --git a/src/socketengine.rs b/src/socketengine.rs index a2079d9..ecdaaba 100644 --- a/src/socketengine.rs +++ b/src/socketengine.rs @@ -2,15 +2,13 @@ //! //! - **Client plaintext** connections run on a single **mio epoll reactor** //! ([`run_reactor`]) — one thread drives tens of thousands of sockets, so the -//! daemon scales to ~50k users without a thread per connection. This is the -//! same readiness layer Tokio is built on; the core stays single-threaded and -//! there is no async runtime. +//! daemon scales to ~50k users without a thread per connection. The core stays +//! single-threaded and there is no async runtime. //! - **TLS** and **server links** keep a thread per connection (few of them, and //! a TLS session can't be split across reader+writer threads). //! //! Both hand the core the same [`OutSink`] output handle, so the core never -//! knows or cares which model a connection uses. (InspIRCd has a `socketengines/` -//! dir of epoll/kqueue/select backends; this is ours, written from scratch.) +//! knows or cares which model a connection uses. use std::collections::{HashMap, HashSet}; use std::io::{self, BufRead, BufReader, Read, Write}; diff --git a/src/tls.rs b/src/tls.rs index d0a0de6..490b410 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,12 +1,9 @@ -//! TLS backends — echoIRCd's answer to InspIRCd's `IOHook` seam and its -//! `ssl_openssl` / `ssl_gnutls` modules. A [`TlsBackend`] wraps an accepted -//! socket in a TLS session; the socket engine then drives the resulting -//! [`TlsConn`] for any listener that has a backend attached. +//! TLS backends: a [`TlsBackend`] wraps an accepted socket in a TLS session; the +//! socket engine then drives the resulting [`TlsConn`] for any listener that has a +//! backend attached. //! -//! This backend is openssl. The `openssl` crate keeps all its `unsafe` internal, -//! so the daemon itself stays `#![forbid(unsafe_code)]`. A pure-Rust `rustls` -//! backend (or a gnutls one) only has to implement these same two traits and it -//! slots straight in — exactly the pluggable-provider shape InspIRCd uses. +//! This backend is openssl. An alternative backend (e.g. rustls) only has to +//! implement these same two traits and it slots straight in. use std::io::{self, Read, Write}; use std::net::{Shutdown, TcpStream}; diff --git a/src/users.rs b/src/users.rs index fbe5b16..82875fc 100644 --- a/src/users.rs +++ b/src/users.rs @@ -1,6 +1,5 @@ //! Users: the `User` record plus nick handling, user modes, oper status and the -//! registration/welcome burst — the same job InspIRCd splits across users/ -//! usermanager, written from scratch in Rust (InspIRCd is a behaviour reference). +//! registration/welcome burst. use std::collections::HashSet; use std::net::{SocketAddr, TcpStream}; @@ -93,7 +92,7 @@ impl UserFlags { } } -/// The IRCv3 capabilities echoIRCd advertises. Order = the CAP LS order. +/// The IRCv3 capabilities advertised. Order = the CAP LS order. pub const SUPPORTED_CAPS: &[&str] = &[ "sasl", "server-time", @@ -125,9 +124,8 @@ pub const SUPPORTED_CAPS: &[&str] = &[ "cap-notify", ]; -/// Per-connection IRCv3 capability state — echoIRCd's answer to InspIRCd's `m_cap` -/// plus the individual `m_ircv3_*` modules, as one flat set (not a plugin per cap). -/// Toggled by `CAP REQ`; consulted wherever a line is formatted per-client. +/// Per-connection IRCv3 capability state, one flat set. Toggled by `CAP REQ`; +/// consulted wherever a line is formatted per-client. #[derive(Default)] pub struct Caps { pub sasl: bool, @@ -307,7 +305,7 @@ pub struct User { pub accept: Vec<String>, // ACCEPT list — lowercased nicks (callerid +g) pub quitting: Option<String>, // set by QUIT; drained by the core pub flags: UserFlags, - pub last_active: u64, // unix secs of the last line we received + pub last_active: u64, // unix secs of the last line received pub ping_sent: bool, // a server PING is outstanding pub ext: Extensible, // typed, module-owned per-user metadata pub out: OutSink, @@ -316,10 +314,9 @@ pub struct User { } impl User { - /// The host others see: an explicit vhost (CHGHOST/SETHOST) wins, then the - /// cloak when +x is set (and one was computed), otherwise the real host. - /// Everything that broadcasts a prefix — JOIN, QUIT, NICK, PRIVMSG source, ban - /// matching — goes through here, so the displayed host is consistent for free. + /// Displayed host: explicit vhost (CHGHOST/SETHOST), else cloak (when +x and + /// one was computed), else real host. Used everywhere a prefix is broadcast so + /// the shown host stays consistent. pub fn host_display(&self) -> &str { if let Some(v) = &self.vhost { v @@ -358,6 +355,8 @@ impl Server { self.numeric(uid, RPL_YOUREOPER, ":You are now an IRC operator"); self.send(uid, format!(":{} MODE {nick} :+os", self.name)); self.snotice(&format!("{nick} is now an IRC operator")); + // operprefix: give this oper the ! prefix in every channel they're already in + crate::modules::operprefix::grant_all(self, uid); // opermodes: extra umodes on oper-up let om = self.conf("opermodes").or_else(|| self.conf("oper_umodes")); if let Some(modes) = om.map(str::to_string) { diff --git a/src/websocket.rs b/src/websocket.rs index 2cd4ea0..bf3fe0b 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -1,12 +1,9 @@ -//! WebSocket transport (RFC 6455) — lets browser IRC clients (KiwiIRC, gamja, -//! The Lounge, reverse's Orbit, …) connect straight to echoIRCd, no Node bridge. -//! It's a transport, not a pluggable module, so it lives beside `tls.rs`/`http.rs` -//! at the I/O edge: a thread-per-connection listener that does the HTTP Upgrade -//! handshake, then frames the IRC byte stream in and out of WebSocket frames. One -//! thread owns each socket (like the TLS path) so frames never interleave. -//! -//! Native only: SHA-1 + base64 for the accept key come from OpenSSL; the framing -//! is hand-rolled; no new crate, no `unsafe`. +//! WebSocket transport (RFC 6455) — lets browser IRC clients connect straight to +//! echoIRCd, no bridge. It's a transport, not a pluggable module, so it lives +//! beside `tls.rs`/`http.rs` at the I/O edge: a thread-per-connection listener +//! that does the HTTP Upgrade handshake, then frames the IRC byte stream in and +//! out of WebSocket frames. One thread owns each socket (like the TLS path) so +//! frames never interleave. SHA-1 + base64 for the accept key come from OpenSSL. //! //! Config (flat keys): //! bind_ws = 127.0.0.1:8097 plaintext ws:// listener diff --git a/src/xline.rs b/src/xline.rs index 51c42a4..c76348c 100644 --- a/src/xline.rs +++ b/src/xline.rs @@ -1,7 +1,7 @@ -//! X-lines — server bans (InspIRCd's `m_xline`): KLINE/GLINE on `user@host`, -//! ZLINE on an IP. Matched at registration (a banned client is refused) and when -//! the line is added (matching clients are killed); expired lines are reaped on -//! the tick. Kept in `Server.xlines`. +//! X-lines — server bans: KLINE/GLINE on `user@host`, ZLINE on an IP. Matched at +//! registration (a banned client is refused) and when the line is added (matching +//! clients are killed); expired lines are reaped on the tick. Kept in +//! `Server.xlines`. use crate::channels::glob_match; use crate::server::{now, Server}; @@ -247,7 +247,7 @@ impl Server { format!("{}.xlines", self.conf_path) } - /// Persist all current x-lines so they survive a restart (InspIRCd `m_xline_db`). + /// Persist all current x-lines so they survive a restart. pub fn save_xlines(&self) { let mut out = String::new(); for x in &self.xlines {