summaryrefslogtreecommitdiff
path: root/src/clients/hybbx_telnet.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
commit20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch)
tree2857f41513a56ad41af97b57362639298aa7f033 /src/clients/hybbx_telnet.c
#2
Diffstat (limited to 'src/clients/hybbx_telnet.c')
-rw-r--r--src/clients/hybbx_telnet.c544
1 files changed, 544 insertions, 0 deletions
diff --git a/src/clients/hybbx_telnet.c b/src/clients/hybbx_telnet.c
new file mode 100644
index 0000000..c19e747
--- /dev/null
+++ b/src/clients/hybbx_telnet.c
@@ -0,0 +1,544 @@
+/**
+ * hybbx-telnet — HyBBX-native telnet client (CLI only, no GUI).
+ *
+ * Full telnet negotiation support for testing HyBBX servers.
+ * Optional 2400 baud pacing and 80-column display profile.
+ */
+
+#if defined(__linux__) || defined(__GLIBC__)
+#define _DEFAULT_SOURCE 1
+#endif
+
+#include "client_line.h"
+#include "client_display.h"
+#include "hybbx/limits.h"
+#include "hybbx/socket.h"
+#include "hybbx/traffic.h"
+#include "hybbx/util.h"
+#include "telnet_proto.h"
+
+#include <errno.h>
+#include <getopt.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#if defined(__AMIGA__)
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#else
+#include <netdb.h>
+#include <poll.h>
+#endif
+
+#define HYBBX_TELNET_DEFAULT_PORT 2323u
+#define HYBBX_TELNET_POLL_MS 50
+
+typedef struct hybbx_telnet_client_config {
+ char host[256];
+ unsigned port;
+ char login_user[64];
+ char login_password[128];
+ unsigned baud;
+ unsigned line_width;
+ int pace_output;
+ int ansi;
+ int verbose;
+ unsigned timeout_sec;
+ int ipv6;
+} hybbx_telnet_client_config_t;
+
+static void config_defaults(hybbx_telnet_client_config_t *cfg)
+{
+ memset(cfg, 0, sizeof(*cfg));
+ hybbx_strlcpy(cfg->host, "127.0.0.1", sizeof(cfg->host));
+ cfg->port = HYBBX_TELNET_DEFAULT_PORT;
+ cfg->baud = HYBBX_BAUD2400;
+ cfg->line_width = HYBBX_LINE_WIDTH;
+ cfg->pace_output = 1;
+ cfg->timeout_sec = 30;
+}
+
+static void print_usage(const char *prog)
+{
+ fprintf(stderr,
+ "hybbx-telnet — HyBBX telnet client for testing (CLI only)\n"
+ "\n"
+ "Usage: %s [options] [host [port]]\n"
+ "\n"
+ "Options:\n"
+ " -H, --host HOST Remote host (default 127.0.0.1)\n"
+ " -p, --port PORT Telnet port (default 2323)\n"
+ " -u, --user USER Send /login USER after connect\n"
+ " -P, --password PASS Password for /login\n"
+ " --baud N Pace output like HyBBX (default 2400, 0=off)\n"
+ " --line-width N Wrap display columns (default 80, 0=off)\n"
+ " --pace yes|no Output pacing (default yes)\n"
+ " --ansi yes|no Pass ANSI sequences (default no)\n"
+ " -6, --ipv6 Prefer IPv6 when resolving host\n"
+ " -t, --timeout SEC Connect timeout (default 30)\n"
+ " -v, --verbose Log telnet negotiation on stderr\n"
+ " -h, --help Show help\n"
+ "\n"
+ "Environment: HYBBX_HOST, HYBBX_PORT\n"
+ "\n",
+ prog);
+}
+
+static int tcp_connect(const hybbx_telnet_client_config_t *cfg, int *out_fd)
+{
+#if defined(__AMIGA__)
+ struct sockaddr_in addr;
+ struct hostent *he;
+ int fd;
+ int on = 1;
+
+ if (cfg->ipv6) {
+ fprintf(stderr, "hybbx-telnet: IPv6 is not supported on AmigaOS\n");
+ return -1;
+ }
+
+ fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0) {
+ fprintf(stderr, "hybbx-telnet: socket failed for %s:%u\n",
+ cfg->host, cfg->port);
+ return -1;
+ }
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons((uint16_t)cfg->port);
+
+ if (inet_aton(cfg->host, &addr.sin_addr) == 0) {
+ he = gethostbyname(cfg->host);
+ if (he == NULL || he->h_addrtype != AF_INET || he->h_length == 0) {
+ fprintf(stderr, "hybbx-telnet: unknown host %s\n", cfg->host);
+ close(fd);
+ return -1;
+ }
+ memcpy(&addr.sin_addr, he->h_addr, (size_t)he->h_length);
+ }
+
+ (void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
+ hybbx_socket_nosigpipe(fd);
+ if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
+ fprintf(stderr, "hybbx-telnet: could not connect to %s:%u\n",
+ cfg->host, cfg->port);
+ close(fd);
+ return -1;
+ }
+
+ *out_fd = fd;
+ return 0;
+#else
+ char portbuf[16];
+ struct addrinfo hints;
+ struct addrinfo *res = NULL;
+ struct addrinfo *ai;
+ int fd = -1;
+ int rc;
+
+ snprintf(portbuf, sizeof(portbuf), "%u", cfg->port);
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_family = cfg->ipv6 ? AF_INET6 : AF_UNSPEC;
+
+ rc = getaddrinfo(cfg->host, portbuf, &hints, &res);
+ if (rc != 0) {
+ fprintf(stderr, "hybbx-telnet: connect %s:%u failed: %s\n",
+ cfg->host, cfg->port, gai_strerror(rc));
+ return -1;
+ }
+
+ for (ai = res; ai != NULL; ai = ai->ai_next) {
+ int on = 1;
+
+ fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+ if (fd < 0) {
+ continue;
+ }
+
+ (void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
+ hybbx_socket_nosigpipe(fd);
+ if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) {
+ break;
+ }
+ close(fd);
+ fd = -1;
+ }
+
+ freeaddrinfo(res);
+
+ if (fd < 0) {
+ fprintf(stderr, "hybbx-telnet: could not connect to %s:%u\n",
+ cfg->host, cfg->port);
+ return -1;
+ }
+
+ *out_fd = fd;
+ return 0;
+#endif
+}
+
+typedef struct display_ctx {
+ hybbx_client_display_t display;
+} display_ctx_t;
+
+static void on_telnet_data(void *userdata, const uint8_t *data, size_t len)
+{
+ display_ctx_t *ctx = (display_ctx_t *)userdata;
+
+ hybbx_client_display_write(&ctx->display, data, len);
+}
+
+static hybbx_result_t send_raw(int fd, const uint8_t *data, size_t len)
+{
+ size_t off = 0;
+
+ while (off < len) {
+ ssize_t n = send(fd, data + off, len - off, MSG_NOSIGNAL);
+ if (n < 0) {
+ if (errno == EINTR) {
+ continue;
+ }
+ return HYBBX_ERR_IO;
+ }
+ if (n == 0) {
+ return HYBBX_ERR_IO;
+ }
+ off += (size_t)n;
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t send_line(int fd, const char *line)
+{
+ size_t len = strlen(line);
+ uint8_t stack[256];
+ uint8_t *buf = stack;
+ size_t cap = sizeof(stack);
+ size_t need = len;
+ size_t i;
+ size_t pos = 0;
+ hybbx_result_t rc;
+
+ for (i = 0; i < len; i++) {
+ if (line[i] == (char)255) {
+ need++;
+ }
+ }
+ need += 1;
+
+ if (need > cap) {
+ buf = malloc(need);
+ if (buf == NULL) {
+ return HYBBX_ERR_NOMEM;
+ }
+ cap = need;
+ }
+
+ for (i = 0; i < len; i++) {
+ buf[pos++] = (uint8_t)line[i];
+ if (line[i] == (char)255) {
+ buf[pos++] = 255;
+ }
+ }
+ buf[pos++] = (uint8_t)'\n';
+
+ rc = send_raw(fd, buf, pos);
+ if (buf != stack) {
+ free(buf);
+ }
+ return rc;
+}
+
+static void maybe_auto_login(int fd, const hybbx_telnet_client_config_t *cfg)
+{
+ char line[192];
+
+ if (cfg->login_user[0] == '\0') {
+ return;
+ }
+
+ snprintf(line, sizeof(line), "/login %s", cfg->login_user);
+ (void)send_line(fd, line);
+ if (cfg->login_password[0] != '\0') {
+ (void)send_line(fd, cfg->login_password);
+ }
+}
+
+static int run_session(int fd, const hybbx_telnet_client_config_t *cfg)
+{
+ hybbx_telnet_parser_t parser;
+ display_ctx_t display;
+ hybbx_client_line_editor_t editor;
+ hybbx_traffic_config_t traffic;
+ char inbuf[HYBBX_LINE_MAX + 2];
+ int running = 1;
+ int editor_ready = 0;
+
+ hybbx_traffic_config_defaults(&traffic);
+ traffic.baud = cfg->baud;
+ traffic.line_width = cfg->line_width;
+ traffic.pace_output = cfg->pace_output;
+ traffic.ansi = cfg->ansi;
+ hybbx_client_display_init(&display.display, &traffic);
+ hybbx_telnet_parser_init(&parser);
+ if (hybbx_client_line_editor_start(&editor, STDIN_FILENO) == 0) {
+ editor_ready = 1;
+ }
+
+ (void)hybbx_telnet_send_greeting(fd);
+ maybe_auto_login(fd, cfg);
+
+ while (running) {
+#if defined(__AMIGA__)
+ fd_set rfds;
+ struct timeval tv;
+ int maxfd;
+ int pr;
+
+ FD_ZERO(&rfds);
+ FD_SET(STDIN_FILENO, &rfds);
+ FD_SET(fd, &rfds);
+ maxfd = fd;
+ if (STDIN_FILENO > maxfd) {
+ maxfd = STDIN_FILENO;
+ }
+ tv.tv_sec = HYBBX_TELNET_POLL_MS / 1000;
+#if defined(__AMIGA__)
+ tv.tv_usec = (unsigned long)((HYBBX_TELNET_POLL_MS % 1000) * 1000);
+#else
+ tv.tv_usec = (HYBBX_TELNET_POLL_MS % 1000) * 1000;
+#endif
+
+ pr = select(maxfd + 1, &rfds, NULL, NULL, &tv);
+ if (pr < 0) {
+ if (errno == EINTR) {
+ continue;
+ }
+ break;
+ }
+
+ if (FD_ISSET(STDIN_FILENO, &rfds)) {
+ 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) {
+ if (send_line(fd, inbuf) != HYBBX_OK) {
+ break;
+ }
+ }
+ }
+
+ if (FD_ISSET(fd, &rfds)) {
+ uint8_t buf[512];
+ ssize_t n = recv(fd, buf, sizeof(buf), 0);
+
+ if (n < 0) {
+ if (errno == EINTR) {
+ continue;
+ }
+ break;
+ }
+ if (n == 0) {
+ break;
+ }
+
+ if (cfg->verbose) {
+ fprintf(stderr, "hybbx-telnet: recv %zd bytes\n", n);
+ }
+
+ if (hybbx_telnet_parser_feed(&parser, fd, buf, (size_t)n,
+ on_telnet_data, &display) != HYBBX_OK) {
+ break;
+ }
+ }
+#else
+ struct pollfd pfds[2];
+ int pr;
+
+ pfds[0].fd = STDIN_FILENO;
+ pfds[0].events = POLLIN;
+ pfds[0].revents = 0;
+ pfds[1].fd = fd;
+ pfds[1].events = POLLIN;
+ pfds[1].revents = 0;
+
+ pr = poll(pfds, 2, HYBBX_TELNET_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) {
+ if (send_line(fd, inbuf) != HYBBX_OK) {
+ break;
+ }
+ }
+ }
+
+ if ((pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
+ break;
+ }
+
+ if ((pfds[1].revents & POLLIN) != 0) {
+ uint8_t buf[512];
+ ssize_t n = recv(fd, buf, sizeof(buf), 0);
+
+ if (n < 0) {
+ if (errno == EINTR) {
+ continue;
+ }
+ break;
+ }
+ if (n == 0) {
+ break;
+ }
+
+ if (cfg->verbose) {
+ fprintf(stderr, "hybbx-telnet: recv %zd bytes\n", n);
+ }
+
+ if (hybbx_telnet_parser_feed(&parser, fd, buf, (size_t)n,
+ on_telnet_data, &display) != HYBBX_OK) {
+ break;
+ }
+ }
+#endif
+ }
+
+ if (editor_ready) {
+ hybbx_client_line_editor_stop(&editor);
+ }
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ hybbx_telnet_client_config_t cfg;
+ int fd = -1;
+ int opt;
+ int argi = 1;
+ const char *env;
+
+ static const struct option long_opts[] = {
+ {"host", required_argument, NULL, 'H'},
+ {"port", required_argument, NULL, 'p'},
+ {"user", required_argument, NULL, 'u'},
+ {"password", required_argument, NULL, 'P'},
+ {"baud", required_argument, NULL, 1000},
+ {"line-width", required_argument, NULL, 1001},
+ {"pace", required_argument, NULL, 1002},
+ {"ansi", required_argument, NULL, 1003},
+ {"ipv6", no_argument, NULL, '6'},
+ {"timeout", required_argument, NULL, 't'},
+ {"verbose", no_argument, NULL, 'v'},
+ {"help", no_argument, NULL, 'h'},
+ {NULL, 0, NULL, 0}
+ };
+
+ config_defaults(&cfg);
+
+ env = getenv("HYBBX_HOST");
+ if (env != NULL && env[0] != '\0') {
+ hybbx_strlcpy(cfg.host, env, sizeof(cfg.host));
+ }
+ env = getenv("HYBBX_PORT");
+ if (env != NULL && env[0] != '\0') {
+ cfg.port = (unsigned)strtoul(env, NULL, 10);
+ }
+
+ while ((opt = getopt_long(argc, argv, "H:p:u:P:6t:vh",
+ 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 'u':
+ hybbx_strlcpy(cfg.login_user, optarg, sizeof(cfg.login_user));
+ break;
+ case 'P':
+ hybbx_strlcpy(cfg.login_password, optarg, sizeof(cfg.login_password));
+ break;
+ case '6':
+ cfg.ipv6 = 1;
+ break;
+ case 't':
+ cfg.timeout_sec = (unsigned)strtoul(optarg, NULL, 10);
+ break;
+ case 'v':
+ cfg.verbose = 1;
+ break;
+ case 'h':
+ print_usage(argv[0]);
+ return EXIT_SUCCESS;
+ case 1000:
+ cfg.baud = (unsigned)strtoul(optarg, NULL, 10);
+ break;
+ case 1001:
+ cfg.line_width = (unsigned)strtoul(optarg, NULL, 10);
+ break;
+ case 1002:
+ cfg.pace_output = hybbx_parse_bool(optarg, cfg.pace_output);
+ break;
+ case 1003:
+ cfg.ansi = hybbx_parse_bool(optarg, cfg.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);
+ }
+
+ (void)cfg.timeout_sec;
+
+ if (tcp_connect(&cfg, &fd) != 0) {
+ return EXIT_FAILURE;
+ }
+
+ if (cfg.verbose) {
+ fprintf(stderr, "hybbx-telnet: connected to %s:%u (baud=%u width=%u)\n",
+ cfg.host, cfg.port, cfg.baud, cfg.line_width);
+ }
+
+ run_session(fd, &cfg);
+ close(fd);
+ return EXIT_SUCCESS;
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com