From 0d4c9297b5b0034252800cbdb7d026a77cf917a3 Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 08:33:56 +0000 Subject: [PATCH] port whoisport, ircv3_network_icon, profileLink, hidewhois (all config-driven) --- echoircd.conf.example | 13 +++++++++ src/config.rs | 42 ++++++++++++++++++++++++++++++ src/coremods/core_info.rs | 55 ++++++++++++++++++++++++++++++--------- src/server.rs | 30 +++++++++++++++++++++ src/users.rs | 8 ++++++ 5 files changed, 135 insertions(+), 13 deletions(-) diff --git a/echoircd.conf.example b/echoircd.conf.example index cc02d8a..09d0099 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -104,3 +104,16 @@ amu_target = both # reputationexpire = 2 1h # score<=2 decays after 1h (repeatable; * = any) # reputationexpire = * 90d # any score decays after 90d # extban usage: MODE #chan +b y:<100 (ban score below 100) +b y:>500 (above 500) + +# --- whoisport: opers see the target's listener port in WHOIS (always on) --- +# --- ircv3_network_icon: advertise a network icon via draft/ICON ISUPPORT --- +# network_icon = https://example.org/icon.png +# --- profileLink: a profile URL in WHOIS for logged-in users --- +# profilelink_baseurl = https://example.org/profile/ +# --- hidewhois: hide sensitive WHOIS lines from ordinary users --- +# hidewhois = yes +# hidewhois_opers = yes # opers still see everything +# hidewhois_selfview = yes # a user sees their own full WHOIS +# hidewhois_hide_server = yes # hide 312 +# hidewhois_hide_idle = yes # hide 317 +# hidewhois_hide_secure = yes # hide 671 diff --git a/src/config.rs b/src/config.rs index 0772655..ed35a1f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,14 @@ //! oper = god secret //! ``` +/// Parse a boolean config value (`yes`/`no`/`true`/`false`/`on`/`off`/`1`/`0`). +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 { @@ -124,6 +132,15 @@ pub struct Config { 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 { @@ -174,6 +191,15 @@ impl Default for Config { 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, } } } @@ -205,6 +231,8 @@ impl Config { Some(c) } + // small helper is defined at module scope (see `yesish`). + /// Parse `key = value` lines into `c`; unknown keys and comments are ignored. fn parse_into(c: &mut Config, text: &str) { for line in text.lines() { @@ -419,6 +447,20 @@ impl Config { } } } + "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 = [public] [tls|insecure] [account|unregistered] // [oper|exclude-oper] [bot|exclude-bot] [webirc|exclude-webirc] diff --git a/src/coremods/core_info.rs b/src/coremods/core_info.rs index 73f51c1..abf12f0 100644 --- a/src/coremods/core_info.rs +++ b/src/coremods/core_info.rs @@ -148,6 +148,9 @@ impl Command for Whois { }; let asker_oper = s.is_oper(uid); let is_self = tuid == uid; + // hidewhois: hide sensitive lines from ordinary users (opers/self exempt per config) + let hide = + s.hidewhois && !(is_self && s.hidewhois_selfview) && !(asker_oper && s.hidewhois_opers); let keys: Vec = s.users[&tuid].channels.iter().cloned().collect(); let ( nick, @@ -203,11 +206,13 @@ impl Command for Whois { if bot { s.numeric(uid, RPL_WHOISBOT, &format!("{nick} :is a bot")); } - s.numeric( - uid, - RPL_WHOISSERVER, - &format!("{nick} {} :echoIRCd", s.name), - ); + if !(hide && s.hidewhois_server) { + s.numeric( + uid, + RPL_WHOISSERVER, + &format!("{nick} {} :echoIRCd", s.name), + ); + } // +I hides the channel list from everyone but the user themselves + opers if !chans.is_empty() && (is_self || asker_oper || !hidechans) { s.numeric( @@ -245,6 +250,28 @@ impl Command for Whois { s.numeric(uid, RPL_WHOISSPECIAL, &format!(":Score: {score}")); } } + // profileLink: a profile URL for logged-in users (when configured) + if !s.profilelink_baseurl.is_empty() { + match &account { + 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 (+ TLS/plain) — opers only + if asker_oper { + let port = if secure { s.tls_port } else { s.plain_port }; + if port != 0 { + s.numeric(uid, RPL_WHOISSPECIAL, &format!(":is using port {port}")); + } + } // opers can see through the cloak to the real host/ip if asker_oper && disp != realhost { s.numeric( @@ -262,7 +289,7 @@ impl Command for Whois { ); } // sslinfo: advertise a secure (TLS) connection - if secure { + if secure && !(hide && s.hidewhois_secure) { s.numeric( uid, RPL_WHOISSECURE, @@ -279,13 +306,15 @@ impl Command for Whois { ); } } - // 317: idle time + signon time - let idle = crate::server::now().saturating_sub(last_active); - s.numeric( - uid, - RPL_WHOISIDLE, - &format!("{nick} {idle} {signon} :seconds idle, signon time"), - ); + // 317: idle time + signon time (hidewhois may suppress it) + if !(hide && s.hidewhois_idle) { + let idle = crate::server::now().saturating_sub(last_active); + s.numeric( + uid, + RPL_WHOISIDLE, + &format!("{nick} {idle} {signon} :seconds idle, signon time"), + ); + } // +W showwhois — tell the target that someone looked them up if showwhois && !is_self { let by = s.users.get(&uid).map(|u| u.prefix()).unwrap_or_default(); diff --git a/src/server.rs b/src/server.rs index 6a1e6ab..f7d845c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -35,6 +35,14 @@ pub const PING_AFTER: u64 = 90; pub const PING_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 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -155,6 +163,17 @@ pub struct Server { 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 // 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 @@ -228,6 +247,17 @@ impl Server { 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)] diff --git a/src/users.rs b/src/users.rs index 6eb4897..f36d520 100644 --- a/src/users.rs +++ b/src/users.rs @@ -447,6 +447,14 @@ impl Server { self.network ), ); + // ircv3_network_icon: advertise draft/ICON when configured + if !self.network_icon.is_empty() { + self.numeric( + uid, + RPL_ISUPPORT, + &format!("ICON={} :are supported by this server", self.network_icon), + ); + } self.numeric( uid, RPL_LUSERCLIENT,