akick: match extbans (account/realname/realmask/unauthed) using the ident, real host and real name the UID carries but was discarding
All checks were successful
CI / check (push) Successful in 4m0s
All checks were successful
CI / check (push) Successful in 4m0s
This commit is contained in:
parent
bc4fada354
commit
426fc694a2
8 changed files with 202 additions and 26 deletions
118
api/src/lib.rs
118
api/src/lib.rs
|
|
@ -21,6 +21,10 @@ pub enum NetEvent {
|
||||||
Idle { requester: String, target: String },
|
Idle { requester: String, target: String },
|
||||||
Privmsg { from: String, to: String, text: String },
|
Privmsg { from: String, to: String, text: String },
|
||||||
UserConnect { uid: String, nick: String, host: String, ip: String },
|
UserConnect { uid: String, nick: String, host: String, ip: String },
|
||||||
|
// The rest of a user's UID that UserConnect omits — ident, real host, and
|
||||||
|
// real name (gecos). The ircd sends them in the same UID line; extban
|
||||||
|
// matching (AKICK) needs them. Emitted right after UserConnect.
|
||||||
|
UserAttrs { uid: String, ident: String, realhost: String, gecos: String },
|
||||||
NickChange { uid: String, nick: String },
|
NickChange { uid: String, nick: String },
|
||||||
// The ircd tells us a user's logged-in account (METADATA accountname), e.g.
|
// The ircd tells us a user's logged-in account (METADATA accountname), e.g.
|
||||||
// replayed on a services netburst so we can restore who was identified. An
|
// replayed on a services netburst so we can restore who was identified. An
|
||||||
|
|
@ -1008,6 +1012,83 @@ pub struct ChanAkickView {
|
||||||
pub reason: String,
|
pub reason: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A live user's identity, matched against an AKICK mask (plain host or extban).
|
||||||
|
/// Built by the engine from what the ircd told us in the UID + metadata.
|
||||||
|
pub struct BanTarget<'a> {
|
||||||
|
pub nick: &'a str,
|
||||||
|
pub ident: &'a str,
|
||||||
|
pub host: &'a str, // displayed host
|
||||||
|
pub realhost: &'a str, // real host
|
||||||
|
pub ip: &'a str,
|
||||||
|
pub gecos: &'a str, // real name
|
||||||
|
pub account: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How echo interprets an AKICK mask. A plain `nick!user@host` glob is `Host`;
|
||||||
|
/// the rest are the InspIRCd matching-extbans echo has the s2s data to match (by
|
||||||
|
/// name `account:`… or by letter `R:`…). `Realmask` is the `nick!user@host+realname`
|
||||||
|
/// form (letter `a`). `Other` is any extban echo has no per-user data for (country,
|
||||||
|
/// class, …) — rejected at AKICK ADD.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AkickMask<'a> {
|
||||||
|
Host(&'a str),
|
||||||
|
Account(&'a str),
|
||||||
|
Unauthed(&'a str),
|
||||||
|
Realname(&'a str),
|
||||||
|
Realmask(&'a str, &'a str), // (hostmask, realname-glob)
|
||||||
|
Other(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AkickMask<'a> {
|
||||||
|
/// Parse a stored/entered AKICK mask. Extbans are `[name|letter]:value` whose
|
||||||
|
/// name holds no `!`/`@` (so an IPv6 host in a normal mask isn't mistaken for
|
||||||
|
/// one). Letters are case-sensitive (R = account, r = realname, a = realmask).
|
||||||
|
pub fn parse(mask: &'a str) -> AkickMask<'a> {
|
||||||
|
let Some((name, value)) = mask.split_once(':').filter(|(n, _)| !n.is_empty() && !n.contains(['!', '@'])) else {
|
||||||
|
return AkickMask::Host(mask);
|
||||||
|
};
|
||||||
|
let letter = if name.len() == 1 {
|
||||||
|
name.chars().next().unwrap()
|
||||||
|
} else {
|
||||||
|
match name.to_ascii_lowercase().as_str() {
|
||||||
|
"account" => 'R',
|
||||||
|
"unauthed" => 'U',
|
||||||
|
"realname" => 'r',
|
||||||
|
"realmask" => 'a',
|
||||||
|
_ => return AkickMask::Other(name),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match letter {
|
||||||
|
'R' => AkickMask::Account(value),
|
||||||
|
'U' => AkickMask::Unauthed(value),
|
||||||
|
'r' => AkickMask::Realname(value),
|
||||||
|
// realmask value is `<hostmask>+<realname>` (m_realnameban).
|
||||||
|
'a' => match value.split_once('+') {
|
||||||
|
Some((hm, real)) => AkickMask::Realmask(hm, real),
|
||||||
|
None => AkickMask::Other(name),
|
||||||
|
},
|
||||||
|
_ => AkickMask::Other(name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether an AKICK `mask` matches a live user `t` — a plain hostmask (globbed
|
||||||
|
/// against the displayed-host, real-host and IP forms, as InspIRCd's CheckBan
|
||||||
|
/// does) or one of the matching-extbans echo has the data for. An `Other` extban
|
||||||
|
/// (no per-user data) never matches.
|
||||||
|
pub fn akick_matches(mask: &str, t: &BanTarget) -> bool {
|
||||||
|
let hm = |host: &str| format!("{}!{}@{}", t.nick, t.ident, host);
|
||||||
|
let host_hit = |g: &str| glob_match(g, &hm(t.host)) || glob_match(g, &hm(t.realhost)) || glob_match(g, &hm(t.ip));
|
||||||
|
match AkickMask::parse(mask) {
|
||||||
|
AkickMask::Host(g) => host_hit(g),
|
||||||
|
AkickMask::Account(g) => t.account.is_some_and(|a| glob_match(g, a)),
|
||||||
|
AkickMask::Unauthed(g) => t.account.is_none() && host_hit(g),
|
||||||
|
AkickMask::Realname(g) => glob_match(g, t.gecos),
|
||||||
|
AkickMask::Realmask(hmask, real) => host_hit(hmask) && glob_match(real, t.gecos),
|
||||||
|
AkickMask::Other(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A memo left for an account (MemoServ).
|
// A memo left for an account (MemoServ).
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MemoView {
|
pub struct MemoView {
|
||||||
|
|
@ -1345,9 +1426,9 @@ impl ChannelView {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
// The first auto-kick entry whose mask matches the given hostmask.
|
// The first auto-kick entry matching a live user (host mask or extban).
|
||||||
pub fn akick_match(&self, hostmask: &str) -> Option<&ChanAkickView> {
|
pub fn akick_match(&self, target: &BanTarget) -> Option<&ChanAkickView> {
|
||||||
self.akick.iter().find(|k| glob_match(&k.mask, hostmask))
|
self.akick.iter().find(|k| akick_matches(&k.mask, target))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1643,6 +1724,8 @@ pub trait NetView {
|
||||||
fn nick_of(&self, uid: &str) -> Option<&str>;
|
fn nick_of(&self, uid: &str) -> Option<&str>;
|
||||||
fn host_of(&self, uid: &str) -> Option<&str>;
|
fn host_of(&self, uid: &str) -> Option<&str>;
|
||||||
fn account_of(&self, uid: &str) -> Option<&str>;
|
fn account_of(&self, uid: &str) -> Option<&str>;
|
||||||
|
// A user's identity for extban / AKICK matching, if known.
|
||||||
|
fn ban_target(&self, uid: &str) -> Option<BanTarget<'_>>;
|
||||||
fn uids_logged_into(&self, account: &str) -> Vec<String>;
|
fn uids_logged_into(&self, account: &str) -> Vec<String>;
|
||||||
fn is_op(&self, channel: &str, uid: &str) -> bool;
|
fn is_op(&self, channel: &str, uid: &str) -> bool;
|
||||||
fn channel_members(&self, channel: &str) -> Vec<String>;
|
fn channel_members(&self, channel: &str) -> Vec<String>;
|
||||||
|
|
@ -1827,6 +1910,35 @@ pub fn human_time(ts: u64) -> String {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn akick_extbans_parse_and_match() {
|
||||||
|
// Parse: hostmask fallback (incl IPv6), named + letter extbans, realmask
|
||||||
|
// split, and an extban echo has no data for.
|
||||||
|
assert_eq!(AkickMask::parse("*!*@host"), AkickMask::Host("*!*@host"));
|
||||||
|
assert_eq!(AkickMask::parse("*!*@2001:db8::1"), AkickMask::Host("*!*@2001:db8::1"), "ipv6 host isn't an extban");
|
||||||
|
assert_eq!(AkickMask::parse("account:bad"), AkickMask::Account("bad"));
|
||||||
|
assert_eq!(AkickMask::parse("R:bad"), AkickMask::Account("bad"), "letter form");
|
||||||
|
assert_eq!(AkickMask::parse("realname:*spam*"), AkickMask::Realname("*spam*"));
|
||||||
|
assert_eq!(AkickMask::parse("realmask:*!*@h+*bot*"), AkickMask::Realmask("*!*@h", "*bot*"));
|
||||||
|
assert!(matches!(AkickMask::parse("country:US"), AkickMask::Other("country")));
|
||||||
|
|
||||||
|
let t = BanTarget { nick: "n", ident: "u", host: "h.com", realhost: "real.h", ip: "1.2.3.4", gecos: "a spammer", account: Some("bad") };
|
||||||
|
assert!(akick_matches("*!*@h.com", &t), "displayed host");
|
||||||
|
assert!(akick_matches("*!*@real.h", &t), "real host too");
|
||||||
|
assert!(akick_matches("*!*@1.2.3.4", &t), "ip too");
|
||||||
|
assert!(akick_matches("account:bad", &t));
|
||||||
|
assert!(!akick_matches("account:other", &t));
|
||||||
|
assert!(akick_matches("realname:*spammer*", &t), "gecos");
|
||||||
|
assert!(akick_matches("realmask:*!*@h.com+*spammer*", &t), "host + realname");
|
||||||
|
assert!(!akick_matches("realmask:*!*@nope+*spammer*", &t), "host part must also match");
|
||||||
|
assert!(!akick_matches("unauthed:*!*@h.com", &t), "logged-in user isn't unauthed");
|
||||||
|
assert!(!akick_matches("country:US", &t), "no data -> never matches");
|
||||||
|
|
||||||
|
let anon = BanTarget { account: None, ..t };
|
||||||
|
assert!(akick_matches("unauthed:*!*@h.com", &anon), "not logged in + host matches");
|
||||||
|
assert!(!akick_matches("account:bad", &anon), "no account -> account extban can't match");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flags_bitset_parses_applies_and_matches_presets() {
|
fn flags_bitset_parses_applies_and_matches_presets() {
|
||||||
// ACCESS_FLAGS is exactly the flag letters in order — one source of truth.
|
// ACCESS_FLAGS is exactly the flag letters in order — one source of truth.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ use echo_api::Store;
|
||||||
use echo_api::{Sender, ServiceCtx};
|
use echo_api::{Sender, ServiceCtx};
|
||||||
|
|
||||||
// AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST
|
// AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST
|
||||||
// Masks are nick!user@host globs; matching users are banned and kicked on join.
|
// Masks are nick!user@host globs or an extban echo can match: account:<glob>,
|
||||||
|
// realname:<glob>, realmask:<host>+<realname>, or unauthed:<host>. Matching users
|
||||||
|
// are banned and kicked on join.
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
let Some(&chan) = args.get(1) else {
|
let Some(&chan) = args.get(1) else {
|
||||||
ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST");
|
ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST");
|
||||||
|
|
@ -24,6 +26,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
|
||||||
ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason]");
|
ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason]");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
// Reject extbans echo has no per-user data to match (country, class, …),
|
||||||
|
// so we never store an akick that can't be enforced.
|
||||||
|
if let echo_api::AkickMask::Other(name) = echo_api::AkickMask::parse(mask) {
|
||||||
|
ctx.notice(me, from.uid, format!("Can't auto-kick by the \x02{name}\x02 extban. Use a host mask or \x02account:\x02 / \x02realname:\x02 / \x02realmask:\x02 / \x02unauthed:\x02."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if !super::require_op(me, from, chan, ctx, db) {
|
if !super::require_op(me, from, chan, ctx, db) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
||||||
match net.account_of(&uid).and_then(|a| info.join_mode(a)) {
|
match net.account_of(&uid).and_then(|a| info.join_mode(a)) {
|
||||||
Some(m) => ctx.channel_mode(me, chan, &status_mode(m, &uid)),
|
Some(m) => ctx.channel_mode(me, chan, &status_mode(m, &uid)),
|
||||||
None => {
|
None => {
|
||||||
let nick = net.nick_of(&uid).unwrap_or("*");
|
if let Some(k) = net.ban_target(&uid).and_then(|t| info.akick_match(&t)) {
|
||||||
let host = net.host_of(&uid).unwrap_or("*");
|
|
||||||
if let Some(k) = info.akick_match(&format!("{nick}!*@{host}")) {
|
|
||||||
ctx.channel_mode(me, chan, &format!("+b {}", k.mask));
|
ctx.channel_mode(me, chan, &format!("+b {}", k.mask));
|
||||||
let reason = if k.reason.is_empty() { "You are banned from this channel." } else { &k.reason };
|
let reason = if k.reason.is_empty() { "You are banned from this channel." } else { &k.reason };
|
||||||
ctx.kick(me, chan, &uid, reason);
|
ctx.kick(me, chan, &uid, reason);
|
||||||
|
|
|
||||||
|
|
@ -106,12 +106,20 @@ impl Protocol for InspIrcd {
|
||||||
// <ip> <signon> … — take the displayed host for ban masks and the ip
|
// <ip> <signon> … — take the displayed host for ban masks and the ip
|
||||||
// (index 7) for session limiting.
|
// (index 7) for session limiting.
|
||||||
"UID" => {
|
"UID" => {
|
||||||
|
// insp4: uuid nickts nick realhost disphost realuser dispuser ip signonts +modes[ params] :gecos
|
||||||
let a: Vec<&str> = tokens.collect();
|
let a: Vec<&str> = tokens.collect();
|
||||||
match (a.first(), a.get(2)) {
|
match (a.first(), a.get(2)) {
|
||||||
(Some(uid), Some(nick)) => {
|
(Some(uid), Some(nick)) => {
|
||||||
|
let realhost = a.get(3).unwrap_or(&"").to_string();
|
||||||
let host = a.get(4).unwrap_or(&"").to_string();
|
let host = a.get(4).unwrap_or(&"").to_string();
|
||||||
|
let ident = a.get(6).unwrap_or(&"").to_string();
|
||||||
let ip = a.get(7).unwrap_or(&"").to_string();
|
let ip = a.get(7).unwrap_or(&"").to_string();
|
||||||
vec![NetEvent::UserConnect { uid: uid.to_string(), nick: nick.to_string(), host, ip }]
|
// gecos is the trailing param (has spaces) — take it whole.
|
||||||
|
let gecos = rest.split_once(" :").map_or("", |(_, g)| g).to_string();
|
||||||
|
vec![
|
||||||
|
NetEvent::UserConnect { uid: uid.to_string(), nick: nick.to_string(), host, ip },
|
||||||
|
NetEvent::UserAttrs { uid: uid.to_string(), ident, realhost, gecos },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
}
|
}
|
||||||
|
|
@ -558,12 +566,17 @@ mod tests {
|
||||||
assert!(matches!(ev.as_slice(), [NetEvent::ServerSplit { server }] if server == "0IR"), "{ev:?}");
|
assert!(matches!(ev.as_slice(), [NetEvent::ServerSplit { server }] if server == "0IR"), "{ev:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// A UID burst introduces the user under their current nick.
|
// A UID burst introduces the user and, alongside, the identity fields the
|
||||||
|
// UID carries that UserConnect omits (ident, real host, gecos).
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_uid_burst() {
|
fn parses_uid_burst() {
|
||||||
let ev = proto().parse(":0IR UID 0IRAAAAAB 1783833000 alice host host user user 0.0.0.0 1783833000 +i :real");
|
let ev = proto().parse(":0IR UID 0IRAAAAAB 1783833000 alice real.host disp.host ruser duser 1.2.3.4 1783833000 +i :Real Name");
|
||||||
assert!(
|
assert!(
|
||||||
matches!(ev.as_slice(), [NetEvent::UserConnect { uid, nick, .. }] if uid == "0IRAAAAAB" && nick == "alice"),
|
matches!(ev.as_slice(), [
|
||||||
|
NetEvent::UserConnect { uid, nick, host, ip },
|
||||||
|
NetEvent::UserAttrs { ident, realhost, gecos, .. },
|
||||||
|
] if uid == "0IRAAAAAB" && nick == "alice" && host == "disp.host" && ip == "1.2.3.4"
|
||||||
|
&& ident == "duser" && realhost == "real.host" && gecos == "Real Name"),
|
||||||
"{ev:?}"
|
"{ev:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -625,9 +625,9 @@ impl ChannelInfo {
|
||||||
.is_some_and(|a| echo_api::level_caps(&a.level).op)
|
.is_some_and(|a| echo_api::level_caps(&a.level).op)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The matching auto-kick entry for `hostmask` (nick!user@host), if any.
|
/// The auto-kick entry matching a live user (plain host mask or extban), if any.
|
||||||
pub fn akick_match(&self, hostmask: &str) -> Option<&ChanAkick> {
|
pub fn akick_match(&self, target: &echo_api::BanTarget) -> Option<&ChanAkick> {
|
||||||
self.akick.iter().find(|k| glob_match(&k.mask, hostmask))
|
self.akick.iter().find(|k| echo_api::akick_matches(&k.mask, target))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mode string services keep applied: +r plus the lock.
|
/// The mode string services keep applied: +r plus the lock.
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,18 @@
|
||||||
let mut db = Db::open(&tmp("akick"), "N1");
|
let mut db = Db::open(&tmp("akick"), "N1");
|
||||||
db.register_channel("#c", "founder").unwrap();
|
db.register_channel("#c", "founder").unwrap();
|
||||||
db.akick_add("#c", "*!*@bad.host", "spam").unwrap();
|
db.akick_add("#c", "*!*@bad.host", "spam").unwrap();
|
||||||
|
// Also an account extban (the evasion-proof kind) and a realname extban.
|
||||||
|
db.akick_add("#c", "account:baddie", "gone").unwrap();
|
||||||
|
db.akick_add("#c", "realname:*viagra*", "spam").unwrap();
|
||||||
let info = db.channel("#c").unwrap();
|
let info = db.channel("#c").unwrap();
|
||||||
assert!(info.akick_match("evil!~e@bad.host").is_some());
|
let t = |nick, ident, host, gecos, account| echo_api::BanTarget { nick, ident, host, realhost: host, ip: "0.0.0.0", gecos, account };
|
||||||
assert!(info.akick_match("good!~g@ok.host").is_none());
|
assert!(info.akick_match(&t("evil", "~e", "bad.host", "", None)).is_some(), "host mask");
|
||||||
|
assert!(info.akick_match(&t("good", "~g", "ok.host", "", None)).is_none());
|
||||||
|
assert!(info.akick_match(&t("x", "y", "ok.host", "", Some("baddie"))).is_some(), "account extban");
|
||||||
|
assert!(info.akick_match(&t("x", "y", "ok.host", "cheap VIAGRA now", None)).is_some(), "realname extban");
|
||||||
|
assert!(info.akick_match(&t("x", "y", "ok.host", "hello", None)).is_none());
|
||||||
assert!(db.akick_del("#c", "*!*@bad.host").unwrap());
|
assert!(db.akick_del("#c", "*!*@bad.host").unwrap());
|
||||||
assert!(!db.akick_del("#c", "*!*@bad.host").unwrap());
|
assert!(!db.akick_del("#c", "*!*@bad.host").unwrap());
|
||||||
assert!(db.channel("#c").unwrap().akick.is_empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The auto-join list adds, updates a key in place, removes case-insensitively,
|
// The auto-join list adds, updates a key in place, removes case-insensitively,
|
||||||
|
|
|
||||||
|
|
@ -1078,6 +1078,10 @@ impl Engine {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NetEvent::UserAttrs { uid, ident, realhost, gecos } => {
|
||||||
|
self.network.set_user_attrs(&uid, ident, realhost, gecos);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
NetEvent::UserConnect { uid, nick, host, ip } => {
|
NetEvent::UserConnect { uid, nick, host, ip } => {
|
||||||
let arriving_nick = nick.clone();
|
let arriving_nick = nick.clone();
|
||||||
self.network.user_connect(uid.clone(), nick, host, ip.clone());
|
self.network.user_connect(uid.clone(), nick, host, ip.clone());
|
||||||
|
|
@ -1218,13 +1222,17 @@ impl Engine {
|
||||||
}
|
}
|
||||||
// No access: an auto-kick match is banned and kicked, else greeted.
|
// No access: an auto-kick match is banned and kicked, else greeted.
|
||||||
None => {
|
None => {
|
||||||
let nick = self.network.nick_of(&uid).unwrap_or("*").to_string();
|
// Match the akick list against the user's full identity —
|
||||||
let host = self.network.host_of(&uid).unwrap_or("*").to_string();
|
// host and the extbans (account/realname/…) the ircd gave us.
|
||||||
let hostmask = format!("{nick}!*@{host}");
|
let akick = {
|
||||||
let akick = self.db.channel(&channel).and_then(|c| c.akick_match(&hostmask)).map(|k| {
|
let target = self.network.ban_target(&uid);
|
||||||
|
target.as_ref().and_then(|t| {
|
||||||
|
self.db.channel(&channel).and_then(|c| c.akick_match(t)).map(|k| {
|
||||||
let reason = if k.reason.is_empty() { "You are banned from this channel.".to_string() } else { k.reason.clone() };
|
let reason = if k.reason.is_empty() { "You are banned from this channel.".to_string() } else { k.reason.clone() };
|
||||||
(k.mask.clone(), reason)
|
(k.mask.clone(), reason)
|
||||||
});
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
match akick {
|
match akick {
|
||||||
Some((mask, reason)) => {
|
Some((mask, reason)) => {
|
||||||
self.network.channel_part(&channel, &uid);
|
self.network.channel_part(&channel, &uid);
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,11 @@ fn incident_code(seq: u64) -> String {
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub uid: String,
|
pub uid: String,
|
||||||
pub nick: String,
|
pub nick: String,
|
||||||
pub host: String,
|
pub ident: String, // user@ — from the UID (was discarded); empty until UserAttrs
|
||||||
|
pub host: String, // the displayed host
|
||||||
|
pub realhost: String, // the real host, for host bans against it
|
||||||
pub ip: String,
|
pub ip: String,
|
||||||
|
pub gecos: String, // real name, for realname / realmask extbans
|
||||||
}
|
}
|
||||||
|
|
||||||
// A channel's live membership, ops, and current key (+k), tracked from the burst.
|
// A channel's live membership, ops, and current key (+k), tracked from the burst.
|
||||||
|
|
@ -129,7 +132,17 @@ impl Network {
|
||||||
if !ip.is_empty() {
|
if !ip.is_empty() {
|
||||||
*self.sessions.entry(ip.clone()).or_insert(0) += 1;
|
*self.sessions.entry(ip.clone()).or_insert(0) += 1;
|
||||||
}
|
}
|
||||||
self.users.insert(uid.clone(), User { uid, nick, host, ip });
|
self.users.insert(uid.clone(), User { uid, nick, ident: String::new(), host, realhost: String::new(), ip, gecos: String::new() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in the rest of a user's identity (ident, real host, real name), which
|
||||||
|
// the UID carries but UserConnect doesn't — needed for extban matching.
|
||||||
|
pub fn set_user_attrs(&mut self, uid: &str, ident: String, realhost: String, gecos: String) {
|
||||||
|
if let Some(u) = self.users.get_mut(uid) {
|
||||||
|
u.ident = ident;
|
||||||
|
u.realhost = realhost;
|
||||||
|
u.gecos = gecos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirror the engine's declarative config operators, so services can enumerate
|
// Mirror the engine's declarative config operators, so services can enumerate
|
||||||
|
|
@ -202,6 +215,21 @@ impl Network {
|
||||||
self.users.get(uid).map(|u| u.host.as_str())
|
self.users.get(uid).map(|u| u.host.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A user's identity for extban / ban matching (nick, ident, hosts, ip, gecos,
|
||||||
|
// account). `None` if we don't know the uid.
|
||||||
|
pub fn ban_target<'a>(&'a self, uid: &str) -> Option<echo_api::BanTarget<'a>> {
|
||||||
|
let u = self.users.get(uid)?;
|
||||||
|
Some(echo_api::BanTarget {
|
||||||
|
nick: &u.nick,
|
||||||
|
ident: &u.ident,
|
||||||
|
host: &u.host,
|
||||||
|
realhost: &u.realhost,
|
||||||
|
ip: &u.ip,
|
||||||
|
gecos: &u.gecos,
|
||||||
|
account: self.accounts.get(uid).map(String::as_str),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Every known user whose uid carries `sid` as its prefix — i.e. those behind
|
// Every known user whose uid carries `sid` as its prefix — i.e. those behind
|
||||||
// a server, used to forget them all when it splits (SQUIT).
|
// a server, used to forget them all when it splits (SQUIT).
|
||||||
pub fn uids_on_server(&self, sid: &str) -> Vec<String> {
|
pub fn uids_on_server(&self, sid: &str) -> Vec<String> {
|
||||||
|
|
@ -483,6 +511,9 @@ impl NetView for Network {
|
||||||
fn nick_of(&self, uid: &str) -> Option<&str> {
|
fn nick_of(&self, uid: &str) -> Option<&str> {
|
||||||
Network::nick_of(self, uid)
|
Network::nick_of(self, uid)
|
||||||
}
|
}
|
||||||
|
fn ban_target(&self, uid: &str) -> Option<echo_api::BanTarget<'_>> {
|
||||||
|
Network::ban_target(self, uid)
|
||||||
|
}
|
||||||
fn host_of(&self, uid: &str) -> Option<&str> {
|
fn host_of(&self, uid: &str) -> Option<&str> {
|
||||||
Network::host_of(self, uid)
|
Network::host_of(self, uid)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue