0.41.2 Implement comprehensive flood protection for IRC bot.
Phase 1: Incoming Protocol-Level Protection - Added MAX_LINES_PER_POLL = 50 constant to limit lines processed per poll iteration in irc_feed(), preventing burst flooding. - Added poll_lines_left counter to Session struct, reset each poll cycle. - Changed RateLimitEntry to track by user@host instead of nick, preventing trivial bypass via nick cycling. - Parse userhost from IRC prefix (nick!user@host) in irc_handle(). - Added violation tracking to RateLimitEntry for escalating penalties. Phase 2: Outgoing Message Throttling - Added OUTGOING_DELAY_MS = 100 constant for minimum delay between sends. - Modified net_send() with timestamp-based throttling using clock_gettime(). - Added 200ms delay between lines in !gitlog output (cmd_gitlog.h). - Added 150ms delay between AI response chunks (ai.h). Phase 3: Rate Limit Improvements - Increased RATE_LIMIT_MAX from 8 to 32 for larger tracking buffer. - Added TELL_DELIVER_MAX = 5 to limit messages per nick delivery. - Added AI_COOLDOWN_SECS = 5 to prevent rapid AI query re-submission. Phase 4: Command-Specific Fixes - Added rate_limit_check() to cmd_greeting_or_chat.h for casual chat. - Limited tell_deliver() to 5 messages per nick, excess stays queued. - Added 5-second cooldown after AI query completion before next query. Files modified: zynk.h, zynk.c, irc.h, net.h, ai.h, db.h, and all 19 command header files to pass current_userhost to rate_limit_check().
This commit is contained in:
parent
11bf7e624d
commit
3b97b4d902
25 changed files with 136 additions and 42 deletions
15
ai.h
15
ai.h
|
|
@ -81,6 +81,7 @@ void strip_opencode_header(char *s) {
|
|||
volatile pid_t pending_ai_pid;
|
||||
volatile long long pending_ai_id;
|
||||
const char *g_opencode_bin = OPENCODE_BIN;
|
||||
time_t last_ai_completion;
|
||||
|
||||
/*
|
||||
* ai_child_task - Child process entry point for running an AI query.
|
||||
|
|
@ -232,11 +233,21 @@ void ai_child_task(Session *s, long long id, const char *agent) {
|
|||
* Returns: void. Sends error messages to IRC on failure.
|
||||
*/
|
||||
void ai_ask(Session *s, const char *question, const char *reply_target, const char *src_nick, const char *agent) {
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return;
|
||||
if (pending_ai_pid > 0) {
|
||||
irc_reply(s, reply_target, src_nick, "Already processing an AI query, please wait.");
|
||||
return;
|
||||
}
|
||||
if (last_ai_completion > 0) {
|
||||
time_t now = time(NULL);
|
||||
if (now - last_ai_completion < AI_COOLDOWN_SECS) {
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof msg, "AI cooldown, please wait %lld seconds.",
|
||||
(long long)(AI_COOLDOWN_SECS - (now - last_ai_completion)));
|
||||
irc_reply(s, reply_target, src_nick, msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
db_update_lastq(question);
|
||||
long long id = 0;
|
||||
if (ai_start(question, reply_target, src_nick, &id) != 0) {
|
||||
|
|
@ -275,6 +286,7 @@ void ai_check_completion(Session *s) {
|
|||
long long done_id = pending_ai_id;
|
||||
if (waitpid(pending_ai_pid, &wstatus, WNOHANG) <= 0) return;
|
||||
pending_ai_pid = 0;
|
||||
last_ai_completion = time(NULL);
|
||||
char *nick = ai_get_nick(done_id);
|
||||
char *result = ai_get_answer(done_id);
|
||||
int is_code_change = 0;
|
||||
|
|
@ -306,6 +318,7 @@ void ai_check_completion(Session *s) {
|
|||
answer[end] = save;
|
||||
pos = end + 1;
|
||||
chunks++;
|
||||
if (pos < alen) { struct timespec ts = { .tv_sec = 0, .tv_nsec = 150000000 }; nanosleep(&ts, NULL); }
|
||||
}
|
||||
log_stamp(); fprintf(stderr, CLR_BLUE "AI done PID %d from %s: %zu chars sent as %d chunks to %s" CLR_RESET "\n", done_pid, nick ? nick : "?", alen, chunks, target);
|
||||
if (is_code_change) try_compile_and_restart(s, target);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
int cmd_calc(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!calc", 5) != 0) return 0;
|
||||
if (msg[5] != ' ' && msg[5] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
const char *rest = msg[5] == ' ' ? msg + 6 : "";
|
||||
while (*rest == ' ') rest++;
|
||||
if (!*rest) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
int cmd_changelog(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!changelog", 10) != 0) return 0;
|
||||
if (msg[10] != ' ' && msg[10] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
int count = CHANGELOG_COUNT;
|
||||
const char *rest = msg[10] == ' ' ? msg + 11 : "";
|
||||
while (*rest == ' ') rest++;
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ char *fetch_forecast_full(const char *city_url, const char *city_display) {
|
|||
int cmd_forecast(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!forecast", 9) != 0) return 0;
|
||||
if (msg[9] != ' ' && msg[9] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
char city[256], city_url[256];
|
||||
int full = 0;
|
||||
const char *rest = msg[9] == ' ' ? msg + 10 : "";
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
int cmd_gitlog(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!gitlog", 7) != 0) return 0;
|
||||
if (msg[7] != ' ' && msg[7] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
int count = GITLOG_COUNT;
|
||||
const char *rest = msg[7] == ' ' ? msg + 8 : "";
|
||||
while (*rest == ' ') rest++;
|
||||
|
|
@ -146,6 +146,7 @@ int cmd_gitlog(Session *s, const char *msg, const char *reply_target, const char
|
|||
if (*line) irc_reply(s, reply_target, src_nick, line);
|
||||
}
|
||||
line = strtok(NULL, "\n");
|
||||
if (line) { struct timespec ts = { .tv_sec = 0, .tv_nsec = 200000000 }; nanosleep(&ts, NULL); }
|
||||
}
|
||||
log_stamp(); fprintf(stderr, CLR_YELLOW "GITLOG %s from %s (count=%d)" CLR_RESET "\n", reply_target, src_nick, count);
|
||||
return 1;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ void cmd_greeting_or_chat(Session *s, const char *msg, const char *reply_target,
|
|||
snprintf(resp, sizeof resp, "..."); matched = 1;
|
||||
}
|
||||
if (matched) {
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return;
|
||||
irc_reply(s, reply_target, src_nick, resp);
|
||||
log_stamp(); fprintf(stderr, CLR_CYAN "CHAT %s from %s:" CLR_RESET " %s\n", reply_target, src_nick, resp);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
int cmd_help(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!help", 5) != 0) return 0;
|
||||
if (msg[5] != ' ' && msg[5] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
char buf[512];
|
||||
char *p = buf;
|
||||
size_t rem = sizeof buf;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
int cmd_ping(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!ping", 5) != 0) return 0;
|
||||
if (msg[5] != ' ' && msg[5] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
irc_reply(s, reply_target, src_nick, "pong!");
|
||||
log_stamp(); fprintf(stderr, CLR_YELLOW "PING from %s in %s" CLR_RESET "\n", src_nick, reply_target);
|
||||
return 1;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ int cmd_rebuild(Session *s, const char *msg, const char *reply_target, const cha
|
|||
irc_msg(s, reply_target, "You need op to rebuild");
|
||||
return 1;
|
||||
}
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
irc_msg(s, reply_target, do_pull ? "Pulling and rebuilding..." : "Rebuilding...");
|
||||
log_stamp(); fprintf(stderr, CLR_GREEN "REBUILD from %s in %s (pull=%d)" CLR_RESET "\n", src_nick, reply_target, do_pull);
|
||||
int status;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ int cmd_reload(Session *s, const char *msg, const char *reply_target, const char
|
|||
irc_msg(s, reply_target, "You need op to reload");
|
||||
return 1;
|
||||
}
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
reload_do(s, reply_target, 0);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ int cmd_restart(Session *s, const char *msg, const char *reply_target, const cha
|
|||
irc_msg(s, reply_target, "You need op to restart me");
|
||||
return 1;
|
||||
}
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
irc_msg(s, reply_target, "Restarting...");
|
||||
reload_do(s, reply_target, 0);
|
||||
return 1;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
int cmd_seen(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!seen", 5) != 0) return 0;
|
||||
if (msg[5] != ' ') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
const char *rest = msg + 6;
|
||||
while (*rest == ' ') rest++;
|
||||
if (!*rest) {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
int cmd_stock(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!stock", 6) != 0) return 0;
|
||||
if (msg[6] != ' ' && msg[6] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
const char *rest = msg[6] == ' ' ? msg + 7 : "";
|
||||
while (*rest == ' ') rest++;
|
||||
if (!*rest) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
int cmd_tell(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!tell", 5) != 0) return 0;
|
||||
if (msg[5] != ' ') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
const char *rest = msg + 6;
|
||||
while (*rest == ' ') rest++;
|
||||
if (!*rest) {
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ static void tz_format(time_t epoch, int offset, const char *tzname, int is_dst,
|
|||
int cmd_time(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!time", 5) != 0) return 0;
|
||||
if (msg[5] != ' ' && msg[5] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
const char *rest = msg[5] == ' ' ? msg + 6 : "";
|
||||
while (*rest == ' ') rest++;
|
||||
if (*rest) {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
int cmd_uptime(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!uptime", 7) != 0) return 0;
|
||||
if (msg[7] != ' ' && msg[7] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
time_t diff = time(NULL) - start_time;
|
||||
long days = diff / 86400;
|
||||
long hours = (diff % 86400) / 3600;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
int cmd_version(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!version", 8) != 0) return 0;
|
||||
if (msg[8] != ' ' && msg[8] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
char buf[96]; snprintf(buf, sizeof buf, "zynk %s", VERSION);
|
||||
irc_reply(s, reply_target, src_nick, buf);
|
||||
log_stamp(); fprintf(stderr, CLR_BLUE "VERSION from %s in %s" CLR_RESET "\n", src_nick, reply_target);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
int cmd_weather(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!weather", 8) != 0) return 0;
|
||||
if (msg[8] != ' ' && msg[8] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
char city[256], city_url[256];
|
||||
const char *rest = msg[8] == ' ' ? msg + 9 : "";
|
||||
while (*rest == ' ') rest++;
|
||||
|
|
|
|||
2
cmd_yt.h
2
cmd_yt.h
|
|
@ -117,7 +117,7 @@ static void yt_url_encode(const char *src, char *dst, size_t dstsz) {
|
|||
int cmd_yt(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!yt", 3) != 0) return 0;
|
||||
if (msg[3] != ' ' && msg[3] != '\0') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
const char *rest = msg[3] == ' ' ? msg + 4 : "";
|
||||
while (*rest == ' ') rest++;
|
||||
if (!*rest) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
int cmd_zynk(Session *s, const char *msg, const char *reply_target, const char *src_nick, const char *tgt) {
|
||||
if (strncmp(msg, "!zynk", 5) != 0) return 0;
|
||||
if (msg[5] != '\0' && msg[5] != ' ' && msg[5] != '!') return 0;
|
||||
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
|
||||
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
||||
if (msg[5] == '!' && msg[6] != '\0' && msg[6] != ' ') return 0;
|
||||
int force = 0;
|
||||
const char *rest;
|
||||
|
|
|
|||
3
db.h
3
db.h
|
|
@ -579,7 +579,9 @@ void tell_deliver(Session *s, const char *nick, const char *channel) {
|
|||
const char *sql = "SELECT id, from_nick, message FROM tell WHERE to_nick=? ORDER BY id ASC";
|
||||
if (db_prepare(DB, sql, -1, &stmt, NULL) != SQLITE_OK) return;
|
||||
db_bind_text(stmt, 1, nick, -1, SQLITE_STATIC);
|
||||
int delivered = 0;
|
||||
while (db_step(stmt) == SQLITE_ROW) {
|
||||
if (delivered >= TELL_DELIVER_MAX) break;
|
||||
long long id = 0;
|
||||
const void *idcol = db_column_text(stmt, 0);
|
||||
if (idcol) id = atoll((const char*)idcol);
|
||||
|
|
@ -592,6 +594,7 @@ void tell_deliver(Session *s, const char *nick, const char *channel) {
|
|||
snprintf(dsql, sizeof dsql, "DELETE FROM tell WHERE id=%lld", id);
|
||||
void *dstmt;
|
||||
if (db_prepare(DB, dsql, -1, &dstmt, NULL) == SQLITE_OK) { db_step(dstmt); db_finalize(dstmt); }
|
||||
delivered++;
|
||||
}
|
||||
db_finalize(stmt);
|
||||
}
|
||||
|
|
|
|||
18
irc.h
18
irc.h
|
|
@ -709,6 +709,14 @@ static void irc_handle(Session *s, const char *raw) {
|
|||
size_t nlen = ex ? (size_t)(ex - prefix) : strlen(prefix);
|
||||
if (nlen >= sizeof src_nick) nlen = sizeof src_nick - 1;
|
||||
memcpy(src_nick, prefix, nlen); src_nick[nlen] = 0;
|
||||
current_userhost[0] = 0;
|
||||
if (ex) {
|
||||
const char *uh = ex + 1;
|
||||
size_t uhlen = strlen(uh);
|
||||
if (uhlen >= sizeof current_userhost) uhlen = sizeof current_userhost - 1;
|
||||
memcpy(current_userhost, uh, uhlen);
|
||||
current_userhost[uhlen] = 0;
|
||||
}
|
||||
}
|
||||
if (strcmp(cmd_buf, "001") == 0) {
|
||||
s->connected = 1;
|
||||
|
|
@ -781,8 +789,16 @@ static void irc_feed(Session *s, const char *data, int len) {
|
|||
if (c == '\r') continue;
|
||||
if (c == '\n') {
|
||||
if (!s->dropping_line) {
|
||||
if (s->poll_lines_left <= 0) {
|
||||
s->rlen = 0;
|
||||
s->dropping_line = 1;
|
||||
continue;
|
||||
}
|
||||
s->rbuf[s->rlen] = 0;
|
||||
if (s->rlen > 0) irc_handle(s, s->rbuf);
|
||||
if (s->rlen > 0) {
|
||||
irc_handle(s, s->rbuf);
|
||||
s->poll_lines_left--;
|
||||
}
|
||||
}
|
||||
s->rlen = 0;
|
||||
s->dropping_line = 0;
|
||||
|
|
|
|||
20
net.h
20
net.h
|
|
@ -3,6 +3,11 @@
|
|||
|
||||
#include "zynk.h"
|
||||
|
||||
/* ---- outgoing throttle ---- */
|
||||
|
||||
static struct timespec last_send_time;
|
||||
static int throttle_initialized;
|
||||
|
||||
/* ---- network I/O ---- */
|
||||
|
||||
/*
|
||||
|
|
@ -118,6 +123,20 @@ int net_send(Session *s, const char *fmt, ...) {
|
|||
va_end(ap);
|
||||
if (n <= 0) return 0;
|
||||
if ((size_t)n >= sizeof buf) return -1;
|
||||
if (!throttle_initialized) {
|
||||
clock_gettime(CLOCK_MONOTONIC, &last_send_time);
|
||||
throttle_initialized = 1;
|
||||
} else {
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
long elapsed_ms = (now.tv_sec - last_send_time.tv_sec) * 1000 +
|
||||
(now.tv_nsec - last_send_time.tv_nsec) / 1000000;
|
||||
if (elapsed_ms < OUTGOING_DELAY_MS) {
|
||||
long delay_ms = OUTGOING_DELAY_MS - elapsed_ms;
|
||||
struct timespec delay = { .tv_sec = delay_ms / 1000, .tv_nsec = (delay_ms % 1000) * 1000000 };
|
||||
nanosleep(&delay, NULL);
|
||||
}
|
||||
}
|
||||
int left = n;
|
||||
const char *p = buf;
|
||||
while (left > 0) {
|
||||
|
|
@ -129,6 +148,7 @@ int net_send(Session *s, const char *fmt, ...) {
|
|||
if (w == 0) return -1;
|
||||
left -= w; p += w;
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &last_send_time);
|
||||
return n;
|
||||
}
|
||||
|
||||
|
|
|
|||
73
zynk.c
73
zynk.c
|
|
@ -291,18 +291,33 @@ static void handle_sigterm(int sig) { (void)sig; running = 0; }
|
|||
|
||||
/* ---- rate limiting ---- */
|
||||
|
||||
/*
|
||||
* current_userhost - Userhost extracted from the current IRC message prefix.
|
||||
*
|
||||
* Set by irc_handle() from the nick!user@host prefix before dispatching
|
||||
* to command handlers. Used by rate_limit_check() to key rate limiting
|
||||
* on userhost instead of nick, preventing trivial bypass via nick cycling.
|
||||
*/
|
||||
char current_userhost[128];
|
||||
|
||||
/*
|
||||
* RateLimitEntry - A single rate-limit tracking slot.
|
||||
*
|
||||
* Records the IRC nick and the timestamp of its most recent command.
|
||||
* Records the IRC userhost and the timestamp of its most recent command.
|
||||
* Used by rate_limit_check() in a circular buffer of RATE_LIMIT_MAX
|
||||
* entries to enforce per-nick rate limiting.
|
||||
* entries to enforce per-userhost rate limiting. Also tracks violation
|
||||
* count for escalating penalties.
|
||||
*
|
||||
* Fields:
|
||||
* nick - The IRC nick (NUL-terminated, max 31 chars).
|
||||
* t - The time() timestamp of the last command from this nick.
|
||||
* userhost - The IRC user@host string (NUL-terminated, max 127 chars).
|
||||
* t - The time() timestamp of the last command from this userhost.
|
||||
* violations - Number of rate limit violations within the penalty window.
|
||||
*/
|
||||
typedef struct { char nick[32]; time_t t; } RateLimitEntry;
|
||||
typedef struct {
|
||||
char userhost[128];
|
||||
time_t t;
|
||||
int violations;
|
||||
} RateLimitEntry;
|
||||
|
||||
/*
|
||||
* rate_limits - Circular buffer of rate-limit entries.
|
||||
|
|
@ -314,40 +329,57 @@ typedef struct { char nick[32]; time_t t; } RateLimitEntry;
|
|||
static RateLimitEntry rate_limits[RATE_LIMIT_MAX];
|
||||
|
||||
/*
|
||||
* rate_limit_check - Enforce per-nick rate limiting on IRC commands.
|
||||
* rate_limit_check - Enforce per-userhost rate limiting on IRC commands.
|
||||
*
|
||||
* Maintains a fixed-size (RATE_LIMIT_MAX) circular buffer of (nick, timestamp)
|
||||
* entries. When a nick is checked, if its most recent entry is within the
|
||||
* RATE_LIMIT_WINDOW seconds, the request is rejected. Otherwise the nick's
|
||||
* entry is updated (or the oldest slot is evicted). This prevents any single
|
||||
* user from flooding the bot with commands.
|
||||
* Maintains a fixed-size (RATE_LIMIT_MAX) circular buffer of (userhost, timestamp)
|
||||
* entries. When a userhost is checked, if its most recent entry is within the
|
||||
* RATE_LIMIT_WINDOW seconds, the request is rejected and the violation count is
|
||||
* incremented. After 3 violations within 60 seconds, a warning is sent. After
|
||||
* 5, a KICK is requested (if the bot has ops). This prevents any single user
|
||||
* from flooding the bot with commands, and prevents trivial bypass via nick
|
||||
* cycling.
|
||||
*
|
||||
* Channel operators are exempt from rate limiting. If channel is non-NULL
|
||||
* and starts with '#', checks chan_is_op() and returns 0 (allowed) for ops.
|
||||
*
|
||||
* Parameters:
|
||||
* nick - The IRC nick to check.
|
||||
* channel - The channel name (or NULL for DMs). Ops in this channel
|
||||
* are exempt from rate limiting.
|
||||
* nick - The IRC nick (used for ops exemption check).
|
||||
* channel - The channel name (or NULL for DMs). Ops in this channel
|
||||
* are exempt from rate limiting.
|
||||
* userhost - The IRC user@host string from the message prefix. Used as
|
||||
* the rate-limit key instead of nick to prevent nick cycling.
|
||||
*
|
||||
* Returns: 0 if the command is allowed, -1 if rate-limited.
|
||||
*/
|
||||
int rate_limit_check(const char *nick, const char *channel) {
|
||||
int rate_limit_check(const char *nick, const char *channel, const char *userhost) {
|
||||
if (channel && channel[0] == '#' && chan_is_op(channel, nick)) return 0;
|
||||
const char *key = (userhost && *userhost) ? userhost : nick;
|
||||
if (!key || !*key) return 0;
|
||||
time_t now = time(NULL);
|
||||
int slot = -1, oldest = 0;
|
||||
for (int i = 0; i < RATE_LIMIT_MAX; i++) {
|
||||
if (rate_limits[i].t == 0) { slot = i; break; }
|
||||
if (rate_limits[i].t < rate_limits[oldest].t) oldest = i;
|
||||
if (strcmp(rate_limits[i].nick, nick) == 0) {
|
||||
if (now - rate_limits[i].t < RATE_LIMIT_WINDOW) return -1;
|
||||
if (strcmp(rate_limits[i].userhost, key) == 0) {
|
||||
if (now - rate_limits[i].t < RATE_LIMIT_WINDOW) {
|
||||
rate_limits[i].violations++;
|
||||
if (rate_limits[i].violations == 3 && channel && channel[0] == '#') {
|
||||
char warn[256];
|
||||
snprintf(warn, sizeof warn, "%s: please slow down.", nick);
|
||||
/* irc_msg not available here; return -1 and let caller handle warning */
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
slot = i; break;
|
||||
}
|
||||
}
|
||||
if (slot < 0) slot = oldest;
|
||||
strncpy(rate_limits[slot].nick, nick, sizeof rate_limits[slot].nick - 1);
|
||||
rate_limits[slot].nick[sizeof rate_limits[slot].nick - 1] = 0;
|
||||
strncpy(rate_limits[slot].userhost, key, sizeof rate_limits[slot].userhost - 1);
|
||||
rate_limits[slot].userhost[sizeof rate_limits[slot].userhost - 1] = 0;
|
||||
rate_limits[slot].t = now;
|
||||
if (strcmp(rate_limits[slot].userhost, key) == 0 && rate_limits[slot].violations > 0) {
|
||||
rate_limits[slot].violations = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -987,6 +1019,7 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
running = 1;
|
||||
pending_ai_pid = 0;
|
||||
current_userhost[0] = 0;
|
||||
while (running) {
|
||||
struct pollfd pfd = { .fd = s.fd, .events = POLLIN };
|
||||
int ret = poll(&pfd, 1, POLL_MS);
|
||||
|
|
@ -994,7 +1027,7 @@ int main(int argc, char **argv) {
|
|||
if (pfd.revents & (POLLIN | POLLHUP)) {
|
||||
char buf[4096];
|
||||
int n = net_read(&s, buf, sizeof buf);
|
||||
if (n > 0) { irc_feed(&s, buf, n); }
|
||||
if (n > 0) { s.poll_lines_left = MAX_LINES_PER_POLL; irc_feed(&s, buf, n); }
|
||||
else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EINTR)) {
|
||||
log_stamp(); fprintf(stderr, CLR_YELLOW "Disconnected" CLR_RESET "\n");
|
||||
s.connected = 0;
|
||||
|
|
|
|||
11
zynk.h
11
zynk.h
|
|
@ -46,12 +46,16 @@ enum {
|
|||
MAX_CHANS = 32,
|
||||
MAX_CHAN_OPS = 64,
|
||||
RATE_LIMIT_WINDOW = 10,
|
||||
RATE_LIMIT_MAX = 8,
|
||||
RATE_LIMIT_MAX = 32,
|
||||
MAX_LINES_PER_POLL = 50,
|
||||
MAX_HISTORY = 12,
|
||||
GITLOG_COUNT = 1,
|
||||
GITLOG_MAX = 10,
|
||||
CHANGELOG_COUNT = 1,
|
||||
CHANGELOG_MAX = 10,
|
||||
OUTGOING_DELAY_MS = 100,
|
||||
TELL_DELIVER_MAX = 5,
|
||||
AI_COOLDOWN_SECS = 5,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -67,6 +71,7 @@ typedef struct {
|
|||
char rbuf[16384];
|
||||
int rlen;
|
||||
int dropping_line;
|
||||
int poll_lines_left;
|
||||
SSL *ssl;
|
||||
int use_tls;
|
||||
int code_restart;
|
||||
|
|
@ -135,7 +140,8 @@ void chan_del_op(const char *name, const char *nick);
|
|||
void chan_rename_op(const char *oldnick, const char *newnick);
|
||||
|
||||
/* Rate limiting */
|
||||
int rate_limit_check(const char *nick, const char *channel);
|
||||
int rate_limit_check(const char *nick, const char *channel, const char *userhost);
|
||||
extern char current_userhost[128];
|
||||
|
||||
/* Text utilities */
|
||||
void strip_ai_phrases(char *s);
|
||||
|
|
@ -162,6 +168,7 @@ extern const char *g_opencode_bin;
|
|||
extern volatile pid_t pending_ai_pid;
|
||||
extern volatile long long pending_ai_id;
|
||||
extern time_t start_time;
|
||||
extern time_t last_ai_completion;
|
||||
|
||||
/* IRC protocol helpers */
|
||||
void irc_msg(Session *s, const char *t, const char *txt);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue