diff options
Diffstat (limited to 'src/core')
50 files changed, 30142 insertions, 0 deletions
diff --git a/src/core/auth.c b/src/core/auth.c new file mode 100644 index 0000000..e68242a --- /dev/null +++ b/src/core/auth.c @@ -0,0 +1,477 @@ +#include "hybbx/auth.h" +#include "hybbx/storage.h" +#include "hybbx/util.h" + +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +void hybbx_auth_config_defaults(hybbx_auth_config_t *auth) +{ + if (auth == NULL) { + return; + } + + auth->auto_login = 1; + hybbx_strlcpy(auth->guest_prefix, HYBBX_AUTH_DEFAULT_GUEST_PREFIX, + sizeof(auth->guest_prefix)); +} + +const char *hybbx_user_level_name(hybbx_user_level_t level) +{ + switch (level) { + case HYBBX_LEVEL_SYSOP: + return "sysop"; + case HYBBX_LEVEL_ADMIN: + return "admin"; + case HYBBX_LEVEL_MOD: + return "mod"; + case HYBBX_LEVEL_USER: + return "user"; + case HYBBX_LEVEL_GUEST: + return "guest"; + default: + return "user"; + } +} + +hybbx_user_level_t hybbx_user_level_parse(const char *name) +{ + if (name == NULL || name[0] == '\0') { + return HYBBX_LEVEL_USER; + } + + if (str_ieq(name, "sysop")) { + return HYBBX_LEVEL_SYSOP; + } + if (str_ieq(name, "admin")) { + return HYBBX_LEVEL_ADMIN; + } + if (str_ieq(name, "mod") || str_ieq(name, "moderator")) { + return HYBBX_LEVEL_MOD; + } + if (str_ieq(name, "guest")) { + return HYBBX_LEVEL_GUEST; + } + + return HYBBX_LEVEL_USER; +} + +int hybbx_user_level_is_guest(hybbx_user_level_t level) +{ + return level == HYBBX_LEVEL_GUEST; +} + +int hybbx_user_level_is_sysop_or_admin(hybbx_user_level_t level) +{ + return level == HYBBX_LEVEL_SYSOP || level == HYBBX_LEVEL_ADMIN; +} + +int hybbx_user_level_is_sysop(hybbx_user_level_t level) +{ + return level == HYBBX_LEVEL_SYSOP; +} + +static int name_has_prefix(const char *username, const char *prefix) +{ + size_t plen; + size_t i; + + if (username == NULL || prefix == NULL) { + return 0; + } + + plen = strlen(prefix); + if (plen == 0) { + return 0; + } + + for (i = 0; i < plen; i++) { + char cu = username[i]; + char cp = prefix[i]; + + if (cu == '\0') { + return 0; + } + + if (cu >= 'A' && cu <= 'Z') { + cu = (char)(cu + 32); + } + if (cp >= 'A' && cp <= 'Z') { + cp = (char)(cp + 32); + } + + if (cu != cp) { + return 0; + } + } + + return 1; +} + +static int username_char_ok(char ch) +{ + return (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '_' || ch == '-'; +} + +void hybbx_username_normalize(char *username) +{ + size_t i; + + if (username == NULL) { + return; + } + + for (i = 0; username[i] != '\0'; i++) { + if (username[i] >= 'A' && username[i] <= 'Z') { + username[i] = (char)(username[i] + 32); + } + } +} + +const char *hybbx_username_display(const char *username, + hybbx_user_level_t level) +{ + hybbx_user_record_t user; + + if (username == NULL || username[0] == '\0') { + return ""; + } + + memset(&user, 0, sizeof(user)); + hybbx_strlcpy(user.username, username, sizeof(user.username)); + hybbx_username_normalize(user.username); + user.level = level; + hybbx_nickname_infer(username, user.nickname, sizeof(user.nickname)); + + return hybbx_user_display_name(&user); +} + +void hybbx_nickname_infer(const char *stored_username, + char *nickname, + size_t nickname_len) +{ + if (stored_username == NULL || nickname == NULL || nickname_len == 0) { + return; + } + + if (str_ieq(stored_username, HYBBX_DEFAULT_SYSOP_USERNAME) || + str_ieq(stored_username, "sysop")) { + hybbx_strlcpy(nickname, HYBBX_DEFAULT_SYSOP_USERNAME, nickname_len); + return; + } + + if (stored_username[0] >= 'a' && stored_username[0] <= 'z') { + nickname[0] = (char)(stored_username[0] - 32); + hybbx_strlcpy(nickname + 1, stored_username + 1, nickname_len - 1); + return; + } + + hybbx_strlcpy(nickname, stored_username, nickname_len); +} + +const char *hybbx_user_display_name(const hybbx_user_record_t *user) +{ + if (user == NULL) { + return ""; + } + + if (user->nickname[0] != '\0') { + return user->nickname; + } + + if (user->username[0] == '\0') { + return ""; + } + + return user->username; +} + +int hybbx_guest_slot_from_username(const char *guest_prefix, + const char *username, + unsigned *slot_out) +{ + const char *prefix; + size_t plen; + size_t i; + unsigned long slot; + char *end; + + if (username == NULL || slot_out == NULL) { + return 0; + } + + prefix = guest_prefix != NULL && guest_prefix[0] != '\0' ? + guest_prefix : HYBBX_AUTH_DEFAULT_GUEST_PREFIX; + plen = strlen(prefix); + if (plen == 0 || strlen(username) <= plen) { + return 0; + } + + for (i = 0; i < plen; i++) { + char a = (char)(prefix[i] >= 'A' && prefix[i] <= 'Z' ? + prefix[i] + 32 : prefix[i]); + char b = (char)(username[i] >= 'A' && username[i] <= 'Z' ? + username[i] + 32 : username[i]); + + if (a != b) { + return 0; + } + } + + slot = strtoul(username + plen, &end, 10); + if (end == username + plen || *end != '\0' || slot < 1 || + slot > HYBBX_GUEST_NUMBER_MAX) { + return 0; + } + + *slot_out = (unsigned)slot; + return 1; +} + +void hybbx_guest_fill_record(const char *guest_prefix, + unsigned slot, + hybbx_user_record_t *out) +{ + const char *prefix; + time_t now; + + if (out == NULL || slot < 1 || slot > HYBBX_GUEST_NUMBER_MAX) { + return; + } + + prefix = guest_prefix != NULL && guest_prefix[0] != '\0' ? + guest_prefix : HYBBX_AUTH_DEFAULT_GUEST_PREFIX; + now = time(NULL); + + memset(out, 0, sizeof(*out)); + out->id = HYBBX_GUEST_USER_ID(slot); + snprintf(out->username, sizeof(out->username), "%s%u", prefix, slot); + hybbx_strlcpy(out->nickname, out->username, sizeof(out->nickname)); + out->level = HYBBX_LEVEL_GUEST; + out->active = 1; + out->created_at = now; +} + +int hybbx_username_valid(const char *username, const char *guest_prefix) +{ + size_t len; + size_t i; + size_t digit_count = 0; + size_t underscore_count = 0; + size_t hyphen_count = 0; + const char *prefix; + + if (username == NULL) { + return 0; + } + + len = strlen(username); + if (len < HYBBX_USERNAME_MIN_LEN || len > HYBBX_USERNAME_MAX_LEN) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!username_char_ok(username[i])) { + return 0; + } + + if (username[i] >= '0' && username[i] <= '9') { + digit_count++; + } else if (username[i] == '_') { + underscore_count++; + } else if (username[i] == '-') { + hyphen_count++; + } + } + + if (digit_count > HYBBX_USERNAME_MAX_DIGITS) { + return 0; + } + + if (underscore_count > 1 || hyphen_count > 1 || + (underscore_count > 0 && hyphen_count > 0)) { + return 0; + } + + prefix = guest_prefix != NULL && guest_prefix[0] != '\0' ? + guest_prefix : HYBBX_AUTH_DEFAULT_GUEST_PREFIX; + + if (name_has_prefix(username, prefix)) { + return 0; + } + + if (str_ieq(username, "sysop") || str_ieq(username, "admin") || + str_ieq(username, "mod") || str_ieq(username, "guest")) { + return 0; + } + + return 1; +} + +int hybbx_password_plain_valid(const char *password) +{ + size_t len; + + if (password == NULL || password[0] == '\0') { + return 0; + } + + if (str_ieq(password, "-")) { + return 0; + } + + len = strlen(password); + if (len < HYBBX_PASSWORD_MIN_LEN || len > HYBBX_PASSWORD_MAX_LEN) { + return 0; + } + + return 1; +} + +static int profile_text_char_ok(char ch) +{ + if (ch == '|') { + return 0; + } + + return (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == ' ' || ch == '-' || ch == '\'' || ch == '.' || ch == ','; +} + +static int profile_text_valid(const char *text, size_t min_len, size_t max_len) +{ + size_t len; + size_t i; + + if (text == NULL) { + return 0; + } + + len = strlen(text); + if (len < min_len || len >= max_len) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!profile_text_char_ok(text[i])) { + return 0; + } + } + + return 1; +} + +static int email_valid(const char *email) +{ + const char *at; + const char *dot; + size_t len; + size_t i; + + if (email == NULL) { + return 0; + } + + len = strlen(email); + if (len < 5 || len >= HYBBX_USER_EMAIL_MAX) { + return 0; + } + + at = strchr(email, '@'); + if (at == NULL || at == email || at[1] == '\0') { + return 0; + } + + dot = strchr(at + 1, '.'); + if (dot == NULL || dot[1] == '\0') { + return 0; + } + + for (i = 0; i < len; i++) { + char ch = email[i]; + + if (ch == '|' || ch == ' ') { + return 0; + } + + if (!((ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '@' || ch == '.' || ch == '-' || ch == '_' || ch == '+')) { + return 0; + } + } + + return 1; +} + +int hybbx_user_profile_valid(const hybbx_user_registration_t *reg) +{ + if (reg == NULL) { + return 0; + } + + if (!profile_text_valid(reg->full_name, 2, HYBBX_USER_FULL_NAME_MAX)) { + return 0; + } + + if (!profile_text_valid(reg->country, 2, HYBBX_USER_COUNTRY_MAX)) { + return 0; + } + + if (!profile_text_valid(reg->location, 2, HYBBX_USER_LOCATION_MAX)) { + return 0; + } + + if (!email_valid(reg->email)) { + return 0; + } + + return 1; +} + +int hybbx_registration_valid(const hybbx_user_registration_t *reg, + const char *guest_prefix) +{ + if (reg == NULL) { + return 0; + } + + if (!hybbx_username_valid(reg->username, guest_prefix)) { + return 0; + } + + if (!hybbx_username_valid(reg->nickname, guest_prefix)) { + return 0; + } + + return hybbx_user_profile_valid(reg); +} diff --git a/src/core/bandwidth_policy.c b/src/core/bandwidth_policy.c new file mode 100644 index 0000000..47a59d7 --- /dev/null +++ b/src/core/bandwidth_policy.c @@ -0,0 +1,229 @@ +#include "hybbx/bandwidth_policy.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/plugin.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <string.h> +#include <time.h> + +#define BW_USER_MAX 128u + +/** Lower = sacrificed first under pressure (AX.25 users before full-duplex TCP). */ +#define BW_PRIORITY_AX25_USER 0 +#define BW_PRIORITY_TCP_USER 1 + +typedef struct bw_user_entry { + hybbx_session_t *session; + time_t connected_at; + int sacrifice_priority; +} bw_user_entry_t; + +typedef struct bw_collect_ctx { + bw_user_entry_t entries[BW_USER_MAX]; + unsigned count; +} bw_collect_ctx_t; + +static int session_sacrifice_priority(const hybbx_session_t *session) +{ + const hybbx_transport_plugin_t *transport; + + if (session == NULL) { + return -1; + } + + transport = session->transport; + if (transport == NULL) { + return -1; + } + + switch (transport->kind) { + case HYBBX_TRANSPORT_PACKET_RADIO: + return BW_PRIORITY_AX25_USER; + case HYBBX_TRANSPORT_TELNET: + return BW_PRIORITY_TCP_USER; + case HYBBX_TRANSPORT_CIRCUIT: + default: + return -1; + } +} + +static void bw_collect_visitor(hybbx_session_t *session, void *userdata) +{ + bw_collect_ctx_t *ctx = (bw_collect_ctx_t *)userdata; + int priority; + + if (ctx == NULL || session == NULL) { + return; + } + + priority = session_sacrifice_priority(session); + if (priority < 0 || !hybbx_session_logged_in(session)) { + return; + } + + if (ctx->count >= BW_USER_MAX) { + return; + } + + ctx->entries[ctx->count].session = session; + ctx->entries[ctx->count].connected_at = hybbx_session_connected_at(session); + ctx->entries[ctx->count].sacrifice_priority = priority; + ctx->count++; +} + +static void bw_sort_victims_first(bw_collect_ctx_t *ctx) +{ + unsigned i; + unsigned j; + + if (ctx == NULL) { + return; + } + + /* Lowest sacrifice_priority first (AX.25), then newest connected_at. */ + for (i = 0; i + 1 < ctx->count; i++) { + for (j = i + 1; j < ctx->count; j++) { + int swap = 0; + + if (ctx->entries[j].sacrifice_priority < + ctx->entries[i].sacrifice_priority) { + swap = 1; + } else if (ctx->entries[j].sacrifice_priority == + ctx->entries[i].sacrifice_priority) { + if (ctx->entries[j].connected_at > + ctx->entries[i].connected_at) { + swap = 1; + } else if (ctx->entries[j].connected_at == + ctx->entries[i].connected_at && + hybbx_session_id(ctx->entries[j].session) > + hybbx_session_id(ctx->entries[i].session)) { + swap = 1; + } + } + + if (swap) { + bw_user_entry_t tmp = ctx->entries[i]; + + ctx->entries[i] = ctx->entries[j]; + ctx->entries[j] = tmp; + } + } + } +} + +static hybbx_session_t *bw_first_unpaused(const bw_collect_ctx_t *ctx) +{ + unsigned i; + + if (ctx == NULL) { + return NULL; + } + + for (i = 0; i < ctx->count; i++) { + if (!hybbx_session_bandwidth_paused(ctx->entries[i].session)) { + return ctx->entries[i].session; + } + } + + return NULL; +} + +static hybbx_session_t *bw_first_paused(const bw_collect_ctx_t *ctx) +{ + unsigned i; + + if (ctx == NULL) { + return NULL; + } + + for (i = 0; i < ctx->count; i++) { + if (hybbx_session_bandwidth_paused(ctx->entries[i].session)) { + return ctx->entries[i].session; + } + } + + return NULL; +} + +unsigned hybbx_bandwidth_policy_user_count(hybbx_service_t *service) +{ + bw_collect_ctx_t ctx; + + if (service == NULL) { + return 0; + } + + memset(&ctx, 0, sizeof(ctx)); + hybbx_service_visit_sessions(service, bw_collect_visitor, &ctx); + return ctx.count; +} + +unsigned hybbx_bandwidth_policy_apply(hybbx_service_t *service, + hybbx_circuit_balance_action_t action) +{ + bw_collect_ctx_t ctx; + unsigned i; + unsigned affected = 0; + + if (service == NULL) { + return 0; + } + + memset(&ctx, 0, sizeof(ctx)); + hybbx_service_visit_sessions(service, bw_collect_visitor, &ctx); + if (ctx.count == 0) { + return 0; + } + + bw_sort_victims_first(&ctx); + + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: { + hybbx_session_t *target = bw_first_unpaused(&ctx); + + if (target != NULL) { + hybbx_session_set_bandwidth_paused(target, 1); + hybbx_log_stats("[bandwidth] paused user %s (QoS: AX.25 before TCP)", + hybbx_session_display_name(target)); + affected = 1; + } + break; + } + case HYBBX_CIRCUIT_BAL_BREAK: { + hybbx_session_t *target = bw_first_paused(&ctx); + + if (target == NULL && ctx.count > 0) { + target = ctx.entries[0].session; + } + + if (target != NULL) { + hybbx_log_stats("[bandwidth] disconnecting user %s (break)", + hybbx_session_display_name(target)); + hybbx_session_disconnect_bandwidth(target); + affected = 1; + } + break; + } + case HYBBX_CIRCUIT_BAL_CANCEL: + for (i = 0; i + 1 < ctx.count; i++) { + hybbx_log_stats("[bandwidth] disconnecting user %s (cancel)", + hybbx_session_display_name(ctx.entries[i].session)); + hybbx_session_disconnect_bandwidth(ctx.entries[i].session); + affected++; + } + break; + case HYBBX_CIRCUIT_BAL_RESUME: + for (i = 0; i < ctx.count; i++) { + hybbx_session_set_bandwidth_paused(ctx.entries[i].session, 0); + } + affected = ctx.count; + break; + case HYBBX_CIRCUIT_BAL_NONE: + default: + break; + } + + return affected; +} diff --git a/src/core/broadcast.c b/src/core/broadcast.c new file mode 100644 index 0000000..6a7cf68 --- /dev/null +++ b/src/core/broadcast.c @@ -0,0 +1,982 @@ +#include "hybbx/broadcast.h" +#include "hybbx/messages.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/plugin.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit.h" +#include "hybbx/ax25.h" +#include "hybbx/texts.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +void hybbx_ax25_frequency_table_clear(hybbx_ax25_frequency_table_t *table) +{ + if (table == NULL) { + return; + } + + memset(table, 0, sizeof(*table)); +} + +int hybbx_ax25_frequency_match(double a_mhz, double b_mhz) +{ + double diff; + + if (a_mhz <= 0.0 || b_mhz <= 0.0) { + return 0; + } + + diff = a_mhz - b_mhz; + if (diff < 0.0) { + diff = -diff; + } + + return diff < 0.001; +} + +void hybbx_ax25_frequency_apply(hybbx_ax25_frequency_table_t *table, + const hybbx_config_t *config) +{ + unsigned count; + unsigned i; + unsigned loaded = 0; + char key[24]; + const char *value; + + if (table == NULL || config == NULL) { + return; + } + + hybbx_ax25_frequency_table_clear(table); + + count = hybbx_config_get_uint(config, "ax25", "frequency_count", + 0u, 0u, HYBBX_AX25_FREQUENCY_MAX); + + for (i = 1; i <= HYBBX_AX25_FREQUENCY_MAX; i++) { + snprintf(key, sizeof(key), "frequency%u", i); + value = hybbx_config_get(config, "ax25", key, NULL); + if (value == NULL || value[0] == '\0') { + if (count > 0 && i > count) { + break; + } + continue; + } + + { + double mhz = strtod(value, NULL); + + if (mhz <= 0.0) { + continue; + } + if (loaded >= HYBBX_AX25_FREQUENCY_MAX) { + break; + } + + table->mhz[loaded] = mhz; + + snprintf(key, sizeof(key), "frequency%u_label", i); + value = hybbx_config_get(config, "ax25", key, NULL); + if (value == NULL || value[0] == '\0') { + snprintf(key, sizeof(key), "frequency%u_name", i); + value = hybbx_config_get(config, "ax25", key, NULL); + } + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(table->labels[loaded], value, + sizeof(table->labels[loaded])); + } else { + snprintf(table->labels[loaded], sizeof(table->labels[loaded]), + "%.3f MHz", mhz); + } + loaded++; + } + + if (count > 0 && i >= count) { + break; + } + } + + table->count = loaded; + + if (table->count > 0) { + hybbx_log_info("[ax25] %u configured frequencies (%.3f … %.3f MHz)", + table->count, table->mhz[0], table->mhz[table->count - 1]); + } else { + hybbx_log_info("[ax25] no frequencies in INI (set frequency1 … in [ax25])"); + } +} + +void hybbx_broadcast_config_defaults(hybbx_broadcast_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + cfg->enabled = 1; + cfg->ax25_enabled = 1; + cfg->ax25_auto = 1; + cfg->ax25_auto_interval_sec = HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC; + cfg->ax25_auto_stagger_sec = 0; + hybbx_strlcpy(cfg->ax25_mycall, "HYBBX", sizeof(cfg->ax25_mycall)); + hybbx_strlcpy(cfg->ax25_dest, "QST", sizeof(cfg->ax25_dest)); + hybbx_strlcpy(cfg->ax25_auto_message, HYBBX_BROADCAST_AUTO_MESSAGE_DEFAULT, + sizeof(cfg->ax25_auto_message)); + hybbx_ax25_frequency_table_clear(&cfg->frequencies); +} + +void hybbx_broadcast_config_apply(hybbx_broadcast_config_t *cfg, + const hybbx_config_t *config) +{ + const char *mycall; + const char *dest; + const char *auto_msg; + unsigned interval; + + if (cfg == NULL || config == NULL) { + return; + } + + hybbx_broadcast_config_defaults(cfg); + + cfg->enabled = hybbx_config_get_bool(config, "broadcast", "enabled", 1); + cfg->ax25_enabled = hybbx_config_get_bool(config, "broadcast", "ax25", 1); + cfg->ax25_auto = hybbx_config_get_bool(config, "broadcast", "ax25_auto", 1); + + interval = hybbx_config_get_uint(config, "broadcast", "ax25_auto_interval", + HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC, + HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC, + 86400u); + cfg->ax25_auto_interval_sec = interval; + cfg->ax25_auto_stagger_sec = 0; + + mycall = hybbx_config_get(config, "broadcast", "ax25_mycall", NULL); + dest = hybbx_config_get(config, "broadcast", "ax25_dest", NULL); + auto_msg = hybbx_config_get(config, "broadcast", "ax25_auto_message", NULL); + if (mycall != NULL && mycall[0] != '\0') { + hybbx_ax25_address_t addr; + + if (hybbx_ax25_address_parse(mycall, &addr) == HYBBX_OK) { + hybbx_strlcpy(cfg->ax25_mycall, mycall, sizeof(cfg->ax25_mycall)); + } else { + hybbx_log_warn("[broadcast] ax25_mycall=%s invalid — using HYBBX", + mycall); + } + } + if (dest != NULL && dest[0] != '\0') { + hybbx_ax25_address_t addr; + + if (hybbx_ax25_address_parse(dest, &addr) == HYBBX_OK) { + hybbx_strlcpy(cfg->ax25_dest, dest, sizeof(cfg->ax25_dest)); + } else { + hybbx_log_warn("[broadcast] ax25_dest=%s invalid — using QST", dest); + hybbx_strlcpy(cfg->ax25_dest, "QST", sizeof(cfg->ax25_dest)); + } + } + if (auto_msg != NULL && auto_msg[0] != '\0') { + size_t msg_len = strlen(auto_msg); + + if (msg_len > HYBBX_BROADCAST_AX25_MESSAGE_MAX) { + hybbx_log_warn("[broadcast] ax25_auto_message truncated (%zu > %u)", + msg_len, (unsigned)HYBBX_BROADCAST_AX25_MESSAGE_MAX); + } + hybbx_strlcpy(cfg->ax25_auto_message, auto_msg, + sizeof(cfg->ax25_auto_message)); + } + + hybbx_ax25_frequency_apply(&cfg->frequencies, config); + + hybbx_log_info("[broadcast] announce=%s ax25_auto=%s interval=%us " + "(min %us, band idle %us, link gap %us, per-link min %us)", + cfg->enabled ? "yes" : "no", + cfg->ax25_auto ? "yes" : "no", + cfg->ax25_auto_interval_sec, + (unsigned)HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC, + (unsigned)HYBBX_BROADCAST_AX25_BAND_IDLE_SEC, + (unsigned)HYBBX_BROADCAST_AX25_LINK_GAP_SEC, + (unsigned)HYBBX_BROADCAST_AX25_LINK_MIN_SEC); +} + +static hybbx_result_t broadcast_build_path(const hybbx_broadcast_config_t *cfg, + hybbx_ax25_path_t *path) +{ + if (cfg == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(path, 0, sizeof(*path)); + + if (cfg->ax25_dest[0] == '\0' || strcmp(cfg->ax25_dest, "*") == 0) { + hybbx_log_warn("[broadcast] ax25_dest=%s invalid — using QST", + cfg->ax25_dest[0] != '\0' ? cfg->ax25_dest : "(empty)"); + if (hybbx_ax25_address_parse("QST", &path->dest) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + } else if (hybbx_ax25_address_parse(cfg->ax25_dest, &path->dest) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + if (hybbx_ax25_address_parse(cfg->ax25_mycall, &path->source) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static time_t g_ax25_last_sent; +static time_t g_ax25_link_last_sent[HYBBX_CIRCUIT_MAX_LINKS]; +static unsigned g_ax25_auto_tick; +static unsigned g_ax25_defer_log_sec; + +typedef struct broadcast_ax25_seq { + int active; + hybbx_service_t *service; + hybbx_circuit_hub_t *hub; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned next_index; + char message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + time_t resume_at; +} broadcast_ax25_seq_t; + +static broadcast_ax25_seq_t g_ax25_seq; + +static double broadcast_link_target_mhz(const hybbx_broadcast_config_t *cfg, + const hybbx_circuit_broadcast_link_t *link, + unsigned link_index) +{ + (void)cfg; + (void)link_index; + + if (link == NULL) { + return 0.0; + } + + return link->frequency_mhz; +} + +static void broadcast_ax25_log_deferred(double frequency_mhz, const char *reason) +{ + if (reason == NULL) { + return; + } + + g_ax25_defer_log_sec++; + if (g_ax25_defer_log_sec < 60u) { + return; + } + + g_ax25_defer_log_sec = 0; + if (frequency_mhz > 0.0) { + hybbx_log_stats("[broadcast] ax25 %.3f MHz deferred (%s)", + frequency_mhz, reason); + } else { + hybbx_log_stats("[broadcast] ax25 auto deferred (%s)", reason); + } +} + +static void broadcast_ax25_seq_defer_until(time_t resume_at, double target_mhz, + const char *reason) +{ + time_t now = time(NULL); + + broadcast_ax25_log_deferred(target_mhz, reason); + if (now != (time_t)-1 && (resume_at == (time_t)-1 || resume_at <= now)) { + resume_at = now + (time_t)HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC; + } + g_ax25_seq.resume_at = resume_at; +} + +static int broadcast_hub_link_band_idle(hybbx_circuit_hub_t *hub, + unsigned slot_index) +{ + if (hub == NULL) { + return 0; + } + + return hybbx_circuit_hub_link_band_idle(hub, slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC); +} + +static int broadcast_ax25_rate_ok(unsigned interval_sec) +{ + time_t now; + time_t elapsed; + + if (g_ax25_last_sent == 0) { + return 1; + } + + now = time(NULL); + if (now == (time_t)-1) { + return 0; + } + + elapsed = now - g_ax25_last_sent; + if (elapsed < 0) { + return 1; + } + + return (unsigned)elapsed >= interval_sec; +} + +static int broadcast_ax25_link_rate_ok(unsigned slot_index, + unsigned interval_sec) +{ + time_t now; + time_t elapsed; + + if (slot_index >= HYBBX_CIRCUIT_MAX_LINKS) { + return 0; + } + + if (g_ax25_link_last_sent[slot_index] == 0) { + return 1; + } + + now = time(NULL); + if (now == (time_t)-1) { + return 0; + } + + elapsed = now - g_ax25_link_last_sent[slot_index]; + if (elapsed < 0) { + return 1; + } + + return (unsigned)elapsed >= interval_sec; +} + +static void broadcast_ax25_mark_sent(void) +{ + time_t now = time(NULL); + + if (now != (time_t)-1) { + g_ax25_last_sent = now; + } +} + +static void broadcast_ax25_mark_link_sent(unsigned slot_index) +{ + time_t now = time(NULL); + + if (slot_index >= HYBBX_CIRCUIT_MAX_LINKS || now == (time_t)-1) { + return; + } + + g_ax25_link_last_sent[slot_index] = now; + g_ax25_last_sent = now; +} + +static size_t broadcast_expand_service_token(char *out, size_t out_len, + const char *tmpl, + const char *service_name) +{ + size_t pos = 0; + size_t i = 0; + + if (out == NULL || out_len == 0 || tmpl == NULL) { + return 0; + } + + if (service_name == NULL || service_name[0] == '\0') { + service_name = HYBBX_DEFAULT_SERVICE_NAME; + } + + out[0] = '\0'; + + while (tmpl[i] != '\0' && pos + 1 < out_len) { + if (strncmp(tmpl + i, HYBBX_BANNER_TOKEN_SERVICE, + strlen(HYBBX_BANNER_TOKEN_SERVICE)) == 0) { + size_t n = strlen(service_name); + + if (pos + n >= out_len) { + n = out_len - pos - 1; + } + memcpy(out + pos, service_name, n); + pos += n; + i += strlen(HYBBX_BANNER_TOKEN_SERVICE); + continue; + } + + out[pos++] = tmpl[i++]; + } + + out[pos] = '\0'; + return pos; +} + +static hybbx_result_t broadcast_ax25_send_slot(hybbx_service_t *service, + unsigned slot_index, + const char *message, + int enforce_rate_limit) +{ + const hybbx_broadcast_config_t *cfg; + hybbx_circuit_hub_t *hub; + hybbx_ax25_path_t path; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t msg_len; + hybbx_result_t rc; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned i; + const char *link_id = ""; + double target_mhz = 0.0; + + if (service == NULL || message == NULL || message[0] == '\0' || + slot_index >= HYBBX_CIRCUIT_MAX_LINKS) { + return HYBBX_ERR_INVALID; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return HYBBX_ERR_BUSY; + } + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + for (i = 0; i < link_count; i++) { + if (links[i].slot_index == slot_index) { + link_id = links[i].link_id; + target_mhz = links[i].frequency_mhz; + break; + } + } + if (link_id[0] == '\0') { + return HYBBX_ERR_DENIED; + } + + if (!hybbx_circuit_hub_link_band_idle(hub, slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC)) { + return HYBBX_ERR_BUSY; + } + + if (enforce_rate_limit && + !broadcast_ax25_link_rate_ok(slot_index, + HYBBX_BROADCAST_AX25_LINK_MIN_SEC)) { + return HYBBX_ERR_BUSY; + } + + msg_len = strlen(message); + if (msg_len > HYBBX_BROADCAST_AX25_MESSAGE_MAX) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_circuit_hub_link_broadcast_qos(hub)) { + return HYBBX_ERR_BUSY; + } + + rc = broadcast_build_path(cfg, &path); + if (rc != HYBBX_OK) { + return rc; + } + + { + uint8_t ax25_frame[HYBBX_AX25_FRAME_MAX]; + size_t ax25_len = hybbx_ax25_build_ui(&path, + (const uint8_t *)message, msg_len, + ax25_frame, sizeof(ax25_frame)); + + if (ax25_len == 0) { + return HYBBX_ERR_IO; + } + + hybbx_log_debug("[broadcast] ax25 path %s>%s payload=%zu slot=%u", + path.source.call, path.dest.call, msg_len, slot_index); + + frame_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25, + HYBBX_CIRCUIT_FLAG_TX, + ax25_frame, ax25_len, + frame, sizeof(frame)); + } + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + rc = hybbx_circuit_hub_send_hbx_slot(hub, slot_index, frame, frame_len, 1); + if (rc != HYBBX_OK) { + return rc; + } + + broadcast_ax25_mark_link_sent(slot_index); + + if (target_mhz > 0.0) { + hybbx_log_stats("[broadcast] ax25 link=%s %.3f MHz (slot %u): %s", + link_id, target_mhz, slot_index, message); + } else { + hybbx_log_stats("[broadcast] ax25 link=%s (slot %u): %s", + link_id, slot_index, message); + } + + return HYBBX_OK; +} + +static hybbx_result_t broadcast_ax25_send(hybbx_service_t *service, + double frequency_mhz, + const char *message, + int enforce_rate_limit) +{ + const hybbx_broadcast_config_t *cfg; + hybbx_circuit_hub_t *hub; + hybbx_ax25_path_t path; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t msg_len; + hybbx_result_t rc; + unsigned sent_links; + int per_link = 0; + + if (service == NULL || message == NULL || message[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return HYBBX_ERR_BUSY; + } + + if (frequency_mhz > 0.0) { + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned i; + int any_rate_ok = 0; + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + for (i = 0; i < link_count; i++) { + if (links[i].frequency_mhz <= 0.0 || + !hybbx_ax25_frequency_match(frequency_mhz, + links[i].frequency_mhz)) { + continue; + } + if (!enforce_rate_limit || + broadcast_ax25_link_rate_ok(links[i].slot_index, + HYBBX_BROADCAST_AX25_LINK_MIN_SEC)) { + any_rate_ok = 1; + break; + } + } + if (!any_rate_ok) { + return HYBBX_ERR_BUSY; + } + per_link = 1; + } else if (enforce_rate_limit && + !broadcast_ax25_rate_ok(cfg->ax25_auto_interval_sec)) { + return HYBBX_ERR_BUSY; + } + + msg_len = strlen(message); + if (msg_len > HYBBX_BROADCAST_AX25_MESSAGE_MAX) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_circuit_hub_link_broadcast_qos(hub)) { + return HYBBX_ERR_BUSY; + } + + rc = broadcast_build_path(cfg, &path); + if (rc != HYBBX_OK) { + return rc; + } + + { + uint8_t ax25_frame[HYBBX_AX25_FRAME_MAX]; + size_t ax25_len = hybbx_ax25_build_ui(&path, + (const uint8_t *)message, msg_len, + ax25_frame, sizeof(ax25_frame)); + + if (ax25_len == 0) { + return HYBBX_ERR_IO; + } + + hybbx_log_debug("[broadcast] ax25 path %s>%s payload=%zu", + path.source.call, path.dest.call, msg_len); + + frame_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25, + HYBBX_CIRCUIT_FLAG_TX, + ax25_frame, ax25_len, + frame, sizeof(frame)); + } + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + rc = hybbx_circuit_hub_multicast_hbx(hub, frame, frame_len, + frequency_mhz, 1, &sent_links); + if (rc != HYBBX_OK) { + return rc; + } + + if (per_link && frequency_mhz > 0.0) { + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned i; + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + for (i = 0; i < link_count; i++) { + if (links[i].frequency_mhz <= 0.0 || + !hybbx_ax25_frequency_match(frequency_mhz, + links[i].frequency_mhz)) { + continue; + } + broadcast_ax25_mark_link_sent(links[i].slot_index); + } + } else { + broadcast_ax25_mark_sent(); + } + + if (frequency_mhz > 0.0) { + hybbx_log_stats("[broadcast] ax25 %.3f MHz (%u hub): %s", + frequency_mhz, sent_links, message); + } else { + hybbx_log_stats("[broadcast] ax25 all qualifying links (%u hub): %s", + sent_links, message); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_broadcast_ax25(hybbx_service_t *service, + double frequency_mhz, + const char *message) +{ + return broadcast_ax25_send(service, frequency_mhz, message, 1); +} + +static int broadcast_ax25_seq_start(hybbx_service_t *service, + hybbx_circuit_hub_t *hub, + const hybbx_circuit_broadcast_link_t *links, + unsigned link_count, + const char *message) +{ + if (g_ax25_seq.active || service == NULL || hub == NULL || + links == NULL || link_count == 0 || message == NULL || + message[0] == '\0' || link_count > HYBBX_CIRCUIT_MAX_LINKS) { + return 0; + } + + memset(&g_ax25_seq, 0, sizeof(g_ax25_seq)); + g_ax25_seq.active = 1; + g_ax25_seq.service = service; + g_ax25_seq.hub = hub; + memcpy(g_ax25_seq.links, links, + link_count * sizeof(g_ax25_seq.links[0])); + g_ax25_seq.link_count = link_count; + hybbx_strlcpy(g_ax25_seq.message, message, sizeof(g_ax25_seq.message)); + return 1; +} + +static void broadcast_ax25_seq_advance(void) +{ + const hybbx_broadcast_config_t *cfg; + time_t now; + unsigned idx; + double target_mhz; + hybbx_result_t rc; + int advance = 0; + + if (!g_ax25_seq.active) { + return; + } + + if (g_ax25_seq.hub == NULL || + !hybbx_circuit_hub_running(g_ax25_seq.hub)) { + g_ax25_seq.active = 0; + return; + } + + now = time(NULL); + if (now == (time_t)-1) { + return; + } + + if (g_ax25_seq.resume_at != 0 && now < g_ax25_seq.resume_at) { + return; + } + + if (g_ax25_seq.next_index >= g_ax25_seq.link_count) { + g_ax25_seq.active = 0; + return; + } + + cfg = hybbx_service_get_broadcast(g_ax25_seq.service); + if (cfg == NULL) { + g_ax25_seq.active = 0; + return; + } + + idx = g_ax25_seq.next_index; + target_mhz = broadcast_link_target_mhz(cfg, &g_ax25_seq.links[idx], idx); + + if (target_mhz <= 0.0) { + hybbx_log_stats("[broadcast] ax25 skip link=%s (no MHz)", + g_ax25_seq.links[idx].link_id); + advance = 1; + } else if (!broadcast_hub_link_band_idle(g_ax25_seq.hub, + g_ax25_seq.links[idx].slot_index)) { + time_t ready = hybbx_circuit_hub_link_band_ready_at( + g_ax25_seq.hub, g_ax25_seq.links[idx].slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC); + + broadcast_ax25_seq_defer_until(ready, target_mhz, "band busy"); + return; + } else if (!broadcast_ax25_link_rate_ok(g_ax25_seq.links[idx].slot_index, + HYBBX_BROADCAST_AX25_LINK_MIN_SEC)) { + broadcast_ax25_seq_defer_until( + now + (time_t)HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC, + target_mhz, "link rate"); + return; + } else { + rc = broadcast_ax25_send_slot(g_ax25_seq.service, + g_ax25_seq.links[idx].slot_index, + g_ax25_seq.message, 0); + if (rc == HYBBX_OK) { + g_ax25_defer_log_sec = 0; + advance = 1; + } else if (rc == HYBBX_ERR_BUSY) { + if (!hybbx_circuit_hub_running(g_ax25_seq.hub)) { + g_ax25_seq.active = 0; + return; + } + { + time_t ready = hybbx_circuit_hub_link_band_ready_at( + g_ax25_seq.hub, g_ax25_seq.links[idx].slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC); + + broadcast_ax25_seq_defer_until(ready, target_mhz, + "band busy"); + } + return; + } else if (rc == HYBBX_ERR_DENIED) { + broadcast_ax25_seq_defer_until( + now + (time_t)HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC, + target_mhz, "no qualifying link"); + return; + } else { + hybbx_log_warn("[broadcast] ax25 %.3f MHz failed (%d)", + target_mhz, (int)rc); + advance = 1; + } + } + + if (!advance) { + return; + } + + g_ax25_seq.next_index++; + if (g_ax25_seq.next_index < g_ax25_seq.link_count) { + g_ax25_seq.resume_at = now + (time_t)HYBBX_BROADCAST_AX25_LINK_GAP_SEC; + } else { + g_ax25_seq.active = 0; + g_ax25_seq.resume_at = 0; + } +} + +void hybbx_broadcast_ax25_seq_cancel(void) +{ + g_ax25_seq.active = 0; + g_ax25_seq.resume_at = 0; +} + +hybbx_result_t hybbx_broadcast_ax25_manual(hybbx_service_t *service) +{ + const hybbx_broadcast_config_t *cfg; + hybbx_circuit_hub_t *hub; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + char message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + unsigned link_count; + + if (service == NULL) { + return HYBBX_ERR_INVALID; + } + + if (g_ax25_seq.active) { + return HYBBX_ERR_BUSY; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return HYBBX_ERR_BUSY; + } + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + if (link_count == 0) { + return HYBBX_ERR_DENIED; + } + + broadcast_expand_service_token(message, sizeof(message), + cfg->ax25_auto_message, + hybbx_service_get_name(service)); + if (message[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + hybbx_log_info("[broadcast] ax25 manual sequential (%u link(s)): %s", + link_count, message); + + if (!broadcast_ax25_seq_start(service, hub, links, link_count, message)) { + return HYBBX_ERR_BUSY; + } + + broadcast_ax25_seq_advance(); + return HYBBX_OK; +} + +void hybbx_broadcast_ax25_tick(hybbx_service_t *service) +{ + const hybbx_broadcast_config_t *cfg; + char message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + hybbx_circuit_hub_t *hub; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned interval; + + if (service == NULL) { + return; + } + + if (g_ax25_seq.active) { + broadcast_ax25_seq_advance(); + return; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled || !cfg->ax25_auto) { + return; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return; + } + + interval = cfg->ax25_auto_interval_sec; + if (interval == 0) { + return; + } + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + if (link_count == 0) { + return; + } + + g_ax25_auto_tick++; + + broadcast_expand_service_token(message, sizeof(message), + cfg->ax25_auto_message, + hybbx_service_get_name(service)); + if (message[0] == '\0') { + return; + } + + if (g_ax25_auto_tick < interval) { + return; + } + if (!broadcast_ax25_rate_ok(interval)) { + return; + } + + hybbx_log_info("[broadcast] ax25 auto sequential (%u link(s)): %s", + link_count, message); + + if (!broadcast_ax25_seq_start(service, hub, links, link_count, message)) { + return; + } + + g_ax25_auto_tick = 0; + broadcast_ax25_seq_advance(); +} + +typedef struct broadcast_announce_ctx { + hybbx_session_t *from; + const char *from_name; + const char *message; +} broadcast_announce_ctx_t; + +static void broadcast_announce_visitor(hybbx_session_t *session, void *userdata) +{ + broadcast_announce_ctx_t *ctx = (broadcast_announce_ctx_t *)userdata; + char line[HYBBX_LINE_MAX]; + + if (session == NULL || ctx == NULL || ctx->message == NULL) { + return; + } + + /* + * Local /broadcast is for online human users only. Circuit link adapter + * sessions (HBX bridges to TNCs) must not receive terminal fan-out — + * it floods low-bandwidth circuit queues and triggers load-balance + * pause/break on packet-radio links. + */ + if (!hybbx_session_is_interactive_user(session)) { + return; + } + if (!hybbx_session_logged_in(session)) { + return; + } + + if (hybbx_msg_format_sysop(line, sizeof(line), + ctx->from_name != NULL ? ctx->from_name : "Announce", + ctx->message) != HYBBX_OK) { + return; + } + if (ctx->from == NULL || session != ctx->from) { + (void)hybbx_session_command_gap(session); + } + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_broadcast_announce(hybbx_service_t *service, + hybbx_session_t *from, + const char *message) +{ + broadcast_announce_ctx_t ctx; + const hybbx_broadcast_config_t *cfg; + size_t msg_len; + + if (service == NULL || message == NULL || message[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + msg_len = strlen(message); + if (msg_len > HYBBX_BROADCAST_MESSAGE_MAX) { + return HYBBX_ERR_INVALID; + } + + ctx.from = from; + ctx.from_name = (from != NULL) ? hybbx_session_display_name(from) : "Announce"; + ctx.message = message; + hybbx_service_visit_sessions(service, broadcast_announce_visitor, &ctx); + + hybbx_log_stats("[broadcast] announce to local Main (%zu bytes): %s", + msg_len, message); + return HYBBX_OK; +} diff --git a/src/core/chat.c b/src/core/chat.c new file mode 100644 index 0000000..fdd4fa4 --- /dev/null +++ b/src/core/chat.c @@ -0,0 +1,463 @@ +#include "hybbx/chat.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/traffic.h" +#include "hybbx/log.h" + +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int channel_name_char_ok(char ch) +{ + return (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == ' ' || ch == '-' || ch == '_'; +} + +static int channel_name_valid(const char *name) +{ + size_t len; + size_t i; + + if (name == NULL) { + return 0; + } + + len = strlen(name); + if (len < 2 || len >= HYBBX_CHAT_CHANNEL_NAME_MAX) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!channel_name_char_ok(name[i])) { + return 0; + } + } + + return 1; +} + +void hybbx_chat_config_defaults(hybbx_chat_config_t *chat) +{ + unsigned i; + + if (chat == NULL) { + return; + } + + chat->channel_count = HYBBX_DEFAULT_CHAT_CHANNELS; + chat->message_max = HYBBX_CHAT_MESSAGE_MAX; + + for (i = 0; i < HYBBX_CHAT_CHANNEL_MAX; i++) { + snprintf(chat->names[i], sizeof(chat->names[i]), "Channel%u", i + 1); + } +} + +void hybbx_chat_config_apply(hybbx_chat_config_t *chat, + const hybbx_config_t *config) +{ + unsigned count; + unsigned i; + char key[16]; + const char *value; + + if (chat == NULL || config == NULL) { + return; + } + + hybbx_chat_config_defaults(chat); + + count = hybbx_config_get_uint(config, "chat", "channels", + HYBBX_DEFAULT_CHAT_CHANNELS, 1u, + HYBBX_CHAT_CHANNEL_MAX); + chat->channel_count = count; + + chat->message_max = hybbx_config_get_uint(config, "chat", "message_max", + HYBBX_CHAT_MESSAGE_MAX, 1u, + HYBBX_LINE_MAX - 1u); + + for (i = 1; i <= chat->channel_count; i++) { + snprintf(key, sizeof(key), "channel%u", i); + value = hybbx_config_get(config, "chat", key, NULL); + if (value != NULL && value[0] != '\0' && channel_name_valid(value)) { + hybbx_strlcpy(chat->names[i - 1], value, + sizeof(chat->names[i - 1])); + } + } + + hybbx_log_info("[chat] channels=%u message_max=%u", + chat->channel_count, chat->message_max); +} + +const char *hybbx_chat_channel_name(const hybbx_chat_config_t *chat, + unsigned channel_index) +{ + if (chat == NULL || channel_index == 0 || + channel_index > chat->channel_count) { + return NULL; + } + + return chat->names[channel_index - 1]; +} + +hybbx_result_t hybbx_chat_resolve_channel(const hybbx_chat_config_t *chat, + const char *spec, + unsigned *out_index) +{ + char *end; + unsigned long parsed; + unsigned i; + + if (chat == NULL || spec == NULL || out_index == NULL || spec[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + parsed = strtoul(spec, &end, 10); + if (end != spec && *end == '\0' && parsed >= 1 && + parsed <= chat->channel_count) { + *out_index = (unsigned)parsed; + return HYBBX_OK; + } + + for (i = 1; i <= chat->channel_count; i++) { + if (str_ieq(chat->names[i - 1], spec)) { + *out_index = i; + return HYBBX_OK; + } + } + + return HYBBX_ERR_NOT_FOUND; +} + +void hybbx_chat_list_channels(hybbx_session_t *session, + const hybbx_chat_config_t *chat) +{ + unsigned i; + char line[HYBBX_CHAT_CHANNEL_NAME_MAX + 32]; + unsigned current; + + if (session == NULL || chat == NULL) { + return; + } + + current = hybbx_session_chat_channel(session); + + hybbx_session_write_line(session, "Channels:"); + for (i = 1; i <= chat->channel_count; i++) { + snprintf(line, sizeof(line), " %u %s", i, chat->names[i - 1]); + hybbx_session_write_line(session, line); + } + + if (current > 0) { + snprintf(line, sizeof(line), "Current: %u %s", + current, hybbx_chat_channel_name(chat, current)); + hybbx_session_write_line(session, line); + } else { + hybbx_session_write_line(session, "Use /chat <n> or <name>."); + } +} + +typedef struct chat_broadcast_ctx { + hybbx_session_t *from; + const char *message; + const char *from_user; + unsigned channel; +} chat_broadcast_ctx_t; + +static void chat_broadcast_visitor(hybbx_session_t *session, void *userdata) +{ + chat_broadcast_ctx_t *ctx = (chat_broadcast_ctx_t *)userdata; + char line[HYBBX_USER_NAME_MAX + HYBBX_LINE_MAX + 16]; + + if (session == NULL || ctx == NULL || ctx->message == NULL) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + return; + } + + if (hybbx_session_chat_channel(session) != ctx->channel) { + return; + } + + if (session == ctx->from) { + snprintf(line, sizeof(line), "ME: %s", ctx->message); + } else { + snprintf(line, sizeof(line), "%s: %s", ctx->from_user, ctx->message); + } + + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_chat_post(hybbx_service_t *service, + hybbx_session_t *from, + const char *message) +{ + chat_broadcast_ctx_t ctx; + const hybbx_chat_config_t *chat; + unsigned channel; + + if (service == NULL || from == NULL || message == NULL) { + return HYBBX_ERR_INVALID; + } + + if (message[0] == '\0') { + return HYBBX_OK; + } + + chat = hybbx_service_get_chat(service); + if (chat == NULL) { + return HYBBX_ERR_INVALID; + } + + if (strlen(message) > chat->message_max) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_session_area(from) != HYBBX_AREA_CHAT) { + return HYBBX_ERR_INVALID; + } + + channel = hybbx_session_chat_channel(from); + if (channel == 0) { + return HYBBX_ERR_INVALID; + } + + ctx.from = from; + ctx.message = message; + ctx.from_user = hybbx_session_display_name(from); + ctx.channel = channel; + + hybbx_service_visit_sessions(service, chat_broadcast_visitor, &ctx); + return HYBBX_OK; +} + +#define CHAT_SHOWALL_MAX 128u + +typedef struct chat_show_entry { + unsigned channel; + char label[HYBBX_USER_NAME_MAX + HYBBX_CHAT_CHANNEL_NAME_MAX + 4]; +} chat_show_entry_t; + +typedef struct chat_show_ctx { + hybbx_session_t *requester; + unsigned channel; + unsigned count; + chat_show_entry_t entries[CHAT_SHOWALL_MAX]; + const hybbx_chat_config_t *chat; +} chat_show_ctx_t; + +static void chat_show_channel_visitor(hybbx_session_t *session, void *userdata) +{ + chat_show_ctx_t *ctx = (chat_show_ctx_t *)userdata; + + if (session == NULL || ctx == NULL || session == ctx->requester) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + return; + } + + if (hybbx_session_chat_channel(session) != ctx->channel) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + hybbx_session_write_line(ctx->requester, hybbx_session_display_name(session)); +} + +void hybbx_chat_show_channel(hybbx_service_t *service, hybbx_session_t *session) +{ + chat_show_ctx_t ctx; + unsigned channel; + + if (service == NULL || session == NULL) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + hybbx_session_write_line(session, "Not in a chat channel."); + return; + } + + channel = hybbx_session_chat_channel(session); + if (channel == 0) { + hybbx_session_write_line(session, "Not in a chat channel."); + return; + } + + ctx.requester = session; + ctx.channel = channel; + ctx.count = 0; + ctx.chat = NULL; + + hybbx_service_visit_sessions(service, chat_show_channel_visitor, &ctx); +} + +static void chat_show_all_collect(hybbx_session_t *session, void *userdata) +{ + chat_show_ctx_t *ctx = (chat_show_ctx_t *)userdata; + unsigned channel; + const char *ch_name; + chat_show_entry_t *entry; + + if (session == NULL || ctx == NULL || ctx->chat == NULL) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + channel = hybbx_session_chat_channel(session); + if (channel == 0 || channel > ctx->chat->channel_count) { + return; + } + + if (ctx->count >= CHAT_SHOWALL_MAX) { + return; + } + + ch_name = hybbx_chat_channel_name(ctx->chat, channel); + if (ch_name == NULL) { + return; + } + + entry = &ctx->entries[ctx->count++]; + entry->channel = channel; + snprintf(entry->label, sizeof(entry->label), "%s@%s", + hybbx_session_display_name(session), ch_name); +} + +static int chat_show_entry_cmp(const void *a, const void *b) +{ + const chat_show_entry_t *ea = (const chat_show_entry_t *)a; + const chat_show_entry_t *eb = (const chat_show_entry_t *)b; + + if (ea->channel != eb->channel) { + return (ea->channel < eb->channel) ? -1 : 1; + } + + return strcmp(ea->label, eb->label); +} + +static void chat_showall_flush(hybbx_session_t *session, char *line, size_t *pos) +{ + if (session == NULL || line == NULL || pos == NULL || *pos == 0) { + return; + } + + line[*pos] = '\0'; + hybbx_session_write_line(session, line); + line[0] = '\0'; + *pos = 0; +} + +static void chat_showall_append(hybbx_session_t *session, char *line, + size_t line_size, size_t *pos, + const char *entry) +{ + size_t entry_len; + size_t need; + + if (session == NULL || line == NULL || pos == NULL || entry == NULL) { + return; + } + + entry_len = strlen(entry); + need = (*pos > 0) ? (*pos + 1 + entry_len) : entry_len; + + if (*pos > 0 && need > HYBBX_LINE_WIDTH) { + chat_showall_flush(session, line, pos); + need = entry_len; + } + + if (entry_len >= line_size) { + return; + } + + if (*pos > 0) { + line[(*pos)++] = ' '; + } + + memcpy(line + *pos, entry, entry_len); + *pos += entry_len; + line[*pos] = '\0'; +} + +void hybbx_chat_show_all(hybbx_service_t *service, hybbx_session_t *session) +{ + chat_show_ctx_t ctx; + char line[HYBBX_LINE_WIDTH + 1]; + size_t pos = 0; + unsigned i; + + if (service == NULL || session == NULL) { + return; + } + + ctx.requester = session; + ctx.channel = 0; + ctx.count = 0; + ctx.chat = hybbx_service_get_chat(service); + if (ctx.chat == NULL) { + return; + } + + hybbx_service_visit_sessions(service, chat_show_all_collect, &ctx); + + if (ctx.count == 0) { + return; + } + + qsort(ctx.entries, ctx.count, sizeof(ctx.entries[0]), chat_show_entry_cmp); + + line[0] = '\0'; + for (i = 0; i < ctx.count; i++) { + chat_showall_append(session, line, sizeof(line), &pos, ctx.entries[i].label); + } + + chat_showall_flush(session, line, &pos); +} diff --git a/src/core/circuit.c b/src/core/circuit.c new file mode 100644 index 0000000..04ac264 --- /dev/null +++ b/src/core/circuit.c @@ -0,0 +1,337 @@ +#include "hybbx/circuit.h" + +#include <string.h> + +static uint32_t read_be32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +static uint16_t read_be16(const uint8_t *p) +{ + return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]); +} + +static void write_be32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)((v >> 24) & 0xFFu); + p[1] = (uint8_t)((v >> 16) & 0xFFu); + p[2] = (uint8_t)((v >> 8) & 0xFFu); + p[3] = (uint8_t)(v & 0xFFu); +} + +static void write_be16(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)((v >> 8) & 0xFFu); + p[1] = (uint8_t)(v & 0xFFu); +} + +const char *hybbx_circuit_proto_name(hybbx_circuit_proto_t proto) +{ + switch (proto) { + case HYBBX_CIRCUIT_PROTO_AX25: + return "ax25"; + case HYBBX_CIRCUIT_PROTO_AX25_UI: + return "ax25_ui"; + case HYBBX_CIRCUIT_PROTO_LINK_AUTH: + return "link_auth"; + case HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK: + return "link_auth_ack"; + case HYBBX_CIRCUIT_PROTO_FLOW_CTRL: + return "flow_ctrl"; + case HYBBX_CIRCUIT_PROTO_TERMINAL: + return "terminal"; + case HYBBX_CIRCUIT_PROTO_PROXY_MAIL: + return "proxy_mail"; + case HYBBX_CIRCUIT_PROTO_PROXY_CHAT: + return "proxy_chat"; + case HYBBX_CIRCUIT_PROTO_RESERVED_APRS: + return "aprs"; + case HYBBX_CIRCUIT_PROTO_RESERVED_NETROM: + return "netrom"; + default: + return "unknown"; + } +} + +void hybbx_circuit_decoder_init(hybbx_circuit_decoder_t *dec) +{ + if (dec == NULL) { + return; + } + + memset(dec, 0, sizeof(*dec)); +} + +static void circuit_deliver(hybbx_circuit_decoder_t *dec, + hybbx_circuit_frame_cb cb, void *userdata) +{ + const uint8_t *payload; + size_t payload_len; + + if (dec->len < HYBBX_CIRCUIT_HEADER_SIZE) { + dec->len = 0; + dec->need = 0; + dec->have_header = 0; + return; + } + + payload = dec->buf + HYBBX_CIRCUIT_HEADER_SIZE; + payload_len = dec->len - HYBBX_CIRCUIT_HEADER_SIZE; + + if (cb != NULL) { + cb(dec->proto, dec->flags, payload, payload_len, userdata); + } + + dec->len = 0; + dec->need = 0; + dec->have_header = 0; +} + +static int circuit_parse_header(hybbx_circuit_decoder_t *dec) +{ + const uint8_t *h = dec->buf; + + if (h[0] != HYBBX_CIRCUIT_MAGIC_0 || h[1] != HYBBX_CIRCUIT_MAGIC_1 || + h[2] != HYBBX_CIRCUIT_MAGIC_2 || h[3] != HYBBX_CIRCUIT_VERSION) { + return 0; + } + + dec->proto = (hybbx_circuit_proto_t)h[4]; + dec->flags = read_be16(h + 5); + dec->need = (size_t)read_be32(h + 7); + if (dec->need > HYBBX_CIRCUIT_MAX_PAYLOAD) { + return 0; + } + + return 1; +} + +void hybbx_circuit_decoder_feed(hybbx_circuit_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_circuit_frame_cb cb, void *userdata) +{ + size_t i; + + if (dec == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + if (dec->len >= sizeof(dec->buf)) { + hybbx_circuit_decoder_init(dec); + } + + dec->buf[dec->len++] = data[i]; + + if (!dec->have_header) { + if (dec->len < HYBBX_CIRCUIT_HEADER_SIZE) { + continue; + } + if (!circuit_parse_header(dec)) { + memmove(dec->buf, dec->buf + 1, dec->len - 1); + dec->len--; + continue; + } + dec->have_header = 1; + } + + if (dec->len < HYBBX_CIRCUIT_HEADER_SIZE + dec->need) { + continue; + } + + circuit_deliver(dec, cb, userdata); + } +} + +size_t hybbx_circuit_encode(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + if (out == NULL || out_cap < HYBBX_CIRCUIT_HEADER_SIZE) { + return 0; + } + + if (payload_len > HYBBX_CIRCUIT_MAX_PAYLOAD) { + return 0; + } + + if (payload_len > 0 && payload == NULL) { + return 0; + } + + if (HYBBX_CIRCUIT_HEADER_SIZE + payload_len > out_cap) { + return 0; + } + + out[0] = HYBBX_CIRCUIT_MAGIC_0; + out[1] = HYBBX_CIRCUIT_MAGIC_1; + out[2] = HYBBX_CIRCUIT_MAGIC_2; + out[3] = HYBBX_CIRCUIT_VERSION; + out[4] = (uint8_t)proto; + write_be16(out + 5, flags); + + write_be32(out + 7, (uint32_t)payload_len); + if (payload_len > 0) { + memcpy(out + HYBBX_CIRCUIT_HEADER_SIZE, payload, payload_len); + } + + return HYBBX_CIRCUIT_HEADER_SIZE + payload_len; +} + +size_t hybbx_circuit_encode_ax25(const uint8_t *frame, size_t frame_len, + uint16_t flags, + uint8_t *out, size_t out_cap) +{ + return hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25, + (uint16_t)(flags | HYBBX_CIRCUIT_FLAG_RX), + frame, frame_len, out, out_cap); +} + +static size_t pack_path_payload(const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + size_t pos = 0; + unsigned i; + + if (path == NULL || out == NULL) { + return 0; + } + + if (1 + payload_len > out_cap) { + return 0; + } + + out[pos++] = (uint8_t)path->digi_count; + if (pos + HYBBX_AX25_ADDR_ENCODED * (2 + path->digi_count) > out_cap) { + return 0; + } + + hybbx_ax25_encode_address(out + pos, &path->dest, 0); + pos += HYBBX_AX25_ADDR_ENCODED; + for (i = 0; i < path->digi_count; i++) { + hybbx_ax25_encode_address(out + pos, &path->digi[i], 0); + pos += HYBBX_AX25_ADDR_ENCODED; + } + hybbx_ax25_encode_address(out + pos, &path->source, 1); + pos += HYBBX_AX25_ADDR_ENCODED; + + if (payload_len > 0) { + if (pos + payload_len > out_cap) { + return 0; + } + memcpy(out + pos, payload, payload_len); + pos += payload_len; + } + + return pos; +} + +size_t hybbx_circuit_encode_ax25_ui(const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t payload_len, + uint16_t flags, + uint8_t *out, size_t out_cap) +{ + uint8_t body[HYBBX_CIRCUIT_MAX_PAYLOAD]; + size_t body_len; + + body_len = pack_path_payload(path, payload, payload_len, body, sizeof(body)); + if (body_len == 0 && payload_len > 0) { + return 0; + } + + return hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25_UI, + (uint16_t)(flags | HYBBX_CIRCUIT_FLAG_PATH), + body, body_len, out, out_cap); +} + +size_t hybbx_circuit_encode_terminal(const char *data, size_t len, + uint8_t *out, size_t out_cap) +{ + return hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_TERMINAL, + HYBBX_CIRCUIT_FLAG_TX, + (const uint8_t *)data, len, out, out_cap); +} + +size_t hybbx_circuit_encode_link_msg(hybbx_circuit_proto_t proto, + const char *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + if (proto != HYBBX_CIRCUIT_PROTO_LINK_AUTH && + proto != HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK) { + return 0; + } + + return hybbx_circuit_encode(proto, HYBBX_CIRCUIT_FLAG_NONE, + (const uint8_t *)payload, payload_len, + out, out_cap); +} + +static void decode_address_field(const uint8_t *in, hybbx_ax25_address_t *out) +{ + size_t i; + + for (i = 0; i < HYBBX_AX25_CALL_MAX; i++) { + char ch = (char)((in[i] >> 1) & 0x7Fu); + out->call[i] = ch == ' ' ? '\0' : ch; + } + out->call[HYBBX_AX25_CALL_MAX] = '\0'; + out->ssid = (unsigned)((in[6] >> 1) & 0x0Fu); +} + +size_t hybbx_circuit_unpack_ax25_ui(const uint8_t *payload, size_t payload_len, + hybbx_ax25_path_t *path, + uint8_t *ui, size_t ui_cap) +{ + size_t pos = 0; + unsigned digi_count; + unsigned i; + + if (payload == NULL || payload_len < 1) { + return 0; + } + + if (path != NULL) { + memset(path, 0, sizeof(*path)); + } + + digi_count = payload[0]; + if (digi_count > HYBBX_AX25_MAX_DIGI) { + return 0; + } + + pos = 1; + if (pos + HYBBX_AX25_ADDR_ENCODED * (size_t)(2 + digi_count) > payload_len) { + return 0; + } + + if (path != NULL) { + decode_address_field(payload + pos, &path->dest); + pos += HYBBX_AX25_ADDR_ENCODED; + for (i = 0; i < digi_count; i++) { + decode_address_field(payload + pos, &path->digi[i]); + pos += HYBBX_AX25_ADDR_ENCODED; + } + path->digi_count = digi_count; + decode_address_field(payload + pos, &path->source); + } else { + pos += HYBBX_AX25_ADDR_ENCODED * (size_t)(2 + digi_count); + } + pos += HYBBX_AX25_ADDR_ENCODED; + + { + size_t ui_len = payload_len - pos; + + if (ui_len > ui_cap) { + return 0; + } + + if (ui_len > 0 && ui != NULL) { + memcpy(ui, payload + pos, ui_len); + } + + return ui_len; + } +} diff --git a/src/core/circuit_balance.c b/src/core/circuit_balance.c new file mode 100644 index 0000000..5571e9d --- /dev/null +++ b/src/core/circuit_balance.c @@ -0,0 +1,545 @@ +#include "hybbx/circuit_balance.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +typedef struct balance_frame { + uint8_t data[HYBBX_CIRCUIT_MAX_FRAME]; + size_t len; +} balance_frame_t; + +struct hybbx_circuit_balance { + hybbx_circuit_balance_config_t cfg; + hybbx_circuit_link_profile_t profile; + balance_frame_t queue[HYBBX_CIRCUIT_BALANCE_QUEUE_MAX]; + size_t q_head; + size_t q_count; + size_t queue_bytes; + hybbx_circuit_balance_action_t action; + hybbx_circuit_balance_action_t last_sent_action; + double drain_accum; + time_t oldest_enqueue; + int link_low_bw; +}; + +static int str_ieq_local(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = *a; + char cb = *b; + + if (ca >= 'A' && ca <= 'Z') { + ca = (char)(ca - 'A' + 'a'); + } + if (cb >= 'A' && cb <= 'Z') { + cb = (char)(cb - 'A' + 'a'); + } + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv_line(const char *payload, size_t len, + const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = payload; + const char *end = payload + len; + size_t key_len = strlen(key); + + while (cursor < end) { + const char *line_end = memchr(cursor, '\n', (size_t)(end - cursor)); + const char *line_stop = line_end != NULL ? line_end : end; + const char *eq = memchr(cursor, '=', (size_t)(line_stop - cursor)); + + if (eq != NULL && (size_t)(eq - cursor) == key_len && + memcmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(line_stop - value); + + while (value_len > 0 && + (value[value_len - 1] == '\r' || + value[value_len - 1] == '\n' || + value[value_len - 1] == ' ')) { + value_len--; + } + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (line_end == NULL) { + break; + } + cursor = line_end + 1; + } + + return NULL; +} + +void hybbx_circuit_balance_config_defaults(hybbx_circuit_balance_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + cfg->enabled = 1; + cfg->lag_sec = 5u; + cfg->queue_pause = 4096u; + cfg->queue_break = 16384u; + cfg->queue_cancel = 65536u; +} + +void hybbx_circuit_link_profile_from_auth(const hybbx_link_auth_t *auth, + hybbx_circuit_link_profile_t *out) +{ + unsigned baud; + + if (out == NULL) { + return; + } + + memset(out, 0, sizeof(*out)); + out->bandwidth = HYBBX_CIRCUIT_BW_HIGH; + out->duplex = HYBBX_CIRCUIT_DUPLEX_FULL; + out->baud = HYBBX_CIRCUIT_BALANCE_DEFAULT_BAUD; + + if (auth == NULL) { + return; + } + + baud = auth->baud; + out->duplex = (hybbx_circuit_duplex_t)auth->duplex; + + if (auth->bandwidth[0] != '\0') { + if (str_ieq_local(auth->bandwidth, "low")) { + out->bandwidth = HYBBX_CIRCUIT_BW_LOW; + } else if (str_ieq_local(auth->bandwidth, "high")) { + out->bandwidth = HYBBX_CIRCUIT_BW_HIGH; + } + } else if (baud > 0 && baud <= HYBBX_CIRCUIT_BALANCE_LOW_BAUD_THRESHOLD) { + out->bandwidth = HYBBX_CIRCUIT_BW_LOW; + } + + if (baud > 0) { + out->baud = baud; + } else if (out->bandwidth == HYBBX_CIRCUIT_BW_LOW) { + out->baud = 1200u; + } + + if (out->duplex == HYBBX_CIRCUIT_DUPLEX_UNSET) { + out->duplex = out->bandwidth == HYBBX_CIRCUIT_BW_LOW ? + HYBBX_CIRCUIT_DUPLEX_HALF : HYBBX_CIRCUIT_DUPLEX_FULL; + } + + if (auth->frequency_mhz[0] != '\0') { + out->frequency_mhz = strtod(auth->frequency_mhz, NULL); + } +} + +const char *hybbx_circuit_balance_action_name(hybbx_circuit_balance_action_t action) +{ + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: + return "pause"; + case HYBBX_CIRCUIT_BAL_BREAK: + return "break"; + case HYBBX_CIRCUIT_BAL_CANCEL: + return "cancel"; + case HYBBX_CIRCUIT_BAL_RESUME: + return "resume"; + case HYBBX_CIRCUIT_BAL_NONE: + default: + return "none"; + } +} + +hybbx_result_t hybbx_circuit_flow_ctrl_parse(const char *payload, size_t len, + hybbx_circuit_balance_action_t *action_out, + char *reason_out, size_t reason_cap) +{ + char scratch[128]; + const char *action_str; + + if (payload == NULL || action_out == NULL) { + return HYBBX_ERR_INVALID; + } + + action_str = find_kv_line(payload, len, "action", scratch, sizeof(scratch)); + if (action_str == NULL) { + return HYBBX_ERR_INVALID; + } + + if (str_ieq_local(action_str, "pause")) { + *action_out = HYBBX_CIRCUIT_BAL_PAUSE; + } else if (str_ieq_local(action_str, "break")) { + *action_out = HYBBX_CIRCUIT_BAL_BREAK; + } else if (str_ieq_local(action_str, "cancel")) { + *action_out = HYBBX_CIRCUIT_BAL_CANCEL; + } else if (str_ieq_local(action_str, "resume")) { + *action_out = HYBBX_CIRCUIT_BAL_RESUME; + } else { + return HYBBX_ERR_INVALID; + } + + if (reason_out != NULL && reason_cap > 0) { + reason_out[0] = '\0'; + if (find_kv_line(payload, len, "reason", scratch, sizeof(scratch)) != NULL) { + hybbx_strlcpy(reason_out, scratch, reason_cap); + } + } + + return HYBBX_OK; +} + +size_t hybbx_circuit_flow_ctrl_format(hybbx_circuit_balance_action_t action, + const char *reason, + char *out, size_t out_cap) +{ + int n; + const char *reason_str = reason != NULL && reason[0] != '\0' ? reason : "-"; + + if (out == NULL || out_cap == 0) { + return 0; + } + + n = snprintf(out, out_cap, "action=%s\nreason=%s\n", + hybbx_circuit_balance_action_name(action), reason_str); + if (n < 0 || (size_t)n >= out_cap) { + return 0; + } + + return (size_t)n; +} + +hybbx_circuit_balance_t *hybbx_circuit_balance_create( + const hybbx_circuit_balance_config_t *cfg) +{ + hybbx_circuit_balance_t *bal; + + bal = calloc(1, sizeof(*bal)); + if (bal == NULL) { + return NULL; + } + + if (cfg != NULL) { + bal->cfg = *cfg; + } else { + hybbx_circuit_balance_config_defaults(&bal->cfg); + } + + hybbx_circuit_link_profile_from_auth(NULL, &bal->profile); + bal->link_low_bw = 0; + return bal; +} + +void hybbx_circuit_balance_destroy(hybbx_circuit_balance_t *bal) +{ + free(bal); +} + +void hybbx_circuit_balance_set_profile(hybbx_circuit_balance_t *bal, + const hybbx_circuit_link_profile_t *profile) +{ + if (bal == NULL || profile == NULL) { + return; + } + + bal->profile = *profile; + bal->link_low_bw = profile->bandwidth == HYBBX_CIRCUIT_BW_LOW; +} + +hybbx_circuit_balance_action_t hybbx_circuit_balance_action( + const hybbx_circuit_balance_t *bal) +{ + if (bal == NULL) { + return HYBBX_CIRCUIT_BAL_NONE; + } + + return bal->action; +} + +size_t hybbx_circuit_balance_queued_bytes(const hybbx_circuit_balance_t *bal) +{ + if (bal == NULL) { + return 0; + } + + return bal->queue_bytes; +} + +void hybbx_circuit_balance_spared_cancel(hybbx_circuit_balance_t *bal) +{ + if (bal == NULL) { + return; + } + + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + bal->action = HYBBX_CIRCUIT_BAL_PAUSE; + bal->last_sent_action = HYBBX_CIRCUIT_BAL_NONE; + } +} + +static unsigned balance_effective_baud(const hybbx_circuit_balance_t *bal) +{ + unsigned baud; + + if (bal == NULL) { + return HYBBX_CIRCUIT_BALANCE_DEFAULT_BAUD; + } + + baud = bal->profile.baud; + if (baud == 0) { + baud = HYBBX_CIRCUIT_BALANCE_DEFAULT_BAUD; + } + return baud; +} + +static unsigned balance_estimated_lag_sec(const hybbx_circuit_balance_t *bal) +{ + unsigned baud; + unsigned lag; + + if (bal == NULL || bal->queue_bytes == 0) { + return 0; + } + + baud = balance_effective_baud(bal); + if (baud == 0) { + return 0; + } + + lag = (unsigned)((bal->queue_bytes * 8u) / baud); + if (bal->oldest_enqueue > 0) { + time_t now = time(NULL); + if (now > bal->oldest_enqueue) { + unsigned wall = (unsigned)(now - bal->oldest_enqueue); + if (wall > lag) { + lag = wall; + } + } + } + return lag; +} + +static void balance_send_flow(hybbx_circuit_balance_t *bal, + hybbx_circuit_balance_action_t action, + const char *reason, + hybbx_circuit_balance_flow_fn flow_fn, + void *flow_ctx) +{ + if (bal == NULL || flow_fn == NULL) { + return; + } + if (action == bal->last_sent_action) { + return; + } + + flow_fn(flow_ctx, action, reason); + bal->last_sent_action = action; +} + +static void balance_clear_queue(hybbx_circuit_balance_t *bal) +{ + if (bal == NULL) { + return; + } + + bal->q_head = 0; + bal->q_count = 0; + bal->queue_bytes = 0; + bal->oldest_enqueue = 0; + bal->drain_accum = 0.0; +} + +static void balance_apply_escalation(hybbx_circuit_balance_t *bal, + hybbx_circuit_balance_flow_fn flow_fn, + void *flow_ctx) +{ + unsigned lag; + const hybbx_circuit_balance_config_t *cfg; + + if (bal == NULL || !bal->cfg.enabled) { + return; + } + + cfg = &bal->cfg; + lag = balance_estimated_lag_sec(bal); + + if (bal->link_low_bw && + (bal->queue_bytes >= cfg->queue_cancel || + lag >= cfg->lag_sec * 2u)) { + if (bal->action != HYBBX_CIRCUIT_BAL_CANCEL) { + bal->action = HYBBX_CIRCUIT_BAL_CANCEL; + balance_clear_queue(bal); + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_CANCEL, "queue_overload", + flow_fn, flow_ctx); + } + return; + } + + if (bal->queue_bytes >= cfg->queue_break || lag >= cfg->lag_sec) { + if (bal->action != HYBBX_CIRCUIT_BAL_BREAK && + bal->action != HYBBX_CIRCUIT_BAL_CANCEL) { + bal->action = HYBBX_CIRCUIT_BAL_BREAK; + balance_clear_queue(bal); + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_BREAK, "queue_lag", + flow_fn, flow_ctx); + bal->action = HYBBX_CIRCUIT_BAL_PAUSE; + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_PAUSE, "stabilize", + flow_fn, flow_ctx); + } + return; + } + + if (bal->queue_bytes >= cfg->queue_pause || + lag >= (cfg->lag_sec > 0 ? cfg->lag_sec / 2u : 0u)) { + if (bal->action == HYBBX_CIRCUIT_BAL_NONE) { + bal->action = HYBBX_CIRCUIT_BAL_PAUSE; + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_PAUSE, "queue_pressure", + flow_fn, flow_ctx); + } + return; + } + + if (bal->action == HYBBX_CIRCUIT_BAL_PAUSE && + bal->queue_bytes < cfg->queue_pause / 2u && + lag < (cfg->lag_sec > 0 ? cfg->lag_sec / 2u : 0u)) { + bal->action = HYBBX_CIRCUIT_BAL_NONE; + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_RESUME, "stabilized", + flow_fn, flow_ctx); + } +} + +hybbx_result_t hybbx_circuit_balance_submit(hybbx_circuit_balance_t *bal, + const uint8_t *frame, size_t len, + hybbx_circuit_balance_send_fn send_fn, + void *send_ctx, + hybbx_circuit_balance_flow_fn flow_fn, + void *flow_ctx) +{ + size_t slot; + balance_frame_t *entry; + + if (bal == NULL || frame == NULL || len == 0 || send_fn == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!bal->cfg.enabled || + bal->profile.bandwidth == HYBBX_CIRCUIT_BW_HIGH) { + return send_fn(send_ctx, frame, len); + } + + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_ERR_BUSY; + } + + if (bal->q_count >= HYBBX_CIRCUIT_BALANCE_QUEUE_MAX) { + balance_apply_escalation(bal, flow_fn, flow_ctx); + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_ERR_BUSY; + } + return HYBBX_ERR_NOMEM; + } + + if (len > sizeof(entry->data)) { + return HYBBX_ERR_INVALID; + } + + slot = (bal->q_head + bal->q_count) % HYBBX_CIRCUIT_BALANCE_QUEUE_MAX; + entry = &bal->queue[slot]; + memcpy(entry->data, frame, len); + entry->len = len; + bal->q_count++; + bal->queue_bytes += len; + if (bal->oldest_enqueue == 0) { + bal->oldest_enqueue = time(NULL); + } + + balance_apply_escalation(bal, flow_fn, flow_ctx); + return HYBBX_OK; +} + +hybbx_circuit_balance_tick_result_t hybbx_circuit_balance_tick( + hybbx_circuit_balance_t *bal, unsigned poll_ms, + hybbx_circuit_balance_send_fn send_fn, void *send_ctx, + hybbx_circuit_balance_flow_fn flow_fn, void *flow_ctx) +{ + unsigned baud; + double budget; + size_t sent_budget; + + if (bal == NULL || send_fn == NULL) { + return HYBBX_CIRCUIT_BAL_TICK_OK; + } + + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK; + } + + if (!bal->cfg.enabled || bal->q_count == 0) { + balance_apply_escalation(bal, flow_fn, flow_ctx); + return HYBBX_CIRCUIT_BAL_TICK_OK; + } + + if (bal->action == HYBBX_CIRCUIT_BAL_PAUSE) { + balance_apply_escalation(bal, flow_fn, flow_ctx); + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK; + } + return HYBBX_CIRCUIT_BAL_TICK_OK; + } + + baud = balance_effective_baud(bal); + if (poll_ms == 0) { + poll_ms = 1; + } + + budget = ((double)baud * (double)poll_ms) / 8000.0; + bal->drain_accum += budget; + sent_budget = (size_t)bal->drain_accum; + bal->drain_accum -= (double)sent_budget; + + while (sent_budget > 0 && bal->q_count > 0) { + balance_frame_t *entry = &bal->queue[bal->q_head]; + + if (entry->len > sent_budget) { + break; + } + + if (send_fn(send_ctx, entry->data, entry->len) != HYBBX_OK) { + break; + } + + sent_budget -= entry->len; + bal->queue_bytes -= entry->len; + bal->q_head = (bal->q_head + 1u) % HYBBX_CIRCUIT_BALANCE_QUEUE_MAX; + bal->q_count--; + } + + if (bal->q_count == 0) { + bal->oldest_enqueue = 0; + } + + balance_apply_escalation(bal, flow_fn, flow_ctx); + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK; + } + + return HYBBX_CIRCUIT_BAL_TICK_OK; +} diff --git a/src/core/circuit_bridge.c b/src/core/circuit_bridge.c new file mode 100644 index 0000000..af3bde4 --- /dev/null +++ b/src/core/circuit_bridge.c @@ -0,0 +1,175 @@ +#include "hybbx/circuit_bridge.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +typedef struct bridge_load_ctx { + hybbx_circuit_bridge_registry_t *reg; + const hybbx_config_t *reg_config; +} bridge_load_ctx_t; + +static int bridge_section_is_numbered_transport(const char *section, + const char *plugin) +{ + char prefix[64]; + size_t prefix_len; + const char *suffix; + + if (section == NULL || plugin == NULL) { + return 0; + } + + snprintf(prefix, sizeof(prefix), "transport.%s", plugin); + prefix_len = strlen(prefix); + if (strncmp(section, prefix, prefix_len) != 0) { + return 0; + } + + suffix = section + prefix_len; + if (suffix[0] == '\0') { + return 0; + } + + while (*suffix != '\0') { + if (*suffix < '0' || *suffix > '9') { + return 0; + } + suffix++; + } + + return 1; +} + +static int bridge_section_is_numbered_packet_radio(const char *section) +{ + return bridge_section_is_numbered_transport(section, "packet_radio"); +} + +static int bridge_section_is_numbered_ardop(const char *section) +{ + return bridge_section_is_numbered_transport(section, "ardop"); +} + +static int bridge_section_is_numbered_crdop(const char *section) +{ + return bridge_section_is_numbered_transport(section, "crdop"); +} + +static int bridge_section_is_numbered_baycom(const char *section) +{ + return bridge_section_is_numbered_transport(section, "baycom"); +} + +static void bridge_load_section(const char *section, void *userdata) +{ + bridge_load_ctx_t *ctx = (bridge_load_ctx_t *)userdata; + hybbx_circuit_bridge_entry_t *entry; + const char *value; + unsigned i; + + if (ctx == NULL || ctx->reg == NULL || section == NULL) { + return; + } + + if (!bridge_section_is_numbered_packet_radio(section) && + !bridge_section_is_numbered_ardop(section) && + !bridge_section_is_numbered_crdop(section) && + !bridge_section_is_numbered_baycom(section) && + !bridge_section_is_numbered_transport(section, "mains_proxy")) { + return; + } + + if (ctx->reg->count >= HYBBX_CIRCUIT_MAX_LINKS) { + return; + } + + entry = &ctx->reg->entries[ctx->reg->count]; + memset(entry, 0, sizeof(*entry)); + + value = hybbx_config_get(ctx->reg_config, section, "link_id", NULL); + if (value == NULL || value[0] == '\0') { + return; + } + hybbx_strlcpy(entry->link_id, value, sizeof(entry->link_id)); + + for (i = 0; i < ctx->reg->count; i++) { + if (strcmp(ctx->reg->entries[i].link_id, entry->link_id) == 0) { + fprintf(stderr, + "[circuit] bridge: duplicate link_id=%s in [%s] — skipped\n", + entry->link_id, section); + return; + } + } + + value = hybbx_config_get(ctx->reg_config, section, "link_password", NULL); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(entry->link_password, value, sizeof(entry->link_password)); + } + + value = hybbx_config_get(ctx->reg_config, section, "link_role", NULL); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(entry->link_role, value, sizeof(entry->link_role)); + } else if (bridge_section_is_numbered_transport(section, "mains_proxy")) { + hybbx_strlcpy(entry->link_role, "proxy", sizeof(entry->link_role)); + } else { + hybbx_strlcpy(entry->link_role, "link", sizeof(entry->link_role)); + } + + value = hybbx_config_get(ctx->reg_config, section, "frequency_mhz", NULL); + if (value == NULL || value[0] == '\0') { + value = hybbx_config_get(ctx->reg_config, section, "frequency", NULL); + } + if (value != NULL && value[0] != '\0') { + double mhz = strtod(value, NULL); + + if (mhz > 0.0) { + entry->frequency_mhz = mhz; + } + } + + ctx->reg->count++; +} + +void hybbx_circuit_bridge_clear(hybbx_circuit_bridge_registry_t *reg) +{ + if (reg == NULL) { + return; + } + memset(reg, 0, sizeof(*reg)); +} + +hybbx_result_t hybbx_circuit_bridge_load(hybbx_circuit_bridge_registry_t *reg, + const hybbx_config_t *config) +{ + bridge_load_ctx_t ctx; + + if (reg == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_circuit_bridge_clear(reg); + ctx.reg = reg; + ctx.reg_config = config; + hybbx_config_foreach_section(config, bridge_load_section, &ctx); + return HYBBX_OK; +} + +const hybbx_circuit_bridge_entry_t *hybbx_circuit_bridge_find( + const hybbx_circuit_bridge_registry_t *reg, const char *link_id) +{ + unsigned i; + + if (reg == NULL || link_id == NULL || link_id[0] == '\0') { + return NULL; + } + + for (i = 0; i < reg->count; i++) { + if (strcmp(reg->entries[i].link_id, link_id) == 0) { + return ®->entries[i]; + } + } + + return NULL; +} diff --git a/src/core/circuit_tcp.c b/src/core/circuit_tcp.c new file mode 100644 index 0000000..58f10e4 --- /dev/null +++ b/src/core/circuit_tcp.c @@ -0,0 +1,1837 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/circuit_tcp.h" +#include "hybbx/mains_proxy.h" +#include "hybbx/circuit.h" +#include "hybbx/broadcast.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/circuit_bridge.h" +#include "hybbx/bandwidth_policy.h" +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/security.h" +#include "hybbx/security_ban.h" +#include "hybbx/storage.h" +#include "hybbx/traffic.h" +#include "hybbx/link.h" +#include "hybbx/password.h" +#include "hybbx/socket.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <time.h> +#include <unistd.h> + +#define HYBBX_CIRCUIT_LINK_POLL_MS 50 +#define HYBBX_CIRCUIT_AUTH_TIMEOUT_MS 15000 + +typedef enum { + CIRCUIT_SLOT_FREE = 0, + CIRCUIT_SLOT_CONNECTING, + CIRCUIT_SLOT_ACTIVE +} circuit_slot_state_t; + +typedef struct hybbx_circuit_link_slot { + hybbx_circuit_hub_t *hub; + circuit_slot_state_t state; + int fd; + pthread_t thread; + char link_id[HYBBX_LINK_ID_MAX]; + hybbx_session_t *session; + hybbx_circuit_decoder_t decoder; + hybbx_circuit_balance_t *balance; + hybbx_circuit_link_profile_t profile; + int profile_set; + time_t last_rf_activity; +} hybbx_circuit_link_slot_t; + +struct hybbx_circuit_hub { + hybbx_service_t *service; + hybbx_circuit_config_t config; + hybbx_link_registry_t links; + hybbx_circuit_bridge_registry_t bridge; + unsigned max_links; + char link_password[128]; + int link_auth; + int listen_v4; + int listen_v6; + pthread_t accept_thread; + pthread_mutex_t lock; + volatile int running; + hybbx_circuit_link_slot_t slots[HYBBX_CIRCUIT_MAX_LINKS]; +}; + +void hybbx_circuit_config_defaults(hybbx_circuit_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + snprintf(cfg->bind4, sizeof(cfg->bind4), "127.0.0.1"); + snprintf(cfg->bind6, sizeof(cfg->bind6), "::1"); + cfg->port = HYBBX_CIRCUIT_DEFAULT_PORT; + cfg->ipv4 = 1; + cfg->ipv6 = 1; + cfg->link_stale_days = HYBBX_LINK_STALE_DAYS; + cfg->link_auth = 1; + hybbx_circuit_balance_config_defaults(&cfg->balance); + cfg->max_links = HYBBX_CIRCUIT_DEFAULT_MAX_LINKS; + hybbx_circuit_bridge_clear(&cfg->bridge); +} + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port, + int backlog) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, backlog > 0 ? backlog : 8) != 0) { + close(fd); + return -1; + } + + return fd; +} + +static int circuit_slot_broadcast_qos(const hybbx_circuit_link_slot_t *slot) +{ + if (slot == NULL || !slot->profile_set) { + return 0; + } + + return slot->profile.bandwidth == HYBBX_CIRCUIT_BW_LOW && + slot->profile.duplex == HYBBX_CIRCUIT_DUPLEX_HALF; +} + +static int circuit_frame_is_ax25_broadcast_tx(const uint8_t *frame, size_t len) +{ + unsigned flags; + uint8_t proto; + + if (frame == NULL || len < HYBBX_CIRCUIT_HEADER_SIZE) { + return 0; + } + + if (frame[0] != HYBBX_CIRCUIT_MAGIC_0 || + frame[1] != HYBBX_CIRCUIT_MAGIC_1 || + frame[2] != HYBBX_CIRCUIT_MAGIC_2 || + frame[3] != HYBBX_CIRCUIT_VERSION) { + return 0; + } + + proto = frame[4]; + if (proto != (uint8_t)HYBBX_CIRCUIT_PROTO_AX25_UI && + proto != (uint8_t)HYBBX_CIRCUIT_PROTO_AX25) { + return 0; + } + + flags = ((unsigned)frame[5] << 8u) | (unsigned)frame[6]; + return (flags & HYBBX_CIRCUIT_FLAG_TX) != 0; +} + +static int circuit_slot_can_send_low_prio(const hybbx_circuit_hub_t *hub, + const hybbx_circuit_link_slot_t *slot) +{ + hybbx_circuit_balance_action_t action; + + if (hub == NULL || slot == NULL || slot->balance == NULL || + !hub->config.balance.enabled || !slot->profile_set) { + return 1; + } + + action = hybbx_circuit_balance_action(slot->balance); + if (action == HYBBX_CIRCUIT_BAL_CANCEL) { + return 0; + } + + /* + * Auto-beacon AX.25 uses circuit_slot_send_raw (no queue slot). Do not + * block on PAUSE/BREAK backlog — only CANCEL drops the link. + */ + return 1; +} + +static unsigned circuit_count_used_slots(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + unsigned count = 0; + + if (hub == NULL) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state != CIRCUIT_SLOT_FREE) { + count++; + } + } + + return count; +} + +static int circuit_find_free_slot(hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return -1; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_FREE && hub->slots[i].fd < 0) { + return (int)i; + } + } + + return -1; +} + +static int circuit_find_active_slot_by_id(const hybbx_circuit_hub_t *hub, + const char *link_id, + const hybbx_circuit_link_slot_t *except) +{ + unsigned i; + + if (hub == NULL || link_id == NULL || link_id[0] == '\0') { + return -1; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + const hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + + if (slot == except) { + continue; + } + if (slot->state == CIRCUIT_SLOT_FREE || slot->link_id[0] == '\0') { + continue; + } + if (strcmp(slot->link_id, link_id) == 0) { + return (int)i; + } + } + + return -1; +} + +typedef struct balance_send_ctx { + hybbx_circuit_link_slot_t *slot; +} balance_send_ctx_t; + +typedef struct flow_ctrl_ctx { + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; +} flow_ctrl_ctx_t; + +static hybbx_result_t circuit_slot_send_raw(hybbx_circuit_link_slot_t *slot, + const uint8_t *frame, size_t len) +{ + hybbx_circuit_hub_t *hub; + ssize_t sent; + size_t off = 0; + int fd; + + if (slot == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hub = slot->hub; + if (hub == NULL) { + return HYBBX_ERR_INVALID; + } + + pthread_mutex_lock(&hub->lock); + fd = slot->fd; + if (fd < 0) { + pthread_mutex_unlock(&hub->lock); + return HYBBX_ERR_BUSY; + } + + while (off < len) { + sent = send(fd, frame + off, len - off, MSG_NOSIGNAL); + if (sent < 0) { + if (errno == EINTR) { + continue; + } + pthread_mutex_unlock(&hub->lock); + return HYBBX_ERR_IO; + } + if (sent == 0) { + pthread_mutex_unlock(&hub->lock); + return HYBBX_ERR_IO; + } + off += (size_t)sent; + } + + pthread_mutex_unlock(&hub->lock); + return HYBBX_OK; +} + +static hybbx_result_t balance_send_raw_cb(void *ctx, const uint8_t *frame, + size_t len) +{ + balance_send_ctx_t *bctx = (balance_send_ctx_t *)ctx; + + if (bctx == NULL || bctx->slot == NULL) { + return HYBBX_ERR_INVALID; + } + + return circuit_slot_send_raw(bctx->slot, frame, len); +} + +static int circuit_bandwidth_spare_link(hybbx_circuit_hub_t *hub, + hybbx_circuit_link_slot_t *slot, + hybbx_circuit_balance_action_t action) +{ + unsigned users_before; + unsigned affected; + + if (hub == NULL || hub->service == NULL || slot == NULL) { + return 0; + } + + users_before = hybbx_bandwidth_policy_user_count(hub->service); + if (users_before == 0) { + return 0; + } + + affected = hybbx_bandwidth_policy_apply(hub->service, action); + if (affected == 0) { + return 0; + } + + if (slot->balance != NULL && + hybbx_circuit_balance_action(slot->balance) == HYBBX_CIRCUIT_BAL_CANCEL) { + hybbx_circuit_balance_spared_cancel(slot->balance); + hybbx_log_stats("[circuit] secondary link %s spared — users sacrificed first", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + } + + return 1; +} + +static void balance_flow_ctrl_cb(void *ctx, + hybbx_circuit_balance_action_t action, + const char *reason) +{ + flow_ctrl_ctx_t *fctx = (flow_ctrl_ctx_t *)ctx; + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; + char payload[128]; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t payload_len; + size_t frame_len; + const char *reason_str = reason != NULL ? reason : "-"; + + if (fctx == NULL || fctx->hub == NULL || fctx->slot == NULL || + action == HYBBX_CIRCUIT_BAL_NONE) { + return; + } + + hub = fctx->hub; + slot = fctx->slot; + + payload_len = hybbx_circuit_flow_ctrl_format(action, reason_str, + payload, sizeof(payload)); + if (payload_len == 0) { + return; + } + + frame_len = hybbx_circuit_encode_link_msg(HYBBX_CIRCUIT_PROTO_FLOW_CTRL, + payload, payload_len, + frame, sizeof(frame)); + if (frame_len > 0) { + (void)circuit_slot_send_raw(slot, frame, frame_len); + } + + if (action == HYBBX_CIRCUIT_BAL_CANCEL) { + if (circuit_bandwidth_spare_link(hub, slot, action)) { + return; + } + hybbx_log_warn("[circuit] load-balance cancelled link %s (%s)", + slot->link_id[0] != '\0' ? slot->link_id : "?", + reason_str); + } else if (action == HYBBX_CIRCUIT_BAL_PAUSE || + action == HYBBX_CIRCUIT_BAL_BREAK || + action == HYBBX_CIRCUIT_BAL_RESUME) { + hybbx_log_stats("[circuit] load-balance %s link=%s (%s)", + hybbx_circuit_balance_action_name(action), + slot->link_id[0] != '\0' ? slot->link_id : "?", + reason_str); + } + + if (hub->service != NULL && + (action == HYBBX_CIRCUIT_BAL_PAUSE || + action == HYBBX_CIRCUIT_BAL_BREAK || + action == HYBBX_CIRCUIT_BAL_RESUME)) { + (void)hybbx_bandwidth_policy_apply(hub->service, action); + } +} + +static void circuit_balance_tick_slot(hybbx_circuit_link_slot_t *slot, + int *cancel_link) +{ + balance_send_ctx_t bctx; + flow_ctrl_ctx_t fctx; + hybbx_circuit_balance_tick_result_t tr; + hybbx_circuit_hub_t *hub; + + if (cancel_link != NULL) { + *cancel_link = 0; + } + + if (slot == NULL || slot->hub == NULL || slot->balance == NULL || + !slot->profile_set) { + return; + } + + hub = slot->hub; + bctx.slot = slot; + fctx.hub = hub; + fctx.slot = slot; + + tr = hybbx_circuit_balance_tick(slot->balance, HYBBX_CIRCUIT_LINK_POLL_MS, + balance_send_raw_cb, &bctx, + balance_flow_ctrl_cb, &fctx); + if (slot->balance != NULL && slot->profile_set && + hub->config.balance.enabled && + slot->profile.bandwidth == HYBBX_CIRCUIT_BW_LOW && + hybbx_circuit_balance_action(slot->balance) == HYBBX_CIRCUIT_BAL_PAUSE && + hybbx_circuit_balance_queued_bytes(slot->balance) >= + hub->config.balance.queue_pause) { + (void)circuit_bandwidth_spare_link(hub, slot, HYBBX_CIRCUIT_BAL_PAUSE); + } + if (tr == HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK && cancel_link != NULL) { + if (circuit_bandwidth_spare_link(hub, slot, HYBBX_CIRCUIT_BAL_CANCEL)) { + tr = HYBBX_CIRCUIT_BAL_TICK_OK; + } else { + *cancel_link = 1; + } + } +} + +static hybbx_result_t circuit_slot_send_hbx(hybbx_circuit_link_slot_t *slot, + const uint8_t *frame, size_t len) +{ + balance_send_ctx_t bctx; + flow_ctrl_ctx_t fctx; + hybbx_circuit_hub_t *hub; + + if (slot == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hub = slot->hub; + if (hub == NULL) { + return HYBBX_ERR_INVALID; + } + + if (slot->balance != NULL && slot->profile_set && + hub->config.balance.enabled) { + bctx.slot = slot; + fctx.hub = hub; + fctx.slot = slot; + return hybbx_circuit_balance_submit(slot->balance, frame, len, + balance_send_raw_cb, &bctx, + balance_flow_ctrl_cb, &fctx); + } + + return circuit_slot_send_raw(slot, frame, len); +} + +hybbx_result_t hybbx_circuit_hub_send_raw(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len) +{ + unsigned i; + + if (hub == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state != CIRCUIT_SLOT_FREE && hub->slots[i].fd >= 0) { + return circuit_slot_send_raw(&hub->slots[i], frame, len); + } + } + + return HYBBX_ERR_BUSY; +} + +static hybbx_result_t circuit_transport_write(hybbx_session_t *session, + const char *data, size_t len) +{ + hybbx_circuit_link_slot_t *slot; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + + if (session == NULL || data == NULL || len == 0) { + return HYBBX_OK; + } + + slot = (hybbx_circuit_link_slot_t *)session->transport_data; + if (slot == NULL) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_circuit_encode_terminal(data, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return circuit_slot_send_hbx(slot, frame, frame_len); +} + +hybbx_result_t hybbx_circuit_hub_send_hbx(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len) +{ + if (hub == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + return hybbx_circuit_hub_multicast_hbx(hub, frame, len, 0.0, 0, NULL); +} + +unsigned hybbx_circuit_hub_active_link_count(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + unsigned count = 0; + + if (hub == NULL) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_ACTIVE) { + count++; + } + } + + return count; +} + +static double circuit_slot_effective_frequency_mhz( + const hybbx_circuit_link_slot_t *slot, + const hybbx_circuit_hub_t *hub) +{ + double mhz; + + if (slot == NULL) { + return 0.0; + } + + mhz = slot->profile.frequency_mhz; + if (mhz <= 0.0 && slot->link_id[0] != '\0' && hub != NULL) { + const hybbx_circuit_bridge_entry_t *be = + hybbx_circuit_bridge_find(&hub->bridge, slot->link_id); + + if (be != NULL && be->frequency_mhz > 0.0) { + mhz = be->frequency_mhz; + } + } + + return mhz; +} + +hybbx_result_t hybbx_circuit_hub_multicast_hbx(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len, + double frequency_mhz, + int require_broadcast_qos, + unsigned *sent_out) +{ + unsigned i; + int sent = 0; + hybbx_result_t last_err = HYBBX_ERR_NOT_FOUND; + + if (sent_out != NULL) { + *sent_out = 0; + } + + if (hub == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + hybbx_result_t rc; + + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + continue; + } + if (require_broadcast_qos && !circuit_slot_broadcast_qos(slot)) { + continue; + } + if (frequency_mhz > 0.0) { + double slot_mhz = circuit_slot_effective_frequency_mhz(slot, hub); + + if (slot_mhz <= 0.0 || + !hybbx_ax25_frequency_match(frequency_mhz, slot_mhz)) { + continue; + } + } + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len) && + !circuit_slot_can_send_low_prio(hub, slot)) { + last_err = HYBBX_ERR_BUSY; + continue; + } + + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len)) { + /* + * Low-priority AX.25 broadcast: admission is decided by balancer + * flow-control state, then transmit immediately (no queue slot). + * This avoids enqueue->break drops that can otherwise log as sent + * without reaching RF. + */ + rc = circuit_slot_send_raw(slot, frame, len); + if (rc == HYBBX_OK) { + hybbx_circuit_hub_note_rf_activity(hub, slot->link_id); + } + } else { + rc = circuit_slot_send_hbx(slot, frame, len); + } + if (rc == HYBBX_OK) { + sent++; + } else { + last_err = rc; + } + } + + if (sent > 0) { + if (sent_out != NULL) { + *sent_out = (unsigned)sent; + } + return HYBBX_OK; + } + + if (require_broadcast_qos) { + if (last_err != HYBBX_ERR_NOT_FOUND) { + return last_err; + } + return HYBBX_ERR_DENIED; + } + + return last_err; +} + +hybbx_result_t hybbx_circuit_hub_send_hbx_slot(hybbx_circuit_hub_t *hub, + unsigned slot_index, + const uint8_t *frame, size_t len, + int require_broadcast_qos) +{ + hybbx_circuit_link_slot_t *slot; + hybbx_result_t rc; + + if (hub == NULL || frame == NULL || len == 0 || + slot_index >= HYBBX_CIRCUIT_MAX_LINKS) { + return HYBBX_ERR_INVALID; + } + + slot = &hub->slots[slot_index]; + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + return HYBBX_ERR_DENIED; + } + if (require_broadcast_qos && !circuit_slot_broadcast_qos(slot)) { + return HYBBX_ERR_DENIED; + } + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len) && + !circuit_slot_can_send_low_prio(hub, slot)) { + return HYBBX_ERR_BUSY; + } + + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len)) { + rc = circuit_slot_send_raw(slot, frame, len); + if (rc == HYBBX_OK) { + hybbx_circuit_hub_note_rf_activity(hub, slot->link_id); + } + } else { + rc = circuit_slot_send_hbx(slot, frame, len); + } + + return rc; +} + +double hybbx_circuit_hub_link_frequency_mhz(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return 0.0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_ACTIVE && hub->slots[i].profile_set) { + return hub->slots[i].profile.frequency_mhz; + } + } + + return 0.0; +} + +int hybbx_circuit_hub_link_broadcast_qos(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_ACTIVE && + circuit_slot_broadcast_qos(&hub->slots[i])) { + return 1; + } + } + + return 0; +} + +static void circuit_broadcast_links_sort(hybbx_circuit_broadcast_link_t *links, + unsigned count) +{ + unsigned i; + unsigned j; + + for (i = 1; i < count; i++) { + hybbx_circuit_broadcast_link_t key = links[i]; + + j = i; + while (j > 0) { + double prev_mhz = links[j - 1].frequency_mhz; + double key_mhz = key.frequency_mhz; + int move = 0; + + if (key_mhz > 0.0 && prev_mhz <= 0.0) { + move = 0; + } else if (key_mhz <= 0.0 && prev_mhz > 0.0) { + move = 1; + } else if (key_mhz > 0.0 && prev_mhz > 0.0 && + key_mhz < prev_mhz) { + move = 1; + } else if (key_mhz <= 0.0 && prev_mhz <= 0.0 && + strcmp(key.link_id, links[j - 1].link_id) < 0) { + move = 1; + } + + if (!move) { + break; + } + + links[j] = links[j - 1]; + j--; + } + links[j] = key; + } +} + +unsigned hybbx_circuit_hub_broadcast_links(const hybbx_circuit_hub_t *hub, + hybbx_circuit_broadcast_link_t *out, + unsigned out_max) +{ + unsigned i; + unsigned count = 0; + + if (hub == NULL || out == NULL || out_max == 0) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + const hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + continue; + } + if (!circuit_slot_broadcast_qos(slot)) { + continue; + } + if (count >= out_max) { + break; + } + + out[count].frequency_mhz = + circuit_slot_effective_frequency_mhz(slot, hub); + out[count].slot_index = i; + hybbx_strlcpy(out[count].link_id, slot->link_id, + sizeof(out[count].link_id)); + count++; + } + + if (count > 1) { + circuit_broadcast_links_sort(out, count); + } + + return count; +} + +static void on_circuit_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + hybbx_circuit_link_slot_t *slot = (hybbx_circuit_link_slot_t *)userdata; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + + (void)flags; + + if (slot == NULL) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_PROXY_MAIL || + proto == HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + if (slot->hub != NULL && slot->hub->service != NULL) { + hybbx_mains_proxy_inbound_frame(slot->hub->service, proto, + payload, len); + } + return; + } + + if (slot->session == NULL || len == 0) { + return; + } + + switch (proto) { + case HYBBX_CIRCUIT_PROTO_AX25: { + size_t ui_len = hybbx_ax25_parse_ui(payload, len, &path, ui, + sizeof(ui)); + if (ui_len > 0) { + (void)hybbx_session_handle_input(slot->session, ui, ui_len); + } + break; + } + case HYBBX_CIRCUIT_PROTO_AX25_UI: { + size_t ui_len = hybbx_circuit_unpack_ax25_ui(payload, len, &path, + ui, sizeof(ui)); + if (ui_len > 0) { + (void)hybbx_session_handle_input(slot->session, ui, ui_len); + } + break; + } + case HYBBX_CIRCUIT_PROTO_TERMINAL: + (void)hybbx_session_handle_input(slot->session, payload, len); + break; + default: + hybbx_log_debug("[circuit] link=%s ignored proto=%s (%u bytes)", + slot->link_id[0] != '\0' ? slot->link_id : "?", + hybbx_circuit_proto_name(proto), (unsigned)len); + break; + } +} + +static void circuit_slot_reset(hybbx_circuit_link_slot_t *slot) +{ + if (slot == NULL) { + return; + } + + if (slot->balance != NULL) { + hybbx_circuit_balance_destroy(slot->balance); + slot->balance = NULL; + } + + if (slot->session != NULL) { + hybbx_session_close(slot->session); + slot->session = NULL; + } + + slot->profile_set = 0; + memset(&slot->profile, 0, sizeof(slot->profile)); + slot->link_id[0] = '\0'; + slot->last_rf_activity = 0; + slot->state = CIRCUIT_SLOT_FREE; + slot->thread = (pthread_t)0; +} + +static void circuit_close_slot(hybbx_circuit_link_slot_t *slot) +{ + hybbx_circuit_hub_t *hub; + int fd; + + if (slot == NULL || slot->hub == NULL) { + return; + } + + hub = slot->hub; + + pthread_mutex_lock(&hub->lock); + fd = slot->fd; + if (fd >= 0) { + close(fd); + slot->fd = -1; + } + pthread_mutex_unlock(&hub->lock); + + circuit_slot_reset(slot); +} + +static void circuit_close_all_slots(hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state != CIRCUIT_SLOT_FREE || hub->slots[i].fd >= 0) { + circuit_close_slot(&hub->slots[i]); + } + } +} + +typedef struct circuit_auth_ctx { + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; + int done; + int ok; + hybbx_link_auth_t auth; +} circuit_auth_ctx_t; + +static void circuit_evict_stale_link(hybbx_circuit_hub_t *hub, + hybbx_circuit_link_slot_t *stale, + const char *link_id) +{ + int fd = -1; + + if (hub == NULL || stale == NULL) { + return; + } + + /* + * Reconnect race: close the stale TCP fd and drop link_id so the new + * auth can proceed. Do not reset session/balance here — the stale link + * thread still owns those until it exits and calls circuit_close_slot. + */ + pthread_mutex_lock(&hub->lock); + fd = stale->fd; + if (fd >= 0) { + (void)shutdown(fd, SHUT_RDWR); + close(fd); + stale->fd = -1; + } + stale->link_id[0] = '\0'; + pthread_mutex_unlock(&hub->lock); + + if (link_id != NULL && link_id[0] != '\0') { + hybbx_log_info("[circuit] replacing stale link id=%s (reconnect)", + link_id); + } +} + +static int circuit_auth_fail_counts_as_abuse(const char *reason) +{ + if (reason == NULL) { + return 0; + } + + /* + * duplicate_id is a reconnect race (stale hub slot), not brute-force. + * circuit_validate_link_auth evicts the stale slot; do not ban for it. + */ + if (strcmp(reason, "duplicate_id") == 0) { + return 0; + } + + return 1; +} + +static void circuit_log_auth_fail(int fd, const char *reason, const char *id) +{ + char ip[HYBBX_REMOTE_ADDR_MAX]; + int count_abuse = circuit_auth_fail_counts_as_abuse(reason); + + if (reason == NULL) { + return; + } + + if (fd >= 0 && hybbx_socket_peer_name(fd, ip, sizeof(ip)) == HYBBX_OK) { + if (id != NULL && id[0] != '\0') { + hybbx_security_log_write( + "link_auth_fail ip=%s id=%s reason=%s transport=circuit", + ip, id, reason); + } else { + hybbx_security_log_write( + "link_auth_fail ip=%s reason=%s transport=circuit", + ip, reason); + } + if (count_abuse) { + hybbx_security_ban_link_auth_fail(ip); + if (id != NULL && id[0] != '\0' && strcmp(reason, "banned") != 0) { + hybbx_security_ban_link_auth_fail_callid(id); + } + } + } else { + hybbx_security_log_write( + "link_auth_fail ip=? reason=%s transport=circuit", reason); + } +} + +static int circuit_auth_password_ok(const hybbx_circuit_hub_t *hub, + const hybbx_circuit_bridge_entry_t *entry, + const char *password) +{ + if (hub == NULL || password == NULL) { + return 0; + } + + if (entry != NULL && entry->link_password[0] != '\0') { + return hybbx_password_match(entry->link_password, password); + } + + if (hub->link_password[0] != '\0') { + return hybbx_password_match(hub->link_password, password); + } + + return 0; +} + +static int circuit_validate_link_auth(circuit_auth_ctx_t *ctx, const char **reason) +{ + const hybbx_circuit_bridge_entry_t *entry = NULL; + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; + + if (ctx == NULL || ctx->hub == NULL || ctx->slot == NULL) { + if (reason != NULL) { + *reason = "invalid"; + } + return 0; + } + + hub = ctx->hub; + slot = ctx->slot; + + if (!hub->link_auth) { + return 1; + } + + if (hub->bridge.count > 0) { + entry = hybbx_circuit_bridge_find(&hub->bridge, ctx->auth.id); + if (entry == NULL) { + if (reason != NULL) { + *reason = "unknown_id"; + } + return 0; + } + } else if (hub->link_password[0] == '\0') { + if (reason != NULL) { + *reason = "no_password"; + } + return 0; + } + + if (!circuit_auth_password_ok(hub, entry, ctx->auth.password)) { + if (reason != NULL) { + *reason = "password"; + } + return 0; + } + + { + int stale_idx = circuit_find_active_slot_by_id(hub, ctx->auth.id, slot); + + if (stale_idx >= 0) { + circuit_evict_stale_link(hub, &hub->slots[(unsigned)stale_idx], + ctx->auth.id); + if (circuit_find_active_slot_by_id(hub, ctx->auth.id, slot) >= 0) { + if (reason != NULL) { + *reason = "duplicate_id"; + } + return 0; + } + } + } + + (void)entry; + (void)slot; + return 1; +} + +static void circuit_on_auth_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + circuit_auth_ctx_t *ctx = (circuit_auth_ctx_t *)userdata; + const hybbx_circuit_bridge_entry_t *entry; + char code[HYBBX_LINK_CODE_MAX]; + char ack[HYBBX_LINK_AUTH_PAYLOAD_MAX]; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + const char *fail_reason = "invalid"; + int fd; + + (void)flags; + + if (ctx == NULL || ctx->done || ctx->slot == NULL) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_LINK_AUTH) { + return; + } + + fd = ctx->slot->fd; + + if (hybbx_link_auth_parse((const char *)payload, len, &ctx->auth) != HYBBX_OK) { + circuit_log_auth_fail(fd, "invalid", NULL); + ctx->done = 1; + ctx->ok = 0; + return; + } + + if (!hybbx_security_ban_callid_accept(ctx->auth.id)) { + circuit_log_auth_fail(fd, "banned", ctx->auth.id); + ctx->done = 1; + ctx->ok = 0; + return; + } + + if (!circuit_validate_link_auth(ctx, &fail_reason)) { + hybbx_log_warn("[circuit] link auth failed for id=%s (%s)", + ctx->auth.id, fail_reason); + circuit_log_auth_fail(fd, fail_reason, ctx->auth.id); + ctx->done = 1; + ctx->ok = 0; + return; + } + + code[0] = '\0'; + (void)hybbx_link_registry_touch(&ctx->hub->links, ctx->auth.id, + ctx->auth.role, code, sizeof(code)); + + snprintf(ack, sizeof(ack), "ok=yes\nid=%s\ncode=%s\n", + ctx->auth.id, code[0] != '\0' ? code : "-"); + frame_len = hybbx_circuit_encode_link_msg(HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK, + ack, strlen(ack), + frame, sizeof(frame)); + if (frame_len > 0) { + (void)circuit_slot_send_raw(ctx->slot, frame, frame_len); + } + + hybbx_strlcpy(ctx->slot->link_id, ctx->auth.id, sizeof(ctx->slot->link_id)); + + hybbx_circuit_link_profile_from_auth(&ctx->auth, &ctx->slot->profile); + entry = hybbx_circuit_bridge_find(&ctx->hub->bridge, ctx->auth.id); + if (entry != NULL && entry->frequency_mhz > 0.0 && + ctx->slot->profile.frequency_mhz <= 0.0) { + ctx->slot->profile.frequency_mhz = entry->frequency_mhz; + } + + if (ctx->slot->balance != NULL) { + hybbx_circuit_balance_set_profile(ctx->slot->balance, &ctx->slot->profile); + } + ctx->slot->profile_set = 1; + + hybbx_log_info("[circuit] link authenticated id=%s role=%s code=%s", + ctx->auth.id, ctx->auth.role, code[0] != '\0' ? code : "-"); + { + char qos_msg[128]; + int qos_len; + + qos_len = snprintf(qos_msg, sizeof(qos_msg), + "[circuit] link QoS bandwidth=%s baud=%u duplex=%s", + ctx->slot->profile.bandwidth == HYBBX_CIRCUIT_BW_LOW ? + "low" : "high", + ctx->slot->profile.baud, + ctx->slot->profile.duplex == HYBBX_CIRCUIT_DUPLEX_HALF ? + "half" : "full"); + if (qos_len > 0 && ctx->slot->profile.frequency_mhz > 0.0) { + snprintf(qos_msg + (size_t)qos_len, + sizeof(qos_msg) - (size_t)qos_len, + " %.3fMHz", ctx->slot->profile.frequency_mhz); + } + hybbx_log_info("%s", qos_msg); + } + + ctx->done = 1; + ctx->ok = 1; +} + +static int circuit_wait_link_auth(hybbx_circuit_link_slot_t *slot) +{ + circuit_auth_ctx_t ctx; + hybbx_circuit_decoder_t dec; + uint8_t buf[256]; + unsigned elapsed = 0; + hybbx_circuit_hub_t *hub; + + if (slot == NULL || slot->hub == NULL) { + return 0; + } + + hub = slot->hub; + + if (!hub->link_auth) { + return 1; + } + + memset(&ctx, 0, sizeof(ctx)); + ctx.hub = hub; + ctx.slot = slot; + hybbx_circuit_decoder_init(&dec); + + while (!ctx.done && elapsed < HYBBX_CIRCUIT_AUTH_TIMEOUT_MS) { + struct pollfd pfd; + ssize_t n; + int pr; + + pthread_mutex_lock(&hub->lock); + pfd.fd = slot->fd; + pthread_mutex_unlock(&hub->lock); + + if (pfd.fd < 0) { + return 0; + } + + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + return 0; + } + if (pr == 0) { + elapsed += HYBBX_CIRCUIT_LINK_POLL_MS; + continue; + } + if ((pfd.revents & POLLIN) == 0) { + return 0; + } + + n = recv(pfd.fd, buf, sizeof(buf), 0); + if (n <= 0) { + return 0; + } + + hybbx_circuit_decoder_feed(&dec, buf, (size_t)n, + circuit_on_auth_frame, &ctx); + } + + if (!ctx.done) { + circuit_log_auth_fail(slot->fd, "timeout_no_link_auth", NULL); + } + + return ctx.ok; +} + +static void *circuit_link_thread(void *arg) +{ + hybbx_circuit_link_slot_t *slot = (hybbx_circuit_link_slot_t *)arg; + hybbx_circuit_hub_t *hub; + uint8_t buf[512]; + hybbx_result_t rc; + + if (slot == NULL || slot->hub == NULL) { + return NULL; + } + + hub = slot->hub; + + if (!circuit_wait_link_auth(slot)) { + hybbx_log_warn("[circuit] link authentication failed or timed out"); + circuit_close_slot(slot); + return NULL; + } + + if (!slot->profile_set) { + hybbx_circuit_link_profile_from_auth(NULL, &slot->profile); + if (slot->balance != NULL) { + hybbx_circuit_balance_set_profile(slot->balance, &slot->profile); + } + slot->profile_set = 1; + } + + hybbx_circuit_decoder_init(&slot->decoder); + + rc = hybbx_session_open(hub->service, &hybbx_plugin_circuit, slot, + &slot->session); + if (rc != HYBBX_OK) { + hybbx_log_warn("[circuit] session open failed for link %s", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + circuit_close_slot(slot); + return NULL; + } + + { + char remote[HYBBX_REMOTE_ADDR_MAX]; + int fd = slot->fd; + + if (fd >= 0 && + hybbx_socket_peer_name(fd, remote, sizeof(remote)) == HYBBX_OK) { + (void)hybbx_session_set_remote(slot->session, remote); + } + } + + slot->state = CIRCUIT_SLOT_ACTIVE; + slot->last_rf_activity = time(NULL); + hybbx_log_info("[circuit] link adapter attached id=%s (HBX bridge active)", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + + while (hub->running) { + struct pollfd pfd; + ssize_t n; + int pr; + + pthread_mutex_lock(&hub->lock); + pfd.fd = slot->fd; + pthread_mutex_unlock(&hub->lock); + + if (pfd.fd < 0) { + break; + } + + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + int cancel_link = 0; + + circuit_balance_tick_slot(slot, &cancel_link); + if (cancel_link) { + break; + } + if (slot->session != NULL) { + rc = hybbx_session_tick(slot->session); + if (rc == HYBBX_SESSION_END) { + break; + } + } + continue; + } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + n = recv(pfd.fd, buf, sizeof(buf), 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + + hybbx_circuit_decoder_feed(&slot->decoder, buf, (size_t)n, + on_circuit_frame, slot); + + { + int cancel_link = 0; + circuit_balance_tick_slot(slot, &cancel_link); + if (cancel_link) { + break; + } + } + } + + hybbx_log_info("[circuit] link detached id=%s", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + circuit_close_slot(slot); + return NULL; +} + +static void *circuit_accept_thread(void *arg) +{ + hybbx_circuit_hub_t *hub = (hybbx_circuit_hub_t *)arg; + + while (hub->running) { + struct pollfd pfds[2]; + int count = 0; + int pr; + int i; + + pfds[0].fd = hub->listen_v4; + pfds[0].events = POLLIN; + pfds[0].revents = 0; + if (hub->listen_v4 >= 0) { + count = 1; + } + + pfds[1].fd = hub->listen_v6; + pfds[1].events = POLLIN; + pfds[1].revents = 0; + if (hub->listen_v6 >= 0) { + count = 2; + } + + if (count == 0) { + break; + } + + pr = poll(pfds, (nfds_t)count, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + continue; + } + + for (i = 0; i < count; i++) { + if ((pfds[i].revents & POLLIN) == 0) { + continue; + } + + { + int client = accept(pfds[i].fd, NULL, NULL); + int slot_idx; + hybbx_circuit_link_slot_t *slot; + + if (client < 0) { + continue; + } + + (void)set_socket_options(client, 0); + + if (!hybbx_security_ban_accept_fd(client)) { + close(client); + continue; + } + + pthread_mutex_lock(&hub->lock); + if (circuit_count_used_slots(hub) >= hub->max_links) { + pthread_mutex_unlock(&hub->lock); + hybbx_log_warn("[circuit] max_links=%u reached — rejecting connection", + hub->max_links); + close(client); + continue; + } + + slot_idx = circuit_find_free_slot(hub); + if (slot_idx < 0) { + pthread_mutex_unlock(&hub->lock); + close(client); + continue; + } + + slot = &hub->slots[slot_idx]; + slot->fd = client; + slot->state = CIRCUIT_SLOT_CONNECTING; + slot->balance = hybbx_circuit_balance_create(&hub->config.balance); + if (slot->balance == NULL) { + hybbx_log_warn("[circuit] balance alloc failed — rejecting link"); + close(client); + slot->fd = -1; + slot->state = CIRCUIT_SLOT_FREE; + pthread_mutex_unlock(&hub->lock); + continue; + } + pthread_mutex_unlock(&hub->lock); + + if (pthread_create(&slot->thread, NULL, circuit_link_thread, + slot) != 0) { + hybbx_log_warn("[circuit] link thread failed"); + circuit_close_slot(slot); + } else { + pthread_detach(slot->thread); + } + } + } + } + + return NULL; +} + +hybbx_circuit_hub_t *hybbx_circuit_hub_create(hybbx_service_t *service) +{ + hybbx_circuit_hub_t *hub; + unsigned i; + + hub = calloc(1, sizeof(*hub)); + if (hub == NULL) { + return NULL; + } + + hub->service = service; + hub->listen_v4 = -1; + hub->listen_v6 = -1; + hub->max_links = HYBBX_CIRCUIT_DEFAULT_MAX_LINKS; + pthread_mutex_init(&hub->lock, NULL); + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + hub->slots[i].hub = hub; + hub->slots[i].fd = -1; + hub->slots[i].state = CIRCUIT_SLOT_FREE; + } + + return hub; +} + +void hybbx_circuit_hub_destroy(hybbx_circuit_hub_t *hub) +{ + if (hub == NULL) { + return; + } + + hybbx_circuit_hub_stop(hub); + pthread_mutex_destroy(&hub->lock); + free(hub); +} + +hybbx_result_t hybbx_circuit_hub_start(hybbx_circuit_hub_t *hub, + const hybbx_circuit_config_t *cfg) +{ + int backlog; + + if (hub == NULL || cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_circuit_hub_stop(hub); + hub->config = *cfg; + hub->bridge = cfg->bridge; + hub->max_links = cfg->max_links; + if (hub->max_links == 0 || hub->max_links > HYBBX_CIRCUIT_MAX_LINKS) { + hub->max_links = HYBBX_CIRCUIT_DEFAULT_MAX_LINKS; + } + hybbx_strlcpy(hub->link_password, cfg->link_password, sizeof(hub->link_password)); + hub->link_auth = cfg->link_auth; + hybbx_link_registry_init(&hub->links, cfg->data_path, cfg->config_path, + cfg->link_stale_days); + (void)hybbx_link_registry_prune(&hub->links); + hub->running = 1; + + backlog = (int)hub->max_links; + if (backlog < 4) { + backlog = 4; + } + + if (cfg->ipv4) { + hub->listen_v4 = create_listen_socket(AF_INET, cfg->bind4, cfg->port, + backlog); + if (hub->listen_v4 < 0) { + hybbx_socket_log_bind_failure("circuit", cfg->bind4, cfg->port); + hybbx_circuit_hub_stop(hub); + return HYBBX_ERR_IO; + } + } + + if (cfg->ipv6) { + hub->listen_v6 = create_listen_socket(AF_INET6, cfg->bind6, cfg->port, + backlog); + if (hub->listen_v6 < 0) { + hybbx_log_warn("[circuit] IPv6 bind [%s]:%u skipped (%s)", + cfg->bind6, cfg->port, strerror(errno)); + } + } + + if (hub->listen_v4 < 0 && hub->listen_v6 < 0) { + hybbx_circuit_hub_stop(hub); + return HYBBX_ERR_IO; + } + + { + char hub_msg[256]; + size_t off = 0; + + off = (size_t)snprintf(hub_msg, sizeof(hub_msg), + "[circuit] internal TCP hub"); + if (hub->listen_v4 >= 0) { + off += (size_t)snprintf(hub_msg + off, sizeof(hub_msg) - off, + " %s:%u", cfg->bind4, cfg->port); + } + if (hub->listen_v6 >= 0) { + off += (size_t)snprintf(hub_msg + off, sizeof(hub_msg) - off, + " [%s]:%u", cfg->bind6, cfg->port); + } + snprintf(hub_msg + off, sizeof(hub_msg) - off, + " (HBX v%u, max_links=%u, bridge=%u)", + (unsigned)HYBBX_CIRCUIT_VERSION, hub->max_links, + hub->bridge.count); + hybbx_log_info("%s", hub_msg); + } + + if (pthread_create(&hub->accept_thread, NULL, circuit_accept_thread, + hub) != 0) { + hybbx_circuit_hub_stop(hub); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +void hybbx_circuit_hub_stop(hybbx_circuit_hub_t *hub) +{ + if (hub == NULL) { + return; + } + + hybbx_broadcast_ax25_seq_cancel(); + hub->running = 0; + + if (hub->listen_v4 >= 0) { + close(hub->listen_v4); + hub->listen_v4 = -1; + } + if (hub->listen_v6 >= 0) { + close(hub->listen_v6); + hub->listen_v6 = -1; + } + + circuit_close_all_slots(hub); + + if (hub->accept_thread) { + pthread_join(hub->accept_thread, NULL); + hub->accept_thread = (pthread_t)0; + } +} + +int hybbx_circuit_hub_running(const hybbx_circuit_hub_t *hub) +{ + return hub != NULL && hub->running; +} + +unsigned hybbx_circuit_hub_port(const hybbx_circuit_hub_t *hub) +{ + if (hub == NULL) { + return HYBBX_CIRCUIT_DEFAULT_PORT; + } + + return hub->config.port; +} + +void hybbx_circuit_hub_prune_links(hybbx_circuit_hub_t *hub) +{ + if (hub != NULL) { + (void)hybbx_link_registry_prune(&hub->links); + } +} + +void hybbx_circuit_hub_note_rf_activity(hybbx_circuit_hub_t *hub, + const char *link_id) +{ + time_t now; + unsigned i; + + if (hub == NULL || link_id == NULL || link_id[0] == '\0') { + return; + } + + now = time(NULL); + + pthread_mutex_lock(&hub->lock); + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + continue; + } + if (strcmp(slot->link_id, link_id) != 0) { + continue; + } + + slot->last_rf_activity = now; + break; + } + pthread_mutex_unlock(&hub->lock); +} + +int hybbx_circuit_hub_link_band_idle(const hybbx_circuit_hub_t *hub, + unsigned slot_index, + unsigned min_idle_sec) +{ + const hybbx_circuit_link_slot_t *slot; + hybbx_circuit_hub_t *mutable_hub; + time_t now; + time_t last_rf; + time_t idle_since; + circuit_slot_state_t state; + int fd; + + if (hub == NULL || slot_index >= HYBBX_CIRCUIT_MAX_LINKS || + min_idle_sec == 0) { + return 0; + } + + mutable_hub = (hybbx_circuit_hub_t *)hub; + pthread_mutex_lock(&mutable_hub->lock); + slot = &hub->slots[slot_index]; + state = slot->state; + fd = slot->fd; + last_rf = slot->last_rf_activity; + pthread_mutex_unlock(&mutable_hub->lock); + + if (state != CIRCUIT_SLOT_ACTIVE || fd < 0) { + return 0; + } + if (last_rf == 0) { + return 1; + } + + now = time(NULL); + idle_since = now - last_rf; + if (idle_since < 0) { + return 0; + } + + return (unsigned)idle_since >= min_idle_sec; +} + +time_t hybbx_circuit_hub_link_band_ready_at(const hybbx_circuit_hub_t *hub, + unsigned slot_index, + unsigned min_idle_sec) +{ + hybbx_circuit_hub_t *mutable_hub; + const hybbx_circuit_link_slot_t *slot; + circuit_slot_state_t state; + int fd; + time_t last_rf; + time_t now; + + if (hub == NULL || slot_index >= HYBBX_CIRCUIT_MAX_LINKS || + min_idle_sec == 0) { + return (time_t)-1; + } + + mutable_hub = (hybbx_circuit_hub_t *)hub; + pthread_mutex_lock(&mutable_hub->lock); + slot = &hub->slots[slot_index]; + state = slot->state; + fd = slot->fd; + last_rf = slot->last_rf_activity; + pthread_mutex_unlock(&mutable_hub->lock); + + if (state != CIRCUIT_SLOT_ACTIVE || fd < 0) { + return (time_t)-1; + } + + now = time(NULL); + if (now == (time_t)-1) { + return (time_t)-1; + } + if (last_rf == 0) { + return now; + } + + if ((unsigned)(now - last_rf) >= min_idle_sec) { + return now; + } + + return last_rf + (time_t)min_idle_sec; +} + +static hybbx_result_t circuit_plugin_init(hybbx_service_t *service) +{ + (void)service; + return HYBBX_OK; +} + +static void circuit_plugin_shutdown(void) +{ +} + +static hybbx_result_t circuit_plugin_start(const char *config) +{ + (void)config; + return HYBBX_OK; +} + +static hybbx_result_t circuit_plugin_stop(void) +{ + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_circuit = { + .name = "circuit", + .kind = HYBBX_TRANSPORT_CIRCUIT, + .version = 1, + .init = circuit_plugin_init, + .shutdown = circuit_plugin_shutdown, + .start = circuit_plugin_start, + .stop = circuit_plugin_stop, + .write = circuit_transport_write, +}; diff --git a/src/core/command.c b/src/core/command.c new file mode 100644 index 0000000..b60ee35 --- /dev/null +++ b/src/core/command.c @@ -0,0 +1,2450 @@ +#include "hybbx/hybbx.h" +#include "hybbx/command.h" +#include "hybbx/commands_registry.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/texts.h" +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/chat.h" +#include "hybbx/conference.h" +#include "hybbx/mail.h" +#include "hybbx/proxymail.h" +#include "hybbx/proxychat.h" +#include "hybbx/broadcast.h" +#include "hybbx/security.h" +#include "hybbx/security_ban.h" +#include "hybbx/service.h" +#include "hybbx/password.h" +#include "hybbx/traffic.h" +#include "hybbx/monitor.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +static hybbx_result_t cmd_proxymail(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd); +static hybbx_result_t cmd_proxychat(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd); + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static void cmd_deny_privilege(hybbx_session_t *session); + +static const char *cmd_help_unknown(hybbx_session_t *session) +{ + if (hybbx_session_is_guest(session) || + (hybbx_session_login_prompt(session) && + !hybbx_session_logged_in(session))) { + return "Unknown command. Try /help."; + } + + return "Unknown command. /help for list."; +} + +static int cmd_verb_allowed_login_prompt(const char *verb) +{ + if (verb == NULL || verb[0] == '\0') { + return 1; + } + + if (hybbx_commands_registry_verb_allowed(HYBBX_LEVEL_GUEST, verb)) { + return 1; + } + + return str_ieq(verb, "exit") || str_ieq(verb, "logout") || + str_ieq(verb, "bye") || str_ieq(verb, "quit"); +} + +static hybbx_result_t cmd_check_access(hybbx_session_t *session, + const char *verb) +{ + hybbx_user_level_t level = hybbx_session_user_level(session); + + if (!hybbx_session_logged_in(session) && + hybbx_session_login_prompt(session)) { + if (cmd_verb_allowed_login_prompt(verb)) { + return HYBBX_OK; + } + hybbx_session_write_line(session, + "Log in with /login or use /register."); + return HYBBX_ERR_DENIED; + } + + if (hybbx_commands_registry_verb_allowed(level, verb)) { + return HYBBX_OK; + } + + /* Optional [monitor] allow= grants beyond commands.yaml min level. */ + if ((str_ieq(verb, "monitor") || str_ieq(verb, "mon")) && + hybbx_monitor_enabled() && + hybbx_monitor_user_allowed(hybbx_session_username(session))) { + return HYBBX_OK; + } + + if (level == HYBBX_LEVEL_GUEST) { + hybbx_session_write_line(session, + "Not available to guests. Try /help."); + } else if (str_ieq(verb, "register")) { + hybbx_session_write_line(session, + "Only guests may self-register with /register."); + } else if (str_ieq(verb, "changeme")) { + hybbx_session_write_line(session, + "Only registered users may use /changeme."); + } else { + hybbx_session_write_line(session, "Insufficient privileges."); + } + + return HYBBX_ERR_DENIED; +} + +static int cmd_deleteme_confirmed(const char *arg) +{ + return arg != NULL && hybbx_bool_is_true(arg); +} + +static void cmd_registry_usage(hybbx_session_t *session, const char *verb) +{ + const char *canonical = hybbx_commands_registry_canonical(verb); + + hybbx_commands_registry_show_help(session, canonical); +} + +static hybbx_result_t cmd_help_topic(hybbx_session_t *session, const char *topic) +{ + hybbx_user_level_t level = hybbx_session_user_level(session); + const char *canonical; + const hybbx_command_def_t *def; + + if (topic == NULL || topic[0] == '\0') { + hybbx_commands_registry_show_menu(session); + return HYBBX_OK; + } + + canonical = hybbx_commands_registry_canonical(topic); + + if (!hybbx_commands_registry_help_allowed(level, canonical)) { + /* Allow-list grants may read /help monitor without Sysop min. */ + if (!((str_ieq(canonical, "monitor") || str_ieq(canonical, "mon")) && + hybbx_monitor_session_may_use(session))) { + hybbx_session_write_line(session, cmd_help_unknown(session)); + return HYBBX_ERR_NOT_FOUND; + } + } + + def = hybbx_commands_registry_find(canonical); + if (def == NULL || def->line1[0] == '\0') { + hybbx_session_write_line(session, cmd_help_unknown(session)); + return HYBBX_ERR_NOT_FOUND; + } + + hybbx_commands_registry_show_help(session, canonical); + return HYBBX_OK; +} + +static hybbx_result_t cmd_help(hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + if (cmd->argc == 0) { + hybbx_commands_registry_show_menu(session); + return HYBBX_OK; + } + + return cmd_help_topic(session, cmd->argv[0]); +} + +static hybbx_result_t cmd_news(hybbx_service_t *service, hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_texts_send_file(texts, session, HYBBX_TEXT_NEWS) != HYBBX_OK) { + hybbx_session_write_line(session, "(no news available)"); + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_banner(hybbx_service_t *service, hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + (void)hybbx_texts_send_banner(texts, session, HYBBX_VERSION_STRING, + hybbx_service_get_name(service)); + return HYBBX_OK; +} + +static hybbx_result_t cmd_motd(hybbx_service_t *service, hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_texts_send_motd(texts, session) != HYBBX_OK) { + hybbx_session_write_line(session, "(no motd available)"); + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_rules(hybbx_service_t *service, hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_texts_send_file(texts, session, HYBBX_TEXT_RULES) != HYBBX_OK) { + hybbx_session_write_line(session, "(no rules available)"); + } + + return HYBBX_OK; +} + +static const char *who_transport_label(const char *transport) +{ + if (transport == NULL || transport[0] == '\0') { + return "unknown"; + } + + if (str_ieq(transport, "packet_radio")) { + return "ax25"; + } + if (str_ieq(transport, "telnet")) { + return "telnet"; + } + if (str_ieq(transport, "circuit")) { + return "circuit"; + } + if (str_ieq(transport, "ssh")) { + return "ssh"; + } + if (str_ieq(transport, "websocket")) { + return "websocket"; + } + + return transport; +} + +typedef struct who_ctx { + hybbx_session_t *requester; + unsigned count; + int at_plugin; +} who_ctx_t; + +static void who_list_visitor(hybbx_session_t *session, void *userdata) +{ + who_ctx_t *ctx = (who_ctx_t *)userdata; + const hybbx_session_record_t *rec; + const char *name; + const char *transport; + char line[96]; + char user_at[80]; + + if (ctx == NULL || session == NULL || !hybbx_session_logged_in(session)) { + return; + } + + if (!hybbx_session_is_interactive_user(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + if (ctx->at_plugin) { + if (hybbx_session_format_user_at_plugin(session, user_at, + sizeof(user_at)) != HYBBX_OK) { + return; + } + snprintf(line, sizeof(line), " %s", user_at); + } else { + name = hybbx_session_display_name(session); + rec = hybbx_session_record(session); + transport = who_transport_label(rec != NULL ? rec->transport : NULL); + if (rec == NULL || rec->transport[0] == '\0') { + transport = who_transport_label(session->transport != NULL ? + session->transport->name : NULL); + } + snprintf(line, sizeof(line), " %-16s %s", name, transport); + } + + hybbx_session_write_line(ctx->requester, line); + ctx->count++; +} + +static hybbx_result_t cmd_who(hybbx_service_t *service, hybbx_session_t *session) +{ + who_ctx_t ctx; + char total[32]; + + ctx.requester = session; + ctx.count = 0; + ctx.at_plugin = hybbx_service_login_announce(service); + + hybbx_session_write_line(session, "Online users:"); + hybbx_service_visit_sessions(service, who_list_visitor, &ctx); + + if (ctx.count == 0) { + hybbx_session_write_line(session, " (none)"); + } + + snprintf(total, sizeof(total), "Total: %u", ctx.count); + hybbx_session_write_line(session, total); + return HYBBX_OK; +} + +typedef struct users_stats_ctx { + size_t by_level[6]; + size_t total; +} users_stats_ctx_t; + +static hybbx_result_t users_stats_cb(const hybbx_user_record_t *user, void *ctx) +{ + users_stats_ctx_t *stats = (users_stats_ctx_t *)ctx; + + if (user == NULL || stats == NULL) { + return HYBBX_OK; + } + + if (user->level < HYBBX_LEVEL_SYSOP || user->level > HYBBX_LEVEL_USER) { + return HYBBX_OK; + } + + stats->by_level[user->level]++; + stats->total++; + return HYBBX_OK; +} + +static void users_compute_percents(const size_t *counts, size_t total, + unsigned *percents) +{ + hybbx_user_level_t level; + hybbx_user_level_t adjust_level = HYBBX_LEVEL_SYSOP; + unsigned sum = 0; + unsigned max_remainder = 0; + + if (total == 0) { + return; + } + + for (level = HYBBX_LEVEL_SYSOP; level <= HYBBX_LEVEL_USER; level++) { + unsigned scaled = (unsigned)((counts[level] * 1000u) / total); + unsigned rem = scaled % 10u; + + percents[level] = scaled / 10u; + sum += percents[level]; + if (rem > max_remainder) { + max_remainder = rem; + adjust_level = level; + } + } + + if (sum < 100u) { + percents[adjust_level] += 100u - sum; + } +} + +static hybbx_result_t cmd_users(hybbx_service_t *service, hybbx_session_t *session) +{ + hybbx_storage_t *storage; + users_stats_ctx_t stats; + unsigned percents[6]; + hybbx_user_level_t level; + char line[48]; + char total[40]; + hybbx_result_t rc; + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + hybbx_session_write_line(session, "User storage unavailable."); + return HYBBX_ERR_INVALID; + } + + memset(&stats, 0, sizeof(stats)); + memset(percents, 0, sizeof(percents)); + + rc = hybbx_storage_foreach_user(storage, users_stats_cb, &stats); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Could not read user database."); + return rc; + } + + hybbx_session_write_line(session, "Registered users:"); + + if (stats.total == 0) { + hybbx_session_write_line(session, " (none)"); + } else { + users_compute_percents(stats.by_level, stats.total, percents); + for (level = HYBBX_LEVEL_SYSOP; level <= HYBBX_LEVEL_USER; level++) { + if (stats.by_level[level] == 0) { + continue; + } + + snprintf(line, sizeof(line), " %-8s %3u%%", + hybbx_user_level_name(level), percents[level]); + hybbx_session_write_line(session, line); + } + } + + snprintf(total, sizeof(total), "Total users: %zu", stats.total); + hybbx_session_write_line(session, total); + return HYBBX_OK; +} + +static hybbx_result_t cmd_session(hybbx_session_t *session) +{ + char buf[128]; + const hybbx_session_record_t *rec = hybbx_session_record(session); + + if (rec == NULL) { + return HYBBX_ERR_INVALID; + } + + snprintf(buf, sizeof(buf), "User: %s", hybbx_session_display_name(session)); + hybbx_session_write_line(session, buf); + snprintf(buf, sizeof(buf), "Level: %s", + hybbx_user_level_name(hybbx_session_user_level(session))); + hybbx_session_write_line(session, buf); + snprintf(buf, sizeof(buf), "Session: %llu", + (unsigned long long)rec->session_id); + hybbx_session_write_line(session, buf); + snprintf(buf, sizeof(buf), "Transport: %s", rec->transport); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t parse_profile_fields(const hybbx_parsed_command_t *cmd, + unsigned name_start, + unsigned tail_extra, + hybbx_user_registration_t *reg) +{ + size_t i; + size_t pos = 0; + unsigned suffix; + + if (cmd == NULL || reg == NULL) { + return HYBBX_ERR_INVALID; + } + + suffix = 3u + tail_extra; + if (cmd->argc < name_start + suffix) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(reg->email, cmd->argv[cmd->argc - 1u - tail_extra], + sizeof(reg->email)); + hybbx_strlcpy(reg->location, cmd->argv[cmd->argc - 2u - tail_extra], + sizeof(reg->location)); + hybbx_strlcpy(reg->country, cmd->argv[cmd->argc - 3u - tail_extra], + sizeof(reg->country)); + + reg->full_name[0] = '\0'; + for (i = name_start; i + suffix < cmd->argc; i++) { + size_t part_len; + const char *part = cmd->argv[i]; + + if (part == NULL) { + continue; + } + + part_len = strlen(part); + if (pos > 0) { + if (pos + 1 >= sizeof(reg->full_name)) { + return HYBBX_ERR_INVALID; + } + reg->full_name[pos++] = ' '; + } + + if (pos + part_len >= sizeof(reg->full_name)) { + return HYBBX_ERR_INVALID; + } + + memcpy(reg->full_name + pos, part, part_len); + pos += part_len; + } + + reg->full_name[pos] = '\0'; + return HYBBX_OK; +} + +static hybbx_result_t parse_registration(const hybbx_parsed_command_t *cmd, + hybbx_user_registration_t *reg, + int with_password) +{ + hybbx_result_t rc; + unsigned min_args = with_password ? 6u : 5u; + unsigned tail_extra = with_password ? 1u : 0u; + + if (cmd == NULL || reg == NULL || cmd->argc < min_args) { + return HYBBX_ERR_INVALID; + } + + memset(reg, 0, sizeof(*reg)); + hybbx_strlcpy(reg->nickname, cmd->argv[0], sizeof(reg->nickname)); + hybbx_strlcpy(reg->username, cmd->argv[0], sizeof(reg->username)); + hybbx_username_normalize(reg->username); + + if (with_password) { + hybbx_strlcpy(reg->password, cmd->argv[cmd->argc - 1], + sizeof(reg->password)); + } + + rc = parse_profile_fields(cmd, 1, tail_extra, reg); + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t parse_changeme(const hybbx_parsed_command_t *cmd, + const char *username, + hybbx_user_registration_t *reg, + const char **old_password, + const char **new_password) +{ + hybbx_result_t rc; + + if (cmd == NULL || username == NULL || reg == NULL || + old_password == NULL || new_password == NULL || cmd->argc < 6) { + return HYBBX_ERR_INVALID; + } + + memset(reg, 0, sizeof(*reg)); + hybbx_strlcpy(reg->username, username, sizeof(reg->username)); + hybbx_username_normalize(reg->username); + *old_password = cmd->argv[0]; + *new_password = cmd->argv[1]; + + rc = parse_profile_fields(cmd, 2, 0, reg); + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t parse_userchange(const hybbx_parsed_command_t *cmd, + hybbx_user_registration_t *reg, + const char **new_password) +{ + hybbx_result_t rc; + + if (cmd == NULL || reg == NULL || new_password == NULL || cmd->argc < 7) { + return HYBBX_ERR_INVALID; + } + + memset(reg, 0, sizeof(*reg)); + hybbx_strlcpy(reg->username, cmd->argv[0], sizeof(reg->username)); + hybbx_username_normalize(reg->username); + *new_password = cmd->argv[1]; + + rc = parse_profile_fields(cmd, 2, 0, reg); + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_lookup_user(hybbx_service_t *service, + const char *username, + hybbx_user_record_t *out) +{ + hybbx_storage_t *storage; + hybbx_result_t rc; + + if (service == NULL || username == NULL || out == NULL || + username[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_resolve_user(storage, username, out); + if (rc == HYBBX_ERR_NOT_FOUND) { + return HYBBX_ERR_NOT_FOUND; + } + + return rc; +} + +static void cmd_deny_privilege(hybbx_session_t *session) +{ + hybbx_session_write_line(session, "Insufficient privileges."); +} + +static hybbx_result_t cmd_activate(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + char buf[128]; + hybbx_result_t rc; + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, "Usage: /activate <username>"); + return HYBBX_OK; + } + + actor_level = hybbx_session_user_level(session); + if (!hybbx_commands_registry_verb_allowed(actor_level, "activate")) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (target.level != HYBBX_LEVEL_USER) { + hybbx_session_write_line(session, + "Only pending registered user accounts can be activated."); + return HYBBX_OK; + } + + if (target.active) { + hybbx_session_write_line(session, "Account is already active."); + return HYBBX_OK; + } + + target.active = 1; + rc = hybbx_storage_update_user(hybbx_service_get_storage(service), &target); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Activated '%s'.", + hybbx_user_display_name(&target)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_promote(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + hybbx_user_level_t new_level; + char buf[128]; + hybbx_result_t rc; + + if (cmd->argc < 2 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0' || + cmd->argv[1] == NULL || cmd->argv[1][0] == '\0') { + hybbx_session_write_line(session, + "Usage: /promote <username> admin|mod"); + return HYBBX_OK; + } + + actor_level = hybbx_session_user_level(session); + new_level = hybbx_user_level_parse(cmd->argv[1]); + if (new_level != HYBBX_LEVEL_ADMIN && new_level != HYBBX_LEVEL_MOD) { + hybbx_session_write_line(session, + "Level must be admin or mod."); + return HYBBX_OK; + } + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (!hybbx_commands_registry_may_promote(actor_level, target.level, target.active, + new_level)) { + if (actor_level == HYBBX_LEVEL_MOD || actor_level == HYBBX_LEVEL_USER) { + cmd_deny_privilege(session); + } else if (new_level == HYBBX_LEVEL_ADMIN) { + hybbx_session_write_line(session, + "Only the Sysop may add Admins."); + } else { + hybbx_session_write_line(session, + "Only Sysop or Admin may add Mods."); + } + return HYBBX_ERR_DENIED; + } + + if (target.level == new_level) { + snprintf(buf, sizeof(buf), "'%s' is already %s.", + hybbx_user_display_name(&target), + hybbx_user_level_name(new_level)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; + } + + target.level = new_level; + rc = hybbx_storage_update_user(hybbx_service_get_storage(service), &target); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Promoted '%s' to %s.", + hybbx_user_display_name(&target), + hybbx_user_level_name(new_level)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_demote(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + hybbx_user_level_t old_level; + char buf[128]; + hybbx_result_t rc; + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, "Usage: /demote <username>"); + return HYBBX_OK; + } + + actor_level = hybbx_session_user_level(session); + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (str_ieq(target.username, hybbx_session_username(session))) { + hybbx_session_write_line(session, "You cannot demote your own account."); + return HYBBX_OK; + } + + old_level = target.level; + if (!hybbx_commands_registry_may_demote(actor_level, target.level)) { + if (actor_level == HYBBX_LEVEL_MOD || actor_level == HYBBX_LEVEL_USER) { + cmd_deny_privilege(session); + } else if (target.level == HYBBX_LEVEL_ADMIN) { + hybbx_session_write_line(session, + "Only the Sysop may remove Admins."); + } else if (target.level == HYBBX_LEVEL_MOD) { + hybbx_session_write_line(session, + "Only Sysop or Admin may remove Mods."); + } else { + hybbx_session_write_line(session, + "That account is not an Admin or Mod."); + } + return HYBBX_ERR_DENIED; + } + + target.level = HYBBX_LEVEL_USER; + rc = hybbx_storage_update_user(hybbx_service_get_storage(service), &target); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Demoted '%s' from %s to user.", + hybbx_user_display_name(&target), + hybbx_user_level_name(old_level)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_delete(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + char buf[128]; + hybbx_result_t rc; + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, "Usage: /delete <username>"); + return HYBBX_OK; + } + + actor_level = hybbx_session_user_level(session); + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (str_ieq(target.username, hybbx_session_username(session))) { + hybbx_session_write_line(session, "You cannot delete your own account."); + return HYBBX_OK; + } + + if (hybbx_user_level_is_sysop(target.level)) { + hybbx_session_write_line(session, + "The Sysop account is permanent and cannot be deleted."); + return HYBBX_OK; + } + + if (!hybbx_commands_registry_may_delete(actor_level, target.level)) { + if (actor_level == HYBBX_LEVEL_MOD || actor_level == HYBBX_LEVEL_USER) { + cmd_deny_privilege(session); + } else if (target.level == HYBBX_LEVEL_ADMIN) { + hybbx_session_write_line(session, + "Admins cannot delete other Admins."); + } else { + cmd_deny_privilege(session); + } + return HYBBX_ERR_DENIED; + } + + rc = hybbx_storage_delete_user(hybbx_service_get_storage(service), target.id); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Deleted account '%s'.", + hybbx_user_display_name(&target)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_deleteme(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + const hybbx_session_record_t *rec; + hybbx_user_level_t level; + const char *arg; + hybbx_result_t rc; + + (void)service; + + level = hybbx_session_user_level(session); + if (hybbx_user_level_is_sysop(level)) { + hybbx_session_write_line(session, + "The Sysop account is permanent and cannot be deleted."); + return HYBBX_OK; + } + + arg = cmd->argc > 0 ? cmd->argv[0] : NULL; + if (arg != NULL && hybbx_bool_is_false(arg)) { + hybbx_session_write_line(session, "Account deletion cancelled."); + return HYBBX_OK; + } + + if (!cmd_deleteme_confirmed(arg)) { + hybbx_session_write_line(session, + "Usage: /deleteme yes|no"); + return HYBBX_OK; + } + + rec = hybbx_session_record(session); + if (rec == NULL || rec->user_id == 0) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_delete_user(hybbx_service_get_storage(service), + rec->user_id); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Account not found."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_session_write_line(session, "Account deleted. Goodbye."); + return HYBBX_SESSION_END; +} + +static hybbx_result_t cmd_register_user(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd, + int staff_created) +{ + hybbx_storage_t *storage; + hybbx_user_registration_t reg; + hybbx_user_record_t user; + char buf[HYBBX_USER_FULL_NAME_MAX + 64]; + hybbx_result_t rc; + + if (staff_created) { + if (cmd->argc < 5) { + hybbx_session_write_line(session, + "Usage: /usercreate <username> <full-name> <country> <location> <email>"); + return HYBBX_OK; + } + } else if (cmd->argc < 6) { + hybbx_session_write_line(session, + "Usage: /register <username> <full-name> <country> <location> <email> <password>"); + return HYBBX_OK; + } + + rc = parse_registration(cmd, ®, staff_created ? 0 : 1); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Registration fields too long."); + return HYBBX_OK; + } + + if (!staff_created && !hybbx_password_plain_valid(reg.password)) { + hybbx_session_write_line(session, + "Password must be 8-24 characters (- not allowed)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_register_user(storage, ®, &user); + if (rc == HYBBX_ERR_BUSY) { + hybbx_session_write_line(session, "Username or nickname already taken."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, + "Invalid username (4-12 chars, one _ or one -, max 4 digits) or bad profile."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (staff_created) { + snprintf(buf, sizeof(buf), "User '%s' created.", + hybbx_user_display_name(&user)); + } else { + snprintf(buf, sizeof(buf), "Registration received for '%s'.", + hybbx_user_display_name(&user)); + } + hybbx_session_write_line(session, buf); + snprintf(buf, sizeof(buf), "Name: %s", user.full_name); + hybbx_session_write_line(session, buf); + if (staff_created) { + hybbx_session_write_line(session, + "Account is inactive. Use /activate before the user can log in."); + } else { + const hybbx_mail_config_t *mail_cfg = hybbx_service_get_mail(service); + + (void)hybbx_mail_notify_staff_registration(service, ®, &user); + hybbx_session_write_line(session, + "A Sysop or Admin must activate your account before you can log in."); + if (mail_cfg != NULL && mail_cfg->enabled) { + hybbx_session_write_line(session, + "Sysop and Admin were notified by mail."); + } + } + return HYBBX_OK; +} + +static hybbx_result_t cmd_register(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + if (!hybbx_commands_registry_verb_allowed(hybbx_session_user_level(session), + "register") && + !(hybbx_session_login_prompt(session) && + !hybbx_session_logged_in(session))) { + hybbx_session_write_line(session, + "Only guests or login-prompt sessions may self-register with /register."); + return HYBBX_ERR_DENIED; + } + + return cmd_register_user(service, session, cmd, 0); +} + +static hybbx_result_t cmd_createuser(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + return cmd_register_user(service, session, cmd, 1); +} + +static hybbx_result_t cmd_changeme(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_storage_t *storage; + hybbx_user_registration_t reg; + hybbx_user_record_t user; + const char *old_password; + const char *new_password; + const char *username; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, + "Only registered users may use /changeme."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc < 6) { + hybbx_session_write_line(session, + "Usage: /changeme <oldpass> <newpass> <full-name> <country> <location> <email>"); + return HYBBX_OK; + } + + username = hybbx_session_username(session); + if (username == NULL || username[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + rc = parse_changeme(cmd, username, ®, &old_password, &new_password); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Profile fields too long."); + return HYBBX_OK; + } + + if (!hybbx_password_plain_valid(new_password)) { + hybbx_session_write_line(session, + "New password must be 8-24 characters (- not allowed)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_find_user(storage, reg.username, &user); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Account not found."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + { + const hybbx_session_record_t *rec = hybbx_session_record(session); + + if (rec == NULL || user.id != rec->user_id) { + hybbx_session_write_line(session, + "You can only change your own account."); + return HYBBX_OK; + } + + if (!hybbx_user_profile_valid(®)) { + hybbx_session_write_line(session, + "Invalid profile (check name, country, location, email)."); + return HYBBX_OK; + } + } + + if (user.password[0] == '\0') { + hybbx_session_write_line(session, + "Account has no password. Ask Sysop or Admin to set one."); + return HYBBX_OK; + } + + if (!hybbx_password_match(user.password, old_password)) { + hybbx_session_write_line(session, "Old password incorrect."); + return HYBBX_OK; + } + + hybbx_strlcpy(user.full_name, reg.full_name, sizeof(user.full_name)); + hybbx_strlcpy(user.country, reg.country, sizeof(user.country)); + hybbx_strlcpy(user.location, reg.location, sizeof(user.location)); + hybbx_strlcpy(user.email, reg.email, sizeof(user.email)); + + rc = hybbx_password_hash(new_password, user.password, sizeof(user.password)); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Could not store new password."); + return rc; + } + + rc = hybbx_storage_update_user(storage, &user); + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_session_write_line(session, "Account updated."); + return HYBBX_OK; +} + +static hybbx_result_t cmd_userchange(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_storage_t *storage; + hybbx_user_registration_t reg; + hybbx_user_record_t user; + const char *new_password; + hybbx_user_level_t actor_level; + hybbx_result_t rc; + char buf[160]; + + actor_level = hybbx_session_user_level(session); + if (!hybbx_user_level_is_sysop_or_admin(actor_level)) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc < 7) { + hybbx_session_write_line(session, + "Usage: /changeuser <user> <newpass> <full-name> <country> <location> <email>"); + return HYBBX_OK; + } + + rc = parse_userchange(cmd, ®, &new_password); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Profile fields too long."); + return HYBBX_OK; + } + + if (!hybbx_password_plain_valid(new_password)) { + hybbx_session_write_line(session, + "New password must be 8-24 characters (- not allowed)."); + return HYBBX_OK; + } + + if (!hybbx_user_profile_valid(®)) { + hybbx_session_write_line(session, + "Invalid profile (check name, country, location, email)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_find_user(storage, reg.username, &user); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (str_ieq(user.username, hybbx_session_username(session))) { + hybbx_session_write_line(session, + "Use /changeme to update your own account."); + return HYBBX_OK; + } + + if (hybbx_user_level_is_sysop(user.level)) { + hybbx_session_write_line(session, + "The Sysop account cannot be changed with /changeuser."); + return HYBBX_OK; + } + + if (!hybbx_commands_registry_may_userchange(actor_level, user.level)) { + if (actor_level == HYBBX_LEVEL_ADMIN && + user.level == HYBBX_LEVEL_ADMIN) { + hybbx_session_write_line(session, + "Admins cannot change other Admins. Sysop only."); + } else { + cmd_deny_privilege(session); + } + return HYBBX_ERR_DENIED; + } + + hybbx_strlcpy(user.full_name, reg.full_name, sizeof(user.full_name)); + hybbx_strlcpy(user.country, reg.country, sizeof(user.country)); + hybbx_strlcpy(user.location, reg.location, sizeof(user.location)); + hybbx_strlcpy(user.email, reg.email, sizeof(user.email)); + + rc = hybbx_password_hash(new_password, user.password, sizeof(user.password)); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Could not store new password."); + return rc; + } + + rc = hybbx_storage_update_user(storage, &user); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Updated account '%s'.", + hybbx_user_display_name(&user)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_userdelete(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + char buf[128]; + hybbx_result_t rc; + + actor_level = hybbx_session_user_level(session); + if (!hybbx_user_level_is_sysop(actor_level)) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, "Usage: /deleteuser <username>"); + return HYBBX_OK; + } + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (str_ieq(target.username, hybbx_session_username(session))) { + hybbx_session_write_line(session, "You cannot delete your own account."); + return HYBBX_OK; + } + + if (!hybbx_commands_registry_may_userdelete(actor_level, target.level)) { + hybbx_session_write_line(session, + "The Sysop account is permanent and cannot be deleted."); + return HYBBX_OK; + } + + rc = hybbx_storage_delete_user(hybbx_service_get_storage(service), target.id); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Deleted account '%s'.", + hybbx_user_display_name(&target)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_login(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_storage_t *storage; + hybbx_user_record_t user; + const hybbx_auth_config_t *auth; + const char *guest_prefix; + char username[HYBBX_USER_NAME_MAX]; + unsigned guest_slot; + hybbx_result_t rc; + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, + "Usage: /login <username> <password>"); + return HYBBX_OK; + } + + auth = hybbx_service_get_auth(service); + guest_prefix = auth != NULL ? auth->guest_prefix : HYBBX_AUTH_DEFAULT_GUEST_PREFIX; + + hybbx_strlcpy(username, cmd->argv[0], sizeof(username)); + hybbx_username_normalize(username); + + if (hybbx_guest_slot_from_username(guest_prefix, username, &guest_slot)) { + if (auth != NULL && auth->auto_login) { + hybbx_session_write_line(session, + "Guest access is automatic on connect (auto_login). " + "/login is for registered accounts only."); + } else { + hybbx_session_write_line(session, + "/login is for registered accounts only."); + } + return HYBBX_OK; + } + + if (cmd->argc < 2 || cmd->argv[1] == NULL || cmd->argv[1][0] == '\0') { + hybbx_session_write_line(session, + "Usage: /login <username> <password>"); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_find_user(storage, username, &user); + + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (hybbx_user_level_is_guest(user.level)) { + hybbx_session_write_line(session, + "Guests are ephemeral and use auto-login on connect."); + return HYBBX_OK; + } + + if (!user.active) { + hybbx_session_write_line(session, + "Account pending activation by Sysop or Admin."); + return HYBBX_OK; + } + + if (user.password[0] == '\0') { + hybbx_session_write_line(session, + "Account has no password. Contact Sysop or Admin."); + return HYBBX_OK; + } + + if (!hybbx_password_match(user.password, cmd->argv[1])) { + const hybbx_session_record_t *rec = hybbx_session_record(session); + + hybbx_security_log_write("login_fail ip=%s user=%s transport=%s", + rec != NULL && rec->remote[0] != '\0' ? + rec->remote : "?", + username, + rec != NULL && rec->transport[0] != '\0' ? + rec->transport : "?"); + if (rec != NULL && rec->remote[0] != '\0') { + hybbx_security_ban_login_fail(rec->remote, rec->transport); + } + hybbx_session_write_line(session, "Invalid password."); + return HYBBX_OK; + } + + if (hybbx_service_find_registered_session(service, user.id, session) != + NULL) { + hybbx_session_write_line(session, + "That account is already logged in elsewhere."); + return HYBBX_OK; + } + + rc = hybbx_session_switch_user(session, &user); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Login failed."); + return rc; + } + + { + time_t since_login = user.last_login_at; + const hybbx_texts_config_t *texts; + + texts = hybbx_service_get_texts(service); + if (texts != NULL) { + (void)hybbx_texts_send_motd(texts, session); + } + + hybbx_mail_announce_since_last_login(service, session, since_login); + + user.last_login_at = time(NULL); + (void)hybbx_storage_update_user(storage, &user); + } + hybbx_session_show_prompt(session); + return HYBBX_OK; +} + +static hybbx_result_t cmd_chat(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + const hybbx_chat_config_t *chat; + unsigned channel_index; + unsigned current; + const char *name; + char line[HYBBX_CHAT_CHANNEL_NAME_MAX + 48]; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use chat."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc > 0 && str_ieq(cmd->argv[0], "proxychat")) { + hybbx_parsed_command_t proxy_cmd; + char proxy_verb[] = "proxychat"; + + memset(&proxy_cmd, 0, sizeof(proxy_cmd)); + proxy_cmd.verb = proxy_verb; + return cmd_proxychat(service, session, &proxy_cmd); + } + + chat = hybbx_service_get_chat(service); + if (chat == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cmd->argc == 0) { + hybbx_chat_list_channels(session, chat); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "show")) { + if (cmd->argc != 1) { + hybbx_session_write_line(session, "Usage: /chat show"); + return HYBBX_OK; + } + hybbx_chat_show_channel(service, session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "showall")) { + if (cmd->argc != 1) { + hybbx_session_write_line(session, "Usage: /chat showall"); + return HYBBX_OK; + } + hybbx_chat_show_all(service, session); + return HYBBX_OK; + } + + rc = hybbx_chat_resolve_channel(chat, cmd->argv[0], &channel_index); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown chat channel."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + current = hybbx_session_chat_channel(session); + if (current == channel_index) { + snprintf(line, sizeof(line), "Already in channel %u %s.", + channel_index, + hybbx_chat_channel_name(chat, channel_index)); + hybbx_session_write_line(session, line); + return HYBBX_OK; + } + + rc = hybbx_session_join_chat_channel(session, channel_index); + if (rc != HYBBX_OK) { + return rc; + } + + name = hybbx_chat_channel_name(chat, channel_index); + snprintf(line, sizeof(line), "Channel %u: %s", channel_index, name); + hybbx_session_write_line(session, line); + hybbx_session_write_line(session, + "Each line is a message; /leave or /main to exit."); + return HYBBX_OK; +} + +static hybbx_result_t cmd_conference(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char topic[HYBBX_CONFERENCE_TOPIC_MAX]; + const char *user; + size_t i; + size_t pos = 0; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use conference."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc < 2) { + hybbx_session_write_line(session, + "Usage: /conference <topic> <user>"); + return HYBBX_OK; + } + + user = cmd->argv[cmd->argc - 1]; + topic[0] = '\0'; + + for (i = 0; i + 1 < cmd->argc; i++) { + size_t part_len = strlen(cmd->argv[i]); + + if (i > 0) { + if (pos + 1 >= sizeof(topic)) { + hybbx_session_write_line(session, "Topic too long."); + return HYBBX_OK; + } + topic[pos++] = ' '; + topic[pos] = '\0'; + } + + if (pos + part_len >= sizeof(topic)) { + hybbx_session_write_line(session, "Topic too long."); + return HYBBX_OK; + } + + memcpy(topic + pos, cmd->argv[i], part_len); + pos += part_len; + topic[pos] = '\0'; + } + + return hybbx_conference_start(service, session, topic, user); +} + +static void mail_join_subject(const hybbx_parsed_command_t *cmd, + char *out, size_t out_len) +{ + size_t i; + size_t pos = 0; + + if (out == NULL || out_len == 0) { + return; + } + + out[0] = '\0'; + if (cmd == NULL || cmd->argc < 3) { + return; + } + + for (i = 2; i < cmd->argc; i++) { + size_t part_len; + + if (cmd->argv[i] == NULL) { + continue; + } + + if (pos > 0) { + if (pos + 1 >= out_len) { + break; + } + out[pos++] = ' '; + out[pos] = '\0'; + } + + part_len = strlen(cmd->argv[i]); + if (pos + part_len >= out_len) { + part_len = out_len - pos - 1; + } + memcpy(out + pos, cmd->argv[i], part_len); + pos += part_len; + out[pos] = '\0'; + } +} + +static hybbx_result_t cmd_proxymail(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use proxymail."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc == 0) { + (void)hybbx_session_enter_proxymail(session); + hybbx_proxymail_list_inbox(service, session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "list")) { + unsigned from = 1; + unsigned to = 0; + + if (cmd->argc >= 2) { + if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) { + hybbx_session_write_line(session, + "Usage: /proxymail list [<from>-<to>]"); + return HYBBX_OK; + } + } + + (void)hybbx_session_enter_proxymail(session); + hybbx_proxymail_list_inbox_range(service, session, from, to); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "delete") || str_ieq(cmd->argv[0], "del")) { + unsigned from = 1; + unsigned to = 0; + + if (cmd->argc < 2 || cmd->argv[1] == NULL) { + hybbx_session_write_line(session, + "Usage: /proxymail delete <n|from-to>"); + return HYBBX_OK; + } + + if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) { + hybbx_session_write_line(session, + "Usage: /proxymail delete <n|from-to>"); + return HYBBX_OK; + } + + (void)hybbx_session_enter_proxymail(session); + return hybbx_proxymail_delete_range(service, session, from, to); + } + + if (str_ieq(cmd->argv[0], "recycle")) { + (void)hybbx_session_enter_proxymail(session); + return hybbx_proxymail_recycle_empty(service, session); + } + + if (str_ieq(cmd->argv[0], "read")) { + unsigned index; + + if (cmd->argc < 2 || cmd->argv[1] == NULL) { + hybbx_session_write_line(session, "Usage: /proxymail read <n>"); + return HYBBX_OK; + } + + index = (unsigned)strtoul(cmd->argv[1], NULL, 10); + if (index == 0) { + hybbx_session_write_line(session, "Usage: /proxymail read <n>"); + return HYBBX_OK; + } + + (void)hybbx_session_enter_proxymail(session); + return hybbx_proxymail_read(service, session, index); + } + + if (str_ieq(cmd->argv[0], "send")) { + if (cmd->argc < 3) { + hybbx_session_write_line(session, + "Usage: /proxymail send <user>@<main> <subject>"); + return HYBBX_OK; + } + + mail_join_subject(cmd, subject, sizeof(subject)); + if (subject[0] == '\0') { + hybbx_session_write_line(session, + "Usage: /proxymail send <user>@<main> <subject>"); + return HYBBX_OK; + } + + rc = hybbx_session_proxymail_compose_start(session, cmd->argv[1], + subject); + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, + "Invalid address or subject. Use user@remote-main."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_session_write_line(session, + "Compose body. /proxymail done to send."); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "done")) { + const char *body; + const char *to_address; + const char *mail_subject; + + if (!hybbx_session_proxymail_composing(session)) { + hybbx_session_write_line(session, "Not composing proxymail."); + return HYBBX_OK; + } + + to_address = hybbx_session_proxymail_compose_to(session); + mail_subject = hybbx_session_proxymail_compose_subject(session); + body = hybbx_session_proxymail_compose_body(session); + + rc = hybbx_proxymail_deliver(service, hybbx_session_display_name(session), + to_address, mail_subject, body); + + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, "Invalid address — use user@mainname."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_UNSUPPORTED) { + hybbx_session_write_line(session, + "Proxymail is not available — enable mains_proxy and peer links."); + hybbx_session_proxymail_compose_cancel(session); + hybbx_session_leave_area(session); + hybbx_session_show_prompt(session); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, + "Unknown remote main — check user@mainname and peer_id."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Send failed."); + return rc; + } + + hybbx_session_write_line(session, "Proxymail sent."); + hybbx_session_proxymail_compose_cancel(session); + hybbx_session_leave_area(session); + hybbx_session_show_prompt(session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "cancel")) { + if (hybbx_session_proxymail_composing(session)) { + hybbx_session_proxymail_compose_cancel(session); + hybbx_session_leave_area(session); + hybbx_session_write_line(session, "Compose cancelled."); + } else { + hybbx_session_write_line(session, "Not composing proxymail."); + } + return HYBBX_OK; + } + + hybbx_session_write_line(session, + "Unknown /proxymail subcommand. Try /help proxymail."); + return HYBBX_OK; +} + +static hybbx_result_t cmd_proxychat(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_result_t rc; + + (void)service; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use proxychat."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc > 0) { + hybbx_session_write_line(session, + "Usage: /proxychat — chat with users on other mains."); + return HYBBX_OK; + } + + rc = hybbx_session_enter_proxychat(session); + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_proxychat_show_banner(session); + return HYBBX_OK; +} + +static hybbx_result_t cmd_mail(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc > 0 && str_ieq(cmd->argv[0], "proxymail")) { + hybbx_parsed_command_t proxy_cmd; + char proxy_verb[] = "proxymail"; + unsigned i; + + memset(&proxy_cmd, 0, sizeof(proxy_cmd)); + proxy_cmd.verb = proxy_verb; + proxy_cmd.argc = cmd->argc > 0 ? cmd->argc - 1 : 0; + for (i = 0; i < proxy_cmd.argc && i < HYBBX_CMD_TOKEN_MAX; i++) { + proxy_cmd.argv[i] = cmd->argv[i + 1]; + } + return cmd_proxymail(service, session, &proxy_cmd); + } + + if (cmd->argc == 0) { + hybbx_mail_list_inbox(service, session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "list")) { + unsigned from = 1; + unsigned to = 0; + + if (cmd->argc >= 2) { + if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) { + hybbx_session_write_line(session, + "Usage: /mail list [<from>-<to>] e.g. list 1-15 list 5-20"); + return HYBBX_OK; + } + } + + hybbx_mail_list_inbox_range(service, session, from, to); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "delete") || str_ieq(cmd->argv[0], "del")) { + unsigned from = 1; + unsigned to = 0; + + if (cmd->argc < 2 || cmd->argv[1] == NULL) { + hybbx_session_write_line(session, + "Usage: /mail delete <n|from-to> e.g. delete 3 delete 5-20"); + return HYBBX_OK; + } + + if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) { + hybbx_session_write_line(session, + "Usage: /mail delete <n|from-to> e.g. delete 3 delete 5-20"); + return HYBBX_OK; + } + + return hybbx_mail_delete_range(service, session, from, to); + } + + if (str_ieq(cmd->argv[0], "recycle")) { + return hybbx_mail_recycle_empty(service, session); + } + + if (str_ieq(cmd->argv[0], "read")) { + unsigned index; + + if (cmd->argc < 2 || cmd->argv[1] == NULL) { + hybbx_session_write_line(session, "Usage: /mail read <n>"); + return HYBBX_OK; + } + + index = (unsigned)strtoul(cmd->argv[1], NULL, 10); + if (index == 0) { + hybbx_session_write_line(session, "Usage: /mail read <n>"); + return HYBBX_OK; + } + + return hybbx_mail_read(service, session, index); + } + + if (str_ieq(cmd->argv[0], "send")) { + if (cmd->argc < 3) { + hybbx_session_write_line(session, + "Usage: /mail send <user> <subject>"); + return HYBBX_OK; + } + + mail_join_subject(cmd, subject, sizeof(subject)); + if (subject[0] == '\0') { + hybbx_session_write_line(session, + "Usage: /mail send <user> <subject>"); + return HYBBX_OK; + } + + rc = hybbx_session_mail_compose_start(session, cmd->argv[1], subject); + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, "Subject too long."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_session_write_line(session, "Compose body. /mail done to send."); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "done")) { + const char *body; + const char *to_user; + const char *mail_subject; + + if (!hybbx_session_mail_composing(session)) { + hybbx_session_write_line(session, "Not composing mail."); + return HYBBX_OK; + } + + to_user = hybbx_session_mail_compose_to(session); + mail_subject = hybbx_session_mail_compose_subject(session); + body = hybbx_session_mail_compose_body(session); + + rc = hybbx_mail_deliver(service, hybbx_session_display_name(session), + to_user, mail_subject, body); + + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown recipient."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_DENIED) { + hybbx_session_write_line(session, "Cannot mail that user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Send failed."); + return rc; + } + + hybbx_session_write_line(session, "Message sent."); + hybbx_session_leave_area(session); + hybbx_session_show_prompt(session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "cancel")) { + if (hybbx_session_mail_composing(session)) { + hybbx_session_leave_area(session); + hybbx_session_write_line(session, "Compose cancelled."); + } else { + hybbx_session_write_line(session, "Not composing mail."); + } + return HYBBX_OK; + } + + hybbx_session_write_line(session, "Unknown /mail subcommand. Try /help mail."); + return HYBBX_OK; +} + +static void cmd_emit_current_area(hybbx_session_t *session) +{ + const char *name = hybbx_session_area_name(hybbx_session_area(session)); + char line[64]; + + snprintf(line, sizeof(line), "%c%s.", + (char)toupper((unsigned char)name[0]), name + 1); + hybbx_session_write_line(session, line); + hybbx_session_show_prompt(session); +} + +static hybbx_result_t cmd_leave(hybbx_session_t *session) +{ + hybbx_session_area_t before = hybbx_session_area(session); + + hybbx_session_leave_area(session); + if (hybbx_session_area(session) != before) { + cmd_emit_current_area(session); + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_main(hybbx_session_t *session) +{ + hybbx_session_area_t before = hybbx_session_area(session); + + hybbx_session_go_main(session); + if (before != HYBBX_AREA_MAIN) { + cmd_emit_current_area(session); + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_exit(hybbx_session_t *session) +{ + hybbx_session_write_line(session, "Goodbye."); + return HYBBX_SESSION_END; +} + +static hybbx_result_t cmd_broadcast(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char message[HYBBX_BROADCAST_MESSAGE_MAX + 1]; + size_t off = 0; + int i; + hybbx_result_t rc; + + if (cmd->argc < 1) { + cmd_registry_usage(session, "broadcast"); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "ax25")) { + if (cmd->argc > 1) { + hybbx_session_write_line(session, + "RF beacon uses ax25_auto_message from INI. Use: /broadcast ax25"); + return HYBBX_OK; + } + + rc = hybbx_broadcast_ax25_manual(service); + if (rc == HYBBX_ERR_UNSUPPORTED) { + hybbx_session_write_line(session, "AX.25 broadcast is disabled."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_DENIED) { + hybbx_session_write_line(session, + "No qualifying packet-radio links for AX.25 broadcast."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_BUSY) { + hybbx_session_write_line(session, + "AX.25 broadcast deferred (link busy)."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "AX.25 broadcast failed."); + return HYBBX_ERR_IO; + } + + hybbx_session_write_line(session, "AX.25 broadcast sent."); + return HYBBX_OK; + } + + message[0] = '\0'; + for (i = 0; i < (int)cmd->argc; i++) { + size_t part_len = strlen(cmd->argv[i]); + + if (off > 0 && off < sizeof(message) - 1) { + message[off++] = ' '; + } + if (off + part_len >= sizeof(message)) { + part_len = sizeof(message) - off - 1; + } + if (part_len > 0) { + memcpy(message + off, cmd->argv[i], part_len); + off += part_len; + } + } + message[off] = '\0'; + + if (message[0] == '\0') { + hybbx_session_write_line(session, "Missing announce message."); + return HYBBX_ERR_INVALID; + } + + rc = hybbx_broadcast_announce(service, session, message); + if (rc == HYBBX_ERR_UNSUPPORTED) { + hybbx_session_write_line(session, "Broadcast is disabled."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, "Message too long."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Broadcast failed."); + return HYBBX_ERR_IO; + } + + hybbx_session_write_line(session, "Broadcast sent."); + return HYBBX_OK; +} + +static hybbx_result_t cmd_shutdown(hybbx_service_t *service, + hybbx_session_t *session) +{ + if (!hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + hybbx_security_log_write("shutdown ip=%s user=%s transport=%s", + hybbx_session_record(session) != NULL && + hybbx_session_record(session)->remote[0] != '\0' ? + hybbx_session_record(session)->remote : "?", + hybbx_session_display_name(session), + hybbx_session_record(session) != NULL ? + hybbx_session_record(session)->transport : "?"); + hybbx_session_write_line(session, "Shutting down HyBBX."); + hybbx_service_request_shutdown(service, 0); + return HYBBX_OK; +} + +static hybbx_result_t cmd_restart(hybbx_service_t *service, + hybbx_session_t *session) +{ + if (!hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + hybbx_security_log_write("restart ip=%s user=%s transport=%s", + hybbx_session_record(session) != NULL && + hybbx_session_record(session)->remote[0] != '\0' ? + hybbx_session_record(session)->remote : "?", + hybbx_session_display_name(session), + hybbx_session_record(session) != NULL ? + hybbx_session_record(session)->transport : "?"); + hybbx_session_write_line(session, "Restarting HyBBX."); + hybbx_service_request_shutdown(service, 1); + return HYBBX_OK; +} + +static hybbx_result_t cmd_version(hybbx_service_t *service, + hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + return hybbx_texts_send_version(texts, session); +} + +static hybbx_result_t cmd_clear(hybbx_session_t *session) +{ + return hybbx_session_clear_terminal(session); +} + +static hybbx_result_t cmd_echo(hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char buf[48]; + + if (cmd->argc < 2) { + snprintf(buf, sizeof(buf), "Input echo is %s.", + hybbx_bool_to_string(hybbx_session_input_echo(session))); + hybbx_session_write_line(session, buf); + return HYBBX_OK; + } + + if (hybbx_bool_is_true(cmd->argv[1])) { + return hybbx_session_set_input_echo(session, 1); + } + + if (hybbx_bool_is_false(cmd->argv[1])) { + return hybbx_session_set_input_echo(session, 0); + } + + hybbx_session_write_line(session, "Usage: /echo yes|no"); + return HYBBX_ERR_INVALID; +} + +typedef struct monitor_list_ctx { + hybbx_session_t *requester; + unsigned count; +} monitor_list_ctx_t; + +static void monitor_list_visitor(hybbx_session_t *session, void *userdata) +{ + monitor_list_ctx_t *ctx = (monitor_list_ctx_t *)userdata; + const hybbx_session_record_t *rec; + const char *user; + const char *plugin; + char line[96]; + + if (ctx == NULL || session == NULL || !hybbx_session_logged_in(session)) { + return; + } + + if (!hybbx_session_is_interactive_user(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + user = hybbx_session_display_name(session); + rec = hybbx_session_record(session); + plugin = (rec != NULL && rec->transport[0] != '\0') + ? rec->transport + : (session->transport != NULL ? session->transport->name : "?"); + snprintf(line, sizeof(line), " %s@%s", user, plugin); + hybbx_session_write_line(ctx->requester, line); + ctx->count++; +} + +static void monitor_show_cmds(hybbx_session_t *session) +{ + hybbx_session_write_line(session, "Monitor admin commands:"); + hybbx_session_write_line(session, + " /monitor meet <user> [force] — conference invite (20s) or force"); + hybbx_session_write_line(session, + "Run any of these as normal /verb (stay invisible while Sysop hidden):"); + hybbx_commands_registry_show_sysop_cmds(session); +} + +static hybbx_result_t cmd_monitor(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + const char *sub; + monitor_list_ctx_t list_ctx; + const hybbx_monitor_config_t *mcfg; + char line[128]; + + if (!hybbx_monitor_enabled()) { + hybbx_session_write_line(session, "Monitor is disabled ([monitor] enabled=no)."); + return HYBBX_ERR_DENIED; + } + + if (!hybbx_monitor_session_may_use(session) && + !hybbx_commands_registry_verb_allowed( + hybbx_session_user_level(session), "monitor")) { + hybbx_session_write_line(session, "Monitor access denied."); + return HYBBX_ERR_DENIED; + } + + sub = (cmd->argc >= 1) ? cmd->argv[0] : "list"; + + if (str_ieq(sub, "on")) { + return hybbx_monitor_set_active(session, 1); + } + + if (str_ieq(sub, "off")) { + return hybbx_monitor_set_active(session, 0); + } + + if (str_ieq(sub, "cmd") || str_ieq(sub, "cmds") || str_ieq(sub, "commands")) { + monitor_show_cmds(session); + return HYBBX_OK; + } + + if (str_ieq(sub, "meet")) { + int force = 0; + const char *user; + + if (cmd->argc < 2) { + hybbx_session_write_line(session, + "Usage: /monitor meet <user> [force]"); + return HYBBX_ERR_INVALID; + } + user = cmd->argv[1]; + if (cmd->argc >= 3 && str_ieq(cmd->argv[2], "force")) { + force = 1; + } + if (!hybbx_monitor_is_active(session)) { + hybbx_session_write_line(session, + "Turn monitor on first: /monitor on"); + return HYBBX_ERR_DENIED; + } + return hybbx_conference_monitor_meet(service, session, user, force); + } + + if (!str_ieq(sub, "list") && cmd->argc >= 1) { + hybbx_session_write_line(session, + "Usage: /monitor on|off|list|cmds|meet <user> [force]"); + return HYBBX_ERR_INVALID; + } + + mcfg = hybbx_monitor_config_get(); + snprintf(line, sizeof(line), "Monitor: %s", + hybbx_monitor_is_active(session) ? "on" : "off"); + hybbx_session_write_line(session, line); + if (mcfg != NULL) { + snprintf(line, sizeof(line), + "Follow: hybbx=%s security=%s invisible-sysop=%s " + "invite_timeout=%us", + mcfg->follow_hybbx ? "yes" : "no", + mcfg->follow_security ? "yes" : "no", + mcfg->invisible_sysop ? "yes" : "no", + mcfg->invite_timeout_sec); + hybbx_session_write_line(session, line); + } + if (hybbx_session_hidden_from_who(session)) { + hybbx_session_write_line(session, "You are hidden from /who."); + } + hybbx_session_write_line(session, "Online (visible):"); + list_ctx.requester = session; + list_ctx.count = 0; + hybbx_service_visit_sessions(service, monitor_list_visitor, &list_ctx); + if (list_ctx.count == 0) { + hybbx_session_write_line(session, " (none)"); + } + return HYBBX_OK; +} + +hybbx_result_t hybbx_command_dispatch(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + if (cmd == NULL || service == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cmd->scope != HYBBX_CMD_SCOPE_HYBBX) { + return HYBBX_ERR_INVALID; + } + + (void)hybbx_session_command_gap(session); + + if (cmd->verb == NULL || cmd->verb[0] == '\0') { + return cmd_help(session, cmd); + } + + if (str_ieq(cmd->verb, "help") || str_ieq(cmd->verb, "?")) { + return cmd_help(session, cmd); + } + + { + hybbx_result_t access = cmd_check_access(session, cmd->verb); + if (access != HYBBX_OK) { + return access; + } + } + + if (str_ieq(cmd->verb, "news")) { + return cmd_news(service, session); + } + + if (str_ieq(cmd->verb, "banner") || str_ieq(cmd->verb, "loginmsg")) { + return cmd_banner(service, session); + } + + if (str_ieq(cmd->verb, "motd")) { + return cmd_motd(service, session); + } + + if (str_ieq(cmd->verb, "rules") || str_ieq(cmd->verb, "legal")) { + return cmd_rules(service, session); + } + + if (str_ieq(cmd->verb, "who") || str_ieq(cmd->verb, "online")) { + return cmd_who(service, session); + } + + if (str_ieq(cmd->verb, "monitor") || str_ieq(cmd->verb, "mon")) { + return cmd_monitor(service, session, cmd); + } + + if (str_ieq(cmd->verb, "users")) { + return cmd_users(service, session); + } + + if (str_ieq(cmd->verb, "session") || str_ieq(cmd->verb, "info")) { + return cmd_session(session); + } + + if (str_ieq(cmd->verb, "version") || str_ieq(cmd->verb, "ver")) { + return cmd_version(service, session); + } + + if (str_ieq(cmd->verb, "leave") || str_ieq(cmd->verb, "back")) { + return cmd_leave(session); + } + + if (str_ieq(cmd->verb, "main")) { + return cmd_main(session); + } + + if (str_ieq(cmd->verb, "menu")) { + if (cmd->argc == 0) { + hybbx_commands_registry_show_menu(session); + return HYBBX_OK; + } + return cmd_help_topic(session, cmd->argv[0]); + } + + if (str_ieq(cmd->verb, "index")) { + if (cmd->argc == 0) { + hybbx_commands_registry_show_index(session); + return HYBBX_OK; + } + return cmd_help_topic(session, cmd->argv[0]); + } + + if (str_ieq(cmd->verb, "alias")) { + if (cmd->argc == 0) { + hybbx_commands_registry_show_aliases(session); + return HYBBX_OK; + } + return cmd_help_topic(session, cmd->argv[0]); + } + + if (str_ieq(cmd->verb, "clear") || str_ieq(cmd->verb, "cls") || + str_ieq(cmd->verb, "reset")) { + return cmd_clear(session); + } + + if (str_ieq(cmd->verb, "echo")) { + return cmd_echo(session, cmd); + } + + if (str_ieq(cmd->verb, "chat")) { + return cmd_chat(service, session, cmd); + } + + if (str_ieq(cmd->verb, "conference") || str_ieq(cmd->verb, "meeting")) { + return cmd_conference(service, session, cmd); + } + + if (str_ieq(cmd->verb, "mail")) { + return cmd_mail(service, session, cmd); + } + + if (str_ieq(cmd->verb, "proxymail")) { + return cmd_proxymail(service, session, cmd); + } + + if (str_ieq(cmd->verb, "proxychat")) { + return cmd_proxychat(service, session, cmd); + } + + if (str_ieq(cmd->verb, "login")) { + return cmd_login(service, session, cmd); + } + + if (str_ieq(cmd->verb, "register")) { + return cmd_register(service, session, cmd); + } + + if (str_ieq(cmd->verb, "changeme")) { + return cmd_changeme(service, session, cmd); + } + + if (str_ieq(cmd->verb, "changeuser") || str_ieq(cmd->verb, "userchange")) { + return cmd_userchange(service, session, cmd); + } + + if (str_ieq(cmd->verb, "deleteuser") || str_ieq(cmd->verb, "userdelete")) { + return cmd_userdelete(service, session, cmd); + } + + if (str_ieq(cmd->verb, "usercreate") || str_ieq(cmd->verb, "createuser")) { + return cmd_createuser(service, session, cmd); + } + + if (str_ieq(cmd->verb, "activate")) { + return cmd_activate(service, session, cmd); + } + + if (str_ieq(cmd->verb, "promote")) { + return cmd_promote(service, session, cmd); + } + + if (str_ieq(cmd->verb, "demote")) { + return cmd_demote(service, session, cmd); + } + + if (str_ieq(cmd->verb, "delete") || str_ieq(cmd->verb, "del")) { + return cmd_delete(service, session, cmd); + } + + if (str_ieq(cmd->verb, "deleteme")) { + return cmd_deleteme(service, session, cmd); + } + + if (str_ieq(cmd->verb, "broadcast") || str_ieq(cmd->verb, "announce")) { + return cmd_broadcast(service, session, cmd); + } + + if (str_ieq(cmd->verb, "shutdown")) { + return cmd_shutdown(service, session); + } + + if (str_ieq(cmd->verb, "restart")) { + return cmd_restart(service, session); + } + + if (str_ieq(cmd->verb, "exit") || str_ieq(cmd->verb, "logout") || + str_ieq(cmd->verb, "bye") || str_ieq(cmd->verb, "quit")) { + return cmd_exit(session); + } + + hybbx_session_write_line(session, cmd_help_unknown(session)); + return HYBBX_ERR_NOT_FOUND; +} diff --git a/src/core/command_parse.c b/src/core/command_parse.c new file mode 100644 index 0000000..f96bf6c --- /dev/null +++ b/src/core/command_parse.c @@ -0,0 +1,347 @@ +#include "hybbx/command.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" + +#include <ctype.h> +#include <stdlib.h> +#include <string.h> + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + if (!hybbx_size_ok(len)) { + return NULL; + } + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static const char *skip_leading_space(const char *line) +{ + if (line == NULL) { + return NULL; + } + + while (*line != '\0' && isspace((unsigned char)*line)) { + line++; + } + + return line; +} + +static char *ltrim_copy(char *s) +{ + char *start; + + if (s == NULL) { + return NULL; + } + + start = (char *)skip_leading_space(s); + if (start != s) { + memmove(s, start, strlen(start) + 1); + } + + return s; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int strip_one_command_alias(char *rest, const char *alias) +{ + size_t alias_len; + + if (alias == NULL || rest == NULL) { + return -1; + } + + alias_len = strlen(alias); + + if (str_ieq(rest, alias)) { + return 1; + } + + if (strncmp(rest, alias, alias_len) == 0 && + isspace((unsigned char)rest[alias_len])) { + memmove(rest, rest + alias_len, strlen(rest + alias_len) + 1); + ltrim_copy(rest); + return rest[0] == '\0' ? 1 : 0; + } + + return -1; +} + +static int strip_command_alias(char *rest) +{ + static const char *aliases[] = { "commands", "command", "cmd", NULL }; + int rc; + size_t i; + + if (rest == NULL) { + return 0; + } + + if (rest[0] == '\0') { + return 1; + } + + for (i = 0; aliases[i] != NULL; i++) { + rc = strip_one_command_alias(rest, aliases[i]); + if (rc >= 0) { + return rc; + } + } + + return 0; +} + +hybbx_command_scope_t hybbx_command_classify(const char *line) +{ + const char *p = skip_leading_space(line); + + if (p == NULL || *p == '\0') { + return HYBBX_CMD_SCOPE_LOCAL; + } + + if (*p == '/') { + return HYBBX_CMD_SCOPE_HYBBX; + } + + if (*p == ';' || *p == '#') { + return HYBBX_CMD_SCOPE_COMMENT; + } + + return HYBBX_CMD_SCOPE_LOCAL; +} + +int hybbx_command_is_comment(const char *line) +{ + return hybbx_command_classify(line) == HYBBX_CMD_SCOPE_COMMENT; +} + +int hybbx_command_is_hybbx(const char *line) +{ + return hybbx_command_classify(line) == HYBBX_CMD_SCOPE_HYBBX; +} + +static size_t count_tokens(const char *line) +{ + size_t count = 0; + size_t i = 0; + int in_token = 0; + + while (line[i] != '\0') { + if (!isspace((unsigned char)line[i])) { + if (!in_token) { + count++; + in_token = 1; + } + } else { + in_token = 0; + } + i++; + } + + return count; +} + +static hybbx_result_t split_tokens(char *line, char **tokens, size_t max) +{ + size_t count = 0; + size_t i = 0; + + while (line[i] != '\0' && count < max) { + while (line[i] != '\0' && isspace((unsigned char)line[i])) { + i++; + } + if (line[i] == '\0') { + break; + } + tokens[count++] = line + i; + while (line[i] != '\0' && !isspace((unsigned char)line[i])) { + i++; + } + if (line[i] != '\0') { + line[i] = '\0'; + i++; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t parse_hybbx_line(const char *line, hybbx_parsed_command_t *out) +{ + char *rest; + char *work; + size_t token_count; + char **tokens; + size_t i; + hybbx_result_t rc; + + if (line[0] != '/') { + return HYBBX_ERR_INVALID; + } + + rest = hybbx_strdup(line + 1); + if (rest == NULL) { + return HYBBX_ERR_NOMEM; + } + + ltrim_copy(rest); + + if (strip_command_alias(rest)) { + out->verb = hybbx_strdup("help"); + if (out->verb == NULL) { + free(rest); + return HYBBX_ERR_NOMEM; + } + free(rest); + return HYBBX_OK; + } + + token_count = count_tokens(rest); + if (token_count == 0) { + out->verb = hybbx_strdup("help"); + free(rest); + if (out->verb == NULL) { + return HYBBX_ERR_NOMEM; + } + return HYBBX_OK; + } + + if (token_count > HYBBX_CMD_TOKEN_MAX) { + free(rest); + return HYBBX_ERR_INVALID; + } + + work = hybbx_strdup(rest); + free(rest); + if (work == NULL) { + return HYBBX_ERR_NOMEM; + } + + tokens = calloc(token_count, sizeof(*tokens)); + if (tokens == NULL) { + free(work); + return HYBBX_ERR_NOMEM; + } + + rc = split_tokens(work, tokens, token_count); + if (rc != HYBBX_OK) { + free(tokens); + free(work); + return rc; + } + + out->verb = hybbx_strdup(tokens[0]); + if (out->verb == NULL) { + free(tokens); + free(work); + return HYBBX_ERR_NOMEM; + } + + out->argc = token_count > 1 ? token_count - 1 : 0; + if (out->argc > 0) { + out->argv = calloc(out->argc, sizeof(*out->argv)); + if (out->argv == NULL) { + free(tokens); + free(work); + hybbx_command_free(out); + return HYBBX_ERR_NOMEM; + } + + for (i = 0; i < out->argc; i++) { + out->argv[i] = hybbx_strdup(tokens[i + 1]); + if (out->argv[i] == NULL) { + free(tokens); + free(work); + hybbx_command_free(out); + return HYBBX_ERR_NOMEM; + } + } + } + + free(tokens); + free(work); + return HYBBX_OK; +} + +hybbx_result_t hybbx_command_parse(const char *line, + hybbx_parsed_command_t *out) +{ + char *trimmed; + hybbx_result_t rc; + + if (line == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + + trimmed = hybbx_strdup(skip_leading_space(line)); + if (trimmed == NULL) { + return HYBBX_ERR_NOMEM; + } + + out->scope = hybbx_command_classify(trimmed); + out->line = trimmed; + + if (out->scope != HYBBX_CMD_SCOPE_HYBBX) { + return HYBBX_OK; + } + + rc = parse_hybbx_line(trimmed, out); + if (rc != HYBBX_OK) { + hybbx_command_free(out); + } + + return rc; +} + +void hybbx_command_free(hybbx_parsed_command_t *cmd) +{ + size_t i; + + if (cmd == NULL) { + return; + } + + free(cmd->line); + free(cmd->verb); + + for (i = 0; i < cmd->argc; i++) { + free(cmd->argv[i]); + } + free(cmd->argv); + + memset(cmd, 0, sizeof(*cmd)); +} diff --git a/src/core/commands_registry.c b/src/core/commands_registry.c new file mode 100644 index 0000000..6f63178 --- /dev/null +++ b/src/core/commands_registry.c @@ -0,0 +1,1429 @@ +#include "hybbx/commands_registry.h" +#include "hybbx/session.h" +#include "hybbx/monitor.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +typedef struct menu_subarea { + char verbs[HYBBX_COMMANDS_VERBS_PER_GROUP][HYBBX_CMD_VERB_MAX]; + unsigned verb_count; +} menu_subarea_t; + +typedef struct menu_area { + char label[16]; + char verbs[HYBBX_COMMANDS_VERBS_PER_GROUP][HYBBX_CMD_VERB_MAX]; + unsigned verb_count; + menu_subarea_t subareas[HYBBX_AREAS_SUB_MAX]; + unsigned subarea_count; +} menu_area_t; + +typedef struct alias_entry { + char canonical[HYBBX_CMD_VERB_MAX]; + char aliases[HYBBX_COMMANDS_ALIAS_PER][HYBBX_CMD_VERB_MAX]; + unsigned alias_count; +} alias_entry_t; + +typedef struct target_right_rule { + hybbx_user_level_t actor; + hybbx_user_level_t targets[5]; + unsigned target_count; +} target_right_rule_t; + +typedef struct promote_right_rule { + hybbx_user_level_t actor; + hybbx_user_level_t to_level; + hybbx_user_level_t from_levels[4]; + unsigned from_count; +} promote_right_rule_t; + +typedef struct demote_right_rule { + hybbx_user_level_t actor; + hybbx_user_level_t from_levels[4]; + unsigned from_count; +} demote_right_rule_t; + +typedef struct commands_registry { + int loaded; + char menu_header[HYBBX_COMMANDS_HEADER_MAX]; + char index_header[HYBBX_COMMANDS_HEADER_MAX]; + char alias_header[HYBBX_COMMANDS_HEADER_MAX]; + menu_area_t areas[HYBBX_AREAS_MAX]; + unsigned area_count; + char menu_levels[HYBBX_MENU_LEVELS_MAX][16]; + char menu_area_labels[HYBBX_MENU_LEVELS_MAX] + [HYBBX_MENU_AREAS_PER_LEVEL][16]; + unsigned menu_area_count[HYBBX_MENU_LEVELS_MAX]; + unsigned menu_level_count; + char index_labels[HYBBX_MENU_AREAS_PER_LEVEL][16]; + unsigned index_label_count; + target_right_rule_t userchange_rules[HYBBX_RIGHTS_TARGET_RULES_MAX]; + unsigned userchange_rule_count; + target_right_rule_t userdelete_rules[HYBBX_RIGHTS_TARGET_RULES_MAX]; + unsigned userdelete_rule_count; + target_right_rule_t delete_rules[HYBBX_RIGHTS_TARGET_RULES_MAX]; + unsigned delete_rule_count; + promote_right_rule_t promote_rules[HYBBX_RIGHTS_PROMOTE_RULES_MAX]; + unsigned promote_rule_count; + demote_right_rule_t demote_rules[HYBBX_RIGHTS_DEMOTE_RULES_MAX]; + unsigned demote_rule_count; + alias_entry_t aliases[HYBBX_COMMANDS_ALIASES_MAX]; + unsigned alias_count; + hybbx_command_def_t commands[HYBBX_COMMANDS_MAX]; + unsigned command_count; + char alias_lines[HYBBX_COMMANDS_ALIAS_LINES][HYBBX_LINE_MAX]; + unsigned alias_line_count; +} commands_registry_t; + +static commands_registry_t g_registry; + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static void trim_inplace(char *s) +{ + char *start = s; + char *end; + + if (s == NULL) { + return; + } + + while (*start != '\0' && isspace((unsigned char)*start)) { + start++; + } + + if (start != s) { + memmove(s, start, strlen(start) + 1); + } + + end = s + strlen(s); + while (end > s && isspace((unsigned char)end[-1])) { + end--; + } + *end = '\0'; +} + +static void strip_quotes(char *s) +{ + size_t len; + + if (s == NULL) { + return; + } + + trim_inplace(s); + len = strlen(s); + if (len >= 2 && + ((s[0] == '"' && s[len - 1] == '"') || + (s[0] == '\'' && s[len - 1] == '\''))) { + s[len - 1] = '\0'; + memmove(s, s + 1, len - 1); + } +} + +static unsigned line_indent(const char *line) +{ + unsigned n = 0; + + while (line[n] == ' ') { + n++; + } + + return n; +} + +static const char *line_key(const char *line) +{ + if (line == NULL) { + return ""; + } + + while (*line == ' ') { + line++; + } + + return line; +} + +static int parse_bracket_list(const char *value, char out[][HYBBX_CMD_VERB_MAX], + unsigned max, unsigned *count) +{ + const char *p; + char token[HYBBX_CMD_VERB_MAX]; + size_t tlen; + + if (value == NULL || count == NULL) { + return 0; + } + + *count = 0; + p = strchr(value, '['); + if (p == NULL) { + return 0; + } + p++; + + while (*p != '\0' && *p != ']') { + while (*p == ' ' || *p == ',') { + p++; + } + if (*p == ']' || *p == '\0') { + break; + } + + tlen = 0; + while (*p != '\0' && *p != ',' && *p != ']' && + !isspace((unsigned char)*p) && tlen + 1 < sizeof(token)) { + token[tlen++] = *p++; + } + token[tlen] = '\0'; + if (token[0] == '\0') { + continue; + } + + if (*count >= max) { + return -1; + } + + hybbx_strlcpy(out[*count], token, HYBBX_CMD_VERB_MAX); + (*count)++; + } + + return 0; +} + +static int parse_level_list(const char *value, hybbx_user_level_t *out, + unsigned max, unsigned *count) +{ + char tokens[8][HYBBX_CMD_VERB_MAX]; + unsigned n = 0; + unsigned i; + + if (value == NULL || count == NULL) { + return 0; + } + + if (parse_bracket_list(value, tokens, max, &n) != 0) { + return -1; + } + + *count = 0; + for (i = 0; i < n && *count < max; i++) { + out[*count] = hybbx_user_level_parse(tokens[i]); + (*count)++; + } + + return 0; +} + +static hybbx_user_level_t level_from_name(const char *name) +{ + return hybbx_user_level_parse(name); +} + +static const char *menu_level_name(hybbx_user_level_t level) +{ + switch (level) { + case HYBBX_LEVEL_SYSOP: + return "Sysop"; + case HYBBX_LEVEL_ADMIN: + return "Admin"; + case HYBBX_LEVEL_MOD: + return "Mod"; + case HYBBX_LEVEL_USER: + return "User"; + default: + return "Guest"; + } +} + +static const menu_area_t *area_find(const char *label) +{ + unsigned i; + + if (label == NULL) { + return NULL; + } + + for (i = 0; i < g_registry.area_count; i++) { + if (str_ieq(g_registry.areas[i].label, label)) { + return &g_registry.areas[i]; + } + } + + return NULL; +} + +static int level_in_list(hybbx_user_level_t level, + const hybbx_user_level_t *list, + unsigned count) +{ + unsigned i; + + for (i = 0; i < count; i++) { + if (list[i] == level) { + return 1; + } + } + + return 0; +} + +static int command_level_allowed(const hybbx_command_def_t *def, + hybbx_user_level_t level) +{ + if (def == NULL) { + return 0; + } + + if (def->only_level != 0 && level != (hybbx_user_level_t)def->only_level) { + return 0; + } + + if (level > def->min_level) { + return 0; + } + + if (def->max_level != 0 && level < def->max_level) { + return 0; + } + + return 1; +} + +/** Non-zero when @p session may see @p verb in menu/index/help (level or grant). */ +static int command_verb_visible(const hybbx_session_t *session, + hybbx_user_level_t level, + const char *verb) +{ + if (hybbx_commands_registry_verb_allowed(level, verb)) { + return 1; + } + + /* [monitor] allow= grants may see /monitor without Sysop min. */ + if (session != NULL && + (str_ieq(verb, "monitor") || str_ieq(verb, "mon")) && + hybbx_monitor_session_may_use(session)) { + return 1; + } + + return 0; +} + +static int target_right_allowed(const target_right_rule_t *rules, + unsigned rule_count, + hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + unsigned i; + + if (hybbx_user_level_is_sysop(target) || + hybbx_user_level_is_guest(target)) { + return 0; + } + + for (i = 0; i < rule_count; i++) { + if (rules[i].actor == actor && + level_in_list(target, rules[i].targets, rules[i].target_count)) { + return 1; + } + } + + return 0; +} + +static hybbx_result_t parse_areas_yaml(commands_registry_t *reg, const char *path) +{ + FILE *fp; + char buf[HYBBX_CONFIG_LINE_MAX]; + enum { + SEC_NONE, + SEC_META, + SEC_AREAS, + SEC_MENU, + SEC_INDEX + } section = SEC_NONE; + menu_area_t *area = NULL; + menu_subarea_t *subarea = NULL; + char cur_menu_level[16]; + + if (reg == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + cur_menu_level[0] = '\0'; + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + while (fgets(buf, sizeof(buf), fp) != NULL) { + char *line = buf; + char *colon; + unsigned indent; + + indent = line_indent(line); + trim_inplace(line); + if (line[0] == '\0' || line[0] == '#') { + continue; + } + + if (indent == 0 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + area = NULL; + subarea = NULL; + cur_menu_level[0] = '\0'; + + if (str_ieq(line, "meta")) { + section = SEC_META; + } else if (str_ieq(line, "areas")) { + section = SEC_AREAS; + } else if (str_ieq(line, "menu")) { + section = SEC_MENU; + } else if (str_ieq(line, "index")) { + section = SEC_INDEX; + } else { + section = SEC_NONE; + } + continue; + } + + if (section == SEC_META && indent >= 2) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "menu_header")) { + hybbx_strlcpy(reg->menu_header, colon + 1, + sizeof(reg->menu_header)); + } else if (str_ieq(line_key(line), "index_header")) { + hybbx_strlcpy(reg->index_header, colon + 1, + sizeof(reg->index_header)); + } else if (str_ieq(line_key(line), "alias_header")) { + hybbx_strlcpy(reg->alias_header, colon + 1, + sizeof(reg->alias_header)); + } + continue; + } + + if (section == SEC_AREAS) { + if (indent == 2 && line[0] == '-') { + if (reg->area_count >= HYBBX_AREAS_MAX) { + continue; + } + area = ®->areas[reg->area_count++]; + memset(area, 0, sizeof(*area)); + subarea = NULL; + + line++; + trim_inplace(line); + colon = strchr(line, ':'); + if (colon != NULL) { + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "label")) { + hybbx_strlcpy(area->label, colon + 1, sizeof(area->label)); + } else if (str_ieq(line_key(line), "commands")) { + parse_bracket_list(colon + 1, area->verbs, + HYBBX_COMMANDS_VERBS_PER_GROUP, + &area->verb_count); + } + } + continue; + } + + if (area == NULL) { + continue; + } + + if (indent == 4 && line[0] == '-') { + if (area->subarea_count < HYBBX_AREAS_SUB_MAX) { + subarea = &area->subareas[area->subarea_count++]; + memset(subarea, 0, sizeof(*subarea)); + } + continue; + } + + if (indent >= 4) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "label")) { + hybbx_strlcpy(area->label, colon + 1, sizeof(area->label)); + } else if (str_ieq(line_key(line), "commands")) { + parse_bracket_list(colon + 1, area->verbs, + HYBBX_COMMANDS_VERBS_PER_GROUP, + &area->verb_count); + } + continue; + } + + if (indent >= 6 && subarea != NULL) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "commands")) { + parse_bracket_list(colon + 1, subarea->verbs, + HYBBX_COMMANDS_VERBS_PER_GROUP, + &subarea->verb_count); + } + } + continue; + } + + if (section == SEC_MENU) { + if (indent == 2 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + if (reg->menu_level_count < HYBBX_MENU_LEVELS_MAX) { + hybbx_strlcpy(reg->menu_levels[reg->menu_level_count], + line_key(line), + sizeof(reg->menu_levels[0])); + hybbx_strlcpy(cur_menu_level, line_key(line), + sizeof(cur_menu_level)); + reg->menu_level_count++; + } + continue; + } + + if (indent >= 4 && line[0] == '-' && cur_menu_level[0] != '\0') { + unsigned i; + + for (i = 0; i < reg->menu_level_count; i++) { + unsigned n; + + if (!str_ieq(reg->menu_levels[i], cur_menu_level)) { + continue; + } + + n = reg->menu_area_count[i]; + if (n >= HYBBX_MENU_AREAS_PER_LEVEL) { + break; + } + + line++; + trim_inplace(line); + hybbx_strlcpy(reg->menu_area_labels[i][n], line, 16); + reg->menu_area_count[i]++; + break; + } + } + continue; + } + + if (section == SEC_INDEX && indent >= 2 && line[0] == '-') { + if (reg->index_label_count < HYBBX_MENU_AREAS_PER_LEVEL) { + line++; + trim_inplace(line); + hybbx_strlcpy(reg->index_labels[reg->index_label_count], + line, 16); + reg->index_label_count++; + } + } + } + + fclose(fp); + + if (reg->menu_header[0] == '\0' || reg->index_header[0] == '\0' || + reg->area_count == 0) { + return HYBBX_ERR_INVALID; + } + + if (reg->index_label_count == 0) { + unsigned i; + + for (i = 0; i < reg->area_count && + reg->index_label_count < HYBBX_MENU_AREAS_PER_LEVEL; + i++) { + hybbx_strlcpy(reg->index_labels[reg->index_label_count], + reg->areas[i].label, 16); + reg->index_label_count++; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t parse_commands_yaml(commands_registry_t *reg, + const char *path) +{ + FILE *fp; + char buf[HYBBX_CONFIG_LINE_MAX]; + enum { + SEC_NONE, + SEC_RIGHTS, + SEC_ALIASES, + SEC_COMMANDS + } section = SEC_NONE; + enum { + RIGHT_NONE, + RIGHT_USERCHANGE, + RIGHT_USERDELETE, + RIGHT_DELETE, + RIGHT_PROMOTE, + RIGHT_DEMOTE + } right_kind = RIGHT_NONE; + char cur_alias_canonical[HYBBX_CMD_VERB_MAX]; + hybbx_command_def_t *cmd = NULL; + target_right_rule_t *target_rule = NULL; + promote_right_rule_t *promote_rule = NULL; + demote_right_rule_t *demote_rule = NULL; + + if (reg == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + cur_alias_canonical[0] = '\0'; + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + while (fgets(buf, sizeof(buf), fp) != NULL) { + char *line = buf; + char *colon; + unsigned indent; + + indent = line_indent(line); + trim_inplace(line); + if (line[0] == '\0' || line[0] == '#') { + continue; + } + + if (indent == 0 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + cmd = NULL; + target_rule = NULL; + promote_rule = NULL; + demote_rule = NULL; + + if (str_ieq(line, "rights")) { + section = SEC_RIGHTS; + right_kind = RIGHT_NONE; + } else if (str_ieq(line, "aliases")) { + section = SEC_ALIASES; + right_kind = RIGHT_NONE; + } else if (str_ieq(line, "commands")) { + section = SEC_COMMANDS; + right_kind = RIGHT_NONE; + } else { + section = SEC_NONE; + } + continue; + } + + if (section == SEC_RIGHTS) { + if (indent == 2 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + target_rule = NULL; + promote_rule = NULL; + demote_rule = NULL; + + if (str_ieq(line_key(line), "userchange")) { + right_kind = RIGHT_USERCHANGE; + } else if (str_ieq(line_key(line), "userdelete")) { + right_kind = RIGHT_USERDELETE; + } else if (str_ieq(line_key(line), "delete")) { + right_kind = RIGHT_DELETE; + } else if (str_ieq(line_key(line), "promote")) { + right_kind = RIGHT_PROMOTE; + } else if (str_ieq(line_key(line), "demote")) { + right_kind = RIGHT_DEMOTE; + } else { + right_kind = RIGHT_NONE; + } + continue; + } + + if (indent == 4 && line[0] == '-') { + if (right_kind == RIGHT_USERCHANGE && + reg->userchange_rule_count < HYBBX_RIGHTS_TARGET_RULES_MAX) { + target_rule = + ®->userchange_rules[reg->userchange_rule_count++]; + memset(target_rule, 0, sizeof(*target_rule)); + } else if (right_kind == RIGHT_USERDELETE && + reg->userdelete_rule_count < + HYBBX_RIGHTS_TARGET_RULES_MAX) { + target_rule = + ®->userdelete_rules[reg->userdelete_rule_count++]; + memset(target_rule, 0, sizeof(*target_rule)); + } else if (right_kind == RIGHT_DELETE && + reg->delete_rule_count < + HYBBX_RIGHTS_TARGET_RULES_MAX) { + target_rule = + ®->delete_rules[reg->delete_rule_count++]; + memset(target_rule, 0, sizeof(*target_rule)); + } else if (right_kind == RIGHT_PROMOTE && + reg->promote_rule_count < + HYBBX_RIGHTS_PROMOTE_RULES_MAX) { + promote_rule = + ®->promote_rules[reg->promote_rule_count++]; + memset(promote_rule, 0, sizeof(*promote_rule)); + } else if (right_kind == RIGHT_DEMOTE && + reg->demote_rule_count < + HYBBX_RIGHTS_DEMOTE_RULES_MAX) { + demote_rule = + ®->demote_rules[reg->demote_rule_count++]; + memset(demote_rule, 0, sizeof(*demote_rule)); + } + continue; + } + + if (indent >= 6) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + + if (target_rule != NULL) { + if (str_ieq(line_key(line), "actor")) { + target_rule->actor = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "targets")) { + parse_level_list(colon + 1, target_rule->targets, 5, + &target_rule->target_count); + } + } else if (promote_rule != NULL) { + if (str_ieq(line_key(line), "actor")) { + promote_rule->actor = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "to")) { + promote_rule->to_level = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "from")) { + parse_level_list(colon + 1, promote_rule->from_levels, + 4, &promote_rule->from_count); + } + } else if (demote_rule != NULL) { + if (str_ieq(line_key(line), "actor")) { + demote_rule->actor = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "from")) { + parse_level_list(colon + 1, demote_rule->from_levels, + 4, &demote_rule->from_count); + } + } + } + continue; + } + + if (section == SEC_ALIASES) { + if (indent == 2 && line[strlen(line) - 1] == ':') { + alias_entry_t *entry; + + line[strlen(line) - 1] = '\0'; + if (reg->alias_count >= HYBBX_COMMANDS_ALIASES_MAX) { + continue; + } + entry = ®->aliases[reg->alias_count++]; + hybbx_strlcpy(entry->canonical, line_key(line), + sizeof(entry->canonical)); + hybbx_strlcpy(cur_alias_canonical, line_key(line), + sizeof(cur_alias_canonical)); + continue; + } + if (indent >= 4 && line[0] == '-' && cur_alias_canonical[0] != '\0') { + alias_entry_t *entry = ®->aliases[reg->alias_count - 1]; + + if (entry->alias_count < HYBBX_COMMANDS_ALIAS_PER) { + line++; + trim_inplace(line); + strip_quotes(line); + hybbx_strlcpy(entry->aliases[entry->alias_count], line, + HYBBX_CMD_VERB_MAX); + entry->alias_count++; + } + } + continue; + } + + if (section == SEC_COMMANDS) { + if (indent == 2 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + if (reg->command_count < HYBBX_COMMANDS_MAX) { + cmd = ®->commands[reg->command_count++]; + memset(cmd, 0, sizeof(*cmd)); + cmd->min_level = HYBBX_LEVEL_GUEST; + hybbx_strlcpy(cmd->verb, line_key(line), sizeof(cmd->verb)); + } + continue; + } + if (indent >= 4 && cmd != NULL) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "group")) { + hybbx_strlcpy(cmd->group, colon + 1, sizeof(cmd->group)); + } else if (str_ieq(line_key(line), "min")) { + cmd->min_level = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "max")) { + cmd->max_level = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "only")) { + cmd->only_level = (int)level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "line1")) { + hybbx_strlcpy(cmd->line1, colon + 1, sizeof(cmd->line1)); + } else if (str_ieq(line_key(line), "line2")) { + hybbx_strlcpy(cmd->line2, colon + 1, sizeof(cmd->line2)); + } + } + } + } + + fclose(fp); + + if (reg->command_count == 0) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static void build_alias_lines(commands_registry_t *reg) +{ + char line[HYBBX_LINE_MAX]; + unsigned i; + unsigned j; + unsigned off; + + reg->alias_line_count = 0; + + for (i = 0; i < reg->alias_count && reg->alias_line_count < HYBBX_COMMANDS_ALIAS_LINES; + i++) { + const alias_entry_t *entry = ®->aliases[i]; + + off = 0; + line[0] = '\0'; + + for (j = 0; j < entry->alias_count; j++) { + const char *alias = entry->aliases[j]; + int n; + + if (strchr(alias, ' ') != NULL) { + n = snprintf(line + off, sizeof(line) - off, "%s%s", + off > 0 ? " " : "", alias); + } else { + n = snprintf(line + off, sizeof(line) - off, "%s%s -> %s", + off > 0 ? " " : "", alias, entry->canonical); + } + + if (n < 0 || (size_t)n >= sizeof(line) - off) { + if (reg->alias_line_count < HYBBX_COMMANDS_ALIAS_LINES) { + hybbx_strlcpy(reg->alias_lines[reg->alias_line_count++], + line, sizeof(reg->alias_lines[0])); + } + off = 0; + line[0] = '\0'; + j--; + continue; + } + off += (unsigned)n; + } + + if (off > 0 && reg->alias_line_count < HYBBX_COMMANDS_ALIAS_LINES) { + hybbx_strlcpy(reg->alias_lines[reg->alias_line_count++], + line, sizeof(reg->alias_lines[0])); + } + } +} + +static hybbx_result_t load_yaml_file(const char *rel_share, + hybbx_result_t (*parse_fn)(commands_registry_t *, + const char *), + const char *label) +{ + char path[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (hybbx_path_resolve(path, sizeof(path), rel_share) == HYBBX_OK) { + rc = parse_fn(&g_registry, path); + if (rc == HYBBX_OK) { + hybbx_log_info("[commands] loaded %s (%s)", path, label); + return HYBBX_OK; + } + } + + return HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_commands_registry_init(void) +{ + hybbx_result_t rc; + + hybbx_commands_registry_shutdown(); + + rc = load_yaml_file(HYBBX_FILE_AREAS, parse_areas_yaml, "areas"); + if (rc != HYBBX_OK) { + rc = load_yaml_file("share/areas.yaml", parse_areas_yaml, "areas"); + } + if (rc != HYBBX_OK) { + hybbx_log_warn("[commands] failed to load " HYBBX_FILE_AREAS); + return HYBBX_ERR_IO; + } + + rc = load_yaml_file(HYBBX_FILE_COMMANDS, parse_commands_yaml, "commands"); + if (rc != HYBBX_OK) { + rc = load_yaml_file("share/commands.yaml", parse_commands_yaml, "commands"); + } + if (rc != HYBBX_OK) { + hybbx_log_warn("[commands] failed to load " HYBBX_FILE_COMMANDS); + hybbx_commands_registry_shutdown(); + return HYBBX_ERR_IO; + } + + build_alias_lines(&g_registry); + g_registry.loaded = 1; + hybbx_log_info("[commands] registry ready (%u areas, %u verbs)", + g_registry.area_count, g_registry.command_count); + return HYBBX_OK; +} + +void hybbx_commands_registry_shutdown(void) +{ + memset(&g_registry, 0, sizeof(g_registry)); +} + +const hybbx_command_def_t *hybbx_commands_registry_find(const char *verb) +{ + unsigned i; + + if (!g_registry.loaded || verb == NULL) { + return NULL; + } + + for (i = 0; i < g_registry.command_count; i++) { + if (str_ieq(g_registry.commands[i].verb, verb)) { + return &g_registry.commands[i]; + } + } + + return NULL; +} + +const char *hybbx_commands_registry_canonical(const char *topic) +{ + unsigned i; + unsigned j; + + if (!g_registry.loaded || topic == NULL || topic[0] == '\0') { + return topic; + } + + if (hybbx_commands_registry_find(topic) != NULL) { + return topic; + } + + for (i = 0; i < g_registry.alias_count; i++) { + for (j = 0; j < g_registry.aliases[i].alias_count; j++) { + const char *alias = g_registry.aliases[i].aliases[j]; + + if (strchr(alias, ' ') != NULL) { + continue; + } + if (str_ieq(alias, topic)) { + return g_registry.aliases[i].canonical; + } + } + } + + return topic; +} + +int hybbx_commands_registry_verb_allowed(hybbx_user_level_t level, + const char *verb) +{ + const char *canonical; + const hybbx_command_def_t *def; + + if (verb == NULL || verb[0] == '\0') { + return 1; + } + + if (!g_registry.loaded) { + return 0; + } + + canonical = hybbx_commands_registry_canonical(verb); + def = hybbx_commands_registry_find(canonical); + if (def == NULL) { + return 0; + } + + return command_level_allowed(def, level); +} + +int hybbx_commands_registry_help_allowed(hybbx_user_level_t level, + const char *verb) +{ + return hybbx_commands_registry_verb_allowed(level, verb); +} + +int hybbx_commands_registry_may_userchange(hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + if (!g_registry.loaded) { + return 0; + } + + return target_right_allowed(g_registry.userchange_rules, + g_registry.userchange_rule_count, + actor, target); +} + +int hybbx_commands_registry_may_userdelete(hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + unsigned i; + + if (!g_registry.loaded || hybbx_user_level_is_sysop(target)) { + return 0; + } + + for (i = 0; i < g_registry.userdelete_rule_count; i++) { + const target_right_rule_t *rule = &g_registry.userdelete_rules[i]; + + if (rule->actor == actor && + level_in_list(target, rule->targets, rule->target_count)) { + return 1; + } + } + + return 0; +} + +int hybbx_commands_registry_may_delete(hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + if (!g_registry.loaded) { + return 0; + } + + if (target == HYBBX_LEVEL_SYSOP) { + return 0; + } + + return target_right_allowed(g_registry.delete_rules, + g_registry.delete_rule_count, + actor, target); +} + +int hybbx_commands_registry_may_promote(hybbx_user_level_t actor, + hybbx_user_level_t target, + int target_active, + hybbx_user_level_t new_level) +{ + unsigned i; + + if (!g_registry.loaded || !target_active || + hybbx_user_level_is_guest(target) || + target == HYBBX_LEVEL_SYSOP) { + return 0; + } + + for (i = 0; i < g_registry.promote_rule_count; i++) { + const promote_right_rule_t *rule = &g_registry.promote_rules[i]; + + if (rule->actor == actor && rule->to_level == new_level && + level_in_list(target, rule->from_levels, rule->from_count)) { + return 1; + } + } + + return 0; +} + +int hybbx_commands_registry_may_demote(hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + unsigned i; + + if (!g_registry.loaded) { + return 0; + } + + for (i = 0; i < g_registry.demote_rule_count; i++) { + const demote_right_rule_t *rule = &g_registry.demote_rules[i]; + + if (rule->actor == actor && + level_in_list(target, rule->from_levels, rule->from_count)) { + return 1; + } + } + + return 0; +} + +#define MENU_LABEL_PREFIX_LEN 13u +#define MENU_CMD_FIELD_MAX (HYBBX_LINE_MAX - MENU_LABEL_PREFIX_LEN) + +static void emit_menu_line(hybbx_session_t *session, const char *label, + const char *cmds, int continuation) +{ + char buf[HYBBX_LINE_MAX]; + + if (continuation) { + snprintf(buf, sizeof(buf), " %s", cmds); + } else { + snprintf(buf, sizeof(buf), " %-10s %s", label, cmds); + } + hybbx_session_write_line(session, buf); +} + +static void format_verbs_line(char *out, size_t out_len, + const char verbs[][HYBBX_CMD_VERB_MAX], + unsigned count) +{ + size_t off = 0; + unsigned i; + + out[0] = '\0'; + for (i = 0; i < count; i++) { + int n; + + if (verbs[i][0] == '\0') { + continue; + } + n = snprintf(out + off, out_len - off, "%s/%s", + off > 0 ? " " : "", verbs[i]); + if (n < 0 || (size_t)n >= out_len - off) { + break; + } + off += (size_t)n; + } +} + +static void emit_verbs_wrapped(hybbx_session_t *session, const char *label, + const char verbs[][HYBBX_CMD_VERB_MAX], + unsigned count) +{ + char chunk[MENU_CMD_FIELD_MAX + 1]; + size_t off = 0; + unsigned physical_line = 0; + unsigned i; + + for (i = 0; i < count; i++) { + char token[HYBBX_CMD_VERB_MAX + 4]; + int n; + + if (verbs[i][0] == '\0') { + continue; + } + + n = snprintf(token, sizeof(token), "%s/%s", + off > 0 ? " " : "", verbs[i]); + if (n < 0) { + continue; + } + + if (off > 0 && (size_t)n + off > MENU_CMD_FIELD_MAX) { + chunk[off] = '\0'; + emit_menu_line(session, physical_line == 0 ? label : "", chunk, + physical_line > 0); + physical_line++; + off = 0; + n = snprintf(token, sizeof(token), "/%s", verbs[i]); + if (n < 0) { + continue; + } + } + + if ((size_t)n >= sizeof(chunk) - off) { + break; + } + + memcpy(chunk + off, token, (size_t)n); + off += (size_t)n; + } + + if (off > 0) { + chunk[off] = '\0'; + emit_menu_line(session, physical_line == 0 ? label : "", chunk, + physical_line > 0); + } +} + +static unsigned collect_area_verbs(const menu_area_t *area, + const hybbx_session_t *session, + hybbx_user_level_t level, + int filter_access, + char out[][HYBBX_CMD_VERB_MAX], + unsigned max) +{ + unsigned count = 0; + unsigned i; + unsigned j; + + if (area == NULL) { + return 0; + } + + for (i = 0; i < area->verb_count && count < max; i++) { + if (filter_access && + !command_verb_visible(session, level, area->verbs[i])) { + continue; + } + hybbx_strlcpy(out[count], area->verbs[i], HYBBX_CMD_VERB_MAX); + count++; + } + + for (i = 0; i < area->subarea_count; i++) { + const menu_subarea_t *sub = &area->subareas[i]; + + for (j = 0; j < sub->verb_count && count < max; j++) { + if (filter_access && + !command_verb_visible(session, level, sub->verbs[j])) { + continue; + } + hybbx_strlcpy(out[count], sub->verbs[j], HYBBX_CMD_VERB_MAX); + count++; + } + } + + return count; +} + +static void render_area(hybbx_session_t *session, const menu_area_t *area, + hybbx_user_level_t level, int filter_access) +{ + char cmds[HYBBX_LINE_MAX]; + char verbs[HYBBX_COMMANDS_VERBS_PER_GROUP][HYBBX_CMD_VERB_MAX]; + unsigned count; + unsigned i; + + if (area == NULL) { + return; + } + + count = collect_area_verbs(area, session, level, filter_access, verbs, + HYBBX_COMMANDS_VERBS_PER_GROUP); + if (count == 0) { + return; + } + + emit_verbs_wrapped(session, area->label, verbs, count); + + if (!filter_access) { + for (i = 0; i < area->subarea_count; i++) { + const menu_subarea_t *sub = &area->subareas[i]; + char sub_verbs[HYBBX_COMMANDS_VERBS_PER_GROUP][HYBBX_CMD_VERB_MAX]; + unsigned sub_count = 0; + unsigned j; + + for (j = 0; j < sub->verb_count; j++) { + hybbx_strlcpy(sub_verbs[sub_count], sub->verbs[j], + HYBBX_CMD_VERB_MAX); + sub_count++; + } + + if (sub_count == 0) { + continue; + } + + format_verbs_line(cmds, sizeof(cmds), + (const char (*)[HYBBX_CMD_VERB_MAX])sub_verbs, + sub_count); + emit_menu_line(session, "", cmds, 1); + } + } +} + +static void render_area_labels(hybbx_session_t *session, + const char labels[][16], + unsigned label_count, + hybbx_user_level_t level, + int filter_access) +{ + unsigned i; + + for (i = 0; i < label_count; i++) { + const menu_area_t *area = area_find(labels[i]); + + if (area != NULL) { + render_area(session, area, level, filter_access); + } + } +} + +static unsigned menu_layout_index(hybbx_user_level_t level) +{ + const char *level_name = menu_level_name(level); + unsigned i; + + for (i = 0; i < g_registry.menu_level_count; i++) { + if (str_ieq(g_registry.menu_levels[i], level_name)) { + return i; + } + } + + return g_registry.menu_level_count; +} + +void hybbx_commands_registry_show_menu(hybbx_session_t *session) +{ + hybbx_user_level_t level; + unsigned layout; + + if (session == NULL || !g_registry.loaded) { + return; + } + + level = hybbx_session_user_level(session); + layout = menu_layout_index(level); + + hybbx_session_write_line(session, g_registry.menu_header); + if (layout < g_registry.menu_level_count) { + render_area_labels(session, + (const char (*)[16]) + g_registry.menu_area_labels[layout], + g_registry.menu_area_count[layout], + level, 1); + } + + /* + * Allow-list grants are not Sysop — their menu layout has no Sysop area. + * Append filtered Sysop area so /monitor is visible when granted. + */ + if (hybbx_monitor_session_may_use(session) && + !hybbx_user_level_is_sysop(level)) { + const menu_area_t *sysop = area_find("Sysop"); + + if (sysop != NULL) { + render_area(session, sysop, level, 1); + } + } +} + +void hybbx_commands_registry_show_index(hybbx_session_t *session) +{ + hybbx_user_level_t level; + + if (session == NULL || !g_registry.loaded) { + return; + } + + level = hybbx_session_user_level(session); + hybbx_session_write_line(session, g_registry.index_header); + /* Filter by access — Sysop cmds (incl. /monitor) only for Sysop + grants. */ + render_area_labels(session, + (const char (*)[16])g_registry.index_labels, + g_registry.index_label_count, + level, 1); +} + +void hybbx_commands_registry_show_aliases(hybbx_session_t *session) +{ + hybbx_user_level_t level; + unsigned i; + + if (session == NULL || !g_registry.loaded) { + return; + } + + level = hybbx_session_user_level(session); + hybbx_session_write_line(session, g_registry.alias_header); + for (i = 0; i < g_registry.alias_count; i++) { + const alias_entry_t *entry = &g_registry.aliases[i]; + char line[HYBBX_LINE_MAX]; + unsigned j; + unsigned off = 0; + + if (!command_verb_visible(session, level, entry->canonical)) { + continue; + } + + line[0] = '\0'; + for (j = 0; j < entry->alias_count; j++) { + const char *alias = entry->aliases[j]; + int n; + + if (strchr(alias, ' ') != NULL) { + n = snprintf(line + off, sizeof(line) - off, "%s%s", + off > 0 ? " " : "", alias); + } else { + n = snprintf(line + off, sizeof(line) - off, "%s%s -> %s", + off > 0 ? " " : "", alias, entry->canonical); + } + if (n < 0 || (size_t)n >= sizeof(line) - off) { + break; + } + off += (unsigned)n; + } + if (off > 0) { + hybbx_session_write_line(session, line); + } + } +} + +void hybbx_commands_registry_show_help(hybbx_session_t *session, + const char *canonical) +{ + const hybbx_command_def_t *def; + + if (session == NULL || canonical == NULL || !g_registry.loaded) { + return; + } + + def = hybbx_commands_registry_find(canonical); + if (def == NULL || def->line1[0] == '\0') { + return; + } + + hybbx_session_write_line(session, def->line1); + if (def->line2[0] != '\0') { + hybbx_session_write_line(session, def->line2); + } +} + +void hybbx_commands_registry_show_sysop_cmds(hybbx_session_t *session) +{ + unsigned i; + + if (session == NULL || !g_registry.loaded) { + return; + } + + for (i = 0; i < g_registry.command_count; i++) { + const hybbx_command_def_t *def = &g_registry.commands[i]; + + /* Sysop=1, Admin=2 — only the admin/sysop privilege set. */ + if (def->min_level == 0 || def->min_level > HYBBX_LEVEL_ADMIN) { + continue; + } + if (def->line1[0] == '\0') { + continue; + } + hybbx_session_write_line(session, def->line1); + } +} diff --git a/src/core/conference.c b/src/core/conference.c new file mode 100644 index 0000000..890348f --- /dev/null +++ b/src/core/conference.c @@ -0,0 +1,720 @@ +#include "hybbx/conference.h" +#include "hybbx/chat.h" +#include "hybbx/messages.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/monitor.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <string.h> +#include <time.h> + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int invite_reply_yes(const char *line) +{ + return line != NULL && + (str_ieq(line, "y") || str_ieq(line, "yes")); +} + +static int invite_reply_no(const char *line) +{ + return line != NULL && + (str_ieq(line, "n") || str_ieq(line, "no")); +} + +static int topic_char_ok(char ch) +{ + return (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == ' ' || ch == '-' || ch == '_'; +} + +static int conference_topic_valid(const char *topic) +{ + size_t len; + size_t i; + + if (topic == NULL) { + return 0; + } + + len = strlen(topic); + if (len < 2 || len >= HYBBX_CONFERENCE_TOPIC_MAX) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!topic_char_ok(topic[i])) { + return 0; + } + } + + return 1; +} + +typedef struct find_user_session_ctx { + const char *username; + hybbx_session_t *found; +} find_user_session_ctx_t; + +static void find_user_session_visitor(hybbx_session_t *session, void *userdata) +{ + find_user_session_ctx_t *ctx = (find_user_session_ctx_t *)userdata; + + if (ctx == NULL || session == NULL || ctx->found != NULL) { + return; + } + + if (!hybbx_session_logged_in(session) || hybbx_session_is_guest(session)) { + return; + } + + if (str_ieq(hybbx_session_username(session), ctx->username)) { + ctx->found = session; + } +} + +static hybbx_session_t *conference_find_online_user(hybbx_service_t *service, + const char *username) +{ + find_user_session_ctx_t ctx; + + if (service == NULL || username == NULL || username[0] == '\0') { + return NULL; + } + + ctx.username = username; + ctx.found = NULL; + hybbx_service_visit_sessions(service, find_user_session_visitor, &ctx); + return ctx.found; +} + +static int conference_session_active(const hybbx_session_t *session) +{ + return session != NULL && + hybbx_session_area(session) == HYBBX_AREA_CONFERENCE; +} + +typedef struct invite_cancel_ctx { + const char *from_username; +} invite_cancel_ctx_t; + +static void invite_cancel_visitor(hybbx_session_t *session, void *userdata) +{ + invite_cancel_ctx_t *ctx = (invite_cancel_ctx_t *)userdata; + + if (session == NULL || ctx == NULL || ctx->from_username == NULL) { + return; + } + + if (!hybbx_conference_invite_pending(session)) { + return; + } + + if (!str_ieq(hybbx_session_conference_invite_from(session), + ctx->from_username)) { + return; + } + + hybbx_session_clear_conference_invite(session); + hybbx_session_write_line(session, "*** Conference invite cancelled."); +} + +static void conference_cancel_invites_from(hybbx_service_t *service, + const char *from_username) +{ + invite_cancel_ctx_t ctx; + + if (service == NULL || from_username == NULL || from_username[0] == '\0') { + return; + } + + ctx.from_username = from_username; + hybbx_service_visit_sessions(service, invite_cancel_visitor, &ctx); +} + +static void conference_notify_partner_left(hybbx_session_t *session, + const char *partner_username) +{ + hybbx_service_t *service; + hybbx_session_t *partner; + char line[HYBBX_LINE_MAX]; + + if (session == NULL || partner_username == NULL || + partner_username[0] == '\0') { + return; + } + + service = hybbx_session_service(session); + if (service == NULL) { + return; + } + + partner = conference_find_online_user(service, partner_username); + if (partner == NULL || !conference_session_active(partner)) { + return; + } + + if (!str_ieq(hybbx_session_conference_partner(partner), + hybbx_session_username(session))) { + return; + } + + hybbx_session_clear_conference(partner); + snprintf(line, sizeof(line), "*** %s left the conference.", + hybbx_session_display_name(session)); + hybbx_session_write_line(partner, line); + if (hybbx_session_area(partner) == HYBBX_AREA_CONFERENCE) { + (void)hybbx_session_leave_area(partner); + } +} + +static hybbx_result_t conference_activate(hybbx_service_t *service, + hybbx_session_t *initiator, + hybbx_session_t *partner, + const char *topic) +{ + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + + if (service == NULL || initiator == NULL || partner == NULL || + topic == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_session_join_conference(initiator, topic, + hybbx_session_username(partner)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = hybbx_session_join_conference(partner, topic, + hybbx_session_username(initiator)); + if (rc != HYBBX_OK) { + hybbx_session_clear_conference(initiator); + (void)hybbx_session_leave_area(initiator); + return rc; + } + + snprintf(line, sizeof(line), "Conference: %s", topic); + hybbx_session_write_line(initiator, line); + hybbx_session_write_line(initiator, + "Each line is a message; /leave or /main to exit."); + + snprintf(line, sizeof(line), "Conference: %s", topic); + hybbx_session_write_line(partner, line); + hybbx_session_write_line(partner, + "Each line is a message; /leave or /main to exit."); + + return HYBBX_OK; +} + +hybbx_result_t hybbx_conference_start(hybbx_service_t *service, + hybbx_session_t *initiator, + const char *topic, + const char *partner_spec) +{ + hybbx_storage_t *storage; + hybbx_user_record_t partner_user; + hybbx_session_t *partner_session; + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + + if (service == NULL || initiator == NULL || topic == NULL || + partner_spec == NULL || topic[0] == '\0' || partner_spec[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (hybbx_session_is_guest(initiator)) { + hybbx_session_write_line(initiator, "Guests cannot use conference."); + return HYBBX_ERR_DENIED; + } + + if (!conference_topic_valid(topic)) { + hybbx_session_write_line(initiator, "Invalid conference topic."); + return HYBBX_ERR_INVALID; + } + + if (conference_session_active(initiator)) { + hybbx_session_write_line(initiator, "Already in a conference."); + return HYBBX_OK; + } + + if (hybbx_conference_invite_pending(initiator)) { + hybbx_session_write_line(initiator, + "Answer the pending conference invite first (y/n)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_resolve_user(storage, partner_spec, &partner_user); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(initiator, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (!partner_user.active) { + hybbx_session_write_line(initiator, "User is not active."); + return HYBBX_OK; + } + + if (hybbx_user_level_is_guest(partner_user.level)) { + hybbx_session_write_line(initiator, "Cannot conference with a guest."); + return HYBBX_OK; + } + + if (str_ieq(partner_user.username, hybbx_session_username(initiator))) { + hybbx_session_write_line(initiator, "Cannot conference with yourself."); + return HYBBX_OK; + } + + if (!hybbx_session_conference_may_invite(initiator, partner_user.username)) { + hybbx_session_write_line(initiator, + "Invite limit: 2 per user per 30 minutes."); + return HYBBX_OK; + } + + partner_session = conference_find_online_user(service, partner_user.username); + if (partner_session == NULL) { + hybbx_session_write_line(initiator, "User is not online."); + return HYBBX_OK; + } + + if (conference_session_active(partner_session)) { + hybbx_session_write_line(initiator, "User is already in a conference."); + return HYBBX_OK; + } + + if (hybbx_conference_invite_pending(partner_session)) { + hybbx_session_write_line(initiator, + "User has a pending conference invite."); + return HYBBX_OK; + } + + hybbx_session_set_conference_invite(partner_session, + hybbx_session_username(initiator), + topic); + hybbx_session_conference_invite_sent(initiator, partner_user.username); + + snprintf(line, sizeof(line), "Conference invite: %s", topic); + (void)hybbx_msg_send_private(partner_session, + hybbx_session_display_name(initiator), line); + hybbx_session_write_line(partner_session, "Accept? y/n or yes/no"); + + snprintf(line, sizeof(line), "Conference invite sent to %s.", + hybbx_user_display_name(&partner_user)); + hybbx_session_write_line(initiator, line); + + return HYBBX_OK; +} + +hybbx_result_t hybbx_conference_reply_invite(hybbx_service_t *service, + hybbx_session_t *session, + const char *line) +{ + char from_username[HYBBX_USER_NAME_MAX]; + char topic[HYBBX_CONFERENCE_TOPIC_MAX]; + hybbx_session_t *initiator; + char msg[HYBBX_LINE_MAX]; + + if (service == NULL || session == NULL || line == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_conference_invite_pending(session)) { + return HYBBX_ERR_INVALID; + } + + if (!invite_reply_yes(line) && !invite_reply_no(line)) { + return HYBBX_ERR_NOT_FOUND; + } + + { + const char *from_ptr = hybbx_session_conference_invite_from(session); + const char *topic_ptr = hybbx_session_conference_invite_topic(session); + + if (from_ptr == NULL || from_ptr[0] == '\0' || + topic_ptr == NULL || topic_ptr[0] == '\0') { + hybbx_session_clear_conference_invite(session); + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(from_username, from_ptr, sizeof(from_username)); + hybbx_strlcpy(topic, topic_ptr, sizeof(topic)); + } + + hybbx_session_clear_conference_invite(session); + + if (invite_reply_no(line)) { + initiator = conference_find_online_user(service, from_username); + if (initiator != NULL) { + snprintf(msg, sizeof(msg), + "meet status=declined user=%s", + hybbx_session_display_name(session)); + (void)hybbx_msg_send_system(initiator, msg); + } + hybbx_session_write_line(session, "*** Conference invite declined."); + return HYBBX_OK; + } + + if (conference_session_active(session)) { + hybbx_session_write_line(session, "Already in a conference."); + return HYBBX_OK; + } + + initiator = conference_find_online_user(service, from_username); + if (initiator == NULL) { + hybbx_session_write_line(session, "Inviter is no longer online."); + return HYBBX_OK; + } + + if (conference_session_active(initiator)) { + hybbx_session_write_line(session, "Inviter is already in a conference."); + return HYBBX_OK; + } + + if (conference_activate(service, initiator, session, topic) != HYBBX_OK) { + hybbx_session_write_line(session, "Conference could not be started."); + return HYBBX_OK; + } + + snprintf(msg, sizeof(msg), "meet status=accepted user=%s", + hybbx_session_display_name(session)); + hybbx_session_write_line(initiator, msg); + if (hybbx_monitor_is_active(initiator)) { + hybbx_monitor_broadcast(service, msg); + } + + return HYBBX_OK; +} + +void hybbx_conference_invite_tick(hybbx_service_t *service, + hybbx_session_t *session) +{ + time_t deadline; + time_t now; + char from_username[HYBBX_USER_NAME_MAX]; + const char *from_ptr; + hybbx_session_t *initiator; + char msg[HYBBX_LINE_MAX]; + + if (service == NULL || session == NULL) { + return; + } + + if (!hybbx_conference_invite_pending(session)) { + return; + } + + deadline = hybbx_session_conference_invite_deadline(session); + if (deadline == 0) { + return; + } + + now = time(NULL); + if (now < deadline) { + return; + } + + from_ptr = hybbx_session_conference_invite_from(session); + if (from_ptr != NULL) { + hybbx_strlcpy(from_username, from_ptr, sizeof(from_username)); + } else { + from_username[0] = '\0'; + } + + hybbx_session_clear_conference_invite(session); + hybbx_session_write_line(session, "*** Conference invite timed out."); + + if (from_username[0] != '\0') { + initiator = conference_find_online_user(service, from_username); + if (initiator != NULL) { + snprintf(msg, sizeof(msg), "meet status=timeout user=%s", + hybbx_session_display_name(session)); + hybbx_session_write_line(initiator, msg); + if (hybbx_monitor_is_active(initiator)) { + hybbx_monitor_broadcast(service, msg); + } + } + } +} + +hybbx_result_t hybbx_conference_monitor_meet(hybbx_service_t *service, + hybbx_session_t *initiator, + const char *partner_spec, + int force) +{ + const hybbx_monitor_config_t *mcfg; + hybbx_storage_t *storage; + hybbx_user_record_t partner_user; + hybbx_session_t *partner_session; + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + const char *topic = "monitor"; + unsigned timeout_sec = 20u; + + if (service == NULL || initiator == NULL || partner_spec == NULL || + partner_spec[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + mcfg = hybbx_monitor_config_get(); + if (mcfg != NULL) { + timeout_sec = mcfg->invite_timeout_sec; + } + + if (hybbx_session_is_guest(initiator)) { + hybbx_session_write_line(initiator, "Guests cannot use meet."); + return HYBBX_ERR_DENIED; + } + + if (conference_session_active(initiator)) { + hybbx_session_write_line(initiator, "Already in a conference."); + return HYBBX_OK; + } + + if (hybbx_conference_invite_pending(initiator)) { + hybbx_session_write_line(initiator, + "Answer the pending conference invite first (y/n)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_resolve_user(storage, partner_spec, &partner_user); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(initiator, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (!partner_user.active) { + hybbx_session_write_line(initiator, "User is not active."); + return HYBBX_OK; + } + + if (hybbx_user_level_is_guest(partner_user.level)) { + hybbx_session_write_line(initiator, "Cannot conference with a guest."); + return HYBBX_OK; + } + + if (str_ieq(partner_user.username, hybbx_session_username(initiator))) { + hybbx_session_write_line(initiator, "Cannot conference with yourself."); + return HYBBX_OK; + } + + partner_session = conference_find_online_user(service, partner_user.username); + if (partner_session == NULL) { + hybbx_session_write_line(initiator, "User is not online."); + return HYBBX_OK; + } + + if (conference_session_active(partner_session)) { + hybbx_session_write_line(initiator, "User is already in a conference."); + return HYBBX_OK; + } + + if (force) { + if (conference_activate(service, initiator, partner_session, + topic) != HYBBX_OK) { + hybbx_session_write_line(initiator, + "Conference could not be started."); + return HYBBX_OK; + } + snprintf(line, sizeof(line), "meet status=forced user=%s", + hybbx_user_display_name(&partner_user)); + hybbx_session_write_line(initiator, line); + hybbx_monitor_broadcast(service, line); + hybbx_session_write_line(partner_session, + "You were placed into a conference by Sysop."); + return HYBBX_OK; + } + + if (hybbx_conference_invite_pending(partner_session)) { + hybbx_session_write_line(initiator, + "User has a pending conference invite."); + return HYBBX_OK; + } + + hybbx_session_set_conference_invite(partner_session, + hybbx_session_username(initiator), + topic); + hybbx_session_set_conference_invite_deadline( + partner_session, time(NULL) + (time_t)timeout_sec); + + snprintf(line, sizeof(line), + "*** Conference invite from %s: %s (reply in %u s)", + hybbx_session_display_name(initiator), topic, timeout_sec); + hybbx_session_write_line(partner_session, line); + hybbx_session_write_line(partner_session, "*** Accept? y/n or yes/no"); + + snprintf(line, sizeof(line), + "meet status=pending user=%s timeout=%us", + hybbx_user_display_name(&partner_user), timeout_sec); + hybbx_session_write_line(initiator, line); + hybbx_monitor_broadcast(service, line); + + return HYBBX_OK; +} + +typedef struct conference_post_ctx { + hybbx_session_t *from; + const char *message; + const char *from_user; + const char *from_login; +} conference_post_ctx_t; + +static void conference_post_visitor(hybbx_session_t *session, void *userdata) +{ + conference_post_ctx_t *ctx = (conference_post_ctx_t *)userdata; + char line[HYBBX_USER_NAME_MAX + HYBBX_LINE_MAX + 16]; + const char *session_login; + + if (session == NULL || ctx == NULL || ctx->message == NULL || + ctx->from == NULL || ctx->from_login == NULL) { + return; + } + + if (!conference_session_active(session)) { + return; + } + + if (session == ctx->from) { + snprintf(line, sizeof(line), "ME: %s", ctx->message); + hybbx_session_write_line(session, line); + return; + } + + session_login = hybbx_session_username(session); + if (session_login == NULL || + !str_ieq(hybbx_session_conference_partner(session), ctx->from_login) || + !str_ieq(hybbx_session_conference_partner(ctx->from), session_login)) { + return; + } + + snprintf(line, sizeof(line), "%s: %s", ctx->from_user, ctx->message); + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_conference_post(hybbx_service_t *service, + hybbx_session_t *from, + const char *message) +{ + conference_post_ctx_t ctx; + const hybbx_chat_config_t *chat; + + if (service == NULL || from == NULL || message == NULL) { + return HYBBX_ERR_INVALID; + } + + if (message[0] == '\0') { + return HYBBX_OK; + } + + if (!conference_session_active(from)) { + return HYBBX_ERR_INVALID; + } + + chat = hybbx_service_get_chat(service); + if (chat == NULL) { + return HYBBX_ERR_INVALID; + } + + if (strlen(message) > chat->message_max) { + return HYBBX_ERR_INVALID; + } + + ctx.from = from; + ctx.message = message; + ctx.from_user = hybbx_session_display_name(from); + ctx.from_login = hybbx_session_username(from); + if (ctx.from_login == NULL || ctx.from_login[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + hybbx_service_visit_sessions(service, conference_post_visitor, &ctx); + return HYBBX_OK; +} + +void hybbx_conference_area_leaving(hybbx_session_t *session) +{ + const char *partner_name; + + if (session == NULL) { + return; + } + + partner_name = hybbx_session_conference_partner(session); + hybbx_session_clear_conference(session); + + if (partner_name != NULL && partner_name[0] != '\0') { + conference_notify_partner_left(session, partner_name); + } +} + +void hybbx_conference_session_closed(hybbx_session_t *session) +{ + hybbx_service_t *service; + const char *username; + + if (session == NULL) { + return; + } + + if (hybbx_conference_invite_pending(session)) { + hybbx_session_clear_conference_invite(session); + } + + if (!conference_session_active(session)) { + service = hybbx_session_service(session); + username = hybbx_session_username(session); + if (service != NULL && username != NULL && username[0] != '\0') { + conference_cancel_invites_from(service, username); + } + return; + } + + hybbx_conference_area_leaving(session); + + service = hybbx_session_service(session); + username = hybbx_session_username(session); + if (service != NULL && username != NULL && username[0] != '\0') { + conference_cancel_invites_from(service, username); + } +} diff --git a/src/core/config.c b/src/core/config.c new file mode 100644 index 0000000..9b84d97 --- /dev/null +++ b/src/core/config.c @@ -0,0 +1,862 @@ +#include "hybbx/config.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" + +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define HYBBX_CONFIG_INITIAL_CAP 32 + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static char *trim_inplace(char *s) +{ + char *end; + + if (s == NULL) { + return NULL; + } + + while (*s != '\0' && isspace((unsigned char)*s)) { + s++; + } + + if (*s == '\0') { + return s; + } + + end = s + strlen(s) - 1; + while (end > s && isspace((unsigned char)*end)) { + *end = '\0'; + end--; + } + + return s; +} + +static void strip_inline_comment_inplace(char *value) +{ + size_t i; + int in_single = 0; + int in_double = 0; + + if (value == NULL) { + return; + } + + for (i = 0; value[i] != '\0'; i++) { + char c = value[i]; + + if (!in_single && !in_double && (c == ';' || c == '#')) { + value[i] = '\0'; + break; + } + + if (!in_double && c == '\'') { + in_single = !in_single; + } else if (!in_single && c == '"') { + in_double = !in_double; + } + } + + { + char *end = value + strlen(value); + + while (end > value && isspace((unsigned char)*(end - 1))) { + *--end = '\0'; + } + } +} + +static int is_comment_line(const char *line) +{ + const char *p = line; + + while (*p != '\0' && isspace((unsigned char)*p)) { + p++; + } + + return *p == ';' || *p == '#'; +} + +static hybbx_result_t append_entry(hybbx_config_t *config, + const char *section, + const char *key, + const char *value) +{ + hybbx_config_entry_t *entry; + hybbx_config_entry_t *grown; + + if (section == NULL || key == NULL || value == NULL) { + return HYBBX_ERR_INVALID; + } + + if (strlen(section) >= HYBBX_CONFIG_SECTION_MAX || + strlen(key) >= HYBBX_CONFIG_KEY_MAX || + strlen(value) >= HYBBX_CONFIG_VALUE_MAX) { + return HYBBX_ERR_INVALID; + } + + if (config->count == 0) { + config->entries = malloc(HYBBX_CONFIG_INITIAL_CAP * sizeof(*config->entries)); + if (config->entries == NULL) { + return HYBBX_ERR_NOMEM; + } + } else if ((config->count % HYBBX_CONFIG_INITIAL_CAP) == 0) { + grown = realloc(config->entries, + (config->count + HYBBX_CONFIG_INITIAL_CAP) * + sizeof(*config->entries)); + if (grown == NULL) { + return HYBBX_ERR_NOMEM; + } + config->entries = grown; + } + + entry = &config->entries[config->count]; + entry->section = hybbx_strdup(section); + entry->key = hybbx_strdup(key); + entry->value = hybbx_strdup(value); + + if (entry->section == NULL || entry->key == NULL || entry->value == NULL) { + free(entry->section); + free(entry->key); + free(entry->value); + return HYBBX_ERR_NOMEM; + } + + config->count++; + return HYBBX_OK; +} + +hybbx_result_t hybbx_config_load(hybbx_config_t *config, const char *path) +{ + FILE *fp; + char line[HYBBX_CONFIG_LINE_MAX]; + char current_section[HYBBX_CONFIG_SECTION_MAX] = ""; + char *eq; + char *key; + char *value; + + if (config == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(config, 0, sizeof(*config)); + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char *content; + size_t len; + + len = strlen(line); + if (len > 0 && line[len - 1] != '\n' && + len >= sizeof(line) - 1) { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_INVALID; + } + + content = trim_inplace(line); + + if (content[0] == '\0' || is_comment_line(content)) { + continue; + } + + if (content[0] == '[') { + char *closing = strchr(content, ']'); + + if (closing == NULL) { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_INVALID; + } + + *closing = '\0'; + strncpy(current_section, content + 1, sizeof(current_section) - 1); + current_section[sizeof(current_section) - 1] = '\0'; + trim_inplace(current_section); + continue; + } + + if (current_section[0] == '\0') { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_INVALID; + } + + eq = strchr(content, '='); + if (eq == NULL) { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_INVALID; + } + + *eq = '\0'; + key = trim_inplace(content); + value = trim_inplace(eq + 1); + strip_inline_comment_inplace(value); + + len = strlen(value); + if (len >= 2 && + ((value[0] == '"' && value[len - 1] == '"') || + (value[0] == '\'' && value[len - 1] == '\''))) { + value[len - 1] = '\0'; + value++; + } + + if (append_entry(config, current_section, key, value) != HYBBX_OK) { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_NOMEM; + } + } + + fclose(fp); + return HYBBX_OK; +} + +void hybbx_config_free(hybbx_config_t *config) +{ + size_t i; + + if (config == NULL) { + return; + } + + for (i = 0; i < config->count; i++) { + free(config->entries[i].section); + free(config->entries[i].key); + free(config->entries[i].value); + } + + free(config->entries); + config->entries = NULL; + config->count = 0; +} + +const char *hybbx_config_get(const hybbx_config_t *config, + const char *section, + const char *key, + const char *default_value) +{ + size_t i; + + if (config == NULL || section == NULL || key == NULL) { + return default_value; + } + + for (i = 0; i < config->count; i++) { + if (strcmp(config->entries[i].section, section) == 0 && + strcmp(config->entries[i].key, key) == 0) { + return config->entries[i].value; + } + } + + return default_value; +} + +int hybbx_config_get_bool(const hybbx_config_t *config, + const char *section, + const char *key, + int default_value) +{ + const char *value = hybbx_config_get(config, section, key, NULL); + + if (value == NULL) { + return default_value; + } + + return hybbx_parse_bool(value, default_value); +} + +unsigned hybbx_config_get_uint(const hybbx_config_t *config, + const char *section, + const char *key, + unsigned default_value, + unsigned min_value, + unsigned max_value) +{ + const char *value; + char *end; + unsigned long parsed; + + value = hybbx_config_get(config, section, key, NULL); + if (value == NULL || value[0] == '\0') { + return default_value; + } + + parsed = strtoul(value, &end, 10); + if (end == value || *end != '\0') { + return default_value; + } + + if (parsed < min_value) { + return min_value; + } + if (parsed > max_value) { + return max_value; + } + + return (unsigned)parsed; +} + +static int config_section_has_keys(const hybbx_config_t *config, + const char *section) +{ + size_t i; + + if (config == NULL || section == NULL) { + return 0; + } + + for (i = 0; i < config->count; i++) { + const hybbx_config_entry_t *entry = &config->entries[i]; + + if (strcmp(entry->section, section) != 0) { + continue; + } + if (strcmp(entry->key, "enabled") == 0) { + continue; + } + return 1; + } + + return 0; +} + +static int config_suffix_is_digits(const char *suffix) +{ + if (suffix == NULL || suffix[0] == '\0') { + return 0; + } + + while (*suffix != '\0') { + if (!isdigit((unsigned char)*suffix)) { + return 0; + } + suffix++; + } + + return 1; +} + +int hybbx_config_resolve_transport_section(const hybbx_config_t *config, + const char *plugin_name, + char *out_section, + size_t out_size) +{ + char prefix[128]; + size_t prefix_len; + size_t i; + + if (config == NULL || plugin_name == NULL || out_section == NULL || + out_size == 0) { + return 0; + } + + snprintf(prefix, sizeof(prefix), "transport.%s", plugin_name); + if (config_section_has_keys(config, prefix)) { + hybbx_strlcpy(out_section, prefix, out_size); + return 1; + } + + prefix_len = strlen(prefix); + for (i = 0; i < config->count; i++) { + const char *sec = config->entries[i].section; + const char *suffix; + + if (sec == NULL || strncmp(sec, prefix, prefix_len) != 0) { + continue; + } + + suffix = sec + prefix_len; + if (!config_suffix_is_digits(suffix)) { + continue; + } + if (!config_section_has_keys(config, sec)) { + continue; + } + + hybbx_strlcpy(out_section, sec, out_size); + return 1; + } + + hybbx_strlcpy(out_section, prefix, out_size); + return 0; +} + +char *hybbx_config_format_section(const hybbx_config_t *config, + const char *section) +{ + size_t i; + size_t cap = 64; + size_t len = 0; + char *out; + int first = 1; + + if (config == NULL || section == NULL) { + return NULL; + } + + out = malloc(cap); + if (out == NULL) { + return NULL; + } + out[0] = '\0'; + + for (i = 0; i < config->count; i++) { + const hybbx_config_entry_t *entry = &config->entries[i]; + size_t need; + + if (strcmp(entry->section, section) != 0) { + continue; + } + + if (strcmp(entry->key, "enabled") == 0) { + continue; + } + + need = strlen(entry->key) + strlen(entry->value) + 2; + if (!first) { + need++; + } + + if (len + need + 1 > cap) { + char *grown; + + while (len + need + 1 > cap) { + cap *= 2; + } + grown = realloc(out, cap); + if (grown == NULL) { + free(out); + return NULL; + } + out = grown; + } + + if (!first) { + out[len++] = ';'; + out[len] = '\0'; + } + + len += (size_t)snprintf(out + len, cap - len, "%s=%s", + entry->key, entry->value); + first = 0; + } + + return out; +} + +typedef struct format_transport_ctx { + const hybbx_config_t *config; + const char *prefix; + size_t prefix_len; + char **sections; + size_t count; + size_t cap; +} format_transport_ctx_t; + +static int format_transport_section_seen(format_transport_ctx_t *ctx, + const char *section) +{ + size_t i; + + for (i = 0; i < ctx->count; i++) { + if (strcmp(ctx->sections[i], section) == 0) { + return 1; + } + } + return 0; +} + +static hybbx_result_t format_transport_section_add(format_transport_ctx_t *ctx, + const char *section) +{ + char *copy; + + if (format_transport_section_seen(ctx, section)) { + return HYBBX_OK; + } + + if (ctx->count >= ctx->cap) { + size_t new_cap = ctx->cap == 0 ? 4 : ctx->cap * 2; + char **grown = realloc(ctx->sections, new_cap * sizeof(*grown)); + + if (grown == NULL) { + return HYBBX_ERR_NOMEM; + } + ctx->sections = grown; + ctx->cap = new_cap; + } + + copy = hybbx_strdup(section); + if (copy == NULL) { + return HYBBX_ERR_NOMEM; + } + + ctx->sections[ctx->count++] = copy; + return HYBBX_OK; +} + +char *hybbx_config_format_transport_sections(const hybbx_config_t *config, + const char *plugin_name) +{ + format_transport_ctx_t ctx; + char prefix[128]; + size_t i; + size_t len = 0; + size_t cap = 64; + char *out; + hybbx_result_t rc; + + if (config == NULL || plugin_name == NULL) { + return NULL; + } + + memset(&ctx, 0, sizeof(ctx)); + ctx.config = config; + snprintf(prefix, sizeof(prefix), "transport.%s", plugin_name); + ctx.prefix = prefix; + ctx.prefix_len = strlen(prefix); + + if (config_section_has_keys(config, prefix)) { + rc = format_transport_section_add(&ctx, prefix); + if (rc != HYBBX_OK) { + goto fail; + } + } + + for (i = 0; i < config->count; i++) { + const char *sec = config->entries[i].section; + const char *suffix; + + if (sec == NULL || strncmp(sec, prefix, ctx.prefix_len) != 0) { + continue; + } + + suffix = sec + ctx.prefix_len; + if (!config_suffix_is_digits(suffix)) { + continue; + } + if (!config_section_has_keys(config, sec)) { + continue; + } + + rc = format_transport_section_add(&ctx, sec); + if (rc != HYBBX_OK) { + goto fail; + } + } + + if (ctx.count == 0) { + for (i = 0; i < ctx.count; i++) { + free(ctx.sections[i]); + } + free(ctx.sections); + return hybbx_config_format_section(config, prefix); + } + + out = malloc(cap); + if (out == NULL) { + rc = HYBBX_ERR_NOMEM; + goto fail; + } + out[0] = '\0'; + + for (i = 0; i < ctx.count; i++) { + char *section_cfg = hybbx_config_format_section(config, ctx.sections[i]); + size_t part_len; + + if (section_cfg == NULL) { + free(out); + rc = HYBBX_ERR_NOMEM; + goto fail; + } + + part_len = strlen(section_cfg); + if (len > 0) { + if (len + 1 + part_len + 1 > cap) { + while (len + 1 + part_len + 1 > cap) { + cap *= 2; + } + { + char *grown = realloc(out, cap); + + if (grown == NULL) { + free(section_cfg); + free(out); + rc = HYBBX_ERR_NOMEM; + goto fail; + } + out = grown; + } + } + out[len++] = HYBBX_PACKET_RADIO_INSTANCE_SEP; + out[len] = '\0'; + } + + if (len + part_len + 1 > cap) { + while (len + part_len + 1 > cap) { + cap *= 2; + } + { + char *grown = realloc(out, cap); + + if (grown == NULL) { + free(section_cfg); + free(out); + rc = HYBBX_ERR_NOMEM; + goto fail; + } + out = grown; + } + } + + memcpy(out + len, section_cfg, part_len + 1); + len += part_len; + free(section_cfg); + } + + for (i = 0; i < ctx.count; i++) { + free(ctx.sections[i]); + } + free(ctx.sections); + return out; + +fail: + for (i = 0; i < ctx.count; i++) { + free(ctx.sections[i]); + } + free(ctx.sections); + return NULL; +} + +char *hybbx_config_prepend_packet_radio_max25(const hybbx_config_t *config, + const char *body) +{ + char prefix[HYBBX_CONFIG_VALUE_MAX * 2]; + char *out; + size_t prefix_len; + size_t body_len; + int check; + + if (config == NULL) { + return body != NULL ? hybbx_strdup(body) : NULL; + } + + if (body == NULL || body[0] == '\0') { + return hybbx_strdup(body != NULL ? body : ""); + } + + check = hybbx_config_get_bool(config, "max25", "check", 1); + snprintf(prefix, sizeof(prefix), + "max25_check=%s;max25_host=%s;max25_port=%u;max25_timeout_ms=%u", + check ? "yes" : "no", + hybbx_config_get(config, "max25", "host", "127.0.0.1"), + hybbx_config_get_uint(config, "max25", "port", + HYBBX_MAX25_DEFAULT_PORT, 1u, 65535u), + hybbx_config_get_uint(config, "max25", "timeout_ms", + HYBBX_MAX25_PROBE_TIMEOUT_MS, + 100u, 60000u)); + + if (body == NULL || body[0] == '\0') { + return hybbx_strdup(prefix); + } + + prefix_len = strlen(prefix); + body_len = strlen(body); + out = malloc(prefix_len + 1u + body_len + 1u); + if (out == NULL) { + return NULL; + } + + memcpy(out, prefix, prefix_len); + out[prefix_len] = HYBBX_PACKET_RADIO_INSTANCE_SEP; + memcpy(out + prefix_len + 1u, body, body_len + 1u); + return out; +} + +char *hybbx_config_format_packet_radio_start(const hybbx_config_t *config) +{ + char *body; + + body = hybbx_config_format_transport_sections(config, "packet_radio"); + if (body == NULL) { + return NULL; + } + + { + char *out = hybbx_config_prepend_packet_radio_max25(config, body); + + free(body); + return out; + } +} + +char *hybbx_config_format_baycom_start(const hybbx_config_t *config) +{ + char *body; + + body = hybbx_config_format_transport_sections(config, "baycom"); + if (body == NULL) { + return NULL; + } + + { + char *out = hybbx_config_prepend_packet_radio_max25(config, body); + + free(body); + return out; + } +} + +void hybbx_config_foreach(const hybbx_config_t *config, + hybbx_config_iter_fn fn, void *ctx) +{ + size_t i; + + if (config == NULL || fn == NULL) { + return; + } + + for (i = 0; i < config->count; i++) { + const hybbx_config_entry_t *entry = &config->entries[i]; + + fn(entry->section, entry->key, entry->value, ctx); + } +} + +void hybbx_config_foreach_section(const hybbx_config_t *config, + hybbx_config_section_iter_fn fn, void *ctx) +{ + size_t i; + + if (config == NULL || fn == NULL) { + return; + } + + for (i = 0; i < config->count; i++) { + const char *section = config->entries[i].section; + size_t j; + int seen = 0; + + for (j = 0; j < i; j++) { + if (strcmp(config->entries[j].section, section) == 0) { + seen = 1; + break; + } + } + + if (!seen) { + fn(section, ctx); + } + } +} + +hybbx_result_t hybbx_config_set(hybbx_config_t *config, + const char *section, + const char *key, + const char *value) +{ + size_t i; + + if (config == NULL || section == NULL || key == NULL || value == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < config->count; i++) { + if (strcmp(config->entries[i].section, section) == 0 && + strcmp(config->entries[i].key, key) == 0) { + char *copy = hybbx_strdup(value); + + if (copy == NULL) { + return HYBBX_ERR_NOMEM; + } + free(config->entries[i].value); + config->entries[i].value = copy; + return HYBBX_OK; + } + } + + return append_entry(config, section, key, value); +} + +void hybbx_config_remove_section(hybbx_config_t *config, const char *section) +{ + size_t i; + + if (config == NULL || section == NULL) { + return; + } + + i = 0; + while (i < config->count) { + if (strcmp(config->entries[i].section, section) == 0) { + free(config->entries[i].section); + free(config->entries[i].key); + free(config->entries[i].value); + if (i + 1 < config->count) { + memmove(&config->entries[i], &config->entries[i + 1], + (config->count - i - 1) * sizeof(config->entries[i])); + } + config->count--; + } else { + i++; + } + } +} + +hybbx_result_t hybbx_config_save(const hybbx_config_t *config, + const char *path) +{ + FILE *fp; + size_t i; + const char *current = NULL; + + if (config == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + for (i = 0; i < config->count; i++) { + const hybbx_config_entry_t *entry = &config->entries[i]; + + if (current == NULL || strcmp(current, entry->section) != 0) { + if (current != NULL) { + fputc('\n', fp); + } + fprintf(fp, "[%s]\n", entry->section); + current = entry->section; + } + fprintf(fp, "%s = %s\n", entry->key, entry->value); + } + + fclose(fp); + return HYBBX_OK; +} diff --git a/src/core/crdop.c b/src/core/crdop.c new file mode 100644 index 0000000..67b3575 --- /dev/null +++ b/src/core/crdop.c @@ -0,0 +1,66 @@ +#include "hybbx/crdop.h" + +#include <ctype.h> +#include <string.h> + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +hybbx_crdop_radio_profile_t hybbx_crdop_profile_parse(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_CRDOP_PROFILE_AMATEUR; + } + + if (str_ieq(value, "cb") || str_ieq(value, "11m") || + str_ieq(value, "citizens_band")) { + return HYBBX_CRDOP_PROFILE_CB; + } + + return HYBBX_CRDOP_PROFILE_AMATEUR; +} + +const char *hybbx_crdop_profile_name(hybbx_crdop_radio_profile_t profile) +{ + return profile == HYBBX_CRDOP_PROFILE_CB ? "cb" : "amateur"; +} + +const char *hybbx_crdop_default_arq_bandwidth(hybbx_crdop_radio_profile_t profile) +{ + return profile == HYBBX_CRDOP_PROFILE_CB ? "500MAX" : "500MAX"; +} + +int hybbx_crdop_bandwidth_exceeds_cb(const char *arq_bandwidth) +{ + const char *p; + unsigned n = 0; + + if (arq_bandwidth == NULL || arq_bandwidth[0] == '\0') { + return 0; + } + + p = arq_bandwidth; + while (*p >= '0' && *p <= '9') { + n = n * 10u + (unsigned)(*p - '0'); + p++; + } + + return n > 1000u; +} diff --git a/src/core/crypto.c b/src/core/crypto.c new file mode 100644 index 0000000..dffcea4 --- /dev/null +++ b/src/core/crypto.c @@ -0,0 +1,302 @@ +#include "hybbx/crypto.h" + +#include "crypto_backends.h" +#include "monocypher.h" + +#include <string.h> + +#define HYBBX_CRYPTO_MAX_BYTES (256u * 1024u) + +static hybbx_result_t crypto_len_ok(size_t len) +{ + if (len > HYBBX_CRYPTO_MAX_BYTES) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_crypto_random(uint8_t *buf, size_t len) +{ + if (buf == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + return hybbx_backend_random(buf, len); +} + +const char *hybbx_crypto_alg_name(hybbx_crypto_alg_t alg) +{ + switch (alg) { + case HYBBX_CRYPTO_AES_256_GCM: + return "aes-256-gcm"; + case HYBBX_CRYPTO_XCHACHA20_POLY1305: + return "xchacha20-poly1305"; + case HYBBX_CRYPTO_X25519_AEAD: + return "x25519-xchacha20-poly1305"; + default: + return "unknown"; + } +} + +size_t hybbx_crypto_nonce_size(hybbx_crypto_alg_t alg) +{ + switch (alg) { + case HYBBX_CRYPTO_AES_256_GCM: + return HYBBX_CRYPTO_AES_GCM_NONCE; + case HYBBX_CRYPTO_XCHACHA20_POLY1305: + return HYBBX_CRYPTO_XCHACHA_NONCE; + default: + return 0; + } +} + +static hybbx_result_t derive_x25519_key(const uint8_t shared[32], + uint8_t key[32]) +{ + static const uint8_t label[] = "hybbx-x25519-aead-key-v1"; + + hybbx_backend_blake2b_keyed(key, 32, shared, 32, label, sizeof(label) - 1); + return HYBBX_OK; +} + +hybbx_result_t hybbx_crypto_encrypt(hybbx_crypto_alg_t alg, + const uint8_t key[HYBBX_CRYPTO_KEY_SIZE], + const uint8_t *nonce, size_t nonce_len, + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + uint8_t tag[HYBBX_CRYPTO_TAG_SIZE]) +{ + hybbx_result_t rc; + + if (key == NULL || nonce == NULL || tag == NULL || + (plaintext_len > 0 && (plaintext == NULL || ciphertext == NULL))) { + return HYBBX_ERR_INVALID; + } + + rc = crypto_len_ok(plaintext_len); + if (rc != HYBBX_OK) { + return rc; + } + + if (alg == HYBBX_CRYPTO_AES_256_GCM) { + if (nonce_len != HYBBX_CRYPTO_AES_GCM_NONCE) { + return HYBBX_ERR_INVALID; + } + if (hybbx_backend_aes256gcm_encrypt(key, nonce, aad, aad_len, plaintext, + plaintext_len, ciphertext, tag) != + 0) { + return HYBBX_ERR_IO; + } + return HYBBX_OK; + } + + if (alg == HYBBX_CRYPTO_XCHACHA20_POLY1305) { + uint8_t nonce24[HYBBX_CRYPTO_XCHACHA_NONCE]; + + if (nonce_len != HYBBX_CRYPTO_XCHACHA_NONCE) { + return HYBBX_ERR_INVALID; + } + memcpy(nonce24, nonce, sizeof(nonce24)); + hybbx_backend_chacha_lock(ciphertext, tag, key, nonce24, aad, aad_len, + plaintext, plaintext_len); + crypto_wipe(nonce24, sizeof(nonce24)); + return HYBBX_OK; + } + + return HYBBX_ERR_INVALID; +} + +hybbx_result_t hybbx_crypto_decrypt(hybbx_crypto_alg_t alg, + const uint8_t key[HYBBX_CRYPTO_KEY_SIZE], + const uint8_t *nonce, size_t nonce_len, + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + const uint8_t tag[HYBBX_CRYPTO_TAG_SIZE]) +{ + hybbx_result_t rc; + int ok; + + if (key == NULL || nonce == NULL || tag == NULL || + (ciphertext_len > 0 && (ciphertext == NULL || plaintext == NULL))) { + return HYBBX_ERR_INVALID; + } + + rc = crypto_len_ok(ciphertext_len); + if (rc != HYBBX_OK) { + return rc; + } + + if (alg == HYBBX_CRYPTO_AES_256_GCM) { + if (nonce_len != HYBBX_CRYPTO_AES_GCM_NONCE) { + return HYBBX_ERR_INVALID; + } + ok = hybbx_backend_aes256gcm_decrypt(key, nonce, aad, aad_len, + ciphertext, ciphertext_len, + plaintext, tag); + return ok == 0 ? HYBBX_OK : HYBBX_ERR_DENIED; + } + + if (alg == HYBBX_CRYPTO_XCHACHA20_POLY1305) { + uint8_t nonce24[HYBBX_CRYPTO_XCHACHA_NONCE]; + + if (nonce_len != HYBBX_CRYPTO_XCHACHA_NONCE) { + return HYBBX_ERR_INVALID; + } + memcpy(nonce24, nonce, sizeof(nonce24)); + ok = hybbx_backend_chacha_unlock(plaintext, tag, key, nonce24, aad, + aad_len, ciphertext, ciphertext_len); + crypto_wipe(nonce24, sizeof(nonce24)); + return ok == 0 ? HYBBX_OK : HYBBX_ERR_DENIED; + } + + return HYBBX_ERR_INVALID; +} + +hybbx_result_t hybbx_crypto_x25519_keypair( + uint8_t public_key[HYBBX_CRYPTO_X25519_PUBLIC_KEY], + uint8_t secret_key[HYBBX_CRYPTO_X25519_SECRET_KEY]) +{ + if (public_key == NULL || secret_key == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_crypto_random(secret_key, HYBBX_CRYPTO_X25519_SECRET_KEY) != + HYBBX_OK) { + return HYBBX_ERR_IO; + } + + secret_key[0] &= 248; + secret_key[31] &= 127; + secret_key[31] |= 64; + + hybbx_backend_x25519_public_key(public_key, secret_key); + return HYBBX_OK; +} + +hybbx_result_t hybbx_crypto_x25519_seal( + const uint8_t recipient_pk[HYBBX_CRYPTO_X25519_PUBLIC_KEY], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len, + uint8_t *sealed, size_t *sealed_len, size_t sealed_cap) +{ + uint8_t ep_sk[32]; + uint8_t ep_pk[32]; + uint8_t shared[32]; + uint8_t key[32]; + uint8_t nonce[HYBBX_CRYPTO_XCHACHA_NONCE]; + uint8_t *cipher; + uint8_t *tag; + size_t need; + hybbx_result_t rc; + + if (recipient_pk == NULL || sealed == NULL || sealed_len == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = crypto_len_ok(plaintext_len); + if (rc != HYBBX_OK) { + return rc; + } + + need = HYBBX_CRYPTO_X25519_SEAL_OVERHEAD + plaintext_len; + if (sealed_cap < need) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_crypto_random(ep_sk, sizeof(ep_sk)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + ep_sk[0] &= 248; + ep_sk[31] &= 127; + ep_sk[31] |= 64; + + hybbx_backend_x25519_public_key(ep_pk, ep_sk); + hybbx_backend_x25519_shared(shared, ep_sk, recipient_pk); + derive_x25519_key(shared, key); + + if (hybbx_crypto_random(nonce, sizeof(nonce)) != HYBBX_OK) { + crypto_wipe(ep_sk, sizeof(ep_sk)); + crypto_wipe(shared, sizeof(shared)); + crypto_wipe(key, sizeof(key)); + return HYBBX_ERR_IO; + } + + memcpy(sealed, ep_pk, 32); + memcpy(sealed + 32, nonce, sizeof(nonce)); + cipher = sealed + 32 + sizeof(nonce); + tag = sealed + 32 + sizeof(nonce) + plaintext_len; + + rc = hybbx_crypto_encrypt(HYBBX_CRYPTO_XCHACHA20_POLY1305, key, nonce, + sizeof(nonce), aad, aad_len, plaintext, + plaintext_len, cipher, tag); + + crypto_wipe(ep_sk, sizeof(ep_sk)); + crypto_wipe(shared, sizeof(shared)); + crypto_wipe(key, sizeof(key)); + + if (rc != HYBBX_OK) { + return rc; + } + + *sealed_len = need; + return HYBBX_OK; +} + +hybbx_result_t hybbx_crypto_x25519_open( + const uint8_t recipient_sk[HYBBX_CRYPTO_X25519_SECRET_KEY], + const uint8_t *aad, size_t aad_len, + const uint8_t *sealed, size_t sealed_len, + uint8_t *plaintext, size_t *plaintext_len, size_t plaintext_cap) +{ + const uint8_t *ep_pk; + const uint8_t *nonce; + const uint8_t *cipher; + const uint8_t *tag; + size_t cipher_len; + uint8_t shared[32]; + uint8_t key[32]; + uint8_t tag_buf[HYBBX_CRYPTO_TAG_SIZE]; + hybbx_result_t rc; + + if (recipient_sk == NULL || sealed == NULL || plaintext == NULL || + plaintext_len == NULL) { + return HYBBX_ERR_INVALID; + } + + if (sealed_len < HYBBX_CRYPTO_X25519_SEAL_OVERHEAD) { + return HYBBX_ERR_INVALID; + } + + cipher_len = sealed_len - HYBBX_CRYPTO_X25519_SEAL_OVERHEAD; + if (cipher_len > plaintext_cap) { + return HYBBX_ERR_INVALID; + } + + ep_pk = sealed; + nonce = sealed + 32; + cipher = sealed + 32 + HYBBX_CRYPTO_XCHACHA_NONCE; + tag = cipher + cipher_len; + memcpy(tag_buf, tag, sizeof(tag_buf)); + + hybbx_backend_x25519_shared(shared, recipient_sk, ep_pk); + derive_x25519_key(shared, key); + + rc = hybbx_crypto_decrypt(HYBBX_CRYPTO_XCHACHA20_POLY1305, key, nonce, + HYBBX_CRYPTO_XCHACHA_NONCE, aad, aad_len, + cipher, cipher_len, plaintext, tag_buf); + + crypto_wipe(shared, sizeof(shared)); + crypto_wipe(key, sizeof(key)); + + if (rc != HYBBX_OK) { + return rc; + } + + *plaintext_len = cipher_len; + return HYBBX_OK; +} diff --git a/src/core/crypto_backends.c b/src/core/crypto_backends.c new file mode 100644 index 0000000..da25497 --- /dev/null +++ b/src/core/crypto_backends.c @@ -0,0 +1,223 @@ +#include "crypto_backends.h" +#include "hybbx/crypto_config.h" + +#include "aes256gcm.h" +#include "monocypher.h" +#include "tinysha256.h" + +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#if defined(__linux__) +#include <sys/random.h> +#endif + +#if defined(HYBBX_HAVE_OPENSSL) +void hybbx_openssl_sha256_hex(const char *data, size_t len, char hex[65]); +int hybbx_openssl_aes256gcm_encrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, uint8_t tag[16]); +int hybbx_openssl_aes256gcm_decrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, const uint8_t tag[16]); +hybbx_result_t hybbx_openssl_random(uint8_t *buf, size_t len); +#endif + +#if defined(HYBBX_HAVE_LIBSODIUM) +void hybbx_libsodium_chacha_lock(uint8_t *ciphertext, uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len); +int hybbx_libsodium_chacha_unlock(uint8_t *plaintext, const uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len); +void hybbx_libsodium_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]); +void hybbx_libsodium_x25519_shared(uint8_t shared[32], + const uint8_t secret_key[32], + const uint8_t peer_public_key[32]); +#endif + +void hybbx_backend_sha256_hex(const char *data, size_t len, char hex[65]) +{ +#if defined(HYBBX_HAVE_OPENSSL) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->password_hash == HYBBX_PASSWORD_HASH_OPENSSL) { + hybbx_openssl_sha256_hex(data, len, hex); + return; + } +#endif + + tinysha256_hex(data, len, hex); +} + +int hybbx_backend_aes256gcm_encrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, uint8_t tag[16]) +{ +#if defined(HYBBX_HAVE_OPENSSL) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->aes_gcm == HYBBX_AES_GCM_OPENSSL) { + return hybbx_openssl_aes256gcm_encrypt(key, nonce, aad, aad_len, + plaintext, plaintext_len, + ciphertext, tag); + } +#endif + + return hybbx_aes256gcm_encrypt(key, nonce, aad, aad_len, plaintext, + plaintext_len, ciphertext, tag); +} + +int hybbx_backend_aes256gcm_decrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, const uint8_t tag[16]) +{ +#if defined(HYBBX_HAVE_OPENSSL) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->aes_gcm == HYBBX_AES_GCM_OPENSSL) { + return hybbx_openssl_aes256gcm_decrypt(key, nonce, aad, aad_len, + ciphertext, ciphertext_len, + plaintext, tag); + } +#endif + + return hybbx_aes256gcm_decrypt(key, nonce, aad, aad_len, ciphertext, + ciphertext_len, plaintext, tag); +} + +void hybbx_backend_chacha_lock(uint8_t *ciphertext, uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len) +{ +#if defined(HYBBX_HAVE_LIBSODIUM) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->chacha == HYBBX_CHACHA_LIBSODIUM) { + hybbx_libsodium_chacha_lock(ciphertext, tag, key, nonce, aad, aad_len, + plaintext, plaintext_len); + return; + } +#endif + + crypto_aead_lock(ciphertext, tag, key, nonce, aad, aad_len, plaintext, + plaintext_len); +} + +int hybbx_backend_chacha_unlock(uint8_t *plaintext, const uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len) +{ +#if defined(HYBBX_HAVE_LIBSODIUM) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->chacha == HYBBX_CHACHA_LIBSODIUM) { + return hybbx_libsodium_chacha_unlock(plaintext, tag, key, nonce, aad, + aad_len, ciphertext, + ciphertext_len); + } +#endif + + return crypto_aead_unlock(plaintext, tag, key, nonce, aad, aad_len, + ciphertext, ciphertext_len); +} + +void hybbx_backend_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]) +{ +#if defined(HYBBX_HAVE_LIBSODIUM) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->x25519 == HYBBX_X25519_LIBSODIUM) { + hybbx_libsodium_x25519_public_key(public_key, secret_key); + return; + } +#endif + + crypto_x25519_public_key(public_key, secret_key); +} + +void hybbx_backend_x25519_shared(uint8_t shared[32], + const uint8_t secret_key[32], + const uint8_t peer_public_key[32]) +{ +#if defined(HYBBX_HAVE_LIBSODIUM) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->x25519 == HYBBX_X25519_LIBSODIUM) { + hybbx_libsodium_x25519_shared(shared, secret_key, peer_public_key); + return; + } +#endif + + crypto_x25519(shared, secret_key, peer_public_key); +} + +void hybbx_backend_blake2b_keyed(uint8_t *hash, size_t hash_size, + const uint8_t *message, size_t message_size, + const uint8_t *key, size_t key_size) +{ + crypto_blake2b_keyed(hash, hash_size, message, message_size, key, key_size); +} + +static hybbx_result_t system_random(uint8_t *buf, size_t len) +{ + size_t done = 0; + +#if defined(__linux__) + while (done < len) { + ssize_t n = getrandom(buf + done, len - done, 0); + + if (n < 0) { + break; + } + done += (size_t)n; + } +#endif + + if (done < len) { + int fd = open("/dev/urandom", O_RDONLY); + + if (fd < 0) { + return HYBBX_ERR_IO; + } + + while (done < len) { + ssize_t n = read(fd, buf + done, len - done); + + if (n <= 0) { + close(fd); + return HYBBX_ERR_IO; + } + done += (size_t)n; + } + + close(fd); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_backend_random(uint8_t *buf, size_t len) +{ +#if defined(HYBBX_HAVE_OPENSSL) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->random == HYBBX_RANDOM_OPENSSL) { + return hybbx_openssl_random(buf, len); + } +#endif + + return system_random(buf, len); +} diff --git a/src/core/crypto_backends.h b/src/core/crypto_backends.h new file mode 100644 index 0000000..0a7f1a1 --- /dev/null +++ b/src/core/crypto_backends.h @@ -0,0 +1,62 @@ +#ifndef HYBBX_CRYPTO_BACKENDS_H +#define HYBBX_CRYPTO_BACKENDS_H + +#include "hybbx/types.h" + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_BACKEND_SHA256_HEX_SIZE 65 +#define HYBBX_BACKEND_AES_GCM_KEY_SIZE 32 +#define HYBBX_BACKEND_AES_GCM_NONCE_SIZE 12 +#define HYBBX_BACKEND_AES_GCM_TAG_SIZE 16 +#define HYBBX_BACKEND_XCHACHA_NONCE_SIZE 24 +#define HYBBX_BACKEND_TAG_SIZE 16 + +void hybbx_backend_sha256_hex(const char *data, size_t len, char hex[65]); + +int hybbx_backend_aes256gcm_encrypt( + const uint8_t key[HYBBX_BACKEND_AES_GCM_KEY_SIZE], + const uint8_t nonce[HYBBX_BACKEND_AES_GCM_NONCE_SIZE], + const uint8_t *aad, size_t aad_len, const uint8_t *plaintext, + size_t plaintext_len, uint8_t *ciphertext, + uint8_t tag[HYBBX_BACKEND_AES_GCM_TAG_SIZE]); + +int hybbx_backend_aes256gcm_decrypt( + const uint8_t key[HYBBX_BACKEND_AES_GCM_KEY_SIZE], + const uint8_t nonce[HYBBX_BACKEND_AES_GCM_NONCE_SIZE], + const uint8_t *aad, size_t aad_len, const uint8_t *ciphertext, + size_t ciphertext_len, uint8_t *plaintext, + const uint8_t tag[HYBBX_BACKEND_AES_GCM_TAG_SIZE]); + +void hybbx_backend_chacha_lock(uint8_t *ciphertext, uint8_t tag[16], + const uint8_t key[32], + const uint8_t nonce[24], const uint8_t *aad, + size_t aad_len, const uint8_t *plaintext, + size_t plaintext_len); + +int hybbx_backend_chacha_unlock(uint8_t *plaintext, const uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len); + +void hybbx_backend_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]); +void hybbx_backend_x25519_shared(uint8_t shared[32], + const uint8_t secret_key[32], + const uint8_t peer_public_key[32]); +void hybbx_backend_blake2b_keyed(uint8_t *hash, size_t hash_size, + const uint8_t *message, size_t message_size, + const uint8_t *key, size_t key_size); + +hybbx_result_t hybbx_backend_random(uint8_t *buf, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CRYPTO_BACKENDS_H */ diff --git a/src/core/crypto_config.c b/src/core/crypto_config.c new file mode 100644 index 0000000..65b64ef --- /dev/null +++ b/src/core/crypto_config.c @@ -0,0 +1,247 @@ +#include "hybbx/crypto_config.h" +#include "hybbx/config.h" +#include "hybbx/log.h" + +#include <ctype.h> +#include <stdio.h> +#include <string.h> + +static hybbx_crypto_config_t g_crypto_config; +static int g_crypto_config_ready; + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static hybbx_password_hash_backend_t parse_password_hash(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "tinysha256") || str_ieq(value, "bundled") || + str_ieq(value, "default")) { + return HYBBX_PASSWORD_HASH_TINYSHA256; + } + + if (str_ieq(value, "openssl") || str_ieq(value, "libcrypto")) { + return HYBBX_PASSWORD_HASH_OPENSSL; + } + + return HYBBX_PASSWORD_HASH_TINYSHA256; +} + +static hybbx_aes_gcm_backend_t parse_aes_gcm(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "tinyaes") || str_ieq(value, "bundled") || + str_ieq(value, "default")) { + return HYBBX_AES_GCM_TINYAES; + } + + if (str_ieq(value, "openssl") || str_ieq(value, "libcrypto")) { + return HYBBX_AES_GCM_OPENSSL; + } + + return HYBBX_AES_GCM_TINYAES; +} + +static hybbx_chacha_backend_t parse_chacha(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "monocypher") || str_ieq(value, "bundled") || + str_ieq(value, "default")) { + return HYBBX_CHACHA_MONOCYPHER; + } + + if (str_ieq(value, "libsodium") || str_ieq(value, "sodium")) { + return HYBBX_CHACHA_LIBSODIUM; + } + + return HYBBX_CHACHA_MONOCYPHER; +} + +static hybbx_x25519_backend_t parse_x25519(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "monocypher") || str_ieq(value, "bundled") || + str_ieq(value, "default")) { + return HYBBX_X25519_MONOCYPHER; + } + + if (str_ieq(value, "libsodium") || str_ieq(value, "sodium")) { + return HYBBX_X25519_LIBSODIUM; + } + + return HYBBX_X25519_MONOCYPHER; +} + +static hybbx_random_backend_t parse_random(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "system") || str_ieq(value, "bundled") || + str_ieq(value, "default") || str_ieq(value, "getrandom") || + str_ieq(value, "urandom")) { + return HYBBX_RANDOM_SYSTEM; + } + + if (str_ieq(value, "openssl") || str_ieq(value, "libcrypto")) { + return HYBBX_RANDOM_OPENSSL; + } + + return HYBBX_RANDOM_SYSTEM; +} + +static void resolve_backends(hybbx_crypto_config_t *cfg) +{ +#if !defined(HYBBX_HAVE_OPENSSL) + if (cfg->password_hash == HYBBX_PASSWORD_HASH_OPENSSL) { + hybbx_log_warn("[crypto] password_hash=openssl requested but HyBBX was not " + "built with OpenSSL; using tinysha256"); + cfg->password_hash = HYBBX_PASSWORD_HASH_TINYSHA256; + } + if (cfg->aes_gcm == HYBBX_AES_GCM_OPENSSL) { + hybbx_log_warn("[crypto] aes_gcm=openssl requested but HyBBX was not built " + "with OpenSSL; using tinyaes"); + cfg->aes_gcm = HYBBX_AES_GCM_TINYAES; + } + if (cfg->random == HYBBX_RANDOM_OPENSSL) { + hybbx_log_warn("[crypto] random=openssl requested but HyBBX was not built " + "with OpenSSL; using system"); + cfg->random = HYBBX_RANDOM_SYSTEM; + } +#endif + +#if !defined(HYBBX_HAVE_LIBSODIUM) + if (cfg->chacha == HYBBX_CHACHA_LIBSODIUM) { + hybbx_log_warn("[crypto] chacha=libsodium requested but HyBBX was not built " + "with libsodium; using monocypher"); + cfg->chacha = HYBBX_CHACHA_MONOCYPHER; + } + if (cfg->x25519 == HYBBX_X25519_LIBSODIUM) { + hybbx_log_warn("[crypto] x25519=libsodium requested but HyBBX was not built " + "with libsodium; using monocypher"); + cfg->x25519 = HYBBX_X25519_MONOCYPHER; + } +#endif +} + +void hybbx_crypto_config_defaults(hybbx_crypto_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->password_hash = HYBBX_PASSWORD_HASH_TINYSHA256; + cfg->aes_gcm = HYBBX_AES_GCM_TINYAES; + cfg->chacha = HYBBX_CHACHA_MONOCYPHER; + cfg->x25519 = HYBBX_X25519_MONOCYPHER; + cfg->random = HYBBX_RANDOM_SYSTEM; +} + +void hybbx_crypto_config_apply(const hybbx_config_t *config) +{ + const char *value; + + hybbx_crypto_config_defaults(&g_crypto_config); + + if (config != NULL) { + value = hybbx_config_get(config, "crypto", "password_hash", NULL); + g_crypto_config.password_hash = parse_password_hash(value); + + value = hybbx_config_get(config, "crypto", "aes_gcm", NULL); + g_crypto_config.aes_gcm = parse_aes_gcm(value); + + value = hybbx_config_get(config, "crypto", "chacha", NULL); + g_crypto_config.chacha = parse_chacha(value); + + value = hybbx_config_get(config, "crypto", "x25519", NULL); + g_crypto_config.x25519 = parse_x25519(value); + + value = hybbx_config_get(config, "crypto", "random", NULL); + g_crypto_config.random = parse_random(value); + } + + resolve_backends(&g_crypto_config); + g_crypto_config_ready = 1; + + hybbx_log_info("[crypto] password_hash=%s aes_gcm=%s chacha=%s x25519=%s random=%s", + hybbx_password_hash_backend_name(g_crypto_config.password_hash), + hybbx_aes_gcm_backend_name(g_crypto_config.aes_gcm), + hybbx_chacha_backend_name(g_crypto_config.chacha), + hybbx_x25519_backend_name(g_crypto_config.x25519), + hybbx_random_backend_name(g_crypto_config.random)); +} + +const hybbx_crypto_config_t *hybbx_crypto_config_get(void) +{ + if (!g_crypto_config_ready) { + hybbx_crypto_config_defaults(&g_crypto_config); + g_crypto_config_ready = 1; + } + + return &g_crypto_config; +} + +const char *hybbx_password_hash_backend_name(hybbx_password_hash_backend_t b) +{ + switch (b) { + case HYBBX_PASSWORD_HASH_OPENSSL: + return "openssl"; + default: + return "tinysha256"; + } +} + +const char *hybbx_aes_gcm_backend_name(hybbx_aes_gcm_backend_t b) +{ + switch (b) { + case HYBBX_AES_GCM_OPENSSL: + return "openssl"; + default: + return "tinyaes"; + } +} + +const char *hybbx_chacha_backend_name(hybbx_chacha_backend_t b) +{ + switch (b) { + case HYBBX_CHACHA_LIBSODIUM: + return "libsodium"; + default: + return "monocypher"; + } +} + +const char *hybbx_x25519_backend_name(hybbx_x25519_backend_t b) +{ + switch (b) { + case HYBBX_X25519_LIBSODIUM: + return "libsodium"; + default: + return "monocypher"; + } +} + +const char *hybbx_random_backend_name(hybbx_random_backend_t b) +{ + switch (b) { + case HYBBX_RANDOM_OPENSSL: + return "openssl"; + default: + return "system"; + } +} diff --git a/src/core/crypto_libsodium.c b/src/core/crypto_libsodium.c new file mode 100644 index 0000000..85cf537 --- /dev/null +++ b/src/core/crypto_libsodium.c @@ -0,0 +1,111 @@ +#include "hybbx/types.h" + +#include <sodium.h> + +#include <stdlib.h> +#include <string.h> + +static int sodium_ready; + +static int ensure_sodium(void) +{ + if (!sodium_ready) { + if (sodium_init() < 0) { + return -1; + } + sodium_ready = 1; + } + + return 0; +} + +void hybbx_libsodium_chacha_lock(uint8_t *ciphertext, uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len) +{ + unsigned long long out_len = 0; + uint8_t *packed; + size_t packed_cap; + + if (ensure_sodium() != 0) { + return; + } + + packed_cap = plaintext_len + + crypto_aead_xchacha20poly1305_ietf_ABYTES; + packed = malloc(packed_cap); + if (packed == NULL) { + return; + } + + if (crypto_aead_xchacha20poly1305_ietf_encrypt( + packed, &out_len, plaintext, plaintext_len, aad, aad_len, NULL, + nonce, key) != 0) { + free(packed); + return; + } + + if (plaintext_len > 0 && ciphertext != NULL) { + memcpy(ciphertext, packed, plaintext_len); + } + memcpy(tag, packed + plaintext_len, + crypto_aead_xchacha20poly1305_ietf_ABYTES); + free(packed); +} + +int hybbx_libsodium_chacha_unlock(uint8_t *plaintext, const uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len) +{ + unsigned long long pt_len = 0; + uint8_t *packed; + size_t packed_len; + int rc; + + if (ensure_sodium() != 0) { + return -1; + } + + packed_len = ciphertext_len + + crypto_aead_xchacha20poly1305_ietf_ABYTES; + packed = malloc(packed_len); + if (packed == NULL) { + return -1; + } + + if (ciphertext_len > 0 && ciphertext != NULL) { + memcpy(packed, ciphertext, ciphertext_len); + } + memcpy(packed + ciphertext_len, tag, + crypto_aead_xchacha20poly1305_ietf_ABYTES); + + rc = crypto_aead_xchacha20poly1305_ietf_decrypt( + plaintext, &pt_len, NULL, packed, packed_len, aad, aad_len, nonce, + key); + + free(packed); + return rc; +} + +void hybbx_libsodium_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]) +{ + if (ensure_sodium() != 0) { + return; + } + + crypto_scalarmult_base(public_key, secret_key); +} + +void hybbx_libsodium_x25519_shared(uint8_t shared[32], + const uint8_t secret_key[32], + const uint8_t peer_public_key[32]) +{ + if (ensure_sodium() != 0) { + return; + } + + crypto_scalarmult(shared, secret_key, peer_public_key); +} diff --git a/src/core/crypto_openssl.c b/src/core/crypto_openssl.c new file mode 100644 index 0000000..551ea83 --- /dev/null +++ b/src/core/crypto_openssl.c @@ -0,0 +1,142 @@ +#include "hybbx/types.h" + +#include <openssl/evp.h> +#include <openssl/rand.h> + +#include <stdio.h> +#include <string.h> + +void hybbx_openssl_sha256_hex(const char *data, size_t len, char hex[65]) +{ + unsigned char digest[32]; + unsigned int digest_len = sizeof(digest); + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + if (ctx == NULL) { + hex[0] = '\0'; + return; + } + + if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1 || + EVP_DigestUpdate(ctx, data, len) != 1 || + EVP_DigestFinal_ex(ctx, digest, &digest_len) != 1) { + EVP_MD_CTX_free(ctx); + hex[0] = '\0'; + return; + } + + EVP_MD_CTX_free(ctx); + + for (size_t i = 0; i < digest_len; i++) { + snprintf(hex + i * 2, 3, "%02x", digest[i]); + } +} + +static int openssl_gcm_crypt(int encrypt, const uint8_t key[32], + const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *in, size_t in_len, + uint8_t *out, const uint8_t tag_in[16], + uint8_t tag_out[16]) +{ + EVP_CIPHER_CTX *ctx = NULL; + int out_len = 0; + int total = 0; + int rc = -1; + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + return -1; + } + + if (EVP_CipherInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL, encrypt) != + 1 || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1 || + EVP_CipherInit_ex(ctx, NULL, NULL, key, nonce, encrypt) != 1) { + goto done; + } + + if (aad_len > 0 && aad != NULL) { + if (EVP_CipherUpdate(ctx, NULL, &out_len, aad, (int)aad_len) != 1) { + goto done; + } + } + + if (in_len > 0) { + if (EVP_CipherUpdate(ctx, out, &out_len, in, (int)in_len) != 1) { + goto done; + } + total = out_len; + } + + if (encrypt) { + if (EVP_EncryptFinal_ex(ctx, out + total, &out_len) != 1) { + goto done; + } + if (tag_out == NULL || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag_out) != 1) { + goto done; + } + rc = 0; + goto done; + } + + if (tag_in == NULL || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, + (void *)(uintptr_t)tag_in) != 1) { + goto done; + } + + if (EVP_DecryptFinal_ex(ctx, out + total, &out_len) != 1) { + goto done; + } + + rc = 0; +done: + EVP_CIPHER_CTX_free(ctx); + return rc; +} + +int hybbx_openssl_aes256gcm_encrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, uint8_t tag[16]) +{ + if (key == NULL || nonce == NULL || tag == NULL) { + return -1; + } + if (plaintext_len > 0 && (plaintext == NULL || ciphertext == NULL)) { + return -1; + } + + return openssl_gcm_crypt(1, key, nonce, aad, aad_len, plaintext, + plaintext_len, ciphertext, NULL, tag); +} + +int hybbx_openssl_aes256gcm_decrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, const uint8_t tag[16]) +{ + if (key == NULL || nonce == NULL || tag == NULL) { + return -1; + } + if (ciphertext_len > 0 && (ciphertext == NULL || plaintext == NULL)) { + return -1; + } + + return openssl_gcm_crypt(0, key, nonce, aad, aad_len, ciphertext, + ciphertext_len, plaintext, tag, NULL); +} + +hybbx_result_t hybbx_openssl_random(uint8_t *buf, size_t len) +{ + if (buf == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + if (RAND_bytes(buf, (int)len) == 1) { + return HYBBX_OK; + } + + return HYBBX_ERR_IO; +} diff --git a/src/core/daemon_wrap.c b/src/core/daemon_wrap.c new file mode 100644 index 0000000..04c990a --- /dev/null +++ b/src/core/daemon_wrap.c @@ -0,0 +1,320 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/daemon_wrap.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +void hybbx_daemon_launch_opts_defaults(hybbx_daemon_launch_opts_t *opts) +{ + if (opts == NULL) { + return; + } + + memset(opts, 0, sizeof(*opts)); + hybbx_strlcpy(opts->session, HYBBX_DAEMON_DEFAULT_SESSION, + sizeof(opts->session)); +} + +static int path_executable(const char *path) +{ + return path != NULL && path[0] != '\0' && access(path, X_OK) == 0; +} + +static int find_in_path(const char *name, char *out, size_t out_len) +{ + const char *path_env; + char copy[HYBBX_PATH_MAX]; + char *save = NULL; + char *dir; + + if (name == NULL || out == NULL || out_len == 0) { + return 0; + } + + path_env = getenv("PATH"); + if (path_env == NULL || path_env[0] == '\0') { + return 0; + } + + snprintf(copy, sizeof(copy), "%s", path_env); + for (dir = strtok_r(copy, ":", &save); dir != NULL; + dir = strtok_r(NULL, ":", &save)) { + if (hybbx_path_join(out, out_len, dir, name) != HYBBX_OK) { + continue; + } + if (path_executable(out)) { + return 1; + } + } + + return 0; +} + +static int wrapped_child(void) +{ + const char *env = getenv(HYBBX_DAEMON_WRAP_ENV); + + return env != NULL && env[0] == '1'; +} + +static int in_screen(void) +{ + const char *sty = getenv("STY"); + + return sty != NULL && sty[0] != '\0'; +} + +static int in_tmux(void) +{ + const char *tmux = getenv("TMUX"); + + return tmux != NULL && tmux[0] != '\0'; +} + +static int arg_is_launch_flag(const char *arg) +{ + if (arg == NULL) { + return 0; + } + + return strcmp(arg, "-f") == 0 || strcmp(arg, "--foreground") == 0 || + strcmp(arg, "--attach") == 0 || strcmp(arg, "--screen") == 0 || + strcmp(arg, "--tmux") == 0; +} + +static int build_inner_argv(char **out, int out_max, + const char *binary, + int argc, char **argv) +{ + int outc = 0; + int i; + + if (out == NULL || out_max < 2 || binary == NULL) { + return -1; + } + + out[outc++] = (char *)binary; + + if (!wrapped_child()) { + out[outc++] = "-f"; + } + + for (i = 1; i < argc && outc < out_max - 1; i++) { + if (strcmp(argv[i], "--screen") == 0 || strcmp(argv[i], "--tmux") == 0) { + if (i + 1 < argc && argv[i + 1][0] != '-' && + !arg_is_launch_flag(argv[i + 1])) { + i++; + } + continue; + } + if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--foreground") == 0 || + strcmp(argv[i], "--attach") == 0) { + continue; + } + + out[outc++] = argv[i]; + } + + out[outc] = NULL; + return outc; +} + +static hybbx_result_t exec_screen_attach(const char *session) +{ + char screen_path[HYBBX_PATH_MAX]; + char *args[8]; + int n = 0; + + if (!find_in_path("screen", screen_path, sizeof(screen_path))) { + fprintf(stderr, "hybbxd: GNU screen not found in PATH\n"); + return HYBBX_ERR_IO; + } + + args[n++] = screen_path; + args[n++] = "-r"; + args[n++] = (char *)session; + args[n++] = NULL; + + execv(screen_path, args); + fprintf(stderr, "hybbxd: screen attach failed\n"); + return HYBBX_ERR_IO; +} + +static hybbx_result_t exec_tmux_attach(const char *session) +{ + char tmux_path[HYBBX_PATH_MAX]; + char *args[8]; + int n = 0; + + if (!find_in_path("tmux", tmux_path, sizeof(tmux_path))) { + fprintf(stderr, "hybbxd: tmux not found in PATH\n"); + return HYBBX_ERR_IO; + } + + args[n++] = tmux_path; + args[n++] = "attach"; + args[n++] = "-t"; + args[n++] = (char *)session; + args[n++] = NULL; + + execv(tmux_path, args); + fprintf(stderr, "hybbxd: tmux attach failed\n"); + return HYBBX_ERR_IO; +} + +static hybbx_result_t spawn_screen(const char *session, const char *binary, + int argc, char **argv) +{ + char screen_path[HYBBX_PATH_MAX]; + char *inner[HYBBX_CMD_TOKEN_MAX + 4]; + char wrap_env[64]; + int innerc; + char *args[HYBBX_CMD_TOKEN_MAX + 8]; + int n = 0; + + if (!find_in_path("screen", screen_path, sizeof(screen_path))) { + fprintf(stderr, "hybbxd: GNU screen not found in PATH\n"); + return HYBBX_ERR_IO; + } + + innerc = build_inner_argv(inner, (int)(sizeof(inner) / sizeof(inner[0])), + binary, argc, argv); + if (innerc < 0) { + return HYBBX_ERR_INVALID; + } + + snprintf(wrap_env, sizeof(wrap_env), "%s=1", HYBBX_DAEMON_WRAP_ENV); + + args[n++] = screen_path; + args[n++] = "-dmS"; + args[n++] = (char *)session; + args[n++] = "env"; + args[n++] = wrap_env; + { + int i; + + for (i = 0; i < innerc; i++) { + args[n++] = inner[i]; + } + } + args[n++] = NULL; + + execv(screen_path, args); + fprintf(stderr, "hybbxd: screen spawn failed\n"); + return HYBBX_ERR_IO; +} + +static hybbx_result_t spawn_tmux(const char *session, const char *binary, + int argc, char **argv) +{ + char tmux_path[HYBBX_PATH_MAX]; + char *inner[HYBBX_CMD_TOKEN_MAX + 4]; + char wrap_env[64]; + int innerc; + char *args[HYBBX_CMD_TOKEN_MAX + 8]; + int n = 0; + + if (!find_in_path("tmux", tmux_path, sizeof(tmux_path))) { + fprintf(stderr, "hybbxd: tmux not found in PATH\n"); + return HYBBX_ERR_IO; + } + + innerc = build_inner_argv(inner, (int)(sizeof(inner) / sizeof(inner[0])), + binary, argc, argv); + if (innerc < 0) { + return HYBBX_ERR_INVALID; + } + + snprintf(wrap_env, sizeof(wrap_env), "%s=1", HYBBX_DAEMON_WRAP_ENV); + + args[n++] = tmux_path; + args[n++] = "new-session"; + args[n++] = "-d"; + args[n++] = "-s"; + args[n++] = (char *)session; + args[n++] = "env"; + args[n++] = wrap_env; + { + int i; + + for (i = 0; i < innerc; i++) { + args[n++] = inner[i]; + } + } + args[n++] = NULL; + + execv(tmux_path, args); + fprintf(stderr, "hybbxd: tmux spawn failed\n"); + return HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_daemon_apply_launch_opts(const hybbx_daemon_launch_opts_t *opts, + const char *binary_path, + int argc, char **argv) +{ + const char *binary = binary_path; + + if (opts == NULL) { + return HYBBX_ERR_INVALID; + } + + if (binary == NULL || binary[0] == '\0') { + binary = HYBBX_DAEMON_BINARY; + } + + if (opts->use_screen && opts->use_tmux) { + fprintf(stderr, "hybbxd: use --screen or --tmux, not both\n"); + return HYBBX_ERR_INVALID; + } + + if (opts->attach) { + if (opts->use_screen) { + return exec_screen_attach(opts->session); + } + if (opts->use_tmux) { + return exec_tmux_attach(opts->session); + } + + fprintf(stderr, "hybbxd: --attach requires --screen or --tmux\n"); + return HYBBX_ERR_INVALID; + } + + if (wrapped_child() || opts->foreground) { + return HYBBX_OK; + } + + if (opts->use_screen) { + if (in_screen()) { + return HYBBX_OK; + } + + if (spawn_screen(opts->session, binary, argc, argv) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + printf("hybbxd: started in screen session '%s'\n", opts->session); + printf("attach: screen -r %s\n", opts->session); + exit(EXIT_SUCCESS); + } + + if (opts->use_tmux) { + if (in_tmux()) { + return HYBBX_OK; + } + + if (spawn_tmux(opts->session, binary, argc, argv) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + printf("hybbxd: started in tmux session '%s'\n", opts->session); + printf("attach: tmux attach -t %s\n", opts->session); + exit(EXIT_SUCCESS); + } + + return HYBBX_OK; +} diff --git a/src/core/instance.c b/src/core/instance.c new file mode 100644 index 0000000..4640678 --- /dev/null +++ b/src/core/instance.c @@ -0,0 +1,272 @@ +#include "hybbx/instance.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include <string.h> + +static int g_instance_standalone; + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +#if defined(__GNUC__) || defined(__clang__) +__attribute__((weak)) +#endif +hybbx_instance_role_t hybbx_instance_role(void) +{ + /* Overridden by instance_role_{main,secondary,proxy}.c on each binary. */ + return HYBBX_INSTANCE_MAIN; +} + +const char *hybbx_instance_binary_name(void) +{ + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_SECONDARY: + return "hybbxsd"; + case HYBBX_INSTANCE_PROXY: + return "hybbxpd"; + case HYBBX_INSTANCE_MAIN: + default: + return "hybbxd"; + } +} + +const char *hybbx_instance_role_name(void) +{ + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_SECONDARY: + return "Secondary"; + case HYBBX_INSTANCE_PROXY: + return "Proxy"; + case HYBBX_INSTANCE_MAIN: + default: + return "Main"; + } +} + +int hybbx_instance_offers_user_bbx(void) +{ + return hybbx_instance_role() == HYBBX_INSTANCE_MAIN; +} + +void hybbx_instance_set_standalone(int enabled) +{ + g_instance_standalone = enabled != 0; +} + +int hybbx_instance_standalone(void) +{ + return g_instance_standalone; +} + +static int instance_main_standalone(void) +{ + return hybbx_instance_role() == HYBBX_INSTANCE_MAIN && + hybbx_instance_standalone(); +} + +int hybbx_instance_plugin_allowed(const char *plugin_name) +{ + hybbx_instance_role_t role; + + if (plugin_name == NULL || plugin_name[0] == '\0') { + return 0; + } + + role = hybbx_instance_role(); + + if (instance_main_standalone()) { + if (str_ieq(plugin_name, "circuit")) { + return 1; + } + if (str_ieq(plugin_name, "mains_proxy")) { + return 1; + } + return 1; + } + + /* Split layout: packet_radio stays in HyBBX only via MAX25 prep on Secondary/Proxy; + * hub Main uses circuit peers instead. */ + if (str_ieq(plugin_name, "packet_radio")) { + return role != HYBBX_INSTANCE_MAIN; + } + + switch (role) { + case HYBBX_INSTANCE_MAIN: + if (str_ieq(plugin_name, "baycom") || + str_ieq(plugin_name, "ardop") || + str_ieq(plugin_name, "crdop")) { + return 0; + } + if (str_ieq(plugin_name, "telnet") || str_ieq(plugin_name, "ssh")) { + return 0; + } + return 1; + + case HYBBX_INSTANCE_PROXY: + if (str_ieq(plugin_name, "websocket")) { + return 0; + } + return 1; + + case HYBBX_INSTANCE_SECONDARY: + if (str_ieq(plugin_name, "telnet") || + str_ieq(plugin_name, "ssh") || + str_ieq(plugin_name, "websocket")) { + return 0; + } + return 1; + + default: + return 0; + } +} + +hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks) +{ + hybbx_instance_role_t role; + + if (networks == NULL) { + return HYBBX_ERR_INVALID; + } + + role = hybbx_instance_role(); + + if (instance_main_standalone()) { + hybbx_log_info("[instance] standalone=yes — %s keeps INI transports " + "(local RF via MAX25 prep when configured)", + hybbx_instance_binary_name()); + hybbx_log_info("[instance] binary=%s role=%s standalone=yes user_bbx=%s " + "telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s crdop=%s " + "websocket=%s circuit=%s mains_proxy=%s", + hybbx_instance_binary_name(), + hybbx_instance_role_name(), + hybbx_instance_offers_user_bbx() ? "yes" : "no", + hybbx_bool_to_string(networks->telnet), + hybbx_bool_to_string(networks->ssh), + hybbx_bool_to_string(networks->ax25), + hybbx_bool_to_string(networks->baycom), + hybbx_bool_to_string(networks->ardop), + hybbx_bool_to_string(networks->crdop), + hybbx_bool_to_string(networks->websocket), + hybbx_bool_to_string(networks->circuit), + hybbx_bool_to_string(networks->mains_proxy)); + return HYBBX_OK; + } + + if (networks->ax25 && role == HYBBX_INSTANCE_MAIN) { + hybbx_log_warn("[instance] %s: ax25/packet_radio on hub Main — forcing off " + "(use standalone=yes or Secondary RF host)", + hybbx_instance_binary_name()); + networks->ax25 = 0; + } + + switch (role) { + case HYBBX_INSTANCE_MAIN: + if (networks->baycom || networks->ardop || networks->crdop) { + hybbx_log_warn("[instance] hybbxd: RF adapters forbidden on Main " + "— forcing baycom/ardop/crdop off (use hybbxpd + MAX25)"); + networks->baycom = 0; + networks->ardop = 0; + networks->crdop = 0; + } + if (networks->ssh) { + hybbx_log_warn("[instance] hybbxd: SSH user path forbidden — " + "WebSocket/reverse-proxy only; forcing ssh=no"); + networks->ssh = 0; + } + if (networks->telnet) { + hybbx_log_warn("[instance] hybbxd: telnet user path forbidden — " + "WebSocket/reverse-proxy only; forcing telnet=no"); + networks->telnet = 0; + } + /* WebSocket is the sole remote user path. */ + if (!networks->websocket) { + hybbx_log_warn("[instance] hybbxd: enabling websocket " + "(required user path)"); + networks->websocket = 1; + } + /* Circuit hub for Proxy attach. */ + if (!networks->circuit) { + hybbx_log_warn("[instance] hybbxd: enabling circuit hub " + "(Proxy in/out)"); + networks->circuit = 1; + } + break; + + case HYBBX_INSTANCE_SECONDARY: + if (networks->telnet) { + hybbx_log_warn("[instance] hybbxsd: no user telnet — forcing off"); + networks->telnet = 0; + } + if (networks->websocket) { + hybbx_log_warn("[instance] hybbxsd: no user WebSocket — forcing off"); + networks->websocket = 0; + } + if (networks->ssh) { + hybbx_log_warn("[instance] hybbxsd: no user SSH — forcing off"); + networks->ssh = 0; + } + /* Peer other Mains — mains_proxy required. */ + if (!networks->mains_proxy) { + hybbx_log_warn("[instance] hybbxsd: enabling mains_proxy " + "(Secondary = other-Main peers)"); + networks->mains_proxy = 1; + } + break; + + case HYBBX_INSTANCE_PROXY: + if (networks->websocket) { + hybbx_log_warn("[instance] hybbxpd: WebSocket forbidden — forcing off"); + networks->websocket = 0; + } + if (networks->baycom) { + hybbx_log_warn("[instance] hybbxpd: baycom enabled — prefer MAX25 " + "relay/gateway/digipeater for hyBBX RF"); + } + if (!networks->circuit) { + hybbx_log_warn("[instance] hybbxpd: enabling circuit " + "(attach toward Main)"); + networks->circuit = 1; + } + break; + + default: + return HYBBX_ERR_INVALID; + } + + hybbx_log_info("[instance] binary=%s role=%s user_bbx=%s " + "telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s crdop=%s " + "websocket=%s circuit=%s mains_proxy=%s", + hybbx_instance_binary_name(), + hybbx_instance_role_name(), + hybbx_instance_offers_user_bbx() ? "yes" : "no", + hybbx_bool_to_string(networks->telnet), + hybbx_bool_to_string(networks->ssh), + hybbx_bool_to_string(networks->ax25), + hybbx_bool_to_string(networks->baycom), + hybbx_bool_to_string(networks->ardop), + hybbx_bool_to_string(networks->crdop), + hybbx_bool_to_string(networks->websocket), + hybbx_bool_to_string(networks->circuit), + hybbx_bool_to_string(networks->mains_proxy)); + + return HYBBX_OK; +} diff --git a/src/core/link.c b/src/core/link.c new file mode 100644 index 0000000..b9a38cd --- /dev/null +++ b/src/core/link.c @@ -0,0 +1,427 @@ +/* + * Link/repeater edge registry: data/links/<id>.ini, [link.<id>], stale prune. + */ +#include "hybbx/link.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <dirent.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> + +static char *link_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static const char *LINK_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +void hybbx_link_generate_code(char *out, size_t out_cap) +{ + static unsigned seed; + size_t i; + size_t n; + + if (out == NULL || out_cap < 2) { + return; + } + + if (seed == 0) { + seed = (unsigned)time(NULL) ^ 0x48594242u; + } + + n = out_cap - 1; + if (n > HYBBX_LINK_CODE_MAX - 1) { + n = HYBBX_LINK_CODE_MAX - 1; + } + + for (i = 0; i < n; i++) { + seed = seed * 1103515245u + 12345u; + out[i] = LINK_CODE_CHARS[(seed >> 16) % 32]; + } + out[n] = '\0'; +} + +void hybbx_link_registry_init(hybbx_link_registry_t *reg, + const char *data_dir, + const char *config_path, + unsigned stale_days) +{ + if (reg == NULL) { + return; + } + + memset(reg, 0, sizeof(*reg)); + if (data_dir != NULL && data_dir[0] != '\0') { + snprintf(reg->dir, sizeof(reg->dir), "%s/links", data_dir); + } + if (config_path != NULL) { + hybbx_strlcpy(reg->config_path, config_path, sizeof(reg->config_path)); + } + reg->stale_days = stale_days > 0 ? stale_days : HYBBX_LINK_STALE_DAYS; +} + +static hybbx_result_t link_shard_path(const hybbx_link_registry_t *reg, + const char *id, + char *out, size_t out_cap) +{ + if (reg == NULL || id == NULL || id[0] == '\0' || out == NULL) { + return HYBBX_ERR_INVALID; + } + + if (snprintf(out, out_cap, "%s/%s.ini", reg->dir, id) >= (int)out_cap) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static hybbx_result_t read_link_last_seen(const char *path, time_t *last_seen) +{ + FILE *fp; + char line[256]; + time_t value = 0; + + if (path == NULL || last_seen == NULL) { + return HYBBX_ERR_INVALID; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + *last_seen = 0; + return HYBBX_OK; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + if (strncmp(line, "last_seen=", 10) == 0) { + value = (time_t)strtol(line + 10, NULL, 10); + } + } + + fclose(fp); + *last_seen = value; + return HYBBX_OK; +} + +static hybbx_result_t write_link_shard(const char *path, + const char *id, + const char *role, + const char *link_code, + time_t now, + int is_new) +{ + FILE *fp; + time_t created = now; + char existing_code[HYBBX_LINK_CODE_MAX]; + + existing_code[0] = '\0'; + if (!is_new) { + FILE *old = fopen(path, "r"); + char line[256]; + + if (old != NULL) { + while (fgets(line, sizeof(line), old) != NULL) { + if (strncmp(line, "created=", 8) == 0) { + created = (time_t)strtol(line + 8, NULL, 10); + } else if (strncmp(line, "link_code=", 10) == 0) { + size_t n = strcspn(line + 10, "\r\n"); + if (n >= sizeof(existing_code)) { + n = sizeof(existing_code) - 1; + } + memcpy(existing_code, line + 10, n); + existing_code[n] = '\0'; + } + } + fclose(old); + } + } + + if (link_code != NULL && link_code[0] != '\0') { + hybbx_strlcpy(existing_code, link_code, sizeof(existing_code)); + } else if (existing_code[0] == '\0') { + hybbx_link_generate_code(existing_code, sizeof(existing_code)); + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "id=%s\n", id); + fprintf(fp, "role=%s\n", role != NULL && role[0] != '\0' ? role : "link"); + fprintf(fp, "link_code=%s\n", existing_code); + fprintf(fp, "created=%ld\n", (long)created); + fprintf(fp, "last_seen=%ld\n", (long)now); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t config_upsert_link_section(const char *config_path, + const char *id, + const char *role, + const char *link_code, + time_t last_seen) +{ + hybbx_config_t cfg; + char section[HYBBX_CONFIG_SECTION_MAX]; + char value[64]; + size_t i; + hybbx_result_t rc; + int found_role = 0; + int found_code = 0; + int found_seen = 0; + + if (config_path == NULL || config_path[0] == '\0' || id == NULL) { + return HYBBX_OK; + } + + rc = hybbx_config_load(&cfg, config_path); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(section, sizeof(section), "link.%s", id); + snprintf(value, sizeof(value), "%ld", (long)last_seen); + + for (i = 0; i < cfg.count; i++) { + if (cfg.entries[i].section == NULL || cfg.entries[i].key == NULL) { + continue; + } + if (strcmp(cfg.entries[i].section, section) != 0) { + continue; + } + if (strcmp(cfg.entries[i].key, "role") == 0) { + free(cfg.entries[i].value); + cfg.entries[i].value = link_strdup(role != NULL ? role : "link"); + found_role = 1; + } else if (strcmp(cfg.entries[i].key, "link_code") == 0) { + free(cfg.entries[i].value); + cfg.entries[i].value = link_strdup(link_code != NULL ? link_code : ""); + found_code = 1; + } else if (strcmp(cfg.entries[i].key, "last_seen") == 0) { + free(cfg.entries[i].value); + cfg.entries[i].value = link_strdup(value); + found_seen = 1; + } + } + + if (!found_role) { + rc = hybbx_config_set(&cfg, section, "role", + role != NULL ? role : "link"); + if (rc != HYBBX_OK) { + hybbx_config_free(&cfg); + return rc; + } + } + if (!found_code && link_code != NULL) { + rc = hybbx_config_set(&cfg, section, "link_code", link_code); + if (rc != HYBBX_OK) { + hybbx_config_free(&cfg); + return rc; + } + } + if (!found_seen) { + rc = hybbx_config_set(&cfg, section, "last_seen", value); + if (rc != HYBBX_OK) { + hybbx_config_free(&cfg); + return rc; + } + } + + rc = hybbx_config_save(&cfg, config_path); + hybbx_config_free(&cfg); + return rc; +} + +hybbx_result_t hybbx_link_registry_touch(hybbx_link_registry_t *reg, + const char *id, + const char *role, + char *link_code_out, + size_t link_code_cap) +{ + char path[HYBBX_PATH_MAX]; + char code[HYBBX_LINK_CODE_MAX]; + time_t now = time(NULL); + struct stat st; + hybbx_result_t rc; + int is_new; + + if (reg == NULL || id == NULL || id[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (reg->dir[0] != '\0') { + mkdir_p(reg->dir); + } + + rc = link_shard_path(reg, id, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + is_new = stat(path, &st) != 0; + code[0] = '\0'; + if (!is_new) { + FILE *fp = fopen(path, "r"); + char line[256]; + + if (fp != NULL) { + while (fgets(line, sizeof(line), fp) != NULL) { + if (strncmp(line, "link_code=", 10) == 0) { + size_t n = strcspn(line + 10, "\r\n"); + if (n >= sizeof(code)) { + n = sizeof(code) - 1; + } + memcpy(code, line + 10, n); + code[n] = '\0'; + } + } + fclose(fp); + } + } + if (code[0] == '\0') { + hybbx_link_generate_code(code, sizeof(code)); + } + + rc = write_link_shard(path, id, role, code, now, is_new); + if (rc != HYBBX_OK) { + return rc; + } + + if (link_code_out != NULL && link_code_cap > 0) { + hybbx_strlcpy(link_code_out, code, link_code_cap); + } + + return config_upsert_link_section(reg->config_path, id, role, code, now); +} + +static hybbx_result_t remove_link_files(const hybbx_link_registry_t *reg, + const char *id) +{ + char path[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + rc = link_shard_path(reg, id, path, sizeof(path)); + if (rc == HYBBX_OK) { + remove(path); + } + + if (reg->config_path[0] != '\0') { + hybbx_config_t cfg; + char section[HYBBX_CONFIG_SECTION_MAX]; + + snprintf(section, sizeof(section), "link.%s", id); + if (hybbx_config_load(&cfg, reg->config_path) == HYBBX_OK) { + hybbx_config_remove_section(&cfg, section); + hybbx_config_save(&cfg, reg->config_path); + hybbx_config_free(&cfg); + } + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_link_registry_prune(hybbx_link_registry_t *reg) +{ + DIR *dir; + struct dirent *ent; + time_t now = time(NULL); + time_t cutoff; + char id[HYBBX_LINK_ID_MAX]; + + if (reg == NULL || reg->dir[0] == '\0') { + return HYBBX_OK; + } + + cutoff = now - (time_t)reg->stale_days * 24 * 60 * 60; + + dir = opendir(reg->dir); + if (dir == NULL) { + return HYBBX_OK; + } + + while ((ent = readdir(dir)) != NULL) { + const char *name = ent->d_name; + size_t nlen = strlen(name); + time_t last_seen; + char path[HYBBX_PATH_MAX]; + + if (nlen < 5 || strcmp(name + nlen - 4, ".ini") != 0) { + continue; + } + + nlen -= 4; + if (nlen >= sizeof(id)) { + continue; + } + memcpy(id, name, nlen); + id[nlen] = '\0'; + + if (snprintf(path, sizeof(path), "%s/%s", reg->dir, name) >= (int)sizeof(path)) { + continue; + } + + if (read_link_last_seen(path, &last_seen) != HYBBX_OK) { + continue; + } + + if (last_seen > 0 && last_seen < cutoff) { + hybbx_log_info("[links] removing stale link '%s' (last auth %ld days ago)", + id, (long)((now - last_seen) / (24 * 60 * 60))); + remove_link_files(reg, id); + } + } + + closedir(dir); + return HYBBX_OK; +} diff --git a/src/core/log.c b/src/core/log.c new file mode 100644 index 0000000..7812c00 --- /dev/null +++ b/src/core/log.c @@ -0,0 +1,694 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/log.h" +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/config.h" +#endif +#include "hybbx/limits.h" +#include "hybbx/util.h" + +#include <dirent.h> +#include <errno.h> +#include <pthread.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> + +#define HYBBX_LOG_LINE_MAX 1024u + +/* + * Weekly HyBBX file log (replaces stuck month-keyed "daily" open handle). + * + * Naming: YYYYMMDD-week-hybbx.log + * YYYYMMDD = ISO week start (Monday, local time). One file per 7-day week. + * + * Cycle (forever): + * Days 1–7 → append to the current week file. + * Day 8 → switch to the new week file (new Monday), then pack every + * closed/legacy packable log under [log] dir into + * logs/arc/<YYYYMMDD>-week-hybbx.tar.bz2 (bzip2) and delete + * those sources only after tar succeeds. + * Pack batches of up to 7 members per archive (oldest first); repeat until + * fewer than 1 packable remain. Legacy daily names (YYYYMMDD-hybbx.log) + * are packable and are swept on day-8 switch (and on startup when ≥7). + * + * security.log is never packed or deleted here. + */ + +#define HYBBX_LOG_ARC_SUBDIR "arc" +#define HYBBX_LOG_PACK_BATCH 7 +#define HYBBX_LOG_PACKABLE_MAX 64 + +static hybbx_log_config_t g_log_config; +static int g_log_ready; +static FILE *g_log_file; +/* ISO-Monday yyyymmdd of the file currently open (0 = none). */ +static int g_log_open_week_yyyymmdd; +static char g_log_open_name[64]; +static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER; + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + hybbx_strlcpy(buf, path, sizeof(buf)); + len = strlen(buf); + while (len > 0 && buf[len - 1] == '/') { + buf[--len] = '\0'; + } + + for (i = 1; i < len; i++) { + if (buf[i] != '/') { + continue; + } + buf[i] = '\0'; + if (buf[0] != '\0' && mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +void hybbx_log_config_defaults(hybbx_log_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->enabled = 1; + cfg->dir[0] = '\0'; + cfg->level = HYBBX_LOG_WARN; +} + +hybbx_log_level_t hybbx_log_parse_level(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_LOG_WARN; + } + + if (strcasecmp(value, "debug") == 0) { + return HYBBX_LOG_DEBUG; + } + if (strcasecmp(value, "stats") == 0) { + return HYBBX_LOG_STATS; + } + if (strcasecmp(value, "info") == 0) { + return HYBBX_LOG_INFO; + } + if (strcasecmp(value, "warn") == 0) { + return HYBBX_LOG_WARN; + } + + return HYBBX_LOG_WARN; +} + +const char *hybbx_log_level_name(hybbx_log_level_t level) +{ + switch (level) { + case HYBBX_LOG_DEBUG: + return "debug"; + case HYBBX_LOG_STATS: + return "stats"; + case HYBBX_LOG_INFO: + return "info"; + case HYBBX_LOG_WARN: + return "warn"; + default: + return "?"; + } +} + +int hybbx_log_level_visible(hybbx_log_level_t level) +{ + if (!g_log_ready) { + return 1; + } + + return level >= g_log_config.level; +} + +const hybbx_log_config_t *hybbx_log_config_get(void) +{ + return g_log_ready ? &g_log_config : NULL; +} + +int hybbx_log_enabled(void) +{ + return g_log_ready && g_log_config.enabled && g_log_file != NULL; +} + +static void log_close_file(void) +{ + if (g_log_file != NULL) { + fclose(g_log_file); + g_log_file = NULL; + } + g_log_open_week_yyyymmdd = 0; + g_log_open_name[0] = '\0'; +} + +/* Monday of the ISO week containing @p tm (local wall calendar). */ +static int log_week_start_tm(const struct tm *tm, struct tm *out) +{ + time_t t; + int from_monday; + + if (tm == NULL || out == NULL) { + return -1; + } + + *out = *tm; + /* tm_wday: 0=Sun … 6=Sat → days since Monday */ + from_monday = (out->tm_wday + 6) % 7; + out->tm_mday -= from_monday; + out->tm_hour = 12; /* avoid DST midnight edge when normalizing */ + out->tm_min = 0; + out->tm_sec = 0; + out->tm_isdst = -1; + t = mktime(out); + if (t == (time_t)-1) { + return -1; + } + if (localtime_r(&t, out) == NULL) { + return -1; + } + return 0; +} + +static int log_week_yyyymmdd(const struct tm *tm) +{ + struct tm week; + + if (log_week_start_tm(tm, &week) != 0) { + return 0; + } + return (week.tm_year + 1900) * 10000 + + (week.tm_mon + 1) * 100 + + week.tm_mday; +} + +static void log_week_basename(int week_yyyymmdd, char *name, size_t name_len) +{ + snprintf(name, name_len, "%08d-week-hybbx.log", week_yyyymmdd); +} + +static int log_build_path_for_week(char *out, size_t out_len, int week_yyyymmdd) +{ + char name[64]; + + log_week_basename(week_yyyymmdd, name, sizeof(name)); + return hybbx_path_join(out, out_len, g_log_config.dir, name) == HYBBX_OK + ? 0 + : -1; +} + +static int log_is_eight_digits(const char *s) +{ + size_t i; + + for (i = 0; i < 8; i++) { + if (s[i] < '0' || s[i] > '9') { + return 0; + } + } + return 1; +} + +/* + * Packable: current-scheme week files, or legacy daily YYYYMMDD-hybbx.log. + * Never security.log, arc/, or the active week file. + */ +static int log_name_is_packable(const char *name) +{ + size_t len; + + if (name == NULL || name[0] == '\0' || name[0] == '.') { + return 0; + } + if (strcmp(name, g_log_open_name) == 0) { + return 0; + } + + len = strlen(name); + if (len == 22 && log_is_eight_digits(name) && + strcmp(name + 8, "-week-hybbx.log") == 0) { + return 1; + } + /* Legacy daily: 20260720-hybbx.log (8 digits + 10). */ + if (len == 18 && log_is_eight_digits(name) && + strcmp(name + 8, "-hybbx.log") == 0) { + return 1; + } + return 0; +} + +static int log_cmp_names(const void *a, const void *b) +{ + return strcmp(*(const char *const *)a, *(const char *const *)b); +} + +static int log_run_tar_bz2(const char *arc_path, char **basenames, int count) +{ + pid_t pid; + int status; + int i; + char *argv[HYBBX_LOG_PACKABLE_MAX + 8]; + int argc = 0; + + if (arc_path == NULL || basenames == NULL || count <= 0 || + count > HYBBX_LOG_PACKABLE_MAX) { + return -1; + } + + argv[argc++] = "tar"; + argv[argc++] = "-C"; + argv[argc++] = (char *)g_log_config.dir; + argv[argc++] = "-cjf"; + argv[argc++] = (char *)arc_path; + for (i = 0; i < count; i++) { + argv[argc++] = basenames[i]; + } + argv[argc] = NULL; + + pid = fork(); + if (pid < 0) { + return -1; + } + if (pid == 0) { + execvp("tar", argv); + _exit(127); + } + + if (waitpid(pid, &status, 0) < 0) { + return -1; + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + return -1; + } + return 0; +} + +static int log_collect_packable(char namebuf[][64], char **names, int max) +{ + DIR *dir; + struct dirent *ent; + int count = 0; + + dir = opendir(g_log_config.dir); + if (dir == NULL) { + return 0; + } + + while ((ent = readdir(dir)) != NULL && count < max) { + if (!log_name_is_packable(ent->d_name)) { + continue; + } + if (strlen(ent->d_name) >= 64) { + continue; + } + hybbx_strlcpy(namebuf[count], ent->d_name, 64); + names[count] = namebuf[count]; + count++; + } + closedir(dir); + return count; +} + +/* Pack up to HYBBX_LOG_PACK_BATCH oldest packable files. Returns packed count. */ +static int log_archive_one_batch(void) +{ + char *names[HYBBX_LOG_PACKABLE_MAX]; + char namebuf[HYBBX_LOG_PACKABLE_MAX][64]; + int count; + int pack_n; + int i; + char arc_dir[HYBBX_PATH_MAX]; + char arc_path[HYBBX_PATH_MAX]; + char arc_name[80]; + struct stat st; + + count = log_collect_packable(namebuf, names, HYBBX_LOG_PACKABLE_MAX); + if (count <= 0) { + return 0; + } + + qsort(names, (size_t)count, sizeof(names[0]), log_cmp_names); + pack_n = count < HYBBX_LOG_PACK_BATCH ? count : HYBBX_LOG_PACK_BATCH; + + if (hybbx_path_join(arc_dir, sizeof(arc_dir), g_log_config.dir, + HYBBX_LOG_ARC_SUBDIR) != HYBBX_OK) { + fprintf(stderr, "[log] archive path too long\n"); + return -1; + } + if (mkdir_p(arc_dir) != 0) { + fprintf(stderr, "[log] cannot create %s\n", arc_dir); + return -1; + } + + snprintf(arc_name, sizeof(arc_name), "%.8s-week-hybbx.tar.bz2", names[0]); + if (hybbx_path_join(arc_path, sizeof(arc_path), arc_dir, arc_name) != + HYBBX_OK) { + fprintf(stderr, "[log] archive file path too long\n"); + return -1; + } + if (stat(arc_path, &st) == 0) { + snprintf(arc_name, sizeof(arc_name), + "%.8s-week-hybbx-%ld.tar.bz2", names[0], (long)time(NULL)); + if (hybbx_path_join(arc_path, sizeof(arc_path), arc_dir, arc_name) != + HYBBX_OK) { + return -1; + } + } + + if (log_run_tar_bz2(arc_path, names, pack_n) != 0) { + fprintf(stderr, "[log] tar.bz2 failed for %s — sources kept\n", + arc_path); + return -1; + } + + for (i = 0; i < pack_n; i++) { + char full[HYBBX_PATH_MAX]; + if (hybbx_path_join(full, sizeof(full), g_log_config.dir, names[i]) != + HYBBX_OK) { + continue; + } + if (unlink(full) != 0) { + fprintf(stderr, "[log] cannot remove packed %s\n", full); + } + } + + fprintf(stderr, "[log] archived %d file(s) → %s\n", pack_n, arc_path); + return pack_n; +} + +/* + * @p on_week_switch: day-8 path — pack every closed/legacy file (batches of 7). + * Otherwise (startup): pack only when ≥7 packable (legacy daily sweep). + */ +static void log_archive_packable(int on_week_switch) +{ + char *names[HYBBX_LOG_PACKABLE_MAX]; + char namebuf[HYBBX_LOG_PACKABLE_MAX][64]; + int count; + int guard; + + if (g_log_config.dir[0] == '\0') { + return; + } + + count = log_collect_packable(namebuf, names, HYBBX_LOG_PACKABLE_MAX); + if (count <= 0) { + return; + } + if (!on_week_switch && count < HYBBX_LOG_PACK_BATCH) { + return; + } + + for (guard = 0; guard < HYBBX_LOG_PACKABLE_MAX; guard++) { + int packed = log_archive_one_batch(); + if (packed <= 0) { + break; + } + count = log_collect_packable(namebuf, names, HYBBX_LOG_PACKABLE_MAX); + if (count <= 0) { + break; + } + if (!on_week_switch && count < HYBBX_LOG_PACK_BATCH) { + break; + } + } +} + +hybbx_result_t hybbx_log_current_path(char *out, size_t out_len) +{ + time_t now; + struct tm tm_buf; + struct tm *tm; + int week; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + out[0] = '\0'; + if (!g_log_ready || !g_log_config.enabled || g_log_config.dir[0] == '\0') { + return HYBBX_ERR_NOT_FOUND; + } + + now = time(NULL); + tm = localtime_r(&now, &tm_buf); + if (tm == NULL) { + return HYBBX_ERR_IO; + } + + week = log_week_yyyymmdd(tm); + if (week == 0 || log_build_path_for_week(out, out_len, week) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static int log_open_for_time(const struct tm *tm) +{ + char path[HYBBX_PATH_MAX]; + FILE *fp; + int week; + int rotated = 0; + + if (tm == NULL) { + return -1; + } + + week = log_week_yyyymmdd(tm); + if (week == 0) { + return -1; + } + + if (g_log_file != NULL && g_log_open_week_yyyymmdd == week) { + return 0; + } + + if (g_log_file != NULL) { + rotated = 1; + } + + log_close_file(); + + if (mkdir_p(g_log_config.dir) != 0) { + fprintf(stderr, "[log] cannot create directory %s\n", g_log_config.dir); + return -1; + } + + if (log_build_path_for_week(path, sizeof(path), week) != 0) { + fprintf(stderr, "[log] path too long for log file\n"); + return -1; + } + + fp = fopen(path, "a"); + if (fp == NULL) { + fprintf(stderr, "[log] cannot open %s\n", path); + return -1; + } + + g_log_file = fp; + g_log_open_week_yyyymmdd = week; + log_week_basename(week, g_log_open_name, sizeof(g_log_open_name)); + + /* Day-8 switch packs closed week (+ legacy). Startup sweeps if ≥7. */ + log_archive_packable(rotated); + + return 0; +} + +static int log_ensure_open(void) +{ + time_t now; + struct tm tm_buf; + struct tm *tm; + + if (!g_log_config.enabled || g_log_config.dir[0] == '\0') { + return -1; + } + + now = time(NULL); + tm = localtime_r(&now, &tm_buf); + if (tm == NULL) { + return -1; + } + + return log_open_for_time(tm); +} + +#if !defined(HYBBX_CLIENT_BUILD) +void hybbx_log_config_apply(const struct hybbx_config *config) +{ + const char *dir_raw; + const char *level_raw; + hybbx_log_level_t parsed; + + hybbx_log_shutdown(); + hybbx_log_config_defaults(&g_log_config); + + if (config != NULL) { + g_log_config.enabled = + hybbx_config_get_bool(config, "log", "enabled", 1); + dir_raw = hybbx_config_get(config, "log", "dir", NULL); + level_raw = hybbx_config_get(config, "log", "level", "warn"); + + parsed = hybbx_log_parse_level(level_raw); + if (level_raw != NULL && level_raw[0] != '\0' && + strcmp(level_raw, hybbx_log_level_name(parsed)) != 0 && + strcasecmp(level_raw, hybbx_log_level_name(parsed)) != 0) { + fprintf(stderr, + "[log] unknown level '%s' — using warn (debug|stats|info|warn)\n", + level_raw); + } + g_log_config.level = parsed; + + if (dir_raw != NULL && dir_raw[0] != '\0') { + if (hybbx_path_resolve(g_log_config.dir, sizeof(g_log_config.dir), + dir_raw) != HYBBX_OK) { + fprintf(stderr, "[log] invalid dir path\n"); + g_log_config.enabled = 0; + } + } else if (g_log_config.enabled) { + if (hybbx_path_resolve(g_log_config.dir, sizeof(g_log_config.dir), + HYBBX_DIR_LOGS) != HYBBX_OK) { + fprintf(stderr, "[log] cannot resolve default log directory\n"); + g_log_config.enabled = 0; + } + } + } else { + if (hybbx_path_resolve(g_log_config.dir, sizeof(g_log_config.dir), + HYBBX_DIR_LOGS) != HYBBX_OK) { + g_log_config.enabled = 0; + } + } + + g_log_ready = 1; + + if (g_log_config.enabled) { + if (log_ensure_open() != 0) { + g_log_config.enabled = 0; + } + } + + printf("[log] enabled=%s dir=%s level=%s week=%s\n", + hybbx_bool_to_string(g_log_config.enabled), + g_log_config.dir[0] != '\0' ? g_log_config.dir : "-", + hybbx_log_level_name(g_log_config.level), + g_log_open_name[0] != '\0' ? g_log_open_name : "-"); +} +#else +void hybbx_log_config_apply(const struct hybbx_config *config) +{ + (void)config; + hybbx_log_config_defaults(&g_log_config); + g_log_ready = 1; +} +#endif + +static void log_write_console(hybbx_log_level_t level, const char *message) +{ + FILE *out = (level == HYBBX_LOG_WARN) ? stderr : stdout; + + fputs(message, out); + fputc('\n', out); + fflush(out); +} + +static void log_write_file(hybbx_log_level_t level, const char *message) +{ + char line[HYBBX_LOG_LINE_MAX + 64]; + time_t now; + struct tm tm_buf; + struct tm *tm; + + if (!g_log_config.enabled) { + return; + } + + pthread_mutex_lock(&g_log_lock); + + if (log_ensure_open() != 0) { + pthread_mutex_unlock(&g_log_lock); + return; + } + + now = time(NULL); + tm = localtime_r(&now, &tm_buf); + if (tm == NULL) { + pthread_mutex_unlock(&g_log_lock); + return; + } + + if (hybbx_time_format_stamp(line, sizeof(line), tm, NULL) != HYBBX_OK) { + pthread_mutex_unlock(&g_log_lock); + return; + } + + { + size_t stamp_len = strlen(line); + snprintf(line + stamp_len, sizeof(line) - stamp_len, + " [%s] %s\n", hybbx_log_level_name(level), message); + } + + fputs(line, g_log_file); + fflush(g_log_file); + + pthread_mutex_unlock(&g_log_lock); +} + +void hybbx_log_write(hybbx_log_level_t level, const char *fmt, ...) +{ + char message[HYBBX_LOG_LINE_MAX]; + va_list ap; + + if (fmt == NULL) { + return; + } + + if (g_log_ready && !hybbx_log_level_visible(level)) { + return; + } + + va_start(ap, fmt); + vsnprintf(message, sizeof(message), fmt, ap); + va_end(ap); + + log_write_console(level, message); + if (g_log_ready) { + log_write_file(level, message); + } +} + +void hybbx_log_shutdown(void) +{ + pthread_mutex_lock(&g_log_lock); + log_close_file(); + g_log_ready = 0; + hybbx_log_config_defaults(&g_log_config); + pthread_mutex_unlock(&g_log_lock); +} diff --git a/src/core/mail.c b/src/core/mail.c new file mode 100644 index 0000000..b3aff97 --- /dev/null +++ b/src/core/mail.c @@ -0,0 +1,1532 @@ +#include "hybbx/mail.h" +#include "hybbx/messages.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/log.h" +#include "mail_sql.h" + +#include <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> + +#define HYBBX_MAIL_DIR_NAME "mail" +#define HYBBX_MAIL_INBOX_NAME "inbox" +#define HYBBX_MAIL_RECYCLE_NAME "recycle" +#define HYBBX_MAIL_NEXT_FILE "mail.next" + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static hybbx_result_t read_counter(const char *path, uint64_t *value) +{ + FILE *fp; + unsigned long long n = 0; + + fp = fopen(path, "r"); + if (fp == NULL) { + *value = 0; + return HYBBX_OK; + } + + if (fscanf(fp, "%llu", &n) != 1) { + fclose(fp); + return HYBBX_ERR_IO; + } + + fclose(fp); + *value = (uint64_t)n; + return HYBBX_OK; +} + +static hybbx_result_t write_counter(const char *path, uint64_t value) +{ + FILE *fp; + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%llu\n", (unsigned long long)value); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t mail_user_inbox_path(const hybbx_mail_config_t *mail, + const char *username, + char *out, size_t out_len) +{ + char user_dir[HYBBX_PATH_MAX]; + char user_norm[HYBBX_USER_NAME_MAX]; + + if (mail == NULL || username == NULL || username[0] == '\0' || + out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(user_norm, username, sizeof(user_norm)); + hybbx_username_normalize(user_norm); + + if (hybbx_path_join(user_dir, sizeof(user_dir), mail->root, user_norm) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, user_dir, HYBBX_MAIL_INBOX_NAME); +} + +static hybbx_result_t mail_user_recycle_path(const hybbx_mail_config_t *mail, + const char *username, + char *out, size_t out_len) +{ + char user_dir[HYBBX_PATH_MAX]; + char user_norm[HYBBX_USER_NAME_MAX]; + + if (mail == NULL || username == NULL || username[0] == '\0' || + out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(user_norm, username, sizeof(user_norm)); + hybbx_username_normalize(user_norm); + + if (hybbx_path_join(user_dir, sizeof(user_dir), mail->root, user_norm) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, user_dir, HYBBX_MAIL_RECYCLE_NAME); +} + +static int parse_msg_filename(const char *name, uint64_t *out_id) +{ + char *end; + unsigned long long id; + + if (name == NULL || out_id == NULL) { + return 0; + } + + if (strlen(name) < 5 || strcmp(name + strlen(name) - 4, ".msg") != 0) { + return 0; + } + + id = strtoull(name, &end, 10); + if (end == name || strcmp(end, ".msg") != 0) { + return 0; + } + + *out_id = (uint64_t)id; + return 1; +} + +static int mail_entry_compare(const void *a, const void *b) +{ + const hybbx_mail_entry_t *ea = (const hybbx_mail_entry_t *)a; + const hybbx_mail_entry_t *eb = (const hybbx_mail_entry_t *)b; + + if (ea->received_at > eb->received_at) { + return -1; + } + if (ea->received_at < eb->received_at) { + return 1; + } + if (ea->id > eb->id) { + return -1; + } + if (ea->id < eb->id) { + return 1; + } + return 0; +} + +static hybbx_result_t load_inbox(const char *inbox_path, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + DIR *dir; + struct dirent *ent; + size_t count = 0; + + if (inbox_path == NULL || entries == NULL || out_count == NULL) { + return HYBBX_ERR_INVALID; + } + + *out_count = 0; + + dir = opendir(inbox_path); + if (dir == NULL) { + if (errno == ENOENT) { + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + hybbx_mail_entry_t entry; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (count >= max_entries) { + break; + } + + memset(&entry, 0, sizeof(entry)); + entry.id = id; + + if (hybbx_path_join(path, sizeof(path), inbox_path, ent->d_name) != + HYBBX_OK) { + continue; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + continue; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char *eq; + char *key; + char *value; + + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + break; + } + + eq = strchr(line, '='); + if (eq == NULL) { + continue; + } + + *eq = '\0'; + key = line; + value = eq + 1; + + while (*value == ' ' || *value == '\t') { + value++; + } + + { + size_t vlen = strlen(value); + + while (vlen > 0 && (value[vlen - 1] == '\n' || + value[vlen - 1] == '\r')) { + value[--vlen] = '\0'; + } + } + + if (str_ieq(key, "from")) { + hybbx_strlcpy(entry.from, value, sizeof(entry.from)); + } else if (str_ieq(key, "subject")) { + hybbx_strlcpy(entry.subject, value, sizeof(entry.subject)); + } else if (str_ieq(key, "time")) { + entry.received_at = (time_t)strtol(value, NULL, 10); + } else if (str_ieq(key, "read")) { + entry.read = hybbx_bool_is_true(value); + } + } + + fclose(fp); + entries[count++] = entry; + } + + closedir(dir); + + if (count > 1) { + qsort(entries, count, sizeof(entries[0]), mail_entry_compare); + } + + *out_count = count; + return HYBBX_OK; +} + +static int mail_uses_sqlite(hybbx_service_t *service) +{ + hybbx_storage_t *storage; + + if (service == NULL) { + return 0; + } + + storage = hybbx_service_get_storage(service); + return storage != NULL && + hybbx_storage_backend(storage) == HYBBX_STORAGE_SQLITE; +} + +static hybbx_result_t mail_load_inbox(hybbx_service_t *service, + const hybbx_mail_config_t *mail, + const char *username, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + hybbx_storage_t *storage; + char inbox[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (mail_uses_sqlite(service)) { + storage = hybbx_service_get_storage(service); + return hybbx_mail_sql_load_inbox(storage, username, entries, + max_entries, out_count); + } + + rc = mail_user_inbox_path(mail, username, inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + return load_inbox(inbox, entries, max_entries, out_count); +} + +static hybbx_result_t msg_file_path(const char *inbox_path, uint64_t id, + char *out, size_t out_len) +{ + char name[32]; + + snprintf(name, sizeof(name), "%06llu.msg", + (unsigned long long)id); + return hybbx_path_join(out, out_len, inbox_path, name); +} + +void hybbx_mail_config_defaults(hybbx_mail_config_t *mail) +{ + if (mail == NULL) { + return; + } + + mail->enabled = 1; + mail->max_messages = HYBBX_MAIL_MAX_MESSAGES; + mail->subject_max = HYBBX_MAIL_SUBJECT_MAX; + mail->body_max = HYBBX_MAIL_BODY_MAX; + mail->recycle_days = HYBBX_MAIL_DEFAULT_RECYCLE_DAYS; + mail->root[0] = '\0'; +} + +void hybbx_mail_config_apply(hybbx_mail_config_t *mail, + const hybbx_config_t *config, + const char *storage_path) +{ + if (mail == NULL) { + return; + } + + hybbx_mail_config_defaults(mail); + + if (config != NULL) { + mail->enabled = hybbx_config_get_bool(config, "mail", "enabled", 1); + mail->max_messages = hybbx_config_get_uint( + config, "mail", "max_messages", HYBBX_MAIL_MAX_MESSAGES, 1u, + HYBBX_MAIL_MAX_MESSAGES); + mail->subject_max = hybbx_config_get_uint( + config, "mail", "subject_max", HYBBX_MAIL_SUBJECT_MAX, 8u, + HYBBX_MAIL_SUBJECT_MAX); + mail->body_max = hybbx_config_get_uint( + config, "mail", "body_max", HYBBX_MAIL_BODY_MAX, 64u, + HYBBX_MAIL_BODY_MAX); + mail->recycle_days = hybbx_config_get_uint( + config, "mail", "recycle_days", HYBBX_MAIL_DEFAULT_RECYCLE_DAYS, + 1u, 365u); + } + + mail->root[0] = '\0'; + if (storage_path != NULL && storage_path[0] != '\0') { + const char *subdir = "mail"; + const char *custom; + + if (config != NULL) { + custom = hybbx_config_get(config, "mail", "path", NULL); + if (custom != NULL && custom[0] != '\0') { + subdir = custom; + } + } + + if (hybbx_path_join(mail->root, sizeof(mail->root), storage_path, + subdir) != HYBBX_OK) { + mail->root[0] = '\0'; + } + } + + hybbx_log_info("[mail] enabled=%s max_messages=%u recycle_days=%u root=%s", + hybbx_bool_to_string(mail->enabled), mail->max_messages, + mail->recycle_days, + mail->root[0] != '\0' ? mail->root : "(unset)"); +} + +static hybbx_result_t mail_ensure_root(const hybbx_mail_config_t *mail) +{ + char next_path[HYBBX_PATH_MAX]; + + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (mail->root[0] == '\0') { + return HYBBX_ERR_IO; + } + + if (mkdir_p(mail->root) != 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(next_path, sizeof(next_path), mail->root, + HYBBX_MAIL_NEXT_FILE) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + { + FILE *fp = fopen(next_path, "r"); + + if (fp == NULL) { + return write_counter(next_path, 0); + } + fclose(fp); + } + + return HYBBX_OK; +} + +static hybbx_result_t mail_next_id(const hybbx_mail_config_t *mail, + uint64_t *out_id) +{ + char next_path[HYBBX_PATH_MAX]; + uint64_t id; + hybbx_result_t rc; + + if (hybbx_path_join(next_path, sizeof(next_path), mail->root, + HYBBX_MAIL_NEXT_FILE) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + rc = read_counter(next_path, &id); + if (rc != HYBBX_OK) { + return rc; + } + + id++; + rc = write_counter(next_path, id); + if (rc != HYBBX_OK) { + return rc; + } + + *out_id = id; + return HYBBX_OK; +} + +static hybbx_result_t mail_trim_inbox(hybbx_service_t *service, + const hybbx_mail_config_t *mail, + const char *username, + unsigned max_messages) +{ + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count; + size_t i; + hybbx_result_t rc; + char inbox[HYBBX_PATH_MAX]; + + if (mail_uses_sqlite(service)) { + return HYBBX_OK; + } + + rc = mail_user_inbox_path(mail, username, inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_inbox(inbox, entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (count <= max_messages) { + return HYBBX_OK; + } + + for (i = max_messages; i < count; i++) { + char path[HYBBX_PATH_MAX]; + + if (msg_file_path(inbox, entries[i].id, path, sizeof(path)) == + HYBBX_OK) { + remove(path); + } + } + + return HYBBX_OK; +} + +static hybbx_result_t mail_ensure_storage(hybbx_service_t *service, + const hybbx_mail_config_t *mail) +{ + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (mail_uses_sqlite(service)) { + return HYBBX_OK; + } + + return mail_ensure_root(mail); +} + +void hybbx_mail_list_inbox(hybbx_service_t *service, hybbx_session_t *session) +{ + hybbx_mail_list_inbox_range(service, session, 1, 0); +} + +static const char *mail_display_name(hybbx_service_t *service, + const char *name, + char *buf, size_t buf_len) +{ + hybbx_storage_t *storage; + hybbx_user_record_t user; + + if (name == NULL || name[0] == '\0') { + return ""; + } + + storage = hybbx_service_get_storage(service); + if (storage != NULL && + hybbx_storage_resolve_user(storage, name, &user) == HYBBX_OK) { + return hybbx_user_display_name(&user); + } + + hybbx_strlcpy(buf, name, buf_len); + return buf; +} + +static void mail_list_print(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_mail_entry_t *entries, + size_t count, + unsigned from, + unsigned to) +{ + size_t i; + size_t start; + size_t end; + char line[HYBBX_MAIL_SUBJECT_MAX + 64]; + char header[48]; + char from_name[HYBBX_USER_NAME_MAX]; + + if (to == 0 || to > count) { + to = (unsigned)count; + } + + if (count == 0) { + hybbx_session_write_line(session, "Inbox empty."); + hybbx_session_write_line(session, + "Use /mail send <user> <subject> to compose."); + return; + } + + if (from == 0 || from > count || from > to) { + hybbx_session_write_line(session, "No messages in that range."); + return; + } + + start = (size_t)(from - 1); + end = (size_t)to; + + if (from == 1 && to == (unsigned)count) { + hybbx_session_write_line(session, "Inbox (newest first):"); + } else { + snprintf(header, sizeof(header), "Inbox %u-%u (newest first):", + from, to); + hybbx_session_write_line(session, header); + } + + for (i = start; i < end; i++) { + snprintf(line, sizeof(line), " %zu %s%s %s", + i + 1, + entries[i].read ? " " : "* ", + mail_display_name(service, entries[i].from, from_name, + sizeof(from_name)), + entries[i].subject[0] != '\0' ? entries[i].subject + : "(no subject)"); + hybbx_session_write_line(session, line); + } + + hybbx_session_write_line(session, + " /mail read <n> delete <n|from-to> list <from-to> recycle"); +} + +static unsigned mail_recycle_purge_expired(const hybbx_mail_config_t *mail, + const char *recycle_path) +{ + DIR *dir; + struct dirent *ent; + time_t now = time(NULL); + time_t max_age; + unsigned removed = 0; + + if (mail == NULL || recycle_path == NULL || recycle_path[0] == '\0') { + return 0; + } + + max_age = (time_t)mail->recycle_days * 86400; + + dir = opendir(recycle_path); + if (dir == NULL) { + if (errno == ENOENT) { + return 0; + } + return 0; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + struct stat st; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (hybbx_path_join(path, sizeof(path), recycle_path, ent->d_name) != + HYBBX_OK) { + continue; + } + + if (stat(path, &st) != 0) { + continue; + } + + if (now - st.st_mtime >= max_age) { + if (remove(path) == 0) { + removed++; + } + } + } + + closedir(dir); + return removed; +} + +static hybbx_result_t mail_move_id_to_recycle(const char *inbox_path, + const char *recycle_path, + uint64_t id) +{ + char src[HYBBX_PATH_MAX]; + char dst[HYBBX_PATH_MAX]; + FILE *in_fp; + FILE *out_fp; + char line[HYBBX_LINE_MAX]; + char tmp[HYBBX_PATH_MAX + 8]; + int has_deleted = 0; + + if (msg_file_path(inbox_path, id, src, sizeof(src)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (mkdir_p(recycle_path) != 0) { + return HYBBX_ERR_IO; + } + + if (msg_file_path(recycle_path, id, dst, sizeof(dst)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (strlen(dst) + 4 >= sizeof(tmp)) { + return HYBBX_ERR_IO; + } + snprintf(tmp, sizeof(tmp), "%s.tmp", dst); + + in_fp = fopen(src, "r"); + out_fp = fopen(tmp, "w"); + if (in_fp == NULL || out_fp == NULL) { + if (in_fp != NULL) { + fclose(in_fp); + } + if (out_fp != NULL) { + fclose(out_fp); + } + remove(tmp); + return HYBBX_ERR_IO; + } + + while (fgets(line, sizeof(line), in_fp) != NULL) { + if (strncmp(line, "deleted=", 8) == 0) { + continue; + } + if (!has_deleted && line[0] == '-' && line[1] == '-' && line[2] == '-') { + fprintf(out_fp, "deleted=%ld\n", (long)time(NULL)); + has_deleted = 1; + } + fputs(line, out_fp); + } + + if (!has_deleted) { + fprintf(out_fp, "deleted=%ld\n", (long)time(NULL)); + } + + fclose(in_fp); + fclose(out_fp); + + if (remove(src) != 0) { + remove(tmp); + return HYBBX_ERR_IO; + } + + if (rename(tmp, dst) != 0) { + remove(tmp); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t mail_purge_user_recycle(hybbx_service_t *service, + const hybbx_mail_config_t *mail, + const char *username) +{ + char recycle[HYBBX_PATH_MAX]; + + if (mail_uses_sqlite(service)) { + (void)hybbx_mail_sql_purge_recycle(hybbx_service_get_storage(service), + mail, username); + return HYBBX_OK; + } + + if (mail_user_recycle_path(mail, username, recycle, sizeof(recycle)) != + HYBBX_OK) { + return HYBBX_ERR_IO; + } + + (void)mail_recycle_purge_expired(mail, recycle); + return HYBBX_OK; +} + +void hybbx_mail_list_inbox_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char inbox[HYBBX_PATH_MAX]; + size_t count; + hybbx_result_t rc; + + if (service == NULL || session == NULL) { + return; + } + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + hybbx_session_write_line(session, "Mail is disabled."); + return; + } + + rc = mail_ensure_storage(service, mail); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Mail storage unavailable."); + return; + } + + if (!mail_uses_sqlite(service)) { + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Mail path error."); + return; + } + } + + (void)mail_purge_user_recycle(service, mail, hybbx_session_username(session)); + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Cannot read inbox."); + return; + } + + mail_list_print(service, session, entries, count, from, to); +} + +void hybbx_mail_announce_since_last_login(hybbx_service_t *service, + hybbx_session_t *session, + time_t since_login) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char inbox[HYBBX_PATH_MAX]; + size_t count; + size_t i; + size_t new_count = 0; + hybbx_result_t rc; + + if (service == NULL || session == NULL) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return; + } + + rc = mail_ensure_storage(service, mail); + if (rc != HYBBX_OK) { + return; + } + + if (!mail_uses_sqlite(service)) { + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return; + } + } + + (void)mail_purge_user_recycle(service, mail, hybbx_session_username(session)); + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK || count == 0) { + return; + } + + for (i = 0; i < count; i++) { + if (since_login == 0 || entries[i].received_at > since_login) { + new_count++; + } + } + + if (new_count == 0) { + return; + } + + if (new_count == 1) { + (void)hybbx_msg_send_system(session, + "You have 1 new message since your last login. (/mail)"); + } else { + char body[96]; + snprintf(body, sizeof(body), + "You have %zu new messages since your last login. (/mail)", + new_count); + (void)hybbx_msg_send_system(session, body); + } +} + +int hybbx_mail_parse_list_range(const char *spec, + unsigned *from, unsigned *to) +{ + const char *dash; + char *end; + unsigned long start; + unsigned long end_num; + + if (from == NULL || to == NULL) { + return 0; + } + + if (spec == NULL || spec[0] == '\0') { + *from = 1; + *to = 0; + return 1; + } + + dash = strchr(spec, '-'); + if (dash == NULL) { + start = strtoul(spec, &end, 10); + if (end == spec || *end != '\0' || start == 0) { + return 0; + } + *from = (unsigned)start; + *to = (unsigned)start; + return 1; + } + + start = strtoul(spec, &end, 10); + if (end != dash || start == 0) { + return 0; + } + + end_num = strtoul(dash + 1, &end, 10); + if (end == dash + 1 || *end != '\0' || end_num == 0) { + return 0; + } + + if (start > end_num) { + return 0; + } + + *from = (unsigned)start; + *to = (unsigned)end_num; + return 1; +} + +static hybbx_result_t mail_resolve_index(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index, + uint64_t *out_id, + char *inbox_path, size_t inbox_len) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count; + hybbx_result_t rc; + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + rc = mail_ensure_storage(service, mail); + if (rc != HYBBX_OK) { + return rc; + } + + if (!mail_uses_sqlite(service)) { + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox_path, inbox_len); + if (rc != HYBBX_OK) { + return rc; + } + } + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (list_index == 0 || list_index > count) { + return HYBBX_ERR_NOT_FOUND; + } + + *out_id = entries[list_index - 1].id; + return HYBBX_OK; +} + +static void mail_print_header_line(hybbx_service_t *service, + hybbx_session_t *session, + const char *line) +{ + char out_line[HYBBX_LINE_MAX]; + char name_buf[HYBBX_USER_NAME_MAX]; + char fallback[HYBBX_USER_NAME_MAX]; + const char *value; + const char *display; + size_t vlen; + + if (line == NULL || session == NULL) { + return; + } + + if (strncmp(line, "from=", 5) == 0) { + value = line + 5; + vlen = strlen(value); + while (vlen > 0 && (value[vlen - 1] == '\n' || value[vlen - 1] == '\r')) { + vlen--; + } + memcpy(name_buf, value, vlen); + name_buf[vlen] = '\0'; + display = mail_display_name(service, name_buf, fallback, sizeof(fallback)); + snprintf(out_line, sizeof(out_line), "from=%s", display); + hybbx_session_write_line(session, out_line); + return; + } + + if (strncmp(line, "to=", 3) == 0) { + value = line + 3; + vlen = strlen(value); + while (vlen > 0 && (value[vlen - 1] == '\n' || value[vlen - 1] == '\r')) { + vlen--; + } + memcpy(name_buf, value, vlen); + name_buf[vlen] = '\0'; + display = mail_display_name(service, name_buf, fallback, sizeof(fallback)); + snprintf(out_line, sizeof(out_line), "to=%s", display); + hybbx_session_write_line(session, out_line); + return; + } + + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_mail_read(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + char inbox[HYBBX_PATH_MAX]; + char path[HYBBX_PATH_MAX]; + uint64_t id; + FILE *fp; + char line[HYBBX_LINE_MAX]; + int in_body = 0; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + if (mail_uses_sqlite(service)) { + (void)hybbx_mail_sql_purge_recycle(hybbx_service_get_storage(service), + hybbx_service_get_mail(service), + hybbx_session_username(session)); + return hybbx_mail_sql_read(service, session, list_index); + } + + (void)mail_purge_user_recycle(service, hybbx_service_get_mail(service), + hybbx_session_username(session)); + + rc = mail_resolve_index(service, session, list_index, &id, + inbox, sizeof(inbox)); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (msg_file_path(inbox, id, path, sizeof(path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + hybbx_session_write_line(session, "Cannot open message."); + return HYBBX_ERR_IO; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + if (!in_body) { + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + in_body = 1; + continue; + } + if (strncmp(line, "read=", 5) == 0) { + continue; + } + if (strncmp(line, "from=", 5) == 0 || + strncmp(line, "to=", 3) == 0) { + mail_print_header_line(service, session, line); + continue; + } + } + hybbx_session_write_line(session, line); + } + + fclose(fp); + + { + char tmp_path[HYBBX_PATH_MAX + 8]; + FILE *in_fp; + FILE *out_fp; + char buf[HYBBX_LINE_MAX]; + + if (strlen(path) + 4 >= sizeof(tmp_path)) { + return HYBBX_OK; + } + snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path); + in_fp = fopen(path, "r"); + out_fp = fopen(tmp_path, "w"); + if (in_fp != NULL && out_fp != NULL) { + while (fgets(buf, sizeof(buf), in_fp) != NULL) { + if (strncmp(buf, "read=", 5) == 0) { + fputs("read=yes\n", out_fp); + } else { + fputs(buf, out_fp); + } + } + fclose(in_fp); + fclose(out_fp); + remove(path); + rename(tmp_path, path); + } else { + if (in_fp != NULL) { + fclose(in_fp); + } + if (out_fp != NULL) { + fclose(out_fp); + } + remove(tmp_path); + } + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_delete_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char inbox[HYBBX_PATH_MAX]; + char recycle[HYBBX_PATH_MAX]; + size_t count; + size_t i; + unsigned moved = 0; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + hybbx_session_write_line(session, "Mail is disabled."); + return HYBBX_ERR_UNSUPPORTED; + } + + if (to == 0) { + to = from; + } + if (from == 0 || from > to) { + hybbx_session_write_line(session, "Usage: /mail delete <n|from-to>"); + return HYBBX_OK; + } + + if (mail_uses_sqlite(service)) { + return hybbx_mail_sql_delete_range(service, session, from, to); + } + + rc = mail_ensure_root(mail); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_recycle_path(mail, hybbx_session_username(session), + recycle, sizeof(recycle)); + if (rc != HYBBX_OK) { + return rc; + } + + (void)mail_purge_user_recycle(service, mail, hybbx_session_username(session)); + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (from > count || to > count) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + + for (i = (size_t)(from - 1); i < (size_t)to; i++) { + rc = mail_move_id_to_recycle(inbox, recycle, entries[i].id); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Delete failed."); + return rc; + } + moved++; + } + + if (moved == 1) { + hybbx_session_write_line(session, + "Message moved to recycle (auto-purge after configured days)."); + } else { + char buf[64]; + + snprintf(buf, sizeof(buf), + "%u messages moved to recycle (auto-purge after %u days).", + moved, mail->recycle_days); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_delete(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + return hybbx_mail_delete_range(service, session, list_index, list_index); +} + +hybbx_result_t hybbx_mail_recycle_empty(hybbx_service_t *service, + hybbx_session_t *session) +{ + const hybbx_mail_config_t *mail; + char recycle[HYBBX_PATH_MAX]; + DIR *dir; + struct dirent *ent; + unsigned removed = 0; + hybbx_result_t rc; + char buf[64]; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + hybbx_session_write_line(session, "Mail is disabled."); + return HYBBX_ERR_UNSUPPORTED; + } + + if (mail_uses_sqlite(service)) { + return hybbx_mail_sql_recycle_empty(service, session); + } + + rc = mail_ensure_root(mail); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_recycle_path(mail, hybbx_session_username(session), + recycle, sizeof(recycle)); + if (rc != HYBBX_OK) { + return rc; + } + + (void)mail_recycle_purge_expired(mail, recycle); + + dir = opendir(recycle); + if (dir == NULL) { + if (errno == ENOENT) { + hybbx_session_write_line(session, "Recycle bin empty."); + return HYBBX_OK; + } + hybbx_session_write_line(session, "Cannot read recycle bin."); + return HYBBX_ERR_IO; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (hybbx_path_join(path, sizeof(path), recycle, ent->d_name) != + HYBBX_OK) { + continue; + } + + if (remove(path) == 0) { + removed++; + } + } + + closedir(dir); + + if (removed == 0) { + hybbx_session_write_line(session, "Recycle bin empty."); + } else { + snprintf(buf, sizeof(buf), "Recycle bin emptied (%u message(s)).", + removed); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +#define HYBBX_MAIL_SYSTEM_FROM "system" + +static void mail_format_utc_time(time_t when, char *buf, size_t len) +{ + if (buf == NULL || len == 0) { + return; + } + + buf[0] = '\0'; + if (when <= 0) { + return; + } + + { + const struct tm *tm = gmtime(&when); + + if (tm != NULL) { + (void)strftime(buf, len, "%Y-%m-%d %H:%M:%S UTC", tm); + } + } +} + +typedef struct mail_notify_staff_ctx { + hybbx_service_t *service; + char subject[HYBBX_MAIL_SUBJECT_MAX]; + char body[HYBBX_MAIL_BODY_MAX]; +} mail_notify_staff_ctx_t; + +static hybbx_result_t mail_notify_staff_cb(const hybbx_user_record_t *user, + void *ctx) +{ + mail_notify_staff_ctx_t *nctx = (mail_notify_staff_ctx_t *)ctx; + + if (user == NULL || nctx == NULL) { + return HYBBX_OK; + } + + if (!user->active || !hybbx_user_level_is_sysop_or_admin(user->level)) { + return HYBBX_OK; + } + + (void)hybbx_mail_deliver(nctx->service, HYBBX_MAIL_SYSTEM_FROM, + user->username, nctx->subject, nctx->body); + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_notify_staff_registration(hybbx_service_t *service, + const hybbx_user_registration_t *reg, + const hybbx_user_record_t *user) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + mail_notify_staff_ctx_t ctx; + char created[48]; + int n; + + if (service == NULL || reg == NULL || user == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + n = snprintf(ctx.subject, sizeof(ctx.subject), + "Registration pending: %s", reg->nickname); + if (n < 0 || (size_t)n >= sizeof(ctx.subject)) { + return HYBBX_ERR_INVALID; + } + + mail_format_utc_time(user->created_at, created, sizeof(created)); + if (created[0] == '\0') { + hybbx_strlcpy(created, "(unknown)", sizeof(created)); + } + + n = snprintf(ctx.body, sizeof(ctx.body), + "Guest self-registration pending approval.\n" + "/register data submitted:\n\n" + " Nickname: %s\n" + " Login: %s\n" + " Full name: %s\n" + " Country: %s\n" + " Location: %s\n" + " Email: %s\n\n" + "Account record:\n\n" + " User ID: %llu\n" + " Level: %s\n" + " Active: %s\n" + " Created: %s\n\n" + "Review all fields. If correct, activate with:\n" + " /activate %s\n", + reg->nickname, + reg->username, + reg->full_name, + reg->country, + reg->location, + reg->email, + (unsigned long long)user->id, + hybbx_user_level_name(user->level), + hybbx_bool_to_string(user->active), + created, + reg->username); + if (n < 0 || (size_t)n >= sizeof(ctx.body)) { + return HYBBX_ERR_INVALID; + } + + ctx.service = service; + + (void)hybbx_storage_foreach_user(storage, mail_notify_staff_cb, &ctx); + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_deliver(hybbx_service_t *service, + const char *from_user, + const char *to_user, + const char *subject, + const char *body) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + hybbx_user_record_t recipient; + char inbox[HYBBX_PATH_MAX]; + char path[HYBBX_PATH_MAX]; + uint64_t id; + FILE *fp; + hybbx_result_t rc; + char to_norm[HYBBX_USER_NAME_MAX]; + size_t body_len; + + if (service == NULL || from_user == NULL || to_user == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (subject == NULL) { + subject = ""; + } + if (body == NULL) { + body = ""; + } + + body_len = strlen(body); + if (strlen(subject) > mail->subject_max || body_len > mail->body_max) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(to_norm, to_user, sizeof(to_norm)); + hybbx_username_normalize(to_norm); + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_resolve_user(storage, to_user, &recipient); + if (rc == HYBBX_ERR_NOT_FOUND) { + return HYBBX_ERR_NOT_FOUND; + } + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_strlcpy(to_norm, recipient.username, sizeof(to_norm)); + + if (hybbx_user_level_is_guest(recipient.level) || !recipient.active) { + return HYBBX_ERR_DENIED; + } + + if (mail_uses_sqlite(service)) { + return hybbx_mail_sql_deliver(storage, mail, from_user, to_norm, + subject, body); + } + + rc = mail_ensure_root(mail); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_inbox_path(mail, to_norm, inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + if (mkdir_p(inbox) != 0) { + return HYBBX_ERR_IO; + } + + rc = mail_next_id(mail, &id); + if (rc != HYBBX_OK) { + return rc; + } + + if (msg_file_path(inbox, id, path, sizeof(path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "id=%llu\n", (unsigned long long)id); + fprintf(fp, "from=%s\n", from_user); + fprintf(fp, "to=%s\n", to_norm); + fprintf(fp, "subject=%s\n", subject); + fprintf(fp, "time=%ld\n", (long)time(NULL)); + fprintf(fp, "read=no\n"); + fprintf(fp, "---\n"); + fputs(body, fp); + if (body_len == 0 || body[body_len - 1] != '\n') { + fputc('\n', fp); + } + fclose(fp); + + (void)mail_trim_inbox(service, mail, to_norm, mail->max_messages); + return HYBBX_OK; +} diff --git a/src/core/mail_sql.c b/src/core/mail_sql.c new file mode 100644 index 0000000..95b93f9 --- /dev/null +++ b/src/core/mail_sql.c @@ -0,0 +1,655 @@ +#include "mail_sql.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" +#include "storage_private.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#ifdef HYBBX_HAVE_SQLITE +#include <sqlite3.h> + +#define MAIL_FOLDER_INBOX 0 +#define MAIL_FOLDER_RECYCLE 1 + +static int mail_entry_compare(const void *a, const void *b) +{ + const hybbx_mail_entry_t *ea = (const hybbx_mail_entry_t *)a; + const hybbx_mail_entry_t *eb = (const hybbx_mail_entry_t *)b; + + if (ea->received_at > eb->received_at) { + return -1; + } + if (ea->received_at < eb->received_at) { + return 1; + } + if (ea->id > eb->id) { + return -1; + } + if (ea->id < eb->id) { + return 1; + } + return 0; +} + +static hybbx_result_t mail_sql_meta_bump(sqlite3 *db, const char *key, + uint64_t *value) +{ + sqlite3_stmt *stmt; + int rc; + + *value = 0; + + rc = sqlite3_prepare_v2(db, + "SELECT value FROM meta WHERE key=?1;", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + if (sqlite3_step(stmt) == SQLITE_ROW) { + *value = (uint64_t)sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + } + + (*value)++; + + rc = sqlite3_prepare_v2(db, + "INSERT INTO meta(key,value) VALUES(?1,?2) " + "ON CONFLICT(key) DO UPDATE SET value=?2;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)*value); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +static void mail_sql_owner_norm(char *owner, size_t len, const char *username) +{ + hybbx_strlcpy(owner, username, len); + hybbx_username_normalize(owner); +} + +hybbx_result_t hybbx_mail_sql_load_inbox(hybbx_storage_t *storage, + const char *username, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + sqlite3 *db; + sqlite3_stmt *stmt; + char owner[HYBBX_USER_NAME_MAX]; + int rc; + size_t count = 0; + + if (storage == NULL || username == NULL || entries == NULL || + out_count == NULL) { + return HYBBX_ERR_INVALID; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), username); + *out_count = 0; + + rc = sqlite3_prepare_v2(db, + "SELECT id,from_user,subject,received_at,read_flag " + "FROM messages WHERE owner=?1 AND folder=?2 " + "ORDER BY received_at DESC, id DESC;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_INBOX); + + while (sqlite3_step(stmt) == SQLITE_ROW && count < max_entries) { + hybbx_mail_entry_t *entry = &entries[count]; + + memset(entry, 0, sizeof(*entry)); + entry->id = (uint64_t)sqlite3_column_int64(stmt, 0); + hybbx_strlcpy(entry->from, (const char *)sqlite3_column_text(stmt, 1), + sizeof(entry->from)); + hybbx_strlcpy(entry->subject, + (const char *)sqlite3_column_text(stmt, 2), + sizeof(entry->subject)); + entry->received_at = (time_t)sqlite3_column_int64(stmt, 3); + entry->read = sqlite3_column_int(stmt, 4); + count++; + } + + sqlite3_finalize(stmt); + + if (count > 1) { + qsort(entries, count, sizeof(entries[0]), mail_entry_compare); + } + + *out_count = count; + return HYBBX_OK; +} + +static hybbx_result_t mail_sql_trim_inbox(sqlite3 *db, const char *owner, + unsigned max_messages) +{ + sqlite3_stmt *stmt; + int rc; + size_t count = 0; + + rc = sqlite3_prepare_v2(db, + "SELECT COUNT(*) FROM messages " + "WHERE owner=?1 AND folder=?2;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_INBOX); + if (sqlite3_step(stmt) == SQLITE_ROW) { + count = (size_t)sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + + if (count <= max_messages) { + return HYBBX_OK; + } + + rc = sqlite3_prepare_v2(db, + "DELETE FROM messages WHERE id IN (" + "SELECT id FROM messages WHERE owner=?1 " + "AND folder=?2 ORDER BY received_at ASC, id ASC " + "LIMIT ?3);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_INBOX); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)(count - max_messages)); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_mail_sql_deliver(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *from_user, + const char *to_user, + const char *subject, + const char *body) +{ + sqlite3 *db; + sqlite3_stmt *stmt; + char owner[HYBBX_USER_NAME_MAX]; + uint64_t id; + int rc; + hybbx_result_t hres; + time_t now = time(NULL); + + if (storage == NULL || mail == NULL || from_user == NULL || + to_user == NULL) { + return HYBBX_ERR_INVALID; + } + + if (subject == NULL) { + subject = ""; + } + if (body == NULL) { + body = ""; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), to_user); + + hres = mail_sql_meta_bump(db, "mail_next", &id); + if (hres != HYBBX_OK) { + return hres; + } + + rc = sqlite3_prepare_v2(db, + "INSERT INTO messages(id,owner,from_user,subject," + "body,received_at,read_flag,deleted_at,folder) " + "VALUES(?1,?2,?3,?4,?5,?6,0,0,?7);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)id); + sqlite3_bind_text(stmt, 2, owner, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, from_user, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 4, subject, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 5, body, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 6, (sqlite3_int64)now); + sqlite3_bind_int(stmt, 7, MAIL_FOLDER_INBOX); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + + return mail_sql_trim_inbox(db, owner, mail->max_messages); +} + +unsigned hybbx_mail_sql_purge_recycle(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *username) +{ + sqlite3 *db; + sqlite3_stmt *stmt; + char owner[HYBBX_USER_NAME_MAX]; + time_t cutoff; + int rc; + + if (storage == NULL || mail == NULL || username == NULL) { + return 0; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return 0; + } + + mail_sql_owner_norm(owner, sizeof(owner), username); + cutoff = time(NULL) - (time_t)mail->recycle_days * 86400; + + rc = sqlite3_prepare_v2(db, + "DELETE FROM messages WHERE owner=?1 AND folder=?2 " + "AND deleted_at > 0 AND deleted_at < ?3;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return 0; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_RECYCLE); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cutoff); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? (unsigned)sqlite3_changes(db) : 0; +} + +static hybbx_result_t mail_sql_resolve_index(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index, + uint64_t *out_id) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count; + hybbx_result_t rc; + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + rc = hybbx_mail_sql_load_inbox(hybbx_service_get_storage(service), + hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (list_index == 0 || list_index > count) { + return HYBBX_ERR_NOT_FOUND; + } + + *out_id = entries[list_index - 1].id; + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_sql_read(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + hybbx_storage_t *storage; + sqlite3 *db; + sqlite3_stmt *stmt; + uint64_t id; + char owner[HYBBX_USER_NAME_MAX]; + char line[HYBBX_LINE_MAX]; + int rc; + hybbx_result_t hres; + const char *from_user; + const char *subject; + const char *body; + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + hres = mail_sql_resolve_index(service, session, list_index, &id); + if (hres == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + if (hres != HYBBX_OK) { + return hres; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), hybbx_session_username(session)); + + rc = sqlite3_prepare_v2(db, + "SELECT from_user,subject,body FROM messages " + "WHERE id=?1 AND owner=?2 AND folder=?3;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)id); + sqlite3_bind_text(stmt, 2, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 3, MAIL_FOLDER_INBOX); + rc = sqlite3_step(stmt); + if (rc != SQLITE_ROW) { + sqlite3_finalize(stmt); + hybbx_session_write_line(session, "Cannot open message."); + return HYBBX_ERR_IO; + } + + from_user = (const char *)sqlite3_column_text(stmt, 0); + subject = (const char *)sqlite3_column_text(stmt, 1); + body = (const char *)sqlite3_column_text(stmt, 2); + if (from_user == NULL) { + from_user = ""; + } + if (subject == NULL) { + subject = ""; + } + if (body == NULL) { + body = ""; + } + + snprintf(line, sizeof(line), "From: %s", from_user); + hybbx_session_write_line(session, line); + snprintf(line, sizeof(line), "Subject: %s", subject); + hybbx_session_write_line(session, line); + + { + const char *p = body; + const char *nl; + + while (*p != '\0') { + nl = strchr(p, '\n'); + if (nl == NULL) { + hybbx_session_write_line(session, p); + break; + } + if (nl > p) { + size_t chunk = (size_t)(nl - p); + + if (chunk >= sizeof(line)) { + chunk = sizeof(line) - 1; + } + memcpy(line, p, chunk); + line[chunk] = '\0'; + hybbx_session_write_line(session, line); + } else { + hybbx_session_write_line(session, ""); + } + p = nl + 1; + } + } + + sqlite3_finalize(stmt); + + rc = sqlite3_prepare_v2(db, + "UPDATE messages SET read_flag=1 " + "WHERE id=?1 AND owner=?2;", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)id); + sqlite3_bind_text(stmt, 2, owner, -1, SQLITE_STATIC); + (void)sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_sql_delete_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + sqlite3 *db; + sqlite3_stmt *stmt; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char owner[HYBBX_USER_NAME_MAX]; + size_t count; + size_t i; + unsigned moved = 0; + time_t now = time(NULL); + int rc; + hybbx_result_t hres; + + mail = hybbx_service_get_mail(service); + storage = hybbx_service_get_storage(service); + if (mail == NULL || !mail->enabled || storage == NULL) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (to == 0) { + to = from; + } + + (void)hybbx_mail_sql_purge_recycle(storage, mail, + hybbx_session_username(session)); + + hres = hybbx_mail_sql_load_inbox(storage, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (hres != HYBBX_OK) { + return hres; + } + + if (from > count || to > count) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), hybbx_session_username(session)); + + rc = sqlite3_prepare_v2(db, + "UPDATE messages SET folder=?4, deleted_at=?3 " + "WHERE id=?1 AND owner=?2 AND folder=?5;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + for (i = (size_t)(from - 1); i < (size_t)to; i++) { + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)entries[i].id); + sqlite3_bind_text(stmt, 2, owner, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)now); + sqlite3_bind_int(stmt, 4, MAIL_FOLDER_RECYCLE); + sqlite3_bind_int(stmt, 5, MAIL_FOLDER_INBOX); + rc = sqlite3_step(stmt); + sqlite3_reset(stmt); + if (rc == SQLITE_DONE && sqlite3_changes(db) > 0) { + moved++; + } + } + + sqlite3_finalize(stmt); + + if (moved == 0) { + hybbx_session_write_line(session, "Delete failed."); + return HYBBX_ERR_IO; + } + + if (moved == 1) { + hybbx_session_write_line(session, + "Message moved to recycle (auto-purge after configured days)."); + } else { + char buf[64]; + + snprintf(buf, sizeof(buf), + "%u messages moved to recycle (auto-purge after %u days).", + moved, mail->recycle_days); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_sql_recycle_empty(hybbx_service_t *service, + hybbx_session_t *session) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + sqlite3 *db; + sqlite3_stmt *stmt; + char owner[HYBBX_USER_NAME_MAX]; + int rc; + char buf[64]; + + mail = hybbx_service_get_mail(service); + storage = hybbx_service_get_storage(service); + if (mail == NULL || !mail->enabled || storage == NULL) { + return HYBBX_ERR_UNSUPPORTED; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), hybbx_session_username(session)); + (void)hybbx_mail_sql_purge_recycle(storage, mail, owner); + + rc = sqlite3_prepare_v2(db, + "DELETE FROM messages WHERE owner=?1 AND folder=?2;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + hybbx_session_write_line(session, "Cannot read recycle bin."); + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_RECYCLE); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + hybbx_session_write_line(session, "Cannot read recycle bin."); + return HYBBX_ERR_IO; + } + + if (sqlite3_changes(db) == 0) { + hybbx_session_write_line(session, "Recycle bin empty."); + } else { + snprintf(buf, sizeof(buf), "Recycle bin emptied (%d message(s)).", + sqlite3_changes(db)); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +#else /* HYBBX_HAVE_SQLITE */ + +hybbx_result_t hybbx_mail_sql_load_inbox(hybbx_storage_t *storage, + const char *username, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + (void)storage; + (void)username; + (void)entries; + (void)max_entries; + (void)out_count; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_mail_sql_deliver(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *from_user, + const char *to_user, + const char *subject, + const char *body) +{ + (void)storage; + (void)mail; + (void)from_user; + (void)to_user; + (void)subject; + (void)body; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_mail_sql_read(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + (void)service; + (void)session; + (void)list_index; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_mail_sql_delete_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + (void)service; + (void)session; + (void)from; + (void)to; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_mail_sql_recycle_empty(hybbx_service_t *service, + hybbx_session_t *session) +{ + (void)service; + (void)session; + return HYBBX_ERR_UNSUPPORTED; +} + +unsigned hybbx_mail_sql_purge_recycle(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *username) +{ + (void)storage; + (void)mail; + (void)username; + return 0; +} + +#endif /* HYBBX_HAVE_SQLITE */ diff --git a/src/core/mail_sql.h b/src/core/mail_sql.h new file mode 100644 index 0000000..cddd856 --- /dev/null +++ b/src/core/mail_sql.h @@ -0,0 +1,40 @@ +#ifndef HYBBX_MAIL_SQL_H +#define HYBBX_MAIL_SQL_H + +#include "hybbx/mail.h" +#include "hybbx/storage.h" +#include "hybbx/types.h" + +struct hybbx_service; +struct hybbx_session; + +hybbx_result_t hybbx_mail_sql_load_inbox(hybbx_storage_t *storage, + const char *username, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count); + +hybbx_result_t hybbx_mail_sql_deliver(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *from_user, + const char *to_user, + const char *subject, + const char *body); + +hybbx_result_t hybbx_mail_sql_read(struct hybbx_service *service, + struct hybbx_session *session, + unsigned list_index); + +hybbx_result_t hybbx_mail_sql_delete_range(struct hybbx_service *service, + struct hybbx_session *session, + unsigned from, + unsigned to); + +hybbx_result_t hybbx_mail_sql_recycle_empty(struct hybbx_service *service, + struct hybbx_session *session); + +unsigned hybbx_mail_sql_purge_recycle(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *username); + +#endif /* HYBBX_MAIL_SQL_H */ diff --git a/src/core/mains_proxy.c b/src/core/mains_proxy.c new file mode 100644 index 0000000..f3475fe --- /dev/null +++ b/src/core/mains_proxy.c @@ -0,0 +1,1034 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/mains_proxy.h" +#include "hybbx/service.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/proxymail.h" +#include "hybbx/proxychat.h" +#include "hybbx/log.h" + +#include <errno.h> +#include <poll.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/* Inter-node mesh: hybbx_circuit_link_connect() only — never peer Main TCP. */ + +#define MAINS_PROXY_RECONNECT_MS 5000u +#define MAINS_PROXY_CONNECT_ATTEMPTS 30u +#define MAINS_PROXY_DEFAULT_TTL 8u + +typedef struct mains_proxy_peer_runtime { + hybbx_mains_proxy_peer_config_t config; + int fd; + hybbx_circuit_decoder_t dec; + unsigned reconnect_ms; +} mains_proxy_peer_runtime_t; + +typedef struct mains_proxy_runtime { + hybbx_service_t *service; + hybbx_mains_proxy_mesh_t mesh; + mains_proxy_peer_runtime_t peers[HYBBX_MAINS_PROXY_MAX_PEERS]; + unsigned live_links; +} mains_proxy_runtime_t; + +static mains_proxy_runtime_t g_rt; + +static const char *mains_proxy_find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (scratch != NULL && scratch_len > 0) { + scratch[0] = '\0'; + } + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *line = cursor; + const char *eq; + const char *end = cursor; + + /* + * Config sections are serialized with ';' between key=value pairs + * (hybbx_config_format_section) but the parser used to look for '\n' + * only. Accept both separators so a single section can be parsed + * regardless of how it was produced. + */ + while (*end != '\0' && *end != ';' && *end != '\n') { + end++; + } + + if (line[0] == '[') { + cursor = (*end == '\0') ? end : end + 1; + continue; + } + + eq = strchr(line, '='); + if (eq == NULL || eq >= end) { + cursor = (*end == '\0') ? end : end + 1; + continue; + } + + if ((size_t)(eq - line) == key_len && + strncmp(line, key, key_len) == 0) { + const char *value = eq + 1; + + while (value < end && (*value == ' ' || *value == '\t')) { + value++; + } + + if (scratch != NULL && scratch_len > 0) { + size_t vlen = (size_t)(end - value); + + while (vlen > 0 && + (value[vlen - 1] == ' ' || value[vlen - 1] == '\t' || + value[vlen - 1] == '\r')) { + vlen--; + } + + if (vlen >= scratch_len) { + vlen = scratch_len - 1; + } + memcpy(scratch, value, vlen); + scratch[vlen] = '\0'; + return scratch; + } + + return value; + } + + cursor = (*end == '\0') ? end : end + 1; + } + + return NULL; +} + +static int str_ieq_local(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int proxy_payload_get_line(const char *payload, size_t len, + const char *key, char *out, size_t out_len) +{ + size_t key_len; + const char *cursor; + const char *end; + + if (payload == NULL || key == NULL || out == NULL || out_len == 0) { + return 0; + } + + out[0] = '\0'; + key_len = strlen(key); + cursor = payload; + end = payload + len; + + while (cursor < end) { + const char *line_end = memchr(cursor, '\n', (size_t)(end - cursor)); + size_t line_len; + + if (line_end == NULL) { + line_end = end; + } + + line_len = (size_t)(line_end - cursor); + if (line_len >= key_len + 1 && memcmp(cursor, key, key_len) == 0 && + cursor[key_len] == '=') { + const char *value = cursor + key_len + 1; + size_t vlen = line_len - key_len - 1; + + while (vlen > 0 && (*value == ' ' || *value == '\t')) { + value++; + vlen--; + } + if (vlen >= out_len) { + vlen = out_len - 1; + } + memcpy(out, value, vlen); + out[vlen] = '\0'; + return 1; + } + + if (line_end >= end) { + break; + } + cursor = line_end + 1; + } + + return 0; +} + +static const char *proxy_payload_body(const char *payload, size_t len, + size_t *body_len) +{ + size_t off; + + if (payload == NULL || body_len == NULL) { + return NULL; + } + + *body_len = 0; + + if (len >= 5 && memcmp(payload, "---\r\n", 5) == 0) { + off = 5; + } else if (len >= 4 && memcmp(payload, "---\n", 4) == 0) { + off = 4; + } else { + return NULL; + } + + while (off < len && (payload[off] == '\r' || payload[off] == '\n')) { + off++; + } + + *body_len = len - off; + return payload + off; +} + +static unsigned proxy_payload_get_ttl(const char *payload, size_t len) +{ + char scratch[HYBBX_CONFIG_LINE_MAX]; + const char *value; + unsigned long ttl; + + if (payload == NULL || len == 0 || len >= sizeof(scratch)) { + return 0; + } + + memcpy(scratch, payload, len); + scratch[len] = '\0'; + + value = NULL; + if (proxy_payload_get_line(scratch, len, "ttl", scratch, sizeof(scratch))) { + value = scratch; + } + + if (value == NULL || value[0] == '\0') { + return 0; + } + + ttl = strtoul(value, NULL, 10); + if (ttl > 255u) { + ttl = 255u; + } + return (unsigned)ttl; +} + +static size_t proxy_build_mail_payload(char *out, size_t out_cap, + const char *from_address, + const char *to_address, + const char *subject, + const char *body, + unsigned ttl) +{ + int n; + + if (out == NULL || out_cap == 0 || from_address == NULL || + to_address == NULL || subject == NULL || body == NULL) { + return 0; + } + + n = snprintf(out, out_cap, "from=%s\nto=%s\nsub=%s\nttl=%u\n---\n%s", + from_address, to_address, subject, ttl, body); + if (n < 0 || (size_t)n >= out_cap) { + return 0; + } + + return (size_t)n; +} + +static size_t proxy_build_chat_payload(char *out, size_t out_cap, + const char *from_address, + const char *line, + unsigned ttl) +{ + int n; + + if (out == NULL || out_cap == 0 || from_address == NULL || + line == NULL) { + return 0; + } + + n = snprintf(out, out_cap, "from=%s\nttl=%u\n---\n%s", from_address, ttl, line); + if (n < 0 || (size_t)n >= out_cap) { + return 0; + } + + return (size_t)n; +} + +static void proxy_dispatch_payload(hybbx_service_t *service, + hybbx_circuit_proto_t proto, + const uint8_t *payload, size_t len) +{ + char from[HYBBX_PROXYMAIL_ADDRESS_MAX]; + char to[HYBBX_PROXYMAIL_ADDRESS_MAX]; + char subj[HYBBX_MAIL_SUBJECT_MAX + 1]; + const char *body; + size_t body_len; + char scratch[HYBBX_CIRCUIT_MAX_PAYLOAD + 1]; + + if (service == NULL || payload == NULL || len == 0 || + len > HYBBX_CIRCUIT_MAX_PAYLOAD) { + return; + } + + memcpy(scratch, payload, len); + scratch[len] = '\0'; + + if (!proxy_payload_get_line(scratch, len, "from", from, sizeof(from))) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_PROXY_MAIL) { + if (!proxy_payload_get_line(scratch, len, "to", to, sizeof(to))) { + return; + } + if (!proxy_payload_get_line(scratch, len, "sub", subj, sizeof(subj))) { + subj[0] = '\0'; + } + body = proxy_payload_body(scratch, len, &body_len); + if (body == NULL) { + return; + } + scratch[0] = '\0'; + if (body_len > 0) { + if (body_len >= sizeof(scratch)) { + body_len = sizeof(scratch) - 1; + } + memcpy(scratch, body, body_len); + scratch[body_len] = '\0'; + } + hybbx_proxymail_receive(service, from, to, subj, scratch); + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + body = proxy_payload_body(scratch, len, &body_len); + if (body == NULL) { + return; + } + scratch[0] = '\0'; + if (body_len > 0) { + if (body_len >= sizeof(scratch)) { + body_len = sizeof(scratch) - 1; + } + memcpy(scratch, body, body_len); + scratch[body_len] = '\0'; + } + hybbx_proxychat_receive(service, from, scratch); + } +} + +static hybbx_result_t proxy_peer_send(int fd, hybbx_circuit_proto_t proto, + const uint8_t *payload, size_t len) +{ + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + + if (fd < 0 || payload == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_circuit_encode(proto, HYBBX_CIRCUIT_FLAG_NONE, + payload, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(fd, frame, frame_len); +} + +static void proxy_peer_disconnect(mains_proxy_peer_runtime_t *peer) +{ + if (peer == NULL) { + return; + } + + if (peer->fd >= 0) { + close(peer->fd); + peer->fd = -1; + } + + if (g_rt.live_links > 0) { + g_rt.live_links--; + } + + peer->reconnect_ms = MAINS_PROXY_RECONNECT_MS; +} + +static void on_mesh_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + mains_proxy_peer_runtime_t *from_peer = (mains_proxy_peer_runtime_t *)userdata; + char relay_buf[HYBBX_CIRCUIT_MAX_PAYLOAD + 1]; + unsigned ttl; + unsigned i; + + (void)flags; + + if (g_rt.service == NULL || payload == NULL || len == 0 || + len > HYBBX_CIRCUIT_MAX_PAYLOAD) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_PROXY_MAIL && + proto != HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + return; + } + + /* Always deliver locally first. */ + proxy_dispatch_payload(g_rt.service, proto, payload, len); + + /* Relay chat to all other live peers with TTL loop prevention. */ + if (proto != HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + return; + } + + ttl = proxy_payload_get_ttl((const char *)payload, len); + if (ttl <= 1) { + return; + } + ttl--; + + if (len >= sizeof(relay_buf)) { + return; + } + memcpy(relay_buf, payload, len); + relay_buf[len] = '\0'; + + { + char ttl_str[32]; + char *ttl_pos; + int n; + + n = snprintf(ttl_str, sizeof(ttl_str), "ttl=%u", ttl); + if (n < 0 || (size_t)n >= sizeof(ttl_str)) { + return; + } + + ttl_pos = strstr(relay_buf, "\nttl="); + if (ttl_pos == NULL) { + /* + * Also accept ttl= at the very beginning of the payload + * (proxy_build_chat_payload puts it as the second line). + */ + if (strncmp(relay_buf, "ttl=", 4) == 0) { + ttl_pos = relay_buf; + } else { + /* No TTL field; drop rather than inject into a foreign payload. */ + return; + } + } else { + /* Point at the 't' of "ttl=", not the leading newline. */ + ttl_pos++; + } + + { + char *start = ttl_pos + 4; + char *end = start; + size_t old_len; + size_t new_len; + size_t tail_len; + + if (start == ttl_pos || *start == '\0' || *start == '\n' || + *start == '\r') { + return; + } + + /* Find end of current ttl line. */ + while (*end != '\0' && *end != '\n' && *end != '\r') { + end++; + } + + old_len = (size_t)(end - start); + new_len = strlen(ttl_str + 4); + tail_len = strlen(end); + + /* + * New value is shorter or equal because TTL only decreases, + * so in-place replacement with memmove is safe. + */ + if (new_len != old_len) { + memmove(start + new_len, end, tail_len + 1); + } + memcpy(start, ttl_str + 4, new_len); + } + } + + for (i = 0; i < g_rt.mesh.peer_count; i++) { + mains_proxy_peer_runtime_t *target = &g_rt.peers[i]; + + if (target->fd < 0) { + continue; + } + if (target == from_peer) { + continue; + } + (void)proxy_peer_send(target->fd, proto, + (const uint8_t *)relay_buf, strlen(relay_buf)); + } +} + +static int peer_name_matches(const hybbx_mains_proxy_peer_config_t *peer, + const char *remote_service) +{ + if (peer == NULL || remote_service == NULL || remote_service[0] == '\0') { + return 0; + } + + if (peer->peer_id[0] != '\0' && + str_ieq_local(peer->peer_id, remote_service)) { + return 1; + } + + if (peer->link_id[0] != '\0' && + str_ieq_local(peer->link_id, remote_service)) { + return 1; + } + + return 0; +} + +static mains_proxy_peer_runtime_t *proxy_find_peer_for_service( + const char *remote_service) +{ + unsigned i; + + for (i = 0; i < g_rt.mesh.peer_count; i++) { + if (g_rt.peers[i].fd < 0) { + continue; + } + if (peer_name_matches(&g_rt.peers[i].config, remote_service)) { + return &g_rt.peers[i]; + } + } + + return NULL; +} + +static hybbx_result_t proxy_peer_connect(mains_proxy_peer_runtime_t *peer) +{ + const hybbx_mains_proxy_peer_config_t *cfg; + const char *host; + unsigned port; + const char *link_id; + const char *peer_label; + unsigned attempt; + hybbx_result_t rc; + + if (peer == NULL) { + return HYBBX_ERR_INVALID; + } + + cfg = &peer->config; + peer_label = cfg->peer_id[0] != '\0' ? cfg->peer_id : "(unnamed)"; + + if (!cfg->enabled) { + return HYBBX_OK; + } + + if (cfg->wire == HYBBX_MAINS_PROXY_WIRE_AX25) { + hybbx_log_warn("[mains_proxy] peer '%s': wire=ax25 not active — use circuit", + peer_label); + return HYBBX_ERR_UNSUPPORTED; + } + + if (cfg->circuit_host[0] == '\0') { + return HYBBX_OK; + } + + if (cfg->link_id[0] == '\0' || cfg->link_password[0] == '\0') { + hybbx_log_warn("[mains_proxy] peer '%s' missing link_id or link_password", + peer_label); + return HYBBX_ERR_INVALID; + } + + host = cfg->circuit_host; + port = cfg->circuit_port; + if (port == 0) { + port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + + link_id = cfg->link_id; + + for (attempt = 0; attempt < MAINS_PROXY_CONNECT_ATTEMPTS; attempt++) { + rc = hybbx_circuit_link_connect(host, port, &peer->fd); + if (rc != HYBBX_OK) { + usleep(100000); + continue; + } + + hybbx_circuit_decoder_init(&peer->dec); + rc = hybbx_circuit_link_authenticate(peer->fd, cfg->link_password, + "proxy", link_id); + if (rc != HYBBX_OK) { + close(peer->fd); + peer->fd = -1; + usleep(100000); + continue; + } + + g_rt.live_links++; + peer->reconnect_ms = 0; + hybbx_log_info("[mains_proxy] linked to peer %s via HBX %s:%u link_id=%s", + peer_label, host, port, link_id); + return HYBBX_OK; + } + + hybbx_log_warn("[mains_proxy] could not link peer '%s' at %s:%u", + peer_label, host, port); + return HYBBX_ERR_IO; +} + +hybbx_mains_proxy_wire_t hybbx_mains_proxy_wire_parse(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_MAINS_PROXY_WIRE_CIRCUIT; + } + + if (str_ieq_local(value, "circuit") || str_ieq_local(value, "tcp") || + str_ieq_local(value, "tcpip") || str_ieq_local(value, "tcp/ip")) { + return HYBBX_MAINS_PROXY_WIRE_CIRCUIT; + } + + if (str_ieq_local(value, "ax25") || str_ieq_local(value, "packet_radio")) { + return HYBBX_MAINS_PROXY_WIRE_AX25; + } + + return HYBBX_MAINS_PROXY_WIRE_CIRCUIT; +} + +const char *hybbx_mains_proxy_wire_name(hybbx_mains_proxy_wire_t wire) +{ + switch (wire) { + case HYBBX_MAINS_PROXY_WIRE_AX25: + return "ax25"; + case HYBBX_MAINS_PROXY_WIRE_CIRCUIT: + default: + return "circuit"; + } +} + +hybbx_mains_proxy_duplex_t hybbx_mains_proxy_duplex_parse(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_MAINS_PROXY_DUPLEX_FULL; + } + + if (str_ieq_local(value, "half") || str_ieq_local(value, "half-duplex") || + str_ieq_local(value, "half_duplex")) { + return HYBBX_MAINS_PROXY_DUPLEX_HALF; + } + + return HYBBX_MAINS_PROXY_DUPLEX_FULL; +} + +const char *hybbx_mains_proxy_duplex_name(hybbx_mains_proxy_duplex_t duplex) +{ + return duplex == HYBBX_MAINS_PROXY_DUPLEX_HALF ? "half" : "full"; +} + +void hybbx_mains_proxy_peer_defaults(hybbx_mains_proxy_peer_config_t *peer) +{ + if (peer == NULL) { + return; + } + + memset(peer, 0, sizeof(*peer)); + peer->circuit_port = HYBBX_CIRCUIT_DEFAULT_PORT; + peer->wire = HYBBX_MAINS_PROXY_WIRE_CIRCUIT; + peer->duplex = HYBBX_MAINS_PROXY_DUPLEX_FULL; + peer->use_secondary = 1; + peer->enabled = 1; +} + +hybbx_result_t hybbx_mains_proxy_peer_parse(const char *config, + hybbx_mains_proxy_peer_config_t *out) +{ + char scratch[HYBBX_CONFIG_LINE_MAX]; + const char *value; + int legacy_host = 0; + int legacy_port = 0; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_mains_proxy_peer_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = mains_proxy_find_kv(config, "enabled", scratch, sizeof(scratch)); + if (value != NULL) { + out->enabled = hybbx_parse_bool(value, 1); + } + + value = mains_proxy_find_kv(config, "peer_id", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->peer_id, value, sizeof(out->peer_id)); + } + + value = mains_proxy_find_kv(config, "circuit_host", scratch, + sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->circuit_host, value, sizeof(out->circuit_host)); + } + + value = mains_proxy_find_kv(config, "circuit_port", scratch, + sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned long port = strtoul(value, NULL, 10); + + if (port >= 1u && port <= 65535u) { + out->circuit_port = (unsigned)port; + } + } + + value = mains_proxy_find_kv(config, "link_id", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->link_id, value, sizeof(out->link_id)); + } + + value = mains_proxy_find_kv(config, "link_password", scratch, + sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->link_password, value, sizeof(out->link_password)); + } + + value = mains_proxy_find_kv(config, "host", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->host, value, sizeof(out->host)); + legacy_host = 1; + } + + value = mains_proxy_find_kv(config, "port", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned long port = strtoul(value, NULL, 10); + + if (port >= 1u && port <= 65535u) { + out->port = (unsigned)port; + } + legacy_port = 1; + } + + if (legacy_host && out->circuit_host[0] == '\0') { + hybbx_strlcpy(out->circuit_host, out->host, + sizeof(out->circuit_host)); + hybbx_log_warn("[mains_proxy] peer '%s': deprecated key host= — use " + "circuit_host= (mapped for now)", + out->peer_id[0] != '\0' ? out->peer_id : "(unnamed)"); + } else if (legacy_host) { + hybbx_log_warn("[mains_proxy] peer '%s': deprecated key host= ignored " + "(circuit_host set)", + out->peer_id[0] != '\0' ? out->peer_id : "(unnamed)"); + } + + if (legacy_port) { + hybbx_log_warn("[mains_proxy] peer '%s': deprecated key port= ignored — " + "use circuit_port= (HBX hub, default %u)", + out->peer_id[0] != '\0' ? out->peer_id : "(unnamed)", + (unsigned)HYBBX_CIRCUIT_DEFAULT_PORT); + } + + value = mains_proxy_find_kv(config, "wire", scratch, sizeof(scratch)); + out->wire = hybbx_mains_proxy_wire_parse(value); + + value = mains_proxy_find_kv(config, "duplex", scratch, sizeof(scratch)); + out->duplex = hybbx_mains_proxy_duplex_parse(value); + + value = mains_proxy_find_kv(config, "use_secondary", scratch, + sizeof(scratch)); + if (value != NULL) { + out->use_secondary = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} + +void hybbx_mains_proxy_mesh_init(hybbx_mains_proxy_mesh_t *mesh) +{ + if (mesh == NULL) { + return; + } + + memset(mesh, 0, sizeof(*mesh)); +} + +hybbx_result_t hybbx_mains_proxy_mesh_start(hybbx_service_t *service, + hybbx_mains_proxy_mesh_t *mesh) +{ + unsigned i; + unsigned configured = 0; + unsigned linked = 0; + + if (mesh == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_mains_proxy_mesh_stop(mesh); + + memset(&g_rt, 0, sizeof(g_rt)); + g_rt.service = service; + g_rt.mesh = *mesh; + + for (i = 0; i < mesh->peer_count; i++) { + g_rt.peers[i].config = mesh->peers[i]; + g_rt.peers[i].fd = -1; + hybbx_circuit_decoder_init(&g_rt.peers[i].dec); + + if (!mesh->peers[i].enabled) { + continue; + } + + configured++; + + if (mesh->peers[i].circuit_host[0] == '\0') { + continue; + } + + if (proxy_peer_connect(&g_rt.peers[i]) == HYBBX_OK && + g_rt.peers[i].fd >= 0) { + linked++; + } + } + + if (configured == 0) { + hybbx_log_warn("[mains_proxy] no active peers configured"); + return HYBBX_ERR_NOT_FOUND; + } + + mesh->running = 1; + g_rt.mesh.running = 1; + + hybbx_log_info("[mains_proxy] mesh started (%u peer(s) configured, %u HBX link(s))", + configured, linked); + + return HYBBX_OK; +} + +void hybbx_mains_proxy_mesh_stop(hybbx_mains_proxy_mesh_t *mesh) +{ + unsigned i; + + if (mesh != NULL && mesh->running) { + hybbx_log_info("[mains_proxy] mesh stopped"); + mesh->running = 0; + } + + for (i = 0; i < HYBBX_MAINS_PROXY_MAX_PEERS; i++) { + proxy_peer_disconnect(&g_rt.peers[i]); + } + + memset(&g_rt, 0, sizeof(g_rt)); +} + +void hybbx_mains_proxy_mesh_tick(hybbx_service_t *service, + hybbx_mains_proxy_mesh_t *mesh) +{ + unsigned i; + static unsigned tick_ms; + + (void)service; + + if (mesh == NULL || !mesh->running) { + return; + } + + tick_ms += 50; + if (tick_ms >= 1000) { + tick_ms = 0; + } + + for (i = 0; i < mesh->peer_count; i++) { + mains_proxy_peer_runtime_t *peer = &g_rt.peers[i]; + struct pollfd pfd; + uint8_t buf[512]; + size_t read_len; + hybbx_result_t rc; + int pr; + + if (!peer->config.enabled) { + continue; + } + + if (peer->fd < 0) { + if (peer->config.circuit_host[0] == '\0') { + continue; + } + if (peer->reconnect_ms > 0) { + if (tick_ms != 0) { + continue; + } + if (peer->reconnect_ms > 50) { + peer->reconnect_ms -= 50; + continue; + } + peer->reconnect_ms = 0; + } + (void)proxy_peer_connect(peer); + continue; + } + + pfd.fd = peer->fd; + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, 0); + if (pr <= 0) { + continue; + } + + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + hybbx_log_stats("[mains_proxy] peer '%s' disconnected", + peer->config.peer_id[0] != '\0' ? peer->config.peer_id + : "(unnamed)"); + proxy_peer_disconnect(peer); + continue; + } + + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + rc = hybbx_circuit_link_read(peer->fd, buf, sizeof(buf), &read_len); + if (rc != HYBBX_OK || read_len == 0) { + proxy_peer_disconnect(peer); + continue; + } + + hybbx_circuit_decoder_feed(&peer->dec, buf, read_len, + on_mesh_frame, peer); + } +} + +int hybbx_mains_proxy_mesh_active(void) +{ + return g_rt.mesh.running && g_rt.live_links > 0; +} + +hybbx_result_t hybbx_mains_proxy_send_mail(hybbx_service_t *service, + const char *from_address, + const char *to_address, + const char *subject, + const char *body) +{ + char user[HYBBX_USER_NAME_MAX]; + char remote[HYBBX_PROXYMAIL_SERVICE_NAME_MAX]; + char payload[HYBBX_CIRCUIT_MAX_PAYLOAD + 1]; + size_t payload_len; + mains_proxy_peer_runtime_t *peer; + + (void)service; + + if (!g_rt.mesh.running || g_rt.live_links == 0) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (!hybbx_proxymail_parse_address(to_address, user, sizeof(user), + remote, sizeof(remote))) { + return HYBBX_ERR_INVALID; + } + + peer = proxy_find_peer_for_service(remote); + if (peer == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + payload_len = proxy_build_mail_payload(payload, sizeof(payload), + from_address, to_address, + subject != NULL ? subject : "", + body != NULL ? body : "", + MAINS_PROXY_DEFAULT_TTL); + if (payload_len == 0) { + return HYBBX_ERR_INVALID; + } + + return proxy_peer_send(peer->fd, HYBBX_CIRCUIT_PROTO_PROXY_MAIL, + (const uint8_t *)payload, payload_len); +} + +hybbx_result_t hybbx_mains_proxy_send_chat(hybbx_service_t *service, + const char *from_address, + const char *line) +{ + char payload[HYBBX_CIRCUIT_MAX_PAYLOAD + 1]; + size_t payload_len; + unsigned i; + hybbx_result_t rc = HYBBX_ERR_NOT_FOUND; + int sent = 0; + + (void)service; + + if (!g_rt.mesh.running || g_rt.live_links == 0 || line == NULL || + line[0] == '\0') { + return HYBBX_ERR_UNSUPPORTED; + } + + payload_len = proxy_build_chat_payload(payload, sizeof(payload), + from_address, line, + MAINS_PROXY_DEFAULT_TTL); + if (payload_len == 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < g_rt.mesh.peer_count; i++) { + if (g_rt.peers[i].fd < 0) { + continue; + } + + if (proxy_peer_send(g_rt.peers[i].fd, HYBBX_CIRCUIT_PROTO_PROXY_CHAT, + (const uint8_t *)payload, + payload_len) == HYBBX_OK) { + sent = 1; + rc = HYBBX_OK; + } + } + + return sent ? rc : HYBBX_ERR_NOT_FOUND; +} + +void hybbx_mains_proxy_inbound_frame(hybbx_service_t *service, + hybbx_circuit_proto_t proto, + const uint8_t *payload, size_t len) +{ + if (service == NULL || payload == NULL || len == 0) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_PROXY_MAIL && + proto != HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + return; + } + + proxy_dispatch_payload(service, proto, payload, len); +} diff --git a/src/core/max25.c b/src/core/max25.c new file mode 100644 index 0000000..2770102 --- /dev/null +++ b/src/core/max25.c @@ -0,0 +1,507 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/max25.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include <arpa/inet.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <poll.h> +#include <sys/socket.h> +#include <unistd.h> +#endif + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +static const char *find_prefixed_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + char prefixed[HYBBX_CONFIG_KEY_MAX + 8]; + + snprintf(prefixed, sizeof(prefixed), "max25_%s", key); + return find_kv(config, prefixed, scratch, scratch_len); +} + +void hybbx_max25_config_defaults(hybbx_max25_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + cfg->check = 1; + hybbx_strlcpy(cfg->host, HYBBX_MAX25_DEFAULT_HOST, sizeof(cfg->host)); + cfg->port = HYBBX_MAX25_DEFAULT_PORT; + cfg->timeout_ms = HYBBX_MAX25_PROBE_TIMEOUT_MS; +} + +void hybbx_max25_config_apply(hybbx_max25_config_t *cfg, + const hybbx_config_t *config) +{ + const char *host; + + if (cfg == NULL || config == NULL) { + return; + } + + hybbx_max25_config_defaults(cfg); + cfg->check = hybbx_config_get_bool(config, "max25", "check", 1); + host = hybbx_config_get(config, "max25", "host", HYBBX_MAX25_DEFAULT_HOST); + hybbx_strlcpy(cfg->host, host, sizeof(cfg->host)); + cfg->port = hybbx_config_get_uint(config, "max25", "port", + HYBBX_MAX25_DEFAULT_PORT, 1u, 65535u); + cfg->timeout_ms = hybbx_config_get_uint(config, "max25", "timeout_ms", + HYBBX_MAX25_PROBE_TIMEOUT_MS, + 100u, 60000u); +} + +void hybbx_max25_config_parse_kv(const char *max25_kv, + hybbx_max25_config_t *cfg) +{ + char scratch[HYBBX_CONFIG_VALUE_MAX]; + const char *value; + unsigned long n; + char *end; + + if (cfg == NULL) { + return; + } + + hybbx_max25_config_defaults(cfg); + if (max25_kv == NULL || max25_kv[0] == '\0') { + cfg->check = 0; + return; + } + + value = find_prefixed_kv(max25_kv, "check", scratch, sizeof(scratch)); + if (value != NULL) { + cfg->check = hybbx_parse_bool(value, 1); + } else { + cfg->check = 0; + } + + value = find_prefixed_kv(max25_kv, "host", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(cfg->host, value, sizeof(cfg->host)); + } + + value = find_prefixed_kv(max25_kv, "port", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + n = strtoul(value, &end, 10); + if (end != value && n > 0 && n <= 65535u) { + cfg->port = (unsigned)n; + } + } + + value = find_prefixed_kv(max25_kv, "timeout_ms", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + n = strtoul(value, &end, 10); + if (end != value && n >= 100 && n <= 60000) { + cfg->timeout_ms = (unsigned)n; + } + } +} + +const char *hybbx_max25_config_skip_prefix(const char *config, + hybbx_max25_config_t *cfg) +{ + char scratch[512]; + const char *sep; + size_t len; + + if (config == NULL || cfg == NULL) { + return config; + } + + sep = strchr(config, HYBBX_PACKET_RADIO_INSTANCE_SEP); + len = sep != NULL ? (size_t)(sep - config) : strlen(config); + if (len == 0 || len >= sizeof(scratch)) { + hybbx_max25_config_defaults(cfg); + cfg->check = 0; + return config; + } + + memcpy(scratch, config, len); + scratch[len] = '\0'; + + if (find_prefixed_kv(scratch, "check", scratch, sizeof(scratch)) == NULL) { + hybbx_max25_config_defaults(cfg); + cfg->check = 0; + return config; + } + + hybbx_max25_config_parse_kv(scratch, cfg); + return sep != NULL ? sep + 1 : config + len; +} + +void hybbx_max25_status_clear(hybbx_max25_status_t *status) +{ + if (status == NULL) { + return; + } + + memset(status, 0, sizeof(*status)); +} + +#if !defined(_WIN32) && !defined(__AMIGA__) + +static void max25_status_assign(char *dst, size_t dst_len, const char *value) +{ + if (dst == NULL || dst_len == 0) { + return; + } + + dst[0] = '\0'; + if (value == NULL || value[0] == '\0') { + return; + } + + hybbx_strlcpy(dst, value, dst_len); +} + +static void max25_status_parse_line(const char *line, + hybbx_max25_status_t *status) +{ + char buf[512]; + char *save = NULL; + char *tok; + const char *payload; + + if (status == NULL || line == NULL) { + return; + } + + if (strncmp(line, "STATUS ", 7) != 0) { + return; + } + + payload = line + 7; + if (strlen(payload) >= sizeof(buf)) { + return; + } + + hybbx_strlcpy(buf, payload, sizeof(buf)); + + for (tok = strtok_r(buf, " ", &save); tok != NULL; + tok = strtok_r(NULL, " ", &save)) { + char *eq = strchr(tok, '='); + + if (eq == NULL) { + continue; + } + + *eq = '\0'; + if (strcmp(tok, "error") == 0) { + max25_status_assign(status->error, sizeof(status->error), eq + 1); + } else if (strcmp(tok, "voice") == 0) { + max25_status_assign(status->voice, sizeof(status->voice), eq + 1); + } else if (strcmp(tok, "stack") == 0) { + max25_status_assign(status->stack, sizeof(status->stack), eq + 1); + } else if (strcmp(tok, "serial") == 0) { + max25_status_assign(status->serial, sizeof(status->serial), eq + 1); + } + } +} + +static int max25_read_line(int fd, char *buf, size_t buflen, + unsigned timeout_ms) +{ + size_t pos = 0; + unsigned elapsed = 0; + const unsigned step_ms = 50u; + + if (buf == NULL || buflen == 0) { + return -1; + } + + buf[0] = '\0'; + + while (pos + 1 < buflen && elapsed <= timeout_ms) { + struct pollfd pfd; + char ch; + ssize_t n; + + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + + if (poll(&pfd, 1, (int)step_ms) <= 0) { + elapsed += step_ms; + continue; + } + + n = recv(fd, &ch, 1, 0); + if (n <= 0) { + return -1; + } + + if (ch == '\n') { + buf[pos] = '\0'; + while (pos > 0 && (buf[pos - 1] == '\r' || buf[pos - 1] == ' ')) { + buf[--pos] = '\0'; + } + return 0; + } + + if (ch != '\r') { + buf[pos++] = ch; + } + } + + return -1; +} + +static int max25_tcp_connect(const char *host, unsigned port, + unsigned timeout_ms, int *out_fd) +{ + struct sockaddr_in6 addr6; + struct sockaddr_in addr4; + struct pollfd pfd; + const struct sockaddr *addr; + socklen_t addr_len; + int fd; + int flags; + int rc; + int so_error = 0; + socklen_t so_len = sizeof(so_error); + + if (out_fd == NULL) { + return -1; + } + + *out_fd = -1; + + if (host == NULL || host[0] == '\0' || port == 0u) { + return -1; + } + + if (strchr(host, ':') != NULL) { + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) { + return -1; + } + fd = socket(AF_INET6, SOCK_STREAM, 0); + addr = (const struct sockaddr *)&addr6; + addr_len = sizeof(addr6); + } else { + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) { + return -1; + } + fd = socket(AF_INET, SOCK_STREAM, 0); + addr = (const struct sockaddr *)&addr4; + addr_len = sizeof(addr4); + } + + if (fd < 0) { + return -1; + } + + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { + close(fd); + return -1; + } + + rc = connect(fd, addr, addr_len); + if (rc != 0 && errno != EINPROGRESS) { + close(fd); + return -1; + } + if (rc != 0) { + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; + + rc = poll(&pfd, 1, (int)timeout_ms); + if (rc <= 0) { + close(fd); + return -1; + } + + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &so_len) != 0 || + so_error != 0) { + close(fd); + return -1; + } + } + + if (fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) != 0) { + close(fd); + return -1; + } + + *out_fd = fd; + return 0; +} + +static void max25_log_accepted_status(const hybbx_max25_config_t *cfg, + const hybbx_max25_status_t *status) +{ + const char *error = status->error[0] != '\0' ? status->error : "n/a"; + const char *voice = status->voice[0] != '\0' ? status->voice : "n/a"; + const char *stack = status->stack[0] != '\0' ? status->stack : "n/a"; + const char *serial = status->serial[0] != '\0' ? status->serial : "n/a"; + + hybbx_log_info("[max25] max25d at %s:%u — STATUS accepted " + "(error=%s voice=%s stack=%s serial=%s; MAX25 reporting only)", + cfg->host, cfg->port, error, voice, stack, serial); +} + +static hybbx_result_t max25_handshake(const hybbx_max25_config_t *cfg, + hybbx_max25_status_t *status) +{ + char line[512]; + int fd = -1; + + if (max25_tcp_connect(cfg->host, cfg->port, cfg->timeout_ms, &fd) != 0) { + return HYBBX_ERR_IO; + } + + if (max25_read_line(fd, line, sizeof(line), cfg->timeout_ms) != 0 || + strcmp(line, "OK") != 0) { + close(fd); + return HYBBX_ERR_IO; + } + + if (max25_read_line(fd, line, sizeof(line), cfg->timeout_ms) != 0 || + strncmp(line, "STATUS ", 7) != 0) { + close(fd); + return HYBBX_ERR_IO; + } + + max25_status_parse_line(line, status); + status->handshake_ok = 1; + close(fd); + return HYBBX_OK; +} + +#endif /* !WIN32 && !AMIGA */ + +hybbx_result_t hybbx_max25_probe(const hybbx_max25_config_t *cfg, + hybbx_max25_status_t *status_out) +{ +#if defined(_WIN32) || defined(__AMIGA__) + (void)cfg; + if (status_out != NULL) { + hybbx_max25_status_clear(status_out); + } + return HYBBX_OK; +#else + hybbx_result_t rc; + + if (cfg == NULL || !cfg->check) { + if (status_out != NULL) { + hybbx_max25_status_clear(status_out); + } + return HYBBX_OK; + } + + if (cfg->host[0] == '\0' || cfg->port == 0u) { + return HYBBX_ERR_INVALID; + } + + if (status_out != NULL) { + hybbx_max25_status_clear(status_out); + } + + rc = max25_handshake(cfg, status_out); + if (rc != HYBBX_OK) { + hybbx_log_warn("[max25] max25d unreachable at %s:%u (timeout %u ms)", + cfg->host, cfg->port, cfg->timeout_ms); + return rc; + } + + if (status_out != NULL) { + max25_log_accepted_status(cfg, status_out); + } else { + hybbx_log_info("[max25] max25d reachable at %s:%u — M25/1 handshake OK", + cfg->host, cfg->port); + } + + return HYBBX_OK; +#endif +} + +hybbx_result_t hybbx_max25_wait_ready(const hybbx_max25_config_t *cfg) +{ +#if defined(_WIN32) || defined(__AMIGA__) + (void)cfg; + return HYBBX_OK; +#else + hybbx_max25_status_t status; + unsigned waited_ms = 0; + const unsigned step_ms = 2000u; + + if (cfg == NULL || !cfg->check) { + return HYBBX_OK; + } + + while (hybbx_max25_probe(cfg, &status) != HYBBX_OK) { + if (waited_ms >= HYBBX_MAX25_PROBE_WAIT_MS) { + hybbx_log_warn("[max25] max25d unreachable at %s:%u after %u s " + "— start MAX25 or set [max25] check=no", + cfg->host, cfg->port, + HYBBX_MAX25_PROBE_WAIT_MS / 1000u); + return HYBBX_ERR_IO; + } + if (waited_ms == 0u) { + hybbx_log_info("[max25] waiting for max25d at %s:%u (up to %u s)", + cfg->host, cfg->port, + HYBBX_MAX25_PROBE_WAIT_MS / 1000u); + } + usleep((useconds_t)step_ms * 1000u); + waited_ms += step_ms; + } + + return HYBBX_OK; +#endif +} diff --git a/src/core/messages.c b/src/core/messages.c new file mode 100644 index 0000000..582d31d --- /dev/null +++ b/src/core/messages.c @@ -0,0 +1,110 @@ +#include "hybbx/messages.h" +#include "hybbx/session.h" +#include "hybbx/traffic.h" + +#include <stdio.h> +#include <string.h> + +static hybbx_result_t msg_format_prefixed(char *out, size_t out_len, + const char *prefix, + const char *from_label, + const char *body) +{ + int n; + + if (out == NULL || out_len == 0 || prefix == NULL || body == NULL) { + return HYBBX_ERR_INVALID; + } + + if (from_label != NULL && from_label[0] != '\0') { + n = snprintf(out, out_len, "%s%s: %s", prefix, from_label, body); + } else { + n = snprintf(out, out_len, "%s%s", prefix, body); + } + + if (n < 0 || (size_t)n >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_msg_format_system(char *out, size_t out_len, + const char *body) +{ + return msg_format_prefixed(out, out_len, HYBBX_MSG_PREFIX_SYSTEM, + NULL, body); +} + +hybbx_result_t hybbx_msg_format_sysop(char *out, size_t out_len, + const char *from_label, + const char *body) +{ + return msg_format_prefixed(out, out_len, HYBBX_MSG_PREFIX_SYSOP, + from_label, body); +} + +hybbx_result_t hybbx_msg_format_private(char *out, size_t out_len, + const char *from_label, + const char *body) +{ + return msg_format_prefixed(out, out_len, HYBBX_MSG_PREFIX_PRIVATE, + from_label, body); +} + +static hybbx_result_t msg_send_formatted(struct hybbx_session *session, + hybbx_result_t (*format_fn)(char *, + size_t, + const char *, + const char *), + const char *from_label, + const char *body) +{ + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + + if (session == NULL || body == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = format_fn(line, sizeof(line), from_label, body); + if (rc != HYBBX_OK) { + return rc; + } + + return hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_msg_send_system(struct hybbx_session *session, + const char *body) +{ + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + + if (session == NULL || body == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_msg_format_system(line, sizeof(line), body); + if (rc != HYBBX_OK) { + return rc; + } + + return hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_msg_send_sysop(struct hybbx_session *session, + const char *from_label, + const char *body) +{ + return msg_send_formatted(session, hybbx_msg_format_sysop, + from_label, body); +} + +hybbx_result_t hybbx_msg_send_private(struct hybbx_session *session, + const char *from_label, + const char *body) +{ + return msg_send_formatted(session, hybbx_msg_format_private, + from_label, body); +} 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; +} diff --git a/src/core/networks.c b/src/core/networks.c new file mode 100644 index 0000000..c52554d --- /dev/null +++ b/src/core/networks.c @@ -0,0 +1,191 @@ +#include "hybbx/networks.h" +#include "hybbx/instance.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <string.h> + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int networks_has_key(const hybbx_config_t *config, const char *key) +{ + const char *value; + + if (config == NULL || key == NULL) { + return 0; + } + + value = hybbx_config_get(config, "networks", key, NULL); + return value != NULL && value[0] != '\0'; +} + +void hybbx_networks_config_defaults(hybbx_networks_config_t *networks) +{ + if (networks == NULL) { + return; + } + + networks->telnet = 1; + networks->ax25 = 0; + networks->baycom = 0; + networks->ardop = 0; + networks->crdop = 0; + networks->ssh = 0; + networks->websocket = 0; + networks->circuit = 1; + networks->mains_proxy = 0; +} + +void hybbx_networks_config_apply(hybbx_networks_config_t *networks, + const hybbx_config_t *config) +{ + if (networks == NULL) { + return; + } + + hybbx_networks_config_defaults(networks); + + if (config == NULL) { + return; + } + + if (networks_has_key(config, "telnet")) { + networks->telnet = hybbx_config_get_bool(config, "networks", + "telnet", 1); + } + + networks->ax25 = hybbx_config_get_bool(config, "networks", "ax25", 0); + + if (networks_has_key(config, "baycom")) { + networks->baycom = hybbx_config_get_bool(config, "networks", "baycom", 0); + } + + if (networks_has_key(config, "ardop")) { + networks->ardop = hybbx_config_get_bool(config, "networks", "ardop", 0); + } + + if (networks_has_key(config, "crdop")) { + networks->crdop = hybbx_config_get_bool(config, "networks", "crdop", 0); + } + + networks->ssh = hybbx_config_get_bool(config, "networks", "ssh", 0); + + networks->websocket = hybbx_config_get_bool(config, "networks", + "websocket", 0); + + if (networks_has_key(config, "circuit")) { + networks->circuit = hybbx_config_get_bool(config, "networks", + "circuit", 1); + } else { + networks->circuit = hybbx_config_get_bool(config, "circuit", "enabled", + 1); + } + + if (networks_has_key(config, "mains_proxy")) { + networks->mains_proxy = hybbx_config_get_bool(config, "networks", + "mains_proxy", 0); + } + + hybbx_log_info("[networks] telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s " + "crdop=%s websocket=%s circuit=%s mains_proxy=%s", + hybbx_bool_to_string(networks->telnet), + hybbx_bool_to_string(networks->ssh), + hybbx_bool_to_string(networks->ax25), + hybbx_bool_to_string(networks->baycom), + hybbx_bool_to_string(networks->ardop), + hybbx_bool_to_string(networks->crdop), + hybbx_bool_to_string(networks->websocket), + hybbx_bool_to_string(networks->circuit), + hybbx_bool_to_string(networks->mains_proxy)); +} + +int hybbx_networks_is_static_transport(const char *plugin_name) +{ + if (plugin_name == NULL || plugin_name[0] == '\0') { + return 0; + } + + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_MAIN: + return str_ieq(plugin_name, "websocket"); + case HYBBX_INSTANCE_SECONDARY: + return str_ieq(plugin_name, "mains_proxy"); + case HYBBX_INSTANCE_PROXY: + default: + return 0; + } +} + +int hybbx_networks_transport_wanted(const char *plugin_name, + const hybbx_networks_config_t *networks) +{ + if (plugin_name == NULL || plugin_name[0] == '\0') { + return 0; + } + + if (!hybbx_instance_plugin_allowed(plugin_name)) { + return 0; + } + + if (hybbx_networks_is_static_transport(plugin_name)) { + return 1; + } + + if (networks == NULL) { + return 0; + } + + if (str_ieq(plugin_name, "telnet")) { + return networks->telnet; + } + + if (str_ieq(plugin_name, "packet_radio")) { + return networks->ax25; + } + + if (str_ieq(plugin_name, "baycom")) { + return networks->baycom; + } + + if (str_ieq(plugin_name, "ardop")) { + return networks->ardop; + } + + if (str_ieq(plugin_name, "crdop")) { + return networks->crdop; + } + + if (str_ieq(plugin_name, "ssh")) { + return networks->ssh; + } + + if (str_ieq(plugin_name, "websocket")) { + return networks->websocket; + } + + if (str_ieq(plugin_name, "mains_proxy")) { + return networks->mains_proxy; + } + + return 0; +} diff --git a/src/core/password.c b/src/core/password.c new file mode 100644 index 0000000..1e8854a --- /dev/null +++ b/src/core/password.c @@ -0,0 +1,396 @@ +#include "hybbx/password.h" +#include "hybbx/crypto.h" +#include "hybbx/util.h" + +#include "crypto_backends.h" + +#include <ctype.h> +#include <stdio.h> +#include <stdint.h> +#include <string.h> + +#define MD5_HEX_SIZE (32 + 1) + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int hex_digit(char ch) +{ + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + if (ch >= 'a' && ch <= 'f') { + return 10 + (ch - 'a'); + } + if (ch >= 'A' && ch <= 'F') { + return 10 + (ch - 'A'); + } + return -1; +} + +static int hex_string_eq(const char *a, const char *b, size_t hex_len) +{ + size_t i; + + for (i = 0; i < hex_len; i++) { + if (tolower((unsigned char)a[i]) != tolower((unsigned char)b[i])) { + return 0; + } + } + + return 1; +} + +static int hex_string_valid(const char *s, size_t hex_len) +{ + size_t i; + + if (s == NULL) { + return 0; + } + + for (i = 0; i < hex_len; i++) { + if (hex_digit(s[i]) < 0) { + return 0; + } + } + + return s[hex_len] == '\0'; +} + +int hybbx_password_is_hashed(const char *stored) +{ + if (stored == NULL || stored[0] == '\0') { + return 0; + } + + if (strncmp(stored, HYBBX_PASSWORD_SHA256_PREFIX, 8) == 0) { + return hex_string_valid(stored + 8, 64); + } + + if (strncmp(stored, HYBBX_PASSWORD_MD5_PREFIX, 5) == 0) { + return hex_string_valid(stored + 5, 32); + } + + return 0; +} + +int hybbx_password_is_plain(const char *stored) +{ + if (stored == NULL || stored[0] == '\0') { + return 0; + } + + return !hybbx_password_is_hashed(stored); +} + +hybbx_result_t hybbx_password_hash_sha256(const char *plain, + char *out, + size_t out_size) +{ + char hex[HYBBX_BACKEND_SHA256_HEX_SIZE]; + size_t need; + + if (plain == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + need = strlen(HYBBX_PASSWORD_SHA256_PREFIX) + 64 + 1; + if (out_size < need) { + return HYBBX_ERR_INVALID; + } + + hybbx_backend_sha256_hex(plain, strlen(plain), hex); + snprintf(out, out_size, "%s%s", HYBBX_PASSWORD_SHA256_PREFIX, hex); + return HYBBX_OK; +} + +hybbx_result_t hybbx_password_hash(const char *plain, char *out, size_t out_size) +{ + return hybbx_password_hash_sha256(plain, out, out_size); +} + +/* Minimal MD5 (RFC 1321) for legacy {md5} verification only. */ +typedef struct md5_ctx { + uint32_t state[4]; + uint32_t count[2]; + uint8_t buffer[64]; +} md5_ctx_t; + +#define MD5_F(x, y, z) (((x) & (y)) | ((~x) & (z))) +#define MD5_G(x, y, z) (((x) & (z)) | ((y) & (~z))) +#define MD5_H(x, y, z) ((x) ^ (y) ^ (z)) +#define MD5_I(x, y, z) ((y) ^ ((x) | (~z))) +#define MD5_ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) + +#define MD5_FF(a, b, c, d, x, s, ac) \ + do { \ + (a) += MD5_F((b), (c), (d)) + (x) + (uint32_t)(ac); \ + (a) = MD5_ROTATE_LEFT((a), (s)); \ + (a) += (b); \ + } while (0) + +#define MD5_GG(a, b, c, d, x, s, ac) \ + do { \ + (a) += MD5_G((b), (c), (d)) + (x) + (uint32_t)(ac); \ + (a) = MD5_ROTATE_LEFT((a), (s)); \ + (a) += (b); \ + } while (0) + +#define MD5_HH(a, b, c, d, x, s, ac) \ + do { \ + (a) += MD5_H((b), (c), (d)) + (x) + (uint32_t)(ac); \ + (a) = MD5_ROTATE_LEFT((a), (s)); \ + (a) += (b); \ + } while (0) + +#define MD5_II(a, b, c, d, x, s, ac) \ + do { \ + (a) += MD5_I((b), (c), (d)) + (x) + (uint32_t)(ac); \ + (a) = MD5_ROTATE_LEFT((a), (s)); \ + (a) += (b); \ + } while (0) + +static void md5_encode(uint8_t *out, const uint32_t *in, size_t len) +{ + size_t i; + size_t j; + + for (i = 0, j = 0; j < len; i++, j += 4) { + out[j] = (uint8_t)(in[i] & 0xff); + out[j + 1] = (uint8_t)((in[i] >> 8) & 0xff); + out[j + 2] = (uint8_t)((in[i] >> 16) & 0xff); + out[j + 3] = (uint8_t)((in[i] >> 24) & 0xff); + } +} + +static void md5_transform(uint32_t state[4], const uint8_t block[64]) +{ + uint32_t a = state[0]; + uint32_t b = state[1]; + uint32_t c = state[2]; + uint32_t d = state[3]; + uint32_t x[16]; + size_t i; + + for (i = 0; i < 16; i++) { + x[i] = ((uint32_t)block[i * 4]) | + ((uint32_t)block[i * 4 + 1] << 8) | + ((uint32_t)block[i * 4 + 2] << 16) | + ((uint32_t)block[i * 4 + 3] << 24); + } + + MD5_FF(a, b, c, d, x[0], 7, 0xd76aa478); + MD5_FF(d, a, b, c, x[1], 12, 0xe8c7b756); + MD5_FF(c, d, a, b, x[2], 17, 0x242070db); + MD5_FF(b, c, d, a, x[3], 22, 0xc1bdceee); + MD5_FF(a, b, c, d, x[4], 7, 0xf57c0faf); + MD5_FF(d, a, b, c, x[5], 12, 0x4787c62a); + MD5_FF(c, d, a, b, x[6], 17, 0xa8304613); + MD5_FF(b, c, d, a, x[7], 22, 0xfd469501); + MD5_FF(a, b, c, d, x[8], 7, 0x698098d8); + MD5_FF(d, a, b, c, x[9], 12, 0x8b44f7af); + MD5_FF(c, d, a, b, x[10], 17, 0xffff5bb1); + MD5_FF(b, c, d, a, x[11], 22, 0x895cd7be); + MD5_FF(a, b, c, d, x[12], 7, 0x6b901122); + MD5_FF(d, a, b, c, x[13], 12, 0xfd987193); + MD5_FF(c, d, a, b, x[14], 17, 0xa679438e); + MD5_FF(b, c, d, a, x[15], 22, 0x49b40821); + MD5_GG(a, b, c, d, x[1], 5, 0xf61e2562); + MD5_GG(d, a, b, c, x[6], 9, 0xc040b340); + MD5_GG(c, d, a, b, x[11], 14, 0x265e5a51); + MD5_GG(b, c, d, a, x[0], 20, 0xe9b6c7aa); + MD5_GG(a, b, c, d, x[5], 5, 0xd62f105d); + MD5_GG(d, a, b, c, x[10], 9, 0x02441453); + MD5_GG(c, d, a, b, x[15], 14, 0xd8a1e681); + MD5_GG(b, c, d, a, x[4], 20, 0xe7d3fbc8); + MD5_GG(a, b, c, d, x[9], 5, 0x21e1cde6); + MD5_GG(d, a, b, c, x[14], 9, 0xc33707d6); + MD5_GG(c, d, a, b, x[3], 14, 0xf4d50d87); + MD5_GG(b, c, d, a, x[8], 20, 0x455a14ed); + MD5_GG(a, b, c, d, x[13], 5, 0xa9e3e905); + MD5_GG(d, a, b, c, x[2], 9, 0xfcefa3f8); + MD5_GG(c, d, a, b, x[7], 14, 0x676f02d9); + MD5_GG(b, c, d, a, x[12], 20, 0x8d2a4c8a); + MD5_HH(a, b, c, d, x[5], 4, 0xfffa3942); + MD5_HH(d, a, b, c, x[8], 11, 0x8771f681); + MD5_HH(c, d, a, b, x[11], 16, 0x6d9d6122); + MD5_HH(b, c, d, a, x[14], 23, 0xfde5380c); + MD5_HH(a, b, c, d, x[1], 4, 0xa4beea44); + MD5_HH(d, a, b, c, x[4], 11, 0x4bdecfa9); + MD5_HH(c, d, a, b, x[7], 16, 0xf6bb4b60); + MD5_HH(b, c, d, a, x[10], 23, 0xbebfbc70); + MD5_HH(a, b, c, d, x[13], 4, 0x289b7ec6); + MD5_HH(d, a, b, c, x[0], 11, 0xeaa127fa); + MD5_HH(c, d, a, b, x[3], 16, 0xd4ef3085); + MD5_HH(b, c, d, a, x[6], 23, 0x04881d05); + MD5_HH(a, b, c, d, x[9], 4, 0xd9d4d039); + MD5_HH(d, a, b, c, x[12], 11, 0xe6db99e5); + MD5_HH(c, d, a, b, x[15], 16, 0x1fa27cf8); + MD5_HH(b, c, d, a, x[2], 23, 0xc4ac5665); + MD5_II(a, b, c, d, x[0], 6, 0xf4292244); + MD5_II(d, a, b, c, x[7], 10, 0x432aff97); + MD5_II(c, d, a, b, x[14], 15, 0xab9423a7); + MD5_II(b, c, d, a, x[5], 21, 0xfc93a039); + MD5_II(a, b, c, d, x[12], 6, 0x655b59c3); + MD5_II(d, a, b, c, x[3], 10, 0x8f0ccc92); + MD5_II(c, d, a, b, x[10], 15, 0xffeff47d); + MD5_II(b, c, d, a, x[1], 21, 0x85845dd1); + MD5_II(a, b, c, d, x[8], 6, 0x6fa87e4f); + MD5_II(d, a, b, c, x[15], 10, 0xfe2ce6e0); + MD5_II(c, d, a, b, x[6], 15, 0xa3014314); + MD5_II(b, c, d, a, x[13], 21, 0x4e0811a1); + MD5_II(a, b, c, d, x[4], 6, 0xf7537e82); + MD5_II(d, a, b, c, x[11], 10, 0xbd3af235); + MD5_II(c, d, a, b, x[2], 15, 0x2ad7d2bb); + MD5_II(b, c, d, a, x[9], 21, 0xeb86d391); + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; +} + +static void md5_init(md5_ctx_t *ctx) +{ + ctx->count[0] = 0; + ctx->count[1] = 0; + ctx->state[0] = 0x67452301; + ctx->state[1] = 0xefcdab89; + ctx->state[2] = 0x98badcfe; + ctx->state[3] = 0x10325476; +} + +static void md5_update(md5_ctx_t *ctx, const uint8_t *data, size_t len) +{ + size_t i; + size_t index = (size_t)((ctx->count[0] >> 3) & 0x3f); + + if ((ctx->count[0] += (uint32_t)(len << 3)) < (uint32_t)(len << 3)) { + ctx->count[1]++; + } + ctx->count[1] += (uint32_t)(len >> 29); + + size_t part_len = 64 - index; + + if (len >= part_len) { + memcpy(&ctx->buffer[index], data, part_len); + md5_transform(ctx->state, ctx->buffer); + for (i = part_len; i + 63 < len; i += 64) { + memcpy(ctx->buffer, &data[i], 64); + md5_transform(ctx->state, ctx->buffer); + } + index = 0; + } else { + i = 0; + } + + memcpy(&ctx->buffer[index], &data[i], len - i); +} + +static void md5_finalize_hex(md5_ctx_t *ctx, char *hex) +{ + uint8_t bits[8]; + uint8_t digest[16]; + size_t index = (size_t)((ctx->count[0] >> 3) & 0x3f); + size_t pad_len = (index < 56) ? (56 - index) : (120 - index); + static const uint8_t padding[64] = { 0x80 }; + size_t i; + + md5_encode(bits, ctx->count, 8); + md5_update(ctx, padding, pad_len); + md5_update(ctx, bits, 8); + md5_encode(digest, ctx->state, 16); + + for (i = 0; i < 16; i++) { + snprintf(hex + i * 2, 3, "%02x", digest[i]); + } +} + +static void md5_hex(const char *plain, char *hex) +{ + md5_ctx_t ctx; + + md5_init(&ctx); + md5_update(&ctx, (const uint8_t *)plain, strlen(plain)); + md5_finalize_hex(&ctx, hex); +} + +int hybbx_password_match(const char *stored, const char *provided) +{ + char hex[HYBBX_BACKEND_SHA256_HEX_SIZE]; + char md5[MD5_HEX_SIZE]; + + if (provided == NULL || provided[0] == '\0') { + return 0; + } + + if (stored == NULL || stored[0] == '\0') { + return 0; + } + + if (strncmp(stored, HYBBX_PASSWORD_SHA256_PREFIX, 8) == 0) { + hybbx_backend_sha256_hex(provided, strlen(provided), hex); + return hex_string_eq(stored + 8, hex, 64); + } + + if (strncmp(stored, HYBBX_PASSWORD_MD5_PREFIX, 5) == 0) { + md5_hex(provided, md5); + return hex_string_eq(stored + 5, md5, 32); + } + + return str_ieq(stored, provided); +} + +hybbx_result_t hybbx_password_generate_alnum(char *out, + size_t out_size, + size_t min_len, + size_t max_len) +{ + static const char alphabet[] = "abcdefghijklmnopqrstuvwxyz0123456789"; + uint8_t rnd[32]; + size_t len; + size_t span; + size_t i; + size_t rnd_off = sizeof(rnd); + + if (out == NULL || out_size == 0 || min_len == 0 || min_len > max_len || + max_len >= out_size) { + return HYBBX_ERR_INVALID; + } + + span = max_len - min_len + 1u; + if (hybbx_crypto_random(rnd, sizeof(rnd)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + len = min_len + (size_t)(rnd[0] % (unsigned)span); + for (i = 0; i < len; i++) { + if (rnd_off >= sizeof(rnd)) { + if (hybbx_crypto_random(rnd, sizeof(rnd)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + rnd_off = 0; + } + out[i] = alphabet[rnd[rnd_off++] % 36u]; + } + out[len] = '\0'; + return HYBBX_OK; +} diff --git a/src/core/privilege.c b/src/core/privilege.c new file mode 100644 index 0000000..254dbc6 --- /dev/null +++ b/src/core/privilege.c @@ -0,0 +1,233 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/privilege.h" +#include "hybbx/config.h" +#include "hybbx/log.h" +#include "hybbx/types.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#if !defined(_WIN32) +#include <errno.h> +#include <grp.h> +#include <pwd.h> +#include <sys/types.h> +#include <unistd.h> +#endif + +#if !defined(_WIN32) + +static int parse_optional_uid(const char *raw, uid_t *out) +{ + char *end; + unsigned long value; + + if (raw == NULL || raw[0] == '\0') { + return 0; + } + errno = 0; + value = strtoul(raw, &end, 10); + if (end == raw || *end != '\0' || errno != 0) { + return -1; + } + *out = (uid_t)value; + return 1; +} + +static int parse_optional_gid(const char *raw, gid_t *out) +{ + char *end; + unsigned long value; + + if (raw == NULL || raw[0] == '\0') { + return 0; + } + errno = 0; + value = strtoul(raw, &end, 10); + if (end == raw || *end != '\0' || errno != 0) { + return -1; + } + *out = (gid_t)value; + return 1; +} + +static hybbx_result_t resolve_target( + const hybbx_config_t *config, + uid_t *uid_out, + gid_t *gid_out, + char name_buf[64], + size_t name_buf_len) +{ + const char *user_name; + const char *group_name; + const char *uid_raw; + const char *gid_raw; + struct passwd *pw = NULL; + struct group *gr = NULL; + uid_t uid_override = 0; + gid_t gid_override = 0; + int have_uid = 0; + int have_gid = 0; + + user_name = hybbx_config_get(config, "service", "user", NULL); + group_name = hybbx_config_get(config, "service", "group", NULL); + uid_raw = hybbx_config_get(config, "service", "uid", NULL); + gid_raw = hybbx_config_get(config, "service", "gid", NULL); + + have_uid = parse_optional_uid(uid_raw, &uid_override); + if (have_uid < 0) { + hybbx_log_warn("[service] invalid uid=%s", uid_raw); + return HYBBX_ERR_INVALID; + } + have_gid = parse_optional_gid(gid_raw, &gid_override); + if (have_gid < 0) { + hybbx_log_warn("[service] invalid gid=%s", gid_raw); + return HYBBX_ERR_INVALID; + } + + if (user_name != NULL && user_name[0] != '\0') { + pw = getpwnam(user_name); + if (pw == NULL) { + hybbx_log_warn("[service] unknown user=%s", user_name); + return HYBBX_ERR_INVALID; + } + *uid_out = pw->pw_uid; + *gid_out = pw->pw_gid; + if (name_buf_len > 0) { + snprintf(name_buf, name_buf_len, "%s", pw->pw_name); + } + } else if (have_uid) { + pw = getpwuid(uid_override); + if (pw == NULL) { + hybbx_log_warn("[service] unknown uid=%s", uid_raw); + return HYBBX_ERR_INVALID; + } + *uid_out = pw->pw_uid; + *gid_out = pw->pw_gid; + if (name_buf_len > 0) { + snprintf(name_buf, name_buf_len, "%s", pw->pw_name); + } + } else { + return HYBBX_ERR_INVALID; + } + + if (have_uid) { + *uid_out = uid_override; + } + if (group_name != NULL && group_name[0] != '\0') { + gr = getgrnam(group_name); + if (gr == NULL) { + hybbx_log_warn("[service] unknown group=%s", group_name); + return HYBBX_ERR_INVALID; + } + *gid_out = gr->gr_gid; + } else if (have_gid) { + if (getgrgid(gid_override) == NULL) { + hybbx_log_warn("[service] unknown gid=%s", gid_raw); + return HYBBX_ERR_INVALID; + } + *gid_out = gid_override; + } + + return HYBBX_OK; +} + +static hybbx_result_t drop_to(uid_t uid, gid_t gid, const char *name) +{ + if (getuid() != 0) { + hybbx_log_warn("[service] privilege drop requires root start (euid=0)"); + return HYBBX_ERR_DENIED; + } + + if (name != NULL && name[0] != '\0') { + if (initgroups(name, gid) != 0) { + hybbx_log_warn("[service] initgroups(%s) failed: %s", + name, strerror(errno)); + return HYBBX_ERR_IO; + } + } + + if (setgid(gid) != 0) { + hybbx_log_warn("[service] setgid(%u) failed: %s", + (unsigned)gid, strerror(errno)); + return HYBBX_ERR_IO; + } + + if (setuid(uid) != 0) { + hybbx_log_warn("[service] setuid(%u) failed: %s", + (unsigned)uid, strerror(errno)); + return HYBBX_ERR_IO; + } + + if (setuid(0) == 0 || seteuid(0) == 0) { + hybbx_log_warn("[service] privilege drop incomplete — still root"); + return HYBBX_ERR_IO; + } + + hybbx_log_info("[service] dropped privileges to uid=%u gid=%u (%s)", + (unsigned)uid, (unsigned)gid, + name != NULL && name[0] != '\0' ? name : "?"); + return HYBBX_OK; +} + +#endif /* !_WIN32 */ + +hybbx_result_t hybbx_privilege_apply_from_config(const hybbx_config_t *config) +{ +#if defined(_WIN32) + (void)config; + return HYBBX_OK; +#else + const char *user_name; + uid_t target_uid = 0; + gid_t target_gid = 0; + char resolved_name[64]; + hybbx_result_t rc; + + if (config == NULL) { + return HYBBX_ERR_INVALID; + } + + resolved_name[0] = '\0'; + user_name = hybbx_config_get(config, "service", "user", NULL); + + if (geteuid() == 0) { + if ((user_name == NULL || user_name[0] == '\0') && + hybbx_config_get(config, "service", "uid", NULL) == NULL) { + hybbx_log_warn( + "[service] refusing to run as root — set [service] user= " + "(and optional group=)"); + return HYBBX_ERR_DENIED; + } + + rc = resolve_target(config, &target_uid, &target_gid, + resolved_name, sizeof(resolved_name)); + if (rc != HYBBX_OK) { + return rc; + } + + return drop_to(target_uid, target_gid, + resolved_name[0] != '\0' ? resolved_name : user_name); + } + + if (user_name != NULL && user_name[0] != '\0') { + struct passwd *pw = getpwnam(user_name); + + if (pw == NULL) { + hybbx_log_warn("[service] unknown configured user=%s", user_name); + return HYBBX_OK; + } + if ((uid_t)geteuid() != pw->pw_uid) { + hybbx_log_warn( + "[service] running as uid=%u but [service] user=%s (uid=%u)", + (unsigned)geteuid(), user_name, (unsigned)pw->pw_uid); + } + } + + return HYBBX_OK; +#endif +} diff --git a/src/core/proxychat.c b/src/core/proxychat.c new file mode 100644 index 0000000..d26a178 --- /dev/null +++ b/src/core/proxychat.c @@ -0,0 +1,134 @@ +#include "hybbx/proxychat.h" +#include "hybbx/mains_proxy.h" +#include "hybbx/service.h" +#include "hybbx/session.h" + +#include <stdio.h> +#include <string.h> + +typedef struct proxychat_fanout_ctx { + hybbx_session_t *from; + const char *line; + const char *from_address; +} proxychat_fanout_ctx_t; + +static void proxychat_local_visitor(hybbx_session_t *session, void *userdata) +{ + proxychat_fanout_ctx_t *ctx = (proxychat_fanout_ctx_t *)userdata; + char line[HYBBX_PROXYMAIL_ADDRESS_MAX + HYBBX_LINE_MAX + 8]; + + if (session == NULL || ctx == NULL || ctx->line == NULL) { + return; + } + + if (session == ctx->from) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_PROXYCHAT) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + snprintf(line, sizeof(line), "<%s> %s", + ctx->from_address != NULL ? ctx->from_address : "?", + ctx->line); + hybbx_session_write_line(session, line); +} + +static void proxychat_show_local(hybbx_session_t *session, + const char *from_address, + const char *line) +{ + char out[HYBBX_PROXYMAIL_ADDRESS_MAX + HYBBX_LINE_MAX + 8]; + + snprintf(out, sizeof(out), "<%s> %s", + from_address != NULL ? from_address : "?", line); + hybbx_session_write_line(session, out); +} + +hybbx_result_t hybbx_proxychat_post(hybbx_service_t *service, + hybbx_session_t *session, + const char *line) +{ + char from_address[HYBBX_PROXYMAIL_ADDRESS_MAX]; + proxychat_fanout_ctx_t ctx; + hybbx_result_t rc; + + if (session == NULL || line == NULL || line[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (service == NULL) { + return HYBBX_ERR_INVALID; + } + + snprintf(from_address, sizeof(from_address), "%s@%s", + hybbx_session_display_name(session), + hybbx_service_get_name(service)); + + proxychat_show_local(session, from_address, line); + + if (!hybbx_mains_proxy_mesh_active()) { + hybbx_session_write_line(session, + "Mains proxy is not running — message shown locally only."); + return HYBBX_OK; + } + + ctx.from = session; + ctx.line = line; + ctx.from_address = from_address; + hybbx_service_visit_sessions(service, proxychat_local_visitor, &ctx); + + rc = hybbx_mains_proxy_send_chat(service, from_address, line); + if (rc == HYBBX_ERR_UNSUPPORTED) { + hybbx_session_write_line(session, + "No outbound peer links — message shown locally only."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, + "No peer links available to relay your message."); + return HYBBX_OK; + } + + return rc; +} + +void hybbx_proxychat_receive(hybbx_service_t *service, + const char *from_address, + const char *line) +{ + proxychat_fanout_ctx_t ctx; + + if (service == NULL || line == NULL || line[0] == '\0') { + return; + } + + ctx.from = NULL; + ctx.line = line; + ctx.from_address = from_address; + hybbx_service_visit_sessions(service, proxychat_local_visitor, &ctx); +} + +void hybbx_proxychat_show_banner(hybbx_session_t *session) +{ + if (session == NULL) { + return; + } + + hybbx_session_write_line(session, + "Proxychat — talk with users on linked mains."); + if (hybbx_mains_proxy_mesh_active()) { + hybbx_session_write_line(session, + "Type a line to send; /leave or /main to exit."); + } else { + hybbx_session_write_line(session, + "No peer links active — check mains_proxy configuration."); + hybbx_session_write_line(session, + "Type a line to try; /leave or /main to exit."); + } +} diff --git a/src/core/proxymail.c b/src/core/proxymail.c new file mode 100644 index 0000000..3981dde --- /dev/null +++ b/src/core/proxymail.c @@ -0,0 +1,676 @@ +#include "hybbx/proxymail.h" +#include "hybbx/mains_proxy.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/storage.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" + +#include <dirent.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> + +#define HYBBX_PROXYMAIL_DIR_NAME "proxymail" +#define HYBBX_PROXYMAIL_INBOX_NAME "inbox" +#define HYBBX_PROXYMAIL_RECYCLE_NAME "recycle" +#define HYBBX_PROXYMAIL_NEXT_FILE "proxymail.next" + +typedef struct proxymail_entry { + uint64_t id; + char from[HYBBX_PROXYMAIL_ADDRESS_MAX]; + char subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + time_t received_at; + int read; +} proxymail_entry_t; + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static hybbx_result_t proxymail_root_path(hybbx_service_t *service, + char *out, size_t out_len) +{ + hybbx_storage_t *storage; + const char *root; + + storage = hybbx_service_get_storage(service); + root = hybbx_storage_root_path(storage); + if (root == NULL || root[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, root, HYBBX_PROXYMAIL_DIR_NAME); +} + +static hybbx_result_t proxymail_user_inbox_path(hybbx_service_t *service, + const char *username, + char *out, size_t out_len) +{ + char root[HYBBX_PATH_MAX]; + char user_dir[HYBBX_PATH_MAX]; + char user_norm[HYBBX_USER_NAME_MAX]; + + if (username == NULL || username[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (proxymail_root_path(service, root, sizeof(root)) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(user_norm, username, sizeof(user_norm)); + hybbx_username_normalize(user_norm); + + if (hybbx_path_join(user_dir, sizeof(user_dir), root, user_norm) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, user_dir, HYBBX_PROXYMAIL_INBOX_NAME); +} + +static int parse_msg_filename(const char *name, uint64_t *id) +{ + char *end; + + if (name == NULL || id == NULL) { + return 0; + } + + if (strlen(name) < 5 || strcmp(name + strlen(name) - 4, ".msg") != 0) { + return 0; + } + + *id = (uint64_t)strtoull(name, &end, 10); + return end != name && strcmp(end, ".msg") == 0; +} + +static int proxymail_entry_cmp(const void *a, const void *b) +{ + const proxymail_entry_t *ea = (const proxymail_entry_t *)a; + const proxymail_entry_t *eb = (const proxymail_entry_t *)b; + + if (ea->id < eb->id) { + return 1; + } + if (ea->id > eb->id) { + return -1; + } + return 0; +} + +static hybbx_result_t proxymail_load_inbox(hybbx_service_t *service, + const char *username, + proxymail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + char inbox_path[HYBBX_PATH_MAX]; + DIR *dir; + struct dirent *ent; + size_t count = 0; + + if (proxymail_user_inbox_path(service, username, inbox_path, + sizeof(inbox_path)) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + *out_count = 0; + + dir = opendir(inbox_path); + if (dir == NULL) { + return errno == ENOENT ? HYBBX_OK : HYBBX_ERR_IO; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + proxymail_entry_t entry; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (count >= max_entries) { + break; + } + + memset(&entry, 0, sizeof(entry)); + entry.id = id; + + if (hybbx_path_join(path, sizeof(path), inbox_path, ent->d_name) != + HYBBX_OK) { + continue; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + continue; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char *eq = strchr(line, '='); + char *key; + char *value; + + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + break; + } + + if (eq == NULL) { + continue; + } + + *eq = '\0'; + key = line; + value = eq + 1; + + while (*value == ' ' || *value == '\t') { + value++; + } + + { + size_t vlen = strlen(value); + + while (vlen > 0 && (value[vlen - 1] == '\n' || + value[vlen - 1] == '\r')) { + value[--vlen] = '\0'; + } + } + + if (str_ieq(key, "from")) { + hybbx_strlcpy(entry.from, value, sizeof(entry.from)); + } else if (str_ieq(key, "subject")) { + hybbx_strlcpy(entry.subject, value, sizeof(entry.subject)); + } else if (str_ieq(key, "time")) { + entry.received_at = (time_t)strtol(value, NULL, 10); + } else if (str_ieq(key, "read")) { + entry.read = hybbx_bool_is_true(value); + } + } + + fclose(fp); + entries[count++] = entry; + } + + closedir(dir); + qsort(entries, count, sizeof(entries[0]), proxymail_entry_cmp); + *out_count = count; + return HYBBX_OK; +} + +static hybbx_result_t proxymail_next_id(hybbx_service_t *service, + uint64_t *out_id) +{ + char root[HYBBX_PATH_MAX]; + char counter_path[HYBBX_PATH_MAX]; + FILE *fp; + unsigned long long n = 0; + + if (proxymail_root_path(service, root, sizeof(root)) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + if (mkdir_p(root) != 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(counter_path, sizeof(counter_path), root, + HYBBX_PROXYMAIL_NEXT_FILE) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + fp = fopen(counter_path, "r"); + if (fp != NULL) { + if (fscanf(fp, "%llu", &n) != 1) { + n = 0; + } + fclose(fp); + } + + n++; + *out_id = (uint64_t)n; + + fp = fopen(counter_path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%llu\n", n); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t proxymail_store_message(hybbx_service_t *service, + const char *username, + const char *from_address, + const char *subject, + const char *body) +{ + char inbox_path[HYBBX_PATH_MAX]; + char msg_path[HYBBX_PATH_MAX]; + char msg_name[32]; + uint64_t id; + FILE *fp; + hybbx_result_t rc; + + rc = proxymail_next_id(service, &id); + if (rc != HYBBX_OK) { + return rc; + } + + if (proxymail_user_inbox_path(service, username, inbox_path, + sizeof(inbox_path)) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + if (mkdir_p(inbox_path) != 0) { + return HYBBX_ERR_IO; + } + + snprintf(msg_name, sizeof(msg_name), "%06llu.msg", + (unsigned long long)id); + if (hybbx_path_join(msg_path, sizeof(msg_path), inbox_path, msg_name) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + fp = fopen(msg_path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "from=%s\n", from_address); + fprintf(fp, "subject=%s\n", subject != NULL ? subject : ""); + fprintf(fp, "time=%ld\n", (long)time(NULL)); + fprintf(fp, "read=no\n"); + fprintf(fp, "---\n%s\n", body != NULL ? body : ""); + fclose(fp); + return HYBBX_OK; +} + +int hybbx_proxymail_parse_address(const char *address, + char *user, size_t user_len, + char *remote_service, size_t remote_len) +{ + const char *at; + size_t ulen; + size_t slen; + + if (user != NULL && user_len > 0) { + user[0] = '\0'; + } + if (remote_service != NULL && remote_len > 0) { + remote_service[0] = '\0'; + } + + if (address == NULL || address[0] == '\0') { + return 0; + } + + at = strchr(address, '@'); + if (at == NULL || at == address || at[1] == '\0') { + return 0; + } + + ulen = (size_t)(at - address); + slen = strlen(at + 1); + if (ulen == 0 || slen == 0 || ulen >= HYBBX_USER_NAME_MAX || + slen >= HYBBX_PROXYMAIL_SERVICE_NAME_MAX) { + return 0; + } + + if (user != NULL && user_len > 0) { + memcpy(user, address, ulen); + user[ulen] = '\0'; + hybbx_username_normalize(user); + if (user[0] == '\0') { + return 0; + } + } + + if (remote_service != NULL && remote_len > 0) { + hybbx_strlcpy(remote_service, at + 1, remote_len); + } + + return 1; +} + +static void proxymail_list_print(hybbx_session_t *session, + const proxymail_entry_t *entries, + size_t count, + unsigned from, + unsigned to) +{ + size_t i; + size_t start; + size_t end; + char line[HYBBX_MAIL_SUBJECT_MAX + 96]; + char header[48]; + + if (to == 0 || to > count) { + to = (unsigned)count; + } + + if (count == 0) { + hybbx_session_write_line(session, "Inbox empty."); + hybbx_session_write_line(session, + "Try: /proxymail send user@mainname Your subject"); + return; + } + + if (from == 0 || from > count || from > to) { + hybbx_session_write_line(session, "No messages in that range."); + return; + } + + start = (size_t)(from - 1); + end = (size_t)to; + + if (from == 1 && to == (unsigned)count) { + hybbx_session_write_line(session, "Proxymail inbox (newest first):"); + } else { + snprintf(header, sizeof(header), "Proxymail %u-%u (newest first):", + from, to); + hybbx_session_write_line(session, header); + } + + for (i = start; i < end; i++) { + snprintf(line, sizeof(line), " %zu %s%s %s", + i + 1, + entries[i].read ? " " : "* ", + entries[i].from, + entries[i].subject[0] != '\0' ? entries[i].subject + : "(no subject)"); + hybbx_session_write_line(session, line); + } + + hybbx_session_write_line(session, + " /proxymail read <n> delete <n|from-to> list <from-to>"); +} + +void hybbx_proxymail_list_inbox(hybbx_service_t *service, + hybbx_session_t *session) +{ + proxymail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count = 0; + + if (!hybbx_mains_proxy_mesh_active()) { + hybbx_session_write_line(session, + "Proxymail — send mail to user@another-main."); + hybbx_session_write_line(session, + "No peer links active — check mains_proxy configuration."); + } + + if (proxymail_load_inbox(service, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, + &count) != HYBBX_OK) { + hybbx_session_write_line(session, "Could not read proxymail inbox."); + return; + } + + proxymail_list_print(session, entries, count, 1, (unsigned)count); +} + +void hybbx_proxymail_list_inbox_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, unsigned to) +{ + proxymail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count = 0; + + if (proxymail_load_inbox(service, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, + &count) != HYBBX_OK) { + hybbx_session_write_line(session, "Could not read proxymail inbox."); + return; + } + + proxymail_list_print(session, entries, count, from, to); +} + +hybbx_result_t hybbx_proxymail_read(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + proxymail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count = 0; + const proxymail_entry_t *entry; + char inbox_path[HYBBX_PATH_MAX]; + char msg_path[HYBBX_PATH_MAX]; + char msg_name[32]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + int in_body = 0; + + if (list_index == 0) { + return HYBBX_ERR_INVALID; + } + + if (proxymail_load_inbox(service, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, + &count) != HYBBX_OK) { + hybbx_session_write_line(session, "Could not read proxymail inbox."); + return HYBBX_ERR_IO; + } + + if ((size_t)list_index > count) { + hybbx_session_write_line(session, "No message at that index."); + return HYBBX_ERR_NOT_FOUND; + } + + entry = &entries[list_index - 1]; + + if (proxymail_user_inbox_path(service, hybbx_session_username(session), + inbox_path, sizeof(inbox_path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + snprintf(msg_name, sizeof(msg_name), "%06llu.msg", + (unsigned long long)entry->id); + if (hybbx_path_join(msg_path, sizeof(msg_path), inbox_path, msg_name) != + HYBBX_OK) { + return HYBBX_ERR_IO; + } + + fp = fopen(msg_path, "r"); + if (fp == NULL) { + hybbx_session_write_line(session, "Could not open message."); + return HYBBX_ERR_IO; + } + + hybbx_session_write_line(session, entry->subject[0] != '\0' + ? entry->subject : "(no subject)"); + hybbx_session_write_line(session, entry->from); + + while (fgets(line, sizeof(line), fp) != NULL) { + if (!in_body) { + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + in_body = 1; + } + continue; + } + + while (line[0] != '\0' && + (line[strlen(line) - 1] == '\n' || + line[strlen(line) - 1] == '\r')) { + line[strlen(line) - 1] = '\0'; + } + + hybbx_session_write_line(session, line); + } + + fclose(fp); + return HYBBX_OK; +} + +hybbx_result_t hybbx_proxymail_delete_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, unsigned to) +{ + proxymail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count = 0; + char inbox_path[HYBBX_PATH_MAX]; + size_t i; + unsigned removed = 0; + + if (proxymail_load_inbox(service, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, + &count) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (to == 0 || to > count) { + to = (unsigned)count; + } + + if (from == 0 || from > count || from > to) { + hybbx_session_write_line(session, "Nothing to delete."); + return HYBBX_OK; + } + + if (proxymail_user_inbox_path(service, hybbx_session_username(session), + inbox_path, sizeof(inbox_path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + for (i = (size_t)(from - 1); i < (size_t)to; i++) { + char msg_path[HYBBX_PATH_MAX]; + char msg_name[32]; + + snprintf(msg_name, sizeof(msg_name), "%06llu.msg", + (unsigned long long)entries[i].id); + if (hybbx_path_join(msg_path, sizeof(msg_path), inbox_path, + msg_name) == HYBBX_OK) { + if (remove(msg_path) == 0) { + removed++; + } + } + } + + if (removed == 0) { + hybbx_session_write_line(session, "Nothing to delete."); + } else { + char buf[48]; + + snprintf(buf, sizeof(buf), "Deleted %u proxymail message(s).", removed); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_proxymail_recycle_empty(hybbx_service_t *service, + hybbx_session_t *session) +{ + (void)service; + hybbx_session_write_line(session, "Recycle bin empty."); + return HYBBX_OK; +} + +hybbx_result_t hybbx_proxymail_deliver(hybbx_service_t *service, + const char *from_user, + const char *to_address, + const char *subject, + const char *body) +{ + char from_address[HYBBX_PROXYMAIL_ADDRESS_MAX]; + const char *local_name; + + if (!hybbx_proxymail_parse_address(to_address, NULL, 0, NULL, 0)) { + return HYBBX_ERR_INVALID; + } + + local_name = hybbx_service_get_name(service); + snprintf(from_address, sizeof(from_address), "%s@%s", + from_user != NULL ? from_user : "user", local_name); + + return hybbx_mains_proxy_send_mail(service, from_address, to_address, + subject, body); +} + +void hybbx_proxymail_receive(hybbx_service_t *service, + const char *from_address, + const char *to_address, + const char *subject, + const char *body) +{ + char user[HYBBX_USER_NAME_MAX]; + char remote[HYBBX_PROXYMAIL_SERVICE_NAME_MAX]; + const char *local_name; + + if (service == NULL || to_address == NULL) { + return; + } + + if (!hybbx_proxymail_parse_address(to_address, user, sizeof(user), + remote, sizeof(remote))) { + return; + } + + local_name = hybbx_service_get_name(service); + if (!str_ieq(remote, local_name)) { + return; + } + + (void)proxymail_store_message(service, user, + from_address != NULL ? from_address + : "unknown", + subject, body); +} diff --git a/src/core/registry.c b/src/core/registry.c new file mode 100644 index 0000000..107cc66 --- /dev/null +++ b/src/core/registry.c @@ -0,0 +1,83 @@ +#include "hybbx/registry.h" +#include "hybbx/limits.h" + +#include <string.h> + +typedef struct registry_entry { + const hybbx_transport_plugin_t *plugin; +} registry_entry_t; + +static registry_entry_t g_registry[HYBBX_MAX_PLUGINS]; +static size_t g_registry_count = 0; + +hybbx_result_t hybbx_registry_register(const hybbx_transport_plugin_t *plugin) +{ + if (plugin == NULL || plugin->name == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_registry_find(plugin->name) != NULL) { + return HYBBX_ERR_BUSY; + } + + if (g_registry_count >= HYBBX_MAX_PLUGINS) { + return HYBBX_ERR_NOMEM; + } + + g_registry[g_registry_count].plugin = plugin; + g_registry_count++; + return HYBBX_OK; +} + +hybbx_result_t hybbx_registry_unregister(const char *name) +{ + size_t i; + + if (name == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < g_registry_count; i++) { + if (strcmp(g_registry[i].plugin->name, name) == 0) { + size_t remaining = g_registry_count - i - 1; + if (remaining > 0) { + memmove(&g_registry[i], &g_registry[i + 1], + remaining * sizeof(registry_entry_t)); + } + g_registry_count--; + return HYBBX_OK; + } + } + + return HYBBX_ERR_NOT_FOUND; +} + +const hybbx_transport_plugin_t *hybbx_registry_find(const char *name) +{ + size_t i; + + if (name == NULL) { + return NULL; + } + + for (i = 0; i < g_registry_count; i++) { + if (strcmp(g_registry[i].plugin->name, name) == 0) { + return g_registry[i].plugin; + } + } + + return NULL; +} + +void hybbx_registry_foreach(hybbx_registry_iter_fn fn, void *userdata) +{ + size_t i; + + if (fn == NULL) { + return; + } + + for (i = 0; i < g_registry_count; i++) { + fn(g_registry[i].plugin, userdata); + } +} diff --git a/src/core/rf_tx_pace.c b/src/core/rf_tx_pace.c new file mode 100644 index 0000000..ec88a0e --- /dev/null +++ b/src/core/rf_tx_pace.c @@ -0,0 +1,100 @@ +#include "hybbx/rf_tx_pace.h" +#include "hybbx/posix_time.h" + +#include <pthread.h> + +static pthread_mutex_t g_pace_lock = PTHREAD_MUTEX_INITIALIZER; +static struct timespec g_last_tx; +static struct timespec g_burst_start; +static int g_have_last_tx; + +static void timespec_now(struct timespec *ts) +{ + clock_gettime(CLOCK_MONOTONIC, ts); +} + +static long timespec_diff_ms(const struct timespec *later, + const struct timespec *earlier) +{ + return (later->tv_sec - earlier->tv_sec) * 1000L + + (later->tv_nsec - earlier->tv_nsec) / 1000000L; +} + +static long gap_ms_since_last(const struct timespec *now) +{ + long elapsed_ms; + + if (!g_have_last_tx) { + return (long)HYBBX_RF_TX_MIN_GAP_MS; + } + elapsed_ms = timespec_diff_ms(now, &g_last_tx); + if (elapsed_ms >= (long)HYBBX_RF_TX_MIN_GAP_MS) { + return 0; + } + return (long)HYBBX_RF_TX_MIN_GAP_MS - elapsed_ms; +} + +static void sleep_ms(long wait_ms) +{ + if (wait_ms > 0) { + usleep((useconds_t)wait_ms * 1000u); + } +} + +static void enforce_burst_limit(struct timespec *now) +{ + long since_burst; + long wait_ms; + + if (!g_have_last_tx) { + g_burst_start = *now; + return; + } + + if (gap_ms_since_last(now) == 0) { + g_burst_start = *now; + return; + } + + since_burst = timespec_diff_ms(now, &g_burst_start); + if (since_burst < (long)HYBBX_RF_TX_MAX_BURST_MS) { + return; + } + + wait_ms = gap_ms_since_last(now); + sleep_ms(wait_ms); + timespec_now(now); + g_burst_start = *now; +} + +static void mark_tx(const struct timespec *now) +{ + g_last_tx = *now; + g_have_last_tx = 1; +} + +void hybbx_rf_tx_burst_guard(void) +{ + struct timespec now; + + pthread_mutex_lock(&g_pace_lock); + timespec_now(&now); + enforce_burst_limit(&now); + mark_tx(&now); + pthread_mutex_unlock(&g_pace_lock); +} + +void hybbx_rf_tx_pace(void) +{ + struct timespec now; + long wait_ms; + + pthread_mutex_lock(&g_pace_lock); + timespec_now(&now); + enforce_burst_limit(&now); + wait_ms = gap_ms_since_last(&now); + sleep_ms(wait_ms); + timespec_now(&now); + mark_tx(&now); + pthread_mutex_unlock(&g_pace_lock); +} diff --git a/src/core/security.c b/src/core/security.c new file mode 100644 index 0000000..1a06600 --- /dev/null +++ b/src/core/security.c @@ -0,0 +1,205 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/security.h" +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/config.h" +#endif +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <errno.h> +#include <pthread.h> +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> + +#define HYBBX_SECURITY_LINE_MAX 512u + +static char g_security_dir[HYBBX_PATH_MAX]; +static int g_security_ready; +static FILE *g_security_file; +static pthread_mutex_t g_security_lock = PTHREAD_MUTEX_INITIALIZER; + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + hybbx_strlcpy(buf, path, sizeof(buf)); + len = strlen(buf); + while (len > 0 && buf[len - 1] == '/') { + buf[--len] = '\0'; + } + + for (i = 1; i < len; i++) { + if (buf[i] != '/') { + continue; + } + buf[i] = '\0'; + if (buf[0] != '\0' && mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static int security_open_file(void) +{ + char path[HYBBX_PATH_MAX]; + + if (g_security_dir[0] == '\0') { + return -1; + } + + if (g_security_file != NULL) { + return 0; + } + + if (mkdir_p(g_security_dir) != 0) { + hybbx_log_warn("[security] cannot create directory %s", + g_security_dir); + return -1; + } + + if (hybbx_path_join(path, sizeof(path), g_security_dir, + HYBBX_SECURITY_LOG_FILE) != HYBBX_OK) { + return -1; + } + + g_security_file = fopen(path, "a"); + if (g_security_file == NULL) { + hybbx_log_warn("[security] cannot open %s", path); + return -1; + } + + return 0; +} + +#if !defined(HYBBX_CLIENT_BUILD) +void hybbx_security_log_config_apply(const struct hybbx_config *config) +{ + const char *dir_raw; + + hybbx_security_log_shutdown(); + g_security_dir[0] = '\0'; + g_security_ready = 0; + + if (config != NULL) { + dir_raw = hybbx_config_get(config, "log", "dir", NULL); + if (dir_raw != NULL && dir_raw[0] != '\0') { + if (hybbx_path_resolve(g_security_dir, sizeof(g_security_dir), + dir_raw) != HYBBX_OK) { + hybbx_log_warn("[security] invalid log dir path"); + return; + } + } else { + if (hybbx_path_resolve(g_security_dir, sizeof(g_security_dir), + HYBBX_DIR_LOGS) != HYBBX_OK) { + hybbx_log_warn("[security] cannot resolve default log dir"); + return; + } + } + } else if (hybbx_path_resolve(g_security_dir, sizeof(g_security_dir), + HYBBX_DIR_LOGS) != HYBBX_OK) { + return; + } + + g_security_ready = 1; + + if (security_open_file() != 0) { + g_security_ready = 0; + return; + } + + hybbx_log_info("[security] log=%s/%s", g_security_dir, HYBBX_SECURITY_LOG_FILE); + hybbx_security_log_write("startup"); +} +#else +void hybbx_security_log_config_apply(const struct hybbx_config *config) +{ + (void)config; +} +#endif + +void hybbx_security_log_write(const char *fmt, ...) +{ + char message[HYBBX_SECURITY_LINE_MAX]; + char line[HYBBX_SECURITY_LINE_MAX + 64]; + char stamp[32]; + va_list ap; + time_t now; + struct tm tm_buf; + struct tm *tm; + + if (!g_security_ready || fmt == NULL) { + return; + } + + va_start(ap, fmt); + vsnprintf(message, sizeof(message), fmt, ap); + va_end(ap); + + now = time(NULL); + tm = localtime_r(&now, &tm_buf); + if (tm == NULL) { + return; + } + + if (strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", tm) == 0) { + return; + } + + snprintf(line, sizeof(line), "%s %s\n", stamp, message); + + pthread_mutex_lock(&g_security_lock); + + if (security_open_file() == 0) { + fputs(line, g_security_file); + fflush(g_security_file); + } + + pthread_mutex_unlock(&g_security_lock); +} + +hybbx_result_t hybbx_security_log_current_path(char *out, size_t out_len) +{ + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + out[0] = '\0'; + if (!g_security_ready || g_security_dir[0] == '\0') { + return HYBBX_ERR_NOT_FOUND; + } + + return hybbx_path_join(out, out_len, g_security_dir, HYBBX_SECURITY_LOG_FILE); +} + +void hybbx_security_log_shutdown(void) +{ + pthread_mutex_lock(&g_security_lock); + + if (g_security_file != NULL) { + fclose(g_security_file); + g_security_file = NULL; + } + + pthread_mutex_unlock(&g_security_lock); + g_security_ready = 0; +} diff --git a/src/core/security_ban.c b/src/core/security_ban.c new file mode 100644 index 0000000..8fc660f --- /dev/null +++ b/src/core/security_ban.c @@ -0,0 +1,1213 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/security_ban.h" +#include "hybbx/security.h" +#include "hybbx/config.h" +#include "hybbx/limits.h" +#include "hybbx/socket.h" +#include "hybbx/storage.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <ctype.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> + +typedef struct hybbx_security_cfg { + int enabled; + unsigned maxretry; + unsigned findtime_sec; + unsigned bantime_sec; + unsigned abuse_maxretry; + unsigned abuse_findtime_sec; + int telnet; + int ssh; + int websocket; + int circuit; + unsigned rate_limit; + unsigned rate_window_sec; + hybbx_ban_backend_t backend; +} hybbx_security_cfg_t; + +typedef struct ban_entry { + char ip[HYBBX_REMOTE_ADDR_MAX]; + time_t expire_at; + int active; +} ban_entry_t; + +typedef struct fail_entry { + char ip[HYBBX_REMOTE_ADDR_MAX]; + time_t stamps[HYBBX_SECURITY_DEFAULT_MAXRETRY]; + unsigned count; + int active; +} fail_entry_t; + +typedef struct rate_entry { + char ip[HYBBX_REMOTE_ADDR_MAX]; + time_t stamps[32]; + unsigned count; + int active; +} rate_entry_t; + +typedef struct ban_callid_entry { + char callid[HYBBX_CALLID_MAX]; + time_t expire_at; + int active; + int permanent; +} ban_callid_entry_t; + +typedef struct callid_track_entry { + char callid[HYBBX_CALLID_MAX]; + time_t stamps[HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY]; + unsigned count; + int active; +} callid_track_entry_t; + +static hybbx_security_cfg_t g_cfg; +static ban_entry_t g_bans[HYBBX_SECURITY_BAN_MAX]; +static ban_callid_entry_t g_callid_bans[HYBBX_SECURITY_BAN_MAX]; +static fail_entry_t g_fails[HYBBX_SECURITY_TRACK_MAX]; +static fail_entry_t g_abuse[HYBBX_SECURITY_TRACK_MAX]; +static callid_track_entry_t g_callid_fails[HYBBX_SECURITY_TRACK_MAX]; +static callid_track_entry_t g_callid_abuse[HYBBX_SECURITY_TRACK_MAX]; +static rate_entry_t g_rates[HYBBX_SECURITY_TRACK_MAX]; +static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; + +static void security_cfg_defaults(hybbx_security_cfg_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->enabled = 1; + cfg->maxretry = HYBBX_SECURITY_DEFAULT_MAXRETRY; + cfg->findtime_sec = HYBBX_SECURITY_DEFAULT_FINDTIME_SEC; + cfg->bantime_sec = HYBBX_SECURITY_DEFAULT_BANTIME_SEC; + cfg->abuse_maxretry = HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY; + cfg->abuse_findtime_sec = HYBBX_SECURITY_DEFAULT_ABUSE_FINDTIME_SEC; + cfg->telnet = 1; + cfg->ssh = 1; + cfg->websocket = 1; + cfg->circuit = 1; + cfg->rate_limit = HYBBX_SECURITY_DEFAULT_RATE_LIMIT; + cfg->rate_window_sec = HYBBX_SECURITY_DEFAULT_RATE_WINDOW_SEC; + cfg->backend = HYBBX_BAN_BACKEND_INTERNAL; +} + +static hybbx_ban_backend_t parse_ban_backend(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_BAN_BACKEND_INTERNAL; + } + + if (strcasecmp(value, "log") == 0) { + return HYBBX_BAN_BACKEND_LOG; + } + if (strcasecmp(value, "iptables") == 0) { + return HYBBX_BAN_BACKEND_IPTABLES; + } + if (strcasecmp(value, "nftables") == 0) { + return HYBBX_BAN_BACKEND_NFTABLES; + } + if (strcasecmp(value, "hosts") == 0) { + return HYBBX_BAN_BACKEND_HOSTS; + } + + return HYBBX_BAN_BACKEND_INTERNAL; +} + +static unsigned parse_uint_clamp(const char *value, unsigned default_value, + unsigned max_value) +{ + char *end = NULL; + unsigned long n; + + if (value == NULL || value[0] == '\0') { + return default_value; + } + + n = strtoul(value, &end, 10); + if (end == value || (end != NULL && *end != '\0')) { + return default_value; + } + + if (n > max_value) { + return max_value; + } + + return (unsigned)n; +} + +static int ip_valid(const char *ip) +{ + return ip != NULL && ip[0] != '\0' && strcmp(ip, "?") != 0; +} + +int hybbx_security_callid_normalize(const char *in, char *out, size_t out_cap) +{ + size_t len; + size_t i; + int has_alpha = 0; + + if (in == NULL || out == NULL || out_cap < 2u) { + return 0; + } + + while (*in == ' ' || *in == '\t') { + in++; + } + + len = 0; + for (i = 0; in[i] != '\0'; i++) { + unsigned char ch = (unsigned char)in[i]; + + if (ch == ' ' || ch == '\t') { + break; + } + + if (len + 1 >= out_cap) { + return 0; + } + + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { + out[len++] = (char)toupper(ch); + has_alpha = 1; + } else if (ch >= '0' && ch <= '9') { + out[len++] = (char)ch; + } else if (ch == '-' || ch == '_' || ch == '.') { + out[len++] = (char)ch; + } else { + return 0; + } + } + + out[len] = '\0'; + return len > 0 && has_alpha; +} + +static ban_callid_entry_t *callid_ban_find(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_callid_bans[i].active && + strcmp(g_callid_bans[i].callid, callid) == 0) { + return &g_callid_bans[i]; + } + } + + return NULL; +} + +static ban_callid_entry_t *callid_ban_alloc(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (!g_callid_bans[i].active) { + hybbx_strlcpy(g_callid_bans[i].callid, callid, + sizeof(g_callid_bans[i].callid)); + g_callid_bans[i].active = 1; + return &g_callid_bans[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_fail_find(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_callid_fails[i].active && + strcmp(g_callid_fails[i].callid, callid) == 0) { + return &g_callid_fails[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_fail_alloc(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_callid_fails[i].active) { + hybbx_strlcpy(g_callid_fails[i].callid, callid, + sizeof(g_callid_fails[i].callid)); + g_callid_fails[i].count = 0; + g_callid_fails[i].active = 1; + return &g_callid_fails[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_abuse_find(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_callid_abuse[i].active && + strcmp(g_callid_abuse[i].callid, callid) == 0) { + return &g_callid_abuse[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_abuse_alloc(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_callid_abuse[i].active) { + hybbx_strlcpy(g_callid_abuse[i].callid, callid, + sizeof(g_callid_abuse[i].callid)); + g_callid_abuse[i].count = 0; + g_callid_abuse[i].active = 1; + return &g_callid_abuse[i]; + } + } + + return NULL; +} + +static void prune_callid_fail_window(callid_track_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= (time_t)g_cfg.findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->callid[0] = '\0'; + } +} + +static void prune_callid_abuse_window(callid_track_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= + (time_t)g_cfg.abuse_findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->callid[0] = '\0'; + } +} + +static void backend_apply_callid(const char *callid, const char *reason) +{ + hybbx_security_log_write("ban callid=%s reason=%s backend=internal", + callid, reason != NULL ? reason : "abuse"); + (void)g_cfg.backend; +} + +static void apply_callid_ban_locked(const char *callid, const char *reason, + time_t now, int permanent) +{ + ban_callid_entry_t *ban; + + ban = callid_ban_find(callid); + if (ban == NULL) { + ban = callid_ban_alloc(callid); + } + + if (ban == NULL) { + return; + } + + ban->permanent = permanent ? 1 : 0; + ban->expire_at = permanent ? (time_t)0 : + now + (time_t)g_cfg.bantime_sec; + backend_apply_callid(callid, reason); +} + +static void config_clear_permanent_callid_bans_locked(void) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_callid_bans[i].active && g_callid_bans[i].permanent) { + g_callid_bans[i].active = 0; + g_callid_bans[i].callid[0] = '\0'; + g_callid_bans[i].permanent = 0; + } + } +} + +static void config_load_callid_bans_locked(const char *list) +{ + char buf[HYBBX_PATH_MAX]; + char norm[HYBBX_CALLID_MAX]; + char *save = NULL; + char *token; + + if (list == NULL || list[0] == '\0') { + return; + } + + hybbx_strlcpy(buf, list, sizeof(buf)); + token = strtok_r(buf, ",", &save); + while (token != NULL) { + while (*token == ' ' || *token == '\t') { + token++; + } + if (hybbx_security_callid_normalize(token, norm, sizeof(norm))) { + apply_callid_ban_locked(norm, "config", time(NULL), 1); + } + token = strtok_r(NULL, ",", &save); + } +} + +static void record_callid_failure_locked(const char *callid, time_t now) +{ + callid_track_entry_t *entry; + + entry = callid_fail_find(callid); + if (entry == NULL) { + entry = callid_fail_alloc(callid); + } + + if (entry == NULL) { + return; + } + + prune_callid_fail_window(entry, now); + + if (!entry->active) { + entry = callid_fail_alloc(callid); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.maxretry) { + apply_callid_ban_locked(callid, "link_auth_fail", now, 0); + entry->active = 0; + entry->count = 0; + entry->callid[0] = '\0'; + } +} + +static void record_callid_abuse_locked(const char *callid, const char *category, + time_t now) +{ + callid_track_entry_t *entry; + char reason[64]; + + entry = callid_abuse_find(callid); + if (entry == NULL) { + entry = callid_abuse_alloc(callid); + } + + if (entry == NULL) { + return; + } + + prune_callid_abuse_window(entry, now); + + if (!entry->active) { + entry = callid_abuse_alloc(callid); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.abuse_maxretry) { + snprintf(reason, sizeof(reason), "abuse:%s", + category != NULL && category[0] != '\0' ? category : "flood"); + apply_callid_ban_locked(callid, reason, now, 0); + entry->active = 0; + entry->count = 0; + entry->callid[0] = '\0'; + } +} + +static int callid_is_banned_locked(const char *callid, time_t now) +{ + ban_callid_entry_t *ban; + + ban = callid_ban_find(callid); + if (ban == NULL) { + return 0; + } + + if (ban->permanent) { + return 1; + } + + if (now < ban->expire_at) { + return 1; + } + + ban->active = 0; + ban->callid[0] = '\0'; + ban->permanent = 0; + return 0; +} + +static int transport_enabled(const char *transport) +{ + if (!g_cfg.enabled || transport == NULL || transport[0] == '\0') { + return 0; + } + + if (strcmp(transport, "telnet") == 0) { + return g_cfg.telnet; + } + if (strcmp(transport, "ssh") == 0) { + return g_cfg.ssh; + } + if (strcmp(transport, "websocket") == 0) { + return g_cfg.websocket; + } + if (strcmp(transport, "circuit") == 0) { + return g_cfg.circuit; + } + + return 1; +} + +static ban_entry_t *ban_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_bans[i].active && strcmp(g_bans[i].ip, ip) == 0) { + return &g_bans[i]; + } + } + + return NULL; +} + +static ban_entry_t *ban_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (!g_bans[i].active) { + hybbx_strlcpy(g_bans[i].ip, ip, sizeof(g_bans[i].ip)); + g_bans[i].active = 1; + return &g_bans[i]; + } + } + + return NULL; +} + +static fail_entry_t *fail_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_fails[i].active && strcmp(g_fails[i].ip, ip) == 0) { + return &g_fails[i]; + } + } + + return NULL; +} + +static fail_entry_t *fail_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_fails[i].active) { + hybbx_strlcpy(g_fails[i].ip, ip, sizeof(g_fails[i].ip)); + g_fails[i].count = 0; + g_fails[i].active = 1; + return &g_fails[i]; + } + } + + return NULL; +} + +static fail_entry_t *abuse_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_abuse[i].active && strcmp(g_abuse[i].ip, ip) == 0) { + return &g_abuse[i]; + } + } + + return NULL; +} + +static fail_entry_t *abuse_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_abuse[i].active) { + hybbx_strlcpy(g_abuse[i].ip, ip, sizeof(g_abuse[i].ip)); + g_abuse[i].count = 0; + g_abuse[i].active = 1; + return &g_abuse[i]; + } + } + + return NULL; +} + +static rate_entry_t *rate_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_rates[i].active && strcmp(g_rates[i].ip, ip) == 0) { + return &g_rates[i]; + } + } + + return NULL; +} + +static rate_entry_t *rate_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_rates[i].active) { + hybbx_strlcpy(g_rates[i].ip, ip, sizeof(g_rates[i].ip)); + g_rates[i].count = 0; + g_rates[i].active = 1; + return &g_rates[i]; + } + } + + return NULL; +} + +static void prune_fail_window(fail_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= (time_t)g_cfg.findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->ip[0] = '\0'; + } +} + +static void prune_abuse_window(fail_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= + (time_t)g_cfg.abuse_findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->ip[0] = '\0'; + } +} + +static void prune_rate_window(rate_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= (time_t)g_cfg.rate_window_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->ip[0] = '\0'; + } +} + +static int run_backend_cmd(const char *cmd) +{ + int rc; + + if (cmd == NULL || cmd[0] == '\0') { + return -1; + } + + rc = system(cmd); + return rc; +} + +static void backend_apply(const char *ip, const char *reason) +{ + char cmd[HYBBX_PATH_MAX]; + + switch (g_cfg.backend) { + case HYBBX_BAN_BACKEND_LOG: + hybbx_security_log_write("ban ip=%s reason=%s backend=log", + ip, reason != NULL ? reason : "abuse"); + break; + + case HYBBX_BAN_BACKEND_IPTABLES: + snprintf(cmd, sizeof(cmd), + "iptables -I INPUT -s %s -j DROP 2>/dev/null", ip); + if (run_backend_cmd(cmd) != 0) { + hybbx_security_log_write( + "ban_backend_fail ip=%s backend=iptables", ip); + } else { + hybbx_security_log_write( + "ban ip=%s reason=%s backend=iptables", + ip, reason != NULL ? reason : "abuse"); + } + break; + + case HYBBX_BAN_BACKEND_NFTABLES: + snprintf(cmd, sizeof(cmd), + "nft add rule inet filter input ip saddr %s drop " + "2>/dev/null", + ip); + if (run_backend_cmd(cmd) != 0) { + hybbx_security_log_write( + "ban_backend_fail ip=%s backend=nftables", ip); + } else { + hybbx_security_log_write( + "ban ip=%s reason=%s backend=nftables", + ip, reason != NULL ? reason : "abuse"); + } + break; + + case HYBBX_BAN_BACKEND_HOSTS: + hybbx_security_log_write( + "ban ip=%s reason=%s backend=hosts (stub — internal only)", + ip, reason != NULL ? reason : "abuse"); + break; + + case HYBBX_BAN_BACKEND_INTERNAL: + default: + hybbx_security_log_write("ban ip=%s reason=%s backend=internal", + ip, reason != NULL ? reason : "abuse"); + break; + } +} + +static void apply_ban_locked(const char *ip, const char *reason, time_t now) +{ + ban_entry_t *ban; + + ban = ban_find(ip); + if (ban == NULL) { + ban = ban_alloc(ip); + } + + if (ban == NULL) { + return; + } + + ban->expire_at = now + (time_t)g_cfg.bantime_sec; + backend_apply(ip, reason); +} + +static void record_failure_locked(const char *ip, time_t now) +{ + fail_entry_t *entry; + + entry = fail_find(ip); + if (entry == NULL) { + entry = fail_alloc(ip); + } + + if (entry == NULL) { + return; + } + + prune_fail_window(entry, now); + + if (!entry->active) { + entry = fail_alloc(ip); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.maxretry) { + apply_ban_locked(ip, "login_fail", now); + entry->active = 0; + entry->count = 0; + entry->ip[0] = '\0'; + } +} + +static void record_abuse_locked(const char *ip, const char *category, time_t now) +{ + fail_entry_t *entry; + char reason[64]; + + entry = abuse_find(ip); + if (entry == NULL) { + entry = abuse_alloc(ip); + } + + if (entry == NULL) { + return; + } + + prune_abuse_window(entry, now); + + if (!entry->active) { + entry = abuse_alloc(ip); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.abuse_maxretry) { + snprintf(reason, sizeof(reason), "abuse:%s", + category != NULL && category[0] != '\0' ? category : "flood"); + apply_ban_locked(ip, reason, now); + entry->active = 0; + entry->count = 0; + entry->ip[0] = '\0'; + } +} + +static void record_rate_locked(const char *ip, time_t now) +{ + rate_entry_t *entry; + + entry = rate_find(ip); + if (entry == NULL) { + entry = rate_alloc(ip); + } + + if (entry == NULL) { + return; + } + + prune_rate_window(entry, now); + + if (!entry->active) { + entry = rate_alloc(ip); + if (entry == NULL) { + return; + } + } + + if (entry->count < (unsigned)(sizeof(entry->stamps) / sizeof(entry->stamps[0]))) { + entry->stamps[entry->count++] = now; + } +} + +void hybbx_security_ban_config_apply(const struct hybbx_config *config) +{ + const char *value; + + pthread_mutex_lock(&g_lock); + + security_cfg_defaults(&g_cfg); + + if (config != NULL) { + value = hybbx_config_get(config, "security", "enabled", NULL); + if (value != NULL) { + g_cfg.enabled = hybbx_parse_bool(value, g_cfg.enabled); + } + + value = hybbx_config_get(config, "security", "maxretry", NULL); + g_cfg.maxretry = parse_uint_clamp(value, g_cfg.maxretry, 100u); + if (g_cfg.maxretry < 1u) { + g_cfg.maxretry = 1u; + } + + value = hybbx_config_get(config, "security", "findtime", NULL); + g_cfg.findtime_sec = + parse_uint_clamp(value, g_cfg.findtime_sec, 86400u); + + value = hybbx_config_get(config, "security", "bantime", NULL); + g_cfg.bantime_sec = + parse_uint_clamp(value, g_cfg.bantime_sec, 86400u); + + value = hybbx_config_get(config, "security", "abuse_maxretry", NULL); + g_cfg.abuse_maxretry = + parse_uint_clamp(value, g_cfg.abuse_maxretry, 1000u); + if (g_cfg.abuse_maxretry < 1u) { + g_cfg.abuse_maxretry = 1u; + } + + value = hybbx_config_get(config, "security", "abuse_findtime", NULL); + g_cfg.abuse_findtime_sec = + parse_uint_clamp(value, g_cfg.abuse_findtime_sec, 86400u); + + value = hybbx_config_get(config, "security", "telnet", NULL); + if (value != NULL) { + g_cfg.telnet = hybbx_parse_bool(value, g_cfg.telnet); + } + + value = hybbx_config_get(config, "security", "ssh", NULL); + if (value != NULL) { + g_cfg.ssh = hybbx_parse_bool(value, g_cfg.ssh); + } + + value = hybbx_config_get(config, "security", "websocket", NULL); + if (value != NULL) { + g_cfg.websocket = hybbx_parse_bool(value, g_cfg.websocket); + } + + value = hybbx_config_get(config, "security", "circuit", NULL); + if (value != NULL) { + g_cfg.circuit = hybbx_parse_bool(value, g_cfg.circuit); + } + + value = hybbx_config_get(config, "security", "rate_limit", NULL); + g_cfg.rate_limit = + parse_uint_clamp(value, g_cfg.rate_limit, 10000u); + + value = hybbx_config_get(config, "security", "rate_window", NULL); + g_cfg.rate_window_sec = + parse_uint_clamp(value, g_cfg.rate_window_sec, 3600u); + + value = hybbx_config_get(config, "security", "ban_backend", NULL); + g_cfg.backend = parse_ban_backend(value); + + config_clear_permanent_callid_bans_locked(); + value = hybbx_config_get(config, "security", "ban_callid", NULL); + config_load_callid_bans_locked(value); + } + + pthread_mutex_unlock(&g_lock); + + if (g_cfg.enabled) { + hybbx_log_info("[security] ban enabled maxretry=%u findtime=%us bantime=%us " + "abuse_maxretry=%u abuse_findtime=%us " + "rate_limit=%u/%us backend=%d", + g_cfg.maxretry, g_cfg.findtime_sec, g_cfg.bantime_sec, + g_cfg.abuse_maxretry, g_cfg.abuse_findtime_sec, + g_cfg.rate_limit, g_cfg.rate_window_sec, (int)g_cfg.backend); + } +} + +void hybbx_security_ban_shutdown(void) +{ + pthread_mutex_lock(&g_lock); + + memset(g_bans, 0, sizeof(g_bans)); + memset(g_callid_bans, 0, sizeof(g_callid_bans)); + memset(g_fails, 0, sizeof(g_fails)); + memset(g_abuse, 0, sizeof(g_abuse)); + memset(g_callid_fails, 0, sizeof(g_callid_fails)); + memset(g_callid_abuse, 0, sizeof(g_callid_abuse)); + memset(g_rates, 0, sizeof(g_rates)); + security_cfg_defaults(&g_cfg); + + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_tick(void) +{ + time_t now = time(NULL); + size_t i; + + if (!g_cfg.enabled) { + return; + } + + pthread_mutex_lock(&g_lock); + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_bans[i].active && now >= g_bans[i].expire_at) { + hybbx_security_log_write("unban ip=%s", g_bans[i].ip); + g_bans[i].active = 0; + g_bans[i].ip[0] = '\0'; + } + if (g_callid_bans[i].active && !g_callid_bans[i].permanent && + g_callid_bans[i].expire_at > (time_t)0 && + now >= g_callid_bans[i].expire_at) { + hybbx_security_log_write("unban callid=%s", g_callid_bans[i].callid); + g_callid_bans[i].active = 0; + g_callid_bans[i].callid[0] = '\0'; + g_callid_bans[i].permanent = 0; + } + } + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_fails[i].active) { + prune_fail_window(&g_fails[i], now); + } + if (g_abuse[i].active) { + prune_abuse_window(&g_abuse[i], now); + } + if (g_callid_fails[i].active) { + prune_callid_fail_window(&g_callid_fails[i], now); + } + if (g_callid_abuse[i].active) { + prune_callid_abuse_window(&g_callid_abuse[i], now); + } + if (g_rates[i].active) { + prune_rate_window(&g_rates[i], now); + } + } + + pthread_mutex_unlock(&g_lock); +} + +int hybbx_security_ban_is_banned(const char *ip) +{ + ban_entry_t *ban; + time_t now = time(NULL); + int banned = 0; + + if (!g_cfg.enabled || !ip_valid(ip)) { + return 0; + } + + pthread_mutex_lock(&g_lock); + + ban = ban_find(ip); + if (ban != NULL) { + if (now < ban->expire_at) { + banned = 1; + } else { + ban->active = 0; + ban->ip[0] = '\0'; + } + } + + pthread_mutex_unlock(&g_lock); + return banned; +} + +int hybbx_security_ban_accept(const char *ip) +{ + rate_entry_t *entry; + ban_entry_t *ban; + time_t now = time(NULL); + int allow = 1; + + if (!g_cfg.enabled || !ip_valid(ip)) { + return 1; + } + + pthread_mutex_lock(&g_lock); + + ban = ban_find(ip); + if (ban != NULL) { + if (now < ban->expire_at) { + allow = 0; + } else { + ban->active = 0; + ban->ip[0] = '\0'; + } + } + + if (allow && g_cfg.rate_limit > 0u && g_cfg.rate_window_sec > 0u) { + entry = rate_find(ip); + if (entry != NULL) { + prune_rate_window(entry, now); + } + + if (entry != NULL && entry->active && + entry->count >= g_cfg.rate_limit) { + apply_ban_locked(ip, "rate_limit", now); + allow = 0; + } else { + record_rate_locked(ip, now); + } + } + + pthread_mutex_unlock(&g_lock); + return allow; +} + +int hybbx_security_ban_accept_fd(int fd) +{ + char ip[HYBBX_REMOTE_ADDR_MAX]; + + if (fd < 0) { + return 1; + } + + if (hybbx_socket_peer_name(fd, ip, sizeof(ip)) != HYBBX_OK) { + return 1; + } + + return hybbx_security_ban_accept(ip); +} + +void hybbx_security_ban_login_fail(const char *ip, const char *transport) +{ + time_t now = time(NULL); + + if (!g_cfg.enabled || !ip_valid(ip) || !transport_enabled(transport)) { + return; + } + + pthread_mutex_lock(&g_lock); + record_failure_locked(ip, now); + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_link_auth_fail(const char *ip) +{ + time_t now = time(NULL); + + if (!g_cfg.enabled || !ip_valid(ip) || !g_cfg.circuit) { + return; + } + + pthread_mutex_lock(&g_lock); + record_failure_locked(ip, now); + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_abuse_report(const char *ip, const char *category) +{ + time_t now = time(NULL); + + if (!g_cfg.enabled || !ip_valid(ip)) { + return; + } + + pthread_mutex_lock(&g_lock); + record_abuse_locked(ip, category, now); + pthread_mutex_unlock(&g_lock); +} + +int hybbx_security_ban_callid_is_banned(const char *callid) +{ + char norm[HYBBX_CALLID_MAX]; + time_t now = time(NULL); + int banned = 0; + + if (!g_cfg.enabled || !hybbx_security_callid_normalize(callid, norm, + sizeof(norm))) { + return 0; + } + + pthread_mutex_lock(&g_lock); + banned = callid_is_banned_locked(norm, now); + pthread_mutex_unlock(&g_lock); + return banned; +} + +int hybbx_security_ban_callid_accept(const char *callid) +{ + return !hybbx_security_ban_callid_is_banned(callid); +} + +void hybbx_security_ban_link_auth_fail_callid(const char *callid) +{ + char norm[HYBBX_CALLID_MAX]; + time_t now = time(NULL); + + if (!g_cfg.enabled || !g_cfg.circuit || + !hybbx_security_callid_normalize(callid, norm, sizeof(norm))) { + return; + } + + pthread_mutex_lock(&g_lock); + record_callid_failure_locked(norm, now); + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_callid_abuse_report(const char *callid, + const char *category) +{ + char norm[HYBBX_CALLID_MAX]; + time_t now = time(NULL); + + if (!g_cfg.enabled || !hybbx_security_callid_normalize(callid, norm, + sizeof(norm))) { + return; + } + + pthread_mutex_lock(&g_lock); + record_callid_abuse_locked(norm, category, now); + pthread_mutex_unlock(&g_lock); +} diff --git a/src/core/service.c b/src/core/service.c new file mode 100644 index 0000000..a8bd402 --- /dev/null +++ b/src/core/service.c @@ -0,0 +1,1270 @@ +/* + * Centralized daemon: INI apply, plugin lifecycle, HBX circuit hub, sessions, + * link registry prune. Wire protocols stay in plugins/ (telnet, packet_radio). + */ +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "storage_private.h" +#include "hybbx/registry.h" +#include "hybbx/config.h" +#include "hybbx/crypto_config.h" +#include "hybbx/traffic.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_bridge.h" +#include "hybbx/link.h" +#include "hybbx/auth.h" +#include "hybbx/storage.h" +#include "hybbx/texts.h" +#include "hybbx/chat.h" +#include "hybbx/mail.h" +#include "hybbx/broadcast.h" + +#ifdef HYBBX_HAVE_PLUGIN_MAINS_PROXY +void hybbx_mains_proxy_plugin_tick(void); +#endif +#include "hybbx/networks.h" +#include "hybbx/instance.h" +#include "hybbx/log.h" +#include "hybbx/monitor.h" +#include "hybbx/security.h" +#include "hybbx/security_ban.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" + +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#if defined(_WIN32) +#include <windows.h> +#else +#include <unistd.h> +#endif + +#define HYBBX_MAX_ACTIVE_TRANSPORTS 8 + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +typedef struct active_transport { + const hybbx_transport_plugin_t *plugin; + int running; +} active_transport_t; + +typedef struct hybbx_attached_session { + struct hybbx_session *session; + struct hybbx_attached_session *next; +} hybbx_attached_session_t; + +#define HYBBX_DEFAULT_DATA_PATH "data" + +struct hybbx_service_internal { + char *name; + char config_path[HYBBX_PATH_MAX]; + char prompt[HYBBX_PROMPT_MAX]; + unsigned max_online; + unsigned guest_timeout_minutes; + int login_announce; + unsigned active_nodes; + pthread_mutex_t node_lock; + pthread_mutex_t session_lock; + hybbx_attached_session_t *sessions; + hybbx_storage_t *storage; + hybbx_auth_config_t auth; + hybbx_texts_config_t texts; + hybbx_chat_config_t chat; + hybbx_mail_config_t mail; + hybbx_broadcast_config_t broadcast; + hybbx_networks_config_t networks; + active_transport_t transports[HYBBX_MAX_ACTIVE_TRANSPORTS]; + size_t transport_count; + hybbx_circuit_hub_t *circuit_hub; + int running; + hybbx_shutdown_mode_t shutdown_mode; + char launch_binary[HYBBX_PATH_MAX]; + pthread_mutex_t guest_lock; + unsigned char guest_in_use[HYBBX_GUEST_NUMBER_MAX + 1]; +}; + +hybbx_service_t *hybbx_service_create(const char *name) +{ + struct hybbx_service_internal *svc; + + svc = calloc(1, sizeof(*svc)); + if (svc == NULL) { + return NULL; + } + + hybbx_auth_config_defaults(&svc->auth); + hybbx_texts_config_defaults(&svc->texts); + hybbx_chat_config_defaults(&svc->chat); + hybbx_mail_config_defaults(&svc->mail); + hybbx_networks_config_defaults(&svc->networks); + svc->max_online = HYBBX_DEFAULT_MAX_ONLINE; + svc->guest_timeout_minutes = HYBBX_DEFAULT_GUEST_TIMEOUT_MINUTES; + svc->login_announce = 0; + svc->active_nodes = 0; + pthread_mutex_init(&svc->node_lock, NULL); + pthread_mutex_init(&svc->session_lock, NULL); + pthread_mutex_init(&svc->guest_lock, NULL); + + svc->name = hybbx_strdup(name != NULL && name[0] != '\0' ? + name : HYBBX_DEFAULT_SERVICE_NAME); + if (svc->name == NULL) { + free(svc); + return NULL; + } + + return (hybbx_service_t *)svc; +} + +void hybbx_service_destroy(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + size_t i; + + if (svc == NULL) { + return; + } + + hybbx_service_stop(service); + + if (svc->circuit_hub != NULL) { + hybbx_circuit_hub_destroy(svc->circuit_hub); + svc->circuit_hub = NULL; + } + + for (i = 0; i < svc->transport_count; i++) { + if (svc->transports[i].running && + svc->transports[i].plugin->stop != NULL) { + svc->transports[i].plugin->stop(); + } + if (svc->transports[i].plugin->shutdown != NULL) { + svc->transports[i].plugin->shutdown(); + } + } + + if (svc->storage != NULL) { + hybbx_storage_close(svc->storage); + svc->storage = NULL; + } + + hybbx_monitor_shutdown(); + hybbx_log_shutdown(); + hybbx_security_log_shutdown(); + hybbx_security_ban_shutdown(); + + { + hybbx_attached_session_t *node = svc->sessions; + + while (node != NULL) { + hybbx_attached_session_t *next = node->next; + + free(node); + node = next; + } + svc->sessions = NULL; + } + + pthread_mutex_destroy(&svc->session_lock); + pthread_mutex_destroy(&svc->guest_lock); + pthread_mutex_destroy(&svc->node_lock); + free(svc->name); + free(svc); +} + +hybbx_storage_t *hybbx_service_get_storage(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return svc->storage; +} + +const hybbx_auth_config_t *hybbx_service_get_auth(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->auth; +} + +const hybbx_texts_config_t *hybbx_service_get_texts(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->texts; +} + +const hybbx_chat_config_t *hybbx_service_get_chat(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->chat; +} + +const hybbx_mail_config_t *hybbx_service_get_mail(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->mail; +} + +const hybbx_broadcast_config_t *hybbx_service_get_broadcast( + const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->broadcast; +} + +hybbx_circuit_hub_t *hybbx_service_circuit_hub(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return svc->circuit_hub; +} + +const char *hybbx_service_get_prompt(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return ""; + } + + return svc->prompt; +} + +const char *hybbx_service_get_name(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL || svc->name == NULL || svc->name[0] == '\0') { + return HYBBX_DEFAULT_SERVICE_NAME; + } + + return svc->name; +} + +unsigned hybbx_service_max_online(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_DEFAULT_MAX_ONLINE; + } + + return svc->max_online; +} + +int hybbx_service_login_announce(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + return svc != NULL && svc->login_announce != 0; +} + +unsigned hybbx_service_active_nodes(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + unsigned count; + + if (svc == NULL) { + return 0; + } + + pthread_mutex_lock((pthread_mutex_t *)&svc->node_lock); + count = svc->active_nodes; + pthread_mutex_unlock((pthread_mutex_t *)&svc->node_lock); + + return count; +} + +unsigned hybbx_service_guest_timeout_seconds(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_DEFAULT_GUEST_TIMEOUT_MINUTES * 60u; + } + + return svc->guest_timeout_minutes * 60u; +} + +hybbx_result_t hybbx_service_guest_assign(hybbx_service_t *service, + const char *guest_prefix, + hybbx_user_record_t *out, + unsigned *slot_out) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + unsigned slot; + + if (svc == NULL || out == NULL || slot_out == NULL) { + return HYBBX_ERR_INVALID; + } + + pthread_mutex_lock(&svc->guest_lock); + for (slot = 1; slot <= HYBBX_GUEST_NUMBER_MAX; slot++) { + if (!svc->guest_in_use[slot]) { + svc->guest_in_use[slot] = 1; + pthread_mutex_unlock(&svc->guest_lock); + + hybbx_guest_fill_record(guest_prefix, slot, out); + *slot_out = slot; + return HYBBX_OK; + } + } + pthread_mutex_unlock(&svc->guest_lock); + + return HYBBX_ERR_BUSY; +} + +void hybbx_service_guest_release(hybbx_service_t *service, unsigned slot) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL || slot < 1 || slot > HYBBX_GUEST_NUMBER_MAX) { + return; + } + + pthread_mutex_lock(&svc->guest_lock); + svc->guest_in_use[slot] = 0; + pthread_mutex_unlock(&svc->guest_lock); +} + +hybbx_result_t hybbx_service_acquire_node(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_ERR_INVALID; + } + + pthread_mutex_lock(&svc->node_lock); + if (svc->active_nodes >= svc->max_online) { + pthread_mutex_unlock(&svc->node_lock); + return HYBBX_ERR_BUSY; + } + + svc->active_nodes++; + pthread_mutex_unlock(&svc->node_lock); + return HYBBX_OK; +} + +void hybbx_service_release_node(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return; + } + + pthread_mutex_lock(&svc->node_lock); + if (svc->active_nodes > 0) { + svc->active_nodes--; + } + pthread_mutex_unlock(&svc->node_lock); +} + +hybbx_result_t hybbx_service_attach_session(hybbx_service_t *service, + hybbx_session_t *session) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + hybbx_attached_session_t *node; + + if (svc == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + node = calloc(1, sizeof(*node)); + if (node == NULL) { + return HYBBX_ERR_NOMEM; + } + + node->session = session; + + pthread_mutex_lock(&svc->session_lock); + node->next = svc->sessions; + svc->sessions = node; + pthread_mutex_unlock(&svc->session_lock); + + return HYBBX_OK; +} + +void hybbx_service_detach_session(hybbx_service_t *service, + hybbx_session_t *session) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + hybbx_attached_session_t *prev; + hybbx_attached_session_t *node; + + if (svc == NULL || session == NULL) { + return; + } + + pthread_mutex_lock(&svc->session_lock); + prev = NULL; + node = svc->sessions; + while (node != NULL) { + if (node->session == session) { + if (prev != NULL) { + prev->next = node->next; + } else { + svc->sessions = node->next; + } + free(node); + break; + } + prev = node; + node = node->next; + } + pthread_mutex_unlock(&svc->session_lock); +} + +void hybbx_service_visit_sessions(hybbx_service_t *service, + hybbx_service_session_visit_fn fn, + void *userdata) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + hybbx_attached_session_t *node; + hybbx_session_t **snapshot = NULL; + size_t count = 0; + size_t i; + + if (svc == NULL || fn == NULL) { + return; + } + + /* + * Snapshot session pointers under session_lock, then invoke callbacks + * without holding the lock. Visitors may write to circuit sessions, + * which can re-enter via load-balance -> bandwidth policy. + */ + pthread_mutex_lock(&svc->session_lock); + for (node = svc->sessions; node != NULL; node = node->next) { + if (node->session != NULL) { + count++; + } + } + + if (count > 0) { + snapshot = calloc(count, sizeof(*snapshot)); + if (snapshot == NULL) { + hybbx_log_warn("[service] session snapshot OOM (%zu sessions) — skipped", + count); + } else { + i = 0; + for (node = svc->sessions; node != NULL; node = node->next) { + if (node->session != NULL) { + snapshot[i++] = node->session; + } + } + } + } + pthread_mutex_unlock(&svc->session_lock); + + if (snapshot == NULL) { + return; + } + + for (i = 0; i < count; i++) { + fn(snapshot[i], userdata); + } + free(snapshot); +} + +typedef struct find_registered_session_ctx { + uint64_t user_id; + hybbx_session_t *exclude; + hybbx_session_t *found; +} find_registered_session_ctx_t; + +static void find_registered_session_visitor(hybbx_session_t *session, + void *userdata) +{ + find_registered_session_ctx_t *ctx = (find_registered_session_ctx_t *)userdata; + const hybbx_session_record_t *rec; + + if (ctx == NULL || ctx->found != NULL || session == NULL || + session == ctx->exclude) { + return; + } + + if (!hybbx_session_logged_in(session) || hybbx_session_is_guest(session)) { + return; + } + + rec = hybbx_session_record(session); + if (rec == NULL || rec->user_id != ctx->user_id) { + return; + } + + ctx->found = session; +} + +hybbx_session_t *hybbx_service_find_registered_session( + hybbx_service_t *service, + uint64_t user_id, + hybbx_session_t *exclude) +{ + find_registered_session_ctx_t ctx; + + if (service == NULL || user_id == 0) { + return NULL; + } + + ctx.user_id = user_id; + ctx.exclude = exclude; + ctx.found = NULL; + hybbx_service_visit_sessions(service, find_registered_session_visitor, &ctx); + return ctx.found; +} + +static void service_apply_service(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + const char *prompt; + const char *max_online_value; + const char *nodes_value; + + svc->prompt[0] = '\0'; + + prompt = hybbx_config_get(config, "service", "prompt", NULL); + if (prompt != NULL && prompt[0] != '\0') { + hybbx_strlcpy(svc->prompt, prompt, sizeof(svc->prompt)); + } + + max_online_value = hybbx_config_get(config, "service", "max_online", NULL); + nodes_value = hybbx_config_get(config, "service", "nodes", NULL); + if (max_online_value != NULL && max_online_value[0] != '\0') { + svc->max_online = hybbx_config_get_uint(config, "service", "max_online", + HYBBX_DEFAULT_MAX_ONLINE, 1u, + 999u); + } else if (nodes_value != NULL && nodes_value[0] != '\0') { + svc->max_online = hybbx_config_get_uint(config, "service", "nodes", + HYBBX_DEFAULT_MAX_ONLINE, 1u, + 999u); + } else { + svc->max_online = HYBBX_DEFAULT_MAX_ONLINE; + } + + svc->login_announce = + hybbx_config_get_bool(config, "service", "login_announce", 0); + + hybbx_log_info("[service] max_online=%u login_announce=%s", + svc->max_online, + svc->login_announce ? "yes" : "no"); +} + +static int str_ieq_local(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static void service_apply_texts(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + const char *path; + char resolved[HYBBX_PATH_MAX]; + + hybbx_texts_config_defaults(&svc->texts); + path = hybbx_config_get(config, "texts", "path", HYBBX_DIR_TEXT); + if (hybbx_path_resolve(resolved, sizeof(resolved), path) == HYBBX_OK) { + hybbx_strlcpy(svc->texts.path, resolved, sizeof(svc->texts.path)); + } else if (path != NULL && path[0] != '\0') { + hybbx_strlcpy(svc->texts.path, path, sizeof(svc->texts.path)); + } +} + +static hybbx_storage_backend_kind_t parse_storage_backend(const char *value) +{ + if (value == NULL || str_ieq_local(value, "flatfile") || + str_ieq_local(value, "flat") || str_ieq_local(value, "files")) { + return HYBBX_STORAGE_FLATFILE; + } + + if (str_ieq_local(value, "sqlite")) { + return HYBBX_STORAGE_SQLITE; + } + + if (str_ieq_local(value, "mysql")) { + return HYBBX_STORAGE_MYSQL; + } + + if (str_ieq_local(value, "mariadb")) { + return HYBBX_STORAGE_MARIADB; + } + + return HYBBX_STORAGE_FLATFILE; +} + +static hybbx_result_t service_open_storage(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + hybbx_storage_options_t options; + hybbx_storage_sql_config_t sql_cfg; + const char *backend_str; + const char *path_raw; + char path_resolved[HYBBX_PATH_MAX]; + const char *guest_prefix; + hybbx_result_t rc; + + if (svc->storage != NULL) { + hybbx_storage_close(svc->storage); + svc->storage = NULL; + } + + backend_str = hybbx_config_get(config, "storage", "backend", "flatfile"); + path_raw = hybbx_config_get(config, "storage", "path", HYBBX_DEFAULT_DATA_PATH); + + rc = hybbx_path_resolve(path_resolved, sizeof(path_resolved), path_raw); + if (rc != HYBBX_OK) { + hybbx_log_warn("[storage] invalid path '%s'", + path_raw != NULL ? path_raw : ""); + return HYBBX_ERR_IO; + } + + guest_prefix = hybbx_config_get(config, "auth", "guest_prefix", NULL); + + options.backend = parse_storage_backend(backend_str); + options.path = path_resolved; + options.guest_prefix = guest_prefix != NULL ? guest_prefix : + svc->auth.guest_prefix; + options.sql_cfg = NULL; + + if (options.backend == HYBBX_STORAGE_SQLITE) { + hybbx_storage_sql_config_apply(&sql_cfg, config, path_resolved); + options.sql_cfg = &sql_cfg; + } + + svc->storage = hybbx_storage_open(&options); + if (svc->storage == NULL) { + hybbx_log_warn("[storage] cannot open '%s' (writable?)", + path_resolved); + return HYBBX_ERR_IO; + } + + hybbx_log_info("[storage] backend=%s path=%s", backend_str, path_resolved); + return HYBBX_OK; +} + +static void service_apply_auth(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + const char *prefix; + + hybbx_auth_config_defaults(&svc->auth); + svc->auth.auto_login = + hybbx_config_get_bool(config, "auth", "auto_login", 1); + + prefix = hybbx_config_get(config, "auth", "guest_prefix", NULL); + if (prefix != NULL && prefix[0] != '\0') { + hybbx_strlcpy(svc->auth.guest_prefix, prefix, + sizeof(svc->auth.guest_prefix)); + } + + svc->guest_timeout_minutes = hybbx_config_get_uint( + config, "auth", "guest_timeout_minutes", + HYBBX_DEFAULT_GUEST_TIMEOUT_MINUTES, 1u, 24u * 60u); + + hybbx_log_info("[service] guest_timeout_minutes=%u", svc->guest_timeout_minutes); +} + +static hybbx_result_t service_apply_circuit(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + hybbx_circuit_config_t cfg; + const char *bind4; + const char *bind6; + hybbx_result_t rc; + + if (!hybbx_config_get_bool(config, "circuit", "enabled", 1)) { + if (svc->circuit_hub != NULL) { + hybbx_circuit_hub_stop(svc->circuit_hub); + } + return HYBBX_OK; + } + + if (!svc->networks.circuit) { + if (svc->circuit_hub != NULL) { + hybbx_circuit_hub_stop(svc->circuit_hub); + } + return HYBBX_OK; + } + + hybbx_circuit_config_defaults(&cfg); + bind4 = hybbx_config_get(config, "circuit", "bind", NULL); + bind6 = hybbx_config_get(config, "circuit", "bind6", NULL); + if (bind4 != NULL && bind4[0] != '\0') { + hybbx_strlcpy(cfg.bind4, bind4, sizeof(cfg.bind4)); + } + if (bind6 != NULL && bind6[0] != '\0') { + hybbx_strlcpy(cfg.bind6, bind6, sizeof(cfg.bind6)); + } + + cfg.port = hybbx_config_get_uint(config, "circuit", "port", + HYBBX_CIRCUIT_DEFAULT_PORT, 1u, 65535u); + cfg.ipv4 = hybbx_config_get_bool(config, "circuit", "ipv4", 1); + cfg.ipv6 = hybbx_config_get_bool(config, "circuit", "ipv6", 1); + cfg.link_auth = hybbx_config_get_bool(config, "circuit", "link_auth", 1); + cfg.link_stale_days = hybbx_config_get_uint( + config, "circuit", "link_stale_days", HYBBX_LINK_STALE_DAYS, 1u, 365u); + cfg.balance.enabled = hybbx_config_get_bool(config, "circuit", "balance", 1); + cfg.balance.lag_sec = hybbx_config_get_uint( + config, "circuit", "balance_lag_sec", 8u, 1u, 120u); + cfg.balance.queue_pause = (size_t)hybbx_config_get_uint( + config, "circuit", "balance_queue_pause", 8192u, 256u, 1048576u); + cfg.balance.queue_break = (size_t)hybbx_config_get_uint( + config, "circuit", "balance_queue_break", 32768u, 1024u, 1048576u); + cfg.balance.queue_cancel = (size_t)hybbx_config_get_uint( + config, "circuit", "balance_queue_cancel", 131072u, 4096u, 4194304u); + if (cfg.balance.queue_pause > cfg.balance.queue_break) { + cfg.balance.queue_break = cfg.balance.queue_pause; + } + if (cfg.balance.queue_break > cfg.balance.queue_cancel) { + cfg.balance.queue_cancel = cfg.balance.queue_break; + } + cfg.max_links = hybbx_config_get_uint( + config, "circuit", "max_links", HYBBX_CIRCUIT_DEFAULT_MAX_LINKS, + 1u, HYBBX_CIRCUIT_MAX_LINKS); + (void)hybbx_circuit_bridge_load(&cfg.bridge, config); + + { + const char *link_pw = hybbx_config_get(config, "circuit", "link_password", NULL); + + if (link_pw != NULL) { + hybbx_strlcpy(cfg.link_password, link_pw, sizeof(cfg.link_password)); + } + if (svc->storage != NULL && svc->storage->path != NULL && + svc->storage->path[0] != '\0') { + hybbx_strlcpy(cfg.data_path, svc->storage->path, sizeof(cfg.data_path)); + } else { + char data_resolved[HYBBX_PATH_MAX]; + const char *path_raw = hybbx_config_get(config, "storage", "path", + HYBBX_DEFAULT_DATA_PATH); + + if (hybbx_path_resolve(data_resolved, sizeof(data_resolved), + path_raw) == HYBBX_OK) { + hybbx_strlcpy(cfg.data_path, data_resolved, sizeof(cfg.data_path)); + } + } + if (svc->config_path[0] != '\0') { + hybbx_strlcpy(cfg.config_path, svc->config_path, sizeof(cfg.config_path)); + } + } + + if (svc->circuit_hub == NULL) { + svc->circuit_hub = hybbx_circuit_hub_create((hybbx_service_t *)svc); + if (svc->circuit_hub == NULL) { + return HYBBX_ERR_NOMEM; + } + } + + rc = hybbx_circuit_hub_start(svc->circuit_hub, &cfg); + return rc; +} + +hybbx_result_t hybbx_service_load_transport(hybbx_service_t *service, + const char *plugin_name) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + const hybbx_transport_plugin_t *plugin; + size_t i; + + if (svc == NULL || plugin_name == NULL) { + return HYBBX_ERR_INVALID; + } + + plugin = hybbx_registry_find(plugin_name); + if (plugin == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + for (i = 0; i < svc->transport_count; i++) { + if (svc->transports[i].plugin == plugin) { + return HYBBX_OK; + } + } + + if (svc->transport_count >= HYBBX_MAX_ACTIVE_TRANSPORTS) { + return HYBBX_ERR_NOMEM; + } + + if (plugin->init != NULL) { + hybbx_result_t rc = plugin->init(service); + if (rc != HYBBX_OK) { + return rc; + } + } + + svc->transports[svc->transport_count].plugin = plugin; + svc->transports[svc->transport_count].running = 0; + svc->transport_count++; + return HYBBX_OK; +} + +static active_transport_t *find_active(struct hybbx_service_internal *svc, + const char *plugin_name) +{ + size_t i; + + for (i = 0; i < svc->transport_count; i++) { + if (strcmp(svc->transports[i].plugin->name, plugin_name) == 0) { + return &svc->transports[i]; + } + } + return NULL; +} + +hybbx_result_t hybbx_service_start_transport(hybbx_service_t *service, + const char *plugin_name, + const char *config) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + active_transport_t *active; + + if (svc == NULL || plugin_name == NULL) { + return HYBBX_ERR_INVALID; + } + + active = find_active(svc, plugin_name); + if (active == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + if (active->running) { + return HYBBX_ERR_BUSY; + } + + if (active->plugin->start == NULL) { + return HYBBX_ERR_UNSUPPORTED; + } + + { + hybbx_result_t rc = active->plugin->start(config); + if (rc != HYBBX_OK) { + return rc; + } + } + + active->running = 1; + return HYBBX_OK; +} + +hybbx_result_t hybbx_service_stop_transport(hybbx_service_t *service, + const char *plugin_name) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + active_transport_t *active; + + if (svc == NULL || plugin_name == NULL) { + return HYBBX_ERR_INVALID; + } + + active = find_active(svc, plugin_name); + if (active == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + if (!active->running) { + return HYBBX_OK; + } + + if (active->plugin->stop != NULL) { + hybbx_result_t rc = active->plugin->stop(); + if (rc != HYBBX_OK) { + return rc; + } + } + + active->running = 0; + return HYBBX_OK; +} + +hybbx_result_t hybbx_service_run(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_ERR_INVALID; + } + + svc->running = 1; + + while (svc->running) { +#if defined(_WIN32) + Sleep(1000); +#else + static unsigned prune_tick; + + sleep(1); + hybbx_security_ban_tick(); + hybbx_monitor_tick(service); + hybbx_broadcast_ax25_tick(service); +#ifdef HYBBX_HAVE_PLUGIN_MAINS_PROXY + hybbx_mains_proxy_plugin_tick(); +#endif + if (svc->storage != NULL) { + hybbx_storage_backup_tick(svc->storage); + } + prune_tick++; + if (prune_tick >= 3600u) { + prune_tick = 0; + if (svc->circuit_hub != NULL) { + hybbx_circuit_hub_prune_links(svc->circuit_hub); + } + } +#endif + } + + return HYBBX_OK; +} + +void hybbx_service_stop(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc != NULL) { + svc->running = 0; + } +} + +void hybbx_service_set_launch_binary(hybbx_service_t *service, const char *argv0) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return; + } + + svc->launch_binary[0] = '\0'; + if (argv0 != NULL && argv0[0] != '\0') { + hybbx_strlcpy(svc->launch_binary, argv0, sizeof(svc->launch_binary)); + } +} + +void hybbx_service_request_shutdown(hybbx_service_t *service, int restart) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return; + } + + svc->shutdown_mode = restart ? HYBBX_SHUTDOWN_RESTART : HYBBX_SHUTDOWN_STOP; + hybbx_service_stop(service); +} + +hybbx_shutdown_mode_t hybbx_service_shutdown_mode(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_SHUTDOWN_NONE; + } + + return svc->shutdown_mode; +} + +const char *hybbx_service_config_path(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL || svc->config_path[0] == '\0') { + return NULL; + } + + return svc->config_path; +} + +void hybbx_service_restart_exec(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + const char *binary; + const char *config_path; + char *argv[4]; + char opt_c[] = "-c"; + + if (svc == NULL) { + return; + } + + binary = svc->launch_binary[0] != '\0' ? svc->launch_binary : HYBBX_DAEMON_BINARY; + config_path = svc->config_path[0] != '\0' ? svc->config_path : NULL; + + argv[0] = (char *)binary; + if (config_path != NULL) { + argv[1] = opt_c; + argv[2] = (char *)config_path; + argv[3] = NULL; + } else { + argv[1] = NULL; + } + + fflush(NULL); + execv(binary, argv); + perror("hybbx restart failed"); +} + +typedef struct apply_transport_ctx { + hybbx_service_t *service; + const hybbx_config_t *config; + const hybbx_networks_config_t *networks; + hybbx_result_t last_error; +} apply_transport_ctx_t; + +static int transport_start_failure_is_fatal(const char *plugin_name) +{ + /* Role static transport must start; others may fail independently. */ + return hybbx_networks_is_static_transport(plugin_name); +} + +static void apply_transport_cb(const hybbx_transport_plugin_t *plugin, + void *userdata) +{ + apply_transport_ctx_t *ctx = (apply_transport_ctx_t *)userdata; + char section[128]; + char *transport_config; + hybbx_result_t rc; + int transport_enabled; + + if (ctx->networks == NULL || + !hybbx_networks_transport_wanted(plugin->name, ctx->networks)) { + return; + } + + if (!hybbx_config_resolve_transport_section(ctx->config, plugin->name, + section, sizeof(section))) { + snprintf(section, sizeof(section), "transport.%s", plugin->name); + } + + if (hybbx_networks_is_static_transport(plugin->name)) { + transport_enabled = 1; + } else { + transport_enabled = hybbx_config_get_bool(ctx->config, section, + "enabled", 1); + } + + if (!transport_enabled) { + return; + } + + rc = hybbx_service_load_transport(ctx->service, plugin->name); + if (rc != HYBBX_OK) { + hybbx_log_warn("[service] %s: plugin load failed (%s)", + plugin->name, hybbx_result_name(rc)); + if (transport_start_failure_is_fatal(plugin->name)) { + ctx->last_error = rc; + } + return; + } + + if (strcmp(plugin->name, "packet_radio") == 0) { + transport_config = + hybbx_config_format_packet_radio_start(ctx->config); + } else if (strcmp(plugin->name, "baycom") == 0) { + transport_config = hybbx_config_format_baycom_start(ctx->config); + } else if (strcmp(plugin->name, "mains_proxy") == 0) { + transport_config = hybbx_config_format_transport_sections(ctx->config, + plugin->name); + } else { + transport_config = NULL; + } + if (transport_config == NULL) { + transport_config = hybbx_config_format_section(ctx->config, section); + } + rc = hybbx_service_start_transport(ctx->service, plugin->name, + transport_config); + free(transport_config); + + if (rc != HYBBX_OK) { + hybbx_log_warn("[service] %s: start failed (%s)", plugin->name, + hybbx_result_name(rc)); + if (transport_start_failure_is_fatal(plugin->name)) { + ctx->last_error = rc; + } + } +} + +hybbx_result_t hybbx_service_apply_config(hybbx_service_t *service, + const hybbx_config_t *config, + const char *config_path) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + const char *service_name; + apply_transport_ctx_t ctx; + char *new_name; + hybbx_result_t rc; + + if (svc == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + svc->config_path[0] = '\0'; + if (config_path != NULL && config_path[0] != '\0') { + hybbx_strlcpy(svc->config_path, config_path, sizeof(svc->config_path)); + } + + service_name = hybbx_config_get(config, "service", "name", NULL); + if (service_name != NULL) { + new_name = hybbx_strdup(service_name); + if (new_name == NULL) { + return HYBBX_ERR_NOMEM; + } + free(svc->name); + svc->name = new_name; + } + + service_apply_auth(svc, config); + hybbx_time_config_apply(config); + hybbx_log_config_apply(config); + hybbx_security_log_config_apply(config); + hybbx_monitor_config_apply(config); + hybbx_security_ban_config_apply(config); + service_apply_texts(svc, config); + hybbx_chat_config_apply(&svc->chat, config); + hybbx_broadcast_config_apply(&svc->broadcast, config); + service_apply_service(svc, config); + hybbx_crypto_config_apply(config); + hybbx_traffic_config_apply(config); + + rc = service_open_storage(svc, config); + if (rc != HYBBX_OK) { + return rc; + } + + { + const char *storage_path = HYBBX_DEFAULT_DATA_PATH; + char storage_resolved[HYBBX_PATH_MAX]; + + if (svc->storage != NULL && svc->storage->path != NULL && + svc->storage->path[0] != '\0') { + storage_path = svc->storage->path; + } else if (hybbx_path_resolve(storage_resolved, sizeof(storage_resolved), + storage_path) == HYBBX_OK) { + storage_path = storage_resolved; + } + hybbx_mail_config_apply(&svc->mail, config, storage_path); + } + + hybbx_networks_config_apply(&svc->networks, config); + + { + int standalone = hybbx_config_get_bool(config, "instance", "standalone", 0); + + if (!standalone && hybbx_instance_role() == HYBBX_INSTANCE_MAIN) { + if (svc->networks.ax25 || svc->networks.baycom || + svc->networks.ardop || svc->networks.crdop) { + standalone = 1; + } + } + hybbx_instance_set_standalone(standalone); + } + + rc = hybbx_networks_enforce_instance(&svc->networks); + if (rc != HYBBX_OK) { + return rc; + } + + rc = service_apply_circuit(svc, config); + if (rc != HYBBX_OK) { + return rc; + } + + ctx.service = service; + ctx.config = config; + ctx.networks = &svc->networks; + ctx.last_error = HYBBX_OK; + + hybbx_registry_foreach(apply_transport_cb, &ctx); + return ctx.last_error; +} diff --git a/src/core/session.c b/src/core/session.c new file mode 100644 index 0000000..9f5d368 --- /dev/null +++ b/src/core/session.c @@ -0,0 +1,2678 @@ +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/command.h" +#include "hybbx/storage.h" +#include "hybbx/texts.h" +#include "hybbx/auth.h" +#include "hybbx/chat.h" +#include "hybbx/conference.h" +#include "hybbx/mail.h" +#include "hybbx/proxymail.h" +#include "hybbx/proxychat.h" +#include "hybbx/terminal.h" +#include "hybbx/traffic.h" +#include "hybbx/log.h" +#include "hybbx/monitor.h" +#include "hybbx/instance.h" +#include "hybbx/hybbx.h" +#include "hybbx/util.h" +#include "hybbx/bandwidth_policy.h" +#include "hybbx/instance.h" +#include "hybbx/messages.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <time.h> + +static int session_str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +typedef struct hybbx_session_core { + hybbx_session_t pub; + hybbx_session_record_t record; + hybbx_user_record_t user; + hybbx_service_t *service; + char line_buf[HYBBX_LINE_MAX]; + size_t line_len; + size_t line_cursor; + char history[HYBBX_HISTORY_MAX][HYBBX_LINE_MAX]; + unsigned history_count; + unsigned history_next; + int history_view; + char history_saved_line[HYBBX_LINE_MAX]; + int expect_lf; + int logged_in; + int login_prompt; + unsigned guest_slot; + time_t guest_expires_at; + hybbx_session_area_t area; + hybbx_session_area_t area_stack[HYBBX_AREA_STACK_MAX]; + unsigned area_depth; + unsigned chat_channel; + int chat_max_notice_shown; + char conference_topic[HYBBX_CONFERENCE_TOPIC_MAX]; + char conference_partner[HYBBX_USER_NAME_MAX]; + int conference_invite_pending; + char conference_invite_from[HYBBX_USER_NAME_MAX]; + char conference_invite_topic[HYBBX_CONFERENCE_TOPIC_MAX]; + time_t conference_invite_deadline; + struct { + char target[HYBBX_USER_NAME_MAX]; + time_t sent_at[HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET]; + } conference_invite_rates[8]; + int monitor_active; + int mail_composing; + char mail_compose_to[HYBBX_USER_NAME_MAX]; + char mail_compose_subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + char mail_compose_body[HYBBX_MAIL_BODY_MAX + 1]; + size_t mail_compose_body_len; + int proxymail_composing; + char proxymail_compose_to[HYBBX_PROXYMAIL_ADDRESS_MAX]; + char proxymail_compose_subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + char proxymail_compose_body[HYBBX_MAIL_BODY_MAX + 1]; + size_t proxymail_compose_body_len; + unsigned out_col; + int input_echo; + int bandwidth_paused; + int bandwidth_disconnect; + unsigned char esc_state; + char csi_buf[24]; + size_t csi_len; +} hybbx_session_core_t; + +#define SESSION_ESC_NONE 0u +#define SESSION_ESC_ESC 1u +#define SESSION_ESC_CSI 2u +#define SESSION_ESC_SS3 3u + +#define SESSION_CSI_BUF_MAX 24u + +static hybbx_result_t session_handle_user_byte(hybbx_session_core_t *core, + unsigned char byte); + +static size_t session_line_max_len(const hybbx_session_core_t *core); +static unsigned session_chat_message_max(const hybbx_session_core_t *core); +static hybbx_result_t session_write_transport(hybbx_session_core_t *core, + const char *data, size_t len); +static hybbx_result_t session_line_refresh_suffix(hybbx_session_core_t *core); +static hybbx_result_t session_line_cursor_left(hybbx_session_core_t *core); +static hybbx_result_t session_line_cursor_right(hybbx_session_core_t *core); +static hybbx_result_t session_line_cursor_home(hybbx_session_core_t *core); +static hybbx_result_t session_line_cursor_end(hybbx_session_core_t *core); +static hybbx_result_t session_line_backspace(hybbx_session_core_t *core); +static hybbx_result_t session_line_delete_forward(hybbx_session_core_t *core); +static hybbx_result_t session_line_insert_char(hybbx_session_core_t *core, + char ch); +static hybbx_result_t session_history_up(hybbx_session_core_t *core); +static hybbx_result_t session_history_down(hybbx_session_core_t *core); +static void session_maybe_announce_login(hybbx_session_core_t *core); + +static void session_escape_reset(hybbx_session_core_t *core) +{ + if (core == NULL) { + return; + } + + core->esc_state = SESSION_ESC_NONE; + core->csi_len = 0; +} + +static int session_csi_is_final(unsigned char ch) +{ + return ch >= 0x40u && ch <= 0x7eu; +} + +static size_t session_line_max_len(const hybbx_session_core_t *core) +{ + size_t line_max = sizeof(core->line_buf) - 1; + + if (core == NULL) { + return HYBBX_LINE_MAX - 1; + } + + if (core->area == HYBBX_AREA_CHAT || core->area == HYBBX_AREA_CONFERENCE || + core->area == HYBBX_AREA_PROXYCHAT) { + line_max = session_chat_message_max(core); + } else if (core->area == HYBBX_AREA_MAIL && core->mail_composing) { + line_max = HYBBX_LINE_MAX - 1; + } else if (core->area == HYBBX_AREA_PROXYMAIL && core->proxymail_composing) { + line_max = HYBBX_LINE_MAX - 1; + } + + return line_max; +} + +static int session_char_echoable(char ch) +{ + unsigned char byte = (unsigned char)ch; + + return byte >= 0x20 && byte != 0x7f; +} + +static hybbx_result_t session_write_transport(hybbx_session_core_t *core, + const char *data, size_t len) +{ + if (core == NULL || data == NULL || len == 0) { + return HYBBX_OK; + } + + if (core->pub.transport != NULL && core->pub.transport->write != NULL) { + return core->pub.transport->write(&core->pub, data, len); + } + + return HYBBX_OK; +} + +static hybbx_result_t session_line_move_back(hybbx_session_core_t *core, + size_t count) +{ + char buf[64]; + size_t chunk; + size_t sent = 0; + + if (core == NULL || count == 0) { + return HYBBX_OK; + } + + memset(buf, '\b', sizeof(buf)); + + while (sent < count) { + hybbx_result_t rc; + + chunk = count - sent; + if (chunk > sizeof(buf)) { + chunk = sizeof(buf); + } + + rc = session_write_transport(core, buf, chunk); + if (rc != HYBBX_OK) { + return rc; + } + sent += chunk; + } + + return HYBBX_OK; +} + +static hybbx_result_t session_line_refresh_suffix(hybbx_session_core_t *core) +{ + size_t suffix_len; + hybbx_result_t rc; + + if (core == NULL || !core->input_echo) { + return HYBBX_OK; + } + + suffix_len = core->line_len - core->line_cursor; + if (suffix_len > 0) { + rc = session_write_transport(core, core->line_buf + core->line_cursor, + suffix_len); + if (rc != HYBBX_OK) { + return rc; + } + } + + rc = session_write_transport(core, "\x1b[K", 3); + if (rc != HYBBX_OK) { + return rc; + } + + return session_line_move_back(core, suffix_len); +} + +static hybbx_result_t session_line_cursor_left(hybbx_session_core_t *core) +{ + if (core == NULL || core->line_cursor == 0) { + return HYBBX_OK; + } + + core->line_cursor--; + if (!core->input_echo) { + return HYBBX_OK; + } + + return session_write_transport(core, "\b", 1); +} + +static hybbx_result_t session_line_cursor_right(hybbx_session_core_t *core) +{ + hybbx_result_t rc; + + if (core == NULL || core->line_cursor >= core->line_len) { + return HYBBX_OK; + } + + if (core->input_echo) { + char out[2]; + + out[0] = core->line_buf[core->line_cursor]; + out[1] = '\0'; + rc = session_write_transport(core, out, 1); + if (rc != HYBBX_OK) { + return rc; + } + } + + core->line_cursor++; + return HYBBX_OK; +} + +static hybbx_result_t session_line_cursor_home(hybbx_session_core_t *core) +{ + hybbx_result_t rc = HYBBX_OK; + + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + while (core->line_cursor > 0) { + rc = session_line_cursor_left(core); + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t session_line_cursor_end(hybbx_session_core_t *core) +{ + hybbx_result_t rc = HYBBX_OK; + + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + while (core->line_cursor < core->line_len) { + rc = session_line_cursor_right(core); + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t session_line_backspace(hybbx_session_core_t *core) +{ + hybbx_result_t rc; + + if (core == NULL || core->line_cursor == 0) { + return HYBBX_OK; + } + + core->line_cursor--; + memmove(core->line_buf + core->line_cursor, + core->line_buf + core->line_cursor + 1, + core->line_len - core->line_cursor - 1); + core->line_len--; + core->line_buf[core->line_len] = '\0'; + + if (!core->input_echo) { + return HYBBX_OK; + } + + rc = session_write_transport(core, "\b", 1); + if (rc != HYBBX_OK) { + return rc; + } + + return session_line_refresh_suffix(core); +} + +static hybbx_result_t session_line_delete_forward(hybbx_session_core_t *core) +{ + if (core == NULL || core->line_cursor >= core->line_len) { + return HYBBX_OK; + } + + memmove(core->line_buf + core->line_cursor, + core->line_buf + core->line_cursor + 1, + core->line_len - core->line_cursor - 1); + core->line_len--; + core->line_buf[core->line_len] = '\0'; + + if (!core->input_echo) { + return HYBBX_OK; + } + + return session_line_refresh_suffix(core); +} + +static hybbx_result_t session_line_insert_char(hybbx_session_core_t *core, + char ch) +{ + size_t line_max; + hybbx_result_t rc; + char out[2]; + + if (core == NULL || !session_char_echoable(ch)) { + return HYBBX_OK; + } + + line_max = session_line_max_len(core); + if (core->line_len >= line_max) { + return HYBBX_OK; + } + + if (core->line_cursor < core->line_len) { + memmove(core->line_buf + core->line_cursor + 1, + core->line_buf + core->line_cursor, + core->line_len - core->line_cursor); + } + + core->line_buf[core->line_cursor] = ch; + core->line_len++; + core->line_cursor++; + core->line_buf[core->line_len] = '\0'; + + if (!core->input_echo) { + return HYBBX_OK; + } + + out[0] = ch; + out[1] = '\0'; + rc = session_write_transport(core, out, 1); + if (rc != HYBBX_OK) { + core->line_cursor--; + core->line_len--; + core->line_buf[core->line_len] = '\0'; + return rc; + } + + return session_line_refresh_suffix(core); +} + +static int session_history_should_store(const char *line) +{ + if (line == NULL || line[0] == '\0') { + return 0; + } + + return strncmp(line, "/login ", 7) != 0 && + strncmp(line, "/register ", 10) != 0 && + strncmp(line, "/changeme ", 10) != 0 && + strncmp(line, "/userchange ", 12) != 0; +} + +static void session_history_add(hybbx_session_core_t *core, const char *line) +{ + if (core == NULL || !session_history_should_store(line)) { + return; + } + + hybbx_strlcpy(core->history[core->history_next], line, + sizeof(core->history[0])); + core->history_next = (core->history_next + 1u) % HYBBX_HISTORY_MAX; + if (core->history_count < HYBBX_HISTORY_MAX) { + core->history_count++; + } + core->history_view = -1; + core->history_saved_line[0] = '\0'; +} + +static hybbx_result_t session_line_replace(hybbx_session_core_t *core, + const char *line) +{ + size_t len; + hybbx_result_t rc; + + if (core == NULL || line == NULL) { + return HYBBX_ERR_INVALID; + } + + len = strlen(line); + if (len >= sizeof(core->line_buf)) { + len = sizeof(core->line_buf) - 1; + } + + if (core->input_echo) { + rc = session_line_move_back(core, core->line_cursor); + if (rc != HYBBX_OK) { + return rc; + } + if (len > 0) { + rc = session_write_transport(core, line, len); + if (rc != HYBBX_OK) { + return rc; + } + } + rc = session_write_transport(core, "\x1b[K", 3); + if (rc != HYBBX_OK) { + return rc; + } + } + + memcpy(core->line_buf, line, len); + core->line_buf[len] = '\0'; + core->line_len = len; + core->line_cursor = len; + return HYBBX_OK; +} + +static hybbx_result_t session_history_up(hybbx_session_core_t *core) +{ + unsigned slot; + + if (core == NULL || core->history_count == 0) { + return HYBBX_OK; + } + + if (core->history_view < 0) { + hybbx_strlcpy(core->history_saved_line, core->line_buf, + sizeof(core->history_saved_line)); + core->history_view = 0; + } else if ((unsigned)(core->history_view + 1) < core->history_count) { + core->history_view++; + } else { + return HYBBX_OK; + } + + slot = (core->history_next + HYBBX_HISTORY_MAX - 1u - + (unsigned)core->history_view) % HYBBX_HISTORY_MAX; + return session_line_replace(core, core->history[slot]); +} + +static hybbx_result_t session_history_down(hybbx_session_core_t *core) +{ + unsigned slot; + + if (core == NULL || core->history_view < 0) { + return HYBBX_OK; + } + + if (core->history_view == 0) { + core->history_view = -1; + return session_line_replace(core, core->history_saved_line); + } + + core->history_view--; + slot = (core->history_next + HYBBX_HISTORY_MAX - 1u - + (unsigned)core->history_view) % HYBBX_HISTORY_MAX; + return session_line_replace(core, core->history[slot]); +} + +static hybbx_result_t session_apply_csi(hybbx_session_core_t *core, + const char *params, char final_ch) +{ + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (final_ch == 'A') { + return session_history_up(core); + } + + if (final_ch == 'B') { + return session_history_down(core); + } + + if (final_ch == 'C') { + return session_line_cursor_right(core); + } + + if (final_ch == 'D') { + return session_line_cursor_left(core); + } + + if (final_ch == 'H') { + return session_line_cursor_home(core); + } + + if (final_ch == 'F') { + return session_line_cursor_end(core); + } + + if (final_ch == '~') { + if (strcmp(params, "1") == 0 || strcmp(params, "7") == 0) { + return session_line_cursor_home(core); + } + if (strcmp(params, "3") == 0) { + return session_line_delete_forward(core); + } + if (strcmp(params, "4") == 0 || strcmp(params, "8") == 0) { + return session_line_cursor_end(core); + } + return HYBBX_OK; + } + + if (final_ch == 'K' || final_ch == 'P') { + return HYBBX_OK; + } + + return HYBBX_OK; +} + +static int session_filter_escape_byte(hybbx_session_core_t *core, unsigned char byte) +{ + if (core == NULL) { + return 0; + } + + if (core->esc_state == SESSION_ESC_NONE && byte == 0x9bu) { + core->esc_state = SESSION_ESC_CSI; + core->csi_len = 0; + return 1; + } + + if (core->esc_state == SESSION_ESC_NONE && byte == 0x1bu) { + core->esc_state = SESSION_ESC_ESC; + return 1; + } + + if (core->esc_state == SESSION_ESC_ESC) { + if (byte == '[') { + core->esc_state = SESSION_ESC_CSI; + core->csi_len = 0; + return 1; + } + if (byte == 'O') { + core->esc_state = SESSION_ESC_SS3; + return 1; + } + + session_escape_reset(core); + return 0; + } + + if (core->esc_state == SESSION_ESC_SS3) { + hybbx_result_t rc = HYBBX_OK; + + if (byte == 'C') { + rc = session_line_cursor_right(core); + } else if (byte == 'D') { + rc = session_line_cursor_left(core); + } else if (byte == 'H') { + rc = session_line_cursor_home(core); + } else if (byte == 'F') { + rc = session_line_cursor_end(core); + } + + session_escape_reset(core); + (void)rc; + return 1; + } + + if (core->esc_state == SESSION_ESC_CSI) { + if (core->csi_len + 1 >= SESSION_CSI_BUF_MAX) { + session_escape_reset(core); + return 1; + } + + core->csi_buf[core->csi_len++] = (char)byte; + + if (session_csi_is_final(byte)) { + char final_ch = (char)byte; + char params[SESSION_CSI_BUF_MAX]; + + if (core->csi_len > 1) { + memcpy(params, core->csi_buf, core->csi_len - 1); + params[core->csi_len - 1] = '\0'; + } else { + params[0] = '\0'; + } + + session_escape_reset(core); + (void)session_apply_csi(core, params, final_ch); + return 1; + } + + return 1; + } + + return 0; +} + +static unsigned session_chat_message_max(const hybbx_session_core_t *core) +{ + const hybbx_chat_config_t *chat; + + if (core == NULL || core->service == NULL) { + return HYBBX_CHAT_MESSAGE_MAX; + } + + chat = hybbx_service_get_chat(core->service); + if (chat == NULL) { + return HYBBX_CHAT_MESSAGE_MAX; + } + + return chat->message_max; +} + +static void session_area_clear_state(hybbx_session_core_t *core, + hybbx_session_area_t area) +{ + if (core == NULL) { + return; + } + + if (area == HYBBX_AREA_CHAT) { + core->chat_channel = 0; + } + + if (area == HYBBX_AREA_CONFERENCE) { + hybbx_conference_area_leaving(&core->pub); + } + + if (area == HYBBX_AREA_MAIL) { + core->mail_composing = 0; + core->mail_compose_body[0] = '\0'; + core->mail_compose_body_len = 0; + core->mail_compose_to[0] = '\0'; + core->mail_compose_subject[0] = '\0'; + } + + if (area == HYBBX_AREA_PROXYMAIL) { + core->proxymail_composing = 0; + core->proxymail_compose_body[0] = '\0'; + core->proxymail_compose_body_len = 0; + core->proxymail_compose_to[0] = '\0'; + core->proxymail_compose_subject[0] = '\0'; + } +} + +static void session_area_sync(hybbx_session_core_t *core) +{ + if (core == NULL || core->area_depth == 0) { + return; + } + + core->area = core->area_stack[core->area_depth - 1]; +} + +static hybbx_result_t session_area_push(hybbx_session_core_t *core, + hybbx_session_area_t area) +{ + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area_depth >= HYBBX_AREA_STACK_MAX) { + return HYBBX_ERR_BUSY; + } + + if (core->area_depth > 0 && + core->area_stack[core->area_depth - 1] == area) { + core->area = area; + return HYBBX_OK; + } + + core->area_stack[core->area_depth] = area; + core->area_depth++; + core->area = area; + return HYBBX_OK; +} + +static void session_release_guest_slot(hybbx_session_core_t *core) +{ + if (core == NULL || core->guest_slot == 0 || core->service == NULL) { + return; + } + + hybbx_service_guest_release(core->service, core->guest_slot); + core->guest_slot = 0; +} + +static void session_arm_guest_timer(hybbx_session_core_t *core) +{ + unsigned timeout_sec; + + if (core == NULL || !hybbx_user_level_is_guest(core->user.level)) { + if (core != NULL) { + core->guest_expires_at = 0; + } + return; + } + + timeout_sec = hybbx_service_guest_timeout_seconds(core->service); + if (timeout_sec == 0) { + core->guest_expires_at = 0; + return; + } + + core->guest_expires_at = time(NULL) + (time_t)timeout_sec; +} + +static hybbx_result_t session_activate_guest(hybbx_session_core_t *core, + const hybbx_user_record_t *guest, + unsigned guest_slot) +{ + hybbx_storage_t *storage; + const char *transport_name; + hybbx_result_t rc; + + if (core == NULL || guest == NULL || guest_slot < 1) { + return HYBBX_ERR_INVALID; + } + + storage = hybbx_service_get_storage(core->service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->guest_slot != 0 && core->guest_slot != guest_slot) { + session_release_guest_slot(core); + } + + transport_name = core->pub.transport != NULL ? + core->pub.transport->name : "unknown"; + + if (core->record.session_id != 0) { + hybbx_storage_session_end(storage, core->record.session_id); + memset(&core->record, 0, sizeof(core->record)); + } + + rc = hybbx_storage_session_begin(storage, guest, transport_name, + &core->record); + if (rc != HYBBX_OK) { + hybbx_service_guest_release(core->service, guest_slot); + return rc; + } + + core->user = *guest; + core->guest_slot = guest_slot; + core->logged_in = 1; + core->login_prompt = 0; + session_arm_guest_timer(core); + return HYBBX_OK; +} + +static hybbx_result_t session_check_guest_expiry(hybbx_session_core_t *core) +{ + time_t now; + + if (core == NULL || core->guest_expires_at == 0) { + return HYBBX_OK; + } + + now = time(NULL); + if (now < core->guest_expires_at) { + return HYBBX_OK; + } + + (void)hybbx_msg_send_system(&core->pub, "Guest time limit. Goodbye."); + return HYBBX_SESSION_END; +} + +static void session_process_line(hybbx_session_core_t *core, const char *line); + +hybbx_result_t hybbx_session_write(hybbx_session_t *session, const char *text) +{ + hybbx_session_core_t *core; + + if (session == NULL || text == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core != NULL && core->bandwidth_paused) { + return HYBBX_OK; + } + + return hybbx_traffic_emit(session, + core != NULL ? &core->out_col : NULL, + text, strlen(text)); +} + +hybbx_result_t hybbx_session_write_line(hybbx_session_t *session, + const char *text) +{ + hybbx_result_t rc; + + if (text != NULL) { + rc = hybbx_session_write(session, text); + if (rc != HYBBX_OK) { + return rc; + } + } + + return hybbx_session_write(session, "\n"); +} + +hybbx_result_t hybbx_session_command_gap(hybbx_session_t *session) +{ + return hybbx_session_write(session, "\n"); +} + +static int session_accepts_input(const hybbx_session_core_t *core) +{ + return core != NULL && (core->logged_in != 0 || core->login_prompt != 0); +} + +hybbx_result_t hybbx_session_show_prompt(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + const char *prompt; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!core->logged_in && !core->login_prompt) { + return HYBBX_ERR_INVALID; + } + + prompt = hybbx_service_get_prompt(core->service); + if (prompt == NULL || prompt[0] == '\0') { + return HYBBX_OK; + } + + return hybbx_session_write(session, prompt); +} + +hybbx_result_t hybbx_session_clear_terminal(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_result_t rc; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !session_accepts_input(core)) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_term_clear_screen(session); + if (rc != HYBBX_OK) { + return rc; + } + + core->line_len = 0; + core->line_cursor = 0; + core->line_buf[0] = '\0'; + core->out_col = 0; + core->expect_lf = 0; + + return hybbx_session_show_prompt(session); +} + +int hybbx_session_input_echo(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->input_echo != 0; +} + +hybbx_result_t hybbx_session_set_input_echo(hybbx_session_t *session, int enabled) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !session_accepts_input(core)) { + return HYBBX_ERR_INVALID; + } + + core->input_echo = enabled ? 1 : 0; + return HYBBX_OK; +} + +static void session_submit_line(hybbx_session_core_t *core) +{ + core->line_buf[core->line_len] = '\0'; + session_history_add(core, core->line_buf); + session_process_line(core, core->line_buf); + core->line_len = 0; + core->line_cursor = 0; + core->history_view = -1; + if ((core->logged_in || core->login_prompt) && + core->area != HYBBX_AREA_CHAT && + core->area != HYBBX_AREA_CONFERENCE && + core->area != HYBBX_AREA_PROXYCHAT && + !(core->area == HYBBX_AREA_MAIL && core->mail_composing) && + !(core->area == HYBBX_AREA_PROXYMAIL && core->proxymail_composing)) { + hybbx_session_show_prompt(&core->pub); + } +} + +static hybbx_result_t session_echo_newline(hybbx_session_core_t *core) +{ + if (core == NULL || !core->input_echo) { + return HYBBX_OK; + } + + return hybbx_session_write(&core->pub, "\n"); +} + +static hybbx_result_t session_handle_user_byte(hybbx_session_core_t *core, + unsigned char byte) +{ + char ch = (char)byte; + + if (ch == '\0') { + return HYBBX_OK; + } + + if (ch == '\b' || ch == 127) { + return session_line_backspace(core); + } + + if (ch == '\r') { + { + hybbx_result_t rc = session_echo_newline(core); + + if (rc != HYBBX_OK) { + return rc; + } + } + session_submit_line(core); + if (!core->logged_in && !core->login_prompt) { + return HYBBX_SESSION_END; + } + core->expect_lf = 1; + return HYBBX_OK; + } + + if (ch == '\n') { + if (core->expect_lf) { + core->expect_lf = 0; + return HYBBX_OK; + } + + { + hybbx_result_t rc = session_echo_newline(core); + + if (rc != HYBBX_OK) { + return rc; + } + } + + session_submit_line(core); + if (!core->logged_in && !core->login_prompt) { + return HYBBX_SESSION_END; + } + return HYBBX_OK; + } + + core->expect_lf = 0; + + return session_line_insert_char(core, ch); +} + +static void session_send_banner(hybbx_session_core_t *core) +{ + const hybbx_texts_config_t *texts; + const char *service_name; + + texts = hybbx_service_get_texts(core->service); + if (texts == NULL) { + return; + } + + service_name = hybbx_service_get_name(core->service); + hybbx_texts_send_banner(texts, &core->pub, HYBBX_VERSION_STRING, service_name); +} + +static void session_process_line(hybbx_session_core_t *core, const char *line) +{ + hybbx_parsed_command_t cmd; + hybbx_command_scope_t scope; + hybbx_result_t rc; + + if (line[0] == '\0') { + return; + } + + scope = hybbx_command_classify(line); + if (scope == HYBBX_CMD_SCOPE_COMMENT) { + return; + } + + if (hybbx_conference_invite_pending(&core->pub) && + scope == HYBBX_CMD_SCOPE_LOCAL) { + if (hybbx_conference_reply_invite(core->service, &core->pub, line) == + HYBBX_OK) { + return; + } + hybbx_session_write_line(&core->pub, "Reply y/n or yes/no."); + return; + } + + if (core->area == HYBBX_AREA_MAIL && core->mail_composing && + scope == HYBBX_CMD_SCOPE_LOCAL) { + const hybbx_mail_config_t *mail; + size_t line_len; + size_t body_max; + + mail = hybbx_service_get_mail(core->service); + body_max = mail != NULL ? mail->body_max : HYBBX_MAIL_BODY_MAX; + line_len = strlen(line); + + if (core->mail_compose_body_len + line_len + 2 > body_max) { + hybbx_session_write_line(&core->pub, "Message body too long."); + return; + } + + if (core->mail_compose_body_len > 0) { + core->mail_compose_body[core->mail_compose_body_len++] = '\n'; + } + memcpy(core->mail_compose_body + core->mail_compose_body_len, + line, line_len + 1); + core->mail_compose_body_len += line_len; + return; + } + + if (core->area == HYBBX_AREA_PROXYMAIL && core->proxymail_composing && + scope == HYBBX_CMD_SCOPE_LOCAL) { + const hybbx_mail_config_t *mail; + size_t line_len; + size_t body_max; + + mail = hybbx_service_get_mail(core->service); + body_max = mail != NULL ? mail->body_max : HYBBX_MAIL_BODY_MAX; + line_len = strlen(line); + + if (core->proxymail_compose_body_len + line_len + 2 > body_max) { + hybbx_session_write_line(&core->pub, "Message body too long."); + return; + } + + if (core->proxymail_compose_body_len > 0) { + core->proxymail_compose_body[core->proxymail_compose_body_len++] = + '\n'; + } + memcpy(core->proxymail_compose_body + core->proxymail_compose_body_len, + line, line_len + 1); + core->proxymail_compose_body_len += line_len; + return; + } + + if (core->area == HYBBX_AREA_PROXYCHAT && scope == HYBBX_CMD_SCOPE_LOCAL) { + if (hybbx_session_is_guest(&core->pub)) { + hybbx_session_write_line(&core->pub, "Guests cannot use proxychat."); + return; + } + + if (strlen(line) > session_chat_message_max(core)) { + hybbx_session_write_line(&core->pub, "Message too long."); + return; + } + + (void)hybbx_proxychat_post(hybbx_session_service(&core->pub), &core->pub, + line); + return; + } + + if (core->area == HYBBX_AREA_CHAT && scope == HYBBX_CMD_SCOPE_LOCAL) { + if (hybbx_session_is_guest(&core->pub)) { + hybbx_session_write_line(&core->pub, "Guests cannot use chat."); + return; + } + + if (strlen(line) > session_chat_message_max(core)) { + hybbx_session_write_line(&core->pub, "Message too long."); + return; + } + + (void)hybbx_chat_post(core->service, &core->pub, line); + return; + } + + if (core->area == HYBBX_AREA_CONFERENCE && scope == HYBBX_CMD_SCOPE_LOCAL) { + if (hybbx_session_is_guest(&core->pub)) { + hybbx_session_write_line(&core->pub, "Guests cannot use conference."); + return; + } + + if (strlen(line) > session_chat_message_max(core)) { + hybbx_session_write_line(&core->pub, "Message too long."); + return; + } + + (void)hybbx_conference_post(core->service, &core->pub, line); + return; + } + + if (scope == HYBBX_CMD_SCOPE_LOCAL) { + return; + } + + rc = hybbx_command_parse(line, &cmd); + if (rc != HYBBX_OK) { + hybbx_session_write_line(&core->pub, "Command parse error."); + return; + } + + rc = hybbx_command_dispatch(core->service, &core->pub, &cmd); + if (rc == HYBBX_SESSION_END) { + core->logged_in = 0; + } else if (rc == HYBBX_ERR_DENIED) { + /* access message already sent */ + } else if (rc != HYBBX_OK && rc != HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(&core->pub, "Command failed."); + } + + hybbx_command_free(&cmd); +} + +hybbx_result_t hybbx_session_open(hybbx_service_t *service, + const hybbx_transport_plugin_t *transport, + void *transport_data, + hybbx_session_t **out) +{ + hybbx_session_core_t *core; + hybbx_storage_t *storage; + const hybbx_auth_config_t *auth; + hybbx_result_t rc; + int suppress_startup_text; + int interactive; + + if (service == NULL || transport == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + interactive = (transport->kind == HYBBX_TRANSPORT_TELNET || + transport->kind == HYBBX_TRANSPORT_SSH || + transport->kind == HYBBX_TRANSPORT_WEBSOCKET); + + if (interactive && !hybbx_instance_offers_user_bbx()) { + hybbx_log_warn("[session] reject %s on %s (no user BBX on this instance)", + transport->name, hybbx_instance_role_name()); + return HYBBX_ERR_DENIED; + } + + rc = hybbx_service_acquire_node(service); + if (rc != HYBBX_OK) { + return rc; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + hybbx_service_release_node(service); + return HYBBX_ERR_INVALID; + } + + auth = hybbx_service_get_auth(service); + if (auth == NULL) { + hybbx_service_release_node(service); + return HYBBX_ERR_INVALID; + } + + core = calloc(1, sizeof(*core)); + if (core == NULL) { + hybbx_service_release_node(service); + return HYBBX_ERR_NOMEM; + } + + core->service = service; + core->pub.transport = transport; + core->pub.transport_data = transport_data; + core->pub.core_data = core; + core->area = HYBBX_AREA_MAIN; + core->area_stack[0] = HYBBX_AREA_MAIN; + core->area_depth = 1; + + { + const hybbx_traffic_config_t *traffic = hybbx_traffic_config_get(); + + core->input_echo = traffic != NULL && traffic->input_echo; + } + + suppress_startup_text = (transport->kind == HYBBX_TRANSPORT_CIRCUIT); + + (void)hybbx_term_init_session(&core->pub); + + if (!suppress_startup_text) { + session_send_banner(core); + } + + if (auth->auto_login) { + unsigned guest_slot = 0; + + rc = hybbx_service_guest_assign(service, auth->guest_prefix, + &core->user, &guest_slot); + if (rc != HYBBX_OK) { + free(core); + hybbx_service_release_node(service); + return rc; + } + + rc = session_activate_guest(core, &core->user, guest_slot); + if (rc != HYBBX_OK) { + free(core); + hybbx_service_release_node(service); + return rc; + } + + if (!suppress_startup_text) { + hybbx_log_info("%s@%s connected (session %llu)", + core->record.username, transport->name, + (unsigned long long)core->record.session_id); + hybbx_monitor_event(service, core->record.username, transport->name, + "connected"); + } + + if (!suppress_startup_text) { + hybbx_session_show_prompt(&core->pub); + } + } else { + core->login_prompt = 1; + if (!suppress_startup_text) { + hybbx_session_show_prompt(&core->pub); + } + } + + *out = &core->pub; + + if (hybbx_service_attach_session(service, &core->pub) != HYBBX_OK) { + if (core->record.session_id != 0) { + hybbx_storage_session_end(storage, core->record.session_id); + } + session_release_guest_slot(core); + free(core); + hybbx_service_release_node(service); + return HYBBX_ERR_NOMEM; + } + + return HYBBX_OK; +} + +void hybbx_session_close(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_storage_t *storage; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + if (hybbx_conference_invite_pending(session) || + hybbx_session_area(session) == HYBBX_AREA_CONFERENCE) { + hybbx_conference_session_closed(session); + } + + storage = hybbx_service_get_storage(core->service); + if (storage != NULL && core->record.session_id != 0) { + const char *plugin = core->record.transport[0] != '\0' + ? core->record.transport + : (session->transport != NULL + ? session->transport->name + : "?"); + hybbx_storage_session_end(storage, core->record.session_id); + hybbx_log_info("%s@%s disconnected (session %llu)", + core->record.username, plugin, + (unsigned long long)core->record.session_id); + hybbx_monitor_event(core->service, core->record.username, plugin, + "disconnected"); + } + + session_release_guest_slot(core); + + if (core->service != NULL) { + hybbx_service_detach_session(core->service, session); + hybbx_service_release_node(core->service); + } + + free(core); +} + +hybbx_result_t hybbx_session_switch_user(hybbx_session_t *session, + const hybbx_user_record_t *user) +{ + hybbx_session_core_t *core; + hybbx_storage_t *storage; + hybbx_session_record_t new_record; + const char *transport_name; + hybbx_result_t rc; + + if (session == NULL || user == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_user_level_is_guest(user->level)) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || core->service == NULL) { + return HYBBX_ERR_INVALID; + } + + session_release_guest_slot(core); + + storage = hybbx_service_get_storage(core->service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + transport_name = core->record.transport; + if (transport_name[0] == '\0' && session->transport != NULL) { + transport_name = session->transport->name; + } + if (transport_name == NULL || transport_name[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (core->record.session_id != 0) { + hybbx_storage_session_end(storage, core->record.session_id); + } + + rc = hybbx_storage_session_begin(storage, user, transport_name, &new_record); + if (rc != HYBBX_OK) { + return rc; + } + + core->user = *user; + core->record = new_record; + core->guest_expires_at = 0; + core->logged_in = 1; + core->login_prompt = 0; + + hybbx_log_info("%s@%s login (session %llu)", + core->record.username, transport_name, + (unsigned long long)core->record.session_id); + hybbx_monitor_event(core->service, core->record.username, transport_name, + "login"); + + session_maybe_announce_login(core); + + return HYBBX_OK; +} + +hybbx_result_t hybbx_session_tick(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->logged_in) { + return HYBBX_ERR_INVALID; + } + + if (core->bandwidth_disconnect) { + return HYBBX_SESSION_END; + } + + if (core->conference_invite_pending && + core->conference_invite_deadline > 0) { + hybbx_conference_invite_tick(core->service, session); + } + + return session_check_guest_expiry(core); +} + +hybbx_result_t hybbx_session_handle_input(hybbx_session_t *session, + const uint8_t *data, size_t len) +{ + hybbx_session_core_t *core; + size_t i; + hybbx_result_t expiry_rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !session_accepts_input(core)) { + return HYBBX_ERR_INVALID; + } + + if (core->bandwidth_paused) { + return HYBBX_OK; + } + + if (core->logged_in) { + expiry_rc = session_check_guest_expiry(core); + if (expiry_rc != HYBBX_OK) { + return expiry_rc; + } + } + + for (i = 0; i < len; i++) { + hybbx_result_t rc; + + if (session_filter_escape_byte(core, data[i])) { + continue; + } + + rc = session_handle_user_byte(core, data[i]); + + if (rc == HYBBX_SESSION_END) { + return HYBBX_SESSION_END; + } + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} + +const char *hybbx_session_display_name(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return ""; + } + + return hybbx_user_display_name(&core->user); +} + +hybbx_result_t hybbx_session_format_user_at_plugin(const hybbx_session_t *session, + char *out, size_t out_len) +{ + const hybbx_session_record_t *rec; + const char *user; + const char *plugin; + + if (session == NULL || out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + user = hybbx_session_display_name(session); + if (user == NULL || user[0] == '\0') { + user = hybbx_session_username(session); + } + if (user == NULL || user[0] == '\0') { + user = "?"; + } + + rec = hybbx_session_record(session); + plugin = (rec != NULL && rec->transport[0] != '\0') ? rec->transport : NULL; + if (plugin == NULL || plugin[0] == '\0') { + plugin = (session->transport != NULL && session->transport->name != NULL) + ? session->transport->name + : "?"; + } + + snprintf(out, out_len, "%s@%s", user, plugin); + return HYBBX_OK; +} + +typedef struct login_announce_ctx { + const char *line; +} login_announce_ctx_t; + +static void login_announce_visitor(hybbx_session_t *session, void *userdata) +{ + login_announce_ctx_t *ctx = (login_announce_ctx_t *)userdata; + + if (session == NULL || ctx == NULL || ctx->line == NULL) { + return; + } + + if (!hybbx_session_is_interactive_user(session) || + !hybbx_session_logged_in(session)) { + return; + } + + hybbx_session_write_line(session, ctx->line); +} + +static void session_maybe_announce_login(hybbx_session_core_t *core) +{ + hybbx_service_t *service; + char user_at[96]; + char line[128]; + login_announce_ctx_t ctx; + + if (core == NULL || core->service == NULL) { + return; + } + + service = core->service; + if (!hybbx_service_login_announce(service)) { + return; + } + + /* Guests: never announce. Hidden Sysop (invisible-sysop / monitor): skip. */ + if (hybbx_user_level_is_guest(core->user.level) || + hybbx_session_hidden_from_who(&core->pub)) { + return; + } + + if (hybbx_session_format_user_at_plugin(&core->pub, user_at, + sizeof(user_at)) != HYBBX_OK) { + return; + } + + snprintf(line, sizeof(line), "*** User login: %s", user_at); + ctx.line = line; + hybbx_service_visit_sessions(service, login_announce_visitor, &ctx); +} + +const char *hybbx_session_username(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return ""; + } + + return core->record.username; +} + +uint64_t hybbx_session_id(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->record.session_id; +} + +const hybbx_session_record_t *hybbx_session_record(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return NULL; + } + + return &core->record; +} + +hybbx_result_t hybbx_session_set_remote(hybbx_session_t *session, + const char *remote) +{ + hybbx_session_core_t *core; + + if (session == NULL || remote == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(core->record.remote, remote, sizeof(core->record.remote)); + return HYBBX_OK; +} + +hybbx_user_level_t hybbx_session_user_level(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_LEVEL_GUEST; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_LEVEL_GUEST; + } + + return core->user.level; +} + +int hybbx_session_is_guest(const hybbx_session_t *session) +{ + return hybbx_user_level_is_guest(hybbx_session_user_level(session)); +} + +int hybbx_session_is_interactive_user(const hybbx_session_t *session) +{ + hybbx_transport_kind_t kind; + + if (session == NULL || session->transport == NULL) { + return 0; + } + + kind = session->transport->kind; + return kind == HYBBX_TRANSPORT_TELNET || + kind == HYBBX_TRANSPORT_SSH || + kind == HYBBX_TRANSPORT_WEBSOCKET; +} + +int hybbx_session_monitor_active(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + return core != NULL && core->monitor_active != 0; +} + +void hybbx_session_set_monitor_active(hybbx_session_t *session, int on) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->monitor_active = on ? 1 : 0; +} + +int hybbx_session_hidden_from_who(const hybbx_session_t *session) +{ + if (session == NULL) { + return 0; + } + + if (!hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + return 0; + } + + if (hybbx_monitor_invisible_sysop()) { + return 1; + } + + return hybbx_session_monitor_active(session) ? 1 : 0; +} + +int hybbx_session_logged_in(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->logged_in != 0; +} + +int hybbx_session_login_prompt(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->login_prompt != 0; +} + +hybbx_session_area_t hybbx_session_area(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_AREA_MAIN; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_AREA_MAIN; + } + + return core->area; +} + +const char *hybbx_session_area_name(hybbx_session_area_t area) +{ + switch (area) { + case HYBBX_AREA_MAIN: + return "main"; + case HYBBX_AREA_MAIL: + return "mail"; + case HYBBX_AREA_CHAT: + return "chat"; + case HYBBX_AREA_CONFERENCE: + return "conference"; + case HYBBX_AREA_PROXYMAIL: + return "proxymail"; + case HYBBX_AREA_PROXYCHAT: + return "proxychat"; + default: + return "main"; + } +} + +hybbx_session_area_t hybbx_session_area_parse(const char *name) +{ + if (name == NULL || name[0] == '\0') { + return HYBBX_AREA_MAIN; + } + + if (session_str_ieq(name, "main")) { + return HYBBX_AREA_MAIN; + } + + if (session_str_ieq(name, "mail")) { + return HYBBX_AREA_MAIL; + } + + if (session_str_ieq(name, "chat")) { + return HYBBX_AREA_CHAT; + } + + if (session_str_ieq(name, "conference")) { + return HYBBX_AREA_CONFERENCE; + } + + if (session_str_ieq(name, "proxymail")) { + return HYBBX_AREA_PROXYMAIL; + } + + if (session_str_ieq(name, "proxychat")) { + return HYBBX_AREA_PROXYCHAT; + } + + return HYBBX_AREA_MAIN; +} + +hybbx_result_t hybbx_session_enter_area(hybbx_session_t *session, + hybbx_session_area_t area) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + return session_area_push(core, area); +} + +hybbx_result_t hybbx_session_leave_area(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_session_area_t leaving; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area_depth <= 1) { + return HYBBX_OK; + } + + leaving = core->area_stack[core->area_depth - 1]; + session_area_clear_state(core, leaving); + core->area_depth--; + session_area_sync(core); + return HYBBX_OK; +} + +hybbx_result_t hybbx_session_go_main(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + while (core->area_depth > 1) { + hybbx_session_area_t leaving = core->area_stack[core->area_depth - 1]; + + session_area_clear_state(core, leaving); + core->area_depth--; + } + + core->area_stack[0] = HYBBX_AREA_MAIN; + core->area_depth = 1; + core->area = HYBBX_AREA_MAIN; + return HYBBX_OK; +} + +hybbx_result_t hybbx_session_join_chat_channel(hybbx_session_t *session, + unsigned channel_index) +{ + hybbx_session_core_t *core; + const hybbx_chat_config_t *chat; + const char *name; + + if (session == NULL || channel_index == 0) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || core->service == NULL) { + return HYBBX_ERR_INVALID; + } + + chat = hybbx_service_get_chat(core->service); + name = hybbx_chat_channel_name(chat, channel_index); + if (name == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + if (core->area != HYBBX_AREA_CHAT) { + hybbx_result_t push_rc = session_area_push(core, HYBBX_AREA_CHAT); + + if (push_rc != HYBBX_OK) { + return push_rc; + } + } + + core->chat_channel = channel_index; + + if (!core->chat_max_notice_shown) { + char notice[96]; + + snprintf(notice, sizeof(notice), "Max %u chars per message.", + chat->message_max); + hybbx_session_write_line(session, notice); + core->chat_max_notice_shown = 1; + } + + return HYBBX_OK; +} + +unsigned hybbx_session_chat_channel(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || core->area != HYBBX_AREA_CHAT) { + return 0; + } + + return core->chat_channel; +} + +hybbx_service_t *hybbx_session_service(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return NULL; + } + + return core->service; +} + +static int session_str_ieq_local(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +int hybbx_session_conference_may_invite(hybbx_session_t *session, + const char *target) +{ + hybbx_session_core_t *core; + size_t s; + size_t k; + time_t now; + unsigned count; + + if (session == NULL || target == NULL || target[0] == '\0') { + return 0; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + now = time(NULL); + for (s = 0; s < sizeof(core->conference_invite_rates) / + sizeof(core->conference_invite_rates[0]); s++) { + if (!session_str_ieq_local(core->conference_invite_rates[s].target, + target)) { + continue; + } + + count = 0; + for (k = 0; k < HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET; k++) { + time_t sent = core->conference_invite_rates[s].sent_at[k]; + + if (sent != 0 && + (now - sent) < (time_t)HYBBX_CONFERENCE_INVITE_WINDOW_SEC) { + count++; + } + } + + return count < HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET; + } + + return 1; +} + +void hybbx_session_conference_invite_sent(hybbx_session_t *session, + const char *target) +{ + hybbx_session_core_t *core; + size_t s; + size_t slot = (size_t)-1; + size_t empty = (size_t)-1; + size_t k; + time_t now; + + if (session == NULL || target == NULL || target[0] == '\0') { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + now = time(NULL); + + for (s = 0; s < sizeof(core->conference_invite_rates) / + sizeof(core->conference_invite_rates[0]); s++) { + if (core->conference_invite_rates[s].target[0] == '\0') { + if (empty == (size_t)-1) { + empty = s; + } + continue; + } + + if (session_str_ieq_local(core->conference_invite_rates[s].target, + target)) { + slot = s; + break; + } + } + + if (slot == (size_t)-1) { + slot = empty; + } + + if (slot == (size_t)-1) { + slot = 0; + } + + if (core->conference_invite_rates[slot].target[0] == '\0') { + hybbx_strlcpy(core->conference_invite_rates[slot].target, target, + sizeof(core->conference_invite_rates[slot].target)); + } + + for (k = 0; k < HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET - 1u; k++) { + core->conference_invite_rates[slot].sent_at[k] = + core->conference_invite_rates[slot].sent_at[k + 1]; + } + + core->conference_invite_rates[slot].sent_at[ + HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET - 1u] = now; +} + +void hybbx_session_set_conference_invite(hybbx_session_t *session, + const char *from_username, + const char *topic) +{ + hybbx_session_core_t *core; + + if (session == NULL || from_username == NULL || topic == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->conference_invite_pending = 1; + core->conference_invite_deadline = 0; + hybbx_strlcpy(core->conference_invite_from, from_username, + sizeof(core->conference_invite_from)); + hybbx_strlcpy(core->conference_invite_topic, topic, + sizeof(core->conference_invite_topic)); +} + +void hybbx_session_set_conference_invite_deadline(hybbx_session_t *session, + time_t deadline) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->conference_invite_deadline = deadline; +} + +time_t hybbx_session_conference_invite_deadline(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->conference_invite_pending) { + return 0; + } + + return core->conference_invite_deadline; +} + +void hybbx_session_clear_conference_invite(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->conference_invite_pending = 0; + core->conference_invite_deadline = 0; + core->conference_invite_from[0] = '\0'; + core->conference_invite_topic[0] = '\0'; +} + +int hybbx_conference_invite_pending(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->conference_invite_pending != 0; +} + +const char *hybbx_session_conference_invite_from(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->conference_invite_pending) { + return NULL; + } + + return core->conference_invite_from; +} + +const char *hybbx_session_conference_invite_topic(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->conference_invite_pending) { + return NULL; + } + + return core->conference_invite_topic; +} + +hybbx_result_t hybbx_session_join_conference(hybbx_session_t *session, + const char *topic, + const char *partner_username) +{ + hybbx_session_core_t *core; + const hybbx_chat_config_t *chat; + + if (session == NULL || topic == NULL || partner_username == NULL || + topic[0] == '\0' || partner_username[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || core->service == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area != HYBBX_AREA_CONFERENCE) { + hybbx_result_t push_rc = session_area_push(core, HYBBX_AREA_CONFERENCE); + + if (push_rc != HYBBX_OK) { + return push_rc; + } + } + + hybbx_strlcpy(core->conference_topic, topic, sizeof(core->conference_topic)); + hybbx_strlcpy(core->conference_partner, partner_username, + sizeof(core->conference_partner)); + + chat = hybbx_service_get_chat(core->service); + if (chat != NULL && !core->chat_max_notice_shown) { + char notice[96]; + + snprintf(notice, sizeof(notice), "Max %u chars per message.", + chat->message_max); + hybbx_session_write_line(session, notice); + core->chat_max_notice_shown = 1; + } + + return HYBBX_OK; +} + +void hybbx_session_clear_conference(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->conference_topic[0] = '\0'; + core->conference_partner[0] = '\0'; +} + +const char *hybbx_session_conference_partner(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || core->conference_partner[0] == '\0') { + return NULL; + } + + return core->conference_partner; +} + +int hybbx_session_mail_composing(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->mail_composing; +} + +hybbx_result_t hybbx_session_mail_compose_start(hybbx_session_t *session, + const char *to_user, + const char *subject) +{ + hybbx_session_core_t *core; + const hybbx_mail_config_t *mail; + hybbx_user_record_t recipient; + hybbx_storage_t *storage; + size_t subject_len; + + if (session == NULL || to_user == NULL || subject == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(core->service); + subject_len = strlen(subject); + if (mail != NULL && subject_len > mail->subject_max) { + return HYBBX_ERR_INVALID; + } + + storage = hybbx_service_get_storage(core->service); + if (storage != NULL && + hybbx_storage_resolve_user(storage, to_user, &recipient) == HYBBX_OK) { + hybbx_strlcpy(core->mail_compose_to, recipient.username, + sizeof(core->mail_compose_to)); + } else { + hybbx_strlcpy(core->mail_compose_to, to_user, + sizeof(core->mail_compose_to)); + hybbx_username_normalize(core->mail_compose_to); + } + hybbx_strlcpy(core->mail_compose_subject, subject, + sizeof(core->mail_compose_subject)); + core->mail_compose_body[0] = '\0'; + core->mail_compose_body_len = 0; + core->mail_composing = 1; + + if (core->area != HYBBX_AREA_MAIL) { + return session_area_push(core, HYBBX_AREA_MAIL); + } + + return HYBBX_OK; +} + +void hybbx_session_mail_compose_cancel(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->mail_composing = 0; + core->mail_compose_body[0] = '\0'; + core->mail_compose_body_len = 0; + core->mail_compose_to[0] = '\0'; + core->mail_compose_subject[0] = '\0'; +} + +const char *hybbx_session_mail_compose_body(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->mail_composing) { + return ""; + } + + return core->mail_compose_body; +} + +const char *hybbx_session_mail_compose_to(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->mail_composing) { + return ""; + } + + return core->mail_compose_to; +} + +const char *hybbx_session_mail_compose_subject(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->mail_composing) { + return ""; + } + + return core->mail_compose_subject; +} + +hybbx_result_t hybbx_session_enter_mail(hybbx_session_t *session) +{ + return hybbx_session_enter_area(session, HYBBX_AREA_MAIL); +} + +hybbx_result_t hybbx_session_enter_chat(hybbx_session_t *session) +{ + return hybbx_session_enter_area(session, HYBBX_AREA_CHAT); +} + +hybbx_result_t hybbx_session_enter_proxymail(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_result_t rc; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area != HYBBX_AREA_MAIL && + core->area != HYBBX_AREA_PROXYMAIL) { + rc = session_area_push(core, HYBBX_AREA_MAIL); + if (rc != HYBBX_OK) { + return rc; + } + } + + return session_area_push(core, HYBBX_AREA_PROXYMAIL); +} + +hybbx_result_t hybbx_session_enter_proxychat(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_result_t rc; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area != HYBBX_AREA_CHAT && + core->area != HYBBX_AREA_PROXYCHAT) { + rc = session_area_push(core, HYBBX_AREA_CHAT); + if (rc != HYBBX_OK) { + return rc; + } + } + + return session_area_push(core, HYBBX_AREA_PROXYCHAT); +} + +int hybbx_session_proxymail_composing(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->proxymail_composing; +} + +hybbx_result_t hybbx_session_proxymail_compose_start(hybbx_session_t *session, + const char *to_address, + const char *subject) +{ + hybbx_session_core_t *core; + const hybbx_mail_config_t *mail; + size_t subject_len; + + if (session == NULL || to_address == NULL || subject == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_proxymail_parse_address(to_address, NULL, 0, NULL, 0)) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(core->service); + subject_len = strlen(subject); + if (mail != NULL && subject_len > mail->subject_max) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(core->proxymail_compose_to, to_address, + sizeof(core->proxymail_compose_to)); + hybbx_strlcpy(core->proxymail_compose_subject, subject, + sizeof(core->proxymail_compose_subject)); + core->proxymail_compose_body[0] = '\0'; + core->proxymail_compose_body_len = 0; + core->proxymail_composing = 1; + + return hybbx_session_enter_proxymail(session); +} + +void hybbx_session_proxymail_compose_cancel(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->proxymail_composing = 0; + core->proxymail_compose_body[0] = '\0'; + core->proxymail_compose_body_len = 0; + core->proxymail_compose_to[0] = '\0'; + core->proxymail_compose_subject[0] = '\0'; +} + +const char *hybbx_session_proxymail_compose_body(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->proxymail_composing) { + return ""; + } + + return core->proxymail_compose_body; +} + +const char *hybbx_session_proxymail_compose_to(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->proxymail_composing) { + return ""; + } + + return core->proxymail_compose_to; +} + +const char *hybbx_session_proxymail_compose_subject( + const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->proxymail_composing) { + return ""; + } + + return core->proxymail_compose_subject; +} + +time_t hybbx_session_connected_at(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->record.connected_at; +} + +int hybbx_session_bandwidth_paused(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->bandwidth_paused; +} + +void hybbx_session_set_bandwidth_paused(hybbx_session_t *session, int paused) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->bandwidth_paused = paused ? 1 : 0; +} + +void hybbx_session_disconnect_bandwidth(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->logged_in) { + return; + } + + core->bandwidth_paused = 0; + (void)hybbx_traffic_emit(session, &core->out_col, + HYBBX_BANDWIDTH_DISCONNECT_MSG, + strlen(HYBBX_BANDWIDTH_DISCONNECT_MSG)); + (void)hybbx_traffic_emit(session, &core->out_col, "\n", 1); + core->bandwidth_disconnect = 1; +} diff --git a/src/core/storage.c b/src/core/storage.c new file mode 100644 index 0000000..8217dd9 --- /dev/null +++ b/src/core/storage.c @@ -0,0 +1,378 @@ +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/config.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "storage_private.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define HYBBX_SQL_DEFAULT_USER_DB "users.db" +#define HYBBX_SQL_DEFAULT_MAIL_DB "mail.db" + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +void hybbx_storage_sql_config_defaults(hybbx_storage_sql_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->user_db[0] = '\0'; + cfg->mail_db[0] = '\0'; + cfg->backup_interval_sec = HYBBX_STORAGE_BACKUP_INTERVAL_DEFAULT_SEC; + cfg->backup_path[0] = '\0'; +} + +void hybbx_storage_sql_config_apply(hybbx_storage_sql_config_t *cfg, + const hybbx_config_t *config, + const char *storage_path) +{ + const char *user_db; + const char *mail_db; + const char *backup_path; + + hybbx_storage_sql_config_defaults(cfg); + if (storage_path == NULL || storage_path[0] == '\0') { + return; + } + + user_db = HYBBX_SQL_DEFAULT_USER_DB; + mail_db = HYBBX_SQL_DEFAULT_MAIL_DB; + backup_path = ""; + + if (config != NULL) { + user_db = hybbx_config_get(config, "storage", "user_db", + HYBBX_SQL_DEFAULT_USER_DB); + mail_db = hybbx_config_get(config, "storage", "mail_db", + HYBBX_SQL_DEFAULT_MAIL_DB); + backup_path = hybbx_config_get(config, "storage", "backup_path", ""); + cfg->backup_interval_sec = hybbx_config_get_uint( + config, "storage", "backup_interval", + HYBBX_STORAGE_BACKUP_INTERVAL_DEFAULT_SEC, 60u, 86400u); + } + + (void)hybbx_path_join(cfg->user_db, sizeof(cfg->user_db), storage_path, + user_db); + (void)hybbx_path_join(cfg->mail_db, sizeof(cfg->mail_db), storage_path, + mail_db); + + if (backup_path != NULL && backup_path[0] != '\0') { + char resolved[HYBBX_PATH_MAX]; + + if (hybbx_path_resolve(resolved, sizeof(resolved), backup_path) == + HYBBX_OK) { + hybbx_strlcpy(cfg->backup_path, resolved, sizeof(cfg->backup_path)); + } else { + (void)hybbx_path_join(cfg->backup_path, sizeof(cfg->backup_path), + storage_path, backup_path); + } + } +} + +const hybbx_storage_sql_config_t *hybbx_storage_sql_config( + const hybbx_storage_t *storage) +{ + if (storage == NULL) { + return NULL; + } + + return &storage->sql_cfg; +} + +void hybbx_storage_backup_tick(hybbx_storage_t *storage) +{ + hybbx_storage_sql_backup_tick(storage); +} + +static void storage_dispatch_sqlite(hybbx_storage_t *storage, + const hybbx_storage_sql_config_t *sql_cfg) +{ + hybbx_storage_sql_config_defaults(&storage->sql_cfg); + if (sql_cfg != NULL) { + storage->sql_cfg = *sql_cfg; + } else if (storage->path != NULL) { + char user_db[HYBBX_PATH_MAX]; + char mail_db[HYBBX_PATH_MAX]; + + (void)hybbx_path_join(user_db, sizeof(user_db), storage->path, + HYBBX_SQL_DEFAULT_USER_DB); + (void)hybbx_path_join(mail_db, sizeof(mail_db), storage->path, + HYBBX_SQL_DEFAULT_MAIL_DB); + hybbx_strlcpy(storage->sql_cfg.user_db, user_db, + sizeof(storage->sql_cfg.user_db)); + hybbx_strlcpy(storage->sql_cfg.mail_db, mail_db, + sizeof(storage->sql_cfg.mail_db)); + } +} + +hybbx_storage_t *hybbx_storage_open(const hybbx_storage_options_t *options) +{ + hybbx_storage_t *storage; + hybbx_result_t rc; + + if (options == NULL || options->path == NULL) { + return NULL; + } + + storage = calloc(1, sizeof(*storage)); + if (storage == NULL) { + return NULL; + } + + storage->backend = options->backend; + storage->path = hybbx_strdup(options->path); + if (storage->path == NULL) { + free(storage); + return NULL; + } + + if (options->guest_prefix != NULL && options->guest_prefix[0] != '\0') { + hybbx_strlcpy(storage->guest_prefix, options->guest_prefix, + sizeof(storage->guest_prefix)); + } else { + hybbx_strlcpy(storage->guest_prefix, HYBBX_AUTH_DEFAULT_GUEST_PREFIX, + sizeof(storage->guest_prefix)); + } + + if (storage->backend == HYBBX_STORAGE_SQLITE) { + storage_dispatch_sqlite(storage, options->sql_cfg); + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + rc = hybbx_storage_flatfile_open(storage); + break; + case HYBBX_STORAGE_SQLITE: + case HYBBX_STORAGE_MYSQL: + case HYBBX_STORAGE_MARIADB: + rc = hybbx_storage_sql_open(storage); + break; + default: + rc = HYBBX_ERR_UNSUPPORTED; + break; + } + + if (rc != HYBBX_OK) { + hybbx_storage_close(storage); + return NULL; + } + + return storage; +} + +void hybbx_storage_close(hybbx_storage_t *storage) +{ + if (storage == NULL) { + return; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + hybbx_storage_flatfile_close(storage); + break; + case HYBBX_STORAGE_SQLITE: + case HYBBX_STORAGE_MYSQL: + case HYBBX_STORAGE_MARIADB: + hybbx_storage_sql_close(storage); + break; + default: + break; + } + + free(storage->path); + free(storage); +} + +hybbx_storage_backend_kind_t hybbx_storage_backend(const hybbx_storage_t *storage) +{ + if (storage == NULL) { + return HYBBX_STORAGE_FLATFILE; + } + return storage->backend; +} + +const char *hybbx_storage_root_path(const hybbx_storage_t *storage) +{ + if (storage == NULL || storage->path == NULL || storage->path[0] == '\0') { + return NULL; + } + return storage->path; +} + +hybbx_result_t hybbx_storage_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out) +{ + if (storage == NULL || reg == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_register_user(storage, reg, out); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_register_user(storage, reg, out); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out) +{ + if (storage == NULL || username == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_find_user(storage, username, out); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_find_user(storage, username, out); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out) +{ + if (storage == NULL || name == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_resolve_user(storage, name, out); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_resolve_user(storage, name, out); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count) +{ + if (storage == NULL || count == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_count_level(storage, level, count); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_count_level(storage, level, count); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx) +{ + if (storage == NULL || fn == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_foreach_user(storage, fn, ctx); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_foreach_user(storage, fn, ctx); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out) +{ + if (storage == NULL || user == NULL || transport == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_session_begin(storage, user, transport, out); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_session_begin(storage, user, transport, out); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_session_end(hybbx_storage_t *storage, + uint64_t session_id) +{ + if (storage == NULL || session_id == 0) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_session_end(storage, session_id); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_session_end(storage, session_id); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user) +{ + if (storage == NULL || user == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_update_user(storage, user); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_update_user(storage, user); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_delete_user(hybbx_storage_t *storage, + uint64_t user_id) +{ + if (storage == NULL || user_id == 0) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_delete_user(storage, user_id); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_delete_user(storage, user_id); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} diff --git a/src/core/storage_flatfile.c b/src/core/storage_flatfile.c new file mode 100644 index 0000000..4915da6 --- /dev/null +++ b/src/core/storage_flatfile.c @@ -0,0 +1,1507 @@ +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/password.h" +#include "hybbx/security.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/log.h" +#include "storage_private.h" + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> + +struct flatfile_state { + char users_dir[HYBBX_PATH_MAX]; + char legacy_users_path[HYBBX_PATH_MAX]; + char sessions_path[HYBBX_PATH_MAX]; + char session_next_path[HYBBX_PATH_MAX]; + char user_next_path[HYBBX_PATH_MAX]; +}; + +typedef struct user_shard { + hybbx_user_record_t users[HYBBX_USERS_PER_FILE]; + size_t count; +} user_shard_t; + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static hybbx_result_t read_counter(const char *path, uint64_t *value) +{ + FILE *fp; + unsigned long long n = 0; + + fp = fopen(path, "r"); + if (fp == NULL) { + *value = 0; + return HYBBX_OK; + } + + if (fscanf(fp, "%llu", &n) != 1) { + fclose(fp); + return HYBBX_ERR_IO; + } + + fclose(fp); + *value = (uint64_t)n; + return HYBBX_OK; +} + +static hybbx_result_t write_counter(const char *path, uint64_t value) +{ + FILE *fp; + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%llu\n", (unsigned long long)value); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t bump_counter(const char *path, uint64_t *value) +{ + hybbx_result_t rc; + + rc = read_counter(path, value); + if (rc != HYBBX_OK) { + return rc; + } + + (*value)++; + return write_counter(path, *value); +} + +static hybbx_result_t append_line(const char *path, const char *line) +{ + FILE *fp; + + fp = fopen(path, "a"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%s\n", line); + fclose(fp); + return HYBBX_OK; +} + +#define HYBBX_USER_NICK_SUFFIX ".nick" + +static hybbx_result_t user_nickname_path(const struct flatfile_state *state, + const char *username, + char *out, size_t out_len) +{ + char name[HYBBX_USER_NAME_MAX + 8]; + int n; + + if (state == NULL || username == NULL || username[0] == '\0' || + out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + n = snprintf(name, sizeof(name), "%s%s", username, HYBBX_USER_NICK_SUFFIX); + if (n < 0 || (size_t)n >= sizeof(name)) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, state->users_dir, name); +} + +static hybbx_result_t read_nickname_file(const struct flatfile_state *state, + const char *username, + char *nickname, + size_t nickname_len) +{ + char path[HYBBX_PATH_MAX]; + FILE *fp; + char line[HYBBX_USER_NICKNAME_MAX]; + char *nl; + hybbx_result_t rc; + + if (nickname == NULL || nickname_len == 0) { + return HYBBX_ERR_INVALID; + } + + nickname[0] = '\0'; + + rc = user_nickname_path(state, username, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + if (fgets(line, sizeof(line), fp) == NULL) { + fclose(fp); + return HYBBX_ERR_IO; + } + + fclose(fp); + + nl = strchr(line, '\n'); + if (nl != NULL) { + *nl = '\0'; + } + nl = strchr(line, '\r'); + if (nl != NULL) { + *nl = '\0'; + } + + if (line[0] == '\0') { + return HYBBX_ERR_NOT_FOUND; + } + + hybbx_strlcpy(nickname, line, nickname_len); + return HYBBX_OK; +} + +static hybbx_result_t write_nickname_file(const struct flatfile_state *state, + const char *username, + const char *nickname) +{ + char path[HYBBX_PATH_MAX]; + FILE *fp; + hybbx_result_t rc; + + if (state == NULL || username == NULL || username[0] == '\0' || + nickname == NULL || nickname[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + rc = user_nickname_path(state, username, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%s\n", nickname); + if (fclose(fp) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void remove_nickname_file(const struct flatfile_state *state, + const char *username) +{ + char path[HYBBX_PATH_MAX]; + + if (state == NULL || username == NULL || username[0] == '\0') { + return; + } + + if (user_nickname_path(state, username, path, sizeof(path)) == HYBBX_OK) { + (void)remove(path); + } +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int file_exists(const char *path) +{ + struct stat st; + + return path != NULL && stat(path, &st) == 0 && S_ISREG(st.st_mode); +} + +static hybbx_result_t user_shard_path(const struct flatfile_state *state, + unsigned shard_index, + char *out, size_t out_len) +{ + char name[32]; + int n; + + if (state == NULL || out == NULL || out_len == 0 || shard_index == 0) { + return HYBBX_ERR_INVALID; + } + + if (shard_index == 1) { + n = snprintf(name, sizeof(name), "users.ini"); + } else { + n = snprintf(name, sizeof(name), "users%u.ini", shard_index); + } + + if (n < 0 || (size_t)n >= sizeof(name)) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, state->users_dir, name); +} + +static unsigned last_user_shard_index(const struct flatfile_state *state) +{ + char path[HYBBX_PATH_MAX]; + unsigned index; + + for (index = 1; index < 10000u; index++) { + if (user_shard_path(state, index, path, sizeof(path)) != HYBBX_OK) { + break; + } + if (!file_exists(path)) { + break; + } + } + + if (index == 1) { + return 0; + } + + return index - 1; +} + +static int parse_legacy_user_line(const char *line, hybbx_user_record_t *out) +{ + unsigned long long id = 0; + unsigned long long active = 0; + long long created = 0; + char username[HYBBX_USER_NAME_MAX]; + char level_name[32]; + char full_name[HYBBX_USER_FULL_NAME_MAX]; + char country[HYBBX_USER_COUNTRY_MAX]; + char location[HYBBX_USER_LOCATION_MAX]; + char email[HYBBX_USER_EMAIL_MAX]; + char password[HYBBX_USER_PASSWORD_MAX]; + int fields; + + if (line == NULL || out == NULL) { + return 0; + } + + full_name[0] = '\0'; + country[0] = '\0'; + location[0] = '\0'; + email[0] = '\0'; + password[0] = '\0'; + + fields = sscanf(line, + "%llu|%63[^|]|%31[^|]|%llu|%lld|" + "%127[^|]|%63[^|]|%127[^|]|%127[^|]|%95[^|]", + &id, username, level_name, &active, &created, + full_name, country, location, email, password); + if (fields < 5) { + return 0; + } + + memset(out, 0, sizeof(*out)); + out->id = (uint64_t)id; + hybbx_strlcpy(out->username, username, sizeof(out->username)); + out->level = hybbx_user_level_parse(level_name); + out->active = (int)active; + out->created_at = (time_t)created; + + if (fields >= 9) { + hybbx_strlcpy(out->full_name, full_name, sizeof(out->full_name)); + hybbx_strlcpy(out->country, country, sizeof(out->country)); + hybbx_strlcpy(out->location, location, sizeof(out->location)); + hybbx_strlcpy(out->email, email, sizeof(out->email)); + } + + if (fields >= 10) { + hybbx_strlcpy(out->password, password, sizeof(out->password)); + } + + return 1; +} + +static int parse_user_section(const hybbx_config_t *cfg, const char *section, + hybbx_user_record_t *out) +{ + const char *value; + unsigned long long n; + + if (cfg == NULL || section == NULL || out == NULL) { + return 0; + } + + if (strncmp(section, HYBBX_USER_INI_SECTION_PREFIX, + strlen(HYBBX_USER_INI_SECTION_PREFIX)) != 0) { + return 0; + } + + memset(out, 0, sizeof(*out)); + + value = hybbx_config_get(cfg, section, "id", NULL); + if (value == NULL || value[0] == '\0') { + return 0; + } + n = strtoull(value, NULL, 10); + if (n == 0) { + return 0; + } + out->id = (uint64_t)n; + + value = hybbx_config_get(cfg, section, "username", NULL); + if (value == NULL || value[0] == '\0') { + return 0; + } + hybbx_strlcpy(out->username, value, sizeof(out->username)); + hybbx_username_normalize(out->username); + + value = hybbx_config_get(cfg, section, "nickname", NULL); + if (value != NULL) { + hybbx_strlcpy(out->nickname, value, sizeof(out->nickname)); + } + + value = hybbx_config_get(cfg, section, "level", "user"); + out->level = hybbx_user_level_parse(value); + out->active = hybbx_config_get_bool(cfg, section, "active", 0); + + value = hybbx_config_get(cfg, section, "created", NULL); + if (value != NULL && value[0] != '\0') { + out->created_at = (time_t)strtoll(value, NULL, 10); + } + + value = hybbx_config_get(cfg, section, "fullname", NULL); + if (value != NULL) { + hybbx_strlcpy(out->full_name, value, sizeof(out->full_name)); + } + + value = hybbx_config_get(cfg, section, "country", NULL); + if (value != NULL) { + hybbx_strlcpy(out->country, value, sizeof(out->country)); + } + + value = hybbx_config_get(cfg, section, "location", NULL); + if (value != NULL) { + hybbx_strlcpy(out->location, value, sizeof(out->location)); + } + + value = hybbx_config_get(cfg, section, "email", NULL); + if (value != NULL) { + hybbx_strlcpy(out->email, value, sizeof(out->email)); + } + + value = hybbx_config_get(cfg, section, "password", NULL); + if (value != NULL) { + hybbx_strlcpy(out->password, value, sizeof(out->password)); + } + + value = hybbx_config_get(cfg, section, "last_login", NULL); + if (value != NULL && value[0] != '\0') { + out->last_login_at = (time_t)strtoll(value, NULL, 10); + } + + return 1; +} + +typedef struct section_list_ctx { + char names[HYBBX_USERS_PER_FILE][HYBBX_CONFIG_SECTION_MAX]; + size_t count; +} section_list_ctx_t; + +static void section_list_collect(const char *section, const char *key, + const char *value, void *ctx) +{ + section_list_ctx_t *list = (section_list_ctx_t *)ctx; + size_t i; + + (void)key; + (void)value; + + if (section == NULL || list == NULL) { + return; + } + + if (strncmp(section, HYBBX_USER_INI_SECTION_PREFIX, + strlen(HYBBX_USER_INI_SECTION_PREFIX)) != 0) { + return; + } + + for (i = 0; i < list->count; i++) { + if (strcmp(list->names[i], section) == 0) { + return; + } + } + + if (list->count >= HYBBX_USERS_PER_FILE) { + return; + } + + hybbx_strlcpy(list->names[list->count], section, + sizeof(list->names[list->count])); + list->count++; +} + +static hybbx_result_t load_user_shard(const char *path, user_shard_t *shard) +{ + hybbx_config_t cfg; + section_list_ctx_t sections; + size_t i; + hybbx_result_t rc; + + if (path == NULL || shard == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(shard, 0, sizeof(*shard)); + memset(§ions, 0, sizeof(sections)); + + rc = hybbx_config_load(&cfg, path); + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_config_foreach(&cfg, section_list_collect, §ions); + + for (i = 0; i < sections.count; i++) { + hybbx_user_record_t user; + + if (!parse_user_section(&cfg, sections.names[i], &user)) { + continue; + } + + if (shard->count >= HYBBX_USERS_PER_FILE) { + hybbx_config_free(&cfg); + return HYBBX_ERR_IO; + } + + shard->users[shard->count++] = user; + } + + hybbx_config_free(&cfg); + return HYBBX_OK; +} + +static hybbx_result_t save_user_shard(const struct flatfile_state *state, + unsigned shard_index, + const user_shard_t *shard) +{ + char path[HYBBX_PATH_MAX]; + FILE *fp; + size_t i; + hybbx_result_t rc; + + if (state == NULL || shard == NULL || shard->count > HYBBX_USERS_PER_FILE) { + return HYBBX_ERR_INVALID; + } + + rc = user_shard_path(state, shard_index, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "; HyBBX user file (max %u users)\n", + (unsigned)HYBBX_USERS_PER_FILE); + fprintf(fp, "; shard %u\n\n", shard_index); + + for (i = 0; i < shard->count; i++) { + const hybbx_user_record_t *user = &shard->users[i]; + + fprintf(fp, "[user.%llu]\n", (unsigned long long)user->id); + fprintf(fp, "id = %llu\n", (unsigned long long)user->id); + fprintf(fp, "username = %s\n", user->username); + fprintf(fp, "nickname = %s\n", user->nickname); + fprintf(fp, "level = %s\n", hybbx_user_level_name(user->level)); + fprintf(fp, "active = %s\n", hybbx_bool_to_string(user->active)); + fprintf(fp, "created = %lld\n", (long long)user->created_at); + fprintf(fp, "fullname = %s\n", user->full_name); + fprintf(fp, "country = %s\n", user->country); + fprintf(fp, "location = %s\n", user->location); + fprintf(fp, "email = %s\n", user->email); + fprintf(fp, "password = %s\n", user->password); + fprintf(fp, "last_login = %lld\n", (long long)user->last_login_at); + + if (user->nickname[0] != '\0') { + (void)write_nickname_file(state, user->username, user->nickname); + } + } + + if (fclose(fp) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t find_user_shard(const struct flatfile_state *state, + uint64_t user_id, + unsigned *shard_index, + size_t *slot_index) +{ + unsigned shard; + unsigned last; + char path[HYBBX_PATH_MAX]; + user_shard_t loaded; + size_t i; + hybbx_result_t rc; + + if (state == NULL || user_id == 0 || shard_index == NULL || + slot_index == NULL) { + return HYBBX_ERR_INVALID; + } + + last = last_user_shard_index(state); + + for (shard = 1; shard <= last; shard++) { + if (user_shard_path(state, shard, path, sizeof(path)) != HYBBX_OK) { + continue; + } + if (!file_exists(path)) { + continue; + } + + rc = load_user_shard(path, &loaded); + if (rc != HYBBX_OK) { + return rc; + } + + for (i = 0; i < loaded.count; i++) { + if (loaded.users[i].id == user_id) { + *shard_index = shard; + *slot_index = i; + return HYBBX_OK; + } + } + } + + return HYBBX_ERR_NOT_FOUND; +} + +static hybbx_result_t append_user_record(struct flatfile_state *state, + const hybbx_user_record_t *user) +{ + unsigned shard; + unsigned last; + char path[HYBBX_PATH_MAX]; + user_shard_t loaded; + hybbx_result_t rc; + + if (state == NULL || user == NULL) { + return HYBBX_ERR_INVALID; + } + + last = last_user_shard_index(state); + if (last == 0) { + shard = 1; + memset(&loaded, 0, sizeof(loaded)); + } else { + rc = user_shard_path(state, last, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_user_shard(path, &loaded); + if (rc != HYBBX_OK) { + return rc; + } + + if (loaded.count >= HYBBX_USERS_PER_FILE) { + shard = last + 1; + memset(&loaded, 0, sizeof(loaded)); + } else { + shard = last; + } + } + + if (loaded.count >= HYBBX_USERS_PER_FILE) { + return HYBBX_ERR_BUSY; + } + + loaded.users[loaded.count++] = *user; + return save_user_shard(state, shard, &loaded); +} + +static hybbx_result_t ensure_default_sysop(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + size_t sysop_count = 0; + hybbx_user_record_t sysop; + char plain_password[HYBBX_PASSWORD_MAX_LEN + 1]; + uint64_t user_id; + time_t now = time(NULL); + hybbx_result_t rc; + + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_flatfile_count_level(storage, HYBBX_LEVEL_SYSOP, + &sysop_count); + if (rc != HYBBX_OK) { + return rc; + } + + if (sysop_count > 0) { + return HYBBX_OK; + } + + rc = bump_counter(state->user_next_path, &user_id); + if (rc != HYBBX_OK) { + return rc; + } + + memset(&sysop, 0, sizeof(sysop)); + sysop.id = user_id; + hybbx_strlcpy(sysop.username, "sysop", sizeof(sysop.username)); + hybbx_strlcpy(sysop.nickname, HYBBX_DEFAULT_SYSOP_USERNAME, + sizeof(sysop.nickname)); + sysop.level = HYBBX_LEVEL_SYSOP; + sysop.active = 1; + sysop.created_at = now; + hybbx_strlcpy(sysop.full_name, "System Operator", sizeof(sysop.full_name)); + if (hybbx_password_generate_alnum(plain_password, sizeof(plain_password), + HYBBX_SYSOP_INIT_PASSWORD_MIN, + HYBBX_SYSOP_INIT_PASSWORD_MAX) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (hybbx_password_hash(plain_password, + sysop.password, sizeof(sysop.password)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + rc = append_user_record(state, &sysop); + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_log_info("[storage] created default Sysop at %s/users/users.ini (login: %s / %s)", + storage->path, sysop.nickname, plain_password); + hybbx_security_log_write("sysop_created user=%s", sysop.nickname); + return HYBBX_OK; +} + +static hybbx_result_t foreach_user(struct flatfile_state *state, + hybbx_result_t (*fn)(const hybbx_user_record_t *user, + void *ctx), + void *ctx) +{ + unsigned shard; + unsigned last; + char path[HYBBX_PATH_MAX]; + user_shard_t loaded; + size_t i; + hybbx_result_t rc; + + if (state == NULL || fn == NULL) { + return HYBBX_ERR_INVALID; + } + + last = last_user_shard_index(state); + for (shard = 1; shard <= last; shard++) { + rc = user_shard_path(state, shard, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_user_shard(path, &loaded); + if (rc != HYBBX_OK) { + return rc; + } + + for (i = 0; i < loaded.count; i++) { + rc = fn(&loaded.users[i], ctx); + if (rc != HYBBX_OK) { + return rc; + } + } + } + + return HYBBX_OK; +} + +typedef struct find_user_ctx { + const char *username; + hybbx_user_record_t *out; + int found; +} find_user_ctx_t; + +static hybbx_result_t find_user_cb(const hybbx_user_record_t *user, void *ctx) +{ + find_user_ctx_t *fctx = (find_user_ctx_t *)ctx; + + if (str_ieq(user->username, fctx->username)) { + *fctx->out = *user; + fctx->found = 1; + return HYBBX_ERR_BUSY; + } + + return HYBBX_OK; +} + +typedef struct find_nickname_ctx { + const char *nickname; + hybbx_user_record_t *out; + int found; +} find_nickname_ctx_t; + +static hybbx_result_t find_nickname_cb(const hybbx_user_record_t *user, void *ctx) +{ + find_nickname_ctx_t *fctx = (find_nickname_ctx_t *)ctx; + + if (user->nickname[0] != '\0' && str_ieq(user->nickname, fctx->nickname)) { + *fctx->out = *user; + fctx->found = 1; + return HYBBX_ERR_BUSY; + } + + return HYBBX_OK; +} + +typedef struct identity_taken_ctx { + const char *username; + const char *nickname; + int taken; +} identity_taken_ctx_t; + +static hybbx_result_t identity_taken_cb(const hybbx_user_record_t *user, + void *ctx) +{ + identity_taken_ctx_t *ictx = (identity_taken_ctx_t *)ctx; + + if (str_ieq(user->username, ictx->username) || + str_ieq(user->username, ictx->nickname) || + (user->nickname[0] != '\0' && + (str_ieq(user->nickname, ictx->nickname) || + str_ieq(user->nickname, ictx->username)))) { + ictx->taken = 1; + return HYBBX_ERR_BUSY; + } + + return HYBBX_OK; +} + +typedef struct count_level_ctx { + hybbx_user_level_t level; + size_t count; +} count_level_ctx_t; + +static hybbx_result_t count_level_cb(const hybbx_user_record_t *user, void *ctx) +{ + count_level_ctx_t *cctx = (count_level_ctx_t *)ctx; + + if (user->level == cctx->level) { + cctx->count++; + } + + return HYBBX_OK; +} + +typedef struct migrate_passwords_ctx { + hybbx_storage_t *storage; + hybbx_result_t last_error; +} migrate_passwords_ctx_t; + +static hybbx_result_t migrate_passwords_cb(const hybbx_user_record_t *user, + void *ctx) +{ + migrate_passwords_ctx_t *mctx = (migrate_passwords_ctx_t *)ctx; + hybbx_user_record_t updated; + hybbx_result_t rc; + + if (!hybbx_password_is_plain(user->password)) { + return HYBBX_OK; + } + + updated = *user; + rc = hybbx_password_hash(user->password, updated.password, + sizeof(updated.password)); + if (rc != HYBBX_OK) { + mctx->last_error = rc; + return rc; + } + + rc = hybbx_storage_update_user(mctx->storage, &updated); + if (rc != HYBBX_OK) { + mctx->last_error = rc; + return rc; + } + + hybbx_log_info("[storage] upgraded plain password to sha256 for user %s", + user->username); + return HYBBX_OK; +} + +static hybbx_result_t migrate_legacy_users_dat(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + FILE *fp; + char line[HYBBX_CONFIG_LINE_MAX]; + user_shard_t shard; + unsigned shard_index = 1; + hybbx_result_t rc; + + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!file_exists(state->legacy_users_path)) { + return HYBBX_OK; + } + + if (last_user_shard_index(state) > 0) { + return HYBBX_OK; + } + + fp = fopen(state->legacy_users_path, "r"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + memset(&shard, 0, sizeof(shard)); + + while (fgets(line, sizeof(line), fp) != NULL) { + hybbx_user_record_t user; + char *nl; + + nl = strchr(line, '\n'); + if (nl != NULL) { + *nl = '\0'; + } + + if (!parse_legacy_user_line(line, &user)) { + continue; + } + + if (shard.count >= HYBBX_USERS_PER_FILE) { + rc = save_user_shard(state, shard_index, &shard); + if (rc != HYBBX_OK) { + fclose(fp); + return rc; + } + shard_index++; + memset(&shard, 0, sizeof(shard)); + } + + shard.users[shard.count++] = user; + } + + fclose(fp); + + if (shard.count > 0) { + rc = save_user_shard(state, shard_index, &shard); + if (rc != HYBBX_OK) { + return rc; + } + } + + { + char backup[HYBBX_PATH_MAX]; + + if (hybbx_path_join(backup, sizeof(backup), storage->path, + "users.dat.migrated") == HYBBX_OK) { + (void)rename(state->legacy_users_path, backup); + } + } + + hybbx_log_info("[storage] migrated legacy users.dat to INI user files"); + return HYBBX_OK; +} + +typedef struct migrate_identities_ctx { + hybbx_storage_t *storage; + struct flatfile_state *state; + hybbx_result_t last_error; +} migrate_identities_ctx_t; + +static hybbx_result_t migrate_identities_cb(const hybbx_user_record_t *user, + void *ctx) +{ + migrate_identities_ctx_t *mctx = (migrate_identities_ctx_t *)ctx; + hybbx_user_record_t updated; + char stored_username[HYBBX_USER_NAME_MAX]; + int changed = 0; + hybbx_result_t rc; + + if (user == NULL || mctx == NULL) { + return HYBBX_OK; + } + + updated = *user; + hybbx_strlcpy(stored_username, updated.username, sizeof(stored_username)); + + hybbx_username_normalize(updated.username); + if (!str_ieq(stored_username, updated.username)) { + changed = 1; + } + + if (updated.nickname[0] == '\0') { + if (read_nickname_file(mctx->state, updated.username, updated.nickname, + sizeof(updated.nickname)) != HYBBX_OK) { + hybbx_nickname_infer(stored_username, updated.nickname, + sizeof(updated.nickname)); + } + changed = 1; + } + + if (!changed) { + (void)write_nickname_file(mctx->state, updated.username, + updated.nickname); + return HYBBX_OK; + } + + rc = hybbx_storage_flatfile_update_user(mctx->storage, &updated); + if (rc != HYBBX_OK) { + mctx->last_error = rc; + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t migrate_user_identities(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + migrate_identities_ctx_t ctx; + + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(&ctx, 0, sizeof(ctx)); + ctx.storage = storage; + ctx.state = state; + ctx.last_error = HYBBX_OK; + return foreach_user(state, migrate_identities_cb, &ctx); +} + +static hybbx_result_t migrate_plain_passwords(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + migrate_passwords_ctx_t ctx; + + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + ctx.storage = storage; + ctx.last_error = HYBBX_OK; + return foreach_user(state, migrate_passwords_cb, &ctx); +} + +hybbx_result_t hybbx_storage_flatfile_open(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + hybbx_result_t rc; + + if (storage == NULL || storage->path == NULL) { + return HYBBX_ERR_INVALID; + } + + if (mkdir_p(storage->path) != 0) { + hybbx_log_warn("[storage] cannot create data path '%s'", + storage->path); + return HYBBX_ERR_IO; + } + + state = calloc(1, sizeof(*state)); + if (state == NULL) { + return HYBBX_ERR_NOMEM; + } + + rc = hybbx_path_join(state->users_dir, sizeof(state->users_dir), + storage->path, "users"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + if (mkdir_p(state->users_dir) != 0) { + free(state); + return HYBBX_ERR_IO; + } + + rc = hybbx_path_join(state->legacy_users_path, sizeof(state->legacy_users_path), + storage->path, "users.dat"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + rc = hybbx_path_join(state->sessions_path, sizeof(state->sessions_path), + storage->path, "sessions.dat"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + rc = hybbx_path_join(state->session_next_path, sizeof(state->session_next_path), + storage->path, "session.next"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + rc = hybbx_path_join(state->user_next_path, sizeof(state->user_next_path), + storage->path, "user.next"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + storage->backend_data = state; + + rc = migrate_legacy_users_dat(storage); + if (rc != HYBBX_OK) { + hybbx_storage_flatfile_close(storage); + return rc; + } + + rc = ensure_default_sysop(storage); + if (rc != HYBBX_OK) { + hybbx_storage_flatfile_close(storage); + return rc; + } + + rc = migrate_user_identities(storage); + if (rc != HYBBX_OK) { + hybbx_storage_flatfile_close(storage); + return rc; + } + + rc = migrate_plain_passwords(storage); + if (rc != HYBBX_OK) { + hybbx_storage_flatfile_close(storage); + return rc; + } + + return HYBBX_OK; +} + +void hybbx_storage_flatfile_close(hybbx_storage_t *storage) +{ + if (storage == NULL) { + return; + } + + free(storage->backend_data); + storage->backend_data = NULL; +} + +hybbx_result_t hybbx_storage_flatfile_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out) +{ + struct flatfile_state *state; + find_user_ctx_t ctx; + hybbx_result_t rc; + + if (storage == NULL || username == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + ctx.username = username; + ctx.out = out; + ctx.found = 0; + + rc = foreach_user(state, find_user_cb, &ctx); + if (rc == HYBBX_ERR_BUSY) { + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_ERR_NOT_FOUND; +} + +hybbx_result_t hybbx_storage_flatfile_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out) +{ + char normalized[HYBBX_USER_NAME_MAX]; + struct flatfile_state *state; + find_nickname_ctx_t nctx; + hybbx_result_t rc; + + if (storage == NULL || name == NULL || out == NULL || name[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(normalized, name, sizeof(normalized)); + hybbx_username_normalize(normalized); + + rc = hybbx_storage_flatfile_find_user(storage, normalized, out); + if (rc == HYBBX_OK) { + return HYBBX_OK; + } + if (rc != HYBBX_ERR_NOT_FOUND) { + return rc; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + nctx.nickname = name; + nctx.out = out; + nctx.found = 0; + + rc = foreach_user(state, find_nickname_cb, &nctx); + if (rc == HYBBX_ERR_BUSY) { + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_ERR_NOT_FOUND; +} + +hybbx_result_t hybbx_storage_flatfile_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count) +{ + struct flatfile_state *state; + count_level_ctx_t ctx; + hybbx_result_t rc; + + if (storage == NULL || count == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + ctx.level = level; + ctx.count = 0; + + rc = foreach_user(state, count_level_cb, &ctx); + if (rc != HYBBX_OK) { + return rc; + } + + *count = ctx.count; + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_flatfile_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx) +{ + struct flatfile_state *state; + + if (storage == NULL || fn == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + return foreach_user(state, fn, ctx); +} + +hybbx_result_t hybbx_storage_flatfile_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out) +{ + struct flatfile_state *state; + hybbx_user_record_t existing; + uint64_t user_id; + time_t now = time(NULL); + hybbx_result_t rc; + + if (storage == NULL || reg == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_registration_valid(reg, storage->guest_prefix)) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_flatfile_find_user(storage, reg->username, &existing); + if (rc == HYBBX_OK) { + return HYBBX_ERR_BUSY; + } + if (rc != HYBBX_ERR_NOT_FOUND) { + return rc; + } + + { + identity_taken_ctx_t taken; + + memset(&taken, 0, sizeof(taken)); + taken.username = reg->username; + taken.nickname = reg->nickname; + + rc = foreach_user(state, identity_taken_cb, &taken); + if (rc == HYBBX_ERR_BUSY || taken.taken) { + return HYBBX_ERR_BUSY; + } + if (rc != HYBBX_OK) { + return rc; + } + } + + rc = bump_counter(state->user_next_path, &user_id); + if (rc != HYBBX_OK) { + return rc; + } + + memset(out, 0, sizeof(*out)); + out->id = user_id; + hybbx_strlcpy(out->username, reg->username, sizeof(out->username)); + hybbx_username_normalize(out->username); + hybbx_strlcpy(out->nickname, reg->nickname, sizeof(out->nickname)); + out->level = HYBBX_LEVEL_USER; + out->active = 0; + out->created_at = now; + hybbx_strlcpy(out->full_name, reg->full_name, sizeof(out->full_name)); + hybbx_strlcpy(out->country, reg->country, sizeof(out->country)); + hybbx_strlcpy(out->location, reg->location, sizeof(out->location)); + hybbx_strlcpy(out->email, reg->email, sizeof(out->email)); + + if (reg->password[0] != '\0') { + if (!hybbx_password_plain_valid(reg->password)) { + return HYBBX_ERR_INVALID; + } + rc = hybbx_password_hash(reg->password, out->password, + sizeof(out->password)); + if (rc != HYBBX_OK) { + return rc; + } + } + + return append_user_record(state, out); +} + +hybbx_result_t hybbx_storage_flatfile_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user) +{ + struct flatfile_state *state; + unsigned shard_index; + size_t slot_index; + char path[HYBBX_PATH_MAX]; + user_shard_t shard; + hybbx_result_t rc; + + if (storage == NULL || user == NULL || user->id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = find_user_shard(state, user->id, &shard_index, &slot_index); + if (rc != HYBBX_OK) { + return rc; + } + + rc = user_shard_path(state, shard_index, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_user_shard(path, &shard); + if (rc != HYBBX_OK) { + return rc; + } + + if (slot_index >= shard.count) { + return HYBBX_ERR_NOT_FOUND; + } + + shard.users[slot_index] = *user; + hybbx_username_normalize(shard.users[slot_index].username); + return save_user_shard(state, shard_index, &shard); +} + +hybbx_result_t hybbx_storage_flatfile_delete_user(hybbx_storage_t *storage, + uint64_t user_id) +{ + struct flatfile_state *state; + unsigned shard_index; + size_t slot_index; + char path[HYBBX_PATH_MAX]; + user_shard_t shard; + hybbx_result_t rc; + + if (storage == NULL || user_id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = find_user_shard(state, user_id, &shard_index, &slot_index); + if (rc != HYBBX_OK) { + return rc; + } + + rc = user_shard_path(state, shard_index, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_user_shard(path, &shard); + if (rc != HYBBX_OK) { + return rc; + } + + if (slot_index >= shard.count) { + return HYBBX_ERR_NOT_FOUND; + } + + remove_nickname_file(state, shard.users[slot_index].username); + + if (slot_index + 1 < shard.count) { + memmove(&shard.users[slot_index], &shard.users[slot_index + 1], + (shard.count - slot_index - 1) * sizeof(shard.users[0])); + } + shard.count--; + + return save_user_shard(state, shard_index, &shard); +} + +hybbx_result_t hybbx_storage_flatfile_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out) +{ + struct flatfile_state *state = storage->backend_data; + char line[384]; + time_t now = time(NULL); + hybbx_result_t rc; + + if (state == NULL || user == NULL || transport == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + + rc = bump_counter(state->session_next_path, &out->session_id); + if (rc != HYBBX_OK) { + return rc; + } + + out->user_id = user->id; + hybbx_strlcpy(out->username, user->username, sizeof(out->username)); + hybbx_strlcpy(out->transport, transport, sizeof(out->transport)); + out->connected_at = now; + out->active = 1; + + snprintf(line, sizeof(line), "%llu|%llu|%s|%s|%lld|0|1", + (unsigned long long)out->session_id, + (unsigned long long)out->user_id, + out->username, out->transport, (long long)out->connected_at); + + return append_line(state->sessions_path, line); +} + +hybbx_result_t hybbx_storage_flatfile_session_end(hybbx_storage_t *storage, + uint64_t session_id) +{ + struct flatfile_state *state = storage->backend_data; + char line[384]; + time_t now = time(NULL); + + if (state == NULL || session_id == 0) { + return HYBBX_ERR_INVALID; + } + + snprintf(line, sizeof(line), "%llu|0|_|_|0|%lld|0", + (unsigned long long)session_id, (long long)now); + + return append_line(state->sessions_path, line); +} diff --git a/src/core/storage_private.h b/src/core/storage_private.h new file mode 100644 index 0000000..579250c --- /dev/null +++ b/src/core/storage_private.h @@ -0,0 +1,81 @@ +#ifndef HYBBX_STORAGE_PRIVATE_H +#define HYBBX_STORAGE_PRIVATE_H + +#include "hybbx/auth.h" +#include "hybbx/storage.h" + +#include <stddef.h> + +struct hybbx_storage { + hybbx_storage_backend_kind_t backend; + char *path; + char guest_prefix[HYBBX_AUTH_GUEST_PREFIX_MAX]; + hybbx_storage_sql_config_t sql_cfg; + void *backend_data; +}; + +hybbx_result_t hybbx_storage_flatfile_open(hybbx_storage_t *storage); +void hybbx_storage_flatfile_close(hybbx_storage_t *storage); +hybbx_result_t hybbx_storage_flatfile_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out); +hybbx_result_t hybbx_storage_flatfile_session_end(hybbx_storage_t *storage, + uint64_t session_id); +hybbx_result_t hybbx_storage_flatfile_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_flatfile_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_flatfile_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count); +hybbx_result_t hybbx_storage_flatfile_foreach_user( + hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx); +hybbx_result_t hybbx_storage_flatfile_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_flatfile_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user); +hybbx_result_t hybbx_storage_flatfile_delete_user(hybbx_storage_t *storage, + uint64_t user_id); + +hybbx_result_t hybbx_storage_sql_open(hybbx_storage_t *storage); +void hybbx_storage_sql_close(hybbx_storage_t *storage); +hybbx_result_t hybbx_storage_sql_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_sql_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_sql_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_sql_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count); +hybbx_result_t hybbx_storage_sql_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx); +hybbx_result_t hybbx_storage_sql_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user); +hybbx_result_t hybbx_storage_sql_delete_user(hybbx_storage_t *storage, + uint64_t user_id); +hybbx_result_t hybbx_storage_sql_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out); +hybbx_result_t hybbx_storage_sql_session_end(hybbx_storage_t *storage, + uint64_t session_id); +void hybbx_storage_sql_backup_files(const hybbx_storage_t *storage); +void hybbx_storage_sql_backup_tick(hybbx_storage_t *storage); + +#ifdef HYBBX_HAVE_SQLITE +struct sqlite3; +struct sqlite3 *hybbx_storage_sql_mail_db(hybbx_storage_t *storage); +#endif + +#endif /* HYBBX_STORAGE_PRIVATE_H */ diff --git a/src/core/storage_sql.c b/src/core/storage_sql.c new file mode 100644 index 0000000..4a5dc13 --- /dev/null +++ b/src/core/storage_sql.c @@ -0,0 +1,1209 @@ +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/config.h" +#include "hybbx/password.h" +#include "hybbx/security.h" +#include "hybbx/util.h" +#include "hybbx/log.h" +#include "storage_private.h" + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +#ifdef HYBBX_HAVE_SQLITE +#include <sqlite3.h> +#endif + +#define HYBBX_SQL_DEFAULT_USER_DB "users.db" +#define HYBBX_SQL_DEFAULT_MAIL_DB "mail.db" + +#ifndef HYBBX_HAVE_SQLITE + +hybbx_result_t hybbx_storage_sql_open(hybbx_storage_t *storage) +{ + (void)storage; + hybbx_log_warn("[storage] SQLite backend requested but hybbx was built without " + "libsqlite3 — use backend=flatfile or rebuild with sqlite3"); + return HYBBX_ERR_UNSUPPORTED; +} + +void hybbx_storage_sql_close(hybbx_storage_t *storage) +{ + (void)storage; +} + +hybbx_result_t hybbx_storage_sql_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out) +{ + (void)storage; + (void)reg; + (void)out; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out) +{ + (void)storage; + (void)username; + (void)out; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out) +{ + (void)storage; + (void)name; + (void)out; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count) +{ + (void)storage; + (void)level; + (void)count; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx) +{ + (void)storage; + (void)fn; + (void)ctx; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user) +{ + (void)storage; + (void)user; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_delete_user(hybbx_storage_t *storage, + uint64_t user_id) +{ + (void)storage; + (void)user_id; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out) +{ + (void)storage; + (void)user; + (void)transport; + (void)out; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_session_end(hybbx_storage_t *storage, + uint64_t session_id) +{ + (void)storage; + (void)session_id; + return HYBBX_ERR_UNSUPPORTED; +} + +void hybbx_storage_sql_backup_files(const hybbx_storage_t *storage) +{ + (void)storage; +} + +void hybbx_storage_sql_backup_tick(hybbx_storage_t *storage) +{ + (void)storage; +} + +#else /* HYBBX_HAVE_SQLITE */ + +struct sql_state { + sqlite3 *users_db; + sqlite3 *mail_db; + unsigned backup_tick; +}; + +static int sql_str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static hybbx_result_t sql_exec(sqlite3 *db, const char *sql) +{ + char *err = NULL; + int rc; + + if (db == NULL || sql == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_exec(db, sql, NULL, NULL, &err); + if (rc != SQLITE_OK) { + hybbx_log_warn("[storage] sqlite: %s", + err != NULL ? err : sqlite3_errmsg(db)); + sqlite3_free(err); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t sql_meta_get(sqlite3 *db, const char *key, uint64_t *value) +{ + sqlite3_stmt *stmt; + int rc; + + *value = 0; + + rc = sqlite3_prepare_v2(db, + "SELECT value FROM meta WHERE key = ?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + *value = (uint64_t)sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + return HYBBX_OK; +} + +static hybbx_result_t sql_meta_set(sqlite3 *db, const char *key, uint64_t value) +{ + sqlite3_stmt *stmt; + int rc; + + rc = sqlite3_prepare_v2(db, + "INSERT INTO meta(key,value) VALUES(?1,?2) " + "ON CONFLICT(key) DO UPDATE SET value=?2;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)value); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +static hybbx_result_t sql_meta_bump(sqlite3 *db, const char *key, uint64_t *value) +{ + hybbx_result_t rc; + + rc = sql_meta_get(db, key, value); + if (rc != HYBBX_OK) { + return rc; + } + + (*value)++; + return sql_meta_set(db, key, *value); +} + +static hybbx_result_t sql_open_db(const char *path, sqlite3 **out_db) +{ + int rc; + int existed = (access(path, F_OK) == 0); + + if (out_db == NULL) { + return HYBBX_ERR_INVALID; + } + + *out_db = NULL; + rc = sqlite3_open(path, out_db); + if (rc != SQLITE_OK) { + hybbx_log_warn("[storage] cannot open '%s': %s", + path, sqlite3_errmsg(*out_db)); + if (*out_db != NULL) { + sqlite3_close(*out_db); + *out_db = NULL; + } + return HYBBX_ERR_IO; + } + + sqlite3_busy_timeout(*out_db, 5000); + if (!existed) { + hybbx_log_info("[storage] created new database %s", path); + } else { + hybbx_log_info("[storage] opened existing database %s", path); + } + + return HYBBX_OK; +} + +static hybbx_result_t sql_init_users_schema(sqlite3 *db) +{ + static const char *schema = + "CREATE TABLE IF NOT EXISTS users (" + " id INTEGER PRIMARY KEY," + " username TEXT NOT NULL UNIQUE," + " nickname TEXT," + " level INTEGER NOT NULL," + " active INTEGER NOT NULL," + " created_at INTEGER NOT NULL," + " full_name TEXT," + " country TEXT," + " location TEXT," + " email TEXT," + " password TEXT NOT NULL," + " last_login_at INTEGER" + ");" + "CREATE INDEX IF NOT EXISTS idx_users_nickname ON users(nickname);" + "CREATE TABLE IF NOT EXISTS sessions (" + " session_id INTEGER PRIMARY KEY," + " user_id INTEGER NOT NULL," + " username TEXT," + " transport TEXT," + " remote TEXT," + " connected_at INTEGER," + " disconnected_at INTEGER," + " active INTEGER" + ");" + "CREATE TABLE IF NOT EXISTS meta (" + " key TEXT PRIMARY KEY," + " value INTEGER NOT NULL" + ");"; + + return sql_exec(db, schema); +} + +static hybbx_result_t sql_init_mail_schema(sqlite3 *db) +{ + static const char *schema = + "CREATE TABLE IF NOT EXISTS messages (" + " id INTEGER PRIMARY KEY," + " owner TEXT NOT NULL," + " from_user TEXT NOT NULL," + " subject TEXT," + " body TEXT," + " received_at INTEGER NOT NULL," + " read_flag INTEGER NOT NULL DEFAULT 0," + " deleted_at INTEGER," + " folder INTEGER NOT NULL DEFAULT 0" + ");" + "CREATE INDEX IF NOT EXISTS idx_mail_owner ON messages(owner, folder, received_at DESC);" + "CREATE TABLE IF NOT EXISTS meta (" + " key TEXT PRIMARY KEY," + " value INTEGER NOT NULL" + ");"; + + return sql_exec(db, schema); +} + +static void sql_row_to_user(sqlite3_stmt *stmt, hybbx_user_record_t *user) +{ + memset(user, 0, sizeof(*user)); + user->id = (uint64_t)sqlite3_column_int64(stmt, 0); + hybbx_strlcpy(user->username, (const char *)sqlite3_column_text(stmt, 1), + sizeof(user->username)); + if (sqlite3_column_text(stmt, 2) != NULL) { + hybbx_strlcpy(user->nickname, (const char *)sqlite3_column_text(stmt, 2), + sizeof(user->nickname)); + } + user->level = (hybbx_user_level_t)sqlite3_column_int(stmt, 3); + user->active = sqlite3_column_int(stmt, 4); + user->created_at = (time_t)sqlite3_column_int64(stmt, 5); + if (sqlite3_column_text(stmt, 6) != NULL) { + hybbx_strlcpy(user->full_name, (const char *)sqlite3_column_text(stmt, 6), + sizeof(user->full_name)); + } + if (sqlite3_column_text(stmt, 7) != NULL) { + hybbx_strlcpy(user->country, (const char *)sqlite3_column_text(stmt, 7), + sizeof(user->country)); + } + if (sqlite3_column_text(stmt, 8) != NULL) { + hybbx_strlcpy(user->location, (const char *)sqlite3_column_text(stmt, 8), + sizeof(user->location)); + } + if (sqlite3_column_text(stmt, 9) != NULL) { + hybbx_strlcpy(user->email, (const char *)sqlite3_column_text(stmt, 9), + sizeof(user->email)); + } + if (sqlite3_column_text(stmt, 10) != NULL) { + hybbx_strlcpy(user->password, (const char *)sqlite3_column_text(stmt, 10), + sizeof(user->password)); + } + user->last_login_at = (time_t)sqlite3_column_int64(stmt, 11); +} + +static hybbx_result_t sql_ensure_default_sysop(hybbx_storage_t *storage) +{ + struct sql_state *state = storage->backend_data; + size_t sysop_count = 0; + hybbx_user_record_t sysop; + char plain_password[HYBBX_PASSWORD_MAX_LEN + 1]; + uint64_t user_id; + sqlite3_stmt *stmt; + int rc; + hybbx_result_t hres; + + hres = hybbx_storage_sql_count_level(storage, HYBBX_LEVEL_SYSOP, &sysop_count); + if (hres != HYBBX_OK) { + return hres; + } + if (sysop_count > 0) { + return HYBBX_OK; + } + + hres = sql_meta_bump(state->users_db, "user_next", &user_id); + if (hres != HYBBX_OK) { + return hres; + } + + memset(&sysop, 0, sizeof(sysop)); + sysop.id = user_id; + hybbx_strlcpy(sysop.username, "sysop", sizeof(sysop.username)); + hybbx_strlcpy(sysop.nickname, HYBBX_DEFAULT_SYSOP_USERNAME, + sizeof(sysop.nickname)); + sysop.level = HYBBX_LEVEL_SYSOP; + sysop.active = 1; + sysop.created_at = time(NULL); + hybbx_strlcpy(sysop.full_name, "System Operator", sizeof(sysop.full_name)); + + if (hybbx_password_generate_alnum(plain_password, sizeof(plain_password), + HYBBX_SYSOP_INIT_PASSWORD_MIN, + HYBBX_SYSOP_INIT_PASSWORD_MAX) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (hybbx_password_hash(plain_password, sysop.password, + sizeof(sysop.password)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + rc = sqlite3_prepare_v2(state->users_db, + "INSERT INTO users(id,username,nickname,level,active," + "created_at,full_name,country,location,email,password," + "last_login_at) VALUES(?1,?2,?3,?4,?5,?6,?7,'','','',?8,0);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)sysop.id); + sqlite3_bind_text(stmt, 2, sysop.username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, sysop.nickname, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 4, (int)sysop.level); + sqlite3_bind_int(stmt, 5, sysop.active); + sqlite3_bind_int64(stmt, 6, (sqlite3_int64)sysop.created_at); + sqlite3_bind_text(stmt, 7, sysop.full_name, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 8, sysop.password, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + + hybbx_log_info("[storage] created default Sysop in %s (login: %s / %s)", + storage->sql_cfg.user_db, sysop.nickname, plain_password); + hybbx_security_log_write("sysop_created user=%s", sysop.nickname); + return HYBBX_OK; +} + +static hybbx_result_t sql_copy_file(const char *src, const char *dst) +{ + FILE *in_fp; + FILE *out_fp; + unsigned char buf[4096]; + size_t n; + + in_fp = fopen(src, "rb"); + if (in_fp == NULL) { + return HYBBX_ERR_IO; + } + + out_fp = fopen(dst, "wb"); + if (out_fp == NULL) { + fclose(in_fp); + return HYBBX_ERR_IO; + } + + while ((n = fread(buf, 1, sizeof(buf), in_fp)) > 0) { + if (fwrite(buf, 1, n, out_fp) != n) { + fclose(in_fp); + fclose(out_fp); + return HYBBX_ERR_IO; + } + } + + fclose(in_fp); + if (fclose(out_fp) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void sql_backup_one(sqlite3 *db, const char *src_path, + const char *backup_dir) +{ + char dst[HYBBX_PATH_MAX]; + const char *base; + hybbx_result_t rc; + + if (db == NULL || src_path == NULL || src_path[0] == '\0') { + return; + } + + (void)sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, NULL, NULL); + + base = strrchr(src_path, '/'); + base = (base != NULL) ? base + 1 : src_path; + + if (backup_dir != NULL && backup_dir[0] != '\0') { + char name[HYBBX_PATH_MAX]; + + if (mkdir_p(backup_dir) != 0) { + return; + } + if (strlen(base) + strlen(HYBBX_STORAGE_BACKUP_SUFFIX) + 1 >= + sizeof(name)) { + return; + } + snprintf(name, sizeof(name), "%s%s", base, HYBBX_STORAGE_BACKUP_SUFFIX); + if (hybbx_path_join(dst, sizeof(dst), backup_dir, name) != HYBBX_OK) { + return; + } + } else { + if (strlen(src_path) + strlen(HYBBX_STORAGE_BACKUP_SUFFIX) + 1 >= + sizeof(dst)) { + return; + } + snprintf(dst, sizeof(dst), "%s%s", src_path, HYBBX_STORAGE_BACKUP_SUFFIX); + } + + rc = sql_copy_file(src_path, dst); + if (rc != HYBBX_OK) { + hybbx_log_warn("[storage] backup failed %s -> %s", src_path, dst); + } +} + +hybbx_result_t hybbx_storage_sql_open(hybbx_storage_t *storage) +{ + struct sql_state *state; + hybbx_result_t rc; + + if (storage == NULL || storage->path == NULL) { + return HYBBX_ERR_INVALID; + } + + if (storage->backend == HYBBX_STORAGE_MYSQL || + storage->backend == HYBBX_STORAGE_MARIADB) { + hybbx_log_warn("[storage] MySQL/MariaDB backends are not supported — " + "use flatfile or sqlite"); + return HYBBX_ERR_UNSUPPORTED; + } + + if (mkdir_p(storage->path) != 0) { + hybbx_log_warn("[storage] cannot create data path '%s'", + storage->path); + return HYBBX_ERR_IO; + } + + state = calloc(1, sizeof(*state)); + if (state == NULL) { + return HYBBX_ERR_NOMEM; + } + + rc = sql_open_db(storage->sql_cfg.user_db, &state->users_db); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + rc = sql_init_users_schema(state->users_db); + if (rc != HYBBX_OK) { + hybbx_storage_sql_close(storage); + free(state); + return rc; + } + + rc = sql_open_db(storage->sql_cfg.mail_db, &state->mail_db); + if (rc != HYBBX_OK) { + sqlite3_close(state->users_db); + free(state); + return rc; + } + + rc = sql_init_mail_schema(state->mail_db); + if (rc != HYBBX_OK) { + sqlite3_close(state->users_db); + sqlite3_close(state->mail_db); + free(state); + return rc; + } + + storage->backend_data = state; + + rc = sql_ensure_default_sysop(storage); + if (rc != HYBBX_OK) { + hybbx_storage_sql_close(storage); + return rc; + } + + hybbx_log_info("[storage] sqlite user_db=%s mail_db=%s backup_interval=%us", + storage->sql_cfg.user_db, storage->sql_cfg.mail_db, + storage->sql_cfg.backup_interval_sec); + + return HYBBX_OK; +} + +void hybbx_storage_sql_close(hybbx_storage_t *storage) +{ + struct sql_state *state; + + if (storage == NULL) { + return; + } + + state = storage->backend_data; + if (state == NULL) { + return; + } + + if (state->users_db != NULL) { + sqlite3_close(state->users_db); + } + if (state->mail_db != NULL) { + sqlite3_close(state->mail_db); + } + + free(state); + storage->backend_data = NULL; +} + +sqlite3 *hybbx_storage_sql_mail_db(hybbx_storage_t *storage) +{ + struct sql_state *state; + + if (storage == NULL) { + return NULL; + } + + state = storage->backend_data; + if (state == NULL) { + return NULL; + } + + return state->mail_db; +} + +static hybbx_result_t sql_find_user_by_column(sqlite3 *db, const char *column, + const char *name, + hybbx_user_record_t *out) +{ + sqlite3_stmt *stmt; + int rc; + const char *sql_username = + "SELECT id,username,nickname,level,active,created_at,full_name," + "country,location,email,password,last_login_at " + "FROM users WHERE lower(username)=lower(?1) LIMIT 1;"; + const char *sql_nickname = + "SELECT id,username,nickname,level,active,created_at,full_name," + "country,location,email,password,last_login_at " + "FROM users WHERE lower(nickname)=lower(?1) LIMIT 1;"; + const char *sql = sql_str_ieq(column, "nickname") ? sql_nickname : + sql_username; + + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + sql_row_to_user(stmt, out); + sqlite3_finalize(stmt); + return HYBBX_OK; + } + + sqlite3_finalize(stmt); + return HYBBX_ERR_NOT_FOUND; +} + +hybbx_result_t hybbx_storage_sql_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out) +{ + struct sql_state *state; + char normalized[HYBBX_USER_NAME_MAX]; + + if (storage == NULL || username == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(normalized, username, sizeof(normalized)); + hybbx_username_normalize(normalized); + return sql_find_user_by_column(state->users_db, "username", normalized, out); +} + +hybbx_result_t hybbx_storage_sql_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out) +{ + hybbx_result_t rc; + + if (storage == NULL || name == NULL || out == NULL || name[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_sql_find_user(storage, name, out); + if (rc == HYBBX_OK) { + return HYBBX_OK; + } + if (rc != HYBBX_ERR_NOT_FOUND) { + return rc; + } + + return sql_find_user_by_column( + ((struct sql_state *)storage->backend_data)->users_db, + "nickname", name, out); +} + +typedef struct foreach_ctx { + hybbx_storage_user_fn fn; + void *user_ctx; +} foreach_ctx_t; + +static int sql_foreach_cb(void *ctx, int n_cols, char **values, char **cols) +{ + hybbx_user_record_t user; + foreach_ctx_t *fctx = (foreach_ctx_t *)ctx; + hybbx_result_t rc; + int i; + + (void)n_cols; + (void)cols; + memset(&user, 0, sizeof(user)); + + for (i = 0; values[i] != NULL; i++) { + switch (i) { + case 0: user.id = (uint64_t)strtoull(values[i], NULL, 10); break; + case 1: hybbx_strlcpy(user.username, values[i], sizeof(user.username)); break; + case 2: hybbx_strlcpy(user.nickname, values[i], sizeof(user.nickname)); break; + case 3: user.level = (hybbx_user_level_t)atoi(values[i]); break; + case 4: user.active = atoi(values[i]); break; + case 5: user.created_at = (time_t)strtoll(values[i], NULL, 10); break; + case 6: hybbx_strlcpy(user.full_name, values[i], sizeof(user.full_name)); break; + case 7: hybbx_strlcpy(user.country, values[i], sizeof(user.country)); break; + case 8: hybbx_strlcpy(user.location, values[i], sizeof(user.location)); break; + case 9: hybbx_strlcpy(user.email, values[i], sizeof(user.email)); break; + case 10: hybbx_strlcpy(user.password, values[i], sizeof(user.password)); break; + case 11: user.last_login_at = (time_t)strtoll(values[i], NULL, 10); break; + default: break; + } + } + + rc = fctx->fn(&user, fctx->user_ctx); + return (rc == HYBBX_OK) ? 0 : 1; +} + +hybbx_result_t hybbx_storage_sql_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx) +{ + struct sql_state *state; + foreach_ctx_t fctx; + char *err = NULL; + int rc; + + if (storage == NULL || fn == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + fctx.fn = fn; + fctx.user_ctx = ctx; + + rc = sqlite3_exec(state->users_db, + "SELECT id,username,nickname,level,active,created_at," + "full_name,country,location,email,password,last_login_at " + "FROM users ORDER BY id;", + sql_foreach_cb, &fctx, &err); + if (rc == SQLITE_ABORT) { + sqlite3_free(err); + return HYBBX_ERR_BUSY; + } + if (rc != SQLITE_OK) { + hybbx_log_warn("[storage] foreach: %s", err != NULL ? err : "error"); + sqlite3_free(err); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_sql_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + + if (storage == NULL || count == NULL) { + return HYBBX_ERR_INVALID; + } + + *count = 0; + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_prepare_v2(state->users_db, + "SELECT COUNT(*) FROM users WHERE level=?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int(stmt, 1, (int)level); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + *count = (size_t)sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + return HYBBX_OK; +} + +typedef struct identity_taken_ctx { + const char *username; + const char *nickname; + int taken; +} identity_taken_ctx_t; + +static hybbx_result_t identity_taken_cb(const hybbx_user_record_t *user, void *ctx) +{ + identity_taken_ctx_t *ictx = (identity_taken_ctx_t *)ctx; + + if (sql_str_ieq(user->username, ictx->username) || + sql_str_ieq(user->username, ictx->nickname) || + (user->nickname[0] != '\0' && + (sql_str_ieq(user->nickname, ictx->nickname) || + sql_str_ieq(user->nickname, ictx->username)))) { + ictx->taken = 1; + return HYBBX_ERR_BUSY; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_sql_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out) +{ + struct sql_state *state; + hybbx_user_record_t existing; + identity_taken_ctx_t taken; + uint64_t user_id; + sqlite3_stmt *stmt; + int rc; + hybbx_result_t hres; + + if (storage == NULL || reg == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_registration_valid(reg, storage->guest_prefix)) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + hres = hybbx_storage_sql_find_user(storage, reg->username, &existing); + if (hres == HYBBX_OK) { + return HYBBX_ERR_BUSY; + } + if (hres != HYBBX_ERR_NOT_FOUND) { + return hres; + } + + memset(&taken, 0, sizeof(taken)); + taken.username = reg->username; + taken.nickname = reg->nickname; + hres = hybbx_storage_sql_foreach_user(storage, identity_taken_cb, &taken); + if (hres == HYBBX_ERR_BUSY || taken.taken) { + return HYBBX_ERR_BUSY; + } + if (hres != HYBBX_OK) { + return hres; + } + + hres = sql_meta_bump(state->users_db, "user_next", &user_id); + if (hres != HYBBX_OK) { + return hres; + } + + memset(out, 0, sizeof(*out)); + out->id = user_id; + hybbx_strlcpy(out->username, reg->username, sizeof(out->username)); + hybbx_username_normalize(out->username); + hybbx_strlcpy(out->nickname, reg->nickname, sizeof(out->nickname)); + out->level = HYBBX_LEVEL_USER; + out->active = 0; + out->created_at = time(NULL); + hybbx_strlcpy(out->full_name, reg->full_name, sizeof(out->full_name)); + hybbx_strlcpy(out->country, reg->country, sizeof(out->country)); + hybbx_strlcpy(out->location, reg->location, sizeof(out->location)); + hybbx_strlcpy(out->email, reg->email, sizeof(out->email)); + + if (reg->password[0] != '\0') { + if (!hybbx_password_plain_valid(reg->password)) { + return HYBBX_ERR_INVALID; + } + hres = hybbx_password_hash(reg->password, out->password, + sizeof(out->password)); + if (hres != HYBBX_OK) { + return hres; + } + } + + rc = sqlite3_prepare_v2(state->users_db, + "INSERT INTO users(id,username,nickname,level,active," + "created_at,full_name,country,location,email,password," + "last_login_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,0);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)out->id); + sqlite3_bind_text(stmt, 2, out->username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, out->nickname, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 4, (int)out->level); + sqlite3_bind_int(stmt, 5, out->active); + sqlite3_bind_int64(stmt, 6, (sqlite3_int64)out->created_at); + sqlite3_bind_text(stmt, 7, out->full_name, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 8, out->country, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 9, out->location, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 10, out->email, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 11, out->password, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_storage_sql_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + + if (storage == NULL || user == NULL || user->id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_prepare_v2(state->users_db, + "UPDATE users SET username=?2,nickname=?3,level=?4," + "active=?5,created_at=?6,full_name=?7,country=?8," + "location=?9,email=?10,password=?11,last_login_at=?12 " + "WHERE id=?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)user->id); + sqlite3_bind_text(stmt, 2, user->username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, user->nickname, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 4, (int)user->level); + sqlite3_bind_int(stmt, 5, user->active); + sqlite3_bind_int64(stmt, 6, (sqlite3_int64)user->created_at); + sqlite3_bind_text(stmt, 7, user->full_name, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 8, user->country, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 9, user->location, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 10, user->email, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 11, user->password, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 12, (sqlite3_int64)user->last_login_at); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + if (sqlite3_changes(state->users_db) == 0) { + return HYBBX_ERR_NOT_FOUND; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_sql_delete_user(hybbx_storage_t *storage, + uint64_t user_id) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + + if (storage == NULL || user_id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_prepare_v2(state->users_db, + "DELETE FROM users WHERE id=?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)user_id); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + if (sqlite3_changes(state->users_db) == 0) { + return HYBBX_ERR_NOT_FOUND; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_sql_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + hybbx_result_t hres; + + if (storage == NULL || user == NULL || transport == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + hres = sql_meta_bump(state->users_db, "session_next", &out->session_id); + if (hres != HYBBX_OK) { + return hres; + } + + out->user_id = user->id; + hybbx_strlcpy(out->username, user->username, sizeof(out->username)); + hybbx_strlcpy(out->transport, transport, sizeof(out->transport)); + out->connected_at = time(NULL); + out->active = 1; + + rc = sqlite3_prepare_v2(state->users_db, + "INSERT INTO sessions(session_id,user_id,username," + "transport,remote,connected_at,disconnected_at,active) " + "VALUES(?1,?2,?3,?4,'',?5,0,1);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)out->session_id); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)out->user_id); + sqlite3_bind_text(stmt, 3, out->username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 4, out->transport, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 5, (sqlite3_int64)out->connected_at); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_storage_sql_session_end(hybbx_storage_t *storage, + uint64_t session_id) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + time_t now = time(NULL); + + if (storage == NULL || session_id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_prepare_v2(state->users_db, + "UPDATE sessions SET disconnected_at=?2, active=0 " + "WHERE session_id=?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)session_id); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)now); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + + if (sqlite3_changes(state->users_db) == 0) { + rc = sqlite3_prepare_v2(state->users_db, + "INSERT INTO sessions(session_id,user_id,username," + "transport,remote,connected_at,disconnected_at," + "active) VALUES(?1,0,'','','',0,?2,0);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)session_id); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)now); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + } + + return HYBBX_OK; +} + +void hybbx_storage_sql_backup_files(const hybbx_storage_t *storage) +{ + struct sql_state *state; + const char *backup_dir; + + if (storage == NULL || storage->backend != HYBBX_STORAGE_SQLITE) { + return; + } + + state = storage->backend_data; + if (state == NULL) { + return; + } + + backup_dir = storage->sql_cfg.backup_path; + if (backup_dir != NULL && backup_dir[0] == '\0') { + backup_dir = NULL; + } + + sql_backup_one(state->users_db, storage->sql_cfg.user_db, backup_dir); + sql_backup_one(state->mail_db, storage->sql_cfg.mail_db, backup_dir); +} + +void hybbx_storage_sql_backup_tick(hybbx_storage_t *storage) +{ + struct sql_state *state; + + if (storage == NULL || storage->backend != HYBBX_STORAGE_SQLITE) { + return; + } + + state = storage->backend_data; + if (state == NULL || storage->sql_cfg.backup_interval_sec == 0) { + return; + } + + state->backup_tick++; + if (state->backup_tick >= storage->sql_cfg.backup_interval_sec) { + state->backup_tick = 0; + hybbx_storage_sql_backup_files(storage); + } +} + +#endif /* HYBBX_HAVE_SQLITE */ diff --git a/src/core/terminal.c b/src/core/terminal.c new file mode 100644 index 0000000..b4929f4 --- /dev/null +++ b/src/core/terminal.c @@ -0,0 +1,98 @@ +#include "hybbx/terminal.h" +#include "hybbx/session.h" +#include "hybbx/traffic.h" +#include "hybbx/limits.h" + +#include <string.h> + +static int term_is_csi_terminator(unsigned char ch) +{ + return ch >= 0x40 && ch <= 0x7E; +} + +size_t hybbx_term_copy_plain(const char *src, char *dst, size_t dst_size) +{ + size_t di = 0; + size_t si = 0; + + if (dst == NULL || dst_size == 0) { + return 0; + } + + dst[0] = '\0'; + + if (src == NULL) { + return 0; + } + + while (src[si] != '\0' && di + 1 < dst_size) { + unsigned char ch = (unsigned char)src[si]; + + if (ch == 0x1Bu) { + si++; + if (src[si] == '\0') { + break; + } + + if (src[si] == '[') { + si++; + while (src[si] != '\0' && !term_is_csi_terminator((unsigned char)src[si])) { + si++; + } + if (src[si] != '\0') { + si++; + } + } else { + si++; + } + continue; + } + + dst[di++] = (char)ch; + si++; + } + + dst[di] = '\0'; + return di; +} + +hybbx_result_t hybbx_term_init_session(hybbx_session_t *session) +{ + const hybbx_traffic_config_t *traffic; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + traffic = hybbx_traffic_config_get(); + if (traffic == NULL || !traffic->ansi) { + return HYBBX_OK; + } + + return hybbx_session_write(session, HYBBX_TERM_SGR_LIGHTGRAY_ON_BLACK); +} + +hybbx_result_t hybbx_term_clear_screen(hybbx_session_t *session) +{ + const hybbx_traffic_config_t *traffic; + int i; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + traffic = hybbx_traffic_config_get(); + if (traffic != NULL && traffic->ansi) { + return hybbx_session_write(session, HYBBX_TERM_CLEAR_SCREEN); + } + + for (i = 0; i < 24; i++) { + hybbx_result_t rc = hybbx_session_write(session, "\n"); + + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} diff --git a/src/core/texts.c b/src/core/texts.c new file mode 100644 index 0000000..f983744 --- /dev/null +++ b/src/core/texts.c @@ -0,0 +1,470 @@ +#include "hybbx/texts.h" +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/auth.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/hybbx.h" + +#include <stdio.h> +#include <string.h> +#include <time.h> + +void hybbx_texts_config_defaults(hybbx_texts_config_t *texts) +{ + if (texts == NULL) { + return; + } + + hybbx_strlcpy(texts->path, HYBBX_DEFAULT_TEXTS_PATH, sizeof(texts->path)); +} + +hybbx_result_t hybbx_texts_resolve(const hybbx_texts_config_t *texts, + const char *filename, + char *out, size_t out_len) +{ + if (texts == NULL || filename == NULL || out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, texts->path, filename); +} + +static size_t append_token_expanded(char *out, size_t out_len, size_t pos, + const char *value) +{ + size_t value_len; + + if (out == NULL || out_len == 0 || value == NULL) { + return pos; + } + + value_len = strlen(value); + + if (pos + value_len >= out_len) { + return out_len - 1; + } + + memcpy(out + pos, value, value_len); + return pos + value_len; +} + +static void expand_text_line(char *out, size_t out_len, + const char *line, + const char *version, + const char *service_name, + const char *username, + const char *os_name, + const char *time_text, + const char *date_text) +{ + size_t pos = 0; + size_t i = 0; + + if (out == NULL || out_len == 0 || line == NULL) { + return; + } + + out[0] = '\0'; + + if (version == NULL) { + version = HYBBX_VERSION_STRING; + } + if (service_name == NULL || service_name[0] == '\0') { + service_name = HYBBX_DEFAULT_SERVICE_NAME; + } + if (username == NULL) { + username = ""; + } + if (os_name == NULL) { + os_name = ""; + } + if (time_text == NULL) { + time_text = ""; + } + if (date_text == NULL) { + date_text = ""; + } + + while (line[i] != '\0' && pos + 1 < out_len) { + if (strncmp(line + i, HYBBX_BANNER_TOKEN_VERSION, + strlen(HYBBX_BANNER_TOKEN_VERSION)) == 0) { + pos = append_token_expanded(out, out_len, pos, version); + i += strlen(HYBBX_BANNER_TOKEN_VERSION); + continue; + } + + if (strncmp(line + i, HYBBX_BANNER_TOKEN_SERVICE, + strlen(HYBBX_BANNER_TOKEN_SERVICE)) == 0) { + pos = append_token_expanded(out, out_len, pos, service_name); + i += strlen(HYBBX_BANNER_TOKEN_SERVICE); + continue; + } + + if (strncmp(line + i, HYBBX_TEXT_TOKEN_USERNAME, + strlen(HYBBX_TEXT_TOKEN_USERNAME)) == 0) { + pos = append_token_expanded(out, out_len, pos, username); + i += strlen(HYBBX_TEXT_TOKEN_USERNAME); + continue; + } + + if (strncmp(line + i, HYBBX_TEXT_TOKEN_OS, + strlen(HYBBX_TEXT_TOKEN_OS)) == 0) { + pos = append_token_expanded(out, out_len, pos, os_name); + i += strlen(HYBBX_TEXT_TOKEN_OS); + continue; + } + + if (strncmp(line + i, HYBBX_TEXT_TOKEN_TIME, + strlen(HYBBX_TEXT_TOKEN_TIME)) == 0) { + pos = append_token_expanded(out, out_len, pos, time_text); + i += strlen(HYBBX_TEXT_TOKEN_TIME); + continue; + } + + if (strncmp(line + i, HYBBX_TEXT_TOKEN_DATE, + strlen(HYBBX_TEXT_TOKEN_DATE)) == 0) { + pos = append_token_expanded(out, out_len, pos, date_text); + i += strlen(HYBBX_TEXT_TOKEN_DATE); + continue; + } + + out[pos++] = line[i++]; + } + + out[pos] = '\0'; +} + +static void format_text_tokens(char *time_text, size_t time_len, + char *date_text, size_t date_len, + const struct tm *tm) +{ + if (time_text != NULL && time_len > 0) { + time_text[0] = '\0'; + if (tm != NULL) { + (void)hybbx_time_format_time(time_text, time_len, tm, NULL); + } + } + + if (date_text != NULL && date_len > 0) { + date_text[0] = '\0'; + if (tm != NULL) { + (void)hybbx_time_format_date(date_text, date_len, tm, NULL); + } + } +} + +static void fill_os_name(char *os_name, size_t os_len) +{ + if (os_name == NULL || os_len == 0) { + return; + } + + if (hybbx_platform_os_name(os_name, os_len) != HYBBX_OK) { + hybbx_strlcpy(os_name, "unknown", os_len); + } +} + +static void expand_banner_line(char *out, size_t out_len, + const char *line, + const char *version, + const char *service_name, + const char *time_text, + const char *date_text) +{ + expand_text_line(out, out_len, line, version, service_name, NULL, NULL, + time_text, date_text); +} + +typedef struct texts_line_emitter { + hybbx_session_t *session; + unsigned pending_blank; +} texts_line_emitter_t; + +static void texts_emit_expanded_line(texts_line_emitter_t *emitter, + const char *expanded) +{ + unsigned i; + + if (emitter == NULL || emitter->session == NULL || expanded == NULL) { + return; + } + + if (expanded[0] == '\0') { + emitter->pending_blank++; + return; + } + + for (i = 0; i < emitter->pending_blank; i++) { + hybbx_session_write(emitter->session, "\n"); + } + emitter->pending_blank = 0; + hybbx_session_write_line(emitter->session, expanded); +} + +static void texts_trim_trailing_cr(char *expanded) +{ + size_t n; + + if (expanded == NULL) { + return; + } + + n = strlen(expanded); + while (n > 0 && (expanded[n - 1] == '\n' || expanded[n - 1] == '\r')) { + expanded[--n] = '\0'; + } +} + +static hybbx_result_t send_banner_fallback(hybbx_session_t *session, + const char *version, + const char *service_name) +{ + char line[HYBBX_LINE_MAX]; + + if (version == NULL) { + version = HYBBX_VERSION_STRING; + } + if (service_name == NULL || service_name[0] == '\0') { + service_name = HYBBX_DEFAULT_SERVICE_NAME; + } + + snprintf(line, sizeof(line), "HyBBX %s", version); + hybbx_session_write_line(session, line); + snprintf(line, sizeof(line), "Online at %s", service_name); + hybbx_session_write_line(session, line); + return HYBBX_OK; +} + +hybbx_result_t hybbx_texts_send_banner(const hybbx_texts_config_t *texts, + hybbx_session_t *session, + const char *version, + const char *service_name) +{ + char path[HYBBX_PATH_MAX]; + char expanded[HYBBX_LINE_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + char time_text[32]; + char date_text[32]; + struct tm now_tm; + const struct tm *tm_ptr = NULL; + hybbx_result_t rc; + + if (texts == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_time_local_now(&now_tm) == HYBBX_OK) { + tm_ptr = &now_tm; + } + format_text_tokens(time_text, sizeof(time_text), + date_text, sizeof(date_text), tm_ptr); + + rc = hybbx_texts_resolve(texts, HYBBX_TEXT_BANNER, path, sizeof(path)); + if (rc != HYBBX_OK) { + return send_banner_fallback(session, version, service_name); + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return send_banner_fallback(session, version, service_name); + } + + { + texts_line_emitter_t emitter; + + emitter.session = session; + emitter.pending_blank = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + expand_banner_line(expanded, sizeof(expanded), line, version, + service_name, time_text, date_text); + texts_trim_trailing_cr(expanded); + texts_emit_expanded_line(&emitter, expanded); + } + } + + fclose(fp); + return HYBBX_OK; +} + +hybbx_result_t hybbx_texts_send_motd(const hybbx_texts_config_t *texts, + hybbx_session_t *session) +{ + char path[HYBBX_PATH_MAX]; + char expanded[HYBBX_LINE_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + char time_text[32]; + char date_text[32]; + struct tm now_tm; + const struct tm *tm_ptr = NULL; + const char *username; + hybbx_result_t rc; + + if (texts == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + username = hybbx_session_display_name(session); + if (username[0] == '\0') { + username = "visitor"; + } + + if (hybbx_time_local_now(&now_tm) == HYBBX_OK) { + tm_ptr = &now_tm; + } + format_text_tokens(time_text, sizeof(time_text), + date_text, sizeof(date_text), tm_ptr); + + rc = hybbx_texts_resolve(texts, HYBBX_TEXT_MOTD, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + { + texts_line_emitter_t emitter; + + emitter.session = session; + emitter.pending_blank = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + expand_text_line(expanded, sizeof(expanded), line, NULL, NULL, + username, NULL, time_text, date_text); + texts_trim_trailing_cr(expanded); + texts_emit_expanded_line(&emitter, expanded); + } + } + + fclose(fp); + return HYBBX_OK; +} + +hybbx_result_t hybbx_texts_send_file(const hybbx_texts_config_t *texts, + hybbx_session_t *session, + const char *filename) +{ + char path[HYBBX_PATH_MAX]; + char expanded[HYBBX_LINE_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + char time_text[32]; + char date_text[32]; + char os_name[64]; + struct tm now_tm; + const struct tm *tm_ptr = NULL; + hybbx_result_t rc; + + if (texts == NULL || session == NULL || filename == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_time_local_now(&now_tm) == HYBBX_OK) { + tm_ptr = &now_tm; + } + format_text_tokens(time_text, sizeof(time_text), + date_text, sizeof(date_text), tm_ptr); + fill_os_name(os_name, sizeof(os_name)); + + rc = hybbx_texts_resolve(texts, filename, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + { + texts_line_emitter_t emitter; + + emitter.session = session; + emitter.pending_blank = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + expand_text_line(expanded, sizeof(expanded), line, + HYBBX_VERSION_STRING, NULL, NULL, os_name, + time_text, date_text); + texts_trim_trailing_cr(expanded); + texts_emit_expanded_line(&emitter, expanded); + } + } + + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t send_version_fallback(hybbx_session_t *session, + const char *os_name) +{ + char line[HYBBX_LINE_MAX]; + + snprintf(line, sizeof(line), "HyBBX %s", HYBBX_VERSION_STRING); + hybbx_session_write_line(session, line); + snprintf(line, sizeof(line), "Operating system: %s", + (os_name != NULL && os_name[0] != '\0') ? os_name : "unknown"); + hybbx_session_write_line(session, line); + return HYBBX_OK; +} + +hybbx_result_t hybbx_texts_send_version(const hybbx_texts_config_t *texts, + hybbx_session_t *session) +{ + char path[HYBBX_PATH_MAX]; + char expanded[HYBBX_LINE_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + char time_text[32]; + char date_text[32]; + char os_name[64]; + struct tm now_tm; + const struct tm *tm_ptr = NULL; + hybbx_result_t rc; + + if (texts == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + fill_os_name(os_name, sizeof(os_name)); + + if (hybbx_time_local_now(&now_tm) == HYBBX_OK) { + tm_ptr = &now_tm; + } + format_text_tokens(time_text, sizeof(time_text), + date_text, sizeof(date_text), tm_ptr); + + rc = hybbx_texts_resolve(texts, HYBBX_TEXT_VERSION, path, sizeof(path)); + if (rc != HYBBX_OK) { + return send_version_fallback(session, os_name); + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return send_version_fallback(session, os_name); + } + + { + texts_line_emitter_t emitter; + + emitter.session = session; + emitter.pending_blank = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + expand_text_line(expanded, sizeof(expanded), line, + HYBBX_VERSION_STRING, NULL, NULL, os_name, + time_text, date_text); + texts_trim_trailing_cr(expanded); + texts_emit_expanded_line(&emitter, expanded); + } + } + + fclose(fp); + return HYBBX_OK; +} diff --git a/src/core/traffic.c b/src/core/traffic.c new file mode 100644 index 0000000..b74d48f --- /dev/null +++ b/src/core/traffic.c @@ -0,0 +1,294 @@ +#include "hybbx/traffic.h" +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/config.h" +#endif +#include "hybbx/session.h" +#include "hybbx/terminal.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <string.h> + +#if defined(_WIN32) +#include <windows.h> +#else +#include <sys/select.h> +#include <sys/time.h> +#endif + +static hybbx_traffic_config_t g_traffic_config; +static int g_traffic_config_ready; + +void hybbx_traffic_config_defaults(hybbx_traffic_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->baud = HYBBX_BAUD2400; + cfg->line_width = HYBBX_LINE_WIDTH; + cfg->pace_output = 1; + cfg->ansi = 0; + cfg->input_echo = 0; +} + +#if !defined(HYBBX_CLIENT_BUILD) +static unsigned parse_uint_default(const hybbx_config_t *config, + const char *section, + const char *key, + unsigned default_value, + unsigned min_value, + unsigned max_value) +{ + unsigned value; + + value = hybbx_config_get_uint(config, section, key, default_value, + min_value, max_value); + return value; +} +#endif + +void hybbx_traffic_config_apply(const struct hybbx_config *config) +{ + hybbx_traffic_config_defaults(&g_traffic_config); + +#if !defined(HYBBX_CLIENT_BUILD) + if (config != NULL) { + g_traffic_config.baud = parse_uint_default( + config, "traffic", "baud", HYBBX_BAUD2400, 300u, 38400u); + g_traffic_config.line_width = parse_uint_default( + config, "traffic", "line_width", HYBBX_LINE_WIDTH, 20u, + HYBBX_LINE_WIDTH_MAX); + g_traffic_config.pace_output = + hybbx_config_get_bool(config, "traffic", "pace_output", 1); + g_traffic_config.ansi = + hybbx_config_get_bool(config, "traffic", "ansi", 0); + g_traffic_config.input_echo = + hybbx_config_get_bool(config, "traffic", "input_echo", 0); + } + + hybbx_log_info("[traffic] baud=%u line_width=%u pace=%s ansi=%s echo=%s", + g_traffic_config.baud, g_traffic_config.line_width, + hybbx_bool_to_string(g_traffic_config.pace_output), + hybbx_bool_to_string(g_traffic_config.ansi), + hybbx_bool_to_string(g_traffic_config.input_echo)); +#else + (void)config; +#endif + + g_traffic_config_ready = 1; +} + +const hybbx_traffic_config_t *hybbx_traffic_config_get(void) +{ + if (!g_traffic_config_ready) { + hybbx_traffic_config_defaults(&g_traffic_config); + g_traffic_config_ready = 1; + } + + return &g_traffic_config; +} + +unsigned hybbx_traffic_byte_delay_us(unsigned baud) +{ + if (baud == 0) { + return 0; + } + + return 10000000u / baud; +} + +static hybbx_result_t emit_raw(struct hybbx_session *session, char ch) +{ + const hybbx_traffic_config_t *cfg = hybbx_traffic_config_get(); + char byte = ch; + hybbx_result_t rc; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + if (session->transport != NULL && session->transport->write != NULL) { + rc = session->transport->write(session, &byte, 1); + } else { + if (fputc((unsigned char)byte, stdout) == EOF) { + return HYBBX_ERR_IO; + } + fflush(stdout); + rc = HYBBX_OK; + } + + if (rc == HYBBX_OK && cfg->pace_output && cfg->baud > 0) { + unsigned delay = hybbx_traffic_byte_delay_us(cfg->baud); + + if (delay > 0) { +#if defined(_WIN32) + Sleep((DWORD)((delay + 999u) / 1000u)); +#else + struct timeval tv; + + tv.tv_sec = (time_t)(delay / 1000000u); +#if defined(__AMIGA__) + tv.tv_usec = (unsigned long)(delay % 1000000u); +#else + tv.tv_usec = (suseconds_t)(delay % 1000000u); +#endif + (void)select(0, NULL, NULL, NULL, &tv); +#endif + } + } + + return rc; +} + +static hybbx_result_t emit_newline(struct hybbx_session *session, + unsigned *out_col) +{ + hybbx_result_t rc; + + rc = emit_raw(session, '\r'); + if (rc != HYBBX_OK) { + return rc; + } + + rc = emit_raw(session, '\n'); + if (rc == HYBBX_OK && out_col != NULL) { + *out_col = 0; + } + + return rc; +} + +static int traffic_plain_char(const char *src, size_t len, size_t *consumed, + char *out) +{ + unsigned char ch; + + if (src == NULL || len == 0 || consumed == NULL || out == NULL) { + return 0; + } + + ch = (unsigned char)src[0]; + + if (ch == '\r') { + *consumed = 1; + *out = '\n'; + return 1; + } + + if (ch == '\n') { + *consumed = 1; + *out = '\n'; + return 1; + } + + if (ch == '\t') { + *consumed = 1; + *out = ' '; + return 1; + } + + if (ch < 0x20 || ch == 0x7f) { + *consumed = 1; + return 0; + } + + if (ch == 0x1b && len >= 2 && src[1] == '[') { + size_t i = 2; + + while (i < len && (src[i] < 0x40 || src[i] > 0x7e)) { + i++; + } + if (i < len) { + *consumed = i + 1; + } else { + *consumed = len; + } + return 0; + } + + if (ch == 0x1b) { + *consumed = 1; + return 0; + } + + *consumed = 1; + *out = (char)ch; + return 1; +} + +hybbx_result_t hybbx_traffic_emit(struct hybbx_session *session, + unsigned *out_col, + const char *data, size_t len) +{ + const hybbx_traffic_config_t *cfg = hybbx_traffic_config_get(); + size_t pos = 0; + unsigned col = out_col != NULL ? *out_col : 0; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + if (len == 0) { + return HYBBX_OK; + } + + if (cfg->ansi && strcmp(data, HYBBX_TERM_SGR_LIGHTGRAY_ON_BLACK) == 0 && + len == strlen(HYBBX_TERM_SGR_LIGHTGRAY_ON_BLACK)) { + size_t i; + + for (i = 0; i < len; i++) { + hybbx_result_t rc = emit_raw(session, data[i]); + + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; + } + + while (pos < len) { + char ch; + size_t step = 0; + hybbx_result_t rc; + + if (!traffic_plain_char(data + pos, len - pos, &step, &ch)) { + pos += step > 0 ? step : 1; + continue; + } + + pos += step; + + if (ch == '\n') { + rc = emit_newline(session, out_col); + if (rc != HYBBX_OK) { + return rc; + } + col = 0; + continue; + } + + if (cfg->line_width > 0 && col >= cfg->line_width) { + rc = emit_newline(session, out_col); + if (rc != HYBBX_OK) { + return rc; + } + col = 0; + } + + rc = emit_raw(session, ch); + if (rc != HYBBX_OK) { + return rc; + } + + col++; + } + + if (out_col != NULL) { + *out_col = col; + } + + return HYBBX_OK; +} diff --git a/src/core/util.c b/src/core/util.c new file mode 100644 index 0000000..1c5e9e4 --- /dev/null +++ b/src/core/util.c @@ -0,0 +1,832 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/socket.h" +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/log.h" +#endif + +#include <ctype.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <sys/socket.h> +#endif + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include <sys/utsname.h> +#endif + +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/config.h" +#endif + +static int bool_token_ieq(const char *value, const char *token) +{ + if (value == NULL || token == NULL) { + return 0; + } + + while (*value != '\0' && *token != '\0') { + unsigned char cv = (unsigned char)tolower((unsigned char)*value); + unsigned char ct = (unsigned char)tolower((unsigned char)*token); + + if (cv != ct) { + return 0; + } + value++; + token++; + } + + return *value == '\0' && *token == '\0'; +} + +static int bool_match_any(const char *value, const char *const *tokens, size_t count) +{ + size_t i; + + if (value == NULL || value[0] == '\0') { + return 0; + } + + for (i = 0; i < count; i++) { + if (bool_token_ieq(value, tokens[i])) { + return 1; + } + } + + return 0; +} + +static const char *const hybbx_bool_true_tokens[] = { + HYBBX_BOOL_YES, "true", "enable", "enabled", "on", "1" +}; + +static const char *const hybbx_bool_false_tokens[] = { + HYBBX_BOOL_NO, "false", "disable", "disabled", "off", "0" +}; + +int hybbx_bool_is_true(const char *value) +{ + return bool_match_any(value, hybbx_bool_true_tokens, + sizeof(hybbx_bool_true_tokens) / + sizeof(hybbx_bool_true_tokens[0])); +} + +int hybbx_bool_is_false(const char *value) +{ + return bool_match_any(value, hybbx_bool_false_tokens, + sizeof(hybbx_bool_false_tokens) / + sizeof(hybbx_bool_false_tokens[0])); +} + +int hybbx_parse_bool(const char *value, int default_value) +{ + if (value == NULL || value[0] == '\0') { + return default_value; + } + + if (hybbx_bool_is_true(value)) { + return 1; + } + + if (hybbx_bool_is_false(value)) { + return 0; + } + + return default_value; +} + +const char *hybbx_bool_to_string(int value) +{ + return value ? HYBBX_BOOL_YES : HYBBX_BOOL_NO; +} + +const char *hybbx_result_name(hybbx_result_t rc) +{ + switch (rc) { + case HYBBX_OK: + return "ok"; + case HYBBX_LOCAL_CMD: + return "local_cmd"; + case HYBBX_SESSION_END: + return "session_end"; + case HYBBX_ERR_INVALID: + return "invalid"; + case HYBBX_ERR_NOMEM: + return "nomem"; + case HYBBX_ERR_NOT_FOUND: + return "not_found"; + case HYBBX_ERR_IO: + return "io"; + case HYBBX_ERR_UNSUPPORTED: + return "unsupported"; + case HYBBX_ERR_BUSY: + return "busy"; + case HYBBX_ERR_DENIED: + return "denied"; + default: + return "unknown"; + } +} + +size_t hybbx_strlcpy(char *dst, const char *src, size_t dst_size) +{ + size_t src_len; + + if (dst_size == 0) { + return src != NULL ? strlen(src) : 0; + } + + if (dst == NULL) { + return 0; + } + + if (src == NULL) { + dst[0] = '\0'; + return 0; + } + + src_len = strlen(src); + if (src_len >= dst_size) { + memcpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; + return dst_size - 1; + } + + memcpy(dst, src, src_len + 1); + return src_len; +} + +hybbx_result_t hybbx_default_user_data_path(char *out, size_t out_len) +{ + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_strlcpy(out, HYBBX_DIR_DATA, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static char g_install_root[HYBBX_PATH_MAX]; + +void hybbx_install_root_set(const char *root) +{ + if (root == NULL || root[0] == '\0') { + g_install_root[0] = '\0'; + return; + } + + hybbx_strlcpy(g_install_root, root, sizeof(g_install_root)); +} + +const char *hybbx_install_root_get(void) +{ + return g_install_root[0] != '\0' ? g_install_root : NULL; +} + +static int path_is_absolute(const char *path) +{ + if (path == NULL || path[0] == '\0') { + return 0; + } + + if (path[0] == '/') { + return 1; + } + +#if defined(_WIN32) || defined(__CYGWIN__) + if (((path[0] >= 'A' && path[0] <= 'Z') || + (path[0] >= 'a' && path[0] <= 'z')) && + path[1] == ':') { + return 1; + } +#endif + + return 0; +} + +hybbx_result_t hybbx_path_resolve(char *out, size_t out_len, const char *path) +{ + char expanded[HYBBX_PATH_MAX]; + const char *root; + hybbx_result_t rc; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_path_expand(expanded, sizeof(expanded), path); + if (rc != HYBBX_OK) { + return rc; + } + + if (path_is_absolute(expanded)) { + if (hybbx_strlcpy(out, expanded, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + root = hybbx_install_root_get(); + if (root != NULL && root[0] != '\0') { + return hybbx_path_join(out, out_len, root, expanded); + } + + if (hybbx_strlcpy(out, expanded, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_path_dirname(const char *path, char *out, size_t out_len) +{ + char copy[HYBBX_PATH_MAX]; + char *slash; + + if (path == NULL || out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_strlcpy(copy, path, sizeof(copy)) >= sizeof(copy)) { + return HYBBX_ERR_INVALID; + } + + slash = strrchr(copy, '/'); + if (slash == NULL) { + if (hybbx_strlcpy(out, ".", out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (slash == copy) { + if (hybbx_strlcpy(out, "/", out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + *slash = '\0'; + if (hybbx_strlcpy(out, copy, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_platform_os_name(char *out, size_t out_len) +{ + const char *name; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + +#if defined(_WIN32) + name = "Windows"; +#elif defined(__AMIGA__) + name = "AmigaOS"; +#else + { + struct utsname u; + + if (uname(&u) != 0) { + return HYBBX_ERR_IO; + } + + name = u.sysname; + if (name == NULL || name[0] == '\0') { + return HYBBX_ERR_IO; + } + + if (strcmp(name, "Darwin") == 0) { + name = "MacOS"; + } + } +#endif + + if (hybbx_strlcpy(out, name, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_path_expand(char *out, size_t out_len, const char *path) +{ + const char *home; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (path == NULL || path[0] == '\0') { + return hybbx_default_user_data_path(out, out_len); + } + + if (path[0] != '~') { + if (hybbx_strlcpy(out, path, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (path[1] != '\0' && path[1] != '/') { + return HYBBX_ERR_INVALID; + } + + home = getenv("HOME"); + if (home == NULL || home[0] == '\0') { + if (path[1] == '\0') { + return hybbx_default_user_data_path(out, out_len); + } + if (hybbx_strlcpy(out, path + 2, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (path[1] == '\0') { + if (hybbx_strlcpy(out, home, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (snprintf(out, out_len, "%s%s", home, path + 1) >= (int)out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +int hybbx_size_ok(size_t len) +{ + return len > 0 && len <= HYBBX_ALLOC_MAX; +} + +static int path_component_valid(const char *name) +{ + if (name == NULL || name[0] == '\0') { + return 0; + } + + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { + return 0; + } + + if (strchr(name, '/') != NULL || strchr(name, '\\') != NULL) { + return 0; + } + + return 1; +} + +hybbx_result_t hybbx_path_join(char *out, size_t out_len, + const char *base, const char *name) +{ + size_t base_len; + int need_slash; + int written; + + if (out == NULL || out_len == 0 || base == NULL || name == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!path_component_valid(name)) { + return HYBBX_ERR_INVALID; + } + + base_len = strlen(base); + if (!hybbx_size_ok(base_len + 1) || !hybbx_size_ok(strlen(name) + 1)) { + return HYBBX_ERR_INVALID; + } + + need_slash = base_len > 0 && base[base_len - 1] != '/'; + written = snprintf(out, out_len, "%s%s%s", base, need_slash ? "/" : "", name); + if (written < 0 || (size_t)written >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static hybbx_time_format_t g_time_format; +static int g_time_format_ready; + +void hybbx_time_format_defaults(hybbx_time_format_t *fmt) +{ + if (fmt == NULL) { + return; + } + + fmt->clock_12h = 0; + fmt->seconds = 1; + fmt->date_format = HYBBX_DATE_ISO; +} + +const char *hybbx_date_format_name(hybbx_date_format_t fmt) +{ + switch (fmt) { + case HYBBX_DATE_ISO_SHORT: + return "iso_short"; + case HYBBX_DATE_US: + return "us"; + case HYBBX_DATE_EU: + return "eu"; + case HYBBX_DATE_ISO: + default: + return "iso"; + } +} + +#if !defined(HYBBX_CLIENT_BUILD) +static hybbx_date_format_t parse_date_format(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_DATE_ISO; + } + + if (bool_token_ieq(value, "iso") || bool_token_ieq(value, "yyyy_mm_dd") || + bool_token_ieq(value, "yyyy/mm/dd")) { + return HYBBX_DATE_ISO; + } + + if (bool_token_ieq(value, "iso_short") || bool_token_ieq(value, "yy_mm_dd") || + bool_token_ieq(value, "yy/mm/dd")) { + return HYBBX_DATE_ISO_SHORT; + } + + if (bool_token_ieq(value, "us") || bool_token_ieq(value, "mm_dd_yyyy") || + bool_token_ieq(value, "mm/dd/yyyy")) { + return HYBBX_DATE_US; + } + + if (bool_token_ieq(value, "eu") || bool_token_ieq(value, "dd_mm_yyyy") || + bool_token_ieq(value, "dd/mm/yyyy")) { + return HYBBX_DATE_EU; + } + + return HYBBX_DATE_ISO; +} +#endif + +hybbx_result_t hybbx_time_local_now(struct tm *out) +{ + time_t now; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + now = time(NULL); + if (now == (time_t)-1) { + return HYBBX_ERR_IO; + } + +#if defined(_WIN32) + if (localtime_s(out, &now) != 0) { + return HYBBX_ERR_IO; + } +#else + if (localtime_r(&now, out) == NULL) { + return HYBBX_ERR_IO; + } +#endif + + return HYBBX_OK; +} + +const hybbx_time_format_t *hybbx_time_format_get(void) +{ + if (!g_time_format_ready) { + hybbx_time_format_defaults(&g_time_format); + g_time_format_ready = 1; + } + + return &g_time_format; +} + +#if !defined(HYBBX_CLIENT_BUILD) +void hybbx_time_config_apply(const struct hybbx_config *config) +{ + const char *clock; + int clock_12h; + + hybbx_time_format_defaults(&g_time_format); + + if (config != NULL) { + clock_12h = hybbx_config_get_bool(config, "time", "clock_12h", 0); + if (!clock_12h) { + clock_12h = hybbx_config_get_bool(config, "time", "am_pm", 0); + } + + clock = hybbx_config_get(config, "time", "clock", NULL); + if (clock != NULL && clock[0] != '\0') { + if (bool_token_ieq(clock, "12h") || bool_token_ieq(clock, "12") || + bool_token_ieq(clock, "am_pm") || bool_token_ieq(clock, "am/pm")) { + clock_12h = 1; + } else if (bool_token_ieq(clock, "24h") || bool_token_ieq(clock, "24")) { + clock_12h = 0; + } + } + + g_time_format.clock_12h = clock_12h; + g_time_format.seconds = + hybbx_config_get_bool(config, "time", "seconds", 1); + g_time_format.date_format = parse_date_format( + hybbx_config_get(config, "time", "date", NULL)); + } + + g_time_format_ready = 1; + + hybbx_log_info("[time] clock=%s seconds=%s date=%s", + g_time_format.clock_12h ? "12h" : "24h", + hybbx_bool_to_string(g_time_format.seconds), + hybbx_date_format_name(g_time_format.date_format)); +} +#else +void hybbx_time_config_apply(const struct hybbx_config *config) +{ + (void)config; + hybbx_time_format_defaults(&g_time_format); + g_time_format_ready = 1; +} +#endif + +hybbx_result_t hybbx_time_format_time(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt) +{ + int hour; + int minute; + int second; + const char *suffix; + int n; + + if (out == NULL || out_len == 0 || tm == NULL) { + return HYBBX_ERR_INVALID; + } + + if (fmt == NULL) { + fmt = hybbx_time_format_get(); + } + + hour = tm->tm_hour; + minute = tm->tm_min; + second = tm->tm_sec; + suffix = ""; + + if (fmt->clock_12h) { + if (hour >= 12) { + suffix = "pm"; + if (hour > 12) { + hour -= 12; + } + } else { + suffix = "am"; + if (hour == 0) { + hour = 12; + } + } + } + + if (fmt->clock_12h) { + if (fmt->seconds) { + n = snprintf(out, out_len, "%02d:%02d:%02d %s", + hour, minute, second, suffix); + } else { + n = snprintf(out, out_len, "%02d:%02d %s", hour, minute, suffix); + } + } else if (fmt->seconds) { + n = snprintf(out, out_len, "%02d:%02d:%02d", hour, minute, second); + } else { + n = snprintf(out, out_len, "%02d:%02d", hour, minute); + } + + if (n < 0 || (size_t)n >= out_len) { + out[0] = '\0'; + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_time_format_date(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt) +{ + int year; + int month; + int day; + int n; + + if (out == NULL || out_len == 0 || tm == NULL) { + return HYBBX_ERR_INVALID; + } + + if (fmt == NULL) { + fmt = hybbx_time_format_get(); + } + + year = tm->tm_year + 1900; + month = tm->tm_mon + 1; + day = tm->tm_mday; + + switch (fmt->date_format) { + case HYBBX_DATE_ISO_SHORT: + n = snprintf(out, out_len, "%02d/%02d/%02d", + year % 100, month, day); + break; + case HYBBX_DATE_US: + n = snprintf(out, out_len, "%02d/%02d/%04d", month, day, year); + break; + case HYBBX_DATE_EU: + n = snprintf(out, out_len, "%02d/%02d/%04d", day, month, year); + break; + case HYBBX_DATE_ISO: + default: + n = snprintf(out, out_len, "%04d/%02d/%02d", year, month, day); + break; + } + + if (n < 0 || (size_t)n >= out_len) { + out[0] = '\0'; + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_time_format_stamp(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt) +{ + int year; + int month; + int day; + int hour; + int minute; + int second; + const char *suffix; + int n; + + if (out == NULL || out_len == 0 || tm == NULL) { + return HYBBX_ERR_INVALID; + } + + if (fmt == NULL) { + fmt = hybbx_time_format_get(); + } + + year = tm->tm_year + 1900; + month = tm->tm_mon + 1; + day = tm->tm_mday; + hour = tm->tm_hour; + minute = tm->tm_min; + second = tm->tm_sec; + suffix = ""; + + if (fmt->clock_12h) { + if (hour >= 12) { + suffix = "pm"; + if (hour > 12) { + hour -= 12; + } + } else { + suffix = "am"; + if (hour == 0) { + hour = 12; + } + } + } + + if (fmt->clock_12h) { + if (fmt->seconds) { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d:%02d %s", + year, month, day, hour, minute, second, suffix); + } else { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d %s", + year, month, day, hour, minute, suffix); + } + } else if (fmt->seconds) { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d:%02d", + year, month, day, hour, minute, second); + } else { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d", + year, month, day, hour, minute); + } + + if (n < 0 || (size_t)n >= out_len) { + out[0] = '\0'; + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +void hybbx_socket_nosigpipe(int fd) +{ +#ifdef SO_NOSIGPIPE + int on = 1; + + if (fd >= 0) { + (void)setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on)); + } +#else + (void)fd; +#endif +} + +void hybbx_socket_log_bind_failure(const char *component, const char *addr, + unsigned port) +{ + int err = errno; + + if (component == NULL || addr == NULL) { + return; + } + +#if !defined(HYBBX_CLIENT_BUILD) + hybbx_log_warn("[%s] failed to bind %s:%u (%s)", component, addr, port, + strerror(err)); + if (err == EADDRINUSE) { + hybbx_log_warn("[%s] port %u already in use — another HyBBX instance may " + "already be running (ss -tlnp | grep %u)", + component, port, port); + } +#else + fprintf(stderr, "[%s] failed to bind %s:%u (%s)\n", component, addr, port, + strerror(err)); + if (err == EADDRINUSE) { + fprintf(stderr, + "[%s] port %u already in use — another HyBBX instance may " + "already be running (ss -tlnp | grep %u)\n", + component, port, port); + } +#endif +} + +hybbx_result_t hybbx_socket_peer_name(int fd, char *buf, size_t buf_len) +{ +#if defined(_WIN32) || defined(__AMIGA__) + (void)fd; + (void)buf; + (void)buf_len; + return HYBBX_ERR_UNSUPPORTED; +#else + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + const void *ip; + char ip_buf[INET6_ADDRSTRLEN]; + + if (buf == NULL || buf_len == 0) { + return HYBBX_ERR_INVALID; + } + + buf[0] = '\0'; + + if (fd < 0) { + return HYBBX_ERR_INVALID; + } + + if (getpeername(fd, (struct sockaddr *)&addr, &addr_len) != 0) { + return HYBBX_ERR_IO; + } + + if (addr.ss_family == AF_INET) { + const struct sockaddr_in *in4 = (const struct sockaddr_in *)&addr; + + ip = &in4->sin_addr; + } else if (addr.ss_family == AF_INET6) { + const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)&addr; + + ip = &in6->sin6_addr; + } else { + return HYBBX_ERR_UNSUPPORTED; + } + + if (inet_ntop(addr.ss_family, ip, ip_buf, sizeof(ip_buf)) == NULL) { + return HYBBX_ERR_IO; + } + + hybbx_strlcpy(buf, ip_buf, buf_len); + return HYBBX_OK; +#endif +} |
