modules: move all per-module state/config out of server.rs/config.rs into own files
This commit is contained in:
parent
0d4c9297b5
commit
131471e245
18 changed files with 525 additions and 425 deletions
60
src/modules/connflood.rs
Normal file
60
src/modules/connflood.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
//! connflood — InspIRCd `m_connflood`. Refuse connections from an IP opening too
|
||||
//! many too fast. Config: `connflood = <max> <secs>`. Per-IP recent-connect times
|
||||
//! live in `Server.ext`, pruned on the tick — nothing lives on `Server`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::module::Module;
|
||||
use crate::server::{now, Server};
|
||||
|
||||
/// per-IP recent connection timestamps. Stored in `Server.ext`.
|
||||
#[derive(Default)]
|
||||
pub struct ConnHistory(pub HashMap<IpAddr, Vec<u64>>);
|
||||
|
||||
/// `(max, secs)` from `connflood = <max> <secs>`, or `None` when disabled.
|
||||
fn cfg(s: &Server) -> Option<(u32, u64)> {
|
||||
let v = s.conf("connflood")?;
|
||||
let mut it = v.split_whitespace();
|
||||
let mx: u32 = it.next()?.parse().ok()?;
|
||||
let sc: u64 = it.next()?.parse().ok()?;
|
||||
(mx > 0 && sc > 0).then_some((mx, sc))
|
||||
}
|
||||
|
||||
/// Record a connection from `ip`; returns true when it exceeds the limit (the
|
||||
/// caller should refuse it). No-op → false when connflood is unconfigured.
|
||||
pub fn over_limit(s: &mut Server, ip: IpAddr) -> bool {
|
||||
let Some((max, secs)) = cfg(s) else {
|
||||
return false;
|
||||
};
|
||||
let n = now();
|
||||
let hist = s
|
||||
.ext
|
||||
.get_or_insert_with::<ConnHistory>(ConnHistory::default)
|
||||
.0
|
||||
.entry(ip)
|
||||
.or_default();
|
||||
hist.retain(|&t| n.saturating_sub(t) < secs);
|
||||
hist.push(n);
|
||||
hist.len() as u32 > max
|
||||
}
|
||||
|
||||
/// Prunes stale per-IP bookkeeping on the tick.
|
||||
pub struct ConnFlood;
|
||||
impl Module for ConnFlood {
|
||||
fn name(&self) -> &'static str {
|
||||
"connflood"
|
||||
}
|
||||
fn on_tick(&mut self, s: &mut Server) {
|
||||
let Some((_, secs)) = cfg(s) else {
|
||||
return;
|
||||
};
|
||||
let n = now();
|
||||
if let Some(h) = s.ext.get_mut::<ConnHistory>() {
|
||||
h.0.retain(|_, times| {
|
||||
times.retain(|&t| n.saturating_sub(t) < secs);
|
||||
!times.is_empty()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/modules/hidewhois.rs
Normal file
26
src/modules/hidewhois.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! hidewhois — InspIRCd `m_hidewhois`. Hides sensitive WHOIS lines (server, idle,
|
||||
//! secure, …) from ordinary users. Opers and the user themselves are exempt when
|
||||
//! the matching config toggle is on. All config-driven; nothing lives on `Server`.
|
||||
|
||||
use crate::server::Server;
|
||||
use crate::Uid;
|
||||
|
||||
/// Whether sensitive WHOIS lines should be hidden for this (viewer, target) pair.
|
||||
pub fn hide(s: &Server, viewer: Uid, target: Uid, viewer_oper: bool) -> bool {
|
||||
if !s.conf_bool("hidewhois", false) {
|
||||
return false;
|
||||
}
|
||||
let selfview = s.conf_bool("hidewhois_selfview", true);
|
||||
let opers = s.conf_bool("hidewhois_opers", true);
|
||||
!(viewer == target && selfview) && !(viewer_oper && opers)
|
||||
}
|
||||
|
||||
pub fn hide_server(s: &Server) -> bool {
|
||||
s.conf_bool("hidewhois_hide_server", true)
|
||||
}
|
||||
pub fn hide_idle(s: &Server) -> bool {
|
||||
s.conf_bool("hidewhois_hide_idle", true)
|
||||
}
|
||||
pub fn hide_secure(s: &Server) -> bool {
|
||||
s.conf_bool("hidewhois_hide_secure", true)
|
||||
}
|
||||
|
|
@ -6,15 +6,20 @@
|
|||
pub mod antimixedutf8;
|
||||
pub mod chathistory;
|
||||
pub mod cloak;
|
||||
pub mod connflood;
|
||||
pub mod dnsbl;
|
||||
pub mod filter;
|
||||
pub mod flood;
|
||||
pub mod hidewhois;
|
||||
pub mod markread;
|
||||
pub mod metadata;
|
||||
pub mod multiline;
|
||||
pub mod network_icon;
|
||||
pub mod profilelink;
|
||||
pub mod reputation;
|
||||
pub mod securitygroups;
|
||||
pub mod snoop;
|
||||
pub mod whoisport;
|
||||
|
||||
use crate::command::Command;
|
||||
use crate::module::Module;
|
||||
|
|
@ -31,6 +36,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
|
|||
Box::new(markread::MarkRead),
|
||||
Box::new(multiline::Multiline),
|
||||
Box::new(reputation::ReputationMod::default()),
|
||||
Box::new(connflood::ConnFlood),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
13
src/modules/network_icon.rs
Normal file
13
src/modules/network_icon.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//! ircv3_network_icon — InspIRCd `m_ircv3_network_icon`. Advertises a network icon
|
||||
//! via the `draft/ICON` ISUPPORT token from `network_icon = <url>`. Config-driven;
|
||||
//! nothing lives on `Server`.
|
||||
|
||||
use crate::server::Server;
|
||||
|
||||
/// The `ICON=<url>` ISUPPORT token, or `None` when unconfigured.
|
||||
pub fn isupport(s: &Server) -> Option<String> {
|
||||
match s.conf("network_icon") {
|
||||
Some(url) if !url.is_empty() => Some(format!("ICON={url}")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
17
src/modules/profilelink.rs
Normal file
17
src/modules/profilelink.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//! profileLink — InspIRCd `m_profileLink`. Adds a profile URL to WHOIS for
|
||||
//! logged-in users from `profilelink_baseurl = <url>`. Config-driven; nothing
|
||||
//! lives on `Server`.
|
||||
|
||||
use crate::server::Server;
|
||||
|
||||
/// The WHOIS profile line for `account`, or `None` when unconfigured.
|
||||
pub fn line(s: &Server, account: &Option<String>) -> Option<String> {
|
||||
let base = s.conf("profilelink_baseurl")?;
|
||||
if base.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(match account {
|
||||
Some(acct) => format!("Profil: {base}{acct}"),
|
||||
None => "Profile: The user is not logged in or the account is not registered.".to_string(),
|
||||
})
|
||||
}
|
||||
|
|
@ -49,10 +49,56 @@ fn mask_ip(ip: IpAddr, v4: u8, v6: u8) -> IpAddr {
|
|||
}
|
||||
}
|
||||
|
||||
// --- config, read straight from the config file (no fields on Server) ----------
|
||||
fn v4prefix(s: &Server) -> u8 {
|
||||
s.conf_num::<u8>("reputation_ipv4prefix", 32).clamp(1, 32)
|
||||
}
|
||||
fn v6prefix(s: &Server) -> u8 {
|
||||
s.conf_num::<u8>("reputation_ipv6prefix", 64).clamp(1, 128)
|
||||
}
|
||||
fn scorecap(s: &Server) -> u32 {
|
||||
s.conf_num("reputation_scorecap", 10000)
|
||||
}
|
||||
fn minchan(s: &Server) -> usize {
|
||||
s.conf_num("reputation_minchanmembers", 3)
|
||||
}
|
||||
fn dur(s: &Server, key: &str, def: u64) -> u64 {
|
||||
s.conf(key)
|
||||
.and_then(crate::xline::parse_duration)
|
||||
.filter(|&d| d > 0)
|
||||
.unwrap_or(def)
|
||||
}
|
||||
fn expire_rules(s: &Server) -> Vec<(i32, u64)> {
|
||||
let rules: Vec<(i32, u64)> = s
|
||||
.conf_all("reputationexpire")
|
||||
.iter()
|
||||
.filter_map(|line| {
|
||||
let mut it = line.split_whitespace();
|
||||
let sc = it.next()?;
|
||||
let age = it.next()?;
|
||||
let score = if sc == "*" { -1 } else { sc.parse().ok()? };
|
||||
let age = crate::xline::parse_duration(age).filter(|&a| a > 0)?;
|
||||
Some((score, age))
|
||||
})
|
||||
.collect();
|
||||
if rules.is_empty() {
|
||||
// Unreal defaults: score<=2 after 1h, <=6 after 7d, <=12 after 30d, any after 90d
|
||||
vec![(2, 3600), (6, 604800), (12, 2592000), (-1, 7776000)]
|
||||
} else {
|
||||
rules
|
||||
}
|
||||
}
|
||||
fn db_path(s: &Server) -> String {
|
||||
match s.conf("reputation_database") {
|
||||
Some(p) if !p.is_empty() => p.to_string(),
|
||||
_ => format!("{}.reputation", s.conf_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// The masked key for `uid`'s address.
|
||||
fn key_of(s: &Server, uid: Uid) -> Option<IpAddr> {
|
||||
let ip = s.users.get(&uid).map(|u| u.addr.ip())?;
|
||||
Some(mask_ip(ip, s.rep_ipv4prefix, s.rep_ipv6prefix))
|
||||
Some(mask_ip(ip, v4prefix(s), v6prefix(s)))
|
||||
}
|
||||
|
||||
/// Whether `uid` is in at least one channel with `min` or more members (the
|
||||
|
|
@ -90,15 +136,15 @@ impl Module for ReputationMod {
|
|||
self.since_bump += t;
|
||||
self.since_expire += t;
|
||||
self.since_save += t;
|
||||
if self.since_bump >= s.rep_bump_secs {
|
||||
if self.since_bump >= dur(s, "reputation_bumpinterval", 300) {
|
||||
self.since_bump = 0;
|
||||
bump_scores(s);
|
||||
}
|
||||
if self.since_expire >= s.rep_expire_secs {
|
||||
if self.since_expire >= dur(s, "reputation_expireinterval", 605) {
|
||||
self.since_expire = 0;
|
||||
expire_old(s);
|
||||
}
|
||||
if self.since_save >= s.rep_save_secs {
|
||||
if self.since_save >= dur(s, "reputation_saveinterval", 902) {
|
||||
self.since_save = 0;
|
||||
save(s);
|
||||
}
|
||||
|
|
@ -109,9 +155,9 @@ impl Module for ReputationMod {
|
|||
/// refresh their last_seen so active addresses don't decay.
|
||||
fn bump_scores(s: &mut Server) {
|
||||
let n = now();
|
||||
let cap = s.rep_scorecap;
|
||||
let min = s.rep_minchanmembers;
|
||||
let (v4, v6) = (s.rep_ipv4prefix, s.rep_ipv6prefix);
|
||||
let cap = scorecap(s);
|
||||
let min = minchan(s);
|
||||
let (v4, v6) = (v4prefix(s), v6prefix(s));
|
||||
let bumps: Vec<(IpAddr, u32)> = s
|
||||
.users
|
||||
.values()
|
||||
|
|
@ -135,7 +181,7 @@ fn bump_scores(s: &mut Server) {
|
|||
/// Drop entries that have aged out under any matching `reputationexpire` rule.
|
||||
fn expire_old(s: &mut Server) {
|
||||
let n = now();
|
||||
let rules = s.rep_expire_rules.clone();
|
||||
let rules = expire_rules(s);
|
||||
if let Some(store) = s.ext.get_mut::<Reputation>() {
|
||||
store.0.retain(|_, e| {
|
||||
let expired = rules.iter().any(|&(score, age)| {
|
||||
|
|
@ -179,7 +225,7 @@ pub fn score_ban_match(s: &Server, uid: Uid, spec: &str) -> bool {
|
|||
|
||||
/// Whether the WHOIS `source` may see `target`'s reputation, per the `whois` mode.
|
||||
pub fn whois_visible(s: &Server, source: Uid, target: Uid) -> bool {
|
||||
match s.rep_whois.as_str() {
|
||||
match s.conf("reputation_whois").unwrap_or("all") {
|
||||
"none" => false,
|
||||
"self" => source == target,
|
||||
"opers" => source == target || s.is_oper(source),
|
||||
|
|
@ -228,7 +274,7 @@ impl Command for ReputationCmd {
|
|||
.map(|u| u.nick.clone())
|
||||
.unwrap_or_default();
|
||||
if let Some(val) = params.get(1).and_then(|v| v.parse::<u32>().ok()) {
|
||||
let (cap, n) = (s.rep_scorecap, now());
|
||||
let (cap, n) = (scorecap(s), now());
|
||||
let store = s.ext.get_or_insert_with::<Reputation>(Reputation::default);
|
||||
let e = store.0.entry(k).or_default();
|
||||
e.score = val.min(cap);
|
||||
|
|
@ -255,14 +301,6 @@ impl Command for ReputationCmd {
|
|||
}
|
||||
}
|
||||
|
||||
fn db_path(s: &Server) -> String {
|
||||
if s.rep_database.is_empty() {
|
||||
format!("{}.reputation", s.conf_path)
|
||||
} else {
|
||||
s.rep_database.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist reputation (masked-ip score last_seen per line) so it survives a restart.
|
||||
pub fn save(s: &Server) {
|
||||
let mut out = String::new();
|
||||
|
|
|
|||
|
|
@ -6,11 +6,80 @@
|
|||
|
||||
use crate::channels::glob_match;
|
||||
use crate::command::{CmdResult, Command};
|
||||
use crate::config::{SecGroup, Tri};
|
||||
use crate::numeric::ERR_NOSUCHNICK;
|
||||
use crate::server::Server;
|
||||
use crate::Uid;
|
||||
|
||||
/// Tri-state for a criterion: don't-care / must-be / must-not-be.
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
enum Tri {
|
||||
#[default]
|
||||
Ignore,
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
/// A UnrealIRCd-style security group — all criteria AND-ed.
|
||||
#[derive(Clone, Default)]
|
||||
struct SecGroup {
|
||||
name: String,
|
||||
public: bool,
|
||||
masks: Vec<String>,
|
||||
exclude_masks: Vec<String>,
|
||||
tls: Tri,
|
||||
account: Tri,
|
||||
oper: Tri,
|
||||
bot: Tri,
|
||||
webirc: Tri,
|
||||
score_min: Option<u32>,
|
||||
score_max: Option<u32>,
|
||||
}
|
||||
|
||||
/// Parse the `securitygroup = <name> [criteria…]` config lines into groups.
|
||||
fn parse_groups(s: &Server) -> Vec<SecGroup> {
|
||||
let mut out = Vec::new();
|
||||
for line in s
|
||||
.conf_all("securitygroup")
|
||||
.iter()
|
||||
.chain(s.conf_all("secgroup"))
|
||||
{
|
||||
let mut it = line.split_whitespace();
|
||||
let Some(name) = it.next() else { continue };
|
||||
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(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.push(g);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Does `uid`'s identity match `mask` (glob against nick!user@{display,real,ip})?
|
||||
fn mask_matches(s: &Server, uid: Uid, mask: &str) -> bool {
|
||||
let Some(u) = s.users.get(&uid) else {
|
||||
|
|
@ -64,17 +133,17 @@ fn matches(s: &Server, uid: Uid, g: &SecGroup) -> bool {
|
|||
|
||||
/// Whether `uid` is a member of the named security group (case-insensitive).
|
||||
pub fn in_group(s: &Server, uid: Uid, name: &str) -> bool {
|
||||
s.sec_groups
|
||||
parse_groups(s)
|
||||
.iter()
|
||||
.any(|g| g.name.eq_ignore_ascii_case(name) && matches(s, uid, g))
|
||||
}
|
||||
|
||||
/// The names of the groups `uid` is in (only public ones unless `include_private`).
|
||||
pub fn user_groups(s: &Server, uid: Uid, include_private: bool) -> Vec<String> {
|
||||
s.sec_groups
|
||||
.iter()
|
||||
parse_groups(s)
|
||||
.into_iter()
|
||||
.filter(|g| (include_private || g.public) && matches(s, uid, g))
|
||||
.map(|g| g.name.clone())
|
||||
.map(|g| g.name)
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
21
src/modules/whoisport.rs
Normal file
21
src/modules/whoisport.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
//! whoisport — InspIRCd `m_whoisport`. Shows an IRC operator, in WHOIS, the
|
||||
//! listener port the target connected to. Config-free (derives the port from the
|
||||
//! `bind` / `bind_tls` listeners); nothing lives on `Server`.
|
||||
|
||||
use crate::server::Server;
|
||||
use crate::Uid;
|
||||
|
||||
fn port_of(addr: &str) -> u16 {
|
||||
addr.rsplit(':')
|
||||
.next()
|
||||
.and_then(|p| p.parse().ok())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The `is using port N` WHOIS line for opers, or `None` if the port is unknown.
|
||||
pub fn line(s: &Server, target: Uid) -> Option<String> {
|
||||
let secure = s.users.get(&target).map(|u| u.secure).unwrap_or(false);
|
||||
let bind = if secure { "bind_tls" } else { "bind" };
|
||||
let port = s.conf(bind).map(port_of).unwrap_or(0);
|
||||
(port != 0).then(|| format!("is using port {port}"))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue