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/ws_proto.c | 565 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 565 insertions(+) create mode 100644 plugins/websocket/ws_proto.c (limited to 'plugins/websocket/ws_proto.c') 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; +} -- cgit v1.3.1