view seobeo/s_web.c @ 125:f236c895604e

[MrJuneJune] Added web socket for chat to this.
author June Park <parkjune1995@gmail.com>
date Thu, 08 Jan 2026 08:46:49 -0800
parents dbf14f84d51c
children 7a63e41a21fb
line wrap: on
line source

#include "seobeo/seobeo.h"

static char g_folder_path[512] = ".";

char* Seobeo_Web_LoadFile(const char *file_path, size_t *p_file_size)
{
  char full_path[1024];
  snprintf(full_path, sizeof(full_path), "%s/%s", g_folder_path, file_path);

  FILE *p_file = fopen(full_path, "rb");
  if (!p_file)
    return NULL;

  fseek(p_file, 0, SEEK_END);
  size_t file_size = ftell(p_file);
  fseek(p_file, 0, SEEK_SET);

  char *p_content = (char*)malloc(file_size + 1);
  if (!p_content)
  {
    fclose(p_file);
    return NULL;
  }

  fread(p_content, 1, file_size, p_file);
  p_content[file_size] = '\0';
  fclose(p_file);

  if (p_file_size)
    *p_file_size = file_size;

  return p_content;
}

void Seobeo_Web_Header_Generate(
    void *buffer, int status,
    const char *content_type, const int content_length)
{
  const char *status_text;
  switch(status)
  {
    case HTTP_OK: status_text = "OK"; break;
    case HTTP_CREATED: status_text = "Created"; break;
    case HTTP_MOVED_PERMANENTLY: status_text = "Moved Permanently"; break;
    case HTTP_FOUND: status_text = "Found"; break;
    case HTTP_BAD_REQUEST: status_text = "Bad Request"; break;
    case HTTP_UNAUTHORIZED: status_text = "Unauthorized"; break;
    case HTTP_FORBIDDEN: status_text = "Forbidden"; break;
    case HTTP_NOT_FOUND: status_text = "Not Found"; break;
    case HTTP_INTERNAL_ERROR: status_text = "Internal Server Error"; break;
    default: status_text = "Unknown"; break;
  }
  
  sprintf(
    buffer, 
    "HTTP/1.1 %d %s\r\n"
    "Content-Type: %s\r\n"
    "Content-Length: %d\r\n"
    "Connection: close\r\n"
    "\r\n", 
    status, status_text, content_type, content_length
  );
}

void Seobeo_Web_HandleClientRequest(Seobeo_Handle  *p_cli_handle,
                                    Seobeo_Cache_Entry *p_html_cache)
{
  // TODO: This should be splitted up instead of handling up to 50 MB as it will fail more often... 
  Dowa_Arena *p_request_arena = Dowa_Arena_Create(50*1024*1024); // 50 MB because of files... 
  if (!p_request_arena) { perror("Dowa_Arena_Create request"); goto clean_up; }

  Dowa_Arena *p_response_arena = Dowa_Arena_Create(50*1024*1024); // 50 MB for response
  if (!p_response_arena) { perror("Dowa_Arena_Create response"); goto clean_up; }

  void *p_response_header = Dowa_Arena_Allocate(p_response_arena, 1024*5); // 5Kb
  if (!p_response_header) { perror("Dowa_Arena_Allocate"); goto clean_up; }

  Seobeo_Request_Entry *p_req_map = NULL;
  int parse_result = Seobeo_Web_Header_Parse(p_cli_handle, &p_req_map, p_request_arena);

  if (parse_result != 0 && parse_result != 1)
  {
    Seobeo_Log(SEOBEO_ERROR, "Seobeo_Web_Header_Parse failed with code %d\n", parse_result);
    Seobeo_Web_Header_Generate(p_response_header,
                               HTTP_BAD_REQUEST,
                               "text/plain", 0);
    Seobeo_Handle_Queue(p_cli_handle,
                        (const uint8*)p_response_header,
                        (uint32)strlen(p_response_header));
    Seobeo_Handle_Flush(p_cli_handle);
    goto clean_up;
  }

  Seobeo_Log(SEOBEO_DEBUG, "Parse completed with code %d\n", parse_result);

  // Recording IP to see who is ddosing or any web scrappers...
  void *p_real_ip_kv = Dowa_HashMap_Get_Ptr(p_req_map, "X-Real-IP");
  const char *real_ip = p_real_ip_kv ? ((Seobeo_Request_Entry*)p_real_ip_kv)->value : NULL;
  // Fallback
  if (!real_ip)
  {
    void *p_forwarded_kv = Dowa_HashMap_Get_Ptr(p_req_map, "X-Forwarded-For");
    real_ip = p_forwarded_kv ? ((Seobeo_Request_Entry*)p_forwarded_kv)->value : NULL;
  }
  // Fallback
  if (!real_ip)
    real_ip = p_cli_handle->host;

  void *p_method_kv = Dowa_HashMap_Get_Ptr(p_req_map, "HTTP_Method");
  const char *method = p_method_kv ? ((Seobeo_Request_Entry*)p_method_kv)->value : NULL;

  void *p_path_kv_log = Dowa_HashMap_Get_Ptr(p_req_map, "Path");
  const char *path_log = p_path_kv_log ? ((Seobeo_Request_Entry*)p_path_kv_log)->value : "/";

  Seobeo_Log(SEOBEO_INFO, "%s - %s %s\n",
             real_ip ? real_ip : "unknown",
             method ? method : "UNKNOWN",
             path_log);

  Seobeo_Log(SEOBEO_DEBUG, "Parsed request, method=%s\n", method ? method : "NULL");

  if (!method)
  {
    Seobeo_Log(SEOBEO_ERROR, "No HTTP method found in request\n");
    Seobeo_Web_Header_Generate(p_response_header,
                               HTTP_BAD_REQUEST,
                               "text/plain", 0);
    Seobeo_Handle_Queue(p_cli_handle,
                        (const uint8*)p_response_header,
                        (uint32)strlen(p_response_header));
    Seobeo_Handle_Flush(p_cli_handle);
    goto clean_up;
  }

  void *p_path_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Path");
  const char *path = p_path_kv ? ((Seobeo_Request_Entry*)p_path_kv)->value : "/";

  // --- Check for WebSocket upgrade request ---
  #ifdef SEOBEO_WEBSOCKET_SERVER
  if (Seobeo_WebSocket_Server_Handle_Upgrade(p_cli_handle, p_req_map, path))
  {
    Seobeo_Log(SEOBEO_INFO, "WebSocket connection established\n");
    if (p_request_arena)
      Dowa_Arena_Free(p_request_arena);
    if (p_response_arena)
      Dowa_Arena_Free(p_response_arena);
    return;
  }
  #endif

  // --- Try to match API route first ---
  Seobeo_Route_Handler handler = Seobeo_Router_Find_Handler(method, path, &p_req_map, p_request_arena);
  if (handler != NULL)
  {
    Seobeo_Request_Entry *p_response_map = handler(p_req_map, p_response_arena);
    Seobeo_Router_Send_Response(p_cli_handle, p_response_map, p_response_arena);
    goto clean_up;
  }

  // --- Static files fallback for GET ---
  if (strcmp(method, "GET") == 0)
  {
    void *p_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Path");
    const char *path = p_kv ? ((Seobeo_Request_Entry*)p_kv)->value : NULL;
    char *file_path = Dowa_Arena_Allocate(p_response_arena, (size_t)5 * 1024); // 5Kb only for path

    if (!path || strcmp(path, "/") == 0)
    {
      strcpy(file_path, "index.html");
    }
    else
    {
      size_t L = strlen(path);
      if (path[0] == '/')
      {
        if (strchr(path, '.') == NULL)
          snprintf(file_path, 512, "%.*s/index.html", (int)(L-1), path+1);
        else
          snprintf(file_path, 512, "%.*s", (int)(L-1), path+1);
      }
      else
        strcpy(file_path, path);
    }

    // Check if file is in cache, load if not
    void *p_file_kv = Dowa_HashMap_Get_Ptr(p_html_cache, file_path);
    const char *file_content = NULL;
    size_t body_size = 0;

    if (p_file_kv)
    {
      // File is cached
      file_content = ((Seobeo_Cache_Entry*)p_file_kv)->value;
      body_size = strlen(file_content);
    }
    else
    {
      file_content = Seobeo_Web_LoadFile(file_path, &body_size);
      if (file_content)
        Dowa_HashMap_Push(p_html_cache, file_path, file_content);
    }

    if (!file_content)
    {
      Seobeo_Web_Header_Generate(p_response_header,
                                 HTTP_NOT_FOUND,
                                 "text/html", 0);
      Seobeo_Handle_Queue(p_cli_handle,
                          (const uint8*)p_response_header,
                          (uint32)strlen(p_response_header));
      Seobeo_Handle_Flush(p_cli_handle);
      goto clean_up;
    }

    // Serve static file
    const char *mime = "application/octet-stream";
    if (strstr(file_path, ".html")) mime = "text/html; charset=utf-8";
    else if (strstr(file_path, ".css")) mime = "text/css";
    else if (strstr(file_path, ".js")) mime = "application/javascript";
    else if (strstr(file_path, ".png")) mime = "image/png";
    else if (strstr(file_path, ".jpg") || strstr(file_path, ".jpeg")) mime = "image/jpeg";
    else if (strstr(file_path, ".gif")) mime = "image/gif";
    else if (strstr(file_path, ".svg")) mime = "image/svg+xml";
    else if (strstr(file_path, ".ico")) mime = "image/x-icon";
    else if (strstr(file_path, ".json")) mime = "application/json";
    else if (strstr(file_path, ".wasm")) mime = "application/wasm";

    Seobeo_Log(SEOBEO_DEBUG, "File path: %s\nBody Size: %zu\n", file_path, body_size);

    Seobeo_Web_Header_Generate(p_response_header,
                               HTTP_OK,
                               mime,
                               body_size);

    Seobeo_Handle_Queue(p_cli_handle,
                        (const uint8*)p_response_header,
                        (uint32)strlen(p_response_header));
    Seobeo_Handle_Queue(p_cli_handle,
                        (const uint8*)file_content,
                        (uint32)body_size);
    Seobeo_Handle_Flush(p_cli_handle);
    Seobeo_Log(SEOBEO_DEBUG, "Request handled successfully\n");
  }
  else
  {
    Seobeo_Web_Header_Generate(p_response_header,
                               HTTP_FORBIDDEN,
                               "text/plain", 0);
    Seobeo_Handle_Queue(p_cli_handle,
                        (const uint8*)p_response_header,
                        (uint32)strlen(p_response_header));
    Seobeo_Handle_Flush(p_cli_handle);
  }
  goto clean_up;

clean_up:
  Seobeo_Log(SEOBEO_INFO, "Clean up all Arenas\n");
  if (p_cli_handle)
    Seobeo_Handle_Destroy(p_cli_handle);
  if (p_request_arena)
    Dowa_Arena_Free(p_request_arena);
  if (p_response_arena)
    Dowa_Arena_Free(p_response_arena);
  return;
}


int Seobeo_Web_Header_Parse(Seobeo_Handle *p_handle, Seobeo_Request_Entry **pp_map, Dowa_Arena *p_arena)
{
  while (1)
  {
    int r = Seobeo_Handle_Read(p_handle);
    if (r < 0)
      return -1;   // fatal error
    if (r == -2)
      return -2;   // connection closed TODO: Add this as part of Handle struct.

    if (p_handle->read_buffer_len >= 4 &&
        strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL)
      break;

    if (r == 0)
      return 1;     // EAGAIN, try again later TODO: Add this as part of Handle struct.
  }

  // "METHOD SP PATH SP VERSION CRLF"
  char *buf       = (char*)p_handle->read_buffer;
  char *hdr_end   = strstr(buf, "\r\n\r\n");
  size_t hdr_len  = hdr_end - buf + 4;

  // Debug: Print the first line of the request
  char *first_line_end = strstr(buf, "\r\n");
  if (first_line_end)
  {
    size_t first_line_len = first_line_end - buf;
    Seobeo_Log(SEOBEO_DEBUG, "Request line (first %zu bytes)\n", first_line_len > 200 ? 200 : first_line_len);
  }

  // This seems kinda bad ?
  char method[16], path[256], version[16];
  int scan_result = sscanf(buf, "%15s %255s %15s", method, path, version);
  Seobeo_Log(SEOBEO_DEBUG, "sscanf returned %d (method='%s', path='%s', version='%s')\n",
         scan_result,
         scan_result >= 1 ? method : "N/A",
         scan_result >= 2 ? path : "N/A",
         scan_result >= 3 ? version : "N/A");

  if (scan_result != 3)
  {
    Seobeo_Log(SEOBEO_ERROR, "Failed to parse request line\n");
    return -1;
  }

  Seobeo_Log(SEOBEO_DEBUG, "Allocating method_copy\n");
  char *method_copy = Dowa_Arena_Allocate(p_arena, strlen(method) + 1);
  if (!method_copy) { Seobeo_Log(SEOBEO_ERROR, "Failed to allocate method_copy\n"); return -1; }
  strcpy(method_copy, method);

  Seobeo_Log(SEOBEO_DEBUG, "Allocating version_copy\n");
  char *version_copy = Dowa_Arena_Allocate(p_arena, strlen(version) + 1);
  if (!version_copy) { Seobeo_Log(SEOBEO_ERROR, "Failed to allocate version_copy\n"); return -1; }
  strcpy(version_copy, version);

  Seobeo_Log(SEOBEO_DEBUG, "Pushing HTTP_Method and Version to map\n");
  Dowa_HashMap_Push_Arena(*pp_map, "HTTP_Method", method_copy, p_arena);
  Dowa_HashMap_Push_Arena(*pp_map, "Version", version_copy, p_arena);
  Seobeo_Log(SEOBEO_DEBUG, "Map now has %zu entries\n", Dowa_Array_Length(*pp_map));

  char *raw_path = path;  
  char *query_start = strchr(raw_path, '?');
  char *query_str   = NULL;
  
  if (query_start)
  {
    *query_start  = '\0';          // now raw_path ends before '?'
    query_str     = query_start + 1;
  }
  
  char *path_copy = Dowa_Arena_Allocate(p_arena, strlen(raw_path) + 1);
  if (!path_copy) return -1;
  strcpy(path_copy, raw_path);
  Dowa_HashMap_Push_Arena(*pp_map, "Path", path_copy, p_arena);
  
  // GET Params
  if (query_str && *query_str)
  {
    char *cur = query_str;
    while (cur && *cur)
    {
      char *next_amp = strchr(cur, '&');
      char *pair_end = next_amp ? next_amp : cur + strlen(cur);

      char *eq = memchr(cur, '=', pair_end - cur);
      if (eq)
      {
        size_t key_len = eq - cur;
        size_t val_len = pair_end - (eq + 1);

        char key_buf[256];
        // Adding query_ in front to separate GET params
        snprintf(key_buf, sizeof(key_buf), "query_%.*s", (int)key_len, cur);

        char *val_copy = Dowa_Arena_Allocate(p_arena, val_len + 1);
        if (!val_copy) return -1;
        memcpy(val_copy, eq + 1, val_len);
        val_copy[val_len] = '\0';

        Dowa_HashMap_Push_Arena(*pp_map, key_buf, val_copy, p_arena);
      }

      cur = next_amp ? next_amp + 1 : NULL;
    }
  }

  // int qp = Dowa_HashMap_Get_Position(map, "QueryParams");
  // Dowa_HashEntry *p_qp_entry = map->entries[qp];
  // printf("query param key: %s\n", p_qp_entry->key);
  // printf("query param value: %s\n",(char *)Dowa_HashMap_Get(p_qp_entry->buffer, "hello"));

  // Parse headers 
  char *line = buf + strlen(method) + 1 + strlen(path) + 1 + strlen(version) + 2;
  while (line < hdr_end)
  {
    char *next = strstr(line, "\r\n");
    if (!next) break;

    // split at colon
    char *colon = memchr(line, ':', next - line);
    if (colon)
    {
      size_t key_len   = colon - line;
      size_t value_len = next - colon - 1;

      char *val_start  = colon + 1;
      if (*val_start == ' ')
      {
        val_start++;
        value_len--;
      }

      char *key = Dowa_Arena_Allocate(p_arena, key_len + 1);
      if (!key) return -1;
      memcpy(key, line, key_len);
      key[key_len] = '\0';

      char *val = Dowa_Arena_Allocate(p_arena, value_len + 1);
      if (!val) return -1;
      memcpy(val, val_start, value_len);
      val[value_len] = '\0';

      // Both key and value are arena-allocated, hashmap will use them
      Dowa_HashMap_Push_Arena(*pp_map, key, val, p_arena);
    }

    line = next + 2;
  }

  Seobeo_Handle_Consume(p_handle, (uint32)hdr_len);

  // Reading Body
  void *p_cl_kv = Dowa_HashMap_Get_Ptr(*pp_map, "Content-Length");
  if (p_cl_kv)
  {
    const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value;
    size_t body_len = atoi(content_length_str);

    Seobeo_Log(SEOBEO_DEBUG, "Content-Length=%zu, reading body in chunks...\n", body_len);

    // Allocate buffer for entire body
    char *body = Dowa_Arena_Allocate(p_arena, body_len + 1);
    if (!body)
    {
      Seobeo_Log(SEOBEO_ERROR, "Failed to allocate %zu bytes for body\n", body_len);
      return -1;
    }

    size_t total_read = 0;

    while (total_read < body_len)
    {
      size_t available = p_handle->read_buffer_len;
      size_t to_copy = (body_len - total_read) < available ? (body_len - total_read) : available;

      if (to_copy > 0)
      {
        memcpy(body + total_read, p_handle->read_buffer, to_copy);
        total_read += to_copy;
        Seobeo_Handle_Consume(p_handle, (uint32)to_copy);

        Seobeo_Log(SEOBEO_DEBUG, "Copied %zu bytes, total %zu/%zu\n", to_copy, total_read, body_len);
      }

      if (total_read < body_len)
      {
        int r = Seobeo_Handle_Read(p_handle);
        if (r < 0)
        {
          Seobeo_Log(SEOBEO_ERROR, "Read failed with %d\n", r);
          return -1;
        }
        if (r == 0)
        {
          // No data available yet, continue waiting
          // printf("DEBUG: Waiting for more data... (have %zu/%zu bytes)\n", total_read, body_len);
          continue;
        }
      }
    }

    body[body_len] = '\0';
    Seobeo_Log(SEOBEO_DEBUG, "Body fully received (%zu bytes)\n", body_len);

    Dowa_HashMap_Push_Arena(*pp_map, "Body", body, p_arena);
  }

  return 0; 
}

void SigchildHandler(int s)
{
  (void)s; 

  // waitpid() might overwrite errno, so we save and restore it:
  int saved_errno = errno;

  while(waitpid(-1, NULL, WNOHANG) > 0);

  errno = saved_errno;
}

int Seobeo_Web_Server_Start(
    const char       *folder_path,
    const char       *port,
    Seobeo_ServerMode mode,
    int                thread_count)
{
  if (folder_path)
    strncpy(g_folder_path, folder_path, sizeof(g_folder_path) - 1);

  Seobeo_Cache_Entry *p_html_cache = NULL;

  Seobeo_Handle *p_server_handle =
    Seobeo_Stream_Handle_Server_Create(NULL, port);
  if (p_server_handle->socket < 0) return 1;

  Seobeo_Log(SEOBEO_INFO, "Listening on port %s\n", port);

  if (mode == SEOBEO_MODE_FORK)
  {
    Seobeo_Log(SEOBEO_INFO, "Server mode: FORK\n");
    struct sigaction sa;
    sa.sa_handler = SigchildHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGCHLD, &sa, NULL);

    while (1)
    {
      Seobeo_Handle *p_cli_handle =
        Seobeo_Stream_Handle_Server_Accept(p_server_handle);
      if (!p_cli_handle) continue;

      if (fork() == 0)
      {
        Seobeo_Web_HandleClientRequest(p_cli_handle,
                                       p_html_cache);
        _exit(0);
      }
    }
  }

  if (mode == SEOBEO_MODE_EDGE)
  {
    Seobeo_Log(SEOBEO_INFO, "Server mode: EDGE\n");
    Seobeo_Web_Edge(p_server_handle, thread_count, p_html_cache);
  }

  return -1;
}

/* Router logic */
struct Seobeo_Route_Struct {
  char *method; // "GET", "POST", "PUT", "DELETE"
  char *path_pattern; // "/v1/users/:id/posts/:post_id"
  Seobeo_Route_Handler handler;

  // Pre-parsed path segments for efficient matching
  char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"]
  boolean *is_param; // [false, false, true, false, true]
  size_t segment_count;
};

static Seobeo_Route *g_routes = NULL;

void Seobeo_Router_Init()
{
  Dowa_Array_Reserve(g_routes, 20);
}

void Seobeo_Router_Register(const char *method, const char *path_pattern, Seobeo_Route_Handler handler)
{
  Seobeo_Route route = {0};

  route.method = strdup(method);
  route.path_pattern = strdup(path_pattern);
  route.handler = handler;
  route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL);
  route.segment_count = Dowa_Array_Length(route.path_segments);
  route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count);

  for (size_t i = 0; i < route.segment_count; i++)
    route.is_param[i] = (route.path_segments[i][0] == ':');

  Dowa_Array_Push(g_routes, route);
}

static boolean match_route_and_extract(
  Seobeo_Route *route,
  const char *request_path,
  Seobeo_Request_Entry **pp_request_map,
  Dowa_Arena *p_arena)
{
  Dowa_Arena *p_temp_arena = Dowa_Arena_Create(1024);
  char **request_segments = Dowa_String_Split(request_path, "/", strlen(request_path), 1, p_temp_arena);
  size_t request_segment_count = Dowa_Array_Length(request_segments);

  // Check segment count matches
  if (request_segment_count != route->segment_count)
  {
    Dowa_Arena_Free(p_temp_arena);
    return FALSE;
  }

  for (size_t i = 0; i < route->segment_count; i++)
  {
    // parameters
    if (route->is_param[i])
    {
      char *param_name = route->path_segments[i];  // e.g., ":id"
      char *param_value = request_segments[i];    // e.g., "123"

      // Should Copy to arena 
      char *key = Dowa_String_Copy_Arena(param_name, p_arena);
      char *value = Dowa_String_Copy_Arena(param_value, p_arena);
      Dowa_HashMap_Push_Arena(*pp_request_map, key, value, p_arena);
    }
    else
    {
      // Does not match.
      if (strcmp(route->path_segments[i], request_segments[i]) != 0)
      {
        Dowa_Arena_Free(p_temp_arena);
        return FALSE;
      }
    }
  }
  
  Dowa_Arena_Free(p_temp_arena);
  return TRUE;
}

Seobeo_Route_Handler Seobeo_Router_Find_Handler(const char *method,
                         const char *path,
                         Seobeo_Request_Entry **pp_request_map,
                         Dowa_Arena *p_arena) {
  if (g_routes == NULL)
  {
    return NULL;
  }

  size_t route_count = Dowa_Array_Length(g_routes);
  for (size_t i = 0; i < route_count; i++)
  {
    Seobeo_Route *route = &g_routes[i];
    if (strcmp(route->method, method) != 0)
    {
      continue;
    }

    if (match_route_and_extract(route, path, pp_request_map, p_arena))
    {
      return route->handler;
    }
  }

  return NULL;
}

void Seobeo_Router_Send_Response(
    Seobeo_Handle *p_handle,
    Seobeo_Request_Entry *p_response_map,
    Dowa_Arena *p_arena)
{
  if (p_response_map == NULL)
  {
    char *header = Dowa_Arena_Allocate(p_arena, 1024);
    Seobeo_Web_Header_Generate(header, HTTP_INTERNAL_ERROR, "text/plain", 21);
    Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
    Seobeo_Handle_Queue(p_handle, (uint8_t*)"Internal Server Error", 21);
    Seobeo_Handle_Flush(p_handle);
    return;
  }

  int status = HTTP_OK;
  void *p_status_kv = Dowa_HashMap_Get_Ptr(p_response_map, "status");
  if (p_status_kv)
  {
    const char *status_str = ((Seobeo_Request_Entry*)p_status_kv)->value;
    status = atoi(status_str);
  }

  const char *body = "";
  void *p_body_kv = Dowa_HashMap_Get_Ptr(p_response_map, "body");
  if (p_body_kv)
  {
    body = ((Seobeo_Request_Entry*)p_body_kv)->value;
  }

  const char *content_type = "text/html";
  void *p_content_type_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-type");
  if (p_content_type_kv)
  {
    content_type = ((Seobeo_Request_Entry*)p_content_type_kv)->value;
  }

  size_t body_length = strlen(body);
  void *p_content_length_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-length");
  if (p_content_length_kv)
  {
    const char *content_length_str = ((Seobeo_Request_Entry*)p_content_length_kv)->value;
    body_length = atoi(content_length_str);
  }

  char *header = Dowa_Arena_Allocate(p_arena, 4096);
  Seobeo_Web_Header_Generate(header, status, content_type, body_length);
  for (int i = 0; i < Dowa_Array_Length(p_response_map); i++)
  {
    if (
      strstr(p_response_map[i].key, "status") ||
      strstr(p_response_map[i].key, "body") ||
      strstr(p_response_map[i].key, "content-type") ||
      strstr(p_response_map[i].key, "content-length") 
    )
      continue;

    int32 current_header_len = strlen(header);
    char *temp = malloc(sizeof(char) * 1024);
    sprintf(temp, "%s: %s\r\n\r\n", p_response_map[i].key, p_response_map[i].value);
    memcpy(&header[current_header_len - 2 /* \r\n */], temp, strlen(temp));
    free(temp);
  }

  Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
  Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length); 
  Seobeo_Handle_Flush(p_handle);
}

void Seobeo_Router_Destroy()
{
  if (g_routes == NULL)
    return;

  size_t route_count = Dowa_Array_Length(g_routes);
  for (size_t i = 0; i < route_count; i++)
  {
    Seobeo_Route *route = &g_routes[i];
    if (route->method) free(route->method);
    if (route->path_pattern) free(route->path_pattern);
    if (route->path_segments) Dowa_Array_Free(route->path_segments);
    if (route->is_param) free(route->is_param);
  }
  Dowa_Array_Free(g_routes);
  g_routes = NULL;
}

// Logging functions moved to s_logging.c