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
|
|
@ -700,7 +700,9 @@ impl Server {
|
||||||
u.channels.insert(key.clone());
|
u.channels.insert(key.clone());
|
||||||
}
|
}
|
||||||
// chancreate: snotice when a brand-new channel comes into being
|
// chancreate: snotice when a brand-new channel comes into being
|
||||||
if is_new && self.announce_chan {
|
if is_new
|
||||||
|
&& (self.conf_bool("chancreate", false) || self.conf_bool("announce_channels", false))
|
||||||
|
{
|
||||||
let who = self
|
let who = self
|
||||||
.users
|
.users
|
||||||
.get(&uid)
|
.get(&uid)
|
||||||
|
|
|
||||||
255
src/config.rs
255
src/config.rs
|
|
@ -8,40 +8,16 @@
|
||||||
//! oper = god secret
|
//! oper = god secret
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Parse a boolean config value (`yes`/`no`/`true`/`false`/`on`/`off`/`1`/`0`).
|
/// 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!(
|
!matches!(
|
||||||
v.to_ascii_lowercase().as_str(),
|
v.to_ascii_lowercase().as_str(),
|
||||||
"off" | "no" | "false" | "0"
|
"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
|
/// A server-link block: how to authenticate a peer named `name` (and, if
|
||||||
/// `autoconnect`, where to dial it). Passwords are the shared link secret.
|
/// `autoconnect`, where to dial it). Passwords are the shared link secret.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -110,37 +86,9 @@ pub struct Config {
|
||||||
pub dnsbl_reason: String, // ban reason for a DNSBL hit
|
pub dnsbl_reason: String, // ban reason for a DNSBL hit
|
||||||
pub sasl_server: String, // linked services server that handles SASL ("" = none)
|
pub sasl_server: String, // linked services server that handles SASL ("" = none)
|
||||||
pub webirc: Vec<(String, String, String)>, // web gateways: (password, name, ip-mask)
|
pub webirc: Vec<(String, String, String)>, // web gateways: (password, name, ip-mask)
|
||||||
pub opermotd: Vec<String>, // OPERMOTD text, one line per entry
|
/// Every `key = value` line, captured raw so modules read their own settings
|
||||||
pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host)
|
/// via `Server::conf*` — no per-module field bloats this struct or `Server`.
|
||||||
pub aliases: Vec<(String, String)>, // command aliases: (name, target-nick)
|
pub raw: HashMap<String, Vec<String>>,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
|
@ -169,37 +117,7 @@ impl Default for Config {
|
||||||
dnsbl_reason: "Your host is listed in a DNS blocklist".to_string(),
|
dnsbl_reason: "Your host is listed in a DNS blocklist".to_string(),
|
||||||
sasl_server: String::new(),
|
sasl_server: String::new(),
|
||||||
webirc: Vec::new(),
|
webirc: Vec::new(),
|
||||||
opermotd: Vec::new(),
|
raw: HashMap::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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,6 +162,9 @@ impl Config {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let (k, v) = (k.trim(), v.trim());
|
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 {
|
match k {
|
||||||
"servername" | "server" => c.servername = v.to_string(),
|
"servername" | "server" => c.servername = v.to_string(),
|
||||||
"network" => c.network = v.to_string(),
|
"network" => c.network = v.to_string(),
|
||||||
|
|
@ -344,162 +265,6 @@ impl Config {
|
||||||
c.webirc.push((pass.to_string(), gw, mask));
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,7 @@ impl Command for Whois {
|
||||||
let asker_oper = s.is_oper(uid);
|
let asker_oper = s.is_oper(uid);
|
||||||
let is_self = tuid == uid;
|
let is_self = tuid == uid;
|
||||||
// hidewhois: hide sensitive lines from ordinary users (opers/self exempt per config)
|
// hidewhois: hide sensitive lines from ordinary users (opers/self exempt per config)
|
||||||
let hide =
|
let hide = crate::modules::hidewhois::hide(s, uid, tuid, asker_oper);
|
||||||
s.hidewhois && !(is_self && s.hidewhois_selfview) && !(asker_oper && s.hidewhois_opers);
|
|
||||||
let keys: Vec<String> = s.users[&tuid].channels.iter().cloned().collect();
|
let keys: Vec<String> = s.users[&tuid].channels.iter().cloned().collect();
|
||||||
let (
|
let (
|
||||||
nick,
|
nick,
|
||||||
|
|
@ -206,7 +205,7 @@ impl Command for Whois {
|
||||||
if bot {
|
if bot {
|
||||||
s.numeric(uid, RPL_WHOISBOT, &format!("{nick} :is a bot"));
|
s.numeric(uid, RPL_WHOISBOT, &format!("{nick} :is a bot"));
|
||||||
}
|
}
|
||||||
if !(hide && s.hidewhois_server) {
|
if !(hide && crate::modules::hidewhois::hide_server(s)) {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
RPL_WHOISSERVER,
|
RPL_WHOISSERVER,
|
||||||
|
|
@ -251,25 +250,13 @@ impl Command for Whois {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// profileLink: a profile URL for logged-in users (when configured)
|
// profileLink: a profile URL for logged-in users (when configured)
|
||||||
if !s.profilelink_baseurl.is_empty() {
|
if let Some(line) = crate::modules::profilelink::line(s, &account) {
|
||||||
match &account {
|
s.numeric(uid, RPL_WHOISSPECIAL, &format!(":{line}"));
|
||||||
Some(acct) => s.numeric(
|
|
||||||
uid,
|
|
||||||
RPL_WHOISSPECIAL,
|
|
||||||
&format!(":Profil: {}{acct}", s.profilelink_baseurl),
|
|
||||||
),
|
|
||||||
None => s.numeric(
|
|
||||||
uid,
|
|
||||||
RPL_WHOISSPECIAL,
|
|
||||||
":Profile: The user is not logged in or the account is not registered.",
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
// whoisport: the listener port — opers only
|
||||||
// whoisport: the listener port (+ TLS/plain) — opers only
|
|
||||||
if asker_oper {
|
if asker_oper {
|
||||||
let port = if secure { s.tls_port } else { s.plain_port };
|
if let Some(line) = crate::modules::whoisport::line(s, tuid) {
|
||||||
if port != 0 {
|
s.numeric(uid, RPL_WHOISSPECIAL, &format!(":{line}"));
|
||||||
s.numeric(uid, RPL_WHOISSPECIAL, &format!(":is using port {port}"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// opers can see through the cloak to the real host/ip
|
// opers can see through the cloak to the real host/ip
|
||||||
|
|
@ -289,7 +276,7 @@ impl Command for Whois {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// sslinfo: advertise a secure (TLS) connection
|
// sslinfo: advertise a secure (TLS) connection
|
||||||
if secure && !(hide && s.hidewhois_secure) {
|
if secure && !(hide && crate::modules::hidewhois::hide_secure(s)) {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
RPL_WHOISSECURE,
|
RPL_WHOISSECURE,
|
||||||
|
|
@ -307,7 +294,7 @@ impl Command for Whois {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 317: idle time + signon time (hidewhois may suppress it)
|
// 317: idle time + signon time (hidewhois may suppress it)
|
||||||
if !(hide && s.hidewhois_idle) {
|
if !(hide && crate::modules::hidewhois::hide_idle(s)) {
|
||||||
let idle = crate::server::now().saturating_sub(last_active);
|
let idle = crate::server::now().saturating_sub(last_active);
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,8 @@ impl Command for OperMotd {
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
}
|
}
|
||||||
let nick = oper_nick(s, uid);
|
let nick = oper_nick(s, uid);
|
||||||
if s.opermotd.is_empty() {
|
let motd = s.conf_all("opermotd").to_vec();
|
||||||
|
if motd.is_empty() {
|
||||||
s.send(
|
s.send(
|
||||||
uid,
|
uid,
|
||||||
format!(":{} NOTICE {nick} :No OPERMOTD is set", s.name),
|
format!(":{} NOTICE {nick} :No OPERMOTD is set", s.name),
|
||||||
|
|
@ -82,7 +83,7 @@ impl Command for OperMotd {
|
||||||
s.name
|
s.name
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
for line in s.opermotd.clone() {
|
for line in motd {
|
||||||
s.send(uid, format!(":{} NOTICE {nick} :- {line}", s.name));
|
s.send(uid, format!(":{} NOTICE {nick} :- {line}", s.name));
|
||||||
}
|
}
|
||||||
s.send(
|
s.send(
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,14 @@ impl Command for Vhost {
|
||||||
}
|
}
|
||||||
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||||
let (user, pass) = (¶ms[0], ¶ms[1]);
|
let (user, pass) = (¶ms[0], ¶ms[1]);
|
||||||
let host = s
|
// vhost blocks live in the config: `vhost = <user> <pass> <host>`
|
||||||
.vhosts
|
let host = s.conf_all("vhost").iter().find_map(|line| {
|
||||||
.iter()
|
let mut it = line.split_whitespace();
|
||||||
.find(|(u, p, _)| u == user && p == pass)
|
match (it.next(), it.next(), it.next()) {
|
||||||
.map(|(_, _, h)| h.clone());
|
(Some(u), Some(p), Some(h)) if u == user && p == pass => Some(h.to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
});
|
||||||
let nick = s
|
let nick = s
|
||||||
.users
|
.users
|
||||||
.get(&uid)
|
.get(&uid)
|
||||||
|
|
|
||||||
142
src/http.rs
Normal file
142
src/http.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
//! Minimal blocking HTTP/HTTPS client — `std::net::TcpStream` + openssl for TLS.
|
||||||
|
//! No new crate, no `unsafe`. Modules that talk to external APIs (account
|
||||||
|
//! registration, captcha verification, …) use this from a **worker thread** and
|
||||||
|
//! deliver the result back to the core as an [`crate::ircd::Event`], exactly like
|
||||||
|
//! the DNS/DNSBL lookups — so a slow or hung endpoint never blocks the main loop.
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpStream;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||||
|
|
||||||
|
/// POST `body` to `url` with `content_type` and extra `headers`. Blocking.
|
||||||
|
/// Returns `(status_code, response_body)` or an error string.
|
||||||
|
pub fn post(
|
||||||
|
url: &str,
|
||||||
|
content_type: &str,
|
||||||
|
body: &str,
|
||||||
|
headers: &[(String, String)],
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<(u16, String), String> {
|
||||||
|
let (scheme, rest) = url.split_once("://").ok_or("bad url (no scheme)")?;
|
||||||
|
let (hostport, path) = match rest.split_once('/') {
|
||||||
|
Some((hp, p)) => (hp, format!("/{p}")),
|
||||||
|
None => (rest, "/".to_string()),
|
||||||
|
};
|
||||||
|
let https = scheme.eq_ignore_ascii_case("https");
|
||||||
|
let (host, port): (&str, u16) = match hostport.rsplit_once(':') {
|
||||||
|
Some((h, p)) => (h, p.parse().unwrap_or(if https { 443 } else { 80 })),
|
||||||
|
None => (hostport, if https { 443 } else { 80 }),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut req = format!(
|
||||||
|
"POST {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: echoIRCd\r\nAccept: */*\r\n\
|
||||||
|
Content-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n",
|
||||||
|
body.len()
|
||||||
|
);
|
||||||
|
for (k, v) in headers {
|
||||||
|
req.push_str(&format!("{k}: {v}\r\n"));
|
||||||
|
}
|
||||||
|
req.push_str("\r\n");
|
||||||
|
req.push_str(body);
|
||||||
|
|
||||||
|
let stream = TcpStream::connect((host, port)).map_err(|e| e.to_string())?;
|
||||||
|
stream.set_read_timeout(Some(timeout)).ok();
|
||||||
|
stream.set_write_timeout(Some(timeout)).ok();
|
||||||
|
|
||||||
|
let raw = if https {
|
||||||
|
let mut b = SslConnector::builder(SslMethod::tls()).map_err(|e| e.to_string())?;
|
||||||
|
b.set_verify(SslVerifyMode::NONE); // APIs are usually behind a trusted reverse proxy
|
||||||
|
let connector = b.build();
|
||||||
|
let mut tls = connector.connect(host, stream).map_err(|e| e.to_string())?;
|
||||||
|
tls.write_all(req.as_bytes()).map_err(|e| e.to_string())?;
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let _ = tls.read_to_end(&mut buf); // close => EOF (Connection: close)
|
||||||
|
buf
|
||||||
|
} else {
|
||||||
|
let mut s = stream;
|
||||||
|
s.write_all(req.as_bytes()).map_err(|e| e.to_string())?;
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let _ = s.read_to_end(&mut buf);
|
||||||
|
buf
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = String::from_utf8_lossy(&raw).into_owned();
|
||||||
|
let (head, rbody) = resp.split_once("\r\n\r\n").unwrap_or((resp.as_str(), ""));
|
||||||
|
let status = head
|
||||||
|
.lines()
|
||||||
|
.next()
|
||||||
|
.and_then(|l| l.split_whitespace().nth(1))
|
||||||
|
.and_then(|c| c.parse::<u16>().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let chunked = head
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.contains("transfer-encoding: chunked");
|
||||||
|
let out = if chunked {
|
||||||
|
dechunk(rbody)
|
||||||
|
} else {
|
||||||
|
rbody.to_string()
|
||||||
|
};
|
||||||
|
Ok((status, out))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode an HTTP/1.1 chunked body (best effort).
|
||||||
|
fn dechunk(body: &str) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut rest = body;
|
||||||
|
while let Some((size_line, after)) = rest.split_once("\r\n") {
|
||||||
|
let size = usize::from_str_radix(size_line.trim().split(';').next().unwrap_or("0"), 16)
|
||||||
|
.unwrap_or(0);
|
||||||
|
if size == 0 || after.len() < size {
|
||||||
|
out.push_str(&after[..after.len().min(size)]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.push_str(&after[..size]);
|
||||||
|
rest = after[size..].strip_prefix("\r\n").unwrap_or(&after[size..]);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// application/x-www-form-urlencoded escape of a single value.
|
||||||
|
pub fn urlencode(s: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(s.len());
|
||||||
|
for b in s.bytes() {
|
||||||
|
match b {
|
||||||
|
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||||
|
out.push(b as char)
|
||||||
|
}
|
||||||
|
b' ' => out.push('+'),
|
||||||
|
_ => out.push_str(&format!("%{b:02X}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pull a top-level `"key": value` out of a flat JSON object — dependency-free,
|
||||||
|
/// good enough for the small, known API responses these modules parse.
|
||||||
|
pub fn json_str(body: &str, key: &str) -> Option<String> {
|
||||||
|
let pat = format!("\"{key}\"");
|
||||||
|
let start = body.find(&pat)? + pat.len();
|
||||||
|
let after_colon = body[start..].find(':')? + start + 1;
|
||||||
|
let rest = body[after_colon..].trim_start();
|
||||||
|
if let Some(r) = rest.strip_prefix('"') {
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut chars = r.chars();
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'\\' => {
|
||||||
|
if let Some(n) = chars.next() {
|
||||||
|
out.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'"' => return Some(out),
|
||||||
|
_ => out.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let end = rest.find([',', '}', '\n']).unwrap_or(rest.len());
|
||||||
|
Some(rest[..end].trim().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/ircd.rs
18
src/ircd.rs
|
|
@ -230,14 +230,15 @@ impl Ircd {
|
||||||
|
|
||||||
let Some(handler) = self.commands.get(cmd) else {
|
let Some(handler) = self.commands.get(cmd) else {
|
||||||
if registered {
|
if registered {
|
||||||
// command aliases (m_alias): `/NS help` -> PRIVMSG NickServ :help
|
// command aliases (m_alias): config `alias = <CMD> <target-nick>`
|
||||||
if let Some(target) = self
|
// e.g. `alias = NS NickServ` makes `/NS help` -> PRIVMSG NickServ :help
|
||||||
.server
|
if let Some(target) = self.server.conf_all("alias").iter().find_map(|line| {
|
||||||
.aliases
|
let mut it = line.split_whitespace();
|
||||||
.iter()
|
match (it.next(), it.next()) {
|
||||||
.find(|(n, _)| n == cmd)
|
(Some(n), Some(t)) if n.eq_ignore_ascii_case(cmd) => Some(t.to_string()),
|
||||||
.map(|(_, t)| t.clone())
|
_ => None,
|
||||||
{
|
}
|
||||||
|
}) {
|
||||||
if !msg.params.is_empty() {
|
if !msg.params.is_empty() {
|
||||||
let text = msg.params.join(" ");
|
let text = msg.params.join(" ");
|
||||||
crate::coremods::core_message::deliver(
|
crate::coremods::core_message::deliver(
|
||||||
|
|
@ -377,7 +378,6 @@ impl Ircd {
|
||||||
self.server.ping_links(); // keepalive on every server link
|
self.server.ping_links(); // keepalive on every server link
|
||||||
self.server.purge_xlines(); // drop expired server bans
|
self.server.purge_xlines(); // drop expired server bans
|
||||||
self.server.purge_tbans(); // lift expired timed channel bans (TBAN)
|
self.server.purge_tbans(); // lift expired timed channel bans (TBAN)
|
||||||
self.server.prune_conn_history(); // connflood bookkeeping
|
|
||||||
for m in &mut self.modules {
|
for m in &mut self.modules {
|
||||||
m.on_tick(&mut self.server); // timer-driven modules (e.g. reputation)
|
m.on_tick(&mut self.server); // timer-driven modules (e.g. reputation)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ pub mod command;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod coremods;
|
pub mod coremods;
|
||||||
pub mod extensible;
|
pub mod extensible;
|
||||||
|
pub mod http;
|
||||||
pub mod ircd;
|
pub mod ircd;
|
||||||
pub mod link;
|
pub mod link;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
|
|
||||||
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 antimixedutf8;
|
||||||
pub mod chathistory;
|
pub mod chathistory;
|
||||||
pub mod cloak;
|
pub mod cloak;
|
||||||
|
pub mod connflood;
|
||||||
pub mod dnsbl;
|
pub mod dnsbl;
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
pub mod flood;
|
pub mod flood;
|
||||||
|
pub mod hidewhois;
|
||||||
pub mod markread;
|
pub mod markread;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod multiline;
|
pub mod multiline;
|
||||||
|
pub mod network_icon;
|
||||||
|
pub mod profilelink;
|
||||||
pub mod reputation;
|
pub mod reputation;
|
||||||
pub mod securitygroups;
|
pub mod securitygroups;
|
||||||
pub mod snoop;
|
pub mod snoop;
|
||||||
|
pub mod whoisport;
|
||||||
|
|
||||||
use crate::command::Command;
|
use crate::command::Command;
|
||||||
use crate::module::Module;
|
use crate::module::Module;
|
||||||
|
|
@ -31,6 +36,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
|
||||||
Box::new(markread::MarkRead),
|
Box::new(markread::MarkRead),
|
||||||
Box::new(multiline::Multiline),
|
Box::new(multiline::Multiline),
|
||||||
Box::new(reputation::ReputationMod::default()),
|
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.
|
/// The masked key for `uid`'s address.
|
||||||
fn key_of(s: &Server, uid: Uid) -> Option<IpAddr> {
|
fn key_of(s: &Server, uid: Uid) -> Option<IpAddr> {
|
||||||
let ip = s.users.get(&uid).map(|u| u.addr.ip())?;
|
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
|
/// 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_bump += t;
|
||||||
self.since_expire += t;
|
self.since_expire += t;
|
||||||
self.since_save += 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;
|
self.since_bump = 0;
|
||||||
bump_scores(s);
|
bump_scores(s);
|
||||||
}
|
}
|
||||||
if self.since_expire >= s.rep_expire_secs {
|
if self.since_expire >= dur(s, "reputation_expireinterval", 605) {
|
||||||
self.since_expire = 0;
|
self.since_expire = 0;
|
||||||
expire_old(s);
|
expire_old(s);
|
||||||
}
|
}
|
||||||
if self.since_save >= s.rep_save_secs {
|
if self.since_save >= dur(s, "reputation_saveinterval", 902) {
|
||||||
self.since_save = 0;
|
self.since_save = 0;
|
||||||
save(s);
|
save(s);
|
||||||
}
|
}
|
||||||
|
|
@ -109,9 +155,9 @@ impl Module for ReputationMod {
|
||||||
/// refresh their last_seen so active addresses don't decay.
|
/// refresh their last_seen so active addresses don't decay.
|
||||||
fn bump_scores(s: &mut Server) {
|
fn bump_scores(s: &mut Server) {
|
||||||
let n = now();
|
let n = now();
|
||||||
let cap = s.rep_scorecap;
|
let cap = scorecap(s);
|
||||||
let min = s.rep_minchanmembers;
|
let min = minchan(s);
|
||||||
let (v4, v6) = (s.rep_ipv4prefix, s.rep_ipv6prefix);
|
let (v4, v6) = (v4prefix(s), v6prefix(s));
|
||||||
let bumps: Vec<(IpAddr, u32)> = s
|
let bumps: Vec<(IpAddr, u32)> = s
|
||||||
.users
|
.users
|
||||||
.values()
|
.values()
|
||||||
|
|
@ -135,7 +181,7 @@ fn bump_scores(s: &mut Server) {
|
||||||
/// Drop entries that have aged out under any matching `reputationexpire` rule.
|
/// Drop entries that have aged out under any matching `reputationexpire` rule.
|
||||||
fn expire_old(s: &mut Server) {
|
fn expire_old(s: &mut Server) {
|
||||||
let n = now();
|
let n = now();
|
||||||
let rules = s.rep_expire_rules.clone();
|
let rules = expire_rules(s);
|
||||||
if let Some(store) = s.ext.get_mut::<Reputation>() {
|
if let Some(store) = s.ext.get_mut::<Reputation>() {
|
||||||
store.0.retain(|_, e| {
|
store.0.retain(|_, e| {
|
||||||
let expired = rules.iter().any(|&(score, age)| {
|
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.
|
/// Whether the WHOIS `source` may see `target`'s reputation, per the `whois` mode.
|
||||||
pub fn whois_visible(s: &Server, source: Uid, target: Uid) -> bool {
|
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,
|
"none" => false,
|
||||||
"self" => source == target,
|
"self" => source == target,
|
||||||
"opers" => source == target || s.is_oper(source),
|
"opers" => source == target || s.is_oper(source),
|
||||||
|
|
@ -228,7 +274,7 @@ impl Command for ReputationCmd {
|
||||||
.map(|u| u.nick.clone())
|
.map(|u| u.nick.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if let Some(val) = params.get(1).and_then(|v| v.parse::<u32>().ok()) {
|
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 store = s.ext.get_or_insert_with::<Reputation>(Reputation::default);
|
||||||
let e = store.0.entry(k).or_default();
|
let e = store.0.entry(k).or_default();
|
||||||
e.score = val.min(cap);
|
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.
|
/// Persist reputation (masked-ip score last_seen per line) so it survives a restart.
|
||||||
pub fn save(s: &Server) {
|
pub fn save(s: &Server) {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,80 @@
|
||||||
|
|
||||||
use crate::channels::glob_match;
|
use crate::channels::glob_match;
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
use crate::config::{SecGroup, Tri};
|
|
||||||
use crate::numeric::ERR_NOSUCHNICK;
|
use crate::numeric::ERR_NOSUCHNICK;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
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})?
|
/// Does `uid`'s identity match `mask` (glob against nick!user@{display,real,ip})?
|
||||||
fn mask_matches(s: &Server, uid: Uid, mask: &str) -> bool {
|
fn mask_matches(s: &Server, uid: Uid, mask: &str) -> bool {
|
||||||
let Some(u) = s.users.get(&uid) else {
|
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).
|
/// Whether `uid` is a member of the named security group (case-insensitive).
|
||||||
pub fn in_group(s: &Server, uid: Uid, name: &str) -> bool {
|
pub fn in_group(s: &Server, uid: Uid, name: &str) -> bool {
|
||||||
s.sec_groups
|
parse_groups(s)
|
||||||
.iter()
|
.iter()
|
||||||
.any(|g| g.name.eq_ignore_ascii_case(name) && matches(s, uid, g))
|
.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`).
|
/// 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> {
|
pub fn user_groups(s: &Server, uid: Uid, include_private: bool) -> Vec<String> {
|
||||||
s.sec_groups
|
parse_groups(s)
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter(|g| (include_private || g.public) && matches(s, uid, g))
|
.filter(|g| (include_private || g.public) && matches(s, uid, g))
|
||||||
.map(|g| g.name.clone())
|
.map(|g| g.name)
|
||||||
.collect()
|
.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}"))
|
||||||
|
}
|
||||||
138
src/server.rs
138
src/server.rs
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::net::{IpAddr, SocketAddr, TcpStream};
|
use std::net::{SocketAddr, TcpStream};
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -35,14 +35,6 @@ pub const PING_AFTER: u64 = 90;
|
||||||
pub const PING_TIMEOUT: u64 = 60;
|
pub const PING_TIMEOUT: u64 = 60;
|
||||||
pub const REG_TIMEOUT: u64 = 60;
|
pub const REG_TIMEOUT: u64 = 60;
|
||||||
|
|
||||||
/// The port from a `host:port` bind string (0 if unparseable) — for whoisport.
|
|
||||||
fn port_of(addr: &str) -> u16 {
|
|
||||||
addr.rsplit(':')
|
|
||||||
.next()
|
|
||||||
.and_then(|p| p.parse().ok())
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn now() -> u64 {
|
pub fn now() -> u64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|
@ -140,40 +132,10 @@ pub struct Server {
|
||||||
pub dnsbl_reason: String, // ban reason on a DNSBL hit
|
pub dnsbl_reason: String, // ban reason on a DNSBL hit
|
||||||
pub sasl_server: String, // services server that handles SASL
|
pub sasl_server: String, // services server that handles SASL
|
||||||
pub webirc: Vec<(String, String, String)>, // web gateways: (password, name, ip-mask)
|
pub webirc: Vec<(String, String, String)>, // web gateways: (password, name, ip-mask)
|
||||||
pub opermotd: Vec<String>, // OPERMOTD text
|
/// Every `key = value` line from the config, so each module reads its own
|
||||||
pub vhosts: Vec<(String, String, String)>, // self-service vhosts: (user, pass, host)
|
/// settings via [`Server::conf`] / [`conf_all`] / [`conf_bool`] / [`conf_num`]
|
||||||
pub aliases: Vec<(String, String)>, // command aliases: (name, target-nick)
|
/// — no per-module field lives on this struct (module-per-file rule).
|
||||||
pub connflood: Option<(u32, u64)>, // (max, secs) connection throttle per IP
|
pub raw_config: HashMap<String, Vec<String>>,
|
||||||
pub conn_history: HashMap<IpAddr, Vec<u64>>, // recent connection times per IP (connflood)
|
|
||||||
pub sec_groups: Vec<crate::config::SecGroup>, // UnrealIRCd-style security groups
|
|
||||||
pub autojoin: Vec<String>, // conn_join: channels joined on connect
|
|
||||||
pub auto_umodes: String, // conn_umodes: umodes set on connect
|
|
||||||
pub conn_banner: Vec<String>, // connbanner: NOTICE lines 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 on channel creation
|
|
||||||
pub rep_database: String, // reputation: db path ("" = <conf>.reputation)
|
|
||||||
pub rep_ipv4prefix: u8, // reputation: IPv4 CIDR prefix for keying
|
|
||||||
pub rep_ipv6prefix: u8, // reputation: IPv6 CIDR prefix for keying
|
|
||||||
pub rep_scorecap: u32, // reputation: max score
|
|
||||||
pub rep_bump_secs: u64, // reputation: seconds between bumps
|
|
||||||
pub rep_expire_secs: u64, // reputation: seconds between expiry runs
|
|
||||||
pub rep_save_secs: u64, // reputation: seconds between saves
|
|
||||||
pub rep_minchanmembers: usize, // reputation: min channel size to bump
|
|
||||||
pub rep_whois: String, // reputation: whois visibility mode
|
|
||||||
pub rep_expire_rules: Vec<(i32, u64)>, // reputation: (score, age) decay rules
|
|
||||||
pub network_icon: String, // ircv3_network_icon: draft/ICON url
|
|
||||||
pub profilelink_baseurl: String, // profileLink: WHOIS profile url base
|
|
||||||
pub hidewhois: bool, // hidewhois: enabled
|
|
||||||
pub hidewhois_opers: bool, // hidewhois: opers exempt
|
|
||||||
pub hidewhois_selfview: bool, // hidewhois: self exempt
|
|
||||||
pub hidewhois_server: bool, // hidewhois: hide 312
|
|
||||||
pub hidewhois_idle: bool, // hidewhois: hide 317
|
|
||||||
pub hidewhois_away: bool, // hidewhois: hide 301
|
|
||||||
pub hidewhois_secure: bool, // hidewhois: hide 671
|
|
||||||
pub plain_port: u16, // whoisport: the plaintext listener port
|
|
||||||
pub tls_port: u16, // whoisport: the TLS listener port (0 = none)
|
|
||||||
// labeled-response: while Some((uid, buf)), that client's own responses are
|
// labeled-response: while Some((uid, buf)), that client's own responses are
|
||||||
// diverted into `buf` instead of the socket, so `on_line` can wrap them with
|
// diverted into `buf` instead of the socket, so `on_line` can wrap them with
|
||||||
// the command's `label` (single tag, BATCH, or ACK). RefCell because the
|
// the command's `label` (single tag, BATCH, or ACK). RefCell because the
|
||||||
|
|
@ -225,45 +187,7 @@ impl Server {
|
||||||
dnsbl_reason: cfg.dnsbl_reason,
|
dnsbl_reason: cfg.dnsbl_reason,
|
||||||
sasl_server: cfg.sasl_server,
|
sasl_server: cfg.sasl_server,
|
||||||
webirc: cfg.webirc,
|
webirc: cfg.webirc,
|
||||||
opermotd: cfg.opermotd,
|
raw_config: cfg.raw,
|
||||||
vhosts: cfg.vhosts,
|
|
||||||
aliases: cfg.aliases,
|
|
||||||
connflood: cfg.connflood,
|
|
||||||
conn_history: HashMap::new(),
|
|
||||||
sec_groups: cfg.sec_groups,
|
|
||||||
autojoin: cfg.autojoin,
|
|
||||||
auto_umodes: cfg.auto_umodes,
|
|
||||||
conn_banner: cfg.conn_banner,
|
|
||||||
oper_autojoin: cfg.oper_autojoin,
|
|
||||||
oper_umodes: cfg.oper_umodes,
|
|
||||||
seenicks: cfg.seenicks,
|
|
||||||
announce_chan: cfg.announce_chan,
|
|
||||||
rep_database: cfg.rep_database,
|
|
||||||
rep_ipv4prefix: cfg.rep_ipv4prefix,
|
|
||||||
rep_ipv6prefix: cfg.rep_ipv6prefix,
|
|
||||||
rep_scorecap: cfg.rep_scorecap,
|
|
||||||
rep_bump_secs: cfg.rep_bump_secs,
|
|
||||||
rep_expire_secs: cfg.rep_expire_secs,
|
|
||||||
rep_save_secs: cfg.rep_save_secs,
|
|
||||||
rep_minchanmembers: cfg.rep_minchanmembers,
|
|
||||||
rep_whois: cfg.rep_whois,
|
|
||||||
network_icon: cfg.network_icon,
|
|
||||||
profilelink_baseurl: cfg.profilelink_baseurl,
|
|
||||||
hidewhois: cfg.hidewhois,
|
|
||||||
hidewhois_opers: cfg.hidewhois_opers,
|
|
||||||
hidewhois_selfview: cfg.hidewhois_selfview,
|
|
||||||
hidewhois_server: cfg.hidewhois_server,
|
|
||||||
hidewhois_idle: cfg.hidewhois_idle,
|
|
||||||
hidewhois_away: cfg.hidewhois_away,
|
|
||||||
hidewhois_secure: cfg.hidewhois_secure,
|
|
||||||
plain_port: port_of(&cfg.bind),
|
|
||||||
tls_port: cfg.bind_tls.as_deref().map(port_of).unwrap_or(0),
|
|
||||||
rep_expire_rules: if cfg.rep_expire_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 {
|
|
||||||
cfg.rep_expire_rules
|
|
||||||
},
|
|
||||||
label_capture: RefCell::new(None),
|
label_capture: RefCell::new(None),
|
||||||
event_tx,
|
event_tx,
|
||||||
conn_counter,
|
conn_counter,
|
||||||
|
|
@ -271,6 +195,35 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The last value set for config `key` (`None` if unset). Modules read their
|
||||||
|
/// own settings through here so no per-module field bloats `Server`/`Config`.
|
||||||
|
pub fn conf(&self, key: &str) -> Option<&str> {
|
||||||
|
self.raw_config
|
||||||
|
.get(key)
|
||||||
|
.and_then(|v| v.last())
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every value set for `key` (repeated lines, e.g. `securitygroup`, `motd`).
|
||||||
|
pub fn conf_all(&self, key: &str) -> &[String] {
|
||||||
|
self.raw_config
|
||||||
|
.get(key)
|
||||||
|
.map(|v| v.as_slice())
|
||||||
|
.unwrap_or(&[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A boolean config value (`yes`/`no`/…); `default` when the key is unset.
|
||||||
|
pub fn conf_bool(&self, key: &str, default: bool) -> bool {
|
||||||
|
self.conf(key).map(crate::config::yesish).unwrap_or(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A parsed config value; `default` when unset or unparseable.
|
||||||
|
pub fn conf_num<T: std::str::FromStr>(&self, key: &str, default: T) -> T {
|
||||||
|
self.conf(key)
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
|
}
|
||||||
|
|
||||||
/// Remember an identity for WHOWAS (capped ring, newest first).
|
/// Remember an identity for WHOWAS (capped ring, newest first).
|
||||||
pub fn push_whowas(
|
pub fn push_whowas(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -348,13 +301,8 @@ impl Server {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// connflood — refuse an IP that's opening connections too fast
|
// connflood — refuse an IP that's opening connections too fast (see modules::connflood)
|
||||||
if let Some((max, secs)) = self.connflood {
|
if crate::modules::connflood::over_limit(self, ip) {
|
||||||
let n = now();
|
|
||||||
let hist = self.conn_history.entry(ip).or_default();
|
|
||||||
hist.retain(|&t| n.saturating_sub(t) < secs);
|
|
||||||
hist.push(n);
|
|
||||||
if hist.len() as u32 > max {
|
|
||||||
self.send(
|
self.send(
|
||||||
uid,
|
uid,
|
||||||
"ERROR :Closing link: (Too many connections from your IP)".to_string(),
|
"ERROR :Closing link: (Too many connections from your IP)".to_string(),
|
||||||
|
|
@ -362,7 +310,6 @@ impl Server {
|
||||||
self.remove_user(uid, "Connection throttled");
|
self.remove_user(uid, "Connection throttled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-registration connection notices, InspIRCd / solanum style. Ident-113
|
// Pre-registration connection notices, InspIRCd / solanum style. Ident-113
|
||||||
// is archaic and firewalled, so those two are cosmetic; the hostname lookup
|
// is archaic and firewalled, so those two are cosmetic; the hostname lookup
|
||||||
|
|
@ -408,17 +355,6 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop stale per-IP connflood bookkeeping (called on the background tick).
|
|
||||||
pub fn prune_conn_history(&mut self) {
|
|
||||||
if let Some((_, secs)) = self.connflood {
|
|
||||||
let n = now();
|
|
||||||
self.conn_history.retain(|_, times| {
|
|
||||||
times.retain(|&t| n.saturating_sub(t) < secs);
|
|
||||||
!times.is_empty()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A pre-registration `:server NOTICE * :*** <msg>` line.
|
/// A pre-registration `:server NOTICE * :*** <msg>` line.
|
||||||
pub(crate) fn notice_star(&self, uid: Uid, msg: &str) {
|
pub(crate) fn notice_star(&self, uid: Uid, msg: &str) {
|
||||||
self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name));
|
self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name));
|
||||||
|
|
|
||||||
35
src/users.rs
35
src/users.rs
|
|
@ -327,12 +327,18 @@ impl Server {
|
||||||
self.send(uid, format!(":{} MODE {nick} :+os", self.name));
|
self.send(uid, format!(":{} MODE {nick} :+os", self.name));
|
||||||
self.snotice(&format!("{nick} is now an IRC operator"));
|
self.snotice(&format!("{nick} is now an IRC operator"));
|
||||||
// opermodes: extra umodes on oper-up
|
// opermodes: extra umodes on oper-up
|
||||||
if !self.oper_umodes.is_empty() {
|
let om = self.conf("opermodes").or_else(|| self.conf("oper_umodes"));
|
||||||
let modes = self.oper_umodes.clone();
|
if let Some(modes) = om.map(str::to_string) {
|
||||||
crate::coremods::core_mode::svs_set_user_modes(self, uid, &modes);
|
crate::coremods::core_mode::svs_set_user_modes(self, uid, &modes);
|
||||||
}
|
}
|
||||||
// operjoin: auto-join configured oper channels
|
// operjoin: auto-join configured oper channels
|
||||||
for chan in self.oper_autojoin.clone() {
|
let chans: Vec<String> = self
|
||||||
|
.conf_all("operjoin")
|
||||||
|
.iter()
|
||||||
|
.flat_map(|v| v.split([',', ' ']).map(str::to_string))
|
||||||
|
.filter(|c| !c.is_empty())
|
||||||
|
.collect();
|
||||||
|
for chan in chans {
|
||||||
self.join(uid, &chan, None);
|
self.join(uid, &chan, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -395,7 +401,7 @@ impl Server {
|
||||||
for t in targets {
|
for t in targets {
|
||||||
self.send(t, line.clone());
|
self.send(t, line.clone());
|
||||||
}
|
}
|
||||||
if self.seenicks {
|
if self.conf_bool("seenicks", false) {
|
||||||
self.snotice(&format!("{old} is now known as {newnick}"));
|
self.snotice(&format!("{old} is now known as {newnick}"));
|
||||||
}
|
}
|
||||||
// WATCH/MONITOR: the old nick is now gone, the new one is here
|
// WATCH/MONITOR: the old nick is now gone, the new one is here
|
||||||
|
|
@ -448,11 +454,11 @@ impl Server {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// ircv3_network_icon: advertise draft/ICON when configured
|
// ircv3_network_icon: advertise draft/ICON when configured
|
||||||
if !self.network_icon.is_empty() {
|
if let Some(tok) = crate::modules::network_icon::isupport(self) {
|
||||||
self.numeric(
|
self.numeric(
|
||||||
uid,
|
uid,
|
||||||
RPL_ISUPPORT,
|
RPL_ISUPPORT,
|
||||||
&format!("ICON={} :are supported by this server", self.network_icon),
|
&format!("{tok} :are supported by this server"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.numeric(
|
self.numeric(
|
||||||
|
|
@ -462,18 +468,25 @@ impl Server {
|
||||||
);
|
);
|
||||||
self.send_motd(uid);
|
self.send_motd(uid);
|
||||||
// connbanner: NOTICE lines to every connecting client
|
// connbanner: NOTICE lines to every connecting client
|
||||||
for line in self.conn_banner.clone() {
|
for line in self.conf_all("connbanner").to_vec() {
|
||||||
self.send(uid, format!(":{} NOTICE {nick} :{line}", self.name));
|
self.send(uid, format!(":{} NOTICE {nick} :{line}", self.name));
|
||||||
}
|
}
|
||||||
// conn_umodes: auto-set user modes on connect
|
// conn_umodes: auto-set user modes on connect
|
||||||
if !self.auto_umodes.is_empty() {
|
let auto_umodes = self.conf("conn_umodes").or_else(|| self.conf("autoumodes"));
|
||||||
let modes = self.auto_umodes.clone();
|
if let Some(modes) = auto_umodes.map(str::to_string) {
|
||||||
crate::coremods::core_mode::svs_set_user_modes(self, uid, &modes);
|
crate::coremods::core_mode::svs_set_user_modes(self, uid, &modes);
|
||||||
}
|
}
|
||||||
self.watch_notify_online(&nick); // tell WATCH/MONITOR watchers
|
self.watch_notify_online(&nick); // tell WATCH/MONITOR watchers
|
||||||
self.events.push_back(Hook::Connect(uid));
|
self.events.push_back(Hook::Connect(uid));
|
||||||
// conn_join: auto-join configured channels
|
// conn_join: auto-join configured channels (comma/space separated, repeatable)
|
||||||
for chan in self.autojoin.clone() {
|
let chans: Vec<String> = self
|
||||||
|
.conf_all("autojoin")
|
||||||
|
.iter()
|
||||||
|
.chain(self.conf_all("conn_join"))
|
||||||
|
.flat_map(|v| v.split([',', ' ']).map(str::to_string))
|
||||||
|
.filter(|c| !c.is_empty())
|
||||||
|
.collect();
|
||||||
|
for chan in chans {
|
||||||
self.join(uid, &chan, None);
|
self.join(uid, &chan, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue