diff --git a/src/config.rs b/src/config.rs index ef99a93..9f00519 100644 --- a/src/config.rs +++ b/src/config.rs @@ -106,9 +106,27 @@ impl Config { conf_path: path.to_string(), ..Config::default() }; - let Ok(text) = std::fs::read_to_string(path) else { - return c; + if let Ok(text) = std::fs::read_to_string(path) { + Self::parse_into(&mut c, &text); + } + c + } + + /// Like [`load`](Config::load) but returns `None` if the file can't be read, + /// so REHASH can keep the running config instead of resetting to defaults — + /// the way InspIRCd keeps the old config when a reload fails. + pub fn try_load(path: &str) -> Option { + let text = std::fs::read_to_string(path).ok()?; + let mut c = Config { + conf_path: path.to_string(), + ..Config::default() }; + Self::parse_into(&mut c, &text); + Some(c) + } + + /// 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() { let line = line.trim(); if line.is_empty() || line.starts_with('#') || line.starts_with(';') { @@ -204,6 +222,5 @@ impl Config { _ => {} } } - c } } diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index 202b5eb..1ae141f 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -3,7 +3,6 @@ use crate::channels::Topic; use crate::command::{CmdResult, Command}; -use crate::config::Config; use crate::coremods::core_mode::apply_mode; use crate::module::Hook; use crate::numeric::*; @@ -19,7 +18,6 @@ pub fn commands() -> Vec> { Box::new(Wallops), Box::new(SvsLogin), Box::new(SvsLogout), - Box::new(Rehash), Box::new(GlobOps), Box::new(SaJoin), Box::new(SaPart), @@ -213,29 +211,6 @@ impl Command for Wallops { } } -/// REHASH — reload the config file (MOTD, oper blocks, cloak key). -struct Rehash; -impl Command for Rehash { - fn name(&self) -> &'static str { - "REHASH" - } - fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult { - if !require_oper(s, uid) { - return CmdResult::Fail; - } - let fresh = Config::load(&s.conf_path); - s.motd = fresh.motd; - s.opers = fresh.opers; - s.cloak_key = fresh.cloak_key; - s.censor = fresh.censor; - s.amu = fresh.amu; - s.resolve_hosts = fresh.resolve_hosts; - s.use_resolved_host = fresh.use_resolved_host; - s.numeric(uid, RPL_REHASHING, &format!("{} :Rehashing", s.conf_path)); - CmdResult::Ok - } -} - /// GLOBOPS — a message to every IRC operator. struct GlobOps; impl Command for GlobOps { diff --git a/src/coremods/core_rehash.rs b/src/coremods/core_rehash.rs new file mode 100644 index 0000000..b915b2a --- /dev/null +++ b/src/coremods/core_rehash.rs @@ -0,0 +1,79 @@ +//! core_rehash — the REHASH command. Re-reads the config file and applies every +//! setting that can change at runtime, the way InspIRCd's rehash does: +//! +//! * opers only; replies with RPL_REHASHING (382) and a server-notice to +s opers; +//! * takes an optional `` (we only rehash if it matches this server — +//! there's no remote-rehash over S2S yet); +//! * **keeps the running config if the file can't be read** (via `Config::try_load`), +//! so a REHASH of a deleted/renamed config never resets opers/cloak-key to defaults. +//! +//! Reloadable live: MOTD, oper blocks, cloak key, +G censor words, antimixedutf8, +//! and the reverse-DNS options. Listener/bind/SID changes still need a restart. + +use crate::channels::glob_match; +use crate::command::{CmdResult, Command}; +use crate::config::Config; +use crate::numeric::*; +use crate::server::Server; +use crate::Uid; + +pub fn commands() -> Vec> { + vec![Box::new(Rehash)] +} + +struct Rehash; +impl Command for Rehash { + fn name(&self) -> &'static str { + "REHASH" + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + if !s.is_oper(uid) { + s.numeric( + uid, + ERR_NOPRIVILEGES, + ":Permission Denied- You're not an IRC operator", + ); + return CmdResult::Fail; + } + // `REHASH ` only rehashes servers matching the mask; we're one server. + if let Some(mask) = params.first() { + if !glob_match(mask, &s.name) { + s.numeric(uid, RPL_REHASHING, &format!("{mask} :No matching servers")); + return CmdResult::Ok; + } + } + let who = s + .users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_default(); + let path = s.conf_path.clone(); + match Config::try_load(&path) { + Some(fresh) => { + s.motd = fresh.motd; + s.opers = fresh.opers; + s.cloak_key = fresh.cloak_key; + s.censor = fresh.censor; + s.amu = fresh.amu; + s.resolve_hosts = fresh.resolve_hosts; + s.use_resolved_host = fresh.use_resolved_host; + s.numeric(uid, RPL_REHASHING, &format!("{path} :Rehashing")); + s.snotice(&format!("{who} is rehashing config: {path}")); + } + None => { + // Unreadable config — keep what's running (do NOT reset to defaults). + s.send( + uid, + format!( + ":{} NOTICE {who} :*** Cannot read {path}; keeping the running config", + s.name + ), + ); + s.snotice(&format!( + "{who} tried to REHASH but {path} could not be read; config unchanged" + )); + } + } + CmdResult::Ok + } +} diff --git a/src/coremods/mod.rs b/src/coremods/mod.rs index fd58778..798b92f 100644 --- a/src/coremods/mod.rs +++ b/src/coremods/mod.rs @@ -9,6 +9,7 @@ pub mod core_info; pub mod core_message; pub mod core_mode; pub mod core_oper; +pub mod core_rehash; pub mod core_user; pub mod core_watch; @@ -24,6 +25,7 @@ pub fn command_table() -> HashMap<&'static str, Box> { .chain(core_message::commands()) .chain(core_mode::commands()) .chain(core_oper::commands()) + .chain(core_rehash::commands()) .chain(core_info::commands()) .chain(core_extra::commands()) .chain(core_watch::commands())