#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 }