operprefix + ojoin: server oper prefix (!/mode y, above owner) auto-granted to opers + OJOIN command

This commit is contained in:
Jean Chevronnet 2026-08-10 10:05:37 +00:00
parent 8ebb106f97
commit 1dd7f77ca8
106 changed files with 687 additions and 711 deletions

View file

@ -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

View file

@ -1,18 +1,12 @@
//! antimixedutf8 — blocks spam that mixes Unicode scripts within words (Latin
//! letters swapped for Cyrillic/Greek look-alikes: "e 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: "e 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!(

View file

@ -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",

View file

@ -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;

View file

@ -1,8 +1,7 @@
//! autoop — the channel list mode `+w <prefix>:<hostmask>` 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;

View file

@ -1,8 +1,7 @@
//! banredirect — a ban of the form `+b <mask>$<#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;

View file

@ -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>(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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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};

View file

@ -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
}

View file

@ -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 <token>`
//! 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 <token>` 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;

View file

@ -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 {

View file

@ -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 {

View file

@ -1,6 +1,6 @@
//! connflood — InspIRCd `m_connflood`. Refuse connections from an IP opening too
//! many too fast. Config: `connflood = <max> <secs>`. 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 = <max> <secs>`. Per-IP recent-connect times live in `Server.ext`,
//! pruned on the tick.
use std::collections::HashMap;
use std::net::IpAddr;

View file

@ -1,15 +1,13 @@
//! customtitle — `TITLE <name> <password>` 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 <name> <password>` lets a user claim a configured title
//! (shown in their WHOIS) and, optionally, a matching vhost. Config, one block per
//! title:
//!
//! ```text
//! customtitle = <name> <password> <vhost|*> <title text…>
//! ```
//!
//! 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;

View file

@ -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};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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('.')

View file

@ -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>);

View file

@ -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};

View file

@ -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]

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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() {

View file

@ -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};

View file

@ -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;

View file

@ -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;

View file

@ -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>() {

View file

@ -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()
}

View file

@ -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;

View file

@ -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;

55
src/modules/ojoin.rs Normal file
View file

@ -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
}
}

79
src/modules/operprefix.rs Normal file
View file

@ -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());
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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};

View file

@ -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

View file

@ -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;

View file

@ -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 \

View file

@ -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;

View file

@ -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};

View file

@ -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(|| {

View file

@ -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;

View file

@ -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};

View file

@ -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;

View file

@ -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> {

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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};

View file

@ -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;