diff options
Diffstat (limited to 'src/clients/hybbx_telnet.c')
| -rw-r--r-- | src/clients/hybbx_telnet.c | 544 |
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; +} |
