From 20cb29c2f8c5c87bc590896854a20b1473ceb358 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Sat, 8 Aug 2026 03:54:55 +0000 Subject: #2 --- plugins/telnet/CMakeLists.txt | 26 +++ plugins/telnet/telnet.c | 516 +++++++++++++++++++++++++++++++++++++++++ plugins/telnet/telnet_config.c | 125 ++++++++++ plugins/telnet/telnet_proto.c | 213 +++++++++++++++++ plugins/telnet/telnet_proto.h | 37 +++ 5 files changed, 917 insertions(+) create mode 100644 plugins/telnet/CMakeLists.txt create mode 100644 plugins/telnet/telnet.c create mode 100644 plugins/telnet/telnet_config.c create mode 100644 plugins/telnet/telnet_proto.c create mode 100644 plugins/telnet/telnet_proto.h (limited to 'plugins/telnet') diff --git a/plugins/telnet/CMakeLists.txt b/plugins/telnet/CMakeLists.txt new file mode 100644 index 0000000..144ba1d --- /dev/null +++ b/plugins/telnet/CMakeLists.txt @@ -0,0 +1,26 @@ +add_library(hybbx_plugin_telnet STATIC + telnet.c + telnet_config.c + telnet_proto.c +) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_telnet + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_telnet + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_telnet PRIVATE hybbx_core Threads::Threads) +hybbx_link_plugin_instances(hybbx_plugin_telnet HYBBX_HAVE_PLUGIN_TELNET) +hybbx_apply_hardening(hybbx_plugin_telnet) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_telnet + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/telnet/telnet.c b/plugins/telnet/telnet.c new file mode 100644 index 0000000..a857b7d --- /dev/null +++ b/plugins/telnet/telnet.c @@ -0,0 +1,516 @@ +/* + * telnet — TCP transport plugin (port 2323). INI: [transport.telnet]. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/socket.h" +#include "hybbx/security_ban.h" +#include "hybbx/telnet.h" +#include "telnet_proto.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct telnet_client { + int fd; + hybbx_telnet_parser_t parser; +} telnet_client_t; + +typedef struct telnet_client_ctx { + hybbx_session_t *session; + hybbx_result_t last_rc; +} telnet_client_ctx_t; + +extern const hybbx_transport_plugin_t hybbx_plugin_telnet; + +static hybbx_service_t *g_service; +static hybbx_telnet_config_t g_config; +static pthread_t g_accept_thread; +static int g_listen_v4 = -1; +static int g_listen_v6 = -1; +static volatile int g_telnet_running = 0; + +static hybbx_result_t telnet_stop(void); + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + 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); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, 16) != 0) { + close(fd); + return -1; + } + + return fd; +} + +static hybbx_result_t telnet_send_byte(int fd, char ch) +{ + ssize_t sent; + const char *p = &ch; + + sent = send(fd, p, 1, MSG_NOSIGNAL); + if (sent < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t telnet_write(hybbx_session_t *session, + const char *data, size_t len) +{ + telnet_client_t *client; + size_t i; + hybbx_result_t rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + client = (telnet_client_t *)session->transport_data; + if (client == NULL || client->fd < 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + char ch = data[i]; + + if (ch == '\n' && (i == 0 || data[i - 1] != '\r')) { + rc = telnet_send_byte(client->fd, '\r'); + if (rc != HYBBX_OK) { + return rc; + } + } + + { + ssize_t sent = send(client->fd, &ch, 1, MSG_NOSIGNAL); + + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (sent == 0) { + return HYBBX_ERR_IO; + } + } + } + + return HYBBX_OK; +} + +static void telnet_on_user_data(void *ctx, const uint8_t *data, size_t len) +{ + telnet_client_ctx_t *tctx = (telnet_client_ctx_t *)ctx; + hybbx_result_t rc; + + if (tctx == NULL || tctx->session == NULL || data == NULL || len == 0) { + return; + } + + rc = hybbx_session_handle_input(tctx->session, data, len); + if (rc != HYBBX_OK) { + tctx->last_rc = rc; + } +} + +#define TELNET_CLIENT_POLL_MS 30000 + +static void telnet_send_busy(int fd) +{ + static const char msg[] = "All nodes in use. Try later.\r\n"; + + if (fd < 0) { + return; + } + + (void)send(fd, msg, sizeof(msg) - 1u, 0); +} + +static void *telnet_client_thread(void *arg) +{ + telnet_client_t *client = (telnet_client_t *)arg; + hybbx_session_t *session = NULL; + telnet_client_ctx_t tctx; + uint8_t buf[256]; + ssize_t n; + hybbx_result_t rc; + + if (client == NULL || g_service == NULL) { + free(client); + return NULL; + } + + (void)set_socket_options(client->fd, 0); + + rc = hybbx_telnet_send_greeting(client->fd); + if (rc != HYBBX_OK) { + close(client->fd); + free(client); + return NULL; + } + + rc = hybbx_session_open(g_service, &hybbx_plugin_telnet, client, &session); + if (rc != HYBBX_OK || session == NULL) { + if (rc == HYBBX_ERR_BUSY) { + telnet_send_busy(client->fd); + } + close(client->fd); + free(client); + return NULL; + } + + { + char remote[64]; + + if (hybbx_socket_peer_name(client->fd, remote, sizeof(remote)) == HYBBX_OK) { + (void)hybbx_session_set_remote(session, remote); + } + } + + hybbx_telnet_parser_init(&client->parser); + tctx.session = session; + tctx.last_rc = HYBBX_OK; + + while (g_telnet_running) { + struct pollfd pfd; + int pr; + + pfd.fd = client->fd; + pfd.events = POLLIN; + pfd.revents = 0; + + pr = poll(&pfd, 1, TELNET_CLIENT_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + rc = hybbx_session_tick(session); + if (rc == HYBBX_SESSION_END) { + tctx.last_rc = HYBBX_SESSION_END; + break; + } + continue; + } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + n = recv(client->fd, buf, sizeof(buf), 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + + rc = hybbx_telnet_parser_feed(&client->parser, client->fd, buf, (size_t)n, + telnet_on_user_data, &tctx); + if (rc != HYBBX_OK) { + break; + } + + if (tctx.last_rc == HYBBX_SESSION_END) { + break; + } + } + + hybbx_session_close(session); + shutdown(client->fd, SHUT_RDWR); + close(client->fd); + free(client); + return NULL; +} + +static void accept_client(int client_fd) +{ + telnet_client_t *client; + pthread_t thread; + pthread_attr_t attr; + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + close(client_fd); + return; + } + + client->fd = client_fd; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + if (pthread_create(&thread, &attr, telnet_client_thread, client) != 0) { + close(client_fd); + free(client); + } + + pthread_attr_destroy(&attr); +} + +static void *telnet_accept_thread(void *arg) +{ + (void)arg; + + while (g_telnet_running) { + struct pollfd fds[2]; + nfds_t nfds = 0; + int i; + int ready; + + memset(fds, 0, sizeof(fds)); + + if (g_listen_v4 >= 0) { + fds[nfds].fd = g_listen_v4; + fds[nfds].events = POLLIN; + nfds++; + } + + if (g_listen_v6 >= 0) { + fds[nfds].fd = g_listen_v6; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) { + break; + } + + ready = poll(fds, nfds, 500); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (ready == 0) { + continue; + } + + for (i = 0; i < (int)nfds; i++) { + if (fds[i].revents & POLLIN) { + int client_fd = accept(fds[i].fd, NULL, NULL); + + if (client_fd >= 0) { + if (!hybbx_security_ban_accept_fd(client_fd)) { + close(client_fd); + continue; + } + accept_client(client_fd); + } + } + } + } + + return NULL; +} + +static hybbx_result_t telnet_init(hybbx_service_t *service) +{ + g_service = service; + return HYBBX_OK; +} + +static void telnet_shutdown(void) +{ + telnet_stop(); +} + +static hybbx_result_t telnet_start(const char *config) +{ + hybbx_result_t rc; + + if (g_telnet_running) { + return HYBBX_ERR_BUSY; + } + + rc = hybbx_telnet_config_parse(config, &g_config); + if (rc != HYBBX_OK) { + return rc; + } + + g_listen_v4 = -1; + g_listen_v6 = -1; + + if (g_config.ipv4) { + g_listen_v4 = create_listen_socket(AF_INET, g_config.bind_v4, g_config.port); + if (g_listen_v4 < 0) { + hybbx_socket_log_bind_failure("telnet", g_config.bind_v4, + g_config.port); + return HYBBX_ERR_IO; + } + } + + if (g_config.ipv6) { + g_listen_v6 = create_listen_socket(AF_INET6, g_config.bind_v6, g_config.port); + if (g_listen_v6 < 0) { + hybbx_log_warn("[telnet] IPv6 bind [%s]:%u skipped (%s)", + g_config.bind_v6, g_config.port, strerror(errno)); + } + } + + if (g_listen_v4 < 0 && g_listen_v6 < 0) { + return HYBBX_ERR_IO; + } + + g_telnet_running = 1; + + if (pthread_create(&g_accept_thread, NULL, telnet_accept_thread, NULL) != 0) { + g_telnet_running = 0; + if (g_listen_v4 >= 0) { + close(g_listen_v4); + g_listen_v4 = -1; + } + if (g_listen_v6 >= 0) { + close(g_listen_v6); + g_listen_v6 = -1; + } + return HYBBX_ERR_IO; + } + + { + char msg[256]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, "[telnet] listening"); + if (g_listen_v4 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv4 %s:%u", + g_config.bind_v4, g_config.port); + } + if (g_listen_v6 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv6 [%s]:%u", + g_config.bind_v6, g_config.port); + } + hybbx_log_info("%s", msg); + } + + return HYBBX_OK; +} + +static hybbx_result_t telnet_stop(void) +{ + if (!g_telnet_running) { + return HYBBX_OK; + } + + g_telnet_running = 0; + + if (g_listen_v4 >= 0) { + shutdown(g_listen_v4, SHUT_RDWR); + close(g_listen_v4); + g_listen_v4 = -1; + } + + if (g_listen_v6 >= 0) { + shutdown(g_listen_v6, SHUT_RDWR); + close(g_listen_v6); + g_listen_v6 = -1; + } + + pthread_join(g_accept_thread, NULL); + hybbx_log_info("[telnet] stop"); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_telnet = { + .name = "telnet", + .kind = HYBBX_TRANSPORT_TELNET, + .version = 1, + .init = telnet_init, + .shutdown = telnet_shutdown, + .start = telnet_start, + .stop = telnet_stop, + .write = telnet_write, +}; diff --git a/plugins/telnet/telnet_config.c b/plugins/telnet/telnet_config.c new file mode 100644 index 0000000..d154f88 --- /dev/null +++ b/plugins/telnet/telnet_config.c @@ -0,0 +1,125 @@ +#include "hybbx/telnet.h" +#include "hybbx/util.h" + +#include +#include + +static unsigned int parse_port(const char *value) +{ + char *end; + unsigned long port; + + if (value == NULL || value[0] == '\0') { + return HYBBX_TELNET_DEFAULT_PORT; + } + + port = strtoul(value, &end, 10); + if (end == value || *end != '\0' || port == 0 || port > 65535u) { + return HYBBX_TELNET_DEFAULT_PORT; + } + + return (unsigned int)port; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (scratch != NULL && scratch_len > 0) { + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + return value; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_telnet_config_defaults(hybbx_telnet_config_t *config) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + hybbx_strlcpy(config->bind_v4, HYBBX_TELNET_DEFAULT_BIND_V4, + sizeof(config->bind_v4)); + hybbx_strlcpy(config->bind_v6, HYBBX_TELNET_DEFAULT_BIND_V6, + sizeof(config->bind_v6)); + config->port = HYBBX_TELNET_DEFAULT_PORT; + config->ipv4 = 1; + config->ipv6 = 1; +} + +hybbx_result_t hybbx_telnet_config_parse(const char *config, + hybbx_telnet_config_t *out) +{ + char scratch[HYBBX_TELNET_BIND_V6_MAX]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_telnet_config_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = find_kv(config, "port", scratch, sizeof(scratch)); + out->port = parse_port(value); + + value = find_kv(config, "bind", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + if (strchr(value, ':') != NULL) { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } else { + hybbx_strlcpy(out->bind_v4, value, sizeof(out->bind_v4)); + } + } + + value = find_kv(config, "bind6", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } + + value = find_kv(config, "ipv4", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv4 = hybbx_parse_bool(value, 1); + } + + value = find_kv(config, "ipv6", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv6 = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} diff --git a/plugins/telnet/telnet_proto.c b/plugins/telnet/telnet_proto.c new file mode 100644 index 0000000..523c68c --- /dev/null +++ b/plugins/telnet/telnet_proto.c @@ -0,0 +1,213 @@ +#include "telnet_proto.h" + +#include "hybbx/socket.h" + +#include +#include +#include + +#define TELNET_IAC 255 +#define TELNET_DONT 254 +#define TELNET_DO 253 +#define TELNET_WONT 252 +#define TELNET_WILL 251 +#define TELNET_SB 250 +#define TELNET_GA 249 +#define TELNET_EL 248 +#define TELNET_EC 247 +#define TELNET_AYT 246 +#define TELNET_AO 245 +#define TELNET_IP 244 +#define TELNET_BRK 243 +#define TELNET_DM 242 +#define TELNET_NOP 241 +#define TELNET_SE 240 + +#define TELNET_OPT_ECHO 1 +#define TELNET_OPT_SGA 3 +#define TELNET_OPT_TTYPE 24 +#define TELNET_OPT_NAWS 31 +#define TELNET_OPT_LINEMODE 34 +#define TELNET_OPT_CHARSET 42 + +enum telnet_parse_state { + TS_DATA = 0, + TS_IAC, + TS_CMD, + TS_SUBNEG, + TS_SUBNEG_IAC +}; + +static hybbx_result_t telnet_send_raw(int fd, const uint8_t *data, size_t len) +{ + size_t sent_total = 0; + + while (sent_total < len) { + ssize_t sent; + + sent = send(fd, data + sent_total, len - sent_total, MSG_NOSIGNAL); + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (sent == 0) { + return HYBBX_ERR_IO; + } + + sent_total += (size_t)sent; + } + + return HYBBX_OK; +} + +static hybbx_result_t telnet_send_option(int fd, uint8_t cmd, uint8_t option) +{ + uint8_t buf[3]; + + buf[0] = TELNET_IAC; + buf[1] = cmd; + buf[2] = option; + return telnet_send_raw(fd, buf, sizeof(buf)); +} + +static hybbx_result_t telnet_reply_do(int fd, uint8_t option) +{ + switch (option) { + case TELNET_OPT_ECHO: + return telnet_send_option(fd, TELNET_WONT, option); + case TELNET_OPT_SGA: + return telnet_send_option(fd, TELNET_WILL, option); + case TELNET_OPT_TTYPE: + case TELNET_OPT_NAWS: + case TELNET_OPT_LINEMODE: + case TELNET_OPT_CHARSET: + return telnet_send_option(fd, TELNET_WONT, option); + default: + return telnet_send_option(fd, TELNET_WONT, option); + } +} + +static hybbx_result_t telnet_reply_will(int fd, uint8_t option) +{ + switch (option) { + case TELNET_OPT_SGA: + return telnet_send_option(fd, TELNET_DO, option); + case TELNET_OPT_ECHO: + return telnet_send_option(fd, TELNET_DONT, option); + default: + return telnet_send_option(fd, TELNET_DONT, option); + } +} + +static void emit_byte(hybbx_telnet_data_cb on_data, void *ctx, uint8_t byte) +{ + if (on_data != NULL) { + on_data(ctx, &byte, 1); + } +} + +void hybbx_telnet_parser_init(hybbx_telnet_parser_t *parser) +{ + if (parser == NULL) { + return; + } + + parser->state = TS_DATA; + parser->command = 0; +} + +hybbx_result_t hybbx_telnet_send_greeting(int fd) +{ + static const uint8_t greeting[] = { + TELNET_IAC, TELNET_WONT, TELNET_OPT_ECHO, + TELNET_IAC, TELNET_WILL, TELNET_OPT_SGA, + TELNET_IAC, TELNET_DONT, TELNET_OPT_LINEMODE, + }; + + if (fd < 0) { + return HYBBX_ERR_INVALID; + } + + return telnet_send_raw(fd, greeting, sizeof(greeting)); +} + +hybbx_result_t hybbx_telnet_parser_feed(hybbx_telnet_parser_t *parser, int fd, + const uint8_t *data, size_t len, + hybbx_telnet_data_cb on_data, + void *ctx) +{ + size_t i; + hybbx_result_t rc; + + if (parser == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + uint8_t byte = data[i]; + + switch (parser->state) { + case TS_DATA: + if (byte == TELNET_IAC) { + parser->state = TS_IAC; + } else { + emit_byte(on_data, ctx, byte); + } + break; + + case TS_IAC: + if (byte == TELNET_IAC) { + emit_byte(on_data, ctx, TELNET_IAC); + parser->state = TS_DATA; + } else if (byte == TELNET_WILL || byte == TELNET_WONT || + byte == TELNET_DO || byte == TELNET_DONT) { + parser->command = (int)byte; + parser->state = TS_CMD; + } else if (byte == TELNET_SB) { + parser->state = TS_SUBNEG; + } else { + parser->state = TS_DATA; + } + break; + + case TS_CMD: + if (fd >= 0) { + if (parser->command == TELNET_DO) { + rc = telnet_reply_do(fd, byte); + if (rc != HYBBX_OK) { + return rc; + } + } else if (parser->command == TELNET_WILL) { + rc = telnet_reply_will(fd, byte); + if (rc != HYBBX_OK) { + return rc; + } + } + } + parser->state = TS_DATA; + break; + + case TS_SUBNEG: + if (byte == TELNET_IAC) { + parser->state = TS_SUBNEG_IAC; + } + break; + + case TS_SUBNEG_IAC: + if (byte == TELNET_SE) { + parser->state = TS_DATA; + } else { + parser->state = TS_SUBNEG; + } + break; + + default: + parser->state = TS_DATA; + break; + } + } + + return HYBBX_OK; +} diff --git a/plugins/telnet/telnet_proto.h b/plugins/telnet/telnet_proto.h new file mode 100644 index 0000000..aa29122 --- /dev/null +++ b/plugins/telnet/telnet_proto.h @@ -0,0 +1,37 @@ +#ifndef HYBBX_TELNET_PROTO_H +#define HYBBX_TELNET_PROTO_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_telnet_parser { + int state; + int command; +} hybbx_telnet_parser_t; + +typedef void (*hybbx_telnet_data_cb)(void *ctx, const uint8_t *data, size_t len); + +void hybbx_telnet_parser_init(hybbx_telnet_parser_t *parser); + +/** Send initial option negotiation (echo, suppress go ahead). */ +hybbx_result_t hybbx_telnet_send_greeting(int fd); + +/** + * Strip telnet protocol bytes and invoke @p on_data for user payload. + */ +hybbx_result_t hybbx_telnet_parser_feed(hybbx_telnet_parser_t *parser, int fd, + const uint8_t *data, size_t len, + hybbx_telnet_data_cb on_data, + void *ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TELNET_PROTO_H */ -- cgit v1.3.1