config: accept oper password=<hash> named token; keep positional password back-compat
This commit is contained in:
parent
c2e0a9eeb4
commit
e8d16a1f96
2 changed files with 109 additions and 16 deletions
|
|
@ -55,7 +55,11 @@ bind_server = 0.0.0.0:7000
|
|||
# webirc = CHANGE_THIS_WEBIRC_SECRET mygateway 203.0.113.9
|
||||
|
||||
# IRC operators — oper = <name> <password> [level] [fp=<sha256>] [type=<id>]
|
||||
# The password may be positional (as below) or a named password=<hash> token;
|
||||
# both accept plaintext, sha256:<hex>, pbkdf2:… or a bcrypt $2b$ hash. Generate a
|
||||
# bcrypt hash with: printf '%s' 'yourpassword' | ./echoircd mkpasswd
|
||||
oper = admin CHANGE_THIS_PASSWORD
|
||||
# oper = admin password=$2b$11$…hash… fp=<sha256> type=netadmin
|
||||
|
||||
# Oper types (optional). An oper with no type= keeps full access. A type is a
|
||||
# named role (the WHOIS "is a <title>") built from reusable capability classes;
|
||||
|
|
|
|||
117
src/config.rs
117
src/config.rs
|
|
@ -6,8 +6,9 @@
|
|||
//! bind = 127.0.0.1:6767
|
||||
//! motd = Welcome to echoIRCd
|
||||
//! oper = god secret # name + password (+ optional level)
|
||||
//! oper = god password=<hash> # same, named form (fp=/type= style)
|
||||
//! oper = god * fp=<sha256-cert-fp> # cert-only login (no password)
|
||||
//! oper = god secret fp=<sha256-cert-fp> # password AND matching cert
|
||||
//! oper = god password=<hash> fp=<cert-fp> # password AND matching cert
|
||||
//! ```
|
||||
|
||||
use crate::map::HashMap;
|
||||
|
|
@ -20,6 +21,16 @@ pub fn yesish(v: &str) -> bool {
|
|||
)
|
||||
}
|
||||
|
||||
/// Whether an oper token is a `key=value` option rather than the positional
|
||||
/// password (so `oper = <name> password=<hash> fp=<fp>` isn't misread as having
|
||||
/// the literal password `password=<hash>`).
|
||||
fn is_oper_option(tok: &str) -> bool {
|
||||
tok.starts_with("password=")
|
||||
|| tok.starts_with("fp=")
|
||||
|| tok.starts_with("certfp=")
|
||||
|| tok.starts_with("type=")
|
||||
}
|
||||
|
||||
/// A server-link block: how to authenticate a peer named `name` (and, if
|
||||
/// `autoconnect`, where to dial it). Passwords are the shared link secret.
|
||||
#[derive(Clone)]
|
||||
|
|
@ -31,9 +42,11 @@ pub struct LinkBlock {
|
|||
pub autoconnect: bool,
|
||||
}
|
||||
|
||||
/// An oper login: `oper = <name> <password|*> [level] [fp=<sha256-fingerprint>]`.
|
||||
/// `password = *` means no password is checked (cert-only login); a `fp=` token
|
||||
/// requires the user's TLS client-certificate SHA-256 fingerprint to match.
|
||||
/// An oper login: `oper = <name> [<password|*> | password=<hash>] [level]
|
||||
/// [fp=<sha256-fingerprint>] [type=<id>]`. The password may be given positionally
|
||||
/// or as a `password=` token; `*` (or an absent password with a `fp=`) means no
|
||||
/// password is checked (cert-only login); a `fp=` token requires the user's TLS
|
||||
/// client-certificate SHA-256 fingerprint to match.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct OperBlock {
|
||||
pub name: String,
|
||||
|
|
@ -227,20 +240,28 @@ impl Config {
|
|||
}
|
||||
"motd" => c.motd.push(v.to_string()),
|
||||
"oper" => {
|
||||
let mut it = v.split_whitespace();
|
||||
if let (Some(n), Some(p)) = (it.next(), it.next()) {
|
||||
let mut it = v.split_whitespace().peekable();
|
||||
if let Some(n) = it.next() {
|
||||
let mut b = OperBlock {
|
||||
name: n.to_string(),
|
||||
password: p.to_string(),
|
||||
level: 0,
|
||||
fingerprint: None,
|
||||
oper_type: None,
|
||||
..Default::default()
|
||||
};
|
||||
// trailing tokens (any order): a number is the operlevel, a
|
||||
// `fp=`/`certfp=` token is the required TLS cert fingerprint,
|
||||
// a `type=` token names the oper type (see modules::opertypes).
|
||||
// Back-compat: a bare second token (not a `key=value` option)
|
||||
// is the positional password (`*` = cert-only), matching the
|
||||
// old `oper = <name> <password|*> ...` form.
|
||||
if let Some(tok) = it.peek() {
|
||||
if !is_oper_option(tok) {
|
||||
b.password = tok.to_string();
|
||||
it.next();
|
||||
}
|
||||
}
|
||||
// Named options in any order: `password=` (preferred),
|
||||
// `fp=`/`certfp=` the required TLS cert fingerprint, `type=`
|
||||
// the oper type; a bare number is the operlevel.
|
||||
for tok in it {
|
||||
if let Some(fp) =
|
||||
if let Some(pw) = tok.strip_prefix("password=") {
|
||||
b.password = pw.to_string();
|
||||
} else if let Some(fp) =
|
||||
tok.strip_prefix("fp=").or_else(|| tok.strip_prefix("certfp="))
|
||||
{
|
||||
b.fingerprint = Some(fp.to_ascii_lowercase());
|
||||
|
|
@ -250,9 +271,14 @@ impl Config {
|
|||
b.level = l;
|
||||
}
|
||||
}
|
||||
// A block with no credential at all (no password, no cert
|
||||
// fingerprint) would let anyone oper up — refuse it, as the
|
||||
// old parser did by requiring a password token.
|
||||
if !b.password.is_empty() || b.fingerprint.is_some() {
|
||||
c.opers.push(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
// +G censor word: `badword = <find> [replace]` (no replace ⇒ block)
|
||||
"badword" => {
|
||||
let mut it = v.splitn(2, char::is_whitespace);
|
||||
|
|
@ -326,3 +352,66 @@ impl Config {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn opers(text: &str) -> Vec<OperBlock> {
|
||||
let mut c = Config::default();
|
||||
Config::parse_into(&mut c, text);
|
||||
c.opers
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positional_password_still_parses() {
|
||||
let o = opers("oper = god secret 5 fp=ABC type=netadmin");
|
||||
assert_eq!(o.len(), 1);
|
||||
assert_eq!(o[0].name, "god");
|
||||
assert_eq!(o[0].password, "secret");
|
||||
assert_eq!(o[0].level, 5);
|
||||
assert_eq!(o[0].fingerprint.as_deref(), Some("abc"));
|
||||
assert_eq!(o[0].oper_type.as_deref(), Some("netadmin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn star_positional_is_cert_only() {
|
||||
let o = opers("oper = god * fp=abc");
|
||||
assert_eq!(o[0].password, "*");
|
||||
assert_eq!(o[0].fingerprint.as_deref(), Some("abc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_positional_password_is_not_a_level() {
|
||||
// a bare second token is the password even when numeric (back-compat)
|
||||
let o = opers("oper = god 12345");
|
||||
assert_eq!(o[0].password, "12345");
|
||||
assert_eq!(o[0].level, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_password_token() {
|
||||
let o = opers("oper = reverse password=$2b$11$abc.def/ghi fp=FF type=netadmin");
|
||||
assert_eq!(o.len(), 1);
|
||||
assert_eq!(o[0].name, "reverse");
|
||||
assert_eq!(o[0].password, "$2b$11$abc.def/ghi");
|
||||
assert_eq!(o[0].fingerprint.as_deref(), Some("ff"));
|
||||
assert_eq!(o[0].oper_type.as_deref(), Some("netadmin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_password_order_independent() {
|
||||
let o = opers("oper = reverse type=admin fp=aa password=hunter2 3");
|
||||
assert_eq!(o[0].password, "hunter2");
|
||||
assert_eq!(o[0].level, 3);
|
||||
assert_eq!(o[0].fingerprint.as_deref(), Some("aa"));
|
||||
assert_eq!(o[0].oper_type.as_deref(), Some("admin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credential_less_block_is_rejected() {
|
||||
// no password and no fp would let anyone oper up — must be dropped
|
||||
assert!(opers("oper = nobody").is_empty());
|
||||
assert!(opers("oper = nobody type=netadmin").is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue