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/websocket/CMakeLists.txt | 37 ++ plugins/websocket/websocket.c | 723 +++++++++++++++++++++++++++++++++++++++ plugins/websocket/ws_config.c | 163 +++++++++ plugins/websocket/ws_proto.c | 565 ++++++++++++++++++++++++++++++ plugins/websocket/ws_proto.h | 63 ++++ plugins/websocket/ws_sha1.c | 138 ++++++++ plugins/websocket/ws_sha1.h | 9 + plugins/websocket/ws_tls.c | 400 ++++++++++++++++++++++ plugins/websocket/ws_tls.h | 43 +++ 9 files changed, 2141 insertions(+) create mode 100644 plugins/websocket/CMakeLists.txt create mode 100644 plugins/websocket/websocket.c create mode 100644 plugins/websocket/ws_config.c create mode 100644 plugins/websocket/ws_proto.c create mode 100644 plugins/websocket/ws_proto.h create mode 100644 plugins/websocket/ws_sha1.c create mode 100644 plugins/websocket/ws_sha1.h create mode 100644 plugins/websocket/ws_tls.c create mode 100644 plugins/websocket/ws_tls.h (limited to 'plugins/websocket') diff --git a/plugins/websocket/CMakeLists.txt b/plugins/websocket/CMakeLists.txt new file mode 100644 index 0000000..a63f83d --- /dev/null +++ b/plugins/websocket/CMakeLists.txt @@ -0,0 +1,37 @@ +add_library(hybbx_plugin_websocket STATIC + websocket.c + ws_config.c + ws_proto.c + ws_sha1.c + ws_tls.c +) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_websocket + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_websocket + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(OpenSSL QUIET) +if(OpenSSL_FOUND) + target_compile_definitions(hybbx_plugin_websocket PRIVATE HYBBX_WS_HAVE_TLS) + target_link_libraries(hybbx_plugin_websocket PRIVATE OpenSSL::SSL OpenSSL::Crypto) + message(STATUS "WebSocket plugin: OpenSSL found — wss + self-signed certs") +else() + message(STATUS "WebSocket plugin: OpenSSL not found — plain ws only") +endif() + +target_link_libraries(hybbx_plugin_websocket PRIVATE hybbx_core Threads::Threads) +hybbx_link_plugin_instances(hybbx_plugin_websocket HYBBX_HAVE_PLUGIN_WEBSOCKET) +hybbx_apply_hardening(hybbx_plugin_websocket) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_websocket + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/websocket/websocket.c b/plugins/websocket/websocket.c new file mode 100644 index 0000000..4ff2230 --- /dev/null +++ b/plugins/websocket/websocket.c @@ -0,0 +1,723 @@ +/* + * websocket — RFC6455 forward-proxy transport. INI: [transport.websocket]. + * + * Raw byte stream to the session core (no HyBBX auth at the wire layer). + * Intended behind a TLS reverse proxy; see docs/WEBSOCKET.md. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/socket.h" +#include "hybbx/security_ban.h" +#include "hybbx/websocket.h" +#include "hybbx/util.h" +#include "ws_proto.h" +#include "ws_tls.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define WS_CLIENT_POLL_MS 5000 +#define WS_CLIENT_PING_IDLE_SEC 20 + +typedef struct ws_client { + hybbx_ws_connection_t ws; + hybbx_session_t *hbx_session; + hybbx_result_t last_rc; + int slot_held; + time_t last_io_at; + uint8_t tx_buf[HYBBX_WS_FRAME_PAYLOAD_MAX]; + size_t tx_len; +} ws_client_t; + +typedef struct ws_client_ctx { + ws_client_t *client; +} ws_client_ctx_t; + +extern const hybbx_transport_plugin_t hybbx_plugin_websocket; + +static hybbx_service_t *g_service; +static hybbx_websocket_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_ws_running = 0; +static unsigned g_active_clients; +static pthread_mutex_t g_client_lock = PTHREAD_MUTEX_INITIALIZER; + +static hybbx_result_t ws_plugin_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 void log_bind_failure(const char *bind_addr, unsigned port) +{ + int err = errno; + + hybbx_socket_log_bind_failure("websocket", bind_addr, port); + if (err == EACCES && port < 1024u) { + hybbx_log_warn("[websocket] port %u is privileged — run as root, grant " + "bind permission, or set port >= 1024 in hybbx.ini", + port); + } +} + +static size_t utf8_char_span(const uint8_t *buf, size_t len, size_t pos) +{ + uint8_t b; + size_t n; + size_t i; + + if (pos >= len) { + return 0; + } + + b = buf[pos]; + if (b < 0x80u) { + return 1; + } + if ((b & 0xE0u) == 0xC0u) { + n = 2; + } else if ((b & 0xF0u) == 0xE0u) { + n = 3; + } else if ((b & 0xF8u) == 0xF0u) { + n = 4; + } else { + return 1; + } + + if (pos + n > len) { + return 0; + } + + for (i = 1; i < n; i++) { + if ((buf[pos + i] & 0xC0u) != 0x80u) { + return 1; + } + } + + return n; +} + +static size_t utf8_complete_prefix(const uint8_t *buf, size_t len) +{ + size_t pos = 0; + + while (pos < len) { + size_t span = utf8_char_span(buf, len, pos); + + if (span == 0) { + break; + } + pos += span; + } + + return pos; +} + +static hybbx_result_t ws_tx_flush(ws_client_t *client, int force_tail) +{ + size_t n; + hybbx_result_t rc; + + if (client == NULL || client->tx_len == 0) { + return HYBBX_OK; + } + + n = utf8_complete_prefix(client->tx_buf, client->tx_len); + if (n == 0 && force_tail) { + client->tx_buf[0] = '?'; + n = 1; + } + if (n == 0) { + return HYBBX_OK; + } + + rc = hybbx_ws_write_text(&client->ws, (const char *)client->tx_buf, n); + if (rc != HYBBX_OK) { + return rc; + } + client->last_io_at = time(NULL); + + if (n < client->tx_len) { + memmove(client->tx_buf, client->tx_buf + n, client->tx_len - n); + } + client->tx_len -= n; + + return HYBBX_OK; +} + +static hybbx_result_t ws_plugin_write(hybbx_session_t *session, + const char *data, size_t len) +{ + ws_client_t *client; + size_t i; + hybbx_result_t rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + client = (ws_client_t *)session->transport_data; + if (client == NULL || !client->ws.established) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + char ch = data[i]; + + if (ch == '\n' && (i == 0 || data[i - 1] != '\r')) { + if (client->tx_len + 2 > sizeof(client->tx_buf)) { + rc = ws_tx_flush(client, 1); + if (rc != HYBBX_OK) { + return rc; + } + } + client->tx_buf[client->tx_len++] = '\r'; + } + + if (client->tx_len >= sizeof(client->tx_buf)) { + rc = ws_tx_flush(client, 1); + if (rc != HYBBX_OK) { + return rc; + } + } + + client->tx_buf[client->tx_len++] = (uint8_t)ch; + + rc = ws_tx_flush(client, 0); + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} + +static void ws_send_busy(ws_client_t *client) +{ + static const char msg[] = "All nodes in use. Try later.\r\n"; + + if (client == NULL) { + return; + } + + (void)hybbx_ws_write_text(&client->ws, msg, sizeof(msg) - 1u); +} + +static void ws_send_limit(ws_client_t *client) +{ + static const char msg[] = + "All WebSocket connections in use. Try later.\r\n"; + + if (client == NULL) { + return; + } + + (void)hybbx_ws_write_text(&client->ws, msg, sizeof(msg) - 1u); +} + +static int ws_client_acquire_slot(ws_client_t *client) +{ + if (client == NULL) { + return 0; + } + + pthread_mutex_lock(&g_client_lock); + if (g_active_clients >= g_config.max_connections) { + pthread_mutex_unlock(&g_client_lock); + return 0; + } + + g_active_clients++; + client->slot_held = 1; + pthread_mutex_unlock(&g_client_lock); + return 1; +} + +static void ws_client_release_slot(ws_client_t *client) +{ + if (client == NULL || !client->slot_held) { + return; + } + + pthread_mutex_lock(&g_client_lock); + if (g_active_clients > 0) { + g_active_clients--; + } + client->slot_held = 0; + pthread_mutex_unlock(&g_client_lock); +} + +static void ws_on_user_data(void *ctx, const uint8_t *data, size_t len) +{ + ws_client_ctx_t *wctx = (ws_client_ctx_t *)ctx; + hybbx_result_t rc; + + if (wctx == NULL || wctx->client == NULL || + wctx->client->hbx_session == NULL || data == NULL || len == 0) { + return; + } + + rc = hybbx_session_handle_input(wctx->client->hbx_session, data, len); + if (rc != HYBBX_OK) { + wctx->client->last_rc = rc; + } +} + +static void *ws_client_thread(void *arg) +{ + ws_client_t *client = (ws_client_t *)arg; + ws_client_ctx_t wctx; + hybbx_result_t rc; + char remote[64]; + + if (client == NULL || g_service == NULL) { + free(client); + return NULL; + } + + remote[0] = '\0'; + (void)hybbx_socket_peer_name(client->ws.fd, remote, sizeof(remote)); + + if (hybbx_ws_tls_server_enabled()) { + void *tls = hybbx_ws_tls_accept(client->ws.fd); + + if (tls == NULL) { + goto cleanup; + } + hybbx_ws_connection_set_tls(&client->ws, tls); + } + + rc = hybbx_ws_server_handshake(&client->ws, g_config.path); + if (rc != HYBBX_OK) { + goto cleanup; + } + + if (!ws_client_acquire_slot(client)) { + ws_send_limit(client); + goto cleanup; + } + + rc = hybbx_session_open(g_service, &hybbx_plugin_websocket, client, + &client->hbx_session); + if (rc != HYBBX_OK || client->hbx_session == NULL) { + if (rc == HYBBX_ERR_BUSY) { + ws_send_busy(client); + } + goto cleanup; + } + + if (remote[0] != '\0') { + (void)hybbx_session_set_remote(client->hbx_session, remote); + } + + wctx.client = client; + client->last_rc = HYBBX_OK; + client->last_io_at = time(NULL); + + while (g_ws_running && client->ws.established) { + struct pollfd pfd; + int pr; + + pfd.fd = client->ws.fd; + pfd.events = POLLIN; + pfd.revents = 0; + + pr = poll(&pfd, 1, WS_CLIENT_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + time_t now = time(NULL); + + if (now != (time_t)-1 && + (client->last_io_at == 0 || + now - client->last_io_at >= WS_CLIENT_PING_IDLE_SEC)) { + rc = hybbx_ws_ping(&client->ws); + if (rc != HYBBX_OK) { + break; + } + client->last_io_at = now; + } + + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + continue; + } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + rc = hybbx_ws_read_frames(&client->ws, ws_on_user_data, &wctx); + if (rc != HYBBX_OK) { + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + break; + } + client->last_io_at = time(NULL); + + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + } + +cleanup: + if (client->hbx_session != NULL) { + hybbx_session_close(client->hbx_session); + client->hbx_session = NULL; + } + + ws_client_release_slot(client); + + (void)ws_tx_flush(client, 1); + + if (client->ws.established) { + (void)hybbx_ws_close(&client->ws); + } + + hybbx_ws_connection_cleanup(&client->ws); + + if (client->ws.fd >= 0) { + shutdown(client->ws.fd, SHUT_RDWR); + close(client->ws.fd); + client->ws.fd = -1; + } + + free(client); + return NULL; +} + +static void accept_client(int client_fd) +{ + ws_client_t *client; + pthread_t thread; + pthread_attr_t attr; + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + close(client_fd); + return; + } + + hybbx_ws_connection_init(&client->ws, client_fd); + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + if (pthread_create(&thread, &attr, ws_client_thread, client) != 0) { + close(client_fd); + free(client); + } + + pthread_attr_destroy(&attr); +} + +static void *ws_accept_thread(void *arg) +{ + (void)arg; + + while (g_ws_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 ws_plugin_init(hybbx_service_t *service) +{ + g_service = service; + return HYBBX_OK; +} + +static void ws_plugin_shutdown(void) +{ + ws_plugin_stop(); +} + +static hybbx_result_t ws_plugin_start(const char *config) +{ + hybbx_result_t rc; + + if (g_ws_running) { + return HYBBX_ERR_BUSY; + } + + rc = hybbx_websocket_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) { + log_bind_failure(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("[websocket] 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; + } + + if (hybbx_ws_tls_compiled()) { + char cert_dir[HYBBX_PATH_MAX]; + hybbx_result_t tls_rc; + + if (hybbx_path_resolve(cert_dir, sizeof(cert_dir), + g_config.cert_dir) != HYBBX_OK) { + hybbx_strlcpy(cert_dir, g_config.cert_dir, sizeof(cert_dir)); + } + + tls_rc = hybbx_ws_tls_ensure_certs(cert_dir); + + if (tls_rc == HYBBX_OK) { + tls_rc = hybbx_ws_tls_server_start(cert_dir); + } + if (tls_rc != HYBBX_OK) { + hybbx_log_warn("[websocket] TLS init failed (%d), plain ws only", + (int)tls_rc); + } + } + + g_ws_running = 1; + g_active_clients = 0; + + if (pthread_create(&g_accept_thread, NULL, ws_accept_thread, NULL) != 0) { + g_ws_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[384]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, "[websocket] 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); + } + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " path=%s max_connections=%u (%s, forward-proxy)", + g_config.path, g_config.max_connections, + hybbx_ws_tls_server_enabled() ? "wss" : "ws"); + hybbx_log_info("%s", msg); + } + + return HYBBX_OK; +} + +static hybbx_result_t ws_plugin_stop(void) +{ + if (!g_ws_running) { + return HYBBX_OK; + } + + g_ws_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_ws_tls_server_stop(); + hybbx_log_info("[websocket] stop"); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_websocket = { + .name = "websocket", + .kind = HYBBX_TRANSPORT_WEBSOCKET, + .version = 1, + .init = ws_plugin_init, + .shutdown = ws_plugin_shutdown, + .start = ws_plugin_start, + .stop = ws_plugin_stop, + .write = ws_plugin_write, +}; diff --git a/plugins/websocket/ws_config.c b/plugins/websocket/ws_config.c new file mode 100644 index 0000000..baf88b2 --- /dev/null +++ b/plugins/websocket/ws_config.c @@ -0,0 +1,163 @@ +#include "hybbx/websocket.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_WEBSOCKET_DEFAULT_PORT; + } + + port = strtoul(value, &end, 10); + if (end == value || *end != '\0' || port == 0 || port > 65535u) { + return HYBBX_WEBSOCKET_DEFAULT_PORT; + } + + return (unsigned int)port; +} + +static unsigned int parse_max_connections(const char *value) +{ + char *end; + unsigned long n; + + if (value == NULL || value[0] == '\0') { + return HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + } + + n = strtoul(value, &end, 10); + if (end == value || *end != '\0' || n == 0 || n > 65535u) { + return HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + } + + return (unsigned int)n; +} + +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_websocket_config_defaults(hybbx_websocket_config_t *config) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + hybbx_strlcpy(config->bind_v4, HYBBX_WEBSOCKET_DEFAULT_BIND_V4, + sizeof(config->bind_v4)); + hybbx_strlcpy(config->bind_v6, HYBBX_WEBSOCKET_DEFAULT_BIND_V6, + sizeof(config->bind_v6)); + hybbx_strlcpy(config->path, HYBBX_WEBSOCKET_DEFAULT_PATH, + sizeof(config->path)); + hybbx_strlcpy(config->cert_dir, HYBBX_WEBSOCKET_DEFAULT_CERT_DIR, + sizeof(config->cert_dir)); + config->port = HYBBX_WEBSOCKET_DEFAULT_PORT; + config->max_connections = HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + config->ipv4 = 1; + config->ipv6 = 1; +} + +hybbx_result_t hybbx_websocket_config_parse(const char *config, + hybbx_websocket_config_t *out) +{ + char scratch[HYBBX_WEBSOCKET_PATH_MAX]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_websocket_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, "max_connections", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "max_online", scratch, sizeof(scratch)); + } + out->max_connections = parse_max_connections(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, "path", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->path, value, sizeof(out->path)); + } + + value = find_kv(config, "cert_dir", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->cert_dir, value, sizeof(out->cert_dir)); + } + + 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/websocket/ws_proto.c b/plugins/websocket/ws_proto.c new file mode 100644 index 0000000..b0e63b5 --- /dev/null +++ b/plugins/websocket/ws_proto.c @@ -0,0 +1,565 @@ +#include "ws_proto.h" +#include "ws_sha1.h" +#include "ws_tls.h" + +#include +#include +#include +#include +#include +#include +#include + +#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +static int header_has_token(const char *value, const char *token) +{ + const char *p; + + if (value == NULL || token == NULL) { + return 0; + } + + for (p = value; *p != '\0'; p++) { + if (strncasecmp(p, token, strlen(token)) == 0) { + return 1; + } + } + + return 0; +} + +static int header_value_equals(const char *value, const char *expected) +{ + size_t expected_len; + + if (value == NULL || expected == NULL) { + return 0; + } + + expected_len = strlen(expected); + if (strncasecmp(value, expected, expected_len) != 0) { + return 0; + } + + { + char c = value[expected_len]; + + return c == '\0' || c == '\r' || c == ' ' || c == '\t' || c == ','; + } +} + +static ssize_t recv_some(int fd, void *buf, size_t len) +{ + ssize_t n; + + n = recv(fd, buf, len, 0); + if (n < 0 && errno == EINTR) { + return 0; + } + return n; +} + +static ssize_t ws_recv_some(hybbx_ws_connection_t *ws, void *buf, size_t len) +{ + if (ws == NULL) { + return -1; + } + + if (ws->tls != NULL) { + return hybbx_ws_tls_recv(ws->tls, buf, len); + } + + return recv_some(ws->fd, buf, len); +} + +static ssize_t send_all(int fd, const void *buf, size_t len) +{ + const uint8_t *p = (const uint8_t *)buf; + size_t sent = 0; + + while (sent < len) { + ssize_t n = send(fd, p + sent, len - sent, MSG_NOSIGNAL); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + return -1; + } + if (n == 0) { + return -1; + } + sent += (size_t)n; + } + + return (ssize_t)sent; +} + +static ssize_t ws_send_all(hybbx_ws_connection_t *ws, const void *buf, + size_t len) +{ + if (ws == NULL) { + return -1; + } + + if (ws->tls != NULL) { + return hybbx_ws_tls_send(ws->tls, buf, len); + } + + return send_all(ws->fd, buf, len); +} + +static int base64_encode(const uint8_t *in, size_t in_len, char *out, + size_t out_len) +{ + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t i; + size_t o = 0; + + if (out_len < ((in_len + 2) / 3) * 4 + 1) { + return -1; + } + + for (i = 0; i < in_len; i += 3) { + uint32_t v = (uint32_t)in[i] << 16; + + if (i + 1 < in_len) { + v |= (uint32_t)in[i + 1] << 8; + } + if (i + 2 < in_len) { + v |= (uint32_t)in[i + 2]; + } + + out[o++] = tbl[(v >> 18) & 63u]; + out[o++] = tbl[(v >> 12) & 63u]; + out[o++] = (i + 1 < in_len) ? tbl[(v >> 6) & 63u] : '='; + out[o++] = (i + 2 < in_len) ? tbl[v & 63u] : '='; + } + + out[o] = '\0'; + return 0; +} + +static const char *header_value(const char *headers, const char *name) +{ + size_t name_len = strlen(name); + const char *p = headers; + + while (p != NULL && *p != '\0') { + const char *line_end = strstr(p, "\r\n"); + + if (line_end == NULL) { + break; + } + + if ((size_t)(line_end - p) > name_len + 2 && + strncasecmp(p, name, name_len) == 0 && p[name_len] == ':') { + const char *value = p + name_len + 1; + + while (*value == ' ' || *value == '\t') { + value++; + } + return value; + } + + p = line_end + 2; + } + + return NULL; +} + +static int path_matches(const char *request_line, const char *path) +{ + const char *sp1; + const char *sp2; + size_t path_len; + + if (path == NULL || path[0] == '\0') { + return 1; + } + + sp1 = strchr(request_line, ' '); + if (sp1 == NULL) { + return 0; + } + sp1++; + sp2 = strchr(sp1, ' '); + if (sp2 == NULL) { + return 0; + } + + path_len = strlen(path); + if ((size_t)(sp2 - sp1) < path_len) { + return 0; + } + if (strncmp(sp1, path, path_len) != 0) { + return 0; + } + if (sp1[path_len] != ' ' && sp1[path_len] != '?' && + sp1[path_len] != '\0') { + return 0; + } + + return 1; +} + +void hybbx_ws_connection_init(hybbx_ws_connection_t *ws, int fd) +{ + if (ws == NULL) { + return; + } + + memset(ws, 0, sizeof(*ws)); + ws->fd = fd; +} + +void hybbx_ws_connection_set_tls(hybbx_ws_connection_t *ws, void *tls) +{ + if (ws == NULL) { + return; + } + + ws->tls = tls; +} + +void hybbx_ws_connection_cleanup(hybbx_ws_connection_t *ws) +{ + if (ws == NULL) { + return; + } + + if (ws->tls != NULL) { + hybbx_ws_tls_shutdown(ws->tls); + hybbx_ws_tls_free(ws->tls); + ws->tls = NULL; + } +} + +hybbx_result_t hybbx_ws_server_handshake(hybbx_ws_connection_t *ws, + const char *path) +{ + char buf[HYBBX_WS_HANDSHAKE_MAX]; + size_t total = 0; + const char *key_hdr; + char key[128]; + char accept_src[256]; + uint8_t digest[20]; + char accept_b64[64]; + char response[512]; + const char *upgrade; + const char *connection; + + if (ws == NULL || ws->fd < 0) { + return HYBBX_ERR_INVALID; + } + + while (total + 1 < sizeof(buf)) { + ssize_t n = ws_recv_some(ws, buf + total, sizeof(buf) - 1 - total); + + if (n < 0) { + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + + total += (size_t)n; + buf[total] = '\0'; + + if (strstr(buf, "\r\n\r\n") != NULL) { + break; + } + } + + if (strncmp(buf, "GET ", 4) != 0) { + return HYBBX_ERR_DENIED; + } + + { + const char *eol = strstr(buf, "\r\n"); + char first_line[256]; + size_t line_len; + + if (eol == NULL) { + return HYBBX_ERR_DENIED; + } + line_len = (size_t)(eol - buf); + if (line_len >= sizeof(first_line)) { + return HYBBX_ERR_DENIED; + } + memcpy(first_line, buf, line_len); + first_line[line_len] = '\0'; + if (!path_matches(first_line, path)) { + return HYBBX_ERR_DENIED; + } + } + + upgrade = header_value(buf, "Upgrade"); + connection = header_value(buf, "Connection"); + if (upgrade == NULL || connection == NULL || + !header_value_equals(upgrade, "websocket") || + !header_has_token(connection, "upgrade")) { + return HYBBX_ERR_DENIED; + } + + key_hdr = header_value(buf, "Sec-WebSocket-Key"); + if (key_hdr == NULL) { + return HYBBX_ERR_DENIED; + } + + { + const char *key_end = strstr(key_hdr, "\r\n"); + size_t key_len; + + if (key_end == NULL) { + return HYBBX_ERR_DENIED; + } + key_len = (size_t)(key_end - key_hdr); + while (key_len > 0 && + (key_hdr[key_len - 1] == ' ' || key_hdr[key_len - 1] == '\t')) { + key_len--; + } + if (key_len >= sizeof(key)) { + return HYBBX_ERR_DENIED; + } + memcpy(key, key_hdr, key_len); + key[key_len] = '\0'; + } + + snprintf(accept_src, sizeof(accept_src), "%s%s", key, WS_GUID); + hybbx_ws_sha1((const uint8_t *)accept_src, strlen(accept_src), digest); + if (base64_encode(digest, sizeof(digest), accept_b64, + sizeof(accept_b64)) != 0) { + return HYBBX_ERR_IO; + } + + snprintf(response, sizeof(response), + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n" + "\r\n", + accept_b64); + + if (ws_send_all(ws, response, strlen(response)) < 0) { + return HYBBX_ERR_IO; + } + + ws->established = 1; + ws->rx_len = 0; + return HYBBX_OK; +} + +static hybbx_result_t ws_handle_frame(hybbx_ws_connection_t *ws, + const uint8_t *frame, size_t frame_len, + hybbx_ws_data_cb on_data, void *ctx) +{ + uint8_t opcode; + uint64_t payload_len; + size_t header_len; + uint8_t mask[4]; + uint8_t payload[HYBBX_WS_FRAME_PAYLOAD_MAX]; + size_t i; + + if (frame_len < 2) { + return HYBBX_ERR_IO; + } + + opcode = frame[0] & 0x0Fu; + if ((frame[0] & 0x80) == 0) { + return HYBBX_ERR_UNSUPPORTED; + } + + if ((frame[1] & 0x80) == 0) { + return HYBBX_ERR_IO; + } + + payload_len = frame[1] & 0x7Fu; + header_len = 2; + + if (payload_len == 126) { + if (frame_len < 4) { + return HYBBX_ERR_IO; + } + payload_len = ((uint64_t)frame[2] << 8) | frame[3]; + header_len = 4; + } else if (payload_len == 127) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (payload_len > HYBBX_WS_FRAME_PAYLOAD_MAX) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (frame_len < header_len + 4 + payload_len) { + return HYBBX_ERR_IO; + } + + memcpy(mask, frame + header_len, 4); + header_len += 4; + + for (i = 0; i < payload_len; i++) { + payload[i] = frame[header_len + i] ^ mask[i % 4]; + } + + switch (opcode) { + case 0x1: + case 0x2: + if (on_data != NULL && payload_len > 0) { + on_data(ctx, payload, (size_t)payload_len); + } + return HYBBX_OK; + case 0x8: + return HYBBX_ERR_IO; + case 0x9: + { + uint8_t pong[2]; + + pong[0] = 0x8A; + pong[1] = 0x00; + (void)ws_send_all(ws, pong, 2); + } + return HYBBX_OK; + case 0xA: + return HYBBX_OK; + default: + return HYBBX_OK; + } +} + +hybbx_result_t hybbx_ws_read_frames(hybbx_ws_connection_t *ws, + hybbx_ws_data_cb on_data, + void *ctx) +{ + uint8_t tmp[256]; + ssize_t n; + + if (ws == NULL || !ws->established) { + return HYBBX_ERR_INVALID; + } + + n = ws_recv_some(ws, tmp, sizeof(tmp)); + if (n < 0) { + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + + if (ws->rx_len + (size_t)n > sizeof(ws->rx_buf)) { + ws->rx_len = 0; + return HYBBX_ERR_IO; + } + + memcpy(ws->rx_buf + ws->rx_len, tmp, (size_t)n); + ws->rx_len += (size_t)n; + + while (ws->rx_len >= 2) { + uint64_t payload_len = ws->rx_buf[1] & 0x7Fu; + size_t header_len = 2; + size_t frame_len; + hybbx_result_t rc; + + if (payload_len == 126) { + if (ws->rx_len < 4) { + return HYBBX_OK; + } + payload_len = ((uint64_t)ws->rx_buf[2] << 8) | ws->rx_buf[3]; + header_len = 4; + } else if (payload_len == 127) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (payload_len > HYBBX_WS_FRAME_PAYLOAD_MAX) { + return HYBBX_ERR_UNSUPPORTED; + } + + frame_len = header_len + 4 + (size_t)payload_len; + if (ws->rx_len < frame_len) { + return HYBBX_OK; + } + + rc = ws_handle_frame(ws, ws->rx_buf, frame_len, on_data, ctx); + if (rc != HYBBX_OK) { + return rc; + } + + if (ws->rx_len > frame_len) { + memmove(ws->rx_buf, ws->rx_buf + frame_len, + ws->rx_len - frame_len); + } + ws->rx_len -= frame_len; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_write_text(hybbx_ws_connection_t *ws, + const char *data, size_t len) +{ + uint8_t hdr[10]; + size_t hdr_len = 2; + + if (ws == NULL || data == NULL || !ws->established) { + return HYBBX_ERR_INVALID; + } + + if (len > HYBBX_WS_FRAME_PAYLOAD_MAX) { + return HYBBX_ERR_INVALID; + } + + hdr[0] = 0x81; + if (len < 126) { + hdr[1] = (uint8_t)len; + hdr_len = 2; + } else { + hdr[1] = 126; + hdr[2] = (uint8_t)((len >> 8) & 0xFFu); + hdr[3] = (uint8_t)(len & 0xFFu); + hdr_len = 4; + } + + if (ws_send_all(ws, hdr, hdr_len) < 0) { + return HYBBX_ERR_IO; + } + if (len > 0 && ws_send_all(ws, data, len) < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_ping(hybbx_ws_connection_t *ws) +{ + uint8_t frame[2]; + + if (ws == NULL || !ws->established) { + return HYBBX_ERR_INVALID; + } + + frame[0] = 0x89; + frame[1] = 0x00; + + if (ws_send_all(ws, frame, sizeof(frame)) < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_close(hybbx_ws_connection_t *ws) +{ + uint8_t frame[2]; + + if (ws == NULL || ws->fd < 0) { + return HYBBX_ERR_INVALID; + } + + frame[0] = 0x88; + frame[1] = 0x00; + (void)ws_send_all(ws, frame, 2); + ws->established = 0; + return HYBBX_OK; +} diff --git a/plugins/websocket/ws_proto.h b/plugins/websocket/ws_proto.h new file mode 100644 index 0000000..5a1cb23 --- /dev/null +++ b/plugins/websocket/ws_proto.h @@ -0,0 +1,63 @@ +#ifndef HYBBX_WS_PROTO_H +#define HYBBX_WS_PROTO_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_WS_HANDSHAKE_MAX 4096u +#define HYBBX_WS_FRAME_PAYLOAD_MAX 4096u + +typedef void (*hybbx_ws_data_cb)(void *ctx, const uint8_t *data, size_t len); + +typedef struct hybbx_ws_connection { + int fd; + void *tls; + int established; + uint8_t rx_buf[HYBBX_WS_FRAME_PAYLOAD_MAX + 16]; + size_t rx_len; +} hybbx_ws_connection_t; + +void hybbx_ws_connection_init(hybbx_ws_connection_t *ws, int fd); + +/** Attach TLS session (opaque SSL*) after accept; plain ws when NULL. */ +void hybbx_ws_connection_set_tls(hybbx_ws_connection_t *ws, void *tls); + +/** Release TLS resources; does not close @c fd. */ +void hybbx_ws_connection_cleanup(hybbx_ws_connection_t *ws); + +/** + * Perform server-side HTTP Upgrade handshake. + * @p path must match the request URI path (e.g. /hybbx) when non-empty. + */ +hybbx_result_t hybbx_ws_server_handshake(hybbx_ws_connection_t *ws, + const char *path); + +/** + * Read WebSocket frames; invoke @p on_data for each complete text/binary + * payload from the client. Returns HYBBX_ERR_IO on disconnect. + */ +hybbx_result_t hybbx_ws_read_frames(hybbx_ws_connection_t *ws, + hybbx_ws_data_cb on_data, + void *ctx); + +/** Send one server text frame (unmasked). */ +hybbx_result_t hybbx_ws_write_text(hybbx_ws_connection_t *ws, + const char *data, size_t len); + +/** Send one server ping frame (unmasked, empty payload). */ +hybbx_result_t hybbx_ws_ping(hybbx_ws_connection_t *ws); + +/** Send WebSocket close frame. */ +hybbx_result_t hybbx_ws_close(hybbx_ws_connection_t *ws); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_WS_PROTO_H */ diff --git a/plugins/websocket/ws_sha1.c b/plugins/websocket/ws_sha1.c new file mode 100644 index 0000000..53ad4c0 --- /dev/null +++ b/plugins/websocket/ws_sha1.c @@ -0,0 +1,138 @@ +/* + * Minimal SHA-1 for WebSocket handshake (RFC 6455). Public-domain style. + */ +#include "ws_sha1.h" + +#include +#include +#include + +typedef struct ws_sha1_ctx { + uint32_t state[5]; + uint64_t count; + uint8_t buffer[64]; +} ws_sha1_ctx_t; + +static uint32_t rol(uint32_t v, unsigned bits) +{ + return (v << bits) | (v >> (32u - bits)); +} + +static void ws_sha1_transform(ws_sha1_ctx_t *ctx, const uint8_t block[64]) +{ + uint32_t w[80]; + uint32_t a, b, c, d, e, f, k, t; + unsigned i; + + for (i = 0; i < 16; i++) { + w[i] = ((uint32_t)block[i * 4] << 24) | + ((uint32_t)block[i * 4 + 1] << 16) | + ((uint32_t)block[i * 4 + 2] << 8) | + (uint32_t)block[i * 4 + 3]; + } + for (i = 16; i < 80; i++) { + w[i] = rol(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + } + + a = ctx->state[0]; + b = ctx->state[1]; + c = ctx->state[2]; + d = ctx->state[3]; + e = ctx->state[4]; + + for (i = 0; i < 80; i++) { + if (i < 20) { + f = (b & c) | ((~b) & d); + k = 0x5A827999u; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1u; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDCu; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6u; + } + t = rol(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; + } + + ctx->state[0] += a; + ctx->state[1] += b; + ctx->state[2] += c; + ctx->state[3] += d; + ctx->state[4] += e; +} + +static void ws_sha1_init(ws_sha1_ctx_t *ctx) +{ + ctx->state[0] = 0x67452301u; + ctx->state[1] = 0xEFCDAB89u; + ctx->state[2] = 0x98BADCFEu; + ctx->state[3] = 0x10325476u; + ctx->state[4] = 0xC3D2E1F0u; + ctx->count = 0; +} + +static void ws_sha1_update(ws_sha1_ctx_t *ctx, const uint8_t *data, size_t len) +{ + size_t i = 0; + size_t idx = (size_t)((ctx->count >> 3) & 63u); + + ctx->count += (uint64_t)len * 8u; + + if (idx + len > 63) { + memcpy(ctx->buffer + idx, data, 64 - idx); + ws_sha1_transform(ctx, ctx->buffer); + for (i = 64 - idx; i + 63 < len; i += 64) { + ws_sha1_transform(ctx, data + i); + } + idx = 0; + } else { + i = 0; + } + + memcpy(ctx->buffer + idx, data + i, len - i); +} + +static void ws_sha1_final(ws_sha1_ctx_t *ctx, uint8_t out[20]) +{ + uint8_t final[64]; + size_t idx = (size_t)((ctx->count >> 3) & 63u); + unsigned i; + + memcpy(final, ctx->buffer, idx); + final[idx++] = 0x80; + if (idx > 56) { + memset(final + idx, 0, 64 - idx); + ws_sha1_transform(ctx, final); + idx = 0; + } + memset(final + idx, 0, 56 - idx); + + for (i = 0; i < 8; i++) { + final[56 + i] = (uint8_t)(ctx->count >> (56 - i * 8)); + } + ws_sha1_transform(ctx, final); + + for (i = 0; i < 5; i++) { + out[i * 4] = (uint8_t)(ctx->state[i] >> 24); + out[i * 4 + 1] = (uint8_t)(ctx->state[i] >> 16); + out[i * 4 + 2] = (uint8_t)(ctx->state[i] >> 8); + out[i * 4 + 3] = (uint8_t)(ctx->state[i]); + } +} + +void hybbx_ws_sha1(const uint8_t *data, size_t len, uint8_t out[20]) +{ + ws_sha1_ctx_t ctx; + + ws_sha1_init(&ctx); + ws_sha1_update(&ctx, data, len); + ws_sha1_final(&ctx, out); +} diff --git a/plugins/websocket/ws_sha1.h b/plugins/websocket/ws_sha1.h new file mode 100644 index 0000000..b685cf7 --- /dev/null +++ b/plugins/websocket/ws_sha1.h @@ -0,0 +1,9 @@ +#ifndef HYBBX_WS_SHA1_H +#define HYBBX_WS_SHA1_H + +#include +#include + +void hybbx_ws_sha1(const uint8_t *data, size_t len, uint8_t out[20]); + +#endif /* HYBBX_WS_SHA1_H */ diff --git a/plugins/websocket/ws_tls.c b/plugins/websocket/ws_tls.c new file mode 100644 index 0000000..679ce59 --- /dev/null +++ b/plugins/websocket/ws_tls.c @@ -0,0 +1,400 @@ +#include "ws_tls.h" + +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/websocket.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include + +#ifdef HYBBX_WS_HAVE_TLS + +#include +#include +#include +#include +#include + +static SSL_CTX *g_ssl_ctx; +static int g_tls_enabled; + +static hybbx_result_t mkdir_cert_dir(const char *cert_dir) +{ + char parent[HYBBX_PATH_MAX]; + struct stat st; + + if (cert_dir == NULL || cert_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (stat(cert_dir, &st) == 0) { + return S_ISDIR(st.st_mode) ? HYBBX_OK : HYBBX_ERR_IO; + } + + if (hybbx_path_dirname(cert_dir, parent, sizeof(parent)) == HYBBX_OK && + parent[0] != '\0' && strcmp(parent, cert_dir) != 0 && + stat(parent, &st) != 0) { + if (mkdir_cert_dir(parent) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + if (mkdir(cert_dir, 0700) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void build_cert_path(const char *cert_dir, const char *name, + char *out, size_t out_len) +{ + if (cert_dir == NULL || name == NULL || out == NULL || out_len == 0) { + if (out != NULL && out_len > 0) { + out[0] = '\0'; + } + return; + } + + snprintf(out, out_len, "%s/%s", cert_dir, name); +} + +static int cert_files_exist(const char *cert_path, const char *key_path) +{ + struct stat st; + + return stat(cert_path, &st) == 0 && S_ISREG(st.st_mode) && + stat(key_path, &st) == 0 && S_ISREG(st.st_mode); +} + +static hybbx_result_t generate_self_signed(const char *cert_path, + const char *key_path) +{ + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *pctx = NULL; + X509 *cert = NULL; + X509_NAME *name; + FILE *fp; + int rc = HYBBX_ERR_IO; + + pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (pctx == NULL || + EVP_PKEY_keygen_init(pctx) <= 0 || + EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 2048) <= 0 || + EVP_PKEY_keygen(pctx, &pkey) <= 0 || pkey == NULL) { + goto done; + } + + cert = X509_new(); + if (cert == NULL) { + goto done; + } + + if (X509_set_version(cert, 2) != 1 || + ASN1_INTEGER_set(X509_get_serialNumber(cert), 1) != 1 || + X509_gmtime_adj(X509_get_notBefore(cert), 0) == NULL || + X509_gmtime_adj(X509_get_notAfter(cert), + (long)HYBBX_WS_TLS_CERT_VALID_DAYS * 24L * 3600L) == + NULL || + X509_set_pubkey(cert, pkey) != 1) { + goto done; + } + + name = X509_get_subject_name(cert); + if (name == NULL || + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, + (const unsigned char *)"hybbx", -1, -1, + 0) != 1 || + X509_set_issuer_name(cert, name) != 1 || + X509_sign(cert, pkey, EVP_sha256()) <= 0) { + goto done; + } + + fp = fopen(key_path, "w"); + if (fp == NULL || + PEM_write_PrivateKey(fp, pkey, NULL, NULL, 0, NULL, NULL) != 1) { + if (fp != NULL) { + fclose(fp); + unlink(key_path); + } + goto done; + } + fclose(fp); + (void)chmod(key_path, 0600); + + fp = fopen(cert_path, "w"); + if (fp == NULL || PEM_write_X509(fp, cert) != 1) { + if (fp != NULL) { + fclose(fp); + unlink(cert_path); + } + goto done; + } + fclose(fp); + (void)chmod(cert_path, 0644); + + rc = HYBBX_OK; + +done: + if (cert != NULL) { + X509_free(cert); + } + if (pkey != NULL) { + EVP_PKEY_free(pkey); + } + if (pctx != NULL) { + EVP_PKEY_CTX_free(pctx); + } + + return rc; +} + +int hybbx_ws_tls_compiled(void) +{ + return 1; +} + +hybbx_result_t hybbx_ws_tls_ensure_certs(const char *cert_dir) +{ + char cert_path[HYBBX_PATH_MAX]; + char key_path[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (cert_dir == NULL || cert_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + rc = mkdir_cert_dir(cert_dir); + if (rc != HYBBX_OK) { + return rc; + } + + build_cert_path(cert_dir, HYBBX_WS_TLS_CERT_FILENAME, cert_path, + sizeof(cert_path)); + build_cert_path(cert_dir, HYBBX_WS_TLS_KEY_FILENAME, key_path, + sizeof(key_path)); + + if (cert_files_exist(cert_path, key_path)) { + return HYBBX_OK; + } + + rc = generate_self_signed(cert_path, key_path); + if (rc == HYBBX_OK) { + hybbx_log_info("[websocket] created self-signed TLS certificate in %s", + cert_dir); + } + + return rc; +} + +hybbx_result_t hybbx_ws_tls_server_start(const char *cert_dir) +{ + char cert_path[HYBBX_PATH_MAX]; + char key_path[HYBBX_PATH_MAX]; + + if (g_tls_enabled) { + return HYBBX_OK; + } + + if (cert_dir == NULL || cert_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + build_cert_path(cert_dir, HYBBX_WS_TLS_CERT_FILENAME, cert_path, + sizeof(cert_path)); + build_cert_path(cert_dir, HYBBX_WS_TLS_KEY_FILENAME, key_path, + sizeof(key_path)); + + if (!cert_files_exist(cert_path, key_path)) { + return HYBBX_ERR_IO; + } + + if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | + OPENSSL_INIT_LOAD_CRYPTO_STRINGS, + NULL) != 1) { + return HYBBX_ERR_IO; + } + + g_ssl_ctx = SSL_CTX_new(TLS_server_method()); + if (g_ssl_ctx == NULL) { + return HYBBX_ERR_IO; + } + + SSL_CTX_set_min_proto_version(g_ssl_ctx, TLS1_2_VERSION); + + if (SSL_CTX_use_certificate_file(g_ssl_ctx, cert_path, SSL_FILETYPE_PEM) != + 1 || + SSL_CTX_use_PrivateKey_file(g_ssl_ctx, key_path, SSL_FILETYPE_PEM) != + 1 || + SSL_CTX_check_private_key(g_ssl_ctx) != 1) { + SSL_CTX_free(g_ssl_ctx); + g_ssl_ctx = NULL; + return HYBBX_ERR_IO; + } + + g_tls_enabled = 1; + return HYBBX_OK; +} + +void hybbx_ws_tls_server_stop(void) +{ + if (g_ssl_ctx != NULL) { + SSL_CTX_free(g_ssl_ctx); + g_ssl_ctx = NULL; + } + g_tls_enabled = 0; +} + +int hybbx_ws_tls_server_enabled(void) +{ + return g_tls_enabled; +} + +void *hybbx_ws_tls_accept(int fd) +{ + SSL *ssl; + + if (!g_tls_enabled || g_ssl_ctx == NULL || fd < 0) { + return NULL; + } + + ssl = SSL_new(g_ssl_ctx); + if (ssl == NULL) { + return NULL; + } + + if (SSL_set_fd(ssl, fd) != 1 || SSL_accept(ssl) <= 0) { + SSL_free(ssl); + return NULL; + } + + return ssl; +} + +ssize_t hybbx_ws_tls_recv(void *tls, void *buf, size_t len) +{ + SSL *ssl = (SSL *)tls; + int n; + + if (ssl == NULL || buf == NULL || len == 0) { + return -1; + } + + do { + n = SSL_read(ssl, buf, (int)len); + } while (n < 0 && SSL_get_error(ssl, n) == SSL_ERROR_SYSCALL && + errno == EINTR); + + return (ssize_t)n; +} + +ssize_t hybbx_ws_tls_send(void *tls, const void *buf, size_t len) +{ + const uint8_t *p = (const uint8_t *)buf; + SSL *ssl = (SSL *)tls; + size_t sent = 0; + + if (ssl == NULL || buf == NULL) { + return -1; + } + + while (sent < len) { + int n; + + do { + n = SSL_write(ssl, p + sent, (int)(len - sent)); + } while (n < 0 && SSL_get_error(ssl, n) == SSL_ERROR_SYSCALL && + errno == EINTR); + + if (n <= 0) { + return -1; + } + sent += (size_t)n; + } + + return (ssize_t)sent; +} + +void hybbx_ws_tls_shutdown(void *tls) +{ + SSL *ssl = (SSL *)tls; + + if (ssl != NULL) { + (void)SSL_shutdown(ssl); + } +} + +void hybbx_ws_tls_free(void *tls) +{ + SSL *ssl = (SSL *)tls; + + if (ssl != NULL) { + SSL_free(ssl); + } +} + +#else /* !HYBBX_WS_HAVE_TLS */ + +int hybbx_ws_tls_compiled(void) +{ + return 0; +} + +hybbx_result_t hybbx_ws_tls_ensure_certs(const char *cert_dir) +{ + (void)cert_dir; + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_tls_server_start(const char *cert_dir) +{ + (void)cert_dir; + return HYBBX_ERR_UNSUPPORTED; +} + +void hybbx_ws_tls_server_stop(void) +{ +} + +int hybbx_ws_tls_server_enabled(void) +{ + return 0; +} + +void *hybbx_ws_tls_accept(int fd) +{ + (void)fd; + return NULL; +} + +ssize_t hybbx_ws_tls_recv(void *tls, void *buf, size_t len) +{ + (void)tls; + (void)buf; + (void)len; + return -1; +} + +ssize_t hybbx_ws_tls_send(void *tls, const void *buf, size_t len) +{ + (void)tls; + (void)buf; + (void)len; + return -1; +} + +void hybbx_ws_tls_shutdown(void *tls) +{ + (void)tls; +} + +void hybbx_ws_tls_free(void *tls) +{ + (void)tls; +} + +#endif /* HYBBX_WS_HAVE_TLS */ diff --git a/plugins/websocket/ws_tls.h b/plugins/websocket/ws_tls.h new file mode 100644 index 0000000..2615fab --- /dev/null +++ b/plugins/websocket/ws_tls.h @@ -0,0 +1,43 @@ +#ifndef HYBBX_WS_TLS_H +#define HYBBX_WS_TLS_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Return 1 when this build links OpenSSL for WebSocket TLS. */ +int hybbx_ws_tls_compiled(void); + +/** + * Create @p cert_dir if needed and generate a self-signed certificate on first + * start when files are missing. No-op when OpenSSL is not compiled in. + */ +hybbx_result_t hybbx_ws_tls_ensure_certs(const char *cert_dir); + +/** Load server TLS context after @p cert_dir is ready. */ +hybbx_result_t hybbx_ws_tls_server_start(const char *cert_dir); + +void hybbx_ws_tls_server_stop(void); + +/** Return 1 when the server accepts TLS (OpenSSL + certs loaded). */ +int hybbx_ws_tls_server_enabled(void); + +/** Perform TLS server handshake on @p fd; returns opaque SSL* or NULL. */ +void *hybbx_ws_tls_accept(int fd); + +ssize_t hybbx_ws_tls_recv(void *tls, void *buf, size_t len); +ssize_t hybbx_ws_tls_send(void *tls, const void *buf, size_t len); + +void hybbx_ws_tls_shutdown(void *tls); +void hybbx_ws_tls_free(void *tls); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_WS_TLS_H */ -- cgit v1.3.1