From 20cb29c2f8c5c87bc590896854a20b1473ceb358 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Sat, 8 Aug 2026 03:54:55 +0000 Subject: #2 --- src/core/max25.c | 507 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 src/core/max25.c (limited to 'src/core/max25.c') 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 +#include +#include +#include + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include +#include +#include +#include +#include +#include +#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 +} -- cgit v1.3.1