config: checkconfig CLI + Config::dump; tls block carries sni/handshake_timeout

This commit is contained in:
Jean Chevronnet 2026-08-23 12:42:30 +00:00
parent b11382bc1e
commit 1166c49443
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
2 changed files with 54 additions and 8 deletions

View file

@ -193,6 +193,23 @@ impl Config {
Some(c)
}
/// A deterministic text dump of every parsed key/value (sorted), used by
/// `echoircd checkconfig` to compare two configs regardless of source format.
pub fn dump(&self) -> String {
let mut keys: Vec<&String> = self.raw.keys().collect();
keys.sort();
let mut out = String::new();
for k in keys {
for v in &self.raw[k] {
out.push_str(k);
out.push_str(" = ");
out.push_str(v);
out.push('\n');
}
}
out
}
// small helper is defined at module scope (see `yesish`).
/// Parse `key = value` lines into `c`; unknown keys and comments are ignored.
@ -585,14 +602,17 @@ fn emit_block(out: &mut String, name: &str, fields: &[(String, String)]) {
}
}
"tls" => {
if let Some(v) = get("backend") {
emit_line(out, "tls_backend", v);
}
if let Some(v) = get("cert") {
emit_line(out, "tls_cert", v);
}
if let Some(v) = get("key") {
emit_line(out, "tls_key", v);
// backend/cert/key map to their tls_ keys; any other field (sni,
// handshake_timeout, …) passes through as tls_<field>, repeatable.
for (f, v) in fields {
let fl = f.to_ascii_lowercase();
let key = match fl.as_str() {
"backend" => "tls_backend".to_string(),
"cert" => "tls_cert".to_string(),
"key" => "tls_key".to_string(),
other => format!("tls_{other}"),
};
emit_line(out, &key, v);
}
}
"cloak" => {

View file

@ -132,6 +132,26 @@ fn mkpasswd_cli(cost_arg: Option<&str>) -> i32 {
}
}
/// `echoircd checkconfig [config]`: parse a config and print a deterministic,
/// sorted dump of every key/value it produces. Two configs (e.g. flat vs block
/// format) that dump identically parse identically.
fn checkconfig_cli(path: &str) -> i32 {
match echoircd::config::Config::try_load(path) {
Some(c) => {
print!("{}", c.dump());
if c.servername.is_empty() {
eprintln!("echoircd: WARNING — servername is empty");
return 2;
}
0
}
None => {
eprintln!("echoircd: cannot read config {path}");
1
}
}
}
fn main() {
let mut args = std::env::args().skip(1);
let first = args.next();
@ -144,6 +164,12 @@ fn main() {
if first.as_deref() == Some("mkpasswd") {
std::process::exit(mkpasswd_cli(args.next().as_deref()));
}
// `echoircd checkconfig [config]` parses a config and prints a sorted dump
// of every key/value (for validating a config or diffing two of them).
if first.as_deref() == Some("checkconfig") {
let cfgpath = args.next().unwrap_or_else(|| "echoircd.conf".to_string());
std::process::exit(checkconfig_cli(&cfgpath));
}
let path = first.unwrap_or_else(|| "echoircd.conf".to_string());
let cfg = Config::load(&path);