Secure file ops, TLS hardening, input validation, security docs. (0.40.7)
- Added secure_state_open(): hardened file open with O_CLOEXEC, O_NOFOLLOW, mode 0600, regular-file and ownership checks. Prevents symlink attacks, TOCTOU races, and world-writable temp files. - Replaced all fopen() calls for temp state files with secure_state_open() + fdopen() across hot reload, compile-restart, and rebuild paths. - Hardened TLS initialization: enforces minimum TLS 1.2, disables compression, checks SSL_CTX_set_default_verify_paths() return value. - CMake compiler/linker hardening: -Wformat=2, -Wstrict-prototypes, -fstack-protector-strong, _FORTIFY_SOURCE=3, RELRO, noexecstack. - Added !calc input validation: max 256 chars, numeric-only characters. - Added !stock symbol validation: alphanumerics plus .-^= only. - Fixed !quit, !reload, !restart to require channel context (DMs rejected). - Fixed log timestamp format: %y (2-digit) to %Y (4-digit year). - Fixed asprintf return value check in !forecast error paths. - Fixed !reload fork-in-place mode: only saves TLS state when exec_new=1. - Cleaned up reload state file on failure and in fork child. - Fixed git SSH-to-HTTPS URL conversion in !gitlog and !changelog. - AI plan mode no longer passes --dangerously-skip-permissions. - Added IRC buffer overflow protection: oversized lines are dropped. - Added doc comments to ZYNK_RELOAD_FILE, secure_state_open(), RateLimitEntry, and rate_limits.
This commit is contained in:
parent
3b863d581d
commit
72f7812630
16 changed files with 187 additions and 38 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -1,3 +1,21 @@
|
|||
2026.07.24 - Secure file ops, TLS hardening, input validation, security docs. (0.40.7)
|
||||
|
||||
- Added `secure_state_open()`: hardened file open with O_CLOEXEC, O_NOFOLLOW, mode 0600, regular-file and ownership checks. Prevents symlink attacks, TOCTOU races, and world-writable temp files.
|
||||
- Replaced all `fopen()` calls for temp state files (`/tmp/zynk_reload`, `/tmp/zynk_code_restart`, `/tmp/zynk_make_err`, `/tmp/zynk_rebuild_err`) with `secure_state_open()` + `fdopen()` across hot reload, compile-restart, and rebuild paths.
|
||||
- Hardened TLS initialization: enforces minimum TLS 1.2 (`SSL_CTX_set_min_proto_version`), disables compression (`SSL_OP_NO_COMPRESSION`), and checks the return value of `SSL_CTX_set_default_verify_paths()` instead of ignoring failures.
|
||||
- CMake compiler/linker hardening: added `-Wformat=2 -Wstrict-prototypes -fstack-protector-strong`, `_FORTIFY_SOURCE=3` for release builds, and RELRO + noexecstack linker flags.
|
||||
- Added `!calc` input validation: rejects expressions longer than 256 chars and disallows non-numeric characters (only digits, `.`, `()`, `+-*/^%`, and whitespace are permitted).
|
||||
- Added `!stock` symbol validation: rejects symbols containing characters other than alphanumerics, `.`, `-`, `^`, or `=`.
|
||||
- Fixed `!quit`, `!reload`, `!restart` to require channel context — these commands now reject DM usage with a clear error instead of silently passing the op check.
|
||||
- Fixed log timestamp format: `%y` (2-digit year) → `%Y` (4-digit year).
|
||||
- Fixed `asprintf` return value check in `!forecast` error paths (avoids using a potentially NULL error string).
|
||||
- Fixed `!reload` fork-in-place mode: only saves TLS session state when `exec_new=1`, since fork-in-place inherits the live socket.
|
||||
- Cleaned up the reload state file on failure and in fork child for fork-in-place mode to avoid stale files.
|
||||
- Fixed git SSH→HTTPS URL conversion in `!gitlog` and `!changelog`: `host:path` now correctly becomes `host/path` instead of `hostpath`.
|
||||
- AI plan mode (`!ai` without explicit agent) no longer passes `--dangerously-skip-permissions`.
|
||||
- Added IRC buffer overflow protection: lines exceeding `RBUF_SZ` are dropped instead of truncating.
|
||||
- Added doc comments to `ZYNK_RELOAD_FILE`, `secure_state_open()`, `RateLimitEntry`, and `rate_limits`.
|
||||
|
||||
2026.07.24 - Strip trailing space from wttr.in weather descriptions. (0.40.6)
|
||||
|
||||
- Fixed trailing space in weather descriptions from wttr.in (e.g. `"Partly Cloudy "` → `"Partly Cloudy"`), which caused a stray space before the closing parenthesis in `!forecast full` output (e.g. `(Partly Cloudy )` instead of `(Partly Cloudy)`). Applied the same fix to the non-full `cond` field used by `!forecast` and `!weather`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
project(zynk VERSION 0.40.6 LANGUAGES C)
|
||||
project(zynk VERSION 0.40.7 LANGUAGES C)
|
||||
|
||||
# Prefer C99; the code is compatible with C99/C11
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
|
|
@ -31,16 +31,6 @@ option(WITH_RELOAD "Hot-reload command (ops only)" ON)
|
|||
option(WITH_RESTART "Restart command (ops only)" ON)
|
||||
option(WITH_GREETING_OR_CHAT "Greeting/chat with AI fallback" ON)
|
||||
|
||||
# Map WITH_* options to HAS_CMD_* compile definitions
|
||||
#foreach(_feat ZYNK PING VERSION HELP QUIT WEATHER FORECAST STOCK
|
||||
# CALC TIME SEEN TELL GITLOG CHANGELOG AI CODE
|
||||
# REBUILD UPTIME RELOAD RESTART GREETING_OR_CHAT)
|
||||
# string(TOLOWER "${_feat}" _feat_lower)
|
||||
# if(WITH_${_feat})
|
||||
# list(APPEND _cmd_defs "HAS_CMD_${_feat}")
|
||||
# endif()
|
||||
#endforeach()
|
||||
|
||||
# Map WITH_* options to HAS_CMD_* compile definitions and print feature status
|
||||
message(STATUS "=============================================================================")
|
||||
message(STATUS "Feature configuration for zynk (${PROJECT_VERSION}):")
|
||||
|
|
@ -71,7 +61,11 @@ add_executable(zynk zynk.c)
|
|||
|
||||
if(BUILD_STRICT)
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU")
|
||||
target_compile_options(zynk PRIVATE -Wall -Wextra -pedantic)
|
||||
target_compile_options(zynk PRIVATE -Wall -Wextra -Wformat=2
|
||||
-Wstrict-prototypes -pedantic -fstack-protector-strong)
|
||||
target_compile_definitions(zynk PRIVATE
|
||||
$<$<OR:$<CONFIG:Release>,$<CONFIG:RelWithDebInfo>,$<CONFIG:MinSizeRel>>:_FORTIFY_SOURCE=3>)
|
||||
target_link_options(zynk PRIVATE -Wl,-z,relro,-z,now -Wl,-z,noexecstack)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
|
|||
2
ai.h
2
ai.h
|
|
@ -168,7 +168,7 @@ void ai_child_task(Session *s, long long id, const char *agent) {
|
|||
if (agent)
|
||||
execl(g_opencode_bin, "opencode", "run", "--dangerously-skip-permissions", "--agent", agent, combined, (char *)NULL);
|
||||
else
|
||||
execl(g_opencode_bin, "opencode", "run", "--dangerously-skip-permissions", "--agent", "plan", combined, (char *)NULL);
|
||||
execl(g_opencode_bin, "opencode", "run", "--agent", "plan", combined, (char *)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
close(ai_pipe[1]);
|
||||
|
|
|
|||
22
cmd_calc.h
22
cmd_calc.h
|
|
@ -29,6 +29,17 @@ int cmd_calc(Session *s, const char *msg, const char *reply_target, const char *
|
|||
irc_msg(s, reply_target, "Usage: !calc <expression> (e.g. !calc 2+2, !calc (3*7)+1)");
|
||||
return 1;
|
||||
}
|
||||
size_t expr_len = strlen(rest);
|
||||
if (expr_len > 256) {
|
||||
irc_msg(s, reply_target, "Calc: expression too long");
|
||||
return 1;
|
||||
}
|
||||
for (const unsigned char *p = (const unsigned char *)rest; *p; p++) {
|
||||
if (!isdigit(*p) && !strchr(".()+-*/%^ \t", *p)) {
|
||||
irc_msg(s, reply_target, "Calc: only numeric expressions are allowed");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
int in_pipe[2], out_pipe[2];
|
||||
if (pipe(in_pipe) < 0 || pipe(out_pipe) < 0) { irc_msg(s, reply_target, "Calc: internal error"); return 1; }
|
||||
pid_t pid = fork();
|
||||
|
|
@ -46,9 +57,14 @@ int cmd_calc(Session *s, const char *msg, const char *reply_target, const char *
|
|||
close(in_pipe[0]);
|
||||
close(out_pipe[1]);
|
||||
const char *pidef = "define pi() { return 3.14159265358979323844; }\n";
|
||||
write(in_pipe[1], pidef, strlen(pidef));
|
||||
write(in_pipe[1], rest, strlen(rest));
|
||||
write(in_pipe[1], "\n", 1);
|
||||
if (dprintf(in_pipe[1], "%s%s\n", pidef, rest) < 0) {
|
||||
close(in_pipe[1]);
|
||||
close(out_pipe[0]);
|
||||
kill(pid, SIGTERM);
|
||||
waitpid(pid, NULL, 0);
|
||||
irc_msg(s, reply_target, "Calc: evaluation failed");
|
||||
return 1;
|
||||
}
|
||||
close(in_pipe[1]);
|
||||
char buf[512], tmp[256];
|
||||
size_t len = 0;
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ int cmd_changelog(Session *s, const char *msg, const char *reply_target, const c
|
|||
char *path = strchr(host, ':');
|
||||
if (path) {
|
||||
char tmp[512];
|
||||
snprintf(tmp, sizeof tmp, "https://%s%s", host, path);
|
||||
snprintf(tmp, sizeof tmp, "https://%.*s/%s", (int)(path - host), host, path + 1);
|
||||
strncpy(remote_url, tmp, sizeof remote_url - 1);
|
||||
remote_url[sizeof remote_url - 1] = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ char *fetch_forecast(const char *city_url, const char *city_display) {
|
|||
char *err = NULL;
|
||||
size_t jlen = strlen(json);
|
||||
if (jlen > 400) jlen = 400;
|
||||
asprintf(&err, "Forecast parse error (response: %.*s)", (int)jlen, json);
|
||||
if (asprintf(&err, "Forecast parse error (response: %.*s)", (int)jlen, json) < 0) err = NULL;
|
||||
free(json);
|
||||
return err;
|
||||
}
|
||||
|
|
@ -230,7 +230,7 @@ char *fetch_forecast_full(const char *city_url, const char *city_display) {
|
|||
char *err = NULL;
|
||||
size_t jlen = strlen(json);
|
||||
if (jlen > 400) jlen = 400;
|
||||
asprintf(&err, "Forecast parse error (response: %.*s)", (int)jlen, json);
|
||||
if (asprintf(&err, "Forecast parse error (response: %.*s)", (int)jlen, json) < 0) err = NULL;
|
||||
free(json);
|
||||
return err;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ int cmd_gitlog(Session *s, const char *msg, const char *reply_target, const char
|
|||
char *path = strchr(host, ':');
|
||||
if (path) {
|
||||
char tmp[512];
|
||||
snprintf(tmp, sizeof tmp, "https://%s%s", host, path);
|
||||
snprintf(tmp, sizeof tmp, "https://%.*s/%s", (int)(path - host), host, path + 1);
|
||||
strncpy(remote_url, tmp, sizeof remote_url - 1);
|
||||
remote_url[sizeof remote_url - 1] = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@
|
|||
*/
|
||||
int cmd_quit(Session *s, const char *msg, const char *reply_target, const char *src_nick, const char *tgt) {
|
||||
if (strcmp(msg, "!quit") != 0 && strcmp(msg, "!die") != 0) return 0;
|
||||
if (tgt[0] == '#' && !chan_is_op(tgt, src_nick)) {
|
||||
if (tgt[0] != '#') {
|
||||
irc_msg(s, reply_target, "!quit can only be used by an op in a channel");
|
||||
return 1;
|
||||
}
|
||||
if (!chan_is_op(tgt, src_nick)) {
|
||||
irc_msg(s, reply_target, "You need op to shut me down");
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,18 @@ int cmd_rebuild(Session *s, const char *msg, const char *reply_target, const cha
|
|||
log_stamp(); fprintf(stderr, CLR_GREEN "REBUILD from %s in %s (pull=%d)" CLR_RESET "\n", src_nick, reply_target, do_pull);
|
||||
int status;
|
||||
char errbuf[512];
|
||||
int efd = secure_state_open("/tmp/zynk_rebuild_err", O_WRONLY | O_CREAT | O_TRUNC);
|
||||
if (efd < 0) {
|
||||
irc_msg(s, reply_target, "Rebuild failed: cannot create secure build log");
|
||||
return 1;
|
||||
}
|
||||
close(efd);
|
||||
if (do_pull) {
|
||||
status = system("git pull 2>/tmp/zynk_rebuild_err");
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
char msg_buf[640];
|
||||
FILE *f = fopen("/tmp/zynk_rebuild_err", "r");
|
||||
efd = secure_state_open("/tmp/zynk_rebuild_err", O_RDONLY);
|
||||
FILE *f = efd >= 0 ? fdopen(efd, "r") : NULL;
|
||||
if (f) { size_t n = fread(errbuf, 1, sizeof errbuf - 1, f); errbuf[n] = 0; fclose(f); char *nl = strchr(errbuf, '\n'); if (nl) *nl = 0; }
|
||||
else snprintf(errbuf, sizeof errbuf, "unknown error");
|
||||
snprintf(msg_buf, sizeof msg_buf, "Rebuild failed (git pull): %s", errbuf);
|
||||
|
|
@ -53,7 +60,8 @@ int cmd_rebuild(Session *s, const char *msg, const char *reply_target, const cha
|
|||
status = system("make clean 2>/tmp/zynk_rebuild_err");
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
char msg_buf[640];
|
||||
FILE *f = fopen("/tmp/zynk_rebuild_err", "r");
|
||||
efd = secure_state_open("/tmp/zynk_rebuild_err", O_RDONLY);
|
||||
FILE *f = efd >= 0 ? fdopen(efd, "r") : NULL;
|
||||
if (f) { size_t n = fread(errbuf, 1, sizeof errbuf - 1, f); errbuf[n] = 0; fclose(f); char *nl = strchr(errbuf, '\n'); if (nl) *nl = 0; }
|
||||
else snprintf(errbuf, sizeof errbuf, "unknown error");
|
||||
snprintf(msg_buf, sizeof msg_buf, "Rebuild failed (make clean): %s", errbuf);
|
||||
|
|
@ -65,11 +73,13 @@ int cmd_rebuild(Session *s, const char *msg, const char *reply_target, const cha
|
|||
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
|
||||
irc_msg(s, reply_target, "Build successful, restarting...");
|
||||
log_stamp(); fprintf(stderr, CLR_GREEN "REBUILD successful, restarting..." CLR_RESET "\n");
|
||||
FILE *cf = fopen("/tmp/zynk_code_restart", "w");
|
||||
int cfd = secure_state_open("/tmp/zynk_code_restart", O_WRONLY | O_CREAT | O_TRUNC);
|
||||
FILE *cf = cfd >= 0 ? fdopen(cfd, "w") : NULL;
|
||||
if (cf) { fprintf(cf, "%s\n", reply_target); fclose(cf); }
|
||||
reload_do(s, reply_target, 1);
|
||||
} else {
|
||||
FILE *f = fopen("/tmp/zynk_rebuild_err", "r");
|
||||
efd = secure_state_open("/tmp/zynk_rebuild_err", O_RDONLY);
|
||||
FILE *f = efd >= 0 ? fdopen(efd, "r") : NULL;
|
||||
if (f) { size_t n = fread(errbuf, 1, sizeof errbuf - 1, f); errbuf[n] = 0; fclose(f); char *nl = strchr(errbuf, '\n'); if (nl) *nl = 0; }
|
||||
else snprintf(errbuf, sizeof errbuf, "unknown error");
|
||||
char msg_buf[640];
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@
|
|||
*/
|
||||
int cmd_reload(Session *s, const char *msg, const char *reply_target, const char *src_nick, const char *tgt) {
|
||||
if (strcmp(msg, "!reload") != 0) return 0;
|
||||
if (tgt[0] == '#' && !chan_is_op(tgt, src_nick)) {
|
||||
if (tgt[0] != '#') {
|
||||
irc_msg(s, reply_target, "!reload can only be used by an op in a channel");
|
||||
return 1;
|
||||
}
|
||||
if (!chan_is_op(tgt, src_nick)) {
|
||||
irc_msg(s, reply_target, "You need op to reload");
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@
|
|||
*/
|
||||
int cmd_restart(Session *s, const char *msg, const char *reply_target, const char *src_nick, const char *tgt) {
|
||||
if (strcmp(msg, "!restart") != 0) return 0;
|
||||
if (tgt[0] == '#' && !chan_is_op(tgt, src_nick)) {
|
||||
if (tgt[0] != '#') {
|
||||
irc_msg(s, reply_target, "!restart can only be used by an op in a channel");
|
||||
return 1;
|
||||
}
|
||||
if (!chan_is_op(tgt, src_nick)) {
|
||||
irc_msg(s, reply_target, "You need op to restart me");
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,9 +41,17 @@ int cmd_stock(Session *s, const char *msg, const char *reply_target, const char
|
|||
int i = 0;
|
||||
while (*p && *p != ' ' && *p != ',' && i < 15) symbols[count][i++] = toupper((unsigned char)*p++);
|
||||
symbols[count][i] = 0;
|
||||
for (int j = 0; symbols[count][j]; j++) {
|
||||
unsigned char c = (unsigned char)symbols[count][j];
|
||||
if (!isalnum(c) && c != '.' && c != '-' && c != '^' && c != '=') {
|
||||
symbols[count][0] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
count++;
|
||||
}
|
||||
for (int si = 0; si < count; si++) {
|
||||
if (!symbols[si][0]) continue;
|
||||
char url[512];
|
||||
snprintf(url, sizeof url, "https://query1.finance.yahoo.com/v8/finance/chart/%s?interval=1d&range=1d", symbols[si]);
|
||||
char *json = fetch_url(url);
|
||||
|
|
|
|||
14
irc.h
14
irc.h
|
|
@ -731,11 +731,19 @@ static void irc_feed(Session *s, const char *data, int len) {
|
|||
char c = data[i];
|
||||
if (c == '\r') continue;
|
||||
if (c == '\n') {
|
||||
s->rbuf[s->rlen] = 0;
|
||||
if (s->rlen > 0) irc_handle(s, s->rbuf);
|
||||
if (!s->dropping_line) {
|
||||
s->rbuf[s->rlen] = 0;
|
||||
if (s->rlen > 0) irc_handle(s, s->rbuf);
|
||||
}
|
||||
s->rlen = 0;
|
||||
s->dropping_line = 0;
|
||||
} else {
|
||||
if (s->rlen < RBUF_SZ - 1) s->rbuf[s->rlen++] = c;
|
||||
if (!s->dropping_line && s->rlen < RBUF_SZ - 1)
|
||||
s->rbuf[s->rlen++] = c;
|
||||
else if (s->rlen >= RBUF_SZ - 1) {
|
||||
s->rlen = 0;
|
||||
s->dropping_line = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
tls.h
12
tls.h
|
|
@ -25,7 +25,17 @@ static int tls_init(void) {
|
|||
SSL_load_error_strings();
|
||||
tls_ctx = SSL_CTX_new(TLS_client_method());
|
||||
if (!tls_ctx) return -1;
|
||||
SSL_CTX_set_default_verify_paths(tls_ctx);
|
||||
if (SSL_CTX_set_min_proto_version(tls_ctx, TLS1_2_VERSION) != 1) {
|
||||
SSL_CTX_free(tls_ctx);
|
||||
tls_ctx = NULL;
|
||||
return -1;
|
||||
}
|
||||
SSL_CTX_set_options(tls_ctx, SSL_OP_NO_COMPRESSION);
|
||||
if (tls_verify && SSL_CTX_set_default_verify_paths(tls_ctx) != 1) {
|
||||
SSL_CTX_free(tls_ctx);
|
||||
tls_ctx = NULL;
|
||||
return -1;
|
||||
}
|
||||
SSL_CTX_set_verify(tls_ctx, tls_verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
84
zynk.c
84
zynk.c
|
|
@ -28,14 +28,52 @@ void log_stamp(void) {
|
|||
time_t t = time(NULL);
|
||||
tm = localtime(&t);
|
||||
char buf[64];
|
||||
strftime(buf, sizeof buf, "%y-%m-%d %H:%M:%S", tm);
|
||||
strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S", tm);
|
||||
fprintf(stderr, CLR_DIM "[%s]" CLR_RESET " ", buf);
|
||||
}
|
||||
|
||||
/* ---- hot reload ---- */
|
||||
|
||||
/*
|
||||
* ZYNK_RELOAD_FILE - Temporary file used to persist TLS session state
|
||||
* across hot reloads. Written by reload_save() with the full SSL session
|
||||
* (hex-encoded) and all connection parameters (host, port, nick, etc.),
|
||||
* then read back by reload_try_restore() so the new process can resume
|
||||
* the IRC connection without a visible disconnect. The file is created
|
||||
* with mode 0600 via secure_state_open() and unlinked after use.
|
||||
*/
|
||||
#define ZYNK_RELOAD_FILE "/tmp/zynk_reload"
|
||||
|
||||
/*
|
||||
* secure_state_open - Open a file descriptor with security hardening.
|
||||
*
|
||||
* Opens the given path with O_CLOEXEC and O_NOFOLLOW appended to the
|
||||
* caller-supplied flags, and mode 0600 (owner read/write only). After
|
||||
* opening, verifies the file is a regular file (S_ISREG) owned by the
|
||||
* effective user (geteuid). If O_CREAT is among the flags, fchmod is
|
||||
* called to enforce S_IRUSR | S_IWUSR. This prevents TOCTOU races,
|
||||
* symlink attacks, and world-writable state files.
|
||||
*
|
||||
* Parameters:
|
||||
* path - The filesystem path to open.
|
||||
* flags - open() flags (O_CLOEXEC and O_NOFOLLOW are added implicitly).
|
||||
*
|
||||
* Returns: A valid file descriptor on success, or -1 on failure (with
|
||||
* errno set to EPERM for ownership/type checks).
|
||||
*/
|
||||
int secure_state_open(const char *path, int flags) {
|
||||
int fd = open(path, flags | O_CLOEXEC | O_NOFOLLOW, 0600);
|
||||
if (fd < 0) return -1;
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode) || st.st_uid != geteuid()) {
|
||||
close(fd);
|
||||
errno = EPERM;
|
||||
return -1;
|
||||
}
|
||||
if (flags & O_CREAT) fchmod(fd, S_IRUSR | S_IWUSR);
|
||||
return fd;
|
||||
}
|
||||
|
||||
/*
|
||||
* reload_read_line - Read a single line from a file, stripping newlines.
|
||||
*
|
||||
|
|
@ -81,7 +119,8 @@ static int reload_save(Session *s) {
|
|||
unsigned char *p = sbuf;
|
||||
i2d_SSL_SESSION(sess, &p);
|
||||
SSL_SESSION_free(sess);
|
||||
FILE *f = fopen(ZYNK_RELOAD_FILE, "w");
|
||||
int fd = secure_state_open(ZYNK_RELOAD_FILE, O_WRONLY | O_CREAT | O_TRUNC);
|
||||
FILE *f = fd >= 0 ? fdopen(fd, "w") : NULL;
|
||||
if (!f) { free(sbuf); return -1; }
|
||||
fprintf(f, "%d\n%s\n%d\n%s\n%s\n%s\n%s\n%s\n%d\n%d\n",
|
||||
slen, s->host, s->port, s->nick, s->user, s->real,
|
||||
|
|
@ -106,7 +145,8 @@ static int reload_save(Session *s) {
|
|||
* Returns: 1 when settings were restored, -1 if no reload file or on error.
|
||||
*/
|
||||
static int reload_try_restore(Session *s) {
|
||||
FILE *f = fopen(ZYNK_RELOAD_FILE, "r");
|
||||
int fd = secure_state_open(ZYNK_RELOAD_FILE, O_RDONLY);
|
||||
FILE *f = fd >= 0 ? fdopen(fd, "r") : NULL;
|
||||
if (!f) return -1;
|
||||
char line[1024];
|
||||
int slen = 0, port = 0, use_tls_f = 0, tls_verify_f = 0;
|
||||
|
|
@ -144,7 +184,8 @@ static int reload_try_restore(Session *s) {
|
|||
s->use_tls = use_tls_f;
|
||||
tls_verify = tls_verify_f;
|
||||
free(sbuf);
|
||||
FILE *cf = fopen("/tmp/zynk_code_restart", "r");
|
||||
int cfd = secure_state_open("/tmp/zynk_code_restart", O_RDONLY);
|
||||
FILE *cf = cfd >= 0 ? fdopen(cfd, "r") : NULL;
|
||||
if (cf) {
|
||||
fclose(cf);
|
||||
unlink("/tmp/zynk_code_restart");
|
||||
|
|
@ -154,6 +195,7 @@ static int reload_try_restore(Session *s) {
|
|||
return 1;
|
||||
fail:
|
||||
fclose(f);
|
||||
unlink(ZYNK_RELOAD_FILE);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -176,7 +218,7 @@ void reload_do(Session *s, const char *reply_target, int exec_new) {
|
|||
irc_msg(s, reply_target, "Cannot reload: not connected with TLS");
|
||||
return;
|
||||
}
|
||||
if (reload_save(s) < 0) {
|
||||
if (exec_new && reload_save(s) < 0) {
|
||||
irc_msg(s, reply_target, "Failed to save session state");
|
||||
return;
|
||||
}
|
||||
|
|
@ -209,6 +251,7 @@ void reload_do(Session *s, const char *reply_target, int exec_new) {
|
|||
}
|
||||
_exit(1);
|
||||
}
|
||||
unlink(ZYNK_RELOAD_FILE);
|
||||
return;
|
||||
}
|
||||
_exit(0);
|
||||
|
|
@ -234,7 +277,26 @@ static void handle_sigterm(int sig) { (void)sig; running = 0; }
|
|||
|
||||
/* ---- rate limiting ---- */
|
||||
|
||||
/*
|
||||
* RateLimitEntry - A single rate-limit tracking slot.
|
||||
*
|
||||
* Records the IRC nick 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.
|
||||
*
|
||||
* Fields:
|
||||
* nick - The IRC nick (NUL-terminated, max 31 chars).
|
||||
* t - The time() timestamp of the last command from this nick.
|
||||
*/
|
||||
typedef struct { char nick[32]; time_t t; } RateLimitEntry;
|
||||
|
||||
/*
|
||||
* rate_limits - Circular buffer of rate-limit entries.
|
||||
*
|
||||
* Holds RATE_LIMIT_MAX slots. When the buffer is full and a new entry
|
||||
* is needed, the oldest slot is evicted (LRU). A nick whose most
|
||||
* recent timestamp is within RATE_LIMIT_WINDOW seconds is rejected.
|
||||
*/
|
||||
static RateLimitEntry rate_limits[RATE_LIMIT_MAX];
|
||||
|
||||
/*
|
||||
|
|
@ -546,17 +608,25 @@ char *fetch_url(const char *url) { return run_curl(url); }
|
|||
*/
|
||||
int try_compile_and_restart(Session *s, const char *target) {
|
||||
log_stamp(); fprintf(stderr, CLR_GREEN "Code change detected, compiling..." CLR_RESET "\n");
|
||||
int efd = secure_state_open("/tmp/zynk_make_err", O_WRONLY | O_CREAT | O_TRUNC);
|
||||
if (efd < 0) {
|
||||
irc_msg(s, target, "Cannot create secure build log");
|
||||
return 0;
|
||||
}
|
||||
close(efd);
|
||||
int status = system("make 2>/tmp/zynk_make_err");
|
||||
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
|
||||
irc_msg(s, target, "Code compiled successfully, restarting...");
|
||||
log_stamp(); fprintf(stderr, CLR_GREEN "Compilation successful, restarting..." CLR_RESET "\n");
|
||||
FILE *cf = fopen("/tmp/zynk_code_restart", "w");
|
||||
int cfd = secure_state_open("/tmp/zynk_code_restart", O_WRONLY | O_CREAT | O_TRUNC);
|
||||
FILE *cf = cfd >= 0 ? fdopen(cfd, "w") : NULL;
|
||||
if (cf) { fprintf(cf, "%s\n", target); fclose(cf); }
|
||||
reload_do(s, target, 1);
|
||||
return 1;
|
||||
} else {
|
||||
char errbuf[512];
|
||||
FILE *f = fopen("/tmp/zynk_make_err", "r");
|
||||
efd = secure_state_open("/tmp/zynk_make_err", O_RDONLY);
|
||||
FILE *f = efd >= 0 ? fdopen(efd, "r") : NULL;
|
||||
if (f) {
|
||||
size_t n = fread(errbuf, 1, sizeof errbuf - 1, f);
|
||||
errbuf[n] = 0;
|
||||
|
|
|
|||
3
zynk.h
3
zynk.h
|
|
@ -19,6 +19,7 @@
|
|||
#include <netdb.h>
|
||||
#include <dlfcn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
#include <openssl/ssl.h>
|
||||
|
||||
#ifndef ZYNK_VERSION
|
||||
|
|
@ -65,6 +66,7 @@ typedef struct {
|
|||
int connected;
|
||||
char rbuf[16384];
|
||||
int rlen;
|
||||
int dropping_line;
|
||||
SSL *ssl;
|
||||
int use_tls;
|
||||
int code_restart;
|
||||
|
|
@ -147,6 +149,7 @@ void strip_formatting(char *s);
|
|||
void strip_opencode_header(char *s);
|
||||
int has_crlf(const char *s);
|
||||
int has_ctl(const char *s);
|
||||
int secure_state_open(const char *path, int flags);
|
||||
int valid_irc_word(const char *s);
|
||||
void normalize_city(const char *in, char *display, size_t dsz, char *url, size_t usz);
|
||||
void collapse_spaces(char *s);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue