Mercurial
view seobeo/s_websocket.c @ 212:84826b3c655b
[MrJuneJune] Forgot to add assets.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 21:38:36 -0800 |
| parents | 0face9898d04 |
| children |
line wrap: on
line source
#include "seobeo/seobeo.h" #include <time.h> #define SEOBEO_WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" static void Seobeo_WebSocket_Parse_Url(const char *url, char **p_host, char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena) { if (!url) return; const char *start = url; *p_use_tls = FALSE; if (strncmp(url, "wss://", 6) == 0) { *p_use_tls = TRUE; start = url + 6; } else if (strncmp(url, "ws://", 5) == 0) { *p_use_tls = FALSE; start = url + 5; } const char *slash = strchr(start, '/'); const char *colon = strchr(start, ':'); if (colon && (!slash || colon < slash)) { size_t host_len = colon - start; *p_host = Dowa_Arena_Allocate(p_arena, host_len + 1); memcpy(*p_host, start, host_len); (*p_host)[host_len] = '\0'; const char *port_start = colon + 1; size_t port_len = slash ? (slash - port_start) : strlen(port_start); *p_port = Dowa_Arena_Allocate(p_arena, port_len + 1); memcpy(*p_port, port_start, port_len); (*p_port)[port_len] = '\0'; } else { size_t host_len = slash ? (slash - start) : strlen(start); *p_host = Dowa_Arena_Allocate(p_arena, host_len + 1); memcpy(*p_host, start, host_len); (*p_host)[host_len] = '\0'; *p_port = Dowa_Arena_Allocate(p_arena, 8); strcpy(*p_port, *p_use_tls ? "443" : "80"); } if (slash) { size_t path_len = strlen(slash); *p_path = Dowa_Arena_Allocate(p_arena, path_len + 1); strcpy(*p_path, slash); } else { *p_path = Dowa_Arena_Allocate(p_arena, 2); strcpy(*p_path, "/"); } } static void Seobeo_WebSocket_Generate_Key(char *key_out, size_t key_size) { static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; srand((unsigned int)time(NULL) ^ (unsigned int)getpid()); uint8 random_bytes[16]; for (int i = 0; i < 16; i++) random_bytes[i] = (uint8)(rand() % 256); int out_idx = 0; for (int i = 0; i < 16; i += 3) { uint32 triple = (random_bytes[i] << 16) | (i + 1 < 16 ? random_bytes[i + 1] << 8 : 0) | (i + 2 < 16 ? random_bytes[i + 2] : 0); key_out[out_idx++] = base64_chars[(triple >> 18) & 0x3F]; key_out[out_idx++] = base64_chars[(triple >> 12) & 0x3F]; key_out[out_idx++] = base64_chars[(triple >> 6) & 0x3F]; key_out[out_idx++] = base64_chars[triple & 0x3F]; } key_out[out_idx] = '\0'; } Seobeo_WebSocket *Seobeo_WebSocket_Connect_With_Headers(const char *url, const char *headers) { Seobeo_WebSocket *p_ws = malloc(sizeof(Seobeo_WebSocket)); if (!p_ws) return NULL; memset(p_ws, 0, sizeof(Seobeo_WebSocket)); p_ws->p_arena = Dowa_Arena_Create(1024 * 1024); if (!p_ws->p_arena) { free(p_ws); return NULL; } size_t url_len = strlen(url); p_ws->url = Dowa_Arena_Allocate(p_ws->p_arena, url_len + 1); strcpy(p_ws->url, url); Seobeo_WebSocket_Parse_Url(url, &p_ws->host, &p_ws->port, &p_ws->path, &p_ws->use_tls, p_ws->p_arena); p_ws->p_handle = Seobeo_Stream_Handle_Client_Create(p_ws->host, p_ws->port, p_ws->use_tls); if (!p_ws->p_handle || p_ws->p_handle->socket < 0) { Seobeo_Log(SEOBEO_ERROR, "Failed to create socket connection\n"); if (p_ws->p_handle) Seobeo_Handle_Destroy(p_ws->p_handle); Dowa_Arena_Free(p_ws->p_arena); free(p_ws); return NULL; } char ws_key[32]; Seobeo_WebSocket_Generate_Key(ws_key, sizeof(ws_key)); // Build handshake with optional custom headers char handshake[4096]; int handshake_len = snprintf(handshake, sizeof(handshake), "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: %s\r\n" "Sec-WebSocket-Version: 13\r\n", p_ws->path, p_ws->host, ws_key); if (headers && strlen(headers) > 0) { const char *line_start = headers; while (*line_start) { while (*line_start == ' ' || *line_start == '\t') line_start++; if (*line_start == '\0') break; const char *line_end = line_start; while (*line_end && *line_end != '\n') line_end++; size_t line_len = line_end - line_start; if (line_len > 0 && memchr(line_start, ':', line_len) != NULL) { while (line_len > 0 && (line_start[line_len-1] == '\r' || line_start[line_len-1] == ' ' || line_start[line_len-1] == '\t')) line_len--; if (line_len > 0 && handshake_len + line_len + 4 < sizeof(handshake)) { memcpy(handshake + handshake_len, line_start, line_len); handshake_len += line_len; handshake[handshake_len++] = '\r'; handshake[handshake_len++] = '\n'; } } line_start = (*line_end == '\n') ? line_end + 1 : line_end; } } handshake[handshake_len++] = '\r'; handshake[handshake_len++] = '\n'; handshake[handshake_len] = '\0'; Seobeo_Handle_Queue(p_ws->p_handle, (uint8*)handshake, (uint32)handshake_len); if (Seobeo_Handle_Flush(p_ws->p_handle) < 0) { Seobeo_Log(SEOBEO_ERROR, "Failed to send WebSocket handshake\n"); Seobeo_Handle_Destroy(p_ws->p_handle); Dowa_Arena_Free(p_ws->p_arena); free(p_ws); return NULL; } while (1) { int r = Seobeo_Handle_Read(p_ws->p_handle); if (r < 0) { Seobeo_Log(SEOBEO_ERROR, "Failed to read handshake response\n"); Seobeo_Handle_Destroy(p_ws->p_handle); Dowa_Arena_Free(p_ws->p_arena); free(p_ws); return NULL; } if (p_ws->p_handle->read_buffer_len >= 4 && strstr((char*)p_ws->p_handle->read_buffer, "\r\n\r\n") != NULL) break; if (r == 0) continue; } char *response = (char*)p_ws->p_handle->read_buffer; if (strstr(response, "HTTP/1.1 101") == NULL) { Seobeo_Log(SEOBEO_ERROR, "WebSocket handshake failed: %s\n", response); Seobeo_Handle_Destroy(p_ws->p_handle); Dowa_Arena_Free(p_ws->p_arena); free(p_ws); return NULL; } char *end_of_headers = strstr(response, "\r\n\r\n"); if (end_of_headers) { size_t header_len = end_of_headers - response + 4; Seobeo_Handle_Consume(p_ws->p_handle, (uint32)header_len); } p_ws->state = SEOBEO_WS_STATE_OPEN; p_ws->fragment_capacity = 4096; p_ws->fragment_buffer = malloc(p_ws->fragment_capacity); Seobeo_Log(SEOBEO_INFO, "WebSocket connected to %s\n", url); return p_ws; } Seobeo_WebSocket *Seobeo_WebSocket_Connect(const char *url) { return Seobeo_WebSocket_Connect_With_Headers(url, NULL); } static int32 Seobeo_WebSocket_Send_Frame(Seobeo_WebSocket *p_ws, Seobeo_WebSocket_Opcode opcode, const uint8 *payload, size_t payload_length, boolean fin) { if (!p_ws || p_ws->state != SEOBEO_WS_STATE_OPEN) return -1; uint8 frame[14]; size_t frame_len = 0; // Big endian frame[0] = (fin ? 0x80 : 0x00) | (opcode & 0x0F); frame_len++; uint8 mask_key[4]; for (int i = 0; i < 4; i++) mask_key[i] = (uint8)(rand() % 256); // within 1 byte if (payload_length < 126) { frame[1] = 0x80 | (uint8)payload_length; frame_len++; } // from now frame 1 is thrown away just keeping it 1 // within 4 bytes else if (payload_length < MAX_INT_16) { frame[1] = 0x80 | 126; frame[2] = (uint8)((payload_length >> 8) & 0xFF); frame[3] = (uint8)(payload_length & 0xFF); frame_len += 3; } // within 8 bytes else { frame[1] = 0x80 | 127; for (int i = 0; i < 8; i++) frame[2 + i] = (uint8)((payload_length >> (56 - i * 8)) & 0xFF); frame_len += 9; } memcpy(frame + frame_len, mask_key, 4); frame_len += 4; Seobeo_Handle_Queue(p_ws->p_handle, frame, (uint32)frame_len); if (payload_length > 0) { uint8 *masked_payload = malloc(payload_length); memcpy(masked_payload, payload, payload_length); Seobeo_WebSocket_Mask_Data(masked_payload, payload_length, mask_key); Seobeo_Handle_Queue(p_ws->p_handle, masked_payload, (uint32)payload_length); free(masked_payload); } return Seobeo_Handle_Flush(p_ws->p_handle); } static int32 Seobeo_WebSocket_Send_Fragmented(Seobeo_WebSocket *p_ws, Seobeo_WebSocket_Opcode opcode, const uint8 *payload, size_t total_length) { if (!payload || total_length == 0) return -1; if (total_length <= MAX_FRAGMENT_SIZE) return Seobeo_WebSocket_Send_Frame(p_ws, opcode, payload, total_length, TRUE); size_t sent = 0; int32 result; result = Seobeo_WebSocket_Send_Frame(p_ws, opcode, payload, MAX_FRAGMENT_SIZE, FALSE); if (result < 0) return result; sent += MAX_FRAGMENT_SIZE; while (sent + MAX_FRAGMENT_SIZE < total_length) { result = Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_CONTINUATION, payload + sent, MAX_FRAGMENT_SIZE, FALSE); if (result < 0) return result; sent += MAX_FRAGMENT_SIZE; } size_t remaining = total_length - sent; return Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_CONTINUATION, payload + sent, remaining, TRUE); } int32 Seobeo_WebSocket_Send_Text(Seobeo_WebSocket *p_ws, const char *text) { if (!text) return -1; return Seobeo_WebSocket_Send_Fragmented(p_ws, SEOBEO_WS_OPCODE_TEXT, (const uint8*)text, strlen(text)); } int32 Seobeo_WebSocket_Send_Binary(Seobeo_WebSocket *p_ws, const uint8 *data, size_t length) { if (!data) return -1; return Seobeo_WebSocket_Send_Fragmented(p_ws, SEOBEO_WS_OPCODE_BINARY, data, length); } int32 Seobeo_WebSocket_Send_Ping(Seobeo_WebSocket *p_ws, const char *payload) { size_t len = payload ? strlen(payload) : 0; return Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_PING, (const uint8*)payload, len, TRUE); } int32 Seobeo_WebSocket_Send_Pong(Seobeo_WebSocket *p_ws, const char *payload) { size_t len = payload ? strlen(payload) : 0; return Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_PONG, (const uint8*)payload, len, TRUE); } Seobeo_WebSocket_Message *Seobeo_WebSocket_Receive(Seobeo_WebSocket *p_ws) { if (!p_ws || p_ws->state == SEOBEO_WS_STATE_CLOSED) return NULL; int r = Seobeo_Handle_Read(p_ws->p_handle); if (r < 0) { Seobeo_Log(SEOBEO_ERROR, "WebSocket read error\n"); p_ws->state = SEOBEO_WS_STATE_CLOSED; return NULL; } if (r == -2) { Seobeo_Log(SEOBEO_INFO, "WebSocket connection closed\n"); p_ws->state = SEOBEO_WS_STATE_CLOSED; return NULL; } if (p_ws->p_handle->read_buffer_len < 2) return NULL; uint8 *buf = p_ws->p_handle->read_buffer; uint8 byte1 = buf[0]; uint8 byte2 = buf[1]; boolean fin = (byte1 & 0x80) != 0; Seobeo_WebSocket_Opcode opcode = (Seobeo_WebSocket_Opcode)(byte1 & 0x0F); boolean masked = (byte2 & 0x80) != 0; uint64 payload_len = byte2 & 0x7F; size_t header_len = 2; if (payload_len == 126) { if (p_ws->p_handle->read_buffer_len < 4) return NULL; payload_len = (buf[2] << 8) | buf[3]; header_len += 2; } else if (payload_len == 127) { if (p_ws->p_handle->read_buffer_len < 10) return NULL; payload_len = 0; for (int i = 0; i < 8; i++) payload_len = (payload_len << 8) | buf[2 + i]; header_len += 8; } uint8 mask_key[4] = {0}; if (masked) { if (p_ws->p_handle->read_buffer_len < header_len + 4) return NULL; memcpy(mask_key, buf + header_len, 4); header_len += 4; } if (p_ws->p_handle->read_buffer_len < header_len + payload_len) return NULL; uint8 *payload = NULL; if (payload_len > 0) { payload = malloc(payload_len); memcpy(payload, buf + header_len, payload_len); if (masked) Seobeo_WebSocket_Mask_Data(payload, payload_len, mask_key); } Seobeo_Handle_Consume(p_ws->p_handle, (uint32)(header_len + payload_len)); if (opcode == SEOBEO_WS_OPCODE_PING) { Seobeo_WebSocket_Send_Pong(p_ws, (char*)payload); if (payload) free(payload); return NULL; } if (opcode == SEOBEO_WS_OPCODE_PONG) { if (payload) free(payload); return NULL; } if (opcode == SEOBEO_WS_OPCODE_CLOSE) { uint16 close_code = 1000; if (payload_len >= 2) close_code = (payload[0] << 8) | payload[1]; Seobeo_Log(SEOBEO_INFO, "WebSocket close received with code %d\n", close_code); p_ws->state = SEOBEO_WS_STATE_CLOSING; Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_CLOSE, payload, payload_len, TRUE); p_ws->state = SEOBEO_WS_STATE_CLOSED; if (payload) free(payload); return NULL; } if (opcode == SEOBEO_WS_OPCODE_CONTINUATION) { if (p_ws->fragment_length + payload_len > p_ws->fragment_capacity) { p_ws->fragment_capacity = (p_ws->fragment_length + payload_len) * 2; p_ws->fragment_buffer = realloc(p_ws->fragment_buffer, p_ws->fragment_capacity); } if (payload_len > 0) { memcpy(p_ws->fragment_buffer + p_ws->fragment_length, payload, payload_len); p_ws->fragment_length += payload_len; } if (payload) free(payload); if (!fin) return NULL; Seobeo_WebSocket_Message *p_msg = malloc(sizeof(Seobeo_WebSocket_Message)); p_msg->opcode = p_ws->fragment_opcode; p_msg->data = malloc(p_ws->fragment_length); memcpy(p_msg->data, p_ws->fragment_buffer, p_ws->fragment_length); p_msg->length = p_ws->fragment_length; p_msg->is_final = TRUE; p_ws->fragment_length = 0; return p_msg; } if (!fin) { p_ws->fragment_opcode = opcode; p_ws->fragment_length = 0; if (payload_len > 0) { if (payload_len > p_ws->fragment_capacity) { p_ws->fragment_capacity = payload_len * 2; p_ws->fragment_buffer = realloc(p_ws->fragment_buffer, p_ws->fragment_capacity); } memcpy(p_ws->fragment_buffer, payload, payload_len); p_ws->fragment_length = payload_len; } if (payload) free(payload); return NULL; } Seobeo_WebSocket_Message *p_msg = malloc(sizeof(Seobeo_WebSocket_Message)); p_msg->opcode = opcode; p_msg->data = payload; p_msg->length = payload_len; p_msg->is_final = fin; return p_msg; } int32 Seobeo_WebSocket_Close(Seobeo_WebSocket *p_ws, uint16 code, const char *reason) { if (!p_ws || p_ws->state == SEOBEO_WS_STATE_CLOSED) return -1; p_ws->state = SEOBEO_WS_STATE_CLOSING; size_t reason_len = reason ? strlen(reason) : 0; size_t payload_len = 2 + reason_len; uint8 *payload = malloc(payload_len); payload[0] = (uint8)((code >> 8) & 0xFF); payload[1] = (uint8)(code & 0xFF); if (reason_len > 0) memcpy(payload + 2, reason, reason_len); int32 result = Seobeo_WebSocket_Send_Frame(p_ws, SEOBEO_WS_OPCODE_CLOSE, payload, payload_len, TRUE); free(payload); p_ws->state = SEOBEO_WS_STATE_CLOSED; return result; } void Seobeo_WebSocket_Destroy(Seobeo_WebSocket *p_ws) { if (!p_ws) return; if (p_ws->state == SEOBEO_WS_STATE_OPEN) Seobeo_WebSocket_Close(p_ws, 1000, "Normal closure"); if (p_ws->p_handle) Seobeo_Handle_Destroy(p_ws->p_handle); if (p_ws->fragment_buffer) free(p_ws->fragment_buffer); if (p_ws->p_arena) Dowa_Arena_Free(p_ws->p_arena); free(p_ws); }