130 lines
4.2 KiB
Rust
130 lines
4.2 KiB
Rust
//! Hold back the `/LIST` command until a user has been connected for a while, which
|
|
//! defeats spambots that connect, `LIST`, spam every channel and leave. Non-exempt
|
|
//! users who `LIST` too early get an optional notice and a throwaway *fake* channel
|
|
//! list (so a bot waiting on the reply is satisfied), then the real `LIST` is denied.
|
|
//! Exempt: opers, logged-in accounts (when `securelist_exemptregistered`), and hosts
|
|
//! matching a `securelist_exception` glob. Off unless `securelist = yes`.
|
|
|
|
use openssl::rand::rand_bytes;
|
|
|
|
use crate::channels::glob_match;
|
|
use crate::module::{ModResult, Module};
|
|
use crate::numeric::{RPL_LIST, RPL_LISTEND, RPL_LISTSTART};
|
|
use crate::server::{now, Server};
|
|
use crate::Uid;
|
|
|
|
/// A small unsigned int from the CSPRNG in `0..bound` (bound>0), else 0.
|
|
fn rand_below(bound: u32) -> u32 {
|
|
if bound == 0 {
|
|
return 0;
|
|
}
|
|
let mut b = [0u8; 4];
|
|
if rand_bytes(&mut b).is_err() {
|
|
return 0;
|
|
}
|
|
u32::from_le_bytes(b) % bound
|
|
}
|
|
|
|
/// A random lowercase-alnum string of `len` chars (for a fake channel suffix).
|
|
fn rand_name(len: usize) -> String {
|
|
const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
|
|
let mut buf = vec![0u8; len];
|
|
if rand_bytes(&mut buf).is_err() {
|
|
return "channel".to_string();
|
|
}
|
|
buf.iter()
|
|
.map(|&b| ALPHABET[b as usize % ALPHABET.len()] as char)
|
|
.collect()
|
|
}
|
|
|
|
/// Is `uid` exempt from the LIST hold?
|
|
fn is_exempt(s: &Server, uid: Uid) -> bool {
|
|
if s.is_oper(uid) {
|
|
return true;
|
|
}
|
|
if s.conf_bool("securelist_exemptregistered", true) && s.is_logged_in(uid) {
|
|
return true;
|
|
}
|
|
let exceptions = s.conf_all("securelist_exception");
|
|
if exceptions.is_empty() {
|
|
return false;
|
|
}
|
|
let Some(u) = s.users.get(&uid) else {
|
|
return false;
|
|
};
|
|
let forms = [
|
|
format!("{}@{}", u.ident, u.host),
|
|
format!("{}@{}", u.ident, u.addr.ip()),
|
|
];
|
|
exceptions
|
|
.iter()
|
|
.any(|mask| forms.iter().any(|f| glob_match(mask, f)))
|
|
}
|
|
|
|
pub struct SecureList;
|
|
|
|
impl Module for SecureList {
|
|
fn name(&self) -> &'static str {
|
|
"securelist"
|
|
}
|
|
|
|
fn on_pre_command(
|
|
&mut self,
|
|
srv: &mut Server,
|
|
uid: Uid,
|
|
cmd: &str,
|
|
_params: &[String],
|
|
) -> ModResult {
|
|
if !srv.conf_bool("securelist", false) || !cmd.eq_ignore_ascii_case("LIST") {
|
|
return ModResult::Passthru;
|
|
}
|
|
if is_exempt(srv, uid) {
|
|
return ModResult::Passthru;
|
|
}
|
|
let waittime = srv.conf_num("securelist_waittime", 60u64);
|
|
let signon = srv.users.get(&uid).map(|u| u.signon).unwrap_or(0);
|
|
let elapsed = now().saturating_sub(signon);
|
|
if waittime > 0 && elapsed >= waittime {
|
|
return ModResult::Passthru;
|
|
}
|
|
|
|
// tell them to wait
|
|
if srv.conf_bool("securelist_showmsg", true) {
|
|
let remain = waittime.saturating_sub(elapsed);
|
|
let nick = srv
|
|
.users
|
|
.get(&uid)
|
|
.map(|u| u.nick.clone())
|
|
.unwrap_or_default();
|
|
srv.send(
|
|
uid,
|
|
format!(
|
|
":{} NOTICE {nick} :*** You cannot view the channel list yet. \
|
|
Please try again in {remain} seconds (or log in to an account).",
|
|
srv.name
|
|
),
|
|
);
|
|
}
|
|
|
|
// throwaway fake list so a bot waiting on the reply is satisfied
|
|
let fakechans = srv.conf_num("securelist_fakechans", 5u32);
|
|
let prefix = srv
|
|
.conf("securelist_fakechanprefix")
|
|
.unwrap_or("#")
|
|
.to_string();
|
|
let topic = srv
|
|
.conf("securelist_fakechantopic")
|
|
.unwrap_or("Fake channel for confusing spambots")
|
|
.to_string();
|
|
let usercount = srv.users.len().max(1) as u32;
|
|
|
|
srv.numeric(uid, RPL_LISTSTART, "Channel :Users Name");
|
|
for _ in 0..fakechans {
|
|
let suffix = rand_name((rand_below(8) + 3) as usize);
|
|
let count = rand_below(usercount) + 1;
|
|
srv.numeric(uid, RPL_LIST, &format!("{prefix}{suffix} {count} :{topic}"));
|
|
}
|
|
srv.numeric(uid, RPL_LISTEND, ":End of channel list.");
|
|
ModResult::Deny
|
|
}
|
|
}
|