config: make flood/dnsbl/multiline/chathistory/extjwt limits + nick/chan/watch/monitor/silence/whowas/timeouts configurable (no hardcoded options)
This commit is contained in:
parent
1969218d3f
commit
61584a545b
13 changed files with 141 additions and 66 deletions
|
|
@ -121,3 +121,41 @@ amu_target = both
|
||||||
# G:<cc> ban extban (e.g. +b G:CN,RU), the oper GEOIP <nick|ip> command and
|
# G:<cc> ban extban (e.g. +b G:CN,RU), the oper GEOIP <nick|ip> command and
|
||||||
# a country line in WHOIS (opers). Point at a GeoLite2-Country.mmdb file:
|
# a country line in WHOIS (opers). Point at a GeoLite2-Country.mmdb file:
|
||||||
# geoip_database = /etc/echoircd/GeoLite2-Country.mmdb
|
# geoip_database = /etc/echoircd/GeoLite2-Country.mmdb
|
||||||
|
|
||||||
|
# --- tunable limits & timeouts (every one shown with its built-in default; set a
|
||||||
|
# line only to override it). None of these are hardcoded — all read at runtime.
|
||||||
|
|
||||||
|
# flood: allow this many messages per this many seconds before dropping (opers exempt)
|
||||||
|
# flood_messages = 8
|
||||||
|
# flood_seconds = 4
|
||||||
|
|
||||||
|
# dnsbl: ban length in seconds when dnsbl_action is kline/gline/zline (default 1 day)
|
||||||
|
# dnsbl_duration = 86400
|
||||||
|
|
||||||
|
# draft/multiline: the max-bytes / max-lines advertised in the cap AND enforced
|
||||||
|
# multiline_maxbytes = 4096
|
||||||
|
# multiline_maxlines = 24
|
||||||
|
|
||||||
|
# CHATHISTORY: messages kept per conversation (also the ceiling a client may request)
|
||||||
|
# chathistory_limit = 256
|
||||||
|
|
||||||
|
# EXTJWT: token line-chunk size (bytes) when splitting a long token across lines
|
||||||
|
# extjwt_chunk = 200
|
||||||
|
|
||||||
|
# per-user list sizes (advertised in ISUPPORT WATCH/MONITOR/SILENCE where applicable)
|
||||||
|
# maxwatch = 128
|
||||||
|
# maxmonitor = 128
|
||||||
|
# maxsilence = 32
|
||||||
|
# maxaccept = 64
|
||||||
|
|
||||||
|
# WHOWAS: number of historical nick records retained
|
||||||
|
# whowas_maxentries = 256
|
||||||
|
|
||||||
|
# nick / channel name length limits (advertised as NICKLEN / CHANNELLEN)
|
||||||
|
# maxnick = 30
|
||||||
|
# maxchannel = 50
|
||||||
|
|
||||||
|
# connection timeouts, in seconds
|
||||||
|
# registration_timeout = 60 # drop clients that never register (NICK+USER) in time
|
||||||
|
# ping_frequency = 90 # send a PING after this much idle time
|
||||||
|
# ping_timeout = 60 # then drop if no PONG within this much longer
|
||||||
|
|
|
||||||
|
|
@ -501,7 +501,7 @@ impl Server {
|
||||||
/// Join a user to a channel (creating it if new, giving the creator +o),
|
/// Join a user to a channel (creating it if new, giving the creator +o),
|
||||||
/// then broadcast JOIN and send TOPIC + NAMES. Queues the join hook.
|
/// then broadcast JOIN and send TOPIC + NAMES. Queues the join hook.
|
||||||
pub fn join(&mut self, uid: Uid, name: &str, key_arg: Option<&str>) {
|
pub fn join(&mut self, uid: Uid, name: &str, key_arg: Option<&str>) {
|
||||||
if !valid_chan(name) {
|
if !valid_chan(name, self.conf_num("maxchannel", 50usize)) {
|
||||||
self.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{name} :No such channel"));
|
self.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{name} :No such channel"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1076,10 +1076,10 @@ impl Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A channel name starts with `#`, is ≤ 50 chars, and has no space/comma/control.
|
/// A channel name starts with `#`, is ≤ 50 chars, and has no space/comma/control.
|
||||||
pub fn valid_chan(name: &str) -> bool {
|
pub fn valid_chan(name: &str, maxlen: usize) -> bool {
|
||||||
name.starts_with('#')
|
name.starts_with('#')
|
||||||
&& name.len() > 1
|
&& name.len() > 1
|
||||||
&& name.len() <= 50
|
&& name.len() <= maxlen
|
||||||
&& !name
|
&& !name
|
||||||
.chars()
|
.chars()
|
||||||
.any(|c| c == ' ' || c == ',' || (c as u32) < 0x20)
|
.any(|c| c == ' ' || c == ',' || (c as u32) < 0x20)
|
||||||
|
|
|
||||||
|
|
@ -391,7 +391,7 @@ impl Command for SaNick {
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
};
|
};
|
||||||
let newnick = ¶ms[1];
|
let newnick = ¶ms[1];
|
||||||
if !valid_nick(newnick) {
|
if !valid_nick(newnick, s.conf_num("maxnick", 30usize)) {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
ERR_ERRONEUSNICKNAME,
|
ERR_ERRONEUSNICKNAME,
|
||||||
|
|
@ -441,7 +441,7 @@ impl Command for SvsNick {
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
};
|
};
|
||||||
let newnick = ¶ms[1];
|
let newnick = ¶ms[1];
|
||||||
if !valid_nick(newnick) {
|
if !valid_nick(newnick, s.conf_num("maxnick", 30usize)) {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
ERR_ERRONEUSNICKNAME,
|
ERR_ERRONEUSNICKNAME,
|
||||||
|
|
@ -761,7 +761,7 @@ impl Command for NickLock {
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
};
|
};
|
||||||
let newnick = ¶ms[1];
|
let newnick = ¶ms[1];
|
||||||
if !valid_nick(newnick) {
|
if !valid_nick(newnick, s.conf_num("maxnick", 30usize)) {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
ERR_ERRONEUSNICKNAME,
|
ERR_ERRONEUSNICKNAME,
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,13 @@ impl Command for Cap {
|
||||||
format!(
|
format!(
|
||||||
":{} CAP {who} LS :{}",
|
":{} CAP {who} LS :{}",
|
||||||
s.name,
|
s.name,
|
||||||
Caps::ls_line(cap302, secure, &acctreg)
|
Caps::ls_line(
|
||||||
|
cap302,
|
||||||
|
secure,
|
||||||
|
&acctreg,
|
||||||
|
crate::modules::multiline::max_bytes(s),
|
||||||
|
crate::modules::multiline::max_lines(s),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -393,7 +399,7 @@ impl Command for Nick {
|
||||||
s.numeric(uid, ERR_NONICKNAMEGIVEN, ":No nickname given");
|
s.numeric(uid, ERR_NONICKNAMEGIVEN, ":No nickname given");
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
};
|
};
|
||||||
if !valid_nick(newnick) {
|
if !valid_nick(newnick, s.conf_num("maxnick", 30usize)) {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
ERR_ERRONEUSNICKNAME,
|
ERR_ERRONEUSNICKNAME,
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,11 @@ fn watch_add(s: &mut Server, uid: Uid, nick: &str) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let low = nick.to_ascii_lowercase();
|
let low = nick.to_ascii_lowercase();
|
||||||
|
let maxwatch = s.conf_num("maxwatch", WATCH_MAX);
|
||||||
let full = s
|
let full = s
|
||||||
.users
|
.users
|
||||||
.get(&uid)
|
.get(&uid)
|
||||||
.map(|u| u.watch.len() >= WATCH_MAX && !u.watch.contains(&low))
|
.map(|u| u.watch.len() >= maxwatch && !u.watch.contains(&low))
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
if full {
|
if full {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
|
|
@ -185,16 +186,17 @@ impl Command for Monitor {
|
||||||
let mut added = Vec::new();
|
let mut added = Vec::new();
|
||||||
for t in targets {
|
for t in targets {
|
||||||
let low = t.to_ascii_lowercase();
|
let low = t.to_ascii_lowercase();
|
||||||
|
let maxmon = s.conf_num("maxmonitor", MONITOR_MAX);
|
||||||
let full = s
|
let full = s
|
||||||
.users
|
.users
|
||||||
.get(&uid)
|
.get(&uid)
|
||||||
.map(|u| u.monitor.len() >= MONITOR_MAX && !u.monitor.contains(&low))
|
.map(|u| u.monitor.len() >= maxmon && !u.monitor.contains(&low))
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
if full {
|
if full {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
ERR_MONLISTFULL,
|
ERR_MONLISTFULL,
|
||||||
&format!("{MONITOR_MAX} {t} :Monitor list is full"),
|
&format!("{maxmon} {t} :Monitor list is full"),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -278,10 +280,11 @@ impl Command for Silence {
|
||||||
let prefix = s.users.get(&uid).map(|u| u.prefix()).unwrap_or_default();
|
let prefix = s.users.get(&uid).map(|u| u.prefix()).unwrap_or_default();
|
||||||
if let Some(m) = arg.strip_prefix('+') {
|
if let Some(m) = arg.strip_prefix('+') {
|
||||||
let mask = normalize_mask(m);
|
let mask = normalize_mask(m);
|
||||||
|
let maxsil = s.conf_num("maxsilence", SILENCE_MAX);
|
||||||
let full = s
|
let full = s
|
||||||
.users
|
.users
|
||||||
.get(&uid)
|
.get(&uid)
|
||||||
.map(|u| u.silence.len() >= SILENCE_MAX && !u.silence.contains(&mask))
|
.map(|u| u.silence.len() >= maxsil && !u.silence.contains(&mask))
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
if full {
|
if full {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
|
|
@ -353,10 +356,11 @@ impl Command for Accept {
|
||||||
}
|
}
|
||||||
let low = name.to_ascii_lowercase();
|
let low = name.to_ascii_lowercase();
|
||||||
if adding {
|
if adding {
|
||||||
|
let maxacc = s.conf_num("maxaccept", ACCEPT_MAX);
|
||||||
let (full, exists) = s
|
let (full, exists) = s
|
||||||
.users
|
.users
|
||||||
.get(&uid)
|
.get(&uid)
|
||||||
.map(|u| (u.accept.len() >= ACCEPT_MAX, u.accept.contains(&low)))
|
.map(|u| (u.accept.len() >= maxacc, u.accept.contains(&low)))
|
||||||
.unwrap_or((true, false));
|
.unwrap_or((true, false));
|
||||||
if exists {
|
if exists {
|
||||||
s.numeric(
|
s.numeric(
|
||||||
|
|
|
||||||
|
|
@ -433,7 +433,7 @@ impl Server {
|
||||||
self.forward_to_target(target, msg, from);
|
self.forward_to_target(target, msg, from);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !valid_nick(newnick)
|
if !valid_nick(newnick, self.conf_num("maxnick", 30usize))
|
||||||
|| self.find_nick(newnick).is_some()
|
|| self.find_nick(newnick).is_some()
|
||||||
|| self.remote_nick.contains_key(&newnick.to_ascii_lowercase())
|
|| self.remote_nick.contains_key(&newnick.to_ascii_lowercase())
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,15 @@ use crate::command::{CmdResult, Command};
|
||||||
use crate::server::{iso_time, now, parse_iso, Server};
|
use crate::server::{iso_time, now, parse_iso, Server};
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
/// Recent messages CHATHISTORY keeps per conversation.
|
/// Default number of messages CHATHISTORY keeps per conversation, if
|
||||||
|
/// `chathistory_limit` is unset. Also the ceiling a client can request.
|
||||||
pub const HISTORY_CAP: usize = 256;
|
pub const HISTORY_CAP: usize = 256;
|
||||||
|
|
||||||
|
/// The configured per-conversation history size (overridable via `chathistory_limit`).
|
||||||
|
pub fn limit(s: &Server) -> usize {
|
||||||
|
s.conf_num("chathistory_limit", HISTORY_CAP).clamp(1, 100_000)
|
||||||
|
}
|
||||||
|
|
||||||
/// One stored message, replayed by CHATHISTORY / the `+H` backlog.
|
/// One stored message, replayed by CHATHISTORY / the `+H` backlog.
|
||||||
pub struct HistMsg {
|
pub struct HistMsg {
|
||||||
pub ts: u64,
|
pub ts: u64,
|
||||||
|
|
@ -40,6 +46,7 @@ pub fn record(
|
||||||
text: &str,
|
text: &str,
|
||||||
msgid: &str,
|
msgid: &str,
|
||||||
) {
|
) {
|
||||||
|
let cap = limit(s);
|
||||||
let buf = s
|
let buf = s
|
||||||
.ext
|
.ext
|
||||||
.get_or_insert_with::<History>(History::default)
|
.get_or_insert_with::<History>(History::default)
|
||||||
|
|
@ -54,7 +61,7 @@ pub fn record(
|
||||||
target: target.to_string(),
|
target: target.to_string(),
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
});
|
});
|
||||||
while buf.len() > HISTORY_CAP {
|
while buf.len() > cap {
|
||||||
buf.pop_front();
|
buf.pop_front();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +112,7 @@ impl Command for ChatHistory {
|
||||||
.get(3)
|
.get(3)
|
||||||
.and_then(|l| l.parse::<usize>().ok())
|
.and_then(|l| l.parse::<usize>().ok())
|
||||||
.unwrap_or(50)
|
.unwrap_or(50)
|
||||||
.clamp(1, HISTORY_CAP);
|
.clamp(1, limit(s));
|
||||||
let me = s
|
let me = s
|
||||||
.users
|
.users
|
||||||
.get(&uid)
|
.get(&uid)
|
||||||
|
|
@ -186,7 +193,7 @@ impl Command for ChatHistory {
|
||||||
let limit = limit_s
|
let limit = limit_s
|
||||||
.and_then(|l| l.parse::<usize>().ok())
|
.and_then(|l| l.parse::<usize>().ok())
|
||||||
.unwrap_or(50)
|
.unwrap_or(50)
|
||||||
.clamp(1, HISTORY_CAP);
|
.clamp(1, limit(s));
|
||||||
|
|
||||||
let bref = s.next_msgid().replace('-', "");
|
let bref = s.next_msgid().replace('-', "");
|
||||||
let mut lines: Vec<String> = Vec::new();
|
let mut lines: Vec<String> = Vec::new();
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use crate::xline::XKind;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
/// Ban length applied by the `*line` actions on a hit.
|
/// Ban length applied by the `*line` actions on a hit.
|
||||||
const DNSBL_BAN: u64 = 86_400; // 1 day
|
const DNSBL_BAN: u64 = 86_400; // default ban length (1 day) if `dnsbl_duration` unset
|
||||||
|
|
||||||
/// Outcome of a DNSBL check for one connecting client.
|
/// Outcome of a DNSBL check for one connecting client.
|
||||||
pub enum Outcome {
|
pub enum Outcome {
|
||||||
|
|
@ -84,22 +84,11 @@ fn act(s: &mut Server, uid: Uid, zone: &str, reply: Ipv4Addr) {
|
||||||
));
|
));
|
||||||
let reason = format!("{} (listed on {zone})", s.dnsbl_reason);
|
let reason = format!("{} (listed on {zone})", s.dnsbl_reason);
|
||||||
let ipstr = ip.to_string();
|
let ipstr = ip.to_string();
|
||||||
|
let dur = s.conf_num("dnsbl_duration", DNSBL_BAN);
|
||||||
match action.as_str() {
|
match action.as_str() {
|
||||||
"kline" => s.add_xline(
|
"kline" => s.add_xline(XKind::Kline, &format!("*@{ipstr}"), dur, "dnsbl", &reason),
|
||||||
XKind::Kline,
|
"gline" => s.add_xline(XKind::Gline, &format!("*@{ipstr}"), dur, "dnsbl", &reason),
|
||||||
&format!("*@{ipstr}"),
|
"zline" => s.add_xline(XKind::Zline, &ipstr, dur, "dnsbl", &reason),
|
||||||
DNSBL_BAN,
|
|
||||||
"dnsbl",
|
|
||||||
&reason,
|
|
||||||
),
|
|
||||||
"gline" => s.add_xline(
|
|
||||||
XKind::Gline,
|
|
||||||
&format!("*@{ipstr}"),
|
|
||||||
DNSBL_BAN,
|
|
||||||
"dnsbl",
|
|
||||||
&reason,
|
|
||||||
),
|
|
||||||
"zline" => s.add_xline(XKind::Zline, &ipstr, DNSBL_BAN, "dnsbl", &reason),
|
|
||||||
"kill" | "reject" => {}
|
"kill" | "reject" => {}
|
||||||
_ => return, // "mark" or unknown: notify only, don't disconnect
|
_ => return, // "mark" or unknown: notify only, don't disconnect
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use crate::server::{now, Server};
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
/// Longest token chunk per EXTJWT line (keeps the whole line well under 512).
|
/// Longest token chunk per EXTJWT line (keeps the whole line well under 512).
|
||||||
const CHUNK: usize = 200;
|
const CHUNK: usize = 200; // default token chunk size if `extjwt_chunk` unset
|
||||||
|
|
||||||
/// Resolve `(secret, duration)` for a service name (`*` = the default service).
|
/// Resolve `(secret, duration)` for a service name (`*` = the default service).
|
||||||
fn service(s: &Server, name: &str) -> Option<(String, u64)> {
|
fn service(s: &Server, name: &str) -> Option<(String, u64)> {
|
||||||
|
|
@ -133,10 +133,11 @@ impl Command for ExtJwt {
|
||||||
};
|
};
|
||||||
|
|
||||||
// send the token, chunked, with a `*` continuation marker on all but the last
|
// send the token, chunked, with a `*` continuation marker on all but the last
|
||||||
|
let chunk_sz = s.conf_num("extjwt_chunk", CHUNK).max(1);
|
||||||
let bytes = token.as_bytes();
|
let bytes = token.as_bytes();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < bytes.len() {
|
while i < bytes.len() {
|
||||||
let end = (i + CHUNK).min(bytes.len());
|
let end = (i + chunk_sz).min(bytes.len());
|
||||||
let chunk = &token[i..end];
|
let chunk = &token[i..end];
|
||||||
let more = end < bytes.len();
|
let more = end < bytes.len();
|
||||||
let line = if more {
|
let line = if more {
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ use crate::module::{ModResult, Module};
|
||||||
use crate::server::{now, Server};
|
use crate::server::{now, Server};
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
const FLOOD_MAX: usize = 8; // messages allowed…
|
// Defaults if unset in the config (`flood_messages` / `flood_seconds`): this many
|
||||||
const FLOOD_WINDOW: u64 = 4; // …within this many seconds
|
// messages allowed within this many seconds.
|
||||||
|
const FLOOD_MAX: usize = 8;
|
||||||
|
const FLOOD_WINDOW: u64 = 4;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FloodState {
|
struct FloodState {
|
||||||
|
|
@ -34,6 +36,8 @@ impl Module for Flood {
|
||||||
_text: &str,
|
_text: &str,
|
||||||
) -> ModResult {
|
) -> ModResult {
|
||||||
let now = now();
|
let now = now();
|
||||||
|
let max = srv.conf_num("flood_messages", FLOOD_MAX);
|
||||||
|
let window = srv.conf_num("flood_seconds", FLOOD_WINDOW);
|
||||||
let (over, warn) = {
|
let (over, warn) = {
|
||||||
let Some(u) = srv.users.get_mut(&uid) else {
|
let Some(u) = srv.users.get_mut(&uid) else {
|
||||||
return ModResult::Passthru;
|
return ModResult::Passthru;
|
||||||
|
|
@ -42,9 +46,9 @@ impl Module for Flood {
|
||||||
return ModResult::Passthru; // opers bypass flood limits
|
return ModResult::Passthru; // opers bypass flood limits
|
||||||
}
|
}
|
||||||
let st = u.ext.get_or_insert_with(FloodState::default);
|
let st = u.ext.get_or_insert_with(FloodState::default);
|
||||||
st.times.retain(|&t| now.saturating_sub(t) < FLOOD_WINDOW);
|
st.times.retain(|&t| now.saturating_sub(t) < window);
|
||||||
st.times.push(now);
|
st.times.push(now);
|
||||||
let over = st.times.len() > FLOOD_MAX;
|
let over = st.times.len() > max;
|
||||||
let warn = over && !st.warned; // notice once per burst
|
let warn = over && !st.warned; // notice once per burst
|
||||||
st.warned = over;
|
st.warned = over;
|
||||||
(over, warn)
|
(over, warn)
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,20 @@ use crate::module::Module;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
/// Limits advertised in the `draft/multiline` cap and enforced while buffering.
|
/// Default limits (overridable via `multiline_maxbytes` / `multiline_maxlines`),
|
||||||
|
/// advertised in the `draft/multiline` cap and enforced while buffering.
|
||||||
pub const MAX_BYTES: usize = 4096;
|
pub const MAX_BYTES: usize = 4096;
|
||||||
pub const MAX_LINES: usize = 24;
|
pub const MAX_LINES: usize = 24;
|
||||||
|
|
||||||
|
/// The configured maximum total bytes of one multiline batch.
|
||||||
|
pub fn max_bytes(s: &Server) -> usize {
|
||||||
|
s.conf_num("multiline_maxbytes", MAX_BYTES)
|
||||||
|
}
|
||||||
|
/// The configured maximum number of lines in one multiline batch.
|
||||||
|
pub fn max_lines(s: &Server) -> usize {
|
||||||
|
s.conf_num("multiline_maxlines", MAX_LINES)
|
||||||
|
}
|
||||||
|
|
||||||
/// An in-progress inbound multiline batch — one long client message being
|
/// An in-progress inbound multiline batch — one long client message being
|
||||||
/// assembled from several `@batch=`-tagged PRIVMSG/NOTICE lines.
|
/// assembled from several `@batch=`-tagged PRIVMSG/NOTICE lines.
|
||||||
pub struct MlineBatch {
|
pub struct MlineBatch {
|
||||||
|
|
@ -58,9 +68,10 @@ pub fn accumulate(
|
||||||
text: &str,
|
text: &str,
|
||||||
concat: bool,
|
concat: bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
let (max_lines, max_bytes) = (max_lines(s), max_bytes(s));
|
||||||
match s.ext.get_mut::<Mline>().and_then(|m| m.0.get_mut(&uid)) {
|
match s.ext.get_mut::<Mline>().and_then(|m| m.0.get_mut(&uid)) {
|
||||||
Some(mb) if mb.bref == bref => {
|
Some(mb) if mb.bref == bref => {
|
||||||
if mb.parts.len() < MAX_LINES && mb.bytes + text.len() <= MAX_BYTES {
|
if mb.parts.len() < max_lines && mb.bytes + text.len() <= max_bytes {
|
||||||
mb.notice = notice;
|
mb.notice = notice;
|
||||||
mb.bytes += text.len();
|
mb.bytes += text.len();
|
||||||
mb.parts.push((text.to_string(), concat));
|
mb.parts.push((text.to_string(), concat));
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,8 @@ impl Server {
|
||||||
account,
|
account,
|
||||||
ts: now(),
|
ts: now(),
|
||||||
});
|
});
|
||||||
while self.whowas.len() > 256 {
|
let cap = self.conf_num("whowas_maxentries", 256usize);
|
||||||
|
while self.whowas.len() > cap {
|
||||||
self.whowas.pop_back();
|
self.whowas.pop_back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -584,8 +585,15 @@ impl Server {
|
||||||
/// without the trailing `:are supported by this server`. Shared by the welcome
|
/// without the trailing `:are supported by this server`. Shared by the welcome
|
||||||
/// burst and the `ISUPPORT` command (draft/extended-isupport).
|
/// burst and the `ISUPPORT` command (draft/extended-isupport).
|
||||||
pub fn isupport_lines(&self) -> Vec<String> {
|
pub fn isupport_lines(&self) -> Vec<String> {
|
||||||
|
// advertised limits mirror the (config-driven) values actually enforced
|
||||||
|
let maxwatch = self.conf_num("maxwatch", crate::watch::WATCH_MAX);
|
||||||
|
let maxmon = self.conf_num("maxmonitor", crate::watch::MONITOR_MAX);
|
||||||
|
let maxsil = self.conf_num("maxsilence", crate::watch::SILENCE_MAX);
|
||||||
|
let chathist = crate::modules::chathistory::limit(self);
|
||||||
|
let maxnick = self.conf_num("maxnick", 30usize);
|
||||||
|
let maxchan = self.conf_num("maxchannel", 50usize);
|
||||||
let mut lines = vec![format!(
|
let mut lines = vec![format!(
|
||||||
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgX,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,Gcgjmnrsy WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={}",
|
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgX,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,Gcgjmnrsy WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}",
|
||||||
self.network
|
self.network
|
||||||
)];
|
)];
|
||||||
if let Some(tok) = crate::modules::network_icon::isupport(self) {
|
if let Some(tok) = crate::modules::network_icon::isupport(self) {
|
||||||
|
|
@ -980,19 +988,22 @@ impl Server {
|
||||||
/// Decide which connections to PING and which to drop, given `now`.
|
/// Decide which connections to PING and which to drop, given `now`.
|
||||||
/// Returns `(to_ping, to_quit)`. Pure over the state, so it's unit-testable.
|
/// Returns `(to_ping, to_quit)`. Pure over the state, so it's unit-testable.
|
||||||
pub fn idle_check(&self, now: u64) -> (Vec<Uid>, Vec<Uid>) {
|
pub fn idle_check(&self, now: u64) -> (Vec<Uid>, Vec<Uid>) {
|
||||||
|
let reg_timeout = self.conf_num("registration_timeout", REG_TIMEOUT);
|
||||||
|
let ping_after = self.conf_num("ping_frequency", PING_AFTER);
|
||||||
|
let ping_timeout = self.conf_num("ping_timeout", PING_TIMEOUT);
|
||||||
let mut ping = Vec::new();
|
let mut ping = Vec::new();
|
||||||
let mut quit = Vec::new();
|
let mut quit = Vec::new();
|
||||||
for (&uid, u) in &self.users {
|
for (&uid, u) in &self.users {
|
||||||
let idle = now.saturating_sub(u.last_active);
|
let idle = now.saturating_sub(u.last_active);
|
||||||
if !u.registered {
|
if !u.registered {
|
||||||
if idle >= REG_TIMEOUT {
|
if idle >= reg_timeout {
|
||||||
quit.push(uid); // never registered in time
|
quit.push(uid); // never registered in time
|
||||||
}
|
}
|
||||||
} else if u.ping_sent {
|
} else if u.ping_sent {
|
||||||
if idle >= PING_AFTER + PING_TIMEOUT {
|
if idle >= ping_after + ping_timeout {
|
||||||
quit.push(uid); // no reply to our PING
|
quit.push(uid); // no reply to our PING
|
||||||
}
|
}
|
||||||
} else if idle >= PING_AFTER {
|
} else if idle >= ping_after {
|
||||||
ping.push(uid); // idle — poke it
|
ping.push(uid); // idle — poke it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1180,13 +1191,13 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nick_and_chan_validation() {
|
fn nick_and_chan_validation() {
|
||||||
assert!(valid_nick("reverse"));
|
assert!(valid_nick("reverse", 30));
|
||||||
assert!(valid_nick("[abc]`"));
|
assert!(valid_nick("[abc]`", 30));
|
||||||
assert!(!valid_nick("1abc")); // can't start with a digit
|
assert!(!valid_nick("1abc", 30)); // can't start with a digit
|
||||||
assert!(!valid_nick(""));
|
assert!(!valid_nick("", 30));
|
||||||
assert!(valid_chan("#argentina"));
|
assert!(valid_chan("#argentina", 50));
|
||||||
assert!(!valid_chan("argentina"));
|
assert!(!valid_chan("argentina", 50));
|
||||||
assert!(!valid_chan("#a b"));
|
assert!(!valid_chan("#a b", 50));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
28
src/users.rs
28
src/users.rs
|
|
@ -7,7 +7,6 @@ use std::net::{SocketAddr, TcpStream};
|
||||||
|
|
||||||
use crate::extensible::Extensible;
|
use crate::extensible::Extensible;
|
||||||
use crate::module::Hook;
|
use crate::module::Hook;
|
||||||
use crate::modules::multiline::{MAX_BYTES as MLINE_MAX_BYTES, MAX_LINES as MLINE_MAX_LINES};
|
|
||||||
use crate::numeric::*;
|
use crate::numeric::*;
|
||||||
use crate::server::{Server, VERSION};
|
use crate::server::{Server, VERSION};
|
||||||
use crate::socketengine::OutSink;
|
use crate::socketengine::OutSink;
|
||||||
|
|
@ -162,7 +161,14 @@ impl Caps {
|
||||||
|
|
||||||
/// The `CAP LS` token list; `sasl` carries its mechanisms for 302 clients.
|
/// The `CAP LS` token list; `sasl` carries its mechanisms for 302 clients.
|
||||||
/// EXTERNAL is only offered on TLS connections (it needs a client cert).
|
/// EXTERNAL is only offered on TLS connections (it needs a client cert).
|
||||||
pub fn ls_line(cap302: bool, secure: bool, acctreg: &str) -> String {
|
/// `mline_bytes`/`mline_lines` are the (config-driven) draft/multiline limits.
|
||||||
|
pub fn ls_line(
|
||||||
|
cap302: bool,
|
||||||
|
secure: bool,
|
||||||
|
acctreg: &str,
|
||||||
|
mline_bytes: usize,
|
||||||
|
mline_lines: usize,
|
||||||
|
) -> String {
|
||||||
SUPPORTED_CAPS
|
SUPPORTED_CAPS
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
|
|
@ -173,9 +179,7 @@ impl Caps {
|
||||||
"sasl=PLAIN".to_string()
|
"sasl=PLAIN".to_string()
|
||||||
}
|
}
|
||||||
} else if *c == "draft/multiline" && cap302 {
|
} else if *c == "draft/multiline" && cap302 {
|
||||||
format!(
|
format!("draft/multiline=max-bytes={mline_bytes},max-lines={mline_lines}")
|
||||||
"draft/multiline=max-bytes={MLINE_MAX_BYTES},max-lines={MLINE_MAX_LINES}"
|
|
||||||
)
|
|
||||||
} else if *c == "draft/account-registration" && cap302 && !acctreg.is_empty() {
|
} else if *c == "draft/account-registration" && cap302 && !acctreg.is_empty() {
|
||||||
format!("draft/account-registration={acctreg}")
|
format!("draft/account-registration={acctreg}")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -537,14 +541,14 @@ pub fn valid_ident(i: &str) -> bool {
|
||||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
|
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn valid_nick(n: &str) -> bool {
|
pub fn valid_nick(n: &str, maxlen: usize) -> bool {
|
||||||
let special = |c: char| "[]\\`_^{}|".contains(c);
|
let special = |c: char| "[]\\`_^{}|".contains(c);
|
||||||
let mut chars = n.chars();
|
let mut chars = n.chars();
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
Some(c) if c.is_ascii_alphabetic() || special(c) => {}
|
Some(c) if c.is_ascii_alphabetic() || special(c) => {}
|
||||||
_ => return false,
|
_ => return false,
|
||||||
}
|
}
|
||||||
n.len() <= 30
|
n.len() <= maxlen
|
||||||
&& n.chars()
|
&& n.chars()
|
||||||
.all(|c| c.is_ascii_alphanumeric() || special(c) || c == '-')
|
.all(|c| c.is_ascii_alphanumeric() || special(c) || c == '-')
|
||||||
}
|
}
|
||||||
|
|
@ -575,12 +579,12 @@ mod tests {
|
||||||
assert!(!c.set("bogus-cap", true)); // unknown cap rejected
|
assert!(!c.set("bogus-cap", true)); // unknown cap rejected
|
||||||
assert!(c.has("server-time") && c.has("multi-prefix") && !c.has("sasl"));
|
assert!(c.has("server-time") && c.has("multi-prefix") && !c.has("sasl"));
|
||||||
assert_eq!(c.enabled(), "server-time multi-prefix"); // SUPPORTED order
|
assert_eq!(c.enabled(), "server-time multi-prefix"); // SUPPORTED order
|
||||||
assert!(Caps::ls_line(true, false, "").contains("sasl=PLAIN")); // 302 shows mechs
|
assert!(Caps::ls_line(true, false, "", 4096, 24).contains("sasl=PLAIN")); // 302 shows mechs
|
||||||
assert!(!Caps::ls_line(true, false, "").contains("EXTERNAL")); // plaintext: no EXTERNAL
|
assert!(!Caps::ls_line(true, false, "", 4096, 24).contains("EXTERNAL")); // plaintext: no EXTERNAL
|
||||||
assert!(Caps::ls_line(true, true, "").contains("sasl=PLAIN,EXTERNAL")); // TLS offers it
|
assert!(Caps::ls_line(true, true, "", 4096, 24).contains("sasl=PLAIN,EXTERNAL")); // TLS offers it
|
||||||
assert!(
|
assert!(
|
||||||
Caps::ls_line(false, false, "").contains("sasl")
|
Caps::ls_line(false, false, "", 4096, 24).contains("sasl")
|
||||||
&& !Caps::ls_line(false, false, "").contains("sasl=")
|
&& !Caps::ls_line(false, false, "", 4096, 24).contains("sasl=")
|
||||||
);
|
);
|
||||||
c.set("server-time", false);
|
c.set("server-time", false);
|
||||||
assert!(!c.has("server-time"));
|
assert!(!c.has("server-time"));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue