summaryrefslogtreecommitdiff
path: root/plugins/websocket
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
commit20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch)
tree2857f41513a56ad41af97b57362639298aa7f033 /plugins/websocket
#2
Diffstat (limited to 'plugins/websocket')
-rw-r--r--plugins/websocket/CMakeLists.txt37
-rw-r--r--plugins/websocket/websocket.c723
-rw-r--r--plugins/websocket/ws_config.c163
-rw-r--r--plugins/websocket/ws_proto.c565
-rw-r--r--plugins/websocket/ws_proto.h63
-rw-r--r--plugins/websocket/ws_sha1.c138
-rw-r--r--plugins/websocket/ws_sha1.h9
-rw-r--r--plugins/websocket/ws_tls.c400
-rw-r--r--plugins/websocket/ws_tls.h43
9 files changed, 2141 insertions, 0 deletions
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 <arpa/inet.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <poll.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#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 <stdlib.h>
+#include <string.h>
+
+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 <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#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 <stddef.h>
+#include <stdint.h>
+
+#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 <stdint.h>
+#include <string.h>
+#include <string.h>
+
+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 <stddef.h>
+#include <stdint.h>
+
+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 <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#ifdef HYBBX_WS_HAVE_TLS
+
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+#include <openssl/ssl.h>
+#include <openssl/x509.h>
+
+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 <stddef.h>
+#include <sys/types.h>
+
+#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 */
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com