modules: move all per-module state/config out of server.rs/config.rs into own files

This commit is contained in:
Jean Chevronnet 2026-08-09 11:18:04 +00:00
parent 0d4c9297b5
commit 131471e245
18 changed files with 525 additions and 425 deletions

View file

@ -8,40 +8,16 @@
//! oper = god secret
//! ```
use std::collections::HashMap;
/// Parse a boolean config value (`yes`/`no`/`true`/`false`/`on`/`off`/`1`/`0`).
fn yesish(v: &str) -> bool {
pub fn yesish(v: &str) -> bool {
!matches!(
v.to_ascii_lowercase().as_str(),
"off" | "no" | "false" | "0"
)
}
/// Tri-state for a security-group criterion: don't-care / must-be / must-not-be.
#[derive(Clone, Copy, PartialEq, Default)]
pub enum Tri {
#[default]
Ignore,
Yes,
No,
}
/// A UnrealIRCd-style security group (InspIRCd `m_securitygroups`). All criteria
/// are AND-ed: a user is a member iff every set criterion matches.
#[derive(Clone, Default)]
pub struct SecGroup {
pub name: String,
pub public: bool, // shown to non-opers
pub masks: Vec<String>, // positive: match any one nick!user@host glob
pub exclude_masks: Vec<String>, // negative: matching any one vetoes membership
pub tls: Tri,
pub account: Tri,
pub oper: Tri,
pub bot: Tri,
pub webirc: Tri,
pub score_min: Option<u32>, // reputation lower bound
pub score_max: Option<u32>, // reputation upper bound
}
/// A server-link block: how to authenticate a peer named `name` (and, if
/// `autoconnect`, where to dial it). Passwords are the shared link secret.
#[derive(Clone)]
@ -110,37 +86,9 @@ pub struct Config {
pub dnsbl_reason: String, // ban reason for a DNSBL hit
pub sasl_server: String, // linked services server that handles SASL ("" = none)
pub webirc: Vec<(String, String, String)>, // web gateways: (password, name, ip-mask)
pub opermotd: Vec<String>, // OPERMOTD text, one line per entry
pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host)
pub aliases: Vec<(String, String)>, // command aliases: (name, target-nick)
pub connflood: Option<(u32, u64)>, // (max conns, per secs) from one IP before refusing
pub sec_groups: Vec<SecGroup>, // UnrealIRCd-style security groups
pub autojoin: Vec<String>, // conn_join: channels every user joins on connect
pub auto_umodes: String, // conn_umodes: umodes set on connect (e.g. "+ix")
pub conn_banner: Vec<String>, // connbanner: NOTICE lines sent on connect
pub oper_autojoin: Vec<String>, // operjoin: channels opers join on /OPER
pub oper_umodes: String, // opermodes: umodes set on /OPER
pub seenicks: bool, // snotice every nick change
pub announce_chan: bool, // chancreate: snotice when a channel is created
pub rep_database: String, // reputation: db file (default <conf>.reputation)
pub rep_ipv4prefix: u8, // reputation: IPv4 CIDR prefix for keying (32)
pub rep_ipv6prefix: u8, // reputation: IPv6 CIDR prefix for keying (64)
pub rep_scorecap: u32, // reputation: max score (10000)
pub rep_bump_secs: u64, // reputation: seconds between score bumps (300)
pub rep_expire_secs: u64, // reputation: seconds between expiry runs (605)
pub rep_save_secs: u64, // reputation: seconds between disk saves (902)
pub rep_minchanmembers: usize, // reputation: only bump if in a chan this big (3)
pub rep_whois: String, // reputation: whois visibility all|opers|self|none
pub rep_expire_rules: Vec<(i32, u64)>, // (score-threshold, age-secs) decay rules
pub network_icon: String, // ircv3_network_icon: draft/ICON ISUPPORT url
pub profilelink_baseurl: String, // profileLink: WHOIS profile url base
pub hidewhois: bool, // hidewhois: hide sensitive WHOIS lines from users
pub hidewhois_opers: bool, // hidewhois: opers still see everything (default yes)
pub hidewhois_selfview: bool, // hidewhois: a user sees their own full WHOIS (yes)
pub hidewhois_server: bool, // hidewhois: hide 312 server line
pub hidewhois_idle: bool, // hidewhois: hide 317 idle line
pub hidewhois_away: bool, // hidewhois: hide 301 away line
pub hidewhois_secure: bool, // hidewhois: hide 671 secure line
/// Every `key = value` line, captured raw so modules read their own settings
/// via `Server::conf*` — no per-module field bloats this struct or `Server`.
pub raw: HashMap<String, Vec<String>>,
}
impl Default for Config {
@ -169,37 +117,7 @@ impl Default for Config {
dnsbl_reason: "Your host is listed in a DNS blocklist".to_string(),
sasl_server: String::new(),
webirc: Vec::new(),
opermotd: Vec::new(),
vhosts: Vec::new(),
aliases: Vec::new(),
connflood: None,
sec_groups: Vec::new(),
autojoin: Vec::new(),
auto_umodes: String::new(),
conn_banner: Vec::new(),
oper_autojoin: Vec::new(),
oper_umodes: String::new(),
seenicks: false,
announce_chan: false,
rep_database: String::new(),
rep_ipv4prefix: 32,
rep_ipv6prefix: 64,
rep_scorecap: 10000,
rep_bump_secs: 300,
rep_expire_secs: 605,
rep_save_secs: 902,
rep_minchanmembers: 3,
rep_whois: "all".to_string(),
rep_expire_rules: Vec::new(),
network_icon: String::new(),
profilelink_baseurl: String::new(),
hidewhois: false,
hidewhois_opers: true,
hidewhois_selfview: true,
hidewhois_server: true,
hidewhois_idle: true,
hidewhois_away: true,
hidewhois_secure: true,
raw: HashMap::new(),
}
}
}
@ -244,6 +162,9 @@ impl Config {
continue;
};
let (k, v) = (k.trim(), v.trim());
// Every line is captured raw so a module can read its own settings via
// `Server::conf*` without a typed field bloating config.rs / server.rs.
c.raw.entry(k.to_string()).or_default().push(v.to_string());
match k {
"servername" | "server" => c.servername = v.to_string(),
"network" => c.network = v.to_string(),
@ -344,162 +265,6 @@ impl Config {
c.webirc.push((pass.to_string(), gw, mask));
}
}
"opermotd" => c.opermotd.push(v.to_string()),
"vhost" => {
// vhost = <user> <pass> <host>
let mut it = v.split_whitespace();
if let (Some(u), Some(p), Some(h)) = (it.next(), it.next(), it.next()) {
c.vhosts.push((u.to_string(), p.to_string(), h.to_string()));
}
}
"alias" => {
// alias = <command> <target-nick> (e.g. `alias = NS NickServ`)
let mut it = v.split_whitespace();
if let (Some(name), Some(target)) = (it.next(), it.next()) {
c.aliases
.push((name.to_ascii_uppercase(), target.to_string()));
}
}
"connflood" => {
// connflood = <max> <secs> — refuse >max connections/secs from one IP
let mut it = v.split_whitespace();
if let (Some(mx), Some(sc)) = (it.next(), it.next()) {
if let (Ok(mx), Ok(sc)) = (mx.parse::<u32>(), sc.parse::<u64>()) {
if mx > 0 && sc > 0 {
c.connflood = Some((mx, sc));
}
}
}
}
"autojoin" | "conn_join" => {
for chan in v.split([',', ' ']).filter(|c| !c.is_empty()) {
c.autojoin.push(chan.to_string());
}
}
"autoumodes" | "conn_umodes" => c.auto_umodes = v.to_string(),
"connbanner" => c.conn_banner.push(v.to_string()),
"operjoin" => {
for chan in v.split([',', ' ']).filter(|c| !c.is_empty()) {
c.oper_autojoin.push(chan.to_string());
}
}
"opermodes" | "oper_umodes" => c.oper_umodes = v.to_string(),
"seenicks" => {
c.seenicks = !matches!(
v.to_ascii_lowercase().as_str(),
"off" | "no" | "false" | "0"
)
}
"chancreate" | "announce_channels" => {
c.announce_chan = !matches!(
v.to_ascii_lowercase().as_str(),
"off" | "no" | "false" | "0"
)
}
"reputation_database" => c.rep_database = v.to_string(),
"reputation_ipv4prefix" => {
if let Ok(n) = v.parse::<u8>() {
c.rep_ipv4prefix = n.clamp(1, 32);
}
}
"reputation_ipv6prefix" => {
if let Ok(n) = v.parse::<u8>() {
c.rep_ipv6prefix = n.clamp(1, 128);
}
}
"reputation_scorecap" => {
if let Ok(n) = v.parse() {
c.rep_scorecap = n;
}
}
"reputation_bumpinterval" => {
if let Some(d) = crate::xline::parse_duration(v).filter(|&d| d > 0) {
c.rep_bump_secs = d;
}
}
"reputation_expireinterval" => {
if let Some(d) = crate::xline::parse_duration(v).filter(|&d| d > 0) {
c.rep_expire_secs = d;
}
}
"reputation_saveinterval" => {
if let Some(d) = crate::xline::parse_duration(v).filter(|&d| d > 0) {
c.rep_save_secs = d;
}
}
"reputation_minchanmembers" => {
if let Ok(n) = v.parse() {
c.rep_minchanmembers = n;
}
}
"reputation_whois" => c.rep_whois = v.to_ascii_lowercase(),
"reputationexpire" => {
// reputationexpire = <score|*> <age> (decay rule; * = any score)
let mut it = v.split_whitespace();
if let (Some(sc), Some(age)) = (it.next(), it.next()) {
let score = if sc == "*" {
-1
} else {
sc.parse().unwrap_or(-1)
};
if let Some(age) = crate::xline::parse_duration(age).filter(|&a| a > 0) {
c.rep_expire_rules.push((score, age));
}
}
}
"network_icon" | "networkicon" => c.network_icon = v.to_string(),
"profilelink" | "profilelink_baseurl" => c.profilelink_baseurl = v.to_string(),
"hidewhois" => {
c.hidewhois = !matches!(
v.to_ascii_lowercase().as_str(),
"off" | "no" | "false" | "0"
)
}
"hidewhois_opers" => c.hidewhois_opers = yesish(v),
"hidewhois_selfview" => c.hidewhois_selfview = yesish(v),
"hidewhois_hide_server" => c.hidewhois_server = yesish(v),
"hidewhois_hide_idle" => c.hidewhois_idle = yesish(v),
"hidewhois_hide_away" => c.hidewhois_away = yesish(v),
"hidewhois_hide_secure" => c.hidewhois_secure = yesish(v),
"securitygroup" | "secgroup" => {
// securitygroup = <name> [public] [tls|insecure] [account|unregistered]
// [oper|exclude-oper] [bot|exclude-bot] [webirc|exclude-webirc]
// [mask=<glob>]... [exclude=<glob>]... [scoremin=N] [scoremax=N]
let mut it = v.split_whitespace();
if let Some(name) = it.next() {
let mut g = SecGroup {
name: name.to_string(),
..Default::default()
};
for tok in it {
let (k, val) = match tok.split_once('=') {
Some((a, b)) => (a, Some(b)),
None => (tok, None),
};
match (k, val) {
("public", _) => g.public = true,
("mask", Some(m)) => g.masks.push(m.to_string()),
("exclude", Some(m)) | ("exclude-mask", Some(m)) => {
g.exclude_masks.push(m.to_string())
}
("tls", _) | ("tls-users", _) => g.tls = Tri::Yes,
("insecure", _) | ("exclude-tls", _) => g.tls = Tri::No,
("account", _) | ("registered", _) => g.account = Tri::Yes,
("unregistered", _) | ("exclude-account", _) => g.account = Tri::No,
("oper", _) => g.oper = Tri::Yes,
("exclude-oper", _) => g.oper = Tri::No,
("bot", _) | ("bmode", _) => g.bot = Tri::Yes,
("exclude-bot", _) | ("exclude-bmode", _) => g.bot = Tri::No,
("webirc", _) => g.webirc = Tri::Yes,
("exclude-webirc", _) => g.webirc = Tri::No,
("scoremin", Some(n)) => g.score_min = n.parse().ok(),
("scoremax", Some(n)) => g.score_max = n.parse().ok(),
_ => {}
}
}
c.sec_groups.push(g);
}
}
_ => {}
}
}