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:
Johannes Findeisen 2026-07-24 03:21:49 +02:00
commit 72f7812630
16 changed files with 187 additions and 38 deletions

View file

@ -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`.

View file

@ -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
View file

@ -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]);

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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];

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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);