diff seobeo/s_websocket_server.c @ 121:7b1719fa918c

[Seobeo] Added web socket server.
author June Park <parkjune1995@gmail.com>
date Thu, 08 Jan 2026 06:45:10 -0800
parents
children f236c895604e
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/s_websocket_server.c	Thu Jan 08 06:45:10 2026 -0800
@@ -0,0 +1,498 @@
+#include "seobeo/seobeo.h"
+#include <time.h>
+
+#ifndef SEOBEO_NO_SSL
+#include <openssl/sha.h>
+#include <openssl/bio.h>
+#include <openssl/evp.h>
+#include <openssl/buffer.h>
+#endif
+
+#define SEOBEO_WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+static Seobeo_WebSocket_Server_Route *g_ws_routes = NULL;
+static Seobeo_WebSocket_Server_Connection *g_ws_connections = NULL;
+
+void Seobeo_WebSocket_Server_Init()
+{
+  Dowa_Array_Reserve(g_ws_routes, 10);
+}
+
+void Seobeo_WebSocket_Server_Register(const char *path, Seobeo_WebSocket_Server_Handler handler, void *p_user_data)
+{
+  Seobeo_WebSocket_Server_Route route = {0};
+  route.path = strdup(path);
+  route.handler = handler;
+  route.p_user_data = p_user_data;
+
+  Dowa_Array_Push(g_ws_routes, route);
+}
+
+static void Seobeo_WebSocket_Server_Compute_Accept_Key(const char *client_key, char *accept_key, size_t accept_key_size)
+{
+#ifdef SEOBEO_NO_SSL
+  snprintf(accept_key, accept_key_size, "dGhlIHNhbXBsZSBub25jZQ==");
+  (void)client_key;
+#else
+  char concatenated[256];
+  snprintf(concatenated, sizeof(concatenated), "%s%s", client_key, SEOBEO_WS_GUID);
+
+  unsigned char hash[SHA_DIGEST_LENGTH];
+  SHA1((unsigned char*)concatenated, strlen(concatenated), hash);
+
+  BIO *b64 = BIO_new(BIO_f_base64());
+  BIO *bio = BIO_new(BIO_s_mem());
+  bio = BIO_push(b64, bio);
+  BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
+  BIO_write(bio, hash, SHA_DIGEST_LENGTH);
+  BIO_flush(bio);
+
+  BUF_MEM *buffer_ptr;
+  BIO_get_mem_ptr(bio, &buffer_ptr);
+
+  size_t copy_len = buffer_ptr->length < accept_key_size - 1 ? buffer_ptr->length : accept_key_size - 1;
+  memcpy(accept_key, buffer_ptr->data, copy_len);
+  accept_key[copy_len] = '\0';
+
+  BIO_free_all(bio);
+#endif
+}
+
+boolean Seobeo_WebSocket_Server_Handle_Upgrade(Seobeo_Handle *p_handle, Seobeo_Request_Entry *p_req_map, const char *path)
+{
+  void *p_upgrade_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Upgrade");
+  if (!p_upgrade_kv)
+    return FALSE;
+
+  const char *upgrade_value = ((Seobeo_Request_Entry*)p_upgrade_kv)->value;
+  if (strcasecmp(upgrade_value, "websocket") != 0)
+    return FALSE;
+
+  void *p_connection_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Connection");
+  if (!p_connection_kv)
+    return FALSE;
+
+  const char *connection_value = ((Seobeo_Request_Entry*)p_connection_kv)->value;
+  if (!strstr(connection_value, "Upgrade"))
+    return FALSE;
+
+  void *p_key_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Sec-WebSocket-Key");
+  if (!p_key_kv)
+    return FALSE;
+
+  const char *client_key = ((Seobeo_Request_Entry*)p_key_kv)->value;
+
+  char accept_key[64];
+  Seobeo_WebSocket_Server_Compute_Accept_Key(client_key, accept_key, sizeof(accept_key));
+
+  char response[512];
+  int response_len = 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_key);
+
+  Seobeo_Handle_Queue(p_handle, (uint8*)response, (uint32)response_len);
+  if (Seobeo_Handle_Flush(p_handle) < 0)
+    return FALSE;
+
+  Seobeo_WebSocket_Server_Connection *p_conn = malloc(sizeof(Seobeo_WebSocket_Server_Connection));
+  memset(p_conn, 0, sizeof(Seobeo_WebSocket_Server_Connection));
+
+  p_conn->p_handle = p_handle;
+  p_conn->is_active = TRUE;
+  p_conn->fragment_capacity = 4096;
+  p_conn->fragment_buffer = malloc(p_conn->fragment_capacity);
+
+  char client_id[64];
+  snprintf(client_id, sizeof(client_id), "client_%p", (void*)p_handle);
+  p_conn->client_id = strdup(client_id);
+
+  p_conn->next = g_ws_connections;
+  g_ws_connections = p_conn;
+
+  Seobeo_Log(SEOBEO_INFO, "WebSocket upgraded on path: %s\n", path);
+
+  Seobeo_WebSocket_Server_Handle_Connection(p_conn);
+
+  return TRUE;
+}
+
+static void Seobeo_WebSocket_Unmask_Data(uint8 *data, size_t length, const uint8 *mask)
+{
+  for (size_t i = 0; i < length; i++)
+    data[i] ^= mask[i % 4];
+}
+
+static int32 Seobeo_WebSocket_Server_Send_Frame(Seobeo_WebSocket_Server_Connection *p_conn, Seobeo_WebSocket_Opcode opcode, const uint8 *payload, size_t payload_length, boolean fin)
+{
+  if (!p_conn || !p_conn->is_active)
+    return -1;
+
+  uint8 frame[14];
+  size_t frame_len = 0;
+
+  frame[0] = (fin ? 0x80 : 0x00) | (opcode & 0x0F);
+  frame_len++;
+
+  // within 1 byte, so max chunk would be 125 bytes 
+  if (payload_length < 126)
+  {
+    frame[1] = (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] = 126;
+    frame[2] = (uint8)((payload_length >> 8) & 0xFF);
+    frame[3] = (uint8)(payload_length & 0xFF);
+    frame_len += 3;
+  }
+  else
+  {
+    frame[1] = 127;
+    for (int i = 0; i < 8; i++)
+      frame[2 + i] = (uint8)((payload_length >> (56 - i * 8)) & 0xFF);
+    frame_len += 9;
+  }
+
+  Seobeo_Handle_Queue(p_conn->p_handle, frame, (uint32)frame_len);
+
+  if (payload_length > 0)
+    Seobeo_Handle_Queue(p_conn->p_handle, payload, (uint32)payload_length);
+
+  return Seobeo_Handle_Flush(p_conn->p_handle);
+}
+
+static int32 Seobeo_WebSocket_Server_Send_Fragmented(Seobeo_WebSocket_Server_Connection *p_conn, 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_Server_Send_Frame(p_conn, opcode, payload, total_length, TRUE);
+
+  size_t sent = 0;
+  int32 result;
+
+  result = Seobeo_WebSocket_Server_Send_Frame(p_conn, 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_Server_Send_Frame(p_conn, 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_Server_Send_Frame(p_conn, SEOBEO_WS_OPCODE_CONTINUATION, payload + sent, remaining, TRUE);
+}
+
+int32 Seobeo_WebSocket_Server_Send_Text(Seobeo_WebSocket_Server_Connection *p_conn, const char *text)
+{
+  if (!text)
+    return -1;
+
+  return Seobeo_WebSocket_Server_Send_Fragmented(p_conn, SEOBEO_WS_OPCODE_TEXT, (const uint8*)text, strlen(text));
+}
+
+int32 Seobeo_WebSocket_Server_Send_Binary(Seobeo_WebSocket_Server_Connection *p_conn, const uint8 *data, size_t length)
+{
+  if (!data)
+    return -1;
+
+  return Seobeo_WebSocket_Server_Send_Fragmented(p_conn, SEOBEO_WS_OPCODE_BINARY, data, length);
+}
+
+static Seobeo_WebSocket_Message *Seobeo_WebSocket_Server_Receive_Frame(Seobeo_WebSocket_Server_Connection *p_conn)
+{
+  if (!p_conn || !p_conn->is_active)
+    return NULL;
+
+  int r = Seobeo_Handle_Read(p_conn->p_handle);
+  if (r < 0)
+  {
+    Seobeo_Log(SEOBEO_ERROR, "WebSocket server read error\n");
+    p_conn->is_active = FALSE;
+    return NULL;
+  }
+
+  if (r == -2)
+  {
+    Seobeo_Log(SEOBEO_INFO, "WebSocket client disconnected\n");
+    p_conn->is_active = FALSE;
+    return NULL;
+  }
+
+  if (p_conn->p_handle->read_buffer_len < 2)
+    return NULL;
+
+  uint8 *buf = p_conn->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_conn->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_conn->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_conn->p_handle->read_buffer_len < header_len + 4)
+      return NULL;
+    memcpy(mask_key, buf + header_len, 4);
+    header_len += 4;
+  }
+
+  if (p_conn->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_Unmask_Data(payload, payload_len, mask_key);
+  }
+
+  Seobeo_Handle_Consume(p_conn->p_handle, (uint32)(header_len + payload_len));
+
+  if (opcode == SEOBEO_WS_OPCODE_PING)
+  {
+    Seobeo_WebSocket_Server_Send_Frame(p_conn, SEOBEO_WS_OPCODE_PONG, payload, payload_len, TRUE);
+    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 from client with code %d\n", close_code);
+
+    Seobeo_WebSocket_Server_Send_Frame(p_conn, SEOBEO_WS_OPCODE_CLOSE, payload, payload_len, TRUE);
+    p_conn->is_active = FALSE;
+
+    if (payload)
+      free(payload);
+    return NULL;
+  }
+
+  if (opcode == SEOBEO_WS_OPCODE_CONTINUATION)
+  {
+    if (p_conn->fragment_length + payload_len > p_conn->fragment_capacity)
+    {
+      p_conn->fragment_capacity = (p_conn->fragment_length + payload_len) * 2;
+      p_conn->fragment_buffer = realloc(p_conn->fragment_buffer, p_conn->fragment_capacity);
+    }
+
+    if (payload_len > 0)
+    {
+      memcpy(p_conn->fragment_buffer + p_conn->fragment_length, payload, payload_len);
+      p_conn->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_conn->fragment_opcode;
+    p_msg->data = malloc(p_conn->fragment_length);
+    memcpy(p_msg->data, p_conn->fragment_buffer, p_conn->fragment_length);
+    p_msg->length = p_conn->fragment_length;
+    p_msg->is_final = TRUE;
+
+    p_conn->fragment_length = 0;
+
+    return p_msg;
+  }
+
+  if (!fin)
+  {
+    p_conn->fragment_opcode = opcode;
+    p_conn->fragment_length = 0;
+
+    if (payload_len > 0)
+    {
+      if (payload_len > p_conn->fragment_capacity)
+      {
+        p_conn->fragment_capacity = payload_len * 2;
+        p_conn->fragment_buffer = realloc(p_conn->fragment_buffer, p_conn->fragment_capacity);
+      }
+
+      memcpy(p_conn->fragment_buffer, payload, payload_len);
+      p_conn->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;
+}
+
+void Seobeo_WebSocket_Server_Handle_Connection(Seobeo_WebSocket_Server_Connection *p_conn)
+{
+  if (!g_ws_routes)
+    return;
+
+  size_t route_count = Dowa_Array_Length(g_ws_routes);
+  if (route_count == 0)
+    return;
+
+  Seobeo_WebSocket_Server_Handler handler = g_ws_routes[0].handler;
+  void *p_user_data = g_ws_routes[0].p_user_data;
+
+  while (p_conn->is_active)
+  {
+    Seobeo_WebSocket_Message *p_msg = Seobeo_WebSocket_Server_Receive_Frame(p_conn);
+    if (p_msg)
+    {
+      if (handler)
+        handler(p_conn, p_msg, p_user_data);
+
+      Seobeo_WebSocket_Message_Destroy(p_msg);
+    }
+
+    usleep(1000);
+  }
+
+  Seobeo_WebSocket_Server_Connection_Destroy(p_conn);
+}
+
+void Seobeo_WebSocket_Server_Broadcast_Text(const char *text)
+{
+  if (!text)
+    return;
+
+  Seobeo_WebSocket_Server_Connection *p_conn = g_ws_connections;
+  while (p_conn)
+  {
+    if (p_conn->is_active)
+      Seobeo_WebSocket_Server_Send_Text(p_conn, text);
+
+    p_conn = p_conn->next;
+  }
+}
+
+void Seobeo_WebSocket_Server_Broadcast_Binary(const uint8 *data, size_t length)
+{
+  if (!data)
+    return;
+
+  Seobeo_WebSocket_Server_Connection *p_conn = g_ws_connections;
+  while (p_conn)
+  {
+    if (p_conn->is_active)
+      Seobeo_WebSocket_Server_Send_Binary(p_conn, data, length);
+
+    p_conn = p_conn->next;
+  }
+}
+
+void Seobeo_WebSocket_Server_Connection_Close(Seobeo_WebSocket_Server_Connection *p_conn, uint16 code, const char *reason)
+{
+  if (!p_conn || !p_conn->is_active)
+    return;
+
+  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);
+
+  Seobeo_WebSocket_Server_Send_Frame(p_conn, SEOBEO_WS_OPCODE_CLOSE, payload, payload_len, TRUE);
+
+  free(payload);
+
+  p_conn->is_active = FALSE;
+}
+
+void Seobeo_WebSocket_Server_Connection_Destroy(Seobeo_WebSocket_Server_Connection *p_conn)
+{
+  if (!p_conn)
+    return;
+
+  if (p_conn->is_active)
+    Seobeo_WebSocket_Server_Connection_Close(p_conn, 1000, "Server closing connection");
+
+  if (p_conn->p_handle)
+    Seobeo_Handle_Destroy(p_conn->p_handle);
+
+  if (p_conn->client_id)
+    free(p_conn->client_id);
+
+  if (p_conn->fragment_buffer)
+    free(p_conn->fragment_buffer);
+
+  Seobeo_WebSocket_Server_Connection **pp = &g_ws_connections;
+  while (*pp)
+  {
+    if (*pp == p_conn)
+    {
+      *pp = p_conn->next;
+      break;
+    }
+    pp = &(*pp)->next;
+  }
+
+  free(p_conn);
+}