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/clients/hybbx_terminal.c | 433 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 src/clients/hybbx_terminal.c (limited to 'src/clients/hybbx_terminal.c') diff --git a/src/clients/hybbx_terminal.c b/src/clients/hybbx_terminal.c new file mode 100644 index 0000000..b6e09e1 --- /dev/null +++ b/src/clients/hybbx_terminal.c @@ -0,0 +1,433 @@ +/** + * hybbx-terminal — HyBBX AX.25 / HBX circuit terminal client (CLI only). + * + * Connects to the internal HBX circuit hub, authenticates with link password, + * and exchanges TERMINAL and AX.25 UI frames for operator sessions. + */ + +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "client_line.h" +#include "client_display.h" +#include "hybbx/ax25.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/limits.h" +#include "hybbx/link.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" + +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_TERMINAL_POLL_MS 50 + +typedef struct hybbx_terminal_config { + char host[256]; + unsigned port; + char link_id[HYBBX_LINK_ID_MAX]; + char link_password[128]; + char link_role[HYBBX_LINK_ROLE_MAX]; + char mycall[32]; + char dest[32]; + char via[128]; + int ax25_ui; + int verbose; + int ipv6; +} hybbx_terminal_config_t; + +typedef struct terminal_session { + int fd; + const hybbx_terminal_config_t *cfg; + hybbx_circuit_decoder_t dec; + hybbx_client_display_t display; + hybbx_ax25_path_t path; + int path_ready; +} terminal_session_t; + +static void config_defaults(hybbx_terminal_config_t *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + hybbx_strlcpy(cfg->host, "127.0.0.1", sizeof(cfg->host)); + cfg->port = HYBBX_CIRCUIT_DEFAULT_PORT; + hybbx_strlcpy(cfg->link_id, "terminal-1", sizeof(cfg->link_id)); + hybbx_strlcpy(cfg->link_role, "gateway", sizeof(cfg->link_role)); +} + +static void print_usage(const char *prog) +{ + fprintf(stderr, + "hybbx-terminal — HyBBX AX.25 / HBX circuit terminal client (CLI only)\n" + "\n" + "Usage: %s [options] [host [port]]\n" + "\n" + "Options:\n" + " -H, --host HOST Circuit hub host (default 127.0.0.1)\n" + " -p, --port PORT Circuit hub port (default 7323)\n" + " --link-id ID Link identity for password auth\n" + " --link-password P Link password (HBX LINK_AUTH)\n" + " --link-role ROLE link/repeater edge role: gateway | repeater | link | digipeater\n" + " --mycall CALL AX.25 source (CALL or CALL-SSID)\n" + " --dest CALL AX.25 destination\n" + " --via LIST Comma-separated digipeater path\n" + " --ax25-ui Send lines as AX.25 UI frames (needs mycall/dest)\n" + " --baud N Pace output (default 2400, 0=off)\n" + " --line-width N Wrap display columns (default 80)\n" + " --pace yes|no Output pacing\n" + " --ansi yes|no Pass ANSI sequences\n" + " -6, --ipv6 Prefer IPv6 (reserved)\n" + " -v, --verbose Log HBX frames on stderr\n" + " -h, --help Show help\n" + "\n" + "Environment: HYBBX_CIRCUIT_HOST, HYBBX_CIRCUIT_PORT\n" + "\n", + prog); +} + +static int build_ax25_path(const hybbx_terminal_config_t *cfg, + hybbx_ax25_path_t *path) +{ + const char *cursor; + char token[32]; + + memset(path, 0, sizeof(*path)); + + if (cfg->dest[0] == '\0' || cfg->mycall[0] == '\0') { + return 0; + } + + if (hybbx_ax25_address_parse(cfg->dest, &path->dest) != HYBBX_OK) { + return 0; + } + if (hybbx_ax25_address_parse(cfg->mycall, &path->source) != HYBBX_OK) { + return 0; + } + + cursor = cfg->via; + while (cursor != NULL && *cursor != '\0' && path->digi_count < HYBBX_AX25_MAX_DIGI) { + const char *comma = strchr(cursor, ','); + size_t len; + + if (comma != NULL) { + len = (size_t)(comma - cursor); + } else { + len = strlen(cursor); + } + if (len >= sizeof(token)) { + len = sizeof(token) - 1; + } + memcpy(token, cursor, len); + token[len] = '\0'; + + if (hybbx_ax25_address_parse(token, &path->digi[path->digi_count]) == HYBBX_OK) { + path->digi_count++; + } + + cursor = comma != NULL ? comma + 1 : NULL; + } + + return 1; +} + +static void on_circuit_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + terminal_session_t *sess = (terminal_session_t *)userdata; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + size_t ui_len; + + (void)flags; + + if (sess == NULL || len == 0) { + return; + } + + if (sess->cfg->verbose) { + fprintf(stderr, "hybbx-terminal: rx proto=%s (%u bytes)\n", + hybbx_circuit_proto_name(proto), (unsigned)len); + } + + switch (proto) { + case HYBBX_CIRCUIT_PROTO_TERMINAL: + hybbx_client_display_write(&sess->display, payload, len); + break; + case HYBBX_CIRCUIT_PROTO_AX25_UI: + ui_len = hybbx_circuit_unpack_ax25_ui(payload, len, &path, ui, sizeof(ui)); + if (ui_len > 0) { + hybbx_client_display_write(&sess->display, ui, ui_len); + } + break; + case HYBBX_CIRCUIT_PROTO_AX25: + ui_len = hybbx_ax25_parse_ui(payload, len, &path, ui, sizeof(ui)); + if (ui_len > 0) { + hybbx_client_display_write(&sess->display, ui, ui_len); + } + break; + default: + break; + } +} + +static hybbx_result_t send_terminal_line(terminal_session_t *sess, + const char *line) +{ + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t len = strlen(line); + + frame_len = hybbx_circuit_encode_terminal(line, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(sess->fd, frame, frame_len); +} + +static hybbx_result_t send_ax25_ui_line(terminal_session_t *sess, + const char *line) +{ + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t len = strlen(line); + + if (!sess->path_ready) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_circuit_encode_ax25_ui(&sess->path, + (const uint8_t *)line, len, + HYBBX_CIRCUIT_FLAG_TX, + frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(sess->fd, frame, frame_len); +} + +static int run_session(terminal_session_t *sess) +{ + hybbx_client_line_editor_t editor; + char inbuf[HYBBX_LINE_MAX + 2]; + int running = 1; + int editor_ready = 0; + + if (hybbx_client_line_editor_start(&editor, STDIN_FILENO) == 0) { + editor_ready = 1; + } + + while (running) { + struct pollfd pfds[2]; + int pr; + uint8_t buf[512]; + + pfds[0].fd = STDIN_FILENO; + pfds[0].events = POLLIN; + pfds[0].revents = 0; + pfds[1].fd = sess->fd; + pfds[1].events = POLLIN; + pfds[1].revents = 0; + + pr = poll(pfds, 2, HYBBX_TERMINAL_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if ((pfds[0].revents & POLLIN) != 0) { + int have_line = 0; + int eof_seen = 0; + + if (!editor_ready || + hybbx_client_line_editor_poll(&editor, inbuf, sizeof(inbuf), + &have_line, &eof_seen) != HYBBX_OK) { + running = 0; + } else if (eof_seen) { + running = 0; + } else if (have_line) { + hybbx_result_t rc; + + if (sess->cfg->ax25_ui && sess->path_ready) { + rc = send_ax25_ui_line(sess, inbuf); + } else { + rc = send_terminal_line(sess, inbuf); + } + if (rc != HYBBX_OK) { + break; + } + } + } + + if ((pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + + if ((pfds[1].revents & POLLIN) != 0) { + size_t read_len; + + if (hybbx_circuit_link_read(sess->fd, buf, sizeof(buf), + &read_len) != HYBBX_OK) { + break; + } + if (read_len == 0) { + continue; + } + + hybbx_circuit_decoder_feed(&sess->dec, buf, read_len, + on_circuit_frame, sess); + } + } + + if (editor_ready) { + hybbx_client_line_editor_stop(&editor); + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + hybbx_terminal_config_t cfg; + terminal_session_t sess; + const char *env; + int opt; + int argi; + hybbx_traffic_config_t traffic; + + static const struct option long_opts[] = { + {"host", required_argument, NULL, 'H'}, + {"port", required_argument, NULL, 'p'}, + {"link-id", required_argument, NULL, 1000}, + {"link-password", required_argument, NULL, 1001}, + {"link-role", required_argument, NULL, 1002}, + {"mycall", required_argument, NULL, 1003}, + {"dest", required_argument, NULL, 1004}, + {"via", required_argument, NULL, 1005}, + {"ax25-ui", no_argument, NULL, 1006}, + {"baud", required_argument, NULL, 1007}, + {"line-width", required_argument, NULL, 1008}, + {"pace", required_argument, NULL, 1009}, + {"ansi", required_argument, NULL, 1010}, + {"ipv6", no_argument, NULL, '6'}, + {"verbose", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + config_defaults(&cfg); + hybbx_traffic_config_defaults(&traffic); + + env = getenv("HYBBX_CIRCUIT_HOST"); + if (env != NULL && env[0] != '\0') { + hybbx_strlcpy(cfg.host, env, sizeof(cfg.host)); + } + env = getenv("HYBBX_CIRCUIT_PORT"); + if (env != NULL && env[0] != '\0') { + cfg.port = (unsigned)strtoul(env, NULL, 10); + } + + while ((opt = getopt_long(argc, argv, "H:p:6vh", long_opts, NULL)) != -1) { + switch (opt) { + case 'H': + hybbx_strlcpy(cfg.host, optarg, sizeof(cfg.host)); + break; + case 'p': + cfg.port = (unsigned)strtoul(optarg, NULL, 10); + break; + case '6': + cfg.ipv6 = 1; + break; + case 'v': + cfg.verbose = 1; + break; + case 'h': + print_usage(argv[0]); + return EXIT_SUCCESS; + case 1000: + hybbx_strlcpy(cfg.link_id, optarg, sizeof(cfg.link_id)); + break; + case 1001: + hybbx_strlcpy(cfg.link_password, optarg, sizeof(cfg.link_password)); + break; + case 1002: + hybbx_strlcpy(cfg.link_role, optarg, sizeof(cfg.link_role)); + break; + case 1003: + hybbx_strlcpy(cfg.mycall, optarg, sizeof(cfg.mycall)); + break; + case 1004: + hybbx_strlcpy(cfg.dest, optarg, sizeof(cfg.dest)); + break; + case 1005: + hybbx_strlcpy(cfg.via, optarg, sizeof(cfg.via)); + break; + case 1006: + cfg.ax25_ui = 1; + break; + case 1007: + traffic.baud = (unsigned)strtoul(optarg, NULL, 10); + break; + case 1008: + traffic.line_width = (unsigned)strtoul(optarg, NULL, 10); + break; + case 1009: + traffic.pace_output = hybbx_parse_bool(optarg, traffic.pace_output); + break; + case 1010: + traffic.ansi = hybbx_parse_bool(optarg, traffic.ansi); + break; + default: + print_usage(argv[0]); + return EXIT_FAILURE; + } + } + + argi = optind; + if (argi < argc) { + hybbx_strlcpy(cfg.host, argv[argi++], sizeof(cfg.host)); + } + if (argi < argc) { + cfg.port = (unsigned)strtoul(argv[argi++], NULL, 10); + } + + memset(&sess, 0, sizeof(sess)); + sess.cfg = &cfg; + hybbx_circuit_decoder_init(&sess.dec); + hybbx_client_display_init(&sess.display, &traffic); + sess.path_ready = build_ax25_path(&cfg, &sess.path); + + if (hybbx_circuit_link_connect(cfg.host, cfg.port, &sess.fd) != HYBBX_OK) { + fprintf(stderr, "hybbx-terminal: connect %s:%u failed\n", + cfg.host, cfg.port); + return EXIT_FAILURE; + } + + if (cfg.link_password[0] != '\0') { + if (hybbx_circuit_link_authenticate(sess.fd, cfg.link_password, + cfg.link_role, cfg.link_id) != HYBBX_OK) { + fprintf(stderr, "hybbx-terminal: link authentication failed\n"); + close(sess.fd); + return EXIT_FAILURE; + } + } + + if (cfg.verbose) { + fprintf(stderr, "hybbx-terminal: connected to %s:%u (ax25-ui=%s)\n", + cfg.host, cfg.port, cfg.ax25_ui ? "yes" : "no"); + } + + run_session(&sess); + close(sess.fd); + return EXIT_SUCCESS; +} -- cgit v1.3.1