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_ssh.c | 495 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 src/clients/hybbx_ssh.c (limited to 'src/clients/hybbx_ssh.c') diff --git a/src/clients/hybbx_ssh.c b/src/clients/hybbx_ssh.c new file mode 100644 index 0000000..c2d6d88 --- /dev/null +++ b/src/clients/hybbx_ssh.c @@ -0,0 +1,495 @@ +/** + * hybbx-ssh — HyBBX-native SSH client (CLI only, no GUI). + * + * Encrypted transport to HyBBX [transport.ssh] (default port 3232). + * Wire SSH username/password satisfy the libssh handshake only; HyBBX + * accounts use guest auto-login or /login like telnet. + * Host keys are not verified — no known_hosts prompt. + */ + +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "client_line.h" +#include "client_display.h" +#include "hybbx/limits.h" +#include "hybbx/ssh.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_SSH_POLL_MS 50 +#define HYBBX_SSH_WIRE_USER "hybbx" +#define HYBBX_SSH_WIRE_PASSWORD "hybbx" + +typedef struct hybbx_ssh_client_config { + char host[256]; + unsigned port; + char wire_user[64]; + char wire_password[128]; + 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_ssh_client_config_t; + +typedef struct ssh_session_ctx { + ssh_session session; + ssh_channel channel; + hybbx_client_display_t display; + hybbx_client_line_editor_t editor; + int editor_ready; +} ssh_session_ctx_t; + +static void config_defaults(hybbx_ssh_client_config_t *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + hybbx_strlcpy(cfg->host, "127.0.0.1", sizeof(cfg->host)); + cfg->port = HYBBX_SSH_DEFAULT_PORT; + hybbx_strlcpy(cfg->wire_user, HYBBX_SSH_WIRE_USER, sizeof(cfg->wire_user)); + hybbx_strlcpy(cfg->wire_password, HYBBX_SSH_WIRE_PASSWORD, + sizeof(cfg->wire_password)); + 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-ssh — HyBBX SSH client (encrypted transport, 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 SSH port (default 3232)\n" + " -u, --user USER Send /login USER after connect\n" + " -P, --password PASS Password for /login\n" + " --wire-user USER SSH wire username (default hybbx)\n" + " --wire-pass PASS SSH wire password (default hybbx)\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 connecting\n" + " -t, --timeout SEC Connect timeout (default 30)\n" + " -v, --verbose Log SSH progress on stderr\n" + " -h, --help Show help\n" + "\n" + "SSH wire credentials are not HyBBX accounts — any value is accepted\n" + "by the server. Use -u/-P for HyBBX /login after connect.\n" + "\n" + "Environment: HYBBX_HOST, HYBBX_PORT, HYBBX_SSH_WIRE_USER,\n" + " HYBBX_SSH_WIRE_PASS\n" + "\n", + prog); +} + +static void log_ssh_error(ssh_session session, const char *what) +{ + const char *msg = ssh_get_error(session); + + if (msg == NULL || msg[0] == '\0') { + msg = "unknown error"; + } + fprintf(stderr, "hybbx-ssh: %s: %s\n", what, msg); +} + +static int ssh_connect_session(const hybbx_ssh_client_config_t *cfg, + ssh_session *out_session, + ssh_channel *out_channel) +{ + ssh_session session; + ssh_channel channel; + char portbuf[16]; + long timeout_sec; + int strict = 0; + int process_config = 0; + int pubkey_auth = 0; + int nodelay = 1; + int rc; + + session = ssh_new(); + if (session == NULL) { + fprintf(stderr, "hybbx-ssh: ssh_new failed\n"); + return -1; + } + + snprintf(portbuf, sizeof(portbuf), "%u", cfg->port); + timeout_sec = (long)cfg->timeout_sec; + + (void)ssh_options_set(session, SSH_OPTIONS_HOST, cfg->host); + (void)ssh_options_set(session, SSH_OPTIONS_PORT_STR, portbuf); + (void)ssh_options_set(session, SSH_OPTIONS_USER, cfg->wire_user); + (void)ssh_options_set(session, SSH_OPTIONS_STRICTHOSTKEYCHECK, &strict); + (void)ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, &process_config); + (void)ssh_options_set(session, SSH_OPTIONS_PUBKEY_AUTH, &pubkey_auth); + (void)ssh_options_set(session, SSH_OPTIONS_TIMEOUT, &timeout_sec); + (void)ssh_options_set(session, SSH_OPTIONS_NODELAY, &nodelay); + + if (cfg->ipv6) { + (void)ssh_options_set(session, SSH_OPTIONS_BINDADDR, "::"); + } + + rc = ssh_connect(session); + if (rc != SSH_OK) { + log_ssh_error(session, "connect"); + ssh_free(session); + return -1; + } + + if (cfg->verbose) { + fprintf(stderr, "hybbx-ssh: SSH handshake complete with %s:%u\n", + cfg->host, cfg->port); + } + + rc = ssh_userauth_password(session, NULL, cfg->wire_password); + if (rc != SSH_AUTH_SUCCESS) { + log_ssh_error(session, "wire authentication"); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + channel = ssh_channel_new(session); + if (channel == NULL) { + log_ssh_error(session, "channel_new"); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + rc = ssh_channel_open_session(channel); + if (rc != SSH_OK) { + log_ssh_error(session, "channel_open_session"); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + rc = ssh_channel_request_pty(channel); + if (rc != SSH_OK) { + log_ssh_error(session, "channel_request_pty"); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + rc = ssh_channel_request_shell(channel); + if (rc != SSH_OK) { + log_ssh_error(session, "channel_request_shell"); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + ssh_set_blocking(session, 0); + + *out_session = session; + *out_channel = channel; + return 0; +} + +static hybbx_result_t send_line(ssh_channel channel, const char *line) +{ + char buf[HYBBX_LINE_MAX + 2]; + size_t len; + int rc; + + snprintf(buf, sizeof(buf), "%s\n", line); + len = strlen(buf); + + rc = ssh_channel_write(channel, buf, (uint32_t)len); + if (rc < 0 || (size_t)rc != len) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void maybe_auto_login(ssh_channel channel, + const hybbx_ssh_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(channel, line); + if (cfg->login_password[0] != '\0') { + (void)send_line(channel, cfg->login_password); + } +} + +static int drain_channel(ssh_session_ctx_t *ctx, int verbose) +{ + uint8_t buf[512]; + int n; + + for (;;) { + n = ssh_channel_read_nonblocking(ctx->channel, buf, sizeof(buf), 0); + if (n == SSH_AGAIN) { + return 0; + } + if (n <= 0) { + if (ssh_channel_is_eof(ctx->channel) || ssh_channel_is_closed(ctx->channel)) { + return -1; + } + return 0; + } + + if (verbose) { + fprintf(stderr, "hybbx-ssh: recv %d bytes\n", n); + } + + hybbx_client_display_write(&ctx->display, buf, (size_t)n); + } +} + +static int run_session(ssh_session_ctx_t *ctx, const hybbx_ssh_client_config_t *cfg) +{ + char inbuf[HYBBX_LINE_MAX + 2]; + int running = 1; + + maybe_auto_login(ctx->channel, cfg); + + while (running) { + struct pollfd pfds[2]; + socket_t ssh_fd; + int pr; + int nready = 1; + + pfds[0].fd = STDIN_FILENO; + pfds[0].events = POLLIN; + pfds[0].revents = 0; + + ssh_fd = ssh_get_fd(ctx->session); + pfds[1].fd = (int)ssh_fd; + pfds[1].events = POLLIN; + pfds[1].revents = 0; + nready = 2; + + pr = poll(pfds, (unsigned int)nready, HYBBX_SSH_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (drain_channel(ctx, cfg->verbose) != 0) { + break; + } + + if ((pfds[0].revents & POLLIN) != 0) { + int have_line = 0; + int eof_seen = 0; + + if (!ctx->editor_ready || + hybbx_client_line_editor_poll(&ctx->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(ctx->channel, inbuf) != HYBBX_OK) { + break; + } + } + } + + if ((pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + + if ((pfds[1].revents & POLLIN) != 0 && drain_channel(ctx, cfg->verbose) != 0) { + break; + } + } + + return 0; +} + +static void session_ctx_init(ssh_session_ctx_t *ctx, + ssh_session session, + ssh_channel channel, + const hybbx_ssh_client_config_t *cfg) +{ + hybbx_traffic_config_t traffic; + + memset(ctx, 0, sizeof(*ctx)); + ctx->session = session; + ctx->channel = channel; + + 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(&ctx->display, &traffic); + + if (hybbx_client_line_editor_start(&ctx->editor, STDIN_FILENO) == 0) { + ctx->editor_ready = 1; + } +} + +static void session_ctx_fini(ssh_session_ctx_t *ctx) +{ + if (ctx->editor_ready) { + hybbx_client_line_editor_stop(&ctx->editor); + ctx->editor_ready = 0; + } +} + +int main(int argc, char *argv[]) +{ + hybbx_ssh_client_config_t cfg; + ssh_session session = NULL; + ssh_channel channel = NULL; + ssh_session_ctx_t ctx; + 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'}, + {"wire-user", required_argument, NULL, 1004}, + {"wire-pass", required_argument, NULL, 1005}, + {"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); + } + env = getenv("HYBBX_SSH_WIRE_USER"); + if (env != NULL && env[0] != '\0') { + hybbx_strlcpy(cfg.wire_user, env, sizeof(cfg.wire_user)); + } + env = getenv("HYBBX_SSH_WIRE_PASS"); + if (env != NULL && env[0] != '\0') { + hybbx_strlcpy(cfg.wire_password, env, sizeof(cfg.wire_password)); + } + + 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; + case 1004: + hybbx_strlcpy(cfg.wire_user, optarg, sizeof(cfg.wire_user)); + break; + case 1005: + hybbx_strlcpy(cfg.wire_password, optarg, sizeof(cfg.wire_password)); + 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); + } + + if (ssh_connect_session(&cfg, &session, &channel) != 0) { + return EXIT_FAILURE; + } + + if (cfg.verbose) { + fprintf(stderr, + "hybbx-ssh: shell ready on %s:%u (baud=%u width=%u wire=%s)\n", + cfg.host, cfg.port, cfg.baud, cfg.line_width, cfg.wire_user); + } + + session_ctx_init(&ctx, session, channel, &cfg); + run_session(&ctx, &cfg); + session_ctx_fini(&ctx); + + ssh_channel_send_eof(channel); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + + return EXIT_SUCCESS; +} -- cgit v1.3.1