diff options
Diffstat (limited to 'src/clients')
| -rw-r--r-- | src/clients/CMakeLists.txt | 96 | ||||
| -rw-r--r-- | src/clients/circuit_client.c | 294 | ||||
| -rw-r--r-- | src/clients/client_display.c | 87 | ||||
| -rw-r--r-- | src/clients/client_display.h | 30 | ||||
| -rw-r--r-- | src/clients/client_line.c | 495 | ||||
| -rw-r--r-- | src/clients/client_line.h | 33 | ||||
| -rw-r--r-- | src/clients/hybbx_ssh.c | 495 | ||||
| -rw-r--r-- | src/clients/hybbx_telnet.c | 544 | ||||
| -rw-r--r-- | src/clients/hybbx_terminal.c | 433 | ||||
| -rw-r--r-- | src/clients/link_auth.c | 182 |
10 files changed, 2689 insertions, 0 deletions
diff --git a/src/clients/CMakeLists.txt b/src/clients/CMakeLists.txt new file mode 100644 index 0000000..2181fbc --- /dev/null +++ b/src/clients/CMakeLists.txt @@ -0,0 +1,96 @@ +# Standalone CLI clients (hybbx-telnet, hybbx-terminal, hybbx-ssh). +# No centralized daemon; flags/env only — see src/clients/*.c for options. + +if(HYBBX_PLATFORM_AMIGAOS) + set(_hybbx_client_sources + ${CMAKE_SOURCE_DIR}/src/core/util.c + ${CMAKE_SOURCE_DIR}/src/core/traffic.c + ${CMAKE_SOURCE_DIR}/src/core/terminal.c + client_line.c + client_display.c + ) +else() + set(_hybbx_client_sources + ${CMAKE_SOURCE_DIR}/src/core/util.c + ${CMAKE_SOURCE_DIR}/src/core/circuit.c + ${CMAKE_SOURCE_DIR}/src/core/traffic.c + ${CMAKE_SOURCE_DIR}/src/core/terminal.c + ${CMAKE_SOURCE_DIR}/plugins/packet_radio/ax25.c + circuit_client.c + link_auth.c + client_line.c + client_display.c + ) +endif() + +add_library(hybbx_client STATIC ${_hybbx_client_sources}) + +target_include_directories(hybbx_client + PUBLIC + $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include> + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/plugins/packet_radio +) + +target_compile_definitions(hybbx_client PRIVATE HYBBX_CLIENT_BUILD=1) +hybbx_apply_hardening(hybbx_client) + +if(HYBBX_PLATFORM_AMIGAOS) + target_compile_options(hybbx_client PRIVATE -mcrt=clib2) +endif() + +add_library(HyBBX::client ALIAS hybbx_client) + +if(HYBBX_BUILD_CLIENT_TELNET) + add_executable(hybbx-telnet + hybbx_telnet.c + ${CMAKE_SOURCE_DIR}/plugins/telnet/telnet_proto.c + ) + target_include_directories(hybbx-telnet PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/plugins/telnet + ) + target_link_libraries(hybbx-telnet PRIVATE hybbx_client) + hybbx_apply_hardening_executable(hybbx-telnet) + if(HYBBX_PLATFORM_AMIGAOS) + target_compile_options(hybbx-telnet PRIVATE -mcrt=clib2) + target_link_options(hybbx-telnet PRIVATE -mcrt=clib2) + target_link_libraries(hybbx-telnet PRIVATE net unix) + endif() + install(TARGETS hybbx-telnet RUNTIME DESTINATION ${HYBBX_INSTALL_HOME}) +endif() + +if(HYBBX_BUILD_CLIENT_TERMINAL) + add_executable(hybbx-terminal hybbx_terminal.c) + target_include_directories(hybbx-terminal PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/plugins/packet_radio + ) + target_link_libraries(hybbx-terminal PRIVATE hybbx_client) + hybbx_apply_hardening_executable(hybbx-terminal) + install(TARGETS hybbx-terminal RUNTIME DESTINATION ${HYBBX_INSTALL_HOME}) +endif() + +if(HYBBX_BUILD_CLIENT_SSH AND HYBBX_HAVE_LIBSSH) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBSSH REQUIRED IMPORTED_TARGET libssh) + + add_executable(hybbx-ssh hybbx_ssh.c) + target_include_directories(hybbx-ssh PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ) + target_link_libraries(hybbx-ssh PRIVATE hybbx_client PkgConfig::LIBSSH) + hybbx_apply_hardening_executable(hybbx-ssh) + install(TARGETS hybbx-ssh RUNTIME DESTINATION ${HYBBX_INSTALL_HOME}) +endif() + +if(HYBBX_INSTALL_DEV) + install(TARGETS hybbx_client + EXPORT HyBBXTargets + ARCHIVE DESTINATION ${HYBBX_INSTALL_HOME}/lib + ) +endif() diff --git a/src/clients/circuit_client.c b/src/clients/circuit_client.c new file mode 100644 index 0000000..778ca87 --- /dev/null +++ b/src/clients/circuit_client.c @@ -0,0 +1,294 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit.h" +#include "hybbx/link.h" +#include "hybbx/socket.h" +#include "hybbx/util.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#define HYBBX_CIRCUIT_LINK_POLL_MS 50 +#define HYBBX_CIRCUIT_AUTH_TIMEOUT_MS 15000 + +static int set_socket_options(int fd) +{ + int on = 1; + int flags; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { + return -1; + } + + return 0; +} + +hybbx_result_t hybbx_circuit_link_connect(const char *host, unsigned port, + int *out_fd) +{ + struct sockaddr_in6 addr6; + struct sockaddr_in addr4; + int fd; + int rc; + + if (host == NULL || out_fd == NULL || port == 0) { + return HYBBX_ERR_INVALID; + } + + 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 HYBBX_ERR_INVALID; + } + + fd = socket(AF_INET6, SOCK_STREAM, 0); + if (fd < 0) { + return HYBBX_ERR_IO; + } + + rc = connect(fd, (struct sockaddr *)&addr6, 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 HYBBX_ERR_INVALID; + } + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return HYBBX_ERR_IO; + } + + rc = connect(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return HYBBX_ERR_IO; + } + + (void)set_socket_options(fd); + *out_fd = fd; + return HYBBX_OK; +} + +hybbx_result_t hybbx_circuit_link_write(int fd, const uint8_t *frame, + size_t len) +{ + ssize_t sent; + size_t off = 0; + + if (fd < 0 || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + while (off < len) { + sent = send(fd, frame + off, len - off, MSG_NOSIGNAL); + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (sent == 0) { + return HYBBX_ERR_IO; + } + off += (size_t)sent; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_circuit_link_read(int fd, uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + ssize_t n; + + if (fd < 0 || buf == NULL || read_len == NULL) { + return HYBBX_ERR_INVALID; + } + + n = recv(fd, buf, buf_len, 0); + if (n < 0) { +#if EAGAIN == EWOULDBLOCK + if (errno == EAGAIN) { +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { +#endif + *read_len = 0; + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + *read_len = 0; + return HYBBX_ERR_IO; + } + + *read_len = (size_t)n; + return HYBBX_OK; +} + +typedef struct link_ack_wait { + int done; + int ok; +} link_ack_wait_t; + +static void on_link_auth_ack(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + link_ack_wait_t *wait = (link_ack_wait_t *)userdata; + size_t i; + + (void)flags; + + if (wait == NULL || wait->done || proto != HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK) { + return; + } + + if (len == 0 || payload == NULL) { + return; + } + + for (i = 0; i + 6 <= len; i++) { + if (memcmp(payload + i, "ok=yes", 6) == 0) { + wait->ok = 1; + break; + } + } + wait->done = 1; +} + +hybbx_result_t hybbx_circuit_link_authenticate(int fd, + const char *password, + const char *role, + const char *id) +{ + return hybbx_circuit_link_authenticate_ex(fd, password, role, id, NULL); +} + +hybbx_result_t hybbx_circuit_link_authenticate_ex(int fd, + const char *password, + const char *role, + const char *id, + const hybbx_circuit_link_qos_t *qos) +{ + hybbx_link_auth_t auth; + char payload[HYBBX_LINK_AUTH_PAYLOAD_MAX]; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t payload_len; + size_t frame_len; + hybbx_circuit_decoder_t dec; + link_ack_wait_t wait; + uint8_t buf[256]; + unsigned elapsed = 0; + + if (fd < 0 || password == NULL || id == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_link_auth_clear(&auth); + hybbx_strlcpy(auth.password, password, sizeof(auth.password)); + hybbx_strlcpy(auth.id, id, sizeof(auth.id)); + if (role != NULL && role[0] != '\0') { + hybbx_strlcpy(auth.role, role, sizeof(auth.role)); + } else { + hybbx_strlcpy(auth.role, "link", sizeof(auth.role)); + } + + if (qos != NULL) { + if (qos->baud > 0) { + auth.baud = qos->baud; + } + if (qos->duplex > 0) { + auth.duplex = qos->duplex; + } + if (qos->bandwidth != NULL && qos->bandwidth[0] != '\0') { + hybbx_strlcpy(auth.bandwidth, qos->bandwidth, sizeof(auth.bandwidth)); + } + if (qos->frequency_mhz != NULL && qos->frequency_mhz[0] != '\0') { + hybbx_strlcpy(auth.frequency_mhz, qos->frequency_mhz, + sizeof(auth.frequency_mhz)); + } + } + + payload_len = hybbx_link_auth_format(&auth, payload, sizeof(payload)); + if (payload_len == 0) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_circuit_encode_link_msg(HYBBX_CIRCUIT_PROTO_LINK_AUTH, + payload, payload_len, + frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_circuit_link_write(fd, frame, frame_len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + hybbx_circuit_decoder_init(&dec); + memset(&wait, 0, sizeof(wait)); + + while (!wait.done && elapsed < HYBBX_CIRCUIT_AUTH_TIMEOUT_MS) { + struct pollfd pfd; + int pr; + size_t read_len; + + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (pr == 0) { + elapsed += HYBBX_CIRCUIT_LINK_POLL_MS; + continue; + } + if ((pfd.revents & POLLIN) == 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_circuit_link_read(fd, buf, sizeof(buf), &read_len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (read_len == 0) { + elapsed += HYBBX_CIRCUIT_LINK_POLL_MS; + continue; + } + + hybbx_circuit_decoder_feed(&dec, buf, read_len, on_link_auth_ack, &wait); + } + + return wait.ok ? HYBBX_OK : HYBBX_ERR_DENIED; +} diff --git a/src/clients/client_display.c b/src/clients/client_display.c new file mode 100644 index 0000000..0f6a183 --- /dev/null +++ b/src/clients/client_display.c @@ -0,0 +1,87 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "client_display.h" + +#include "hybbx/terminal.h" +#include "hybbx/traffic.h" + +#include <stdio.h> +#include <unistd.h> + +void hybbx_client_display_init(hybbx_client_display_t *disp, + const hybbx_traffic_config_t *traffic) +{ + if (disp == NULL) { + return; + } + + disp->traffic = traffic != NULL ? traffic : hybbx_traffic_config_get(); + disp->col = 0; +} + +static void pace_byte(unsigned baud) +{ + unsigned delay_us; + + if (baud == 0) { + return; + } + + delay_us = hybbx_traffic_byte_delay_us(baud); + if (delay_us > 0) { + usleep(delay_us); + } +} + +void hybbx_client_display_byte(hybbx_client_display_t *disp, uint8_t byte) +{ + const hybbx_traffic_config_t *cfg; + + if (disp == NULL) { + return; + } + + cfg = disp->traffic; + if (cfg == NULL) { + fputc((int)byte, stdout); + fflush(stdout); + return; + } + + if (!cfg->ansi && byte == 0x1b) { + return; + } + + if (cfg->line_width > 0 && (byte == '\n' || byte == '\r')) { + disp->col = 0; + } else if (cfg->line_width > 0 && byte >= 0x20 && byte != 0x7f) { + disp->col++; + if (disp->col > cfg->line_width) { + fputc('\n', stdout); + disp->col = 1; + } + } + + fputc((int)byte, stdout); + fflush(stdout); + + if (cfg->pace_output) { + pace_byte(cfg->baud); + } +} + +void hybbx_client_display_write(hybbx_client_display_t *disp, + const uint8_t *data, size_t len) +{ + size_t i; + + if (disp == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + hybbx_client_display_byte(disp, data[i]); + } +} diff --git a/src/clients/client_display.h b/src/clients/client_display.h new file mode 100644 index 0000000..a457f4c --- /dev/null +++ b/src/clients/client_display.h @@ -0,0 +1,30 @@ +#ifndef HYBBX_CLIENT_DISPLAY_H +#define HYBBX_CLIENT_DISPLAY_H + +#include "hybbx/traffic.h" + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_client_display { + const hybbx_traffic_config_t *traffic; + unsigned col; +} hybbx_client_display_t; + +void hybbx_client_display_init(hybbx_client_display_t *disp, + const hybbx_traffic_config_t *traffic); + +void hybbx_client_display_byte(hybbx_client_display_t *disp, uint8_t byte); + +void hybbx_client_display_write(hybbx_client_display_t *disp, + const uint8_t *data, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CLIENT_DISPLAY_H */ diff --git a/src/clients/client_line.c b/src/clients/client_line.c new file mode 100644 index 0000000..ab8c07b --- /dev/null +++ b/src/clients/client_line.c @@ -0,0 +1,495 @@ +#include "client_line.h" + +#include "hybbx/util.h" + +#include <errno.h> +#include <string.h> +#include <termios.h> +#include <unistd.h> + +#define CLIENT_ESC_NONE 0u +#define CLIENT_ESC_ESC 1u +#define CLIENT_ESC_CSI 2u +#define CLIENT_ESC_SS3 3u + +static int history_should_store(const char *line) +{ + if (line == NULL || line[0] == '\0') { + return 0; + } + + return strncmp(line, "/login ", 7) != 0 && + strncmp(line, "/register ", 10) != 0 && + strncmp(line, "/changeme ", 10) != 0 && + strncmp(line, "/userchange ", 12) != 0; +} + +static hybbx_result_t write_all(int fd, const char *buf, size_t len) +{ + size_t off = 0; + + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + + 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 move_back(hybbx_client_line_editor_t *ed, size_t count) +{ + char buf[64]; + size_t sent = 0; + + if (ed == NULL || !ed->raw_enabled || count == 0) { + return HYBBX_OK; + } + + memset(buf, '\b', sizeof(buf)); + while (sent < count) { + size_t chunk = count - sent; + + if (chunk > sizeof(buf)) { + chunk = sizeof(buf); + } + if (write_all(STDOUT_FILENO, buf, chunk) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + sent += chunk; + } + + return HYBBX_OK; +} + +static hybbx_result_t refresh_suffix(hybbx_client_line_editor_t *ed) +{ + size_t suffix_len; + + if (ed == NULL || !ed->raw_enabled) { + return HYBBX_OK; + } + + suffix_len = ed->line_len - ed->line_cursor; + if (suffix_len > 0 && + write_all(STDOUT_FILENO, ed->line + ed->line_cursor, suffix_len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (write_all(STDOUT_FILENO, "\x1b[K", 3) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + return move_back(ed, suffix_len); +} + +static hybbx_result_t cursor_left(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || ed->line_cursor == 0) { + return HYBBX_OK; + } + ed->line_cursor--; + return ed->raw_enabled ? write_all(STDOUT_FILENO, "\b", 1) : HYBBX_OK; +} + +static hybbx_result_t cursor_right(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || ed->line_cursor >= ed->line_len) { + return HYBBX_OK; + } + if (ed->raw_enabled && + write_all(STDOUT_FILENO, ed->line + ed->line_cursor, 1) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + ed->line_cursor++; + return HYBBX_OK; +} + +static hybbx_result_t cursor_home(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL) { + return HYBBX_ERR_INVALID; + } + return move_back(ed, ed->line_cursor); +} + +static hybbx_result_t cursor_end(hybbx_client_line_editor_t *ed) +{ + while (ed != NULL && ed->line_cursor < ed->line_len) { + if (cursor_right(ed) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + return HYBBX_OK; +} + +static hybbx_result_t delete_forward(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || ed->line_cursor >= ed->line_len) { + return HYBBX_OK; + } + + memmove(ed->line + ed->line_cursor, ed->line + ed->line_cursor + 1, + ed->line_len - ed->line_cursor); + ed->line_len--; + ed->line[ed->line_len] = '\0'; + return refresh_suffix(ed); +} + +static hybbx_result_t backspace_char(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || ed->line_cursor == 0) { + return HYBBX_OK; + } + + ed->line_cursor--; + memmove(ed->line + ed->line_cursor, ed->line + ed->line_cursor + 1, + ed->line_len - ed->line_cursor); + ed->line_len--; + ed->line[ed->line_len] = '\0'; + if (ed->raw_enabled && write_all(STDOUT_FILENO, "\b", 1) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + return refresh_suffix(ed); +} + +static hybbx_result_t insert_char(hybbx_client_line_editor_t *ed, char ch) +{ + if (ed == NULL || (unsigned char)ch < 0x20u || ch == 0x7f) { + return HYBBX_OK; + } + if (ed->line_len + 1 >= sizeof(ed->line)) { + return HYBBX_OK; + } + + if (ed->line_cursor < ed->line_len) { + memmove(ed->line + ed->line_cursor + 1, ed->line + ed->line_cursor, + ed->line_len - ed->line_cursor); + } + ed->line[ed->line_cursor] = ch; + ed->line_len++; + ed->line_cursor++; + ed->line[ed->line_len] = '\0'; + + if (!ed->raw_enabled) { + return HYBBX_OK; + } + if (write_all(STDOUT_FILENO, &ch, 1) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + return refresh_suffix(ed); +} + +static void history_add(hybbx_client_line_editor_t *ed, const char *line) +{ + if (ed == NULL || !history_should_store(line)) { + return; + } + + hybbx_strlcpy(ed->history[ed->history_next], line, sizeof(ed->history[0])); + ed->history_next = (ed->history_next + 1u) % HYBBX_HISTORY_MAX; + if (ed->history_count < HYBBX_HISTORY_MAX) { + ed->history_count++; + } + ed->history_view = -1; + ed->history_saved_line[0] = '\0'; +} + +static hybbx_result_t line_replace(hybbx_client_line_editor_t *ed, const char *line) +{ + size_t len; + + if (ed == NULL || line == NULL) { + return HYBBX_ERR_INVALID; + } + + len = strlen(line); + if (len >= sizeof(ed->line)) { + len = sizeof(ed->line) - 1; + } + + if (ed->raw_enabled) { + if (move_back(ed, ed->line_cursor) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (len > 0 && write_all(STDOUT_FILENO, line, len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (write_all(STDOUT_FILENO, "\x1b[K", 3) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + memcpy(ed->line, line, len); + ed->line[len] = '\0'; + ed->line_len = len; + ed->line_cursor = len; + return HYBBX_OK; +} + +static hybbx_result_t history_up(hybbx_client_line_editor_t *ed) +{ + unsigned slot; + + if (ed == NULL || ed->history_count == 0) { + return HYBBX_OK; + } + + if (ed->history_view < 0) { + hybbx_strlcpy(ed->history_saved_line, ed->line, sizeof(ed->history_saved_line)); + ed->history_view = 0; + } else if ((unsigned)(ed->history_view + 1) < ed->history_count) { + ed->history_view++; + } else { + return HYBBX_OK; + } + + slot = (ed->history_next + HYBBX_HISTORY_MAX - 1u - + (unsigned)ed->history_view) % HYBBX_HISTORY_MAX; + return line_replace(ed, ed->history[slot]); +} + +static hybbx_result_t history_down(hybbx_client_line_editor_t *ed) +{ + unsigned slot; + + if (ed == NULL || ed->history_view < 0) { + return HYBBX_OK; + } + if (ed->history_view == 0) { + ed->history_view = -1; + return line_replace(ed, ed->history_saved_line); + } + + ed->history_view--; + slot = (ed->history_next + HYBBX_HISTORY_MAX - 1u - + (unsigned)ed->history_view) % HYBBX_HISTORY_MAX; + return line_replace(ed, ed->history[slot]); +} + +static void escape_reset(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL) { + return; + } + ed->esc_state = CLIENT_ESC_NONE; + ed->csi_len = 0; +} + +static int csi_is_final(unsigned char ch) +{ + return ch >= 0x40u && ch <= 0x7eu; +} + +static hybbx_result_t apply_csi(hybbx_client_line_editor_t *ed, + const char *params, char final_ch) +{ + (void)params; + + if (final_ch == 'A') { + return history_up(ed); + } + if (final_ch == 'B') { + return history_down(ed); + } + if (final_ch == 'C') { + return cursor_right(ed); + } + if (final_ch == 'D') { + return cursor_left(ed); + } + if (final_ch == 'H') { + return cursor_home(ed); + } + if (final_ch == 'F') { + return cursor_end(ed); + } + if (final_ch == '~') { + if (strcmp(params, "1") == 0 || strcmp(params, "7") == 0) { + return cursor_home(ed); + } + if (strcmp(params, "3") == 0) { + return delete_forward(ed); + } + if (strcmp(params, "4") == 0 || strcmp(params, "8") == 0) { + return cursor_end(ed); + } + } + return HYBBX_OK; +} + +static int filter_escape_byte(hybbx_client_line_editor_t *ed, unsigned char byte) +{ + if (ed == NULL) { + return 0; + } + if (ed->esc_state == CLIENT_ESC_NONE && byte == 0x9bu) { + ed->esc_state = CLIENT_ESC_CSI; + ed->csi_len = 0; + return 1; + } + if (ed->esc_state == CLIENT_ESC_NONE && byte == 0x1bu) { + ed->esc_state = CLIENT_ESC_ESC; + return 1; + } + if (ed->esc_state == CLIENT_ESC_ESC) { + if (byte == '[') { + ed->esc_state = CLIENT_ESC_CSI; + ed->csi_len = 0; + return 1; + } + if (byte == 'O') { + ed->esc_state = CLIENT_ESC_SS3; + return 1; + } + escape_reset(ed); + return 0; + } + if (ed->esc_state == CLIENT_ESC_SS3) { + if (byte == 'C') { + (void)cursor_right(ed); + } else if (byte == 'D') { + (void)cursor_left(ed); + } else if (byte == 'H') { + (void)cursor_home(ed); + } else if (byte == 'F') { + (void)cursor_end(ed); + } + escape_reset(ed); + return 1; + } + if (ed->esc_state == CLIENT_ESC_CSI) { + char params[24]; + + if (ed->csi_len + 1 >= sizeof(ed->csi_buf)) { + escape_reset(ed); + return 1; + } + ed->csi_buf[ed->csi_len++] = (char)byte; + if (!csi_is_final(byte)) { + return 1; + } + + if (ed->csi_len > 1) { + memcpy(params, ed->csi_buf, ed->csi_len - 1); + params[ed->csi_len - 1] = '\0'; + } else { + params[0] = '\0'; + } + escape_reset(ed); + (void)apply_csi(ed, params, (char)byte); + return 1; + } + return 0; +} + +int hybbx_client_line_editor_start(hybbx_client_line_editor_t *ed, int stdin_fd) +{ + struct termios raw; + + if (ed == NULL) { + return -1; + } + + memset(ed, 0, sizeof(*ed)); + ed->stdin_fd = stdin_fd; + ed->history_view = -1; + + if (!isatty(stdin_fd)) { + return 0; + } + if (tcgetattr(stdin_fd, &ed->termios_saved) != 0) { + return -1; + } + + raw = ed->termios_saved; + raw.c_iflag &= ~(ICRNL | IXON); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN); + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; + + if (tcsetattr(stdin_fd, TCSAFLUSH, &raw) != 0) { + return -1; + } + + ed->raw_enabled = 1; + return 0; +} + +void hybbx_client_line_editor_stop(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || !ed->raw_enabled) { + return; + } + (void)tcsetattr(ed->stdin_fd, TCSAFLUSH, &ed->termios_saved); + ed->raw_enabled = 0; +} + +hybbx_result_t hybbx_client_line_editor_poll(hybbx_client_line_editor_t *ed, + char *out_line, size_t out_len, + int *have_line, int *eof_seen) +{ + unsigned char buf[64]; + ssize_t n; + size_t i; + + if (ed == NULL || out_line == NULL || out_len == 0 || + have_line == NULL || eof_seen == NULL) { + return HYBBX_ERR_INVALID; + } + + *have_line = 0; + *eof_seen = 0; + + n = read(ed->stdin_fd, buf, sizeof(buf)); + if (n < 0) { + if (errno == EINTR) { + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + *eof_seen = 1; + return HYBBX_OK; + } + + for (i = 0; i < (size_t)n; i++) { + unsigned char ch = buf[i]; + + if (filter_escape_byte(ed, ch)) { + continue; + } + if (ch == '\b' || ch == 127) { + if (backspace_char(ed) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + continue; + } + if (ch == '\r' || ch == '\n') { + if (ed->raw_enabled && + write_all(STDOUT_FILENO, "\r\n", 2) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + hybbx_strlcpy(out_line, ed->line, out_len); + history_add(ed, ed->line); + ed->line[0] = '\0'; + ed->line_len = 0; + ed->line_cursor = 0; + ed->history_view = -1; + *have_line = 1; + return HYBBX_OK; + } + if (insert_char(ed, (char)ch) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + return HYBBX_OK; +} diff --git a/src/clients/client_line.h b/src/clients/client_line.h new file mode 100644 index 0000000..928efe0 --- /dev/null +++ b/src/clients/client_line.h @@ -0,0 +1,33 @@ +#ifndef HYBBX_CLIENT_LINE_H +#define HYBBX_CLIENT_LINE_H + +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#include <stddef.h> +#include <termios.h> + +typedef struct hybbx_client_line_editor { + char line[HYBBX_LINE_MAX]; + size_t line_len; + size_t line_cursor; + char history[HYBBX_HISTORY_MAX][HYBBX_LINE_MAX]; + unsigned history_count; + unsigned history_next; + int history_view; + char history_saved_line[HYBBX_LINE_MAX]; + int raw_enabled; + int stdin_fd; + unsigned char esc_state; + char csi_buf[24]; + size_t csi_len; + struct termios termios_saved; +} hybbx_client_line_editor_t; + +int hybbx_client_line_editor_start(hybbx_client_line_editor_t *ed, int stdin_fd); +void hybbx_client_line_editor_stop(hybbx_client_line_editor_t *ed); +hybbx_result_t hybbx_client_line_editor_poll(hybbx_client_line_editor_t *ed, + char *out_line, size_t out_len, + int *have_line, int *eof_seen); + +#endif /* HYBBX_CLIENT_LINE_H */ 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 <libssh/libssh.h> + +#include <errno.h> +#include <getopt.h> +#include <poll.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#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; +} 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; +} 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 <errno.h> +#include <getopt.h> +#include <poll.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#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; +} diff --git a/src/clients/link_auth.c b/src/clients/link_auth.c new file mode 100644 index 0000000..4ba53e5 --- /dev/null +++ b/src/clients/link_auth.c @@ -0,0 +1,182 @@ +#include "hybbx/link.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static const char *find_kv_line(const char *payload, size_t len, + const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = payload; + const char *end = payload + len; + size_t key_len = strlen(key); + + while (cursor < end) { + const char *line_end = memchr(cursor, '\n', (size_t)(end - cursor)); + const char *line_stop = line_end != NULL ? line_end : end; + const char *eq = memchr(cursor, '=', (size_t)(line_stop - cursor)); + + if (eq != NULL && (size_t)(eq - cursor) == key_len && + memcmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(line_stop - value); + + while (value_len > 0 && + (value[value_len - 1] == '\r' || + value[value_len - 1] == '\n' || + value[value_len - 1] == ' ')) { + value_len--; + } + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (line_end == NULL) { + break; + } + cursor = line_end + 1; + } + + return NULL; +} + +void hybbx_link_auth_clear(hybbx_link_auth_t *auth) +{ + if (auth == NULL) { + return; + } + + memset(auth, 0, sizeof(*auth)); +} + +size_t hybbx_link_auth_format(const hybbx_link_auth_t *auth, + char *out, size_t out_cap) +{ + int n; + + if (auth == NULL || out == NULL || out_cap == 0) { + return 0; + } + + if (auth->password[0] == '\0' || auth->id[0] == '\0') { + return 0; + } + + n = snprintf(out, out_cap, + "password=%s\nrole=%s\nid=%s\n", + auth->password, + auth->role[0] != '\0' ? auth->role : "link", + auth->id); + if (n < 0 || (size_t)n >= out_cap) { + return 0; + } + + if (auth->baud > 0) { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "baud=%u\n", auth->baud); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } + + if (auth->duplex == 1) { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "duplex=half\n"); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } else if (auth->duplex == 2) { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "duplex=full\n"); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } + + if (auth->bandwidth[0] != '\0') { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "bandwidth=%s\n", auth->bandwidth); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } + + if (auth->frequency_mhz[0] != '\0') { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "frequency_mhz=%s\n", auth->frequency_mhz); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } + + return (size_t)n; +} + +hybbx_result_t hybbx_link_auth_parse(const char *payload, size_t len, + hybbx_link_auth_t *auth) +{ + char scratch[128]; + + if (payload == NULL || auth == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_link_auth_clear(auth); + + if (find_kv_line(payload, len, "password", scratch, sizeof(scratch)) == NULL) { + return HYBBX_ERR_INVALID; + } + hybbx_strlcpy(auth->password, scratch, sizeof(auth->password)); + + if (find_kv_line(payload, len, "id", scratch, sizeof(scratch)) == NULL) { + return HYBBX_ERR_INVALID; + } + hybbx_strlcpy(auth->id, scratch, sizeof(auth->id)); + + if (find_kv_line(payload, len, "role", scratch, sizeof(scratch)) != NULL) { + hybbx_strlcpy(auth->role, scratch, sizeof(auth->role)); + } else { + hybbx_strlcpy(auth->role, "link", sizeof(auth->role)); + } + + if (find_kv_line(payload, len, "baud", scratch, sizeof(scratch)) != NULL) { + unsigned long baud = strtoul(scratch, NULL, 10); + if (baud > 0) { + auth->baud = (unsigned)baud; + } + } + + if (find_kv_line(payload, len, "duplex", scratch, sizeof(scratch)) != NULL) { + if (strcmp(scratch, "full") == 0 || strcmp(scratch, "full-duplex") == 0) { + auth->duplex = 2; + } else if (strcmp(scratch, "half") == 0 || + strcmp(scratch, "half-duplex") == 0 || + strcmp(scratch, "simplex") == 0) { + auth->duplex = 1; + } + } + + if (find_kv_line(payload, len, "bandwidth", scratch, sizeof(scratch)) != NULL) { + hybbx_strlcpy(auth->bandwidth, scratch, sizeof(auth->bandwidth)); + } + + if (find_kv_line(payload, len, "frequency_mhz", scratch, sizeof(scratch)) != NULL) { + hybbx_strlcpy(auth->frequency_mhz, scratch, sizeof(auth->frequency_mhz)); + } else if (find_kv_line(payload, len, "frequency", scratch, + sizeof(scratch)) != NULL) { + hybbx_strlcpy(auth->frequency_mhz, scratch, sizeof(auth->frequency_mhz)); + } + + return HYBBX_OK; +} |
