channels/whois: validate +k/+l, cap and case-fold ban lists, honour -n for externals, hide +s/+p/+i from whois/who/names, 301 in whois, conf-key the join/nickflood lockout

This commit is contained in:
Jean Chevronnet 2026-08-16 17:56:24 +00:00
parent 4028cd3c84
commit 579c321670
5 changed files with 90 additions and 25 deletions

View file

@ -224,13 +224,28 @@ impl Command for Whois {
};
let chans: Vec<String> = keys
.iter()
.filter_map(|k| s.channels.get(k).map(|c| c.name.clone()))
.filter_map(|k| s.channels.get(k))
.filter(|c| {
// a +s/+p channel is shown only to the target itself, an oper, or a
// fellow member — never leaked to an outside asker.
is_self
|| asker_oper
|| (!c.modes.secret && !c.modes.private)
|| c.members.contains_key(&uid)
})
.map(|c| {
let pfx = c.members.get(&tuid).map(|m| m.prefix_char()).unwrap_or("");
format!("{pfx}{}", c.name)
})
.collect();
s.numeric(
uid,
RPL_WHOISUSER,
&format!("{nick} {ident} {disp} * :{realname}"),
);
if let Some(away) = s.users.get(&tuid).and_then(|u| u.flags.away.clone()) {
s.numeric(uid, RPL_AWAY, &format!("{nick} :{away}"));
}
if bot {
s.numeric(uid, RPL_WHOISBOT, &format!("{nick} :is a bot"));
}
@ -434,7 +449,20 @@ impl Command for Who {
None => Vec::new(),
}
} else if let Some(tuid) = s.find_nick(target) {
vec![(tuid, "*".to_string(), String::new())]
// hide a +i (invisible) user from a WHO by someone who shares no channel
// with them (self and opers always see them).
let hidden = s.users.get(&tuid).map(|u| u.flags.invisible).unwrap_or(false)
&& tuid != uid
&& !asker_oper
&& !s
.channels
.values()
.any(|c| c.members.contains_key(&uid) && c.members.contains_key(&tuid));
if hidden {
Vec::new()
} else {
vec![(tuid, "*".to_string(), String::new())]
}
} else {
Vec::new()
};

View file

@ -161,20 +161,29 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool)
};
if target.starts_with('#') {
let key = target.to_ascii_lowercase();
let member = s
let (member, no_external) = s
.channels
.get(&key)
.map(|c| c.members.contains_key(&uid))
.unwrap_or(false);
.map(|c| (c.members.contains_key(&uid), c.modes.no_external))
.unwrap_or((false, true));
if !member {
if !notice {
s.numeric(
uid,
ERR_CANNOTSENDTOCHAN,
&format!("{target} :Cannot send to channel"),
);
// +n (default): only members may message the channel. With -n an external
// user may — unless banned (+b, not +e-excepted), so -n can't evade a ban.
let banned = s
.channels
.get(&key)
.map(|c| s.ban_list_hit(uid, &c.bans) && !s.ban_list_hit(uid, &c.excepts))
.unwrap_or(true);
if no_external || banned {
if !notice {
s.numeric(
uid,
ERR_CANNOTSENDTOCHAN,
&format!("{target} :Cannot send to channel"),
);
}
return CmdResult::Fail;
}
return CmdResult::Fail;
}
// +U opmoderated — an unprivileged user's message isn't blocked; it's routed
// to channel ops only (below). It also overrides +m's block for that purpose.