operprefix + ojoin: server oper prefix (!/mode y, above owner) auto-granted to opers + OJOIN command
This commit is contained in:
parent
8ebb106f97
commit
1dd7f77ca8
106 changed files with 687 additions and 711 deletions
11
Cargo.toml
11
Cargo.toml
|
|
@ -2,7 +2,7 @@
|
||||||
name = "echoircd"
|
name = "echoircd"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
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"
|
license = "MIT"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
@ -14,12 +14,11 @@ name = "echoircd"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# TLS backend (InspIRCd ships ssl_openssl / ssl_gnutls as modules; this is our
|
# TLS backend: openssl. The crate keeps all `unsafe` internal, so the daemon
|
||||||
# openssl backend — the crate keeps all `unsafe` internal, so the daemon stays
|
# stays `#![forbid(unsafe_code)]`. A pure-Rust `rustls` backend can slot in beside it.
|
||||||
# `#![forbid(unsafe_code)]`). A pure-Rust `rustls` backend can slot in beside it.
|
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
# epoll/kqueue reactor for the client socket engine — the minimal readiness layer
|
# epoll/kqueue reactor for the client socket engine — a minimal readiness layer
|
||||||
# Tokio itself is built on. Lets one thread drive tens of thousands of connections
|
# (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),
|
# 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.
|
# so the daemon is still `#![forbid(unsafe_code)]`; no async runtime is pulled in.
|
||||||
mio = { version = "1", features = ["os-poll", "net"] }
|
mio = { version = "1", features = ["os-poll", "net"] }
|
||||||
|
|
|
||||||
34
README.md
34
README.md
|
|
@ -1,11 +1,10 @@
|
||||||
# echoIRCd
|
# echoIRCd
|
||||||
|
|
||||||
A from-scratch IRC daemon written in **native Rust**. The architecture is
|
A from-scratch IRC daemon written in Rust. Commands are objects, modes are
|
||||||
*inspired by* InspIRCd's shape — commands as objects, modes as handler objects,
|
handler objects, and modules hook lifecycle events. Design goals:
|
||||||
modules with lifecycle hooks — but every line is original Rust, not a port or a
|
`#![forbid(unsafe_code)]`, dependency-light (just two small crates — `openssl`
|
||||||
translation. Design goals: `#![forbid(unsafe_code)]`, dependency-light (just two
|
for TLS and `mio` for the epoll socket engine), and lock-free (a single core
|
||||||
small crates — `openssl` for TLS and `mio` for the epoll socket engine), and
|
thread owns all state).
|
||||||
lock-free (a single core thread owns all state).
|
|
||||||
|
|
||||||
> Status: early but capable. It boots, registers clients, speaks a large chunk of
|
> 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
|
> 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
|
- **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
|
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
|
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
|
release build and a high `LimitNOFILE`). It's a bare epoll/kqueue readiness
|
||||||
built on, but without pulling in an async runtime, so the single-threaded core
|
reactor — no async runtime is pulled in, so the single-threaded core is
|
||||||
is untouched.
|
untouched.
|
||||||
- **TLS and server links** keep a thread per connection — there are few of them,
|
- **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.
|
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
|
Both models hand the core the same `OutSink`, so it never knows or cares which one
|
||||||
a connection uses.
|
a connection uses.
|
||||||
|
|
||||||
Where this improves on the C++ original it's inspired by: `Uid` handles instead
|
Memory-safety by design: `Uid` handles instead of raw pointers (no use-after-free,
|
||||||
of raw `User*` (no use-after-free, no cull list), an `Extensible` typemap instead
|
no cull list), an `Extensible` typemap instead of `void*` module data (freed
|
||||||
of `void*` module data (freed automatically on drop), `&str` slices instead of
|
automatically on drop), `&str` slices, and compiled-in trait objects instead of a
|
||||||
`char*`, and compiled-in trait objects instead of a fragile `.so` ABI.
|
fragile `.so` ABI.
|
||||||
|
|
||||||
### The two extension points
|
### 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.
|
users and channels, nick-collision handling, netsplit.
|
||||||
- An **antimixedutf8** anti-spam module (blocks mixed-script look-alike spam).
|
- 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*
|
echoIRCd is original Rust — no code is copied or translated from any other
|
||||||
only — no code is copied or translated. `scripts/native-rust-guard.sh` enforces
|
project. `scripts/native-rust-guard.sh` enforces this (no `unsafe`, no C/FFI, and
|
||||||
this (no `unsafe`, no C/FFI, dependencies limited to `openssl` + `mio`, and no
|
dependencies limited to `openssl` + `mio`); it runs on every edit.
|
||||||
copy/translation wording in comments); it runs on every edit.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ resolve_hosts = on
|
||||||
# and reports "Found your hostname". Only matters when resolve_hosts = on.
|
# and reports "Found your hostname". Only matters when resolve_hosts = on.
|
||||||
use_resolved_host = 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:
|
# for multiple zones. On a listing, `dnsbl_action` decides what happens:
|
||||||
# mark = just show the "*** ... LISTED" notice, let them in (default, safe)
|
# mark = just show the "*** ... LISTED" notice, let them in (default, safe)
|
||||||
# kill = disconnect them (no persistent ban)
|
# kill = disconnect them (no persistent ban)
|
||||||
|
|
@ -84,14 +84,14 @@ amu_target = both
|
||||||
# --- connflood: refuse >max connections per <secs> from a single IP ---
|
# --- connflood: refuse >max connections per <secs> from a single IP ---
|
||||||
# connflood = 5 10
|
# connflood = 5 10
|
||||||
|
|
||||||
# --- security groups (UnrealIRCd-style): securitygroup = <name> [criteria...]
|
# --- security groups: securitygroup = <name> [criteria...]
|
||||||
# criteria: public tls insecure account unregistered oper exclude-oper
|
# criteria: public tls insecure account unregistered oper exclude-oper
|
||||||
# bot exclude-bot webirc exclude-webirc mask=<glob> exclude=<glob>
|
# bot exclude-bot webirc exclude-webirc mask=<glob> exclude=<glob>
|
||||||
# scoremin=<n> scoremax=<n> — use as an extban: MODE #c +b g:<name>
|
# scoremin=<n> scoremax=<n> — use as an extban: MODE #c +b g:<name>
|
||||||
# securitygroup = trusted account tls public
|
# securitygroup = trusted account tls public
|
||||||
# securitygroup = newbies scoremax=10 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: <conf>.reputation
|
# reputation_database = reputation.db # default: <conf>.reputation
|
||||||
# reputation_ipv4prefix = 32 # CIDR bits used to key IPv4 scores
|
# reputation_ipv4prefix = 32 # CIDR bits used to key IPv4 scores
|
||||||
# reputation_ipv6prefix = 64 # CIDR bits used to key IPv6 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_server = yes # hide 312
|
||||||
# hidewhois_hide_idle = yes # hide 317
|
# hidewhois_hide_idle = yes # hide 317
|
||||||
# hidewhois_hide_secure = yes # hide 671
|
# 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):
|
# staff can watch it in a normal window. Set the channel (create/keep it opped):
|
||||||
# chanlog = #snotices
|
# 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
|
# `b:<#channel>`, so `+b b:#staff` catches everyone banned in #staff (shares a
|
||||||
# ban list between channels).
|
# 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> <nick> <text> to speak under a spoofed relay
|
# capability can /RELAYMSG <#chan> <nick> <text> to speak under a spoofed relay
|
||||||
# nick (for bridges). The nick must contain a separator and not collide.
|
# nick (for bridges). The nick must contain a separator and not collide.
|
||||||
# relaymsg_separators = /
|
# relaymsg_separators = /
|
||||||
# relaymsg_ident = relay
|
# relaymsg_ident = relay
|
||||||
# relaymsg_host = relay.example.com # default: the server name
|
# 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.
|
# which shows "is available for help" in the user's WHOIS.
|
||||||
# --- globops (m_globops): no config — adds the oper command /GLOBOPS <message>,
|
# --- globops: no config — adds the oper command /GLOBOPS <message>,
|
||||||
# broadcasting to all opers (like the server-notice stream).
|
# 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):
|
# any of these commands (HTTP scanners blurt GET/POST before NICK/USER):
|
||||||
# autodrop_commands = GET POST HEAD CONNECT PUT DELETE OPTIONS TRACE PATCH
|
# 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,
|
# (the setter, opers and links always see it). Repeatable,
|
||||||
# `hidemode = <modechar> <rank>` (owner|admin|op|halfop|voice). e.g. hide bans:
|
# `hidemode = <modechar> <rank>` (owner|admin|op|halfop|voice). e.g. hide bans:
|
||||||
# hidemode = b op
|
# 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,
|
# default; this restricts a given list to a minimum rank. Repeatable,
|
||||||
# `hidelist = <modechar> <rank>` (rank: owner|admin|op|halfop|voice). Opers see
|
# `hidelist = <modechar> <rank>` (rank: owner|admin|op|halfop|voice). Opers see
|
||||||
# everything. e.g. only ops may view the ban list:
|
# everything. e.g. only ops may view the ban list:
|
||||||
# hidelist = b op
|
# 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 <prefix>:<hostmask>`, e.g.
|
# status prefix to matching users on join, `+w <prefix>:<hostmask>`, e.g.
|
||||||
# /MODE #chan +w o:*!*@trusted.host (auto-op)
|
# /MODE #chan +w o:*!*@trusted.host (auto-op)
|
||||||
# /MODE #chan +w v:*!*@*.friend.net (auto-voice)
|
# /MODE #chan +w v:*!*@*.friend.net (auto-voice)
|
||||||
# /MODE #chan +w lists the entries.
|
# /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 <mask>$<#channel>` bounces a matching user into #channel instead of
|
# ban `+b <mask>$<#channel>` bounces a matching user into #channel instead of
|
||||||
# refusing them, e.g. /MODE #main +b *!*@*.spammer.net$#quarantine
|
# refusing them, e.g. /MODE #main +b *!*@*.spammer.net$#quarantine
|
||||||
# The redirect fires at most once (never loops).
|
# 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
|
# question before their private messages are delivered (opers & logged-in
|
||||||
# accounts are exempt). Cheap anti-spam-bot gate.
|
# accounts are exempt). Cheap anti-spam-bot gate.
|
||||||
# solvemsg = yes
|
# solvemsg = yes
|
||||||
# --- dccallow (m_dccallow): block unwanted DCC transfers unless the recipient
|
# --- dccallow: block unwanted DCC transfers unless the recipient
|
||||||
# ran /DCCALLOW +<nick>. Blocked file globs are repeatable; blockchat also
|
# ran /DCCALLOW +<nick>. Blocked file globs are repeatable; blockchat also
|
||||||
# gates DCC CHAT. Recipients manage their allow-list with DCCALLOW +/-/LIST.
|
# gates DCC CHAT. Recipients manage their allow-list with DCCALLOW +/-/LIST.
|
||||||
# dccallow_blockfile = *.exe
|
# dccallow_blockfile = *.exe
|
||||||
# dccallow_blockfile = *.scr
|
# dccallow_blockfile = *.scr
|
||||||
# dccallow_blockchat = yes
|
# dccallow_blockchat = yes
|
||||||
# dccallow_maxentries = 20
|
# 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
|
# a server PING with the exact cookie — filters bots that never PONG. Real
|
||||||
# clients auto-reply, so it's transparent to them.
|
# clients auto-reply, so it's transparent to them.
|
||||||
# conn_waitpong = yes
|
# conn_waitpong = yes
|
||||||
# conn_waitpong_killonbadreply = yes # drop on a wrong pong (default: keep waiting)
|
# 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 = <COMMAND> <path>`. The file is read fresh each use, so
|
# file: `showfile = <COMMAND> <path>`. The file is read fresh each use, so
|
||||||
# edits show without a rehash. e.g. make /RULES stream a rules file:
|
# edits show without a rehash. e.g. make /RULES stream a rules file:
|
||||||
# showfile = RULES /etc/echoircd/rules.txt
|
# 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:<cc> ban extban (e.g. +b G:CN,RU), the oper GEOIP <nick|ip> command and
|
# G:<cc> ban extban (e.g. +b G:CN,RU), the oper GEOIP <nick|ip> command and
|
||||||
# a country line in WHOIS (opers). Point at a GeoLite2-Country.mmdb file:
|
# a country line in WHOIS (opers). Point at a GeoLite2-Country.mmdb file:
|
||||||
# geoip_database = /etc/echoircd/GeoLite2-Country.mmdb
|
# geoip_database = /etc/echoircd/GeoLite2-Country.mmdb
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# native-rust-guard — echoIRCd's standing invariant.
|
# native-rust-guard — echoIRCd's standing invariant.
|
||||||
#
|
#
|
||||||
# echoIRCd is ORIGINAL Rust. InspIRCd (and any other ircd) is a reference for
|
# echoIRCd is ORIGINAL Rust — no code copied or translated from any other project.
|
||||||
# BEHAVIOUR / protocol / API shape ONLY — never copied, never translated. Every
|
# Every module, command and core function is written natively in Rust. This guard
|
||||||
# module, command and core function is written natively in Rust. This guard fails
|
# fails if that slips. Run it any time: bash scripts/native-rust-guard.sh
|
||||||
# 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.
|
||||||
# It is also wired into a Claude Code hook so it runs automatically on edits.
|
|
||||||
set -u
|
set -u
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
cd "$ROOT" || exit 2
|
cd "$ROOT" || exit 2
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
//! Account layer — the ircd's *services-ready* account support, modelled on
|
//! Account layer — *services-ready* account support. **This is NOT a services
|
||||||
//! InspIRCd's `m_services_account`. **This is NOT a services daemon.**
|
//! daemon.**
|
||||||
//!
|
//!
|
||||||
//! echoIRCd stores no passwords and runs no NickServ — registering nicks/channels
|
//! 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
|
//! is a services package's job, linked in over S2S. The ircd owns only the
|
||||||
//! ircd owns is only the plumbing a service plugs into:
|
//! plumbing a service plugs into:
|
||||||
//! * a per-user **account name** (`User.account`) — the extension a service sets
|
//! * a per-user **account name** (`User.account`) — the `accountname` a service
|
||||||
//! or clears (InspIRCd's `accountname` metadata), which flips user mode `+r`;
|
//! sets or clears, which flips user mode `+r`;
|
||||||
//! * the account-gated **modes** (chan `+R`/`+M`, user `+r`/`+R`) that key off it
|
//! * the account-gated **modes** (chan `+R`/`+M`, user `+r`/`+R`) that key off it
|
||||||
//! and live in [`crate::mode`];
|
//! and live in [`crate::mode`];
|
||||||
//! * the **interface** a service drives it through: [`Server::set_login`] /
|
//! * the **interface** a service drives it through: [`Server::set_login`] /
|
||||||
//! [`Server::logout`], reached today via the oper/`SVSLOGIN` command and, once
|
//! [`Server::logout`], reached via the oper/`SVSLOGIN` command and, over S2S +
|
||||||
//! S2S + SASL land, by a linked services pseudoserver.
|
//! SASL, by a linked services pseudoserver.
|
||||||
//!
|
|
||||||
//! So a real network runs Anope *beside* echoIRCd; the ircd just has to be ready.
|
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! Channels: the `Channel` record, membership, channel modes, bans, invites and
|
//! Channels: the `Channel` record, membership, channel modes, bans, invites and
|
||||||
//! JOIN/NAMES — the same job InspIRCd splits across channels/channelmanager, but
|
//! JOIN/NAMES.
|
||||||
//! written from scratch in Rust (InspIRCd is a behaviour reference, not a source).
|
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
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`].
|
/// Per-member prefix modes (+q/+a/+o/+h/+v). Flag modes live in [`ChanModes`].
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Member {
|
pub struct Member {
|
||||||
|
pub oprefix: bool, // operprefix/ojoin: server oper prefix (!), highest rank
|
||||||
pub owner: bool, // +q (~)
|
pub owner: bool, // +q (~)
|
||||||
pub admin: bool, // +a (&)
|
pub admin: bool, // +a (&)
|
||||||
pub op: bool, // +o (@)
|
pub op: bool, // +o (@)
|
||||||
|
|
@ -24,6 +24,7 @@ pub struct Member {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prefix ranks, high→low — gate who may grant a prefix / kick whom.
|
/// 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_OWNER: u8 = 5;
|
||||||
pub const RANK_ADMIN: u8 = 4;
|
pub const RANK_ADMIN: u8 = 4;
|
||||||
pub const RANK_OP: u8 = 3;
|
pub const RANK_OP: u8 = 3;
|
||||||
|
|
@ -33,7 +34,9 @@ pub const RANK_VOICE: u8 = 1;
|
||||||
impl Member {
|
impl Member {
|
||||||
/// This member's numeric rank (0 = plain member).
|
/// This member's numeric rank (0 = plain member).
|
||||||
pub fn rank(&self) -> u8 {
|
pub fn rank(&self) -> u8 {
|
||||||
if self.owner {
|
if self.oprefix {
|
||||||
|
RANK_OPER
|
||||||
|
} else if self.owner {
|
||||||
RANK_OWNER
|
RANK_OWNER
|
||||||
} else if self.admin {
|
} else if self.admin {
|
||||||
RANK_ADMIN
|
RANK_ADMIN
|
||||||
|
|
@ -50,7 +53,9 @@ impl Member {
|
||||||
|
|
||||||
/// Highest prefix char for NAMES (`""` for a plain member).
|
/// Highest prefix char for NAMES (`""` for a plain member).
|
||||||
pub fn prefix_char(&self) -> &'static str {
|
pub fn prefix_char(&self) -> &'static str {
|
||||||
if self.owner {
|
if self.oprefix {
|
||||||
|
"!"
|
||||||
|
} else if self.owner {
|
||||||
"~"
|
"~"
|
||||||
} else if self.admin {
|
} else if self.admin {
|
||||||
"&"
|
"&"
|
||||||
|
|
@ -68,6 +73,7 @@ impl Member {
|
||||||
/// Set/clear a prefix mode by its letter (used by the S2S mode applier).
|
/// Set/clear a prefix mode by its letter (used by the S2S mode applier).
|
||||||
pub fn set_prefix(&mut self, letter: char, on: bool) {
|
pub fn set_prefix(&mut self, letter: char, on: bool) {
|
||||||
match letter {
|
match letter {
|
||||||
|
'y' => self.oprefix = on,
|
||||||
'q' => self.owner = on,
|
'q' => self.owner = on,
|
||||||
'a' => self.admin = on,
|
'a' => self.admin = on,
|
||||||
'o' => self.op = on,
|
'o' => self.op = on,
|
||||||
|
|
@ -81,6 +87,7 @@ impl Member {
|
||||||
pub fn all_prefixes(&self) -> String {
|
pub fn all_prefixes(&self) -> String {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
for (on, c) in [
|
for (on, c) in [
|
||||||
|
(self.oprefix, '!'),
|
||||||
(self.owner, '~'),
|
(self.owner, '~'),
|
||||||
(self.admin, '&'),
|
(self.admin, '&'),
|
||||||
(self.op, '@'),
|
(self.op, '@'),
|
||||||
|
|
@ -329,8 +336,7 @@ impl Channel {
|
||||||
self.members.is_empty() && self.rmembers.is_empty()
|
self.members.is_empty() && self.rmembers.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to keep this channel in the table: it has members, or it's +P
|
/// Keep this channel in the table: it has members, or it's +P (permanent).
|
||||||
/// (permanent). The predicate every `channels.retain` prune uses.
|
|
||||||
pub fn keep_alive(&self) -> bool {
|
pub fn keep_alive(&self) -> bool {
|
||||||
!self.is_empty() || self.modes.permanent
|
!self.is_empty() || self.modes.permanent
|
||||||
}
|
}
|
||||||
|
|
@ -339,8 +345,7 @@ impl Channel {
|
||||||
impl Server {
|
impl Server {
|
||||||
/// A member's channel rank (0 if not a member).
|
/// A member's channel rank (0 if not a member).
|
||||||
pub fn rank(&self, uid: Uid, key: &str) -> u8 {
|
pub fn rank(&self, uid: Uid, key: &str) -> u8 {
|
||||||
// SAMODE/SAKICK run as the server: every access check keys off rank(),
|
// SAMODE/SAKICK: mode_sudo makes every rank() check pass, bypassing the ladder.
|
||||||
// so a transient sudo makes them bypass the ladder cleanly.
|
|
||||||
if self.mode_sudo {
|
if self.mode_sudo {
|
||||||
return RANK_OWNER;
|
return RANK_OWNER;
|
||||||
}
|
}
|
||||||
|
|
@ -516,8 +521,8 @@ impl Server {
|
||||||
{
|
{
|
||||||
return; // unknown user, or already joined
|
return; // unknown user, or already joined
|
||||||
}
|
}
|
||||||
// IRC operators override the join restrictions below (m_override); each
|
// IRC operators override the join restrictions below; each bypass sets
|
||||||
// bypass sets `overrode`, snoticed once the join succeeds (accountability).
|
// `overrode`, snoticed once the join succeeds.
|
||||||
let is_oper = self.users.get(&uid).map(|u| u.flags.oper).unwrap_or(false);
|
let is_oper = self.users.get(&uid).map(|u| u.flags.oper).unwrap_or(false);
|
||||||
let mut overrode = false;
|
let mut overrode = false;
|
||||||
// CBAN — a forbidden channel name (opers bypass)
|
// 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
|
/// Whether any entry in `list` catches `uid`: a plain `nick!user@host` glob, or
|
||||||
/// `key` with no matching `kind:` exception in +e. The stored mask is
|
/// a matching extban (`g:` group, `y:` reputation, `r:` realname, `j:` channel,
|
||||||
/// `kind:<hostmask>`; we glob the hostmask part against the user's prefix.
|
/// `s:` server, `G:` geoip, `b:` banlist). Acting extbans (`m:`/`c:`/`n:`) never
|
||||||
/// Whether any entry in `list` catches `uid`: a plain `nick!user@host` glob,
|
/// match here — they restrict actions, not join/ban membership.
|
||||||
/// or the `g:<group>` security-group matching extban. 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 {
|
pub fn ban_list_hit(&self, uid: Uid, list: &[Ban]) -> bool {
|
||||||
let who = self.users.get(&uid).map(|u| u.prefix()).unwrap_or_default();
|
let who = self.users.get(&uid).map(|u| u.prefix()).unwrap_or_default();
|
||||||
list.iter().any(|b| {
|
list.iter().any(|b| {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
//! The command API — echoIRCd's answer to InspIRCd's `Command` class.
|
//! Command API: a stateless handler registered by name. The core validates
|
||||||
//!
|
|
||||||
//! A command is a stateless handler registered by name. The core validates
|
|
||||||
//! `min_params` and the registration gate (`before_reg`) before calling
|
//! `min_params` and the registration gate (`before_reg`) before calling
|
||||||
//! [`Command::handle`], which gets `&mut Server` and does the work.
|
//! [`Command::handle`], which gets `&mut Server` and does the work.
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
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)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum CmdResult {
|
pub enum CmdResult {
|
||||||
Ok,
|
Ok,
|
||||||
|
|
|
||||||
|
|
@ -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
|
//! ```text
|
||||||
//! servername = echo.devtronic.pro
|
//! servername = echo.devtronic.pro
|
||||||
|
|
@ -137,8 +137,7 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`load`](Config::load) but returns `None` if the file can't be read,
|
/// 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 —
|
/// so REHASH can keep the running config instead of resetting to defaults.
|
||||||
/// the way InspIRCd keeps the old config when a reload fails.
|
|
||||||
pub fn try_load(path: &str) -> Option<Config> {
|
pub fn try_load(path: &str) -> Option<Config> {
|
||||||
let text = std::fs::read_to_string(path).ok()?;
|
let text = std::fs::read_to_string(path).ok()?;
|
||||||
let mut c = Config {
|
let mut c = Config {
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TBAN — set a +b ban that lifts itself after a duration (InspIRCd `m_timedbans`).
|
/// TBAN — set a +b ban that lifts itself after a duration. `TBAN <#chan>
|
||||||
/// `TBAN <#chan> <duration> <mask>`; needs half-op or above. The background tick
|
/// <duration> <mask>`; needs half-op or above. The background tick removes it and
|
||||||
/// removes it and announces `MODE -b` when it expires.
|
/// announces `MODE -b` when it expires.
|
||||||
struct Tban;
|
struct Tban;
|
||||||
impl Command for Tban {
|
impl Command for Tban {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
@ -313,8 +313,8 @@ impl Command for Invite {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// UNINVITE — revoke a pending invite (InspIRCd `m_uninvite`). `UNINVITE <nick>
|
/// UNINVITE — revoke a pending invite. `UNINVITE <nick> <#chan>`; a channel op
|
||||||
/// <#chan>`; a channel op cancels an invite they (or another op) issued.
|
/// cancels an invite they (or another op) issued.
|
||||||
struct Uninvite;
|
struct Uninvite;
|
||||||
impl Command for Uninvite {
|
impl Command for Uninvite {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ impl Command for Info {
|
||||||
fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult {
|
fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult {
|
||||||
for line in [
|
for line in [
|
||||||
format!("echoircd-{VERSION} — a from-scratch IRC daemon in Rust"),
|
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),
|
format!("Running the {} network", s.network),
|
||||||
] {
|
] {
|
||||||
s.numeric(uid, RPL_INFO, &format!(":{line}"));
|
s.numeric(uid, RPL_INFO, &format!(":{line}"));
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SSLINFO — report a user's TLS status and client-cert fingerprint (InspIRCd
|
/// SSLINFO — report a user's TLS status and client-cert fingerprint. You may
|
||||||
/// `m_sslinfo`). You may query yourself; querying another user requires oper.
|
/// query yourself; querying another user requires oper.
|
||||||
struct SslInfo;
|
struct SslInfo;
|
||||||
impl Command for SslInfo {
|
impl Command for SslInfo {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
|
||||||
|
|
@ -649,7 +649,7 @@ impl Command for Notice {
|
||||||
|
|
||||||
/// TAGMSG — an IRCv3 message that carries only client tags (typing, reactions, …)
|
/// TAGMSG — an IRCv3 message that carries only client tags (typing, reactions, …)
|
||||||
/// and no text. Relayed to targets whose clients enabled `message-tags`; clients
|
/// 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;
|
struct TagMsg;
|
||||||
impl Command for TagMsg {
|
impl Command for TagMsg {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! core_mode — the MODE command. Both channel and user modes are dispatched to
|
//! MODE: parse the modestring and dispatch each letter to its handler in
|
||||||
//! the handler objects in [`crate::mode`] (InspIRCd-style `ModeHandler`s); this
|
//! [`crate::mode`] (channel and user modes alike).
|
||||||
//! file just parses the modestring and orchestrates.
|
|
||||||
|
|
||||||
use crate::channels::RANK_HALFOP;
|
use crate::channels::RANK_HALFOP;
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! core_oper — IRC operator commands: OPER, KILL, WALLOPS. Mirrors InspIRCd's
|
//! IRC operator commands: OPER, KILL, WALLOPS, the SA*/SVS* set, X-lines, and
|
||||||
//! `coremods/core_oper/`. Oper blocks are configured with `oper = name pass`.
|
//! the oper CHG*/SET* tools. Oper blocks are configured with `oper = name pass`.
|
||||||
|
|
||||||
use crate::channels::Topic;
|
use crate::channels::Topic;
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
|
|
@ -56,8 +56,8 @@ pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OPERMOTD — show the IRC-operators' message of the day (InspIRCd `m_opermotd`),
|
/// OPERMOTD — show the IRC-operators' message of the day, configured with
|
||||||
/// configured with repeated `opermotd = <line>` entries.
|
/// repeated `opermotd = <line>` entries.
|
||||||
struct OperMotd;
|
struct OperMotd;
|
||||||
impl Command for OperMotd {
|
impl Command for OperMotd {
|
||||||
fn name(&self) -> &'static str {
|
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
|
/// 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);
|
pub struct Swhois(pub String);
|
||||||
|
|
||||||
/// Reject non-opers with 481; returns whether the caller is an oper.
|
/// 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
|
/// SVSLOGIN / SVSLOGOUT — the services interface to the account layer
|
||||||
/// ([`crate::accounts`]). Over S2S these arrive from a services pseudoserver
|
/// ([`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
|
/// until S2S exists an oper may invoke them to drive `+r` and the account-gated
|
||||||
/// account-gated channel modes. `SVSLOGIN <nick> <account>` logs a user in
|
/// channel modes. `SVSLOGIN <nick> <account>` logs a user in (`account` of `*`/`0`
|
||||||
/// (`account` of `*`/`0` logs out); `SVSLOGOUT <nick>` logs them out.
|
/// logs out); `SVSLOGOUT <nick>` logs them out.
|
||||||
struct SvsLogin;
|
struct SvsLogin;
|
||||||
impl Command for SvsLogin {
|
impl Command for SvsLogin {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
@ -415,8 +415,8 @@ impl Command for SaNick {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SVS* : the services interface. Same enforcement as the SA* oper commands,
|
// --- SVS* : the services interface. Same enforcement as the SA* oper commands,
|
||||||
// under the names a services package speaks (like SVSLOGIN). Gated to opers/
|
// under the names a services package speaks. Gated to opers/services; a linked
|
||||||
// services; a linked services pseudoserver drives these once S2S routes them.
|
// services pseudoserver drives these once S2S routes them.
|
||||||
|
|
||||||
/// SVSNICK — force a nick change (nick-registration enforcement). An optional
|
/// SVSNICK — force a nick change (nick-registration enforcement). An optional
|
||||||
/// third param is the new-nick TS, accepted and ignored (single-TS model).
|
/// 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
|
/// CBAN — forbid a channel-name glob (opers bypass it). Mask alone removes; a
|
||||||
/// mask + duration adds. InspIRCd `m_cban`.
|
/// mask + duration adds.
|
||||||
struct Cban;
|
struct Cban;
|
||||||
impl Command for Cban {
|
impl Command for Cban {
|
||||||
fn name(&self) -> &'static str {
|
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
|
/// NICKLOCK — force a user's nick and lock it so they can't change it.
|
||||||
/// `m_nicklock`). `NICKLOCK <nick> <newnick>`; opers/services still can.
|
/// `NICKLOCK <nick> <newnick>`; opers/services still can.
|
||||||
struct NickLock;
|
struct NickLock;
|
||||||
impl Command for NickLock {
|
impl Command for NickLock {
|
||||||
fn name(&self) -> &'static str {
|
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
|
/// SAQUIT — force a user to quit the network. Looks to everyone like a normal
|
||||||
/// everyone like a normal client QUIT.
|
/// client QUIT.
|
||||||
struct SaQuit;
|
struct SaQuit;
|
||||||
impl Command for SaQuit {
|
impl Command for SaQuit {
|
||||||
fn name(&self) -> &'static str {
|
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
|
/// CHGNAME — change another user's real name (the oper-driven counterpart to
|
||||||
/// counterpart to SETNAME; broadcast to `setname`-capable peers so clients update live.
|
/// SETNAME); broadcast to `setname`-capable peers so clients update live.
|
||||||
struct ChgName;
|
struct ChgName;
|
||||||
impl Command for ChgName {
|
impl Command for ChgName {
|
||||||
fn name(&self) -> &'static str {
|
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
|
/// CLEARCHAN — kick every user out of a channel. Each removal is a normal KICK,
|
||||||
/// removal is a normal KICK, propagated like SAKICK.
|
/// propagated like SAKICK.
|
||||||
struct ClearChan;
|
struct ClearChan;
|
||||||
impl Command for ClearChan {
|
impl Command for ClearChan {
|
||||||
fn name(&self) -> &'static str {
|
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;
|
struct Check;
|
||||||
impl Command for Check {
|
impl Command for Check {
|
||||||
fn name(&self) -> &'static str {
|
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 — attach (or clear) an extra WHOIS line on a user. `SWHOIS <nick>
|
||||||
/// `SWHOIS <nick> :<text>`; an empty text removes it. Shown as RPL_WHOISSPECIAL.
|
/// :<text>`; an empty text removes it. Shown as RPL_WHOISSPECIAL.
|
||||||
struct SwhoisCmd;
|
struct SwhoisCmd;
|
||||||
impl Command for SwhoisCmd {
|
impl Command for SwhoisCmd {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
@ -1361,8 +1361,8 @@ impl Command for SwhoisCmd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SETIDLE — reset your own idle time (InspIRCd `m_setidle`). `SETIDLE <seconds>`
|
/// SETIDLE — reset your own idle time. `SETIDLE <seconds>` backdates the
|
||||||
/// backdates the last-activity clock so WHOIS shows that idle time.
|
/// last-activity clock so WHOIS shows that idle time.
|
||||||
struct SetIdle;
|
struct SetIdle;
|
||||||
impl Command for SetIdle {
|
impl Command for SetIdle {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
@ -1384,8 +1384,8 @@ impl Command for SetIdle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ALLTIME — show the current server time to the requesting oper (InspIRCd
|
/// ALLTIME — show the current server time to the requesting oper (on a single
|
||||||
/// `m_alltime`; on a single server there's just the one time to report).
|
/// server there's just the one time to report).
|
||||||
struct AllTime;
|
struct AllTime;
|
||||||
impl Command for AllTime {
|
impl Command for AllTime {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
//! core_rehash — the REHASH command. Re-reads the config file and applies every
|
//! REHASH: re-read the config file and apply every setting that can change at
|
||||||
//! setting that can change at runtime, the way InspIRCd's rehash does:
|
//! runtime (opers only; replies RPL_REHASHING 382). Takes an optional
|
||||||
//!
|
//! `<servermask>`, matched against this server's name (no remote rehash over S2S).
|
||||||
//! * opers only; replies with RPL_REHASHING (382) and a server-notice to +s opers;
|
//! A missing/unreadable config file leaves the running config intact (via
|
||||||
//! * takes an optional `<servermask>` (we only rehash if it matches this server —
|
//! `Config::try_load`), so opers/cloak-key are never reset to defaults.
|
||||||
//! 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.
|
|
||||||
//!
|
//!
|
||||||
//! Reloadable live: MOTD, oper blocks, cloak key, +G censor words, antimixedutf8,
|
//! Reloadable live: MOTD, oper blocks, cloak key, +G censor words, antimixedutf8,
|
||||||
//! and the reverse-DNS options. Listener/bind/SID changes still need a restart.
|
//! 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();
|
let path = s.conf_path.clone();
|
||||||
match Config::try_load(&path) {
|
match Config::try_load(&path) {
|
||||||
Some(fresh) => {
|
Some(fresh) => {
|
||||||
// echoIRCd's own announcement (not InspIRCd's) — broadcast to
|
// announce to everyone connected, not just opers
|
||||||
// everyone connected, not just opers.
|
|
||||||
s.announce(&format!(
|
s.announce(&format!(
|
||||||
"admin {who} has changed the configuration of the server."
|
"admin {who} has changed the configuration of the server."
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// VHOST — claim a self-service virtual host with `VHOST <user> <pass>` matching a
|
/// VHOST — claim a self-service virtual host with `VHOST <user> <pass>` matching a
|
||||||
/// configured `vhost = <user> <pass> <host>` block (InspIRCd `m_vhost`).
|
/// configured `vhost = <user> <pass> <host>` block.
|
||||||
struct Vhost;
|
struct Vhost;
|
||||||
impl Command for Vhost {
|
impl Command for Vhost {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
@ -247,10 +247,10 @@ fn cap_target(s: &Server, uid: Uid) -> String {
|
||||||
.unwrap_or_else(|| "*".to_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
|
/// 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
|
/// it and `set_login` applied on success. With no services linked, SASL fails
|
||||||
/// no services — SASL fails cleanly.
|
/// cleanly.
|
||||||
struct Authenticate;
|
struct Authenticate;
|
||||||
impl Command for Authenticate {
|
impl Command for Authenticate {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
@ -274,7 +274,7 @@ impl Command for Authenticate {
|
||||||
let arg = ¶ms[0];
|
let arg = ¶ms[0];
|
||||||
let mech = s.users.get(&uid).and_then(|u| u.sasl_mech.clone());
|
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`);
|
// 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();
|
let have_services = s.sasl_link().is_some();
|
||||||
match mech {
|
match mech {
|
||||||
// step 1 — the client picks a mechanism
|
// step 1 — the client picks a mechanism
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
//! core_watch — WATCH, MONITOR (IRCv3) and SILENCE. The per-user lists live on
|
//! WATCH, MONITOR (IRCv3), SILENCE and ACCEPT. The per-user lists live on the
|
||||||
//! the `User`; the online/offline notifications are driven from the lifecycle
|
//! `User`; online/offline notifications are driven from the lifecycle code via
|
||||||
//! code via [`crate::server::Server::watch_notify_online`] / `_offline`. Mirrors
|
//! [`crate::server::Server::watch_notify_online`] / `_offline`.
|
||||||
//! InspIRCd's `m_watch` / `m_monitor` / `m_silence`.
|
|
||||||
|
|
||||||
use crate::channels::normalize_mask;
|
use crate::channels::normalize_mask;
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
//! The built-in commands, grouped the way InspIRCd groups its `coremods/`:
|
//! Built-in commands. Each module exposes `commands()`; [`command_table`]
|
||||||
//! `core_user`, `core_channel`, `core_message`, `core_mode`, `core_oper`,
|
//! assembles the registry the core dispatches through.
|
||||||
//! `core_info`. Each module exposes `commands()`; [`command_table`] assembles the
|
|
||||||
//! registry the core dispatches through.
|
|
||||||
|
|
||||||
pub mod core_channel;
|
pub mod core_channel;
|
||||||
pub mod core_extra;
|
pub mod core_extra;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,7 @@
|
||||||
//! Typed per-object metadata — echoIRCd's answer to InspIRCd's `Extensible` /
|
//! Typed per-object metadata: a `TypeId`-keyed typemap. A module stores its own
|
||||||
//! `ExtensionItem`.
|
//! 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,
|
||||||
//! In C++ InspIRCd, a module attaches data to a user/channel through a `void*`
|
//! no manual free, no dangling data.
|
||||||
//! `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.
|
|
||||||
|
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Minimal blocking HTTP/HTTPS client — `std::net::TcpStream` + openssl for TLS.
|
//! Minimal blocking HTTP/HTTPS client — `std::net::TcpStream` + openssl for TLS.
|
||||||
//! No new crate, no `unsafe`. Modules that talk to external APIs (account
|
//! Modules that talk to external APIs (account registration, captcha
|
||||||
//! registration, captcha verification, …) use this from a **worker thread** and
|
//! verification, …) use this from a **worker thread** and deliver the result back
|
||||||
//! deliver the result back to the core as an [`crate::ircd::Event`], exactly like
|
//! to the core as an [`crate::ircd::Event`], so a slow or hung endpoint never
|
||||||
//! the DNS/DNSBL lookups — so a slow or hung endpoint never blocks the main loop.
|
//! blocks the main loop.
|
||||||
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
|
|
|
||||||
18
src/ircd.rs
18
src/ircd.rs
|
|
@ -1,6 +1,6 @@
|
||||||
//! The core: owns the [`Server`] state, the command table and the module list,
|
//! 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
|
//! and turns a stream of [`Event`]s into IRC. Runs on one thread, so no state is
|
||||||
//! thread, so no state is ever locked.
|
//! ever locked.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{SocketAddr, TcpStream};
|
use std::net::{SocketAddr, TcpStream};
|
||||||
|
|
@ -92,10 +92,10 @@ impl Ircd {
|
||||||
conn_counter: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
conn_counter: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
) -> Ircd {
|
) -> Ircd {
|
||||||
let mut server = Server::new(cfg, event_tx, conn_counter);
|
let mut server = Server::new(cfg, event_tx, conn_counter);
|
||||||
server.load_xlines(); // restore persisted bans (m_xline_db)
|
server.load_xlines(); // restore persisted bans
|
||||||
crate::modules::metadata::load(&mut server); // restore channel metadata (m_metadata_db)
|
crate::modules::metadata::load(&mut server); // restore channel metadata
|
||||||
crate::modules::reputation::load(&mut server); // restore per-IP reputation
|
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 {
|
Ircd {
|
||||||
server,
|
server,
|
||||||
commands: command_table(),
|
commands: command_table(),
|
||||||
|
|
@ -283,13 +283,13 @@ impl Ircd {
|
||||||
|
|
||||||
let Some(handler) = self.commands.get(cmd) else {
|
let Some(handler) = self.commands.get(cmd) else {
|
||||||
if registered {
|
if registered {
|
||||||
// showfile (m_showfile): config `showfile = <CMD> <path>` streams a
|
// config `showfile = <CMD> <path>` streams a text file as its own
|
||||||
// text file as its own command (e.g. /RULES), like a config-named alias.
|
// command (e.g. /RULES).
|
||||||
if crate::modules::showfile::maybe_show(&mut self.server, uid, cmd) {
|
if crate::modules::showfile::maybe_show(&mut self.server, uid, cmd) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// command aliases (m_alias): config `alias = <CMD> <target-nick>`
|
// config `alias = <CMD> <target-nick>`: `alias = NS NickServ` makes
|
||||||
// e.g. `alias = NS NickServ` makes `/NS help` -> PRIVMSG NickServ :help
|
// `/NS help` -> PRIVMSG NickServ :help
|
||||||
if let Some(target) = self.server.conf_all("alias").iter().find_map(|line| {
|
if let Some(target) = self.server.conf_all("alias").iter().find_map(|line| {
|
||||||
let mut it = line.split_whitespace();
|
let mut it = line.split_whitespace();
|
||||||
match (it.next(), it.next()) {
|
match (it.next(), it.next()) {
|
||||||
|
|
|
||||||
|
|
@ -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`,
|
//! - **engine** — `server` (the core + state), `users`, `channels`, `message`,
|
||||||
//! `numeric`, `config`.
|
//! `numeric`, `config`.
|
||||||
//! - **`coremods`** — the built-in commands, grouped the way InspIRCd groups its
|
//! - **`coremods`** — the built-in commands (core_user, core_channel,
|
||||||
//! `coremods/` (core_user, core_channel, core_message, core_mode, core_info).
|
//! core_message, core_mode, core_info).
|
||||||
//! - **`modules`** — optional, pluggable behaviour via lifecycle hooks.
|
//! - **`modules`** — optional, pluggable behaviour via lifecycle hooks.
|
||||||
//! - **`socketengine`** — the I/O edge (accept + per-connection threads).
|
//! - **`socketengine`** — the I/O edge (accept + per-connection threads).
|
||||||
//! - **`ircd`** — the single-threaded core loop that ties it together.
|
//! - **`ircd`** — the single-threaded core loop that ties it together.
|
||||||
|
|
|
||||||
|
|
@ -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
|
//! 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
|
//! `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`
|
//! * **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).
|
//! that clashes with a local user kills the local (both sides ⇒ both vanish).
|
||||||
//! * **netsplit** — dropping a link QUITs every user behind it.
|
//! * **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};
|
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 {
|
pub fn valid_sid(s: &str) -> bool {
|
||||||
let b = s.as_bytes();
|
let b = s.as_bytes();
|
||||||
b.len() == 3
|
b.len() == 3
|
||||||
|
|
@ -79,7 +76,7 @@ pub fn valid_sid(s: &str) -> bool {
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
/// Mint the next network-wide UID for a local user: our SID + 6 base-26 chars
|
/// 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 {
|
pub fn next_uuid(&mut self) -> String {
|
||||||
let mut x = self.uuid_counter;
|
let mut x = self.uuid_counter;
|
||||||
self.uuid_counter += 1;
|
self.uuid_counter += 1;
|
||||||
|
|
|
||||||
41
src/mode.rs
41
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`];
|
//! Channel modes implement [`ChanMode`] and user modes implement [`UserMode`];
|
||||||
//! the MODE command parses the modestring and dispatches to the handler for each
|
//! 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
|
//! letter, so adding a mode is a new handler + one line in a table — never an
|
||||||
//! edit to the parser.
|
//! 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.
|
||||||
//! 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.
|
|
||||||
|
|
||||||
use crate::channels::{
|
use crate::channels::{
|
||||||
normalize_ban_mask, Ban, ChanModes, Channel, MsgFlood, Rate, RANK_ADMIN, RANK_HALFOP, RANK_OP,
|
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
|
/// `+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
|
/// current member is already on TLS (else `ERR_ALLMUSTSSL`); the join-time block
|
||||||
|
|
@ -884,8 +879,8 @@ impl ChanMode for RedirectMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// +B `<percent>` — reject channel messages that are at least `<percent>` uppercase
|
/// +B `<percent>` — reject channel messages that are at least `<percent>` uppercase.
|
||||||
/// (InspIRCd `m_anticaps`). Enforced in the message path; ops are exempt.
|
/// Enforced in the message path; ops are exempt.
|
||||||
struct AntiCapsMode;
|
struct AntiCapsMode;
|
||||||
static ANTICAPS: AntiCapsMode = AntiCapsMode;
|
static ANTICAPS: AntiCapsMode = AntiCapsMode;
|
||||||
impl ChanMode for AntiCapsMode {
|
impl ChanMode for AntiCapsMode {
|
||||||
|
|
@ -922,8 +917,8 @@ impl ChanMode for AntiCapsMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// +J `<secs>` — after being kicked, a user can't rejoin for `<secs>` seconds
|
/// +J `<secs>` — after being kicked, a user can't rejoin for `<secs>` seconds.
|
||||||
/// (InspIRCd `m_kicknorejoin`). Enforced in `Server::join`.
|
/// Enforced in `Server::join`.
|
||||||
struct KickNoRejoinMode;
|
struct KickNoRejoinMode;
|
||||||
static KICKNOREJOIN: KickNoRejoinMode = KickNoRejoinMode;
|
static KICKNOREJOIN: KickNoRejoinMode = KickNoRejoinMode;
|
||||||
impl ChanMode for KickNoRejoinMode {
|
impl ChanMode for KickNoRejoinMode {
|
||||||
|
|
@ -963,8 +958,8 @@ impl ChanMode for KickNoRejoinMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// +d `<secs>` — a newly-joined member can't speak for `<secs>` seconds (InspIRCd
|
/// +d `<secs>` — a newly-joined member can't speak for `<secs>` seconds.
|
||||||
/// `m_delaymsg`). Enforced in the message path; voiced-or-above are exempt.
|
/// Enforced in the message path; voiced-or-above are exempt.
|
||||||
struct DelayMsgMode;
|
struct DelayMsgMode;
|
||||||
static DELAYMSG: DelayMsgMode = DelayMsgMode;
|
static DELAYMSG: DelayMsgMode = DelayMsgMode;
|
||||||
impl ChanMode for DelayMsgMode {
|
impl ChanMode for DelayMsgMode {
|
||||||
|
|
@ -1005,7 +1000,7 @@ impl ChanMode for DelayMsgMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// +K `<n>` — block a message identical to one of the sender's previous `<n>`
|
/// +K `<n>` — block a message identical to one of the sender's previous `<n>`
|
||||||
/// lines in this channel (InspIRCd `m_repeat`, simplified). Ops are exempt.
|
/// lines in this channel. Ops are exempt.
|
||||||
struct RepeatMode;
|
struct RepeatMode;
|
||||||
static REPEAT: RepeatMode = RepeatMode;
|
static REPEAT: RepeatMode = RepeatMode;
|
||||||
impl ChanMode for RepeatMode {
|
impl ChanMode for RepeatMode {
|
||||||
|
|
@ -1047,9 +1042,8 @@ impl ChanMode for RepeatMode {
|
||||||
|
|
||||||
// === user modes ============================================================
|
// === user modes ============================================================
|
||||||
|
|
||||||
/// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`], and the
|
/// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`]: an
|
||||||
/// same win over the C++ mode system: an unbounded slice of zero-sized
|
/// unbounded slice of zero-sized `&'static` handlers.
|
||||||
/// `&'static` handlers, no bitmask cap, no allocation, no `unsafe`.
|
|
||||||
pub trait UserMode: Sync {
|
pub trait UserMode: Sync {
|
||||||
fn letter(&self) -> char;
|
fn letter(&self) -> char;
|
||||||
/// Apply `+`/`-` to the user; return `true` if it took effect (echo it).
|
/// Apply `+`/`-` to the user; return `true` if it took effect (echo it).
|
||||||
|
|
@ -1188,12 +1182,15 @@ impl UserMode for OperMode {
|
||||||
if adding {
|
if adding {
|
||||||
return false; // never self-granted
|
return false; // never self-granted
|
||||||
}
|
}
|
||||||
|
if s.users.get(&uid).is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if let Some(u) = s.users.get_mut(&uid) {
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
u.flags.oper = false;
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
//! The module API — echoIRCd's answer to InspIRCd's `Module` class.
|
//! Module API: modules hook lifecycle events. "Pre" hooks return a [`ModResult`]
|
||||||
//!
|
//! and can **deny** an action; "notify" hooks are informational. The core fires
|
||||||
//! Modules hook lifecycle events. "Pre" hooks return a [`ModResult`] and can
|
//! pre-hooks inline (so a `Deny` actually blocks) and notify-hooks from a queue
|
||||||
//! **deny** an action; "notify" hooks are informational. The core fires pre-hooks
|
//! after the triggering command finishes — so a handler can emit an event without
|
||||||
//! inline (so a `Deny` actually blocks) and notify-hooks from a queue after the
|
//! ever touching the module list. All hooks get `&mut Server`, so a module can act
|
||||||
//! triggering command finishes — so a handler can emit an event without ever
|
//! (send lines, force a join, …).
|
||||||
//! 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`.
|
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
//! account_registration — IRCv3 `draft/account-registration` (the `REGISTER` /
|
//! account_registration — IRCv3 `draft/account-registration` (`REGISTER` / `VERIFY`
|
||||||
//! `VERIFY` commands and the cap that advertises them), bridged to a configurable
|
//! commands and the cap advertising them), bridged to a configurable HTTP accounts
|
||||||
//! HTTP accounts API. reverse's own module: the old Swaygo Django backend is gone,
|
//! API. POSTs form-encoded fields with an `X-API-Key` header.
|
||||||
//! so this talks to whatever `acctregister_registerurl` / `_verifyurl` you point it
|
|
||||||
//! at, POSTing form-encoded fields with an `X-API-Key` header.
|
|
||||||
//!
|
//!
|
||||||
//! The API call runs on a worker thread (`Server::spawn_http`) and its result comes
|
//! 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
|
//! 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 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)
|
//! the new account. Per-IP rate-limit state lives in `Server.ext`.
|
||||||
//! lives in `Server.ext`.
|
|
||||||
//!
|
//!
|
||||||
//! Config (all under flat keys):
|
//! Config (all under flat keys):
|
||||||
//! account_registration = yes enable
|
//! account_registration = yes enable
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
//! antimixedutf8 — blocks spam that mixes Unicode scripts within words (Latin
|
//! antimixedutf8 — blocks spam that mixes Unicode scripts within words (Latin
|
||||||
//! letters swapped for Cyrillic/Greek look-alikes: "FᏒee Ⅴ1аgrа"), a very common
|
//! letters swapped for Cyrillic/Greek look-alikes: "FᏒee Ⅴ1аgrа"). The scoring
|
||||||
//! obfuscation. This is reverse's own detection model — the scoring rules and the
|
//! rules and the confusable / fancy-Latin / zero-width tables define what counts as
|
||||||
//! confusable / fancy-Latin / zero-width tables — implemented from scratch in
|
//! spam.
|
||||||
//! native Rust. (The *tables and weights* are the detector's spec: they define
|
|
||||||
//! what counts as spam. Everything around them is original echoIRCd code.)
|
|
||||||
//!
|
//!
|
||||||
//! Per word: letters from more than one script score; so do words that are ASCII
|
//! 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,
|
//! mixed with Latin-confusable letters, words built almost entirely of confusables,
|
||||||
//! and "fancy" styled-Latin words. Zero-width chars score too. At/above the
|
//! and "fancy" styled-Latin words. Zero-width chars score too. At/above the
|
||||||
//! configured threshold the action fires (block | kill | gline | kline | zline).
|
//! 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::module::{ModResult, Module};
|
||||||
use crate::server::Server;
|
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
|
/// A non-Latin letter that looks like an ASCII Latin letter (a homoglyph). Catches
|
||||||
/// spammers swap in). Catches pure-homoglyph words that script-mixing misses,
|
/// pure-homoglyph words that script-mixing misses, without tripping on genuine
|
||||||
/// without tripping on genuine monolingual text.
|
/// monolingual text.
|
||||||
fn is_latin_confusable(cp: u32) -> bool {
|
fn is_latin_confusable(cp: u32) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
cp,
|
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)]
|
#[derive(Default)]
|
||||||
struct Scorer {
|
struct Scorer {
|
||||||
mixedwords: u32, // words mixing >1 real script
|
mixedwords: u32, // words mixing >1 real script
|
||||||
|
|
@ -119,9 +113,8 @@ impl Scorer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn end_word(&mut self) {
|
fn end_word(&mut self) {
|
||||||
// Confusable mixed WITH real ASCII in one word = the classic "swap a few
|
// Confusable mixed with real ASCII in one word: count once here so a single
|
||||||
// letters" attack (already script-mixing) — count it ONCE here so a single
|
// stray homoglyph doesn't also score as script-mixing.
|
||||||
// stray homoglyph doesn't double-score.
|
|
||||||
if self.word_has_confusable && self.word_has_ascii {
|
if self.word_has_confusable && self.word_has_ascii {
|
||||||
self.homoglyphwords += 1;
|
self.homoglyphwords += 1;
|
||||||
} else if self.word_scripts() >= 2 {
|
} else if self.word_scripts() >= 2 {
|
||||||
|
|
@ -283,17 +276,15 @@ impl Module for AntiMixedUtf8 {
|
||||||
u.addr.ip().to_string(),
|
u.addr.ip().to_string(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
// Show opers WHAT was blocked (a sanitized snippet) so they can judge the
|
// Snotice a sanitized snippet so opers can judge the catch / spot false positives.
|
||||||
// catch and spot false positives — the whole point of an antispam log.
|
|
||||||
srv.snotice(&format!(
|
srv.snotice(&format!(
|
||||||
"ANTIMIXEDUTF8: blocked spam from {mask} to {target} (score {score} >= {}): {}",
|
"ANTIMIXEDUTF8: blocked spam from {mask} to {target} (score {score} >= {}): {}",
|
||||||
srv.amu.threshold,
|
srv.amu.threshold,
|
||||||
snippet(body)
|
snippet(body)
|
||||||
));
|
));
|
||||||
|
|
||||||
// Always tell the sender their message was blocked and that opers were
|
// Notify the sender even for punitive actions: the writer flushes queued
|
||||||
// told — even for punitive actions, since the writer flushes queued lines
|
// lines before a disconnect.
|
||||||
// before a disconnect.
|
|
||||||
srv.send(
|
srv.send(
|
||||||
uid,
|
uid,
|
||||||
format!(
|
format!(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
//! antirandom — detect spam drones whose nick/ident/realname is random-looking,
|
//! antirandom — detect spam drones whose nick/ident/realname is random-looking,
|
||||||
//! by scoring character patterns and acting when a threshold is crossed. This is
|
//! by scoring character patterns and acting when a threshold is crossed.
|
||||||
//! 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.
|
|
||||||
//!
|
//!
|
||||||
//! Score, summed over nick (+ ident + realname when `checkfull`):
|
//! Score, summed over nick (+ ident + realname when `checkfull`):
|
||||||
//! - a run reaching 5 digits / 4 vowels / 4 consonants adds that length; each
|
//! - 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 |
|
//! At/above `antirandom_threshold` the action fires: kill | gline | kline |
|
||||||
//! zline | block. Opers and logged-in accounts are always exempt. Off unless
|
//! 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::module::{ModResult, Module};
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
@ -19,7 +16,7 @@ use crate::xline::XKind;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
/// Adjacent letter pairs that rarely occur in real words but pepper random
|
/// 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]] = &[
|
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"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",
|
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",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@
|
||||||
//! ```text
|
//! ```text
|
||||||
//! autodrop_commands = GET POST HEAD CONNECT PUT DELETE OPTIONS TRACE PATCH
|
//! 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::module::{ModResult, Module};
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
//! autoop — the channel list mode `+w <prefix>:<hostmask>` grants a status prefix
|
//! 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
|
//! 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,
|
//! 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
|
//! stored verbatim) and is applied on join via the server-authority mode path.
|
||||||
//! path. Reference: InspIRCd's `m_autoop`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::module::Module;
|
use crate::module::Module;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
//! banredirect — a ban of the form `+b <mask>$<#channel>` bounces a matching,
|
//! banredirect — a ban of the form `+b <mask>$<#channel>` bounces a matching,
|
||||||
//! banned user into `#channel` instead of refusing them outright. The redirect
|
//! banned user into `#channel` instead of refusing them outright. The redirect
|
||||||
//! fires at most once, guarded by `Server.in_redirect` (shared with the `+L`
|
//! fires at most once, guarded by `Server.in_redirect` (shared with the `+L`
|
||||||
//! full-channel redirect), so it can never loop. Reference: InspIRCd's
|
//! full-channel redirect), so it can never loop.
|
||||||
//! `m_banredirect`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
//! blockamsg — block the mass "/amsg" and "/ame" commands that mIRC/HexChat send
|
//! blockamsg — block the mass "/amsg" and "/ame" commands (one message fanned out
|
||||||
//! (one message fanned out to every channel you're on), a classic advertise/flood
|
//! to every channel the sender is on), a classic advertise/flood vector. A
|
||||||
//! vector. A PRIVMSG/NOTICE whose target list is two-or-more channels is blocked
|
//! PRIVMSG/NOTICE whose target list is two-or-more channels is blocked when either:
|
||||||
//! when either: the same text was just sent to a *different* target list within
|
//! the same text was just sent to a *different* target list within `blockamsg_delay`
|
||||||
//! `blockamsg_delay` seconds, or the number of channel targets equals the number
|
//! seconds, or the number of channel targets equals the number of channels the
|
||||||
//! of channels the sender is on (>1). Off unless `blockamsg = yes`. Per-user
|
//! sender is on (>1). Off unless `blockamsg = yes`. Per-user last-message state
|
||||||
//! last-message bookkeeping lives in `Server.ext`; nothing on `Server`.
|
//! lives in `Server.ext`.
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_blockamsg`. Original native Rust.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
@ -64,7 +62,7 @@ impl Module for BlockAmsg {
|
||||||
|
|
||||||
let store = srv.ext.get_or_insert_with::<LastMsg>(LastMsg::default);
|
let store = srv.ext.get_or_insert_with::<LastMsg>(LastMsg::default);
|
||||||
let prev = store.0.get(&uid).cloned();
|
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));
|
store.0.insert(uid, (text.clone(), list.clone(), n));
|
||||||
|
|
||||||
let repeat_hit = prev
|
let repeat_hit = prev
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
//! chanlog — mirror server notices (the `snotice` stream opers see with +s) into a
|
//! 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
|
//! channel, so staff can watch the log in a normal channel window. Off unless
|
||||||
//! `chanlog = #channel` is configured. Reference: InspIRCd's `m_chanlog`.
|
//! `chanlog = #channel` is configured.
|
||||||
//! Original native Rust.
|
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
//! control codes or fancy Unicode an admin doesn't want in channel names). Existing
|
//! 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
|
//! channels are unaffected. Off unless `channames_deny` is set. Dispatched from
|
||||||
//! `Server::join`.
|
//! `Server::join`.
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_channames`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::numeric::ERR_BADCHANNEL;
|
use crate::numeric::ERR_BADCHANNEL;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
//! are in. `+b j:#lobby` bans everyone who is also in `#lobby`; an optional status
|
//! 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
|
//! 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
|
//! 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.
|
//! channel ban matcher.
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_channelban`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::{glob_match, RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE};
|
use crate::channels::{glob_match, RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE};
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
//! chathistory — InspIRCd's `m_chathistory` family (draft/chathistory +
|
//! chathistory — draft/chathistory + draft/message-redaction. Recent PRIVMSG/NOTICE
|
||||||
//! draft/message-redaction). Recent PRIVMSG/NOTICE traffic is kept in a capped
|
//! traffic is kept in a capped per-conversation ring (channels and DM pairs) so
|
||||||
//! per-conversation ring (channels and DM pairs) so clients can replay it on
|
//! clients can replay it on demand or on join (the channel `+H` backlog lives in
|
||||||
//! demand or on join (the channel `+H` backlog lives in `channels::replay_chanhistory`).
|
//! `channels::replay_chanhistory`). The ring lives in `Server.ext`; the message path
|
||||||
//! Self-contained: the ring lives in `Server.ext`; the message path records into it
|
//! records into it via [`record`], and the CHATHISTORY and REDACT commands
|
||||||
//! via [`record`], and the CHATHISTORY and REDACT commands read/edit it here.
|
//! read/edit it here.
|
||||||
|
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
//! A cloak hides the real IP while preserving subnet structure, so a channel ban
|
||||||
//! real IP while *preserving subnet structure*, so a channel ban on a whole /24
|
//! on a /24 or /16 still matches. IPv4 `a.b.c.d` becomes one keyed segment per
|
||||||
//! or /16 still bites. The cloak is shown under user mode **+x**, which this
|
//! octet-prefix tier, most-specific first, with a literal `.IP` suffix marking a
|
||||||
//! module auto-sets on connect; only opers may drop it (see [`crate::mode`]),
|
//! cloaked address (a cloaked hostname keeps its domain instead):
|
||||||
//! 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`:
|
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! HASH(a.b.c.d) . HASH(a.b.c) . HASH(a.b) . HASH(a) . IP
|
//! HASH(a.b.c.d) . HASH(a.b.c) . HASH(a.b) . HASH(a) . IP
|
||||||
//! (/32) (/24) (/16) (/8)
|
//! (/32) (/24) (/16) (/8)
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! so two IPs in the same /24 share the `…/24./16./8.IP` tail (same /16 shares
|
//! Two IPs in the same /24 share the `…/24./16./8.IP` tail; the exact address
|
||||||
//! `…/16./8.IP`), and the exact address never leaks. Where this improves on the
|
//! never appears. The hash is SHA-256 (via the openssl already linked for TLS).
|
||||||
//! 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.
|
|
||||||
|
|
||||||
use openssl::sha::sha256;
|
use openssl::sha::sha256;
|
||||||
|
|
||||||
|
|
@ -29,7 +21,7 @@ use crate::module::Module;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
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";
|
const IP_SUFFIX: &str = ".IP";
|
||||||
|
|
||||||
pub struct Cloak;
|
pub struct Cloak;
|
||||||
|
|
@ -42,7 +34,7 @@ impl Module for Cloak {
|
||||||
/// Compute the cloak once, at connect, and cloak the user by default (+x).
|
/// Compute the cloak once, at connect, and cloak the user by default (+x).
|
||||||
fn on_user_connect(&mut self, srv: &mut Server, uid: Uid) {
|
fn on_user_connect(&mut self, srv: &mut Server, uid: Uid) {
|
||||||
let Some(key) = srv.cloak_key.clone() else {
|
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 {
|
let Some(host) = srv.users.get(&uid).map(|u| u.host.clone()) else {
|
||||||
return;
|
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
|
/// - 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
|
/// octet-prefix tier (/32 · /24 · /16 · /8), so subnet bans keep working while
|
||||||
/// the exact address never appears.
|
/// 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
|
/// - hostname → keep the last two labels (the domain), mask everything to the left
|
||||||
/// (no `.IP` — a resolved name isn't a raw address).
|
/// (no `.IP` — a resolved name isn't a raw address).
|
||||||
pub fn cloak_host(key: &str, host: &str) -> String {
|
pub fn cloak_host(key: &str, host: &str) -> String {
|
||||||
|
|
@ -125,7 +117,7 @@ mod tests {
|
||||||
let c = cloak_host("secret", "203.0.113.7");
|
let c = cloak_host("secret", "203.0.113.7");
|
||||||
assert_eq!(c, cloak_host("secret", "203.0.113.7")); // stable
|
assert_eq!(c, cloak_host("secret", "203.0.113.7")); // stable
|
||||||
assert!(!c.contains("203.0.113")); // the dotted IP never appears
|
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
|
assert_eq!(c.split('.').count(), 5); // H32.H24.H16.H8.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
//! cloudflare_challenge — gate registration behind a Cloudflare Turnstile-style
|
//! cloudflare_challenge: gate registration behind a challenge verified by an
|
||||||
//! challenge, verified by an IP-bound HS256 JWT. Structurally like [`recaptcha`]
|
//! IP-bound HS256 JWT, presented via the `VERIFYCHALLENGE <token>` command. A
|
||||||
//! but keyed on `cloudflare_*` config and driven by the `VERIFYCHALLENGE <token>`
|
//! validly-signed, unexpired, IP-matching token is sufficient proof; no backend
|
||||||
//! command. reverse's own module; runs in JWT-only mode (a validly-signed,
|
//! call is made.
|
||||||
//! unexpired, IP-matching token is proof — no backend call needed).
|
|
||||||
//!
|
//!
|
||||||
//! Off unless `cloudflare_challenge = yes` with `cloudflare_secret` +
|
//! Off unless `cloudflare_challenge = yes` with `cloudflare_secret` +
|
||||||
//! `cloudflare_url` set. Config-driven; the passed-verification set lives in
|
//! `cloudflare_url` set. The passed-verification set lives in `Server.ext`.
|
||||||
//! `Server.ext`.
|
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@
|
||||||
//! conn_waitpong_killonbadreply = yes # disconnect on a wrong pong (default: keep waiting)
|
//! 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`,
|
//! The gate is the core `User.waitpong` field (checked in `try_register`); this
|
||||||
//! like `dns_pending`); this module just arms it at connect and clears it on the
|
//! module arms it at connect and clears it on the matching PONG.
|
||||||
//! matching PONG. Behaviour reference: InspIRCd's `m_conn_waitpong`. Native Rust.
|
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
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).
|
/// 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]) {
|
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 {
|
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("");
|
let got = params.last().map(String::as_str).unwrap_or("");
|
||||||
if got == want {
|
if got == want {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! connectban — z-line an IP range that opens an excessive number of connections
|
//! connectban: z-line an IP range that opens too many connections. Each connection
|
||||||
//! to the server. Each connection bumps a per-range counter; when it reaches
|
//! bumps a per-range counter; at `connectban_threshold` the range is z-lined for
|
||||||
//! `connectban_threshold` the range is z-lined for `connectban_duration` and the
|
//! `connectban_duration` and the counter cleared. The tally is periodically wiped
|
||||||
//! counter cleared. The whole tally is periodically wiped (`connectban_gcinterval`)
|
//! (`connectban_gcinterval`) so long-lived counts don't accumulate. A
|
||||||
//! so long-lived counts don't accumulate — mirroring InspIRCd's garbage-collect.
|
//! `connectban_bootwait` grace after start avoids banning the restart reconnect
|
||||||
//! A `connectban_bootwait` grace after start avoids banning the reconnect storm
|
//! storm. Off unless `connectban = yes`; all state in `Server.ext`.
|
||||||
//! when the server (re)starts. Off unless `connectban = yes`; all state in
|
|
||||||
//! `Server.ext`, nothing on `Server`.
|
|
||||||
//!
|
//!
|
||||||
//! Because echoIRCd z-lines match by glob (not CIDR), the banned range is emitted
|
//! z-lines match by glob, not CIDR, so the banned range is emitted as a wildcard
|
||||||
//! as a wildcard mask (`1.2.3.*` for an IPv4 /24, the exact IP for a /32).
|
//! mask (`1.2.3.*` for an IPv4 /24, the exact IP for a /32).
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_connectban`. Original native Rust.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
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;
|
pub struct ConnectBan;
|
||||||
impl Module for ConnectBan {
|
impl Module for ConnectBan {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! connflood — InspIRCd `m_connflood`. Refuse connections from an IP opening too
|
//! connflood: refuse connections from an IP opening too many too fast. Config:
|
||||||
//! many too fast. Config: `connflood = <max> <secs>`. Per-IP recent-connect times
|
//! `connflood = <max> <secs>`. Per-IP recent-connect times live in `Server.ext`,
|
||||||
//! live in `Server.ext`, pruned on the tick — nothing lives on `Server`.
|
//! pruned on the tick.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
//! customtitle — `TITLE <name> <password>` lets a user claim a configured vanity
|
//! customtitle: `TITLE <name> <password>` lets a user claim a configured title
|
||||||
//! title (shown in their WHOIS) and, optionally, a matching vhost — a lightweight
|
//! (shown in their WHOIS) and, optionally, a matching vhost. Config, one block per
|
||||||
//! "mini-oper" identity without operator privileges. Config, one block per title:
|
//! title:
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! customtitle = <name> <password> <vhost|*> <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`.
|
//! 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::command::{CmdResult, Command};
|
||||||
use crate::modules::password_hash;
|
use crate::modules::password_hash;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! dccallow — block unwanted DCC file transfers (and optionally DCC CHAT) unless
|
//! dccallow: block DCC SEND matching configured filename globs (and, with
|
||||||
//! the recipient has explicitly allowed the sender with `/DCCALLOW +<nick>`.
|
//! `dccallow_blockchat`, DCC CHAT) unless the recipient has allowed the sender
|
||||||
//! Mirrors InspIRCd's `m_dccallow`. Config (all optional, all runtime-read):
|
//! with `/DCCALLOW +<nick>`. Config (all optional, all runtime-read):
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! dccallow_blockfile = *.exe # repeatable: filename globs to block on DCC SEND
|
//! 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)
|
//! 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
|
//! A user's allow-list lives on their `User.ext`, so it vanishes when they quit.
|
||||||
//! it vanishes cleanly when they quit. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! denychans — forbid joining channels whose name matches a `badchan` glob, with
|
//! denychans: forbid joining channels whose name matches a `badchan` glob, with an
|
||||||
//! an optional redirect to a safe channel and an `allowopers` bypass. A `goodchan`
|
//! optional redirect to a safe channel and an `allowopers` bypass. A `goodchan`
|
||||||
//! glob whitelists names back out of a broad `badchan` pattern. Config:
|
//! glob whitelists names back out of a broad `badchan` pattern. Config:
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
|
|
@ -7,11 +7,8 @@
|
||||||
//! goodchan = #evilgenius
|
//! goodchan = #evilgenius
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Dispatched straight from `Server::join` (like the CBAN check), so it works
|
//! Dispatched from `Server::join`, so it applies per-channel even when several are
|
||||||
//! per-channel even when several are joined at once. All config-driven; nothing
|
//! joined at once.
|
||||||
//! lives on `Server`.
|
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_denychans`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::numeric::{ERR_BADCHANNEL, ERR_LINKCHANNEL};
|
use crate::numeric::{ERR_BADCHANNEL, ERR_LINKCHANNEL};
|
||||||
|
|
@ -26,8 +23,7 @@ struct BadChan {
|
||||||
allowopers: bool,
|
allowopers: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reuse the quoted-attribute tokenizer shape: split on whitespace but keep
|
/// Split on whitespace but keep `key="quoted value"` together.
|
||||||
/// `key="quoted value"` together.
|
|
||||||
fn tokenize(line: &str) -> Vec<String> {
|
fn tokenize(line: &str) -> Vec<String> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut cur = String::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
|
/// 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
|
/// blocked; emits the numeric and performs a redirect join if configured.
|
||||||
/// redirect join if configured. `is_oper` lets an `allowopers` badchan through.
|
/// `is_oper` lets an `allowopers` badchan through.
|
||||||
pub fn intercept(s: &mut Server, uid: Uid, name: &str, is_oper: bool) -> bool {
|
pub fn intercept(s: &mut Server, uid: Uid, name: &str, is_oper: bool) -> bool {
|
||||||
if s.conf_all("badchan").is_empty() {
|
if s.conf_all("badchan").is_empty() {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -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
|
//! `disabled_commands = LIST WHO KNOCK` (space-separated; repeatable). A disabled
|
||||||
//! command replies with `421` as if it didn't exist. Off unless configured.
|
//! 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::module::{ModResult, Module};
|
||||||
use crate::numeric::ERR_UNKNOWNCOMMAND;
|
use crate::numeric::ERR_UNKNOWNCOMMAND;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
//! DNSBL — DNS blocklist checks on connect, InspIRCd `m_dnsbl` style. On connect
|
//! DNSBL: DNS blocklist checks on connect. The resolver thread reverses the
|
||||||
//! the resolver thread reverses the client's IP under each configured blocklist
|
//! client's IP under each configured blocklist zone and A-looks it up (see
|
||||||
//! zone and A-looks it up (see [`crate::resolver`]); a listing triggers the
|
//! [`crate::resolver`]); a listing triggers the configured action. Works for IPv4
|
||||||
//! configured action. Works for IPv4 **and** IPv6 (v4 reversed octets or v6
|
//! (reversed octets) and IPv6 (reversed nibbles); a v4-only blocklist NXDOMAINs a
|
||||||
//! reversed nibbles under the zone) — a v4-only blocklist simply NXDOMAINs a v6
|
//! v6 query, which reads as "not listed".
|
||||||
//! query, which reads as "not listed".
|
|
||||||
//!
|
//!
|
||||||
//! Actions (`dnsbl_action`): `mark` just shows the notice and lets them in
|
//! Actions (`dnsbl_action`): `mark` shows the notice and lets them in (default),
|
||||||
//! (default, safe), `kill` disconnects, `kline`/`gline`/`zline` add a 1-day ban
|
//! `kill` disconnects, `kline`/`gline`/`zline` add a 1-day ban and disconnect.
|
||||||
//! and disconnect. This isn't a hook `Module` — it's driven from the connection
|
//! Driven from the connection lifecycle (`Server::add_conn` → `on_resolved`)
|
||||||
//! lifecycle (`Server::add_conn` → `on_resolved`) — but it lives here as its own
|
//! rather than as a hook `Module`.
|
||||||
//! self-contained unit.
|
|
||||||
|
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
//! extbanbanlist — the matching extban `b:<#channel>`: a user is caught if they are
|
//! extbanbanlist: matching extban `b:<#channel>` catches a user if they are on
|
||||||
//! on `#channel`'s ban list. Lets one channel share (borrow) another's bans, e.g.
|
//! `#channel`'s ban list, letting one channel borrow another's bans (e.g.
|
||||||
//! `+b b:#staff` bans everyone banned in #staff. Reference: InspIRCd's
|
//! `+b b:#staff` bans everyone banned in #staff).
|
||||||
//! `m_extbanbanlist`. Original native Rust.
|
|
||||||
//!
|
//!
|
||||||
//! The match is deliberately *non-recursive* — it only tests the referenced
|
//! The match is deliberately non-recursive: it tests only the referenced channel's
|
||||||
//! channel's plain host-mask bans (and its plain excepts), never that channel's own
|
//! plain host-mask bans (and its plain excepts), never that channel's own extbans,
|
||||||
//! extbans, so two channels referencing each other can't loop.
|
//! so two channels referencing each other can't loop.
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
//! extended_isupport — the `draft/extended-isupport` capability. reverse's own
|
//! `draft/extended-isupport` capability: lets a client re-request the current
|
||||||
//! module. Normally ISUPPORT (005) is a one-shot at registration; with this cap a
|
//! ISUPPORT (005) tokens at any time via the `ISUPPORT` command. With the `batch`
|
||||||
//! client can send the `ISUPPORT` command any time to re-request the current tokens
|
//! cap the reply is wrapped in a `draft/isupport` BATCH so the set arrives atomically.
|
||||||
//! (handy after a rehash changes them). If the client also has `batch`, the reply
|
//! Emission lives in `Server::send_isupport`, shared with the welcome burst.
|
||||||
//! 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.
|
|
||||||
|
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
use crate::numeric::ERR_UNKNOWNCOMMAND;
|
use crate::numeric::ERR_UNKNOWNCOMMAND;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
//! ircv3_extjwt — the `EXTJWT` command: hand a client a short-lived, server-signed
|
//! `EXTJWT` command: issues a short-lived, server-signed HS256 JWT a client can
|
||||||
//! JWT it can present to an *external* service (a web app, file host, …) to prove
|
//! present to an external service to prove its IRC identity, modes and channel
|
||||||
//! "this IRC user, with these modes, in this channel, right now". The service
|
//! membership. Uses the [`crate::modules::jwt`] signer.
|
||||||
//! trusts the token because it shares the HS256 secret. Uses the native
|
|
||||||
//! [`crate::modules::jwt`] signer.
|
|
||||||
//!
|
//!
|
||||||
//! `EXTJWT *|<channel> [<service>]` → one or more
|
//! `EXTJWT *|<channel> [<service>]` → one or more
|
||||||
//! `:<server> EXTJWT <target> <service> [*] <chunk>` lines (a `*` param before the
|
//! `:<server> EXTJWT <target> <service> [*] <chunk>` lines (a `*` param before the
|
||||||
|
|
@ -11,8 +9,6 @@
|
||||||
//!
|
//!
|
||||||
//! Config: `extjwt_secret` (+ `extjwt_duration`, default 30s); optional named
|
//! Config: `extjwt_secret` (+ `extjwt_duration`, default 30s); optional named
|
||||||
//! services via `extjwt_service = <name> <secret> [duration]`. Off with no secret.
|
//! 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::command::{CmdResult, Command};
|
||||||
use crate::modules::jwt;
|
use crate::modules::jwt;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,19 @@
|
||||||
//! filehost — the DRAFT `reverse.im/filehost` IRCv3 extension. reverse's own
|
//! `reverse.im/filehost` (draft) IRCv3 extension: advertises an external
|
||||||
//! module: it advertises an external file-hosting service to clients and hands a
|
//! file-hosting service and hands a logged-in user a short-lived, server-signed JWT
|
||||||
//! logged-in user a short-lived, server-signed JWT upload link (so the web uploader
|
//! upload link so the web uploader trusts them without a second login.
|
||||||
//! trusts them without a second login — pairs with reverse's rubot upload service).
|
|
||||||
//!
|
//!
|
||||||
//! Surfaces:
|
//! Surfaces:
|
||||||
//! * ISUPPORT `reverse.im/FILEHOST=<website>` + the `reverse.im/filehost` cap
|
//! * ISUPPORT `reverse.im/FILEHOST=<website>` + the `reverse.im/filehost` cap.
|
||||||
//! (so a client knows the service exists and can show an upload button).
|
|
||||||
//! * `FILEHOST [info]` — login-gated; replies with `<website>/upload?token=<jwt>`
|
//! * `FILEHOST [info]` — login-gated; replies with `<website>/upload?token=<jwt>`
|
||||||
//! and usage info.
|
//! and usage info.
|
||||||
//! * a `reverse.im/filehost` message tag carrying JSON metadata (url/filename/
|
//! * a `reverse.im/filehost` message tag carrying JSON metadata (url/filename/
|
||||||
//! type) attached to any message that contains a `<website>/files/…` link, so
|
//! type) attached to any message containing a `<website>/files/…` link, so
|
||||||
//! clients render the file inline. Scoped to the message's recipients (not the
|
//! clients render the file inline. Scoped to the message's recipients.
|
||||||
//! whole network — cleaner than the reference's broadcast).
|
|
||||||
//! * `filehost_requiressl`: refuse to relay a filehost link from a plaintext user.
|
//! * `filehost_requiressl`: refuse to relay a filehost link from a plaintext user.
|
||||||
//!
|
//!
|
||||||
//! Config: `filehost_website` (enables it) `filehost_jwt_secret` `filehost_jwt_issuer`
|
//! Config: `filehost_website` (enables it) `filehost_jwt_secret` `filehost_jwt_issuer`
|
||||||
//! (default FILEHOST) `filehost_token_expiry` (secs, default 3600) `filehost_requiressl`
|
//! (default FILEHOST) `filehost_token_expiry` (secs, default 3600) `filehost_requiressl`
|
||||||
//! (default yes) `filehost_auth_message`.
|
//! (default yes) `filehost_auth_message`.
|
||||||
//!
|
|
||||||
//! Behaviour reference: reverse's InspIRCd `m_ircv3_FILEHOST`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
use crate::module::{ModResult, Module};
|
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}"))
|
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 {
|
fn file_type(filename: &str) -> &'static str {
|
||||||
let ext = filename
|
let ext = filename
|
||||||
.rsplit_once('.')
|
.rsplit_once('.')
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
//! filter — InspIRCd's `m_filter`: oper-configured spam/word filters. A glob is
|
//! Oper-configured spam/word filters: a glob is matched against PRIVMSG/NOTICE text
|
||||||
//! matched against PRIVMSG/NOTICE text and, on a hit, an action is taken. Fully
|
//! and, on a hit, an action is taken. The rule set lives in `Server.ext`, the
|
||||||
//! self-contained: the rule set lives in `Server.ext` (the module-owned typemap),
|
//! `FILTER` command manages it, and the `on_pre_message` hook enforces it.
|
||||||
//! the `FILTER` command manages it, and the `on_pre_message` hook enforces it —
|
|
||||||
//! nothing leaks into server.rs or config.rs.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
|
|
@ -21,7 +19,7 @@ pub struct SpamFilter {
|
||||||
pub reason: String,
|
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)]
|
#[derive(Default)]
|
||||||
pub struct Filters(pub Vec<SpamFilter>);
|
pub struct Filters(pub Vec<SpamFilter>);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
//! Flood protection — a module that rate-limits messages.
|
//! Per-user message-rate limit (`flood_messages` within `flood_seconds`); opers
|
||||||
//!
|
//! exempt. Recent message times live in the user's typed
|
||||||
//! It keeps each user's recent message times in that user's typed
|
//! [`crate::extensible::Extensible`] slot, so the state is freed when the user quits.
|
||||||
//! [`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).
|
|
||||||
|
|
||||||
use crate::module::{ModResult, Module};
|
use crate::module::{ModResult, Module};
|
||||||
use crate::server::{now, Server};
|
use crate::server::{now, Server};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
//! geoip — native MaxMind DB (`.mmdb`) country lookup, with the `G:<cc>` geoban
|
//! MaxMind DB (`.mmdb`) country lookup, with the `G:<cc>` geoban extban, the
|
||||||
//! extban, the `GEOIP` command and a WHOIS country line. The `maxminddb` crate is
|
//! `GEOIP` command and a WHOIS country line. The binary format is parsed by hand:
|
||||||
//! off-limits (openssl+mio only), so the binary format is parsed by hand in pure
|
//! the metadata section, the record-size-aware search tree, and the typed data
|
||||||
//! std: the metadata section, the record-size-aware search tree, and the typed data
|
//! decoder.
|
||||||
//! decoder — no crate, no `unsafe`, no C FFI.
|
|
||||||
//!
|
//!
|
||||||
//! Config: `geoip_database = /path/to/GeoLite2-Country.mmdb` (loaded once at boot).
|
//! 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::net::IpAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -375,15 +371,18 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
// A real GeoLite2-Country.mmdb if one is present; otherwise the test no-ops so
|
// Use a database from the env var if set, else common locations; the test
|
||||||
// CI (which has no database) stays green.
|
// no-ops when none is present so CI stays green.
|
||||||
const DB_CANDIDATES: &[&str] = &[
|
const DB_CANDIDATES: &[&str] = &[
|
||||||
"/home/debian/irc/ircd/inspircd/run/conf/geodata/GeoLite2-Country.mmdb",
|
|
||||||
"/usr/share/GeoIP/GeoLite2-Country.mmdb",
|
"/usr/share/GeoIP/GeoLite2-Country.mmdb",
|
||||||
|
"/etc/echoircd/GeoLite2-Country.mmdb",
|
||||||
];
|
];
|
||||||
|
|
||||||
fn load() -> Option<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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! globops — `GLOBOPS <message>` lets an oper send a message to all opers (the
|
//! `GLOBOPS <message>`: lets an oper send a message to all opers via the
|
||||||
//! server-notice stream, echoIRCd's equivalent of InspIRCd's `+g` snomask).
|
//! server-notice stream.
|
||||||
//! Reference: InspIRCd's `m_globops`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
use crate::numeric::ERR_NOPRIVILEGES;
|
use crate::numeric::ERR_NOPRIVILEGES;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
//! hashident — replace a user's ident with a stable, opaque 12-character token
|
//! Replaces a user's ident with a stable, opaque 12-character token derived from
|
||||||
//! derived from their IP, so the username field leaks nothing (no `~guest`, no
|
//! their IP, so the username field leaks nothing yet stays constant per address.
|
||||||
//! probed identd name) yet stays constant per address. reverse's own module.
|
|
||||||
//!
|
//!
|
||||||
//! The token is the first 6 bytes of `HMAC-SHA256(hashident_key, ip)`, hex-encoded
|
//! 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 —
|
//! (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).
|
//! the key is what makes the mapping unforgeable. Applied once, right after connect.
|
||||||
//! Applied once, right after the user finishes connecting; all config-driven.
|
|
||||||
|
|
||||||
use openssl::hash::MessageDigest;
|
use openssl::hash::MessageDigest;
|
||||||
use openssl::pkey::PKey;
|
use openssl::pkey::PKey;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
//! hidelist — hide a channel list mode's entries (e.g. the +b ban list) from users
|
//! Hides a channel list mode's entries (e.g. the +b ban list) from members below a
|
||||||
//! below a configured rank, so ordinary members can't enumerate who's banned.
|
//! configured rank. Config, repeatable: `hidelist = <modechar> <rank>` where rank is
|
||||||
//! Config, repeatable: `hidelist = <modechar> <rank>` where rank is one of
|
//! one of owner|admin|op|halfop|voice. Opers always see.
|
||||||
//! owner|admin|op|halfop|voice. Opers always see. Reference: InspIRCd's
|
|
||||||
//! `m_hidelist`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::{RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE};
|
use crate::channels::{RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE};
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
//! hidemode — hide specific mode changes from channel members below a rank, so
|
//! Hides specific mode changes from channel members below a rank (e.g. bans being
|
||||||
//! ordinary users don't see e.g. bans being set/removed. Config, repeatable:
|
//! set/removed). Config, repeatable: `hidemode = <modechar> <rank>` (rank:
|
||||||
//! `hidemode = <modechar> <rank>` (rank: owner|admin|op|halfop|voice). The setter,
|
//! owner|admin|op|halfop|voice). The setter, opers and linked servers always see
|
||||||
//! opers and linked servers always see the full change. Reference: InspIRCd's
|
//! the full change.
|
||||||
//! `m_hidemode`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::{RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE};
|
use crate::channels::{RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE};
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! hidewhois — InspIRCd `m_hidewhois`. Hides sensitive WHOIS lines (server, idle,
|
//! Hides sensitive WHOIS lines (server, idle, secure, …) from ordinary users. Opers
|
||||||
//! secure, …) from ordinary users. Opers and the user themselves are exempt when
|
//! and the user themselves are exempt when the matching config toggle is on.
|
||||||
//! the matching config toggle is on. All config-driven; nothing lives on `Server`.
|
//! Config: `hidewhois`, `hidewhois_selfview`, `hidewhois_opers`,
|
||||||
|
//! `hidewhois_hide_server`, `hidewhois_hide_idle`, `hidewhois_hide_secure`.
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
//! irccloudtags — support for IRCCloud's client-only message tags
|
//! irccloudtags — validate IRCCloud client-only message tags (`+draft/unreact`,
|
||||||
//! (`+draft/unreact`, `+draft/edit`, `+draft/edit-text`, `+draft/attachments`,
|
//! `+draft/edit`, `+draft/edit-text`, `+draft/attachments`,
|
||||||
//! `+draft/attachment-fallback`). echoIRCd already relays *all* `+` client tags to
|
//! `+draft/attachment-fallback`). Each must carry a value; an empty one is rejected
|
||||||
//! `message-tags` clients, so these flow for free; what this module adds is the
|
//! with `FAIL … MESSAGE_TAG_TOO_SHORT`. Relaying of `+` client tags is handled
|
||||||
//! spec validation InspIRCd's `m_ircv3_irccloudtags` does — each of these tags MUST
|
//! elsewhere by the `message-tags` cap.
|
||||||
//! 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.
|
|
||||||
|
|
||||||
use crate::module::{ModResult, Module};
|
use crate::module::{ModResult, Module};
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
@ -34,14 +31,14 @@ impl Module for IrcCloudTags {
|
||||||
cmd: &str,
|
cmd: &str,
|
||||||
_params: &[String],
|
_params: &[String],
|
||||||
) -> ModResult {
|
) -> ModResult {
|
||||||
// only messages carry client tags
|
// only message commands carry client tags
|
||||||
if !(cmd.eq_ignore_ascii_case("PRIVMSG")
|
if !(cmd.eq_ignore_ascii_case("PRIVMSG")
|
||||||
|| cmd.eq_ignore_ascii_case("NOTICE")
|
|| cmd.eq_ignore_ascii_case("NOTICE")
|
||||||
|| cmd.eq_ignore_ascii_case("TAGMSG"))
|
|| cmd.eq_ignore_ascii_case("TAGMSG"))
|
||||||
{
|
{
|
||||||
return ModResult::Passthru;
|
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()) {
|
for raw in srv.line_ctags.split(';').filter(|t| !t.is_empty()) {
|
||||||
let (name, val) = raw.split_once('=').unwrap_or((raw, ""));
|
let (name, val) = raw.split_once('=').unwrap_or((raw, ""));
|
||||||
if TAGS.contains(&name) && val.is_empty() {
|
if TAGS.contains(&name) && val.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
//! jsonlog — the `draft/json-log` capability. reverse's own module. When an oper
|
//! jsonlog — the `draft/json-log` capability. When an oper negotiates
|
||||||
//! negotiates `CAP REQ draft/json-log`, every server notice they receive carries a
|
//! `CAP REQ draft/json-log`, every server notice they receive carries a structured
|
||||||
//! structured JSON object (timestamp, level, subsystem, msg, …) as an IRCv3 message
|
//! JSON object (timestamp, level, subsystem, msg, …) as an IRCv3 message tag; the
|
||||||
//! **tag** — the human-readable text stays in the NOTICE, the machine-readable copy
|
//! human-readable text stays in the NOTICE. Dispatched from `Server::snotice`; the
|
||||||
//! rides alongside. Companion to the RPC `log.*` methods, which expose the same data
|
//! tag build lives here.
|
||||||
//! over HTTP. Dispatched straight from `Server::snotice`; the tag build lives here.
|
|
||||||
//!
|
|
||||||
//! Behaviour reference: reverse's InspIRCd `m_jsonrpclog`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::modules::rpc::json::{obj, qstr};
|
use crate::modules::rpc::json::{obj, qstr};
|
||||||
use crate::server::{iso_time, now, Server};
|
use crate::server::{iso_time, now, Server};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
//! maphide — hide the server map (`LINKS` / `MAP`) from ordinary users, so the
|
//! maphide — hide the server map (`LINKS` / `MAP`) from non-opers, so network
|
||||||
//! network topology isn't exposed to non-operators. Off unless `maphide = yes`.
|
//! topology isn't exposed. Off unless `maphide = yes`.
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_maphide`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::module::{ModResult, Module};
|
use crate::module::{ModResult, Module};
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! markread — InspIRCd's `m_ircv3_read_marker` (draft/read-marker). A client sets
|
//! markread — IRCv3 draft/read-marker. A client sets or queries the "last read"
|
||||||
//! or queries the "last read" timestamp per conversation; markers are keyed by
|
//! timestamp per conversation; markers are keyed by account when logged in (shared
|
||||||
//! account when logged in (so they're shared across a user's devices and survive
|
//! across a user's devices, surviving reconnects) and echoed to every connection
|
||||||
//! reconnects) and echoed to every connection sharing that identity. Self-contained:
|
//! sharing that identity. The store lives in `Server.ext`, cleaned up by the
|
||||||
//! the marker store lives in `Server.ext`, cleaned up by the on_user_quit hook.
|
//! on_user_quit hook.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
//! metadata — InspIRCd's `m_ircv3_metadata` (draft/metadata-2). Client METADATA
|
//! metadata — IRCv3 draft/metadata-2. Client METADATA GET/LIST/SET/CLEAR on users
|
||||||
//! GET/LIST/SET/CLEAR on users and channels, op-gated, with change notices in a
|
//! and channels, op-gated, with change notices in a `metadata` batch. The store
|
||||||
//! `metadata` batch. Self-contained: the store lives in `Server.ext`, cleaned up
|
//! lives in `Server.ext`, cleaned up by the on_user_quit hook.
|
||||||
//! by the on_user_quit hook; the command and its logic are all here.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
@ -161,7 +160,7 @@ impl Command for MetadataCmd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if key.starts_with('#') {
|
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 setter = s.users.get(&uid).map(|u| u.prefix()).unwrap_or_default();
|
||||||
let note = match &value {
|
let note = match &value {
|
||||||
|
|
@ -214,9 +213,9 @@ fn db_path(s: &Server) -> String {
|
||||||
format!("{}.metadata", s.conf_path)
|
format!("{}.metadata", s.conf_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persist channel metadata (the `#`-keyed entries) so it survives a restart —
|
/// Persist channel metadata (the `#`-keyed entries) so it survives a restart.
|
||||||
/// InspIRCd `m_ircv3_metadata_db`. Per-user metadata (`u<uid>`) is intentionally
|
/// Per-user metadata (`u<uid>`) is intentionally not saved: uids don't persist
|
||||||
/// not saved: uids don't persist across restarts.
|
/// across restarts.
|
||||||
pub fn save(s: &Server) {
|
pub fn save(s: &Server) {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
if let Some(st) = s.ext.get::<MetaStore>() {
|
if let Some(st) = s.ext.get::<MetaStore>() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Optional, pluggable modules — echoIRCd's answer to InspIRCd's `src/modules/`.
|
//! Optional, pluggable modules. Most hook lifecycle events via the
|
||||||
//! Most hook lifecycle events via the [`crate::module::Module`] trait; [`dnsbl`]
|
//! [`crate::module::Module`] trait; [`dnsbl`] is the exception — it's driven from
|
||||||
//! is the exception — it's driven straight from the connection lifecycle rather
|
//! the connection lifecycle rather than the hook bus, but lives here as its own
|
||||||
//! than the hook bus, but lives here as its own self-contained unit.
|
//! self-contained unit.
|
||||||
|
|
||||||
pub mod account_registration;
|
pub mod account_registration;
|
||||||
pub mod antimixedutf8;
|
pub mod antimixedutf8;
|
||||||
|
|
@ -44,6 +44,8 @@ pub mod markread;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod multiline;
|
pub mod multiline;
|
||||||
pub mod network_icon;
|
pub mod network_icon;
|
||||||
|
pub mod ojoin;
|
||||||
|
pub mod operprefix;
|
||||||
pub mod password_hash;
|
pub mod password_hash;
|
||||||
pub mod profilelink;
|
pub mod profilelink;
|
||||||
pub mod randquote;
|
pub mod randquote;
|
||||||
|
|
@ -99,6 +101,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
|
||||||
Box::new(solvemsg::SolveMsg),
|
Box::new(solvemsg::SolveMsg),
|
||||||
Box::new(autoop::AutoOp),
|
Box::new(autoop::AutoOp),
|
||||||
Box::new(autodrop::AutoDrop),
|
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
|
/// 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>> {
|
pub fn module_commands() -> Vec<Box<dyn Command>> {
|
||||||
filter::commands()
|
filter::commands()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -135,5 +138,6 @@ pub fn module_commands() -> Vec<Box<dyn Command>> {
|
||||||
.chain(geoip::commands())
|
.chain(geoip::commands())
|
||||||
.chain(globops::commands())
|
.chain(globops::commands())
|
||||||
.chain(relaymsg::commands())
|
.chain(relaymsg::commands())
|
||||||
|
.chain(ojoin::commands())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
//! multiline — the server side of IRCv3 draft/multiline. A client wraps one long
|
//! multiline — server side of IRCv3 draft/multiline. A client wraps one long message
|
||||||
//! message in a `BATCH +<ref> draft/multiline <target>`; the `@batch=<ref>`-tagged
|
//! in a `BATCH +<ref> draft/multiline <target>`; the `@batch=<ref>`-tagged
|
||||||
//! PRIVMSG/NOTICE lines are buffered here (see `Ircd::dispatch`) and, when
|
//! PRIVMSG/NOTICE lines are buffered (see `Ircd::dispatch`) and, when `BATCH -<ref>`
|
||||||
//! `BATCH -<ref>` closes, reassembled (honouring `draft/multiline-concat`) and
|
//! closes, reassembled (honouring `draft/multiline-concat`) and delivered as normal
|
||||||
//! delivered as normal messages. Self-contained: the in-flight batches live in
|
//! messages. Limits: multiline_maxbytes / multiline_maxlines. In-flight batches live
|
||||||
//! `Server.ext`, cleaned up by the on_user_quit hook; the BATCH command and the
|
//! in `Server.ext`, cleaned up by the on_user_quit hook.
|
||||||
//! accumulate/close logic are all here.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! ircv3_network_icon — InspIRCd `m_ircv3_network_icon`. Advertises a network icon
|
//! network_icon — advertise a network icon via the `ICON` ISUPPORT token from
|
||||||
//! via the `draft/ICON` ISUPPORT token from `network_icon = <url>`. Config-driven;
|
//! `network_icon = <url>`.
|
||||||
//! nothing lives on `Server`.
|
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
||||||
|
|
|
||||||
55
src/modules/ojoin.rs
Normal file
55
src/modules/ojoin.rs
Normal 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
79
src/modules/operprefix.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
//! password_hash — hashed `<oper>` passwords plus a `/MKPASSWD` helper to make
|
//! Hashed `<oper>` passwords plus a `/MKPASSWD` helper, backed by OpenSSL.
|
||||||
//! them. This is echoIRCd's answer to InspIRCd's hash-provider family (md5, sha1,
|
//! Verifies stored hashes and generates new ones (md5, sha1, sha2, pbkdf2).
|
||||||
//! 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.
|
|
||||||
//!
|
//!
|
||||||
//! A stored password is either plaintext (no recognised prefix — backward
|
//! A stored password is either plaintext (no recognised prefix) or `"<algo>:<hex>"`:
|
||||||
//! compatible) or `"<algo>:<hex>"`:
|
|
||||||
//! * `md5:` `sha1:` `sha256:` `sha512:` — a plain hex digest of the password
|
//! * `md5:` `sha1:` `sha256:` `sha512:` — a plain hex digest of the password
|
||||||
//! * `pbkdf2:<iters>:<salthex>:<hashhex>` — PBKDF2-HMAC-SHA256, salted
|
//! * `pbkdf2:<iters>:<salthex>:<hashhex>` — PBKDF2-HMAC-SHA256, salted
|
||||||
//!
|
//!
|
||||||
//! Comparisons are constant-time (`openssl::memcmp`). Everything is self-contained
|
//! Comparisons are constant-time (`openssl::memcmp`). The OPER handler calls [`verify`].
|
||||||
//! here; the OPER handler just calls [`verify`].
|
|
||||||
|
|
||||||
use openssl::hash::{hash, MessageDigest};
|
use openssl::hash::{hash, MessageDigest};
|
||||||
use openssl::pkcs5::pbkdf2_hmac;
|
use openssl::pkcs5::pbkdf2_hmac;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
//! profileLink — InspIRCd `m_profileLink`. Adds a profile URL to WHOIS for
|
//! Adds a profile URL to WHOIS for logged-in users, from `profilelink_baseurl = <url>`.
|
||||||
//! logged-in users from `profilelink_baseurl = <url>`. Config-driven; nothing
|
|
||||||
//! lives on `Server`.
|
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
//! randquote — greet each connecting user with a random line from a configured
|
//! Greet each connecting user with a random line from the configured quote set.
|
||||||
//! set of quotes. Off unless one or more `randquote = <line>` are configured.
|
//! Off unless one or more `randquote = <line>` are configured.
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_randquote`. Original native Rust.
|
|
||||||
|
|
||||||
use openssl::rand::rand_bytes;
|
use openssl::rand::rand_bytes;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
//! realnameban — the `r:` matching extban: match a user by their real name
|
//! The `r:` matching extban: match a user by real name (GECOS) instead of host.
|
||||||
//! (GECOS) instead of their host. `+b r:*some spammer*` bans everyone whose
|
//! `+b r:*some spammer*` bans everyone whose realname matches the glob.
|
||||||
//! realname matches the glob. Dispatched from the channel ban matcher; the logic
|
//! Dispatched from the channel ban matcher.
|
||||||
//! lives here in its own file.
|
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_realnameban`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
//! recaptcha — gate registration behind a human-verification step. An unverified
|
//! Gate registration behind a human-verification step. An unverified user is handed
|
||||||
//! user is handed a one-time, IP-bound HS256 JWT and a URL to solve a reCAPTCHA at;
|
//! a one-time, IP-bound HS256 JWT and a URL to solve a reCAPTCHA at; once solved they
|
||||||
//! once solved they present the signed token back with `CAPTCHA <token>` and the
|
//! present the signed token back with `CAPTCHA <token>` and the connection is allowed.
|
||||||
//! connection is allowed. reverse's own module.
|
|
||||||
//!
|
//!
|
||||||
//! Modes:
|
//! Modes:
|
||||||
//! * JWT-only (default): a validly-signed, unexpired, IP-matching token is proof
|
//! * JWT-only (default): a validly-signed, unexpired, IP-matching token is proof
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
//! must contain a configured separator and must not collide with a real nick.
|
//! must contain a configured separator and must not collide with a real nick.
|
||||||
//!
|
//!
|
||||||
//! Config: `relaymsg_separators` (default `/`), `relaymsg_ident` (default `relay`),
|
//! Config: `relaymsg_separators` (default `/`), `relaymsg_ident` (default `relay`),
|
||||||
//! `relaymsg_host` (default = server name). Reference: InspIRCd's `m_relaymsg`.
|
//! `relaymsg_host` (default = server name). Local delivery only; cross-server ENCAP
|
||||||
//! (Local delivery; cross-server ENCAP relay is not propagated.) Original native Rust.
|
//! relay is not propagated.
|
||||||
|
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
use crate::numeric::{ERR_BADRELAYNICK, ERR_CANNOTSENDTOCHAN, ERR_NOPRIVILEGES, ERR_NOSUCHCHANNEL};
|
use crate::numeric::{ERR_BADRELAYNICK, ERR_CANNOTSENDTOCHAN, ERR_NOPRIVILEGES, ERR_NOSUCHCHANNEL};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
//! reputation — InspIRCd `m_reputation` (© reverse). Per-network-address reputation
|
//! Per-network-address reputation scoring. Every `bumpinterval` (default 5m) each
|
||||||
//! scoring. Every `bumpinterval` (default 5m) each connected user's masked address
|
//! connected user's masked address gains +1 (+2 if logged into services), provided
|
||||||
//! gains +1 (+2 if logged into services), provided they're in a channel with at
|
//! they're in a channel with at least `minchanmembers` members. Scores decay per the
|
||||||
//! least `minchanmembers` members. Scores decay per the `reputationexpire` rules
|
//! `reputationexpire` rules and persist to disk. Exposes the `y:` score extban, WHOIS
|
||||||
//! and persist to disk. Exposes the `y:` score extban, WHOIS visibility, and the
|
//! visibility, and the `REPUTATION` oper command. Config-driven (see `[reputation_*]`).
|
||||||
//! `REPUTATION` oper command. Everything is config-driven (see `[reputation_*]`).
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
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 {
|
fn v4prefix(s: &Server) -> u8 {
|
||||||
s.conf_num::<u8>("reputation_ipv4prefix", 32).clamp(1, 32)
|
s.conf_num::<u8>("reputation_ipv4prefix", 32).clamp(1, 32)
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +81,7 @@ fn expire_rules(s: &Server) -> Vec<(i32, u64)> {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if rules.is_empty() {
|
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)]
|
vec![(2, 3600), (6, 604800), (12, 2592000), (-1, 7776000)]
|
||||||
} else {
|
} else {
|
||||||
rules
|
rules
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
//! restrictchans — only opers may *create* new channels; everyone can still join
|
//! Only opers may *create* new channels; everyone can still join existing ones.
|
||||||
//! existing ones. A `restrictchan = <glob>` whitelist lets ordinary users create
|
//! A `restrictchan = <glob>` whitelist lets ordinary users create channels whose
|
||||||
//! channels whose name matches (e.g. `restrictchan = #public-*`). Off unless
|
//! name matches (e.g. `restrictchan = #public-*`). Off unless `restrictchans = yes`.
|
||||||
//! `restrictchans = yes`. Dispatched from `Server::join` (like denychans).
|
//! Dispatched from `Server::join`.
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_restrictchans`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::numeric::ERR_BADCHANNEL;
|
use crate::numeric::ERR_BADCHANNEL;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! restrictcommands — hold back chosen commands from brand-new / unregistered
|
//! Hold back chosen commands from brand-new / unregistered users, with exemptions.
|
||||||
//! users (UnrealIRCd's `set::restrict-commands`), with exemptions. reverse's own
|
//! Each restriction is one config line:
|
||||||
//! module. Each restriction is one config line:
|
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! restrictcommand = LIST connectdelay=60 exemptidentified=yes exemptwebirc=yes \
|
//! restrictcommand = LIST connectdelay=60 exemptidentified=yes exemptwebirc=yes \
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
//! restrictmsg — stop ordinary users from private-messaging each other. A PM
|
//! Stop ordinary users from private-messaging each other. A PM between two users is
|
||||||
//! between two users is allowed only when the sender is an oper, the target is an
|
//! allowed only when the sender is an oper, the target is an oper, or the target is a
|
||||||
//! oper, or the target is a service/bot (so users can still reach NickServ etc.).
|
//! service/bot (so users can still reach NickServ etc.). Channel messages are never
|
||||||
//! Channel messages are never affected. Off unless `restrictmsg = yes` — read
|
//! affected. Off unless `restrictmsg = yes`.
|
||||||
//! straight from the config, nothing on `Server`.
|
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_restrictmsg`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::module::{ModResult, Module};
|
use crate::module::{ModResult, Module};
|
||||||
use crate::numeric::ERR_CANTSENDTOUSER;
|
use crate::numeric::ERR_CANTSENDTOUSER;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
//! rmode — `RMODE <channel> <listmode> [pattern]`, bulk-remove entries from a
|
//! `RMODE <channel> <listmode> [pattern]` — bulk-remove entries from a channel list
|
||||||
//! channel list mode (`b` bans, `e` ban exceptions, `I` invite exceptions). With a
|
//! mode (`b` bans, `e` ban exceptions, `I` invite exceptions). With a `pattern` glob
|
||||||
//! `pattern` glob only matching entries are cleared; without one, all are. Needs
|
//! only matching entries are cleared; without one, all are. Needs half-op+ (opers
|
||||||
//! half-op+ (opers bypass). The removals go through the normal mode engine (chunked
|
//! bypass). Removals go through the normal mode engine (chunked to keep each `MODE`
|
||||||
//! to keep each `MODE` line legal), so they broadcast and propagate like any other.
|
//! line legal), so they broadcast and propagate like any other.
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_rmode`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::{glob_match, RANK_HALFOP};
|
use crate::channels::{glob_match, RANK_HALFOP};
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
//! rpc ban provider — `xline.list`, `xline.add`, `xline.del`. InspIRCd's
|
//! X-line RPC provider: `xline.list`, `xline.add`, `xline.del`. Covers every
|
||||||
//! `m_rpc_ban`. Covers every echoIRCd x-line kind (K/G/Z/E/SHUN/Q/CBAN) through the
|
//! x-line kind (K/G/Z/E/SHUN/Q/CBAN) via the same `add_xline`/`remove_xline`
|
||||||
//! same `add_xline`/`remove_xline` primitives the oper commands use.
|
//! primitives the oper commands use.
|
||||||
|
|
||||||
use super::json::{self, obj, qstr};
|
use super::json::{self, obj, qstr};
|
||||||
use super::RpcError;
|
use super::RpcError;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::xline::{parse_duration, XKind};
|
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> {
|
fn kind_of(t: &str) -> Option<XKind> {
|
||||||
let up = t.to_ascii_uppercase();
|
let up = t.to_ascii_uppercase();
|
||||||
XKind::from_tag(&up).or_else(|| {
|
XKind::from_tag(&up).or_else(|| {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! rpc channel provider — `channel.list`, `channel.get`, and the mutators
|
//! Channel RPC provider: `channel.list`, `channel.get`, and the mutators
|
||||||
//! `channel.kick`, `channel.set_topic`. InspIRCd's `m_rpc_channel`. (`channel.set_mode`
|
//! `channel.kick`, `channel.set_topic`, `channel.set_mode`.
|
||||||
//! lands with the shared server-side mode applier in a later pass.)
|
|
||||||
|
|
||||||
use super::json::{obj, qstr};
|
use super::json::{obj, qstr};
|
||||||
use super::RpcError;
|
use super::RpcError;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! rpc core provider — introspection: `rpc.methods` (list the interface),
|
//! Core RPC introspection: `rpc.methods` (list the interface), `rpc.info`
|
||||||
//! `rpc.info` (identity + methods), and `server.info` / `stats.get` (identity +
|
//! (identity + methods), and `server.info` / `stats.get` (identity + network counts).
|
||||||
//! network counts). InspIRCd's `m_rpc_core` + the legacy `stats.get`.
|
|
||||||
|
|
||||||
use super::json::{obj, qstr};
|
use super::json::{obj, qstr};
|
||||||
use super::{RpcError, ALL_METHODS};
|
use super::{RpcError, ALL_METHODS};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
//! The RPC HTTP server: a blocking listener thread that accepts a connection,
|
//! RPC HTTP server: a blocking listener thread that accepts a connection, reads
|
||||||
//! reads one HTTP request, authenticates it, and forwards the JSON-RPC body to the
|
//! one HTTP request, authenticates it, forwards the JSON-RPC body to the core as
|
||||||
//! core as `Event::RpcRequest` — then writes back whatever the core replies. Low
|
//! `Event::RpcRequest`, and writes back the core's reply. Low volume (admin
|
||||||
//! volume (admin tooling), so thread-per-connection is fine. Native `TcpStream`
|
//! tooling), so thread-per-connection is fine.
|
||||||
//! only; no `unsafe`, no new crate.
|
|
||||||
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::{TcpListener, TcpStream};
|
use std::net::{TcpListener, TcpStream};
|
||||||
|
|
@ -68,9 +67,9 @@ fn handle(
|
||||||
stream.set_read_timeout(Some(IO_TIMEOUT))?;
|
stream.set_read_timeout(Some(IO_TIMEOUT))?;
|
||||||
stream.set_write_timeout(Some(IO_TIMEOUT))?;
|
stream.set_write_timeout(Some(IO_TIMEOUT))?;
|
||||||
|
|
||||||
// read until we have the full header block, then the declared body — whether
|
// Read until the full header block, then the declared body — whether
|
||||||
// it's Content-Length-framed or Transfer-Encoding: chunked (like InspIRCd's
|
// Content-Length-framed or Transfer-Encoding: chunked. Body starts 4 bytes
|
||||||
// http_parser handles). Body starts 4 bytes past the header terminator.
|
// past the header terminator.
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let mut chunk = [0u8; 8192];
|
let mut chunk = [0u8; 8192];
|
||||||
let mut head_end = None;
|
let mut head_end = None;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
//! Minimal JSON for the RPC subsystem — no serde (openssl+mio-only crate policy).
|
//! Minimal JSON for the RPC subsystem. Two jobs: pull a named field out of a
|
||||||
//! Two jobs: pull a named field out of a flat-ish request object (`get_*`), and
|
//! request object (`get_*`), and escape strings when building result JSON. The
|
||||||
//! escape strings when *building* result JSON with `format!`. The scanners respect
|
//! scanners respect nesting and string escapes, so `get_raw` only matches a
|
||||||
//! nesting and string escapes, so `get_raw` only ever matches a **top-level** key
|
//! top-level key (a `"nick"` buried in a nested value or a string won't false-match).
|
||||||
//! (a `"nick"` buried inside a nested value or another string won't false-match).
|
|
||||||
|
|
||||||
/// Given `b[i] == b'"'`, return the index just past the closing quote.
|
/// Given `b[i] == b'"'`, return the index just past the closing quote.
|
||||||
fn scan_string(b: &[u8], mut i: usize) -> Option<usize> {
|
fn scan_string(b: &[u8], mut i: usize) -> Option<usize> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! rpc log provider — `log.tail` and `log.events`. InspIRCd's `m_rpc_log` /
|
//! Log RPC provider: `log.tail` and `log.events`. There is no log file (logging
|
||||||
//! `m_jsonrpclog`. echoIRCd has no log *file* (it logs to journald), so both read
|
//! goes to journald), so both read the in-memory server-log ring that
|
||||||
//! the in-memory server-log ring that `Server::snotice` feeds (`Server.log`).
|
//! `Server::snotice` feeds (`Server.log`).
|
||||||
|
|
||||||
use super::json::{self, obj, qstr};
|
use super::json::{self, obj, qstr};
|
||||||
use super::RpcError;
|
use super::RpcError;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! rpc message provider — `message.send_notice`. InspIRCd's `m_rpc_message`.
|
//! Message RPC provider: `message.send_notice`. Sends a server NOTICE to a
|
||||||
//! Sends a server NOTICE to a channel (`#…`), a single user (nick), or every
|
//! channel (`#…`), a single user (nick), or every local user (`*` / `$*`).
|
||||||
//! local user (`*` / `$*`).
|
|
||||||
|
|
||||||
use super::json::{self, obj};
|
use super::json::{self, obj};
|
||||||
use super::RpcError;
|
use super::RpcError;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
//! rpc — a JSON-RPC 2.0 control interface over a small native HTTP server, the
|
//! JSON-RPC 2.0 control interface over a small HTTP server. Admin tools call it to
|
||||||
//! echoIRCd analogue of InspIRCd's `m_httpd` + `m_jsonrpc` + `m_rpc_*`. Admin tools
|
//! introspect and drive the ircd (list/kill users, manage bans, rehash…).
|
||||||
//! 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
|
//! * [`httpd`] — the listener thread: accept, parse HTTP, authenticate, and hand
|
||||||
//! the JSON-RPC body to the core as `Event::RpcRequest`.
|
//! 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` /
|
//! * `core` / `user` / `channel` / `server` / `stats` / `ban` / `message` /
|
||||||
//! `whowas` / `spamfilter` / `log` — the method providers, called on the core
|
//! `whowas` / `spamfilter` / `log` — the method providers, called on the core
|
||||||
//! thread with `&mut Server`.
|
//! 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
|
/// 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 {
|
pub fn envelope(method: &str, id: &str, result: Result<String, RpcError>) -> String {
|
||||||
let id = if id.trim().is_empty() { "null" } else { id };
|
let id = if id.trim().is_empty() { "null" } else { id };
|
||||||
match result {
|
match result {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! rpc server provider — `server.list`, `server.rehash`, `server.disconnect`.
|
//! Server RPC provider: `server.list`, `server.rehash`, `server.connect`,
|
||||||
//! InspIRCd's `m_rpc_server`. (`server.connect` needs the socketengine's dialer,
|
//! `server.disconnect`.
|
||||||
//! which isn't reachable from the core thread — use the `CONNECT` command instead.)
|
|
||||||
|
|
||||||
use super::json::{obj, qstr};
|
use super::json::{obj, qstr};
|
||||||
use super::RpcError;
|
use super::RpcError;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! rpc spamfilter provider — `spamfilter.list`, `spamfilter.add`, `spamfilter.del`.
|
//! Spamfilter RPC provider: `spamfilter.list`, `spamfilter.add`, `spamfilter.del`.
|
||||||
//! InspIRCd's `m_rpc_spamfilter`. Operates on the same [`crate::modules::filter`]
|
//! Operates on the same [`crate::modules::filter`] rule set (in `Server.ext`) that
|
||||||
//! rule set (stored in `Server.ext`) that the `FILTER` command and the enforcement
|
//! the `FILTER` command and the enforcement hook use, so a rule added here takes
|
||||||
//! hook use, so a rule added here takes effect immediately.
|
//! effect immediately.
|
||||||
|
|
||||||
use super::json::{self, obj, qstr};
|
use super::json::{self, obj, qstr};
|
||||||
use super::RpcError;
|
use super::RpcError;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! rpc stats provider — read-only introspection: `module.list`, `oper.list`,
|
//! Stats RPC provider: read-only introspection via `module.list`, `oper.list`,
|
||||||
//! `security_group.list`. InspIRCd's `m_rpc_stats`.
|
//! `security_group.list`.
|
||||||
|
|
||||||
use super::json::{obj, qstr};
|
use super::json::{obj, qstr};
|
||||||
use super::RpcError;
|
use super::RpcError;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! rpc user provider — `user.list`, `user.get`, and the mutators `user.kill`,
|
//! User RPC provider: `user.list`, `user.get`, and the mutators `user.kill`,
|
||||||
//! `user.set_mode`, `user.set_vhost`, `user.set_nick`, `user.set_oper`. InspIRCd's
|
//! `user.set_mode`, `user.set_vhost`, `user.set_nick`, `user.set_oper`. Mutators
|
||||||
//! `m_rpc_user`. Mutators route through the same `Server` primitives the commands
|
//! route through the same `Server` primitives the commands use, so behaviour and
|
||||||
//! use, so behaviour and side-effects (QUIT/CHGHOST/MODE broadcasts) stay identical.
|
//! side-effects (QUIT/CHGHOST/MODE broadcasts) stay identical.
|
||||||
|
|
||||||
use super::json::{self, obj, qstr};
|
use super::json::{self, obj, qstr};
|
||||||
use super::RpcError;
|
use super::RpcError;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! rpc whowas provider — `whowas.get`. InspIRCd's `m_rpc_whowas`. Returns the
|
//! Whowas RPC provider: `whowas.get`. Returns the recent-nick-history entries
|
||||||
//! recent-nick-history entries the ircd keeps for `WHOWAS`.
|
//! the ircd keeps for `WHOWAS`.
|
||||||
|
|
||||||
use super::json::{self, obj, qstr};
|
use super::json::{self, obj, qstr};
|
||||||
use super::RpcError;
|
use super::RpcError;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
//! securelist — hold back the `/LIST` command until a user has been connected for
|
//! Hold back the `/LIST` command until a user has been connected for a while, which
|
||||||
//! a while, which defeats spambots that connect, `LIST`, spam every channel and
|
//! defeats spambots that connect, `LIST`, spam every channel and leave. Non-exempt
|
||||||
//! leave. Non-exempt users who `LIST` too early get an optional notice and a
|
//! users who `LIST` too early get an optional notice and a throwaway *fake* channel
|
||||||
//! throwaway *fake* channel list (so a bot waiting on the reply is satisfied and
|
//! list (so a bot waiting on the reply is satisfied), then the real `LIST` is denied.
|
||||||
//! wastes its time), then the real `LIST` is denied. Exempt: opers, logged-in
|
//! Exempt: opers, logged-in accounts (when `securelist_exemptregistered`), and hosts
|
||||||
//! accounts (when `securelist_exemptregistered`), and hosts matching a
|
//! matching a `securelist_exception` glob. Off unless `securelist = yes`.
|
||||||
//! `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.
|
|
||||||
|
|
||||||
use openssl::rand::rand_bytes;
|
use openssl::rand::rand_bytes;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
//! securitygroups — UnrealIRCd-style security groups (InspIRCd `m_securitygroups`).
|
//! Named security groups. A `securitygroup` config line defines a named set of users
|
||||||
//! A `securitygroup` config line defines a named set of users by AND-ed criteria
|
//! by AND-ed criteria (host masks, TLS, account, oper, bot, webirc, reputation score
|
||||||
//! (host masks, TLS, account, oper, bot, webirc, reputation score range). Groups
|
//! range). Groups drive the `g:` matching extban, the `SECURITYGROUPS` command, and a
|
||||||
//! drive the `g:` matching extban, the `SECURITYGROUPS` command, and a WHOIS line.
|
//! WHOIS line.
|
||||||
//! Self-contained: the group defs live in `Server.sec_groups`; evaluation is here.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
|
|
@ -19,7 +18,7 @@ enum Tri {
|
||||||
No,
|
No,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A UnrealIRCd-style security group — all criteria AND-ed.
|
/// A security group — all criteria AND-ed.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
struct SecGroup {
|
struct SecGroup {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
//! serverban — the `s:` matching extban: match a user by the name of the server
|
//! The `s:` matching extban: match a user by the name of the server they are
|
||||||
//! they are connected to. `+b s:irc.example.net` bans everyone on that server.
|
//! connected to. `+b s:irc.example.net` bans everyone on that server. Ban matching
|
||||||
//! Ban matching only ever runs against local users (join happens locally), so a
|
//! only ever runs against local users, so a matched user is on this server and the
|
||||||
//! matched user is on this server — we glob the mask against our own name.
|
//! mask is globbed against the local server name. Dispatched from the channel ban
|
||||||
//! Dispatched from the channel ban matcher; the logic lives here.
|
//! matcher.
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_serverban`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//! showfile — serve a text file as its own command (InspIRCd's `m_showfile`).
|
//! Serve a text file as its own command. Config, one line per file:
|
||||||
//! Config, one line per file:
|
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! showfile = <COMMAND> <path> # e.g. showfile = RULES /etc/echoircd/rules.txt
|
//! 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
|
//! 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
|
//! 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.
|
//! needed. The file is read fresh on each use, so edits show without a REHASH.
|
||||||
//! Original native Rust.
|
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
//! solvemsg — a lightweight anti-spam gate: before an un-vouched user's *private*
|
//! A lightweight anti-spam gate: before an un-vouched user's *private* messages are
|
||||||
//! messages are delivered, they must answer one small arithmetic question. Opers
|
//! delivered, they must answer one small arithmetic question. Opers and users logged
|
||||||
//! and users logged into an account are exempt. Off unless `solvemsg = yes`.
|
//! 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
|
//! 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.
|
//! 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::module::{ModResult, Module};
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
@ -17,7 +16,7 @@ struct SolveState {
|
||||||
answer: Option<i64>,
|
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 {
|
fn rnd(max: u8) -> u8 {
|
||||||
let mut b = [0u8; 1];
|
let mut b = [0u8; 1];
|
||||||
let _ = openssl::rand::rand_bytes(&mut b);
|
let _ = openssl::rand::rand_bytes(&mut b);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
//! tline — `TLINE <mask>`, an oper command that reports how many currently-connected
|
//! `TLINE <mask>` — oper command reporting how many currently-connected local users
|
||||||
//! local users a would-be K/G/Z-line mask matches, so you can gauge the blast radius
|
//! a would-be K/G/Z-line mask matches, to gauge the blast radius before setting the ban.
|
||||||
//! before actually setting the ban.
|
|
||||||
//!
|
|
||||||
//! Behaviour reference: InspIRCd's `m_tline`. Original native Rust.
|
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! whoisport — InspIRCd `m_whoisport`. Shows an IRC operator, in WHOIS, the
|
//! Shows an IRC operator, in WHOIS, the listener port the target connected to.
|
||||||
//! listener port the target connected to. Config-free (derives the port from the
|
//! Derives the port from the `bind` / `bind_tls` listeners.
|
||||||
//! `bind` / `bind_tls` listeners); nothing lives on `Server`.
|
|
||||||
|
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
//! DNS lookups — echoIRCd's answer to InspIRCd's async resolver + `m_dnsbl`, done
|
//! DNS lookups over std UDP (no DNS crate). Two things:
|
||||||
//! from scratch with std UDP (no DNS crate, no `unsafe`). Two things:
|
|
||||||
//!
|
//!
|
||||||
//! * **reverse-DNS**: PTR-resolve a client IP and **forward-confirm** it (the name
|
//! * **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
|
//! must resolve back to the same IP, so a client can't fake a hostname);
|
||||||
//! anti-spoofing InspIRCd does);
|
|
||||||
//! * **DNSBL**: reverse the client's v4 octets under a blocklist zone and A-lookup
|
//! * **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
|
//! 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
|
//! IP. Runs off the core thread (never blocks the daemon), bounded in time (the UDP
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue