diff options
Diffstat (limited to 'src/core/monitor.c')
| -rw-r--r-- | src/core/monitor.c | 497 |
1 files changed, 497 insertions, 0 deletions
diff --git a/src/core/monitor.c b/src/core/monitor.c new file mode 100644 index 0000000..0b6d0c5 --- /dev/null +++ b/src/core/monitor.c @@ -0,0 +1,497 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/monitor.h" +#include "hybbx/auth.h" +#include "hybbx/config.h" +#include "hybbx/log.h" +#include "hybbx/security.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <string.h> +#include <strings.h> +#include <sys/stat.h> +#include <sys/types.h> + +#define MONITOR_LINE_MAX 512u +#define MONITOR_READ_CHUNK 4096u + +typedef struct monitor_tail { + char path[HYBBX_PATH_MAX]; + off_t offset; + int active; + char carry[MONITOR_LINE_MAX]; + size_t carry_len; +} monitor_tail_t; + +static hybbx_monitor_config_t g_mon_cfg; +static int g_mon_ready; +static monitor_tail_t g_tail_hybbx; +static monitor_tail_t g_tail_security; +static int g_any_monitor_active; + +void hybbx_monitor_config_defaults(hybbx_monitor_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + cfg->enabled = 1; + cfg->follow_hybbx = 1; + cfg->follow_security = 1; + cfg->invisible_sysop = 0; + cfg->invite_timeout_sec = 20u; +} + +static void monitor_parse_allow(hybbx_monitor_config_t *cfg, const char *raw) +{ + char buf[HYBBX_CONFIG_VALUE_MAX]; + char *p; + char *comma; + + cfg->allow_count = 0; + if (raw == NULL || raw[0] == '\0') { + return; + } + + hybbx_strlcpy(buf, raw, sizeof(buf)); + p = buf; + while (p != NULL && *p != '\0' && + cfg->allow_count < HYBBX_MONITOR_ALLOW_MAX) { + char *tok = p; + comma = strchr(p, ','); + if (comma != NULL) { + *comma = '\0'; + p = comma + 1; + } else { + p = NULL; + } + while (*tok == ' ' || *tok == '\t') { + tok++; + } + if (*tok == '\0') { + continue; + } + { + char *end = tok + strlen(tok); + while (end > tok && (end[-1] == ' ' || end[-1] == '\t')) { + *--end = '\0'; + } + } + if (tok[0] == '\0') { + continue; + } + hybbx_strlcpy(cfg->allow[cfg->allow_count], tok, + sizeof(cfg->allow[cfg->allow_count])); + cfg->allow_count++; + } +} + +void hybbx_monitor_config_apply(const struct hybbx_config *config) +{ + const char *allow_raw; + + hybbx_monitor_shutdown(); + hybbx_monitor_config_defaults(&g_mon_cfg); + + if (config != NULL) { + g_mon_cfg.enabled = + hybbx_config_get_bool(config, "monitor", "enabled", 1); + g_mon_cfg.follow_hybbx = + hybbx_config_get_bool(config, "monitor", "follow_hybbx", 1); + g_mon_cfg.follow_security = + hybbx_config_get_bool(config, "monitor", "follow_security", 1); + /* Prefer hyphen key; accept underscore alias. */ + if (hybbx_config_get(config, "monitor", "invisible-sysop", NULL) != + NULL) { + g_mon_cfg.invisible_sysop = hybbx_config_get_bool( + config, "monitor", "invisible-sysop", 0); + } else { + g_mon_cfg.invisible_sysop = hybbx_config_get_bool( + config, "monitor", "invisible_sysop", 0); + } + g_mon_cfg.invite_timeout_sec = hybbx_config_get_uint( + config, "monitor", "invite_timeout_sec", 20u, 5u, 300u); + allow_raw = hybbx_config_get(config, "monitor", "allow", NULL); + monitor_parse_allow(&g_mon_cfg, allow_raw); + } + + g_mon_ready = 1; + hybbx_log_info("[monitor] enabled=%s follow_hybbx=%s follow_security=%s " + "invisible-sysop=%s invite_timeout_sec=%u allow=%u", + g_mon_cfg.enabled ? "yes" : "no", + g_mon_cfg.follow_hybbx ? "yes" : "no", + g_mon_cfg.follow_security ? "yes" : "no", + g_mon_cfg.invisible_sysop ? "yes" : "no", + g_mon_cfg.invite_timeout_sec, + g_mon_cfg.allow_count); +} + +const hybbx_monitor_config_t *hybbx_monitor_config_get(void) +{ + return g_mon_ready ? &g_mon_cfg : NULL; +} + +int hybbx_monitor_enabled(void) +{ + return g_mon_ready && g_mon_cfg.enabled; +} + +int hybbx_monitor_invisible_sysop(void) +{ + return g_mon_ready && g_mon_cfg.invisible_sysop; +} + +int hybbx_monitor_user_allowed(const char *username) +{ + unsigned i; + + if (!g_mon_ready || username == NULL || username[0] == '\0') { + return 0; + } + + for (i = 0; i < g_mon_cfg.allow_count; i++) { + if (strcasecmp(g_mon_cfg.allow[i], username) == 0) { + return 1; + } + } + return 0; +} + +int hybbx_monitor_session_may_use(const struct hybbx_session *session) +{ + const char *user; + + if (!hybbx_monitor_enabled() || session == NULL) { + return 0; + } + + if (hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + return 1; + } + + user = hybbx_session_username(session); + return hybbx_monitor_user_allowed(user); +} + +typedef struct mon_count_ctx { + unsigned count; +} mon_count_ctx_t; + +static void mon_count_visitor(hybbx_session_t *session, void *userdata) +{ + mon_count_ctx_t *ctx = (mon_count_ctx_t *)userdata; + + if (session != NULL && hybbx_monitor_is_active(session)) { + ctx->count++; + } +} + +static void monitor_refresh_active_flag(hybbx_service_t *service) +{ + mon_count_ctx_t ctx; + + ctx.count = 0; + if (service != NULL) { + hybbx_service_visit_sessions(service, mon_count_visitor, &ctx); + } + g_any_monitor_active = ctx.count > 0; +} + +static void monitor_tail_seek_end(monitor_tail_t *tail, const char *path) +{ + struct stat st; + + tail->path[0] = '\0'; + tail->offset = 0; + tail->active = 0; + + if (path == NULL || path[0] == '\0') { + return; + } + + hybbx_strlcpy(tail->path, path, sizeof(tail->path)); + if (stat(tail->path, &st) != 0) { + return; + } + + tail->offset = st.st_size; + tail->active = 1; +} + +static void monitor_reset_tails(void) +{ + char path[HYBBX_PATH_MAX]; + + memset(&g_tail_hybbx, 0, sizeof(g_tail_hybbx)); + memset(&g_tail_security, 0, sizeof(g_tail_security)); + + if (g_mon_cfg.follow_hybbx && + hybbx_log_current_path(path, sizeof(path)) == HYBBX_OK) { + monitor_tail_seek_end(&g_tail_hybbx, path); + } + if (g_mon_cfg.follow_security && + hybbx_security_log_current_path(path, sizeof(path)) == HYBBX_OK) { + monitor_tail_seek_end(&g_tail_security, path); + } +} + +hybbx_result_t hybbx_monitor_set_active(hybbx_session_t *session, int on) +{ + hybbx_service_t *service; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_monitor_enabled()) { + hybbx_session_write_line(session, "Monitor is disabled in config."); + return HYBBX_ERR_DENIED; + } + + hybbx_session_set_monitor_active(session, on ? 1 : 0); + service = hybbx_session_service(session); + monitor_refresh_active_flag(service); + + if (on) { + /* Fresh follow from current EOF — no backlog. */ + monitor_reset_tails(); + hybbx_session_write_line(session, "Monitor on."); + if (hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + if (hybbx_monitor_invisible_sysop()) { + hybbx_session_write_line(session, + "Sysop: hidden from /who (/monitor invisible-sysop=yes)."); + } else { + hybbx_session_write_line(session, + "Sysop: hidden from /who and /online while monitor is on."); + } + } + } else { + hybbx_session_write_line(session, "Monitor off."); + monitor_refresh_active_flag(service); + if (!g_any_monitor_active) { + memset(&g_tail_hybbx, 0, sizeof(g_tail_hybbx)); + memset(&g_tail_security, 0, sizeof(g_tail_security)); + } + } + + return HYBBX_OK; +} + +int hybbx_monitor_is_active(const hybbx_session_t *session) +{ + return hybbx_session_monitor_active(session); +} + +typedef struct mon_bcast_ctx { + const char *line; + hybbx_session_t *skip; +} mon_bcast_ctx_t; + +static void mon_bcast_visitor(hybbx_session_t *session, void *userdata) +{ + mon_bcast_ctx_t *ctx = (mon_bcast_ctx_t *)userdata; + char out[MONITOR_LINE_MAX + 16]; + + if (session == NULL || ctx == NULL || ctx->line == NULL) { + return; + } + if (session == ctx->skip) { + return; + } + if (!hybbx_monitor_is_active(session)) { + return; + } + + snprintf(out, sizeof(out), "[monitor] %s", ctx->line); + hybbx_session_write_line(session, out); +} + +void hybbx_monitor_broadcast(hybbx_service_t *service, const char *line) +{ + mon_bcast_ctx_t ctx; + + if (service == NULL || line == NULL || line[0] == '\0') { + return; + } + + ctx.line = line; + ctx.skip = NULL; + hybbx_service_visit_sessions(service, mon_bcast_visitor, &ctx); +} + +void hybbx_monitor_event(hybbx_service_t *service, + const char *username, + const char *plugin, + const char *detail) +{ + char line[MONITOR_LINE_MAX]; + const char *user = (username != NULL && username[0] != '\0') ? username : "?"; + const char *plug = (plugin != NULL && plugin[0] != '\0') ? plugin : "?"; + + if (service == NULL || !g_any_monitor_active) { + /* Still compute flag lazily */ + monitor_refresh_active_flag(service); + if (!g_any_monitor_active) { + return; + } + } + + if (detail != NULL && detail[0] != '\0') { + snprintf(line, sizeof(line), "%s@%s %s", user, plug, detail); + } else { + snprintf(line, sizeof(line), "%s@%s", user, plug); + } + + hybbx_monitor_broadcast(service, line); +} + +static void monitor_push_raw_line(hybbx_service_t *service, const char *tag, + const char *text) +{ + char line[MONITOR_LINE_MAX]; + + if (text == NULL) { + return; + } + while (*text == '\r' || *text == '\n') { + text++; + } + if (*text == '\0') { + return; + } + + snprintf(line, sizeof(line), "%s %s", tag, text); + hybbx_monitor_broadcast(service, line); +} + +static void monitor_tail_read(hybbx_service_t *service, monitor_tail_t *tail, + const char *tag) +{ + FILE *fp; + struct stat st; + char chunk[MONITOR_READ_CHUNK]; + size_t n; + char *p; + char *nl; + + if (tail == NULL || !tail->active || tail->path[0] == '\0') { + return; + } + + if (stat(tail->path, &st) != 0) { + return; + } + + if (st.st_size < tail->offset) { + /* Truncated / rotated — follow from start of new content. */ + tail->offset = 0; + tail->carry_len = 0; + } + + if (st.st_size == tail->offset) { + return; + } + + fp = fopen(tail->path, "r"); + if (fp == NULL) { + return; + } + + if (fseeko(fp, tail->offset, SEEK_SET) != 0) { + fclose(fp); + return; + } + + while ((n = fread(chunk, 1, sizeof(chunk) - 1, fp)) > 0) { + chunk[n] = '\0'; + p = chunk; + while (*p != '\0') { + nl = strchr(p, '\n'); + if (nl == NULL) { + size_t left = strlen(p); + if (tail->carry_len + left >= sizeof(tail->carry)) { + tail->carry_len = 0; + } + memcpy(tail->carry + tail->carry_len, p, left); + tail->carry_len += left; + tail->carry[tail->carry_len] = '\0'; + break; + } + *nl = '\0'; + if (tail->carry_len > 0) { + size_t room = sizeof(tail->carry) - 1u - tail->carry_len; + size_t take = strlen(p); + if (take > room) { + take = room; + } + memcpy(tail->carry + tail->carry_len, p, take); + tail->carry_len += take; + tail->carry[tail->carry_len] = '\0'; + monitor_push_raw_line(service, tag, tail->carry); + tail->carry_len = 0; + } else { + monitor_push_raw_line(service, tag, p); + } + p = nl + 1; + } + tail->offset = ftello(fp); + } + + fclose(fp); + + /* Refresh path if weekly rotate changed hybbx log name. */ + if (tail == &g_tail_hybbx) { + char path[HYBBX_PATH_MAX]; + if (hybbx_log_current_path(path, sizeof(path)) == HYBBX_OK && + strcmp(path, tail->path) != 0) { + monitor_tail_seek_end(tail, path); + } + } +} + +void hybbx_monitor_tick(hybbx_service_t *service) +{ + if (!g_mon_ready || !g_mon_cfg.enabled || service == NULL) { + return; + } + + monitor_refresh_active_flag(service); + if (!g_any_monitor_active) { + return; + } + + if (!g_tail_hybbx.active && g_mon_cfg.follow_hybbx) { + char path[HYBBX_PATH_MAX]; + if (hybbx_log_current_path(path, sizeof(path)) == HYBBX_OK) { + monitor_tail_seek_end(&g_tail_hybbx, path); + } + } + if (!g_tail_security.active && g_mon_cfg.follow_security) { + char path[HYBBX_PATH_MAX]; + if (hybbx_security_log_current_path(path, sizeof(path)) == HYBBX_OK) { + monitor_tail_seek_end(&g_tail_security, path); + } + } + + if (g_mon_cfg.follow_hybbx) { + monitor_tail_read(service, &g_tail_hybbx, "hybbx:"); + } + if (g_mon_cfg.follow_security) { + monitor_tail_read(service, &g_tail_security, "security:"); + } +} + +void hybbx_monitor_shutdown(void) +{ + memset(&g_mon_cfg, 0, sizeof(g_mon_cfg)); + memset(&g_tail_hybbx, 0, sizeof(g_tail_hybbx)); + memset(&g_tail_security, 0, sizeof(g_tail_security)); + g_mon_ready = 0; + g_any_monitor_active = 0; +} |
