diff seobeo/s_web.c @ 183:a8976a008a9d

[BenchMark] Added bun bench mark to test seoboe vs other popular benchmarks.
author MrJuneJune <me@mrjunejune.com>
date Fri, 23 Jan 2026 21:19:08 -0800
parents bdcc610eeed8
children 8c74204fd362
line wrap: on
line diff
--- a/seobeo/s_web.c	Thu Jan 22 21:23:17 2026 -0800
+++ b/seobeo/s_web.c	Fri Jan 23 21:19:08 2026 -0800
@@ -1,4 +1,6 @@
 #include "seobeo/seobeo.h"
+#include <strings.h>  // for strcasecmp
+#include <time.h>     // for time_t
 
 static char g_folder_path[512] = ".";
 
@@ -36,6 +38,14 @@
     void *buffer, int status,
     const char *content_type, const int content_length)
 {
+  Seobeo_Web_Header_Generate_KeepAlive(buffer, status, content_type, content_length, FALSE);
+}
+
+void Seobeo_Web_Header_Generate_KeepAlive(
+    void *buffer, int status,
+    const char *content_type, const int content_length,
+    boolean keep_alive)
+{
   const char *status_text;
   switch(status)
   {
@@ -50,37 +60,76 @@
     case HTTP_INTERNAL_ERROR: status_text = "Internal Server Error"; break;
     default: status_text = "Unknown"; break;
   }
-  
+
   sprintf(
-    buffer, 
+    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
+    "Connection: %s\r\n"
+    "\r\n",
+    status, status_text, content_type, content_length,
+    keep_alive ? "keep-alive" : "close"
   );
 }
 
-void Seobeo_Web_HandleClientRequest(Seobeo_Handle  *p_cli_handle,
-                                    Seobeo_Cache_Entry *p_html_cache)
+// Default arena size (5MB) - will allocate more if Content-Length requires it
+#define DEFAULT_ARENA_SIZE (5 * 1024 * 1024)
+
+// Helper to check if Connection header contains a specific value (case-insensitive)
+static boolean connection_header_contains(const char *header_value, const char *target)
 {
-  // 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; }
+  if (!header_value || !target) return FALSE;
+
+  // Make a copy to tokenize
+  char *copy = strdup(header_value);
+  if (!copy) return FALSE;
 
-  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; }
+  boolean found = FALSE;
+  char *token = strtok(copy, ", \t");
+  while (token) {
+    if (strcasecmp(token, target) == 0) {
+      found = TRUE;
+      break;
+    }
+    token = strtok(NULL, ", \t");
+  }
+  free(copy);
+  return found;
+}
+
+// Returns TRUE if connection should be kept alive, FALSE if it should be closed
+boolean Seobeo_Web_ClientHandle_Request(Seobeo_Handle *p_cli_handle,
+                                        Seobeo_Cache_Entry *p_html_cache,
+                                        boolean use_keep_alive)
+{
+  boolean should_keep_alive = FALSE;
+
+  // Start with default arena size (5MB)
+  size_t arena_size = DEFAULT_ARENA_SIZE;
+
+  // We'll peek at Content-Length to potentially allocate larger arena
+  // For now, start with default and handle body reading separately
+  Dowa_Arena *p_request_arena = Dowa_Arena_Create(arena_size);
+  if (!p_request_arena) { perror("Dowa_Arena_Create request"); return FALSE; }
+
+  Dowa_Arena *p_response_arena = Dowa_Arena_Create(arena_size);
+  if (!p_response_arena) { perror("Dowa_Arena_Create response"); Dowa_Arena_Free(p_request_arena); return FALSE; }
 
   void *p_response_header = Dowa_Arena_Allocate(p_response_arena, 1024*5); // 5Kb
-  if (!p_response_header) { perror("Dowa_Arena_Allocate"); goto clean_up; }
+  if (!p_response_header) { perror("Dowa_Arena_Allocate"); goto clean_up_arenas; }
 
   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);
+  // Handle parse errors
+  if (parse_result < 0) {
+    // Fatal error or connection closed
+    Seobeo_Log(SEOBEO_DEBUG, "Seobeo_Web_Header_Parse failed with code %d\n", parse_result);
+    if (parse_result == -2) {
+      // Connection closed by client - don't send error response
+      goto clean_up_arenas;
+    }
     Seobeo_Web_Header_Generate(p_response_header,
                                HTTP_BAD_REQUEST,
                                "text/plain", 0);
@@ -88,40 +137,44 @@
                         (const uint8*)p_response_header,
                         (uint32)strlen(p_response_header));
     Seobeo_Handle_Flush(p_cli_handle);
-    goto clean_up;
+    goto clean_up_arenas;
   }
 
-  Seobeo_Log(SEOBEO_DEBUG, "Parse completed with code %d\n", parse_result);
+  // Determine keep-alive based on HTTP version and Connection header
+  // HTTP/1.1: keep-alive by default unless "Connection: close"
+  // HTTP/1.0: close by default unless "Connection: keep-alive"
+
+  void *p_ver_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Version");
+  const char *http_version = p_ver_kv ? ((Seobeo_Request_Entry*)p_ver_kv)->value : "HTTP/1.0";
+  boolean is_http11 = (strstr(http_version, "1.1") != NULL);
+
+  void *p_conn_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Connection");
+  const char *conn_header = p_conn_kv ? ((Seobeo_Request_Entry*)p_conn_kv)->value : NULL;
 
-  // 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;
+  if (conn_header) {
+    // Explicit Connection header - check for keep-alive or close
+    if (connection_header_contains(conn_header, "close")) {
+      should_keep_alive = FALSE;
+    } else if (connection_header_contains(conn_header, "keep-alive")) {
+      should_keep_alive = use_keep_alive;
+    } else {
+      // Unknown value, use version default
+      should_keep_alive = is_http11 && use_keep_alive;
+    }
+  } else {
+    // No Connection header - use HTTP version defaults
+    should_keep_alive = is_http11 && use_keep_alive;
   }
-  // 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");
+  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 : "/";
 
   if (!method)
   {
-    Seobeo_Log(SEOBEO_ERROR, "No HTTP method found in request\n");
+    Seobeo_Log(SEOBEO_DEBUG, "No HTTP method found in request\n");
     Seobeo_Web_Header_Generate(p_response_header,
                                HTTP_BAD_REQUEST,
                                "text/plain", 0);
@@ -129,23 +182,18 @@
                         (const uint8*)p_response_header,
                         (uint32)strlen(p_response_header));
     Seobeo_Handle_Flush(p_cli_handle);
-    goto clean_up;
+    should_keep_alive = FALSE;
+    goto clean_up_arenas;
   }
 
-  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
-  Seobeo_Log(SEOBEO_DEBUG, "Web soceket path \n");
   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;
+    Dowa_Arena_Free(p_request_arena);
+    Dowa_Arena_Free(p_response_arena);
+    return FALSE; // WebSocket takes over, don't keep-alive in HTTP sense
   }
   #endif
 
@@ -154,16 +202,14 @@
   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;
+    Seobeo_Router_Send_Response_KeepAlive(p_cli_handle, p_response_map, p_response_arena, should_keep_alive);
+    goto clean_up_arenas;
   }
 
-  // --- Static files fallback for GET ---
+  // --- Static files fallback for GET (use original large arena logic) ---
   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
+    char *file_path = Dowa_Arena_Allocate(p_response_arena, (size_t)5 * 1024);
 
     if (!path || strcmp(path, "/") == 0)
     {
@@ -183,14 +229,12 @@
         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 - use stored size for binary file support
       Seobeo_Cached_File *cached = ((Seobeo_Cache_Entry*)p_file_kv)->value;
       file_content = cached->content;
       body_size = cached->size;
@@ -209,41 +253,28 @@
 
     if (!file_content)
     {
-      Seobeo_Web_Header_Generate(p_response_header,
+      Seobeo_Web_Header_Generate_KeepAlive(p_response_header,
                                  HTTP_NOT_FOUND,
-                                 "text/html", 0);
+                                 "text/html", 0, should_keep_alive);
       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;
+      goto clean_up_arenas;
     }
 
-    Seobeo_Log(SEOBEO_DEBUG, "Serving Static Files\n");
-    // 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, ".webp")) mime = "image/webp";
-    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";
-    else if (strstr(file_path, ".mp4")) mime = "video/mp4";
-    else if (strstr(file_path, ".webm")) mime = "video/webm";
-    else if (strstr(file_path, ".glb")) mime = "model/gltf-binary";
-    else if (strstr(file_path, ".gltf")) mime = "model/gltf+json";
 
-    Seobeo_Log(SEOBEO_DEBUG, "File path: %s\nBody Size: %zu\n", file_path, body_size);
-
-    Seobeo_Web_Header_Generate(p_response_header,
+    Seobeo_Web_Header_Generate_KeepAlive(p_response_header,
                                HTTP_OK,
                                mime,
-                               body_size);
+                               body_size, should_keep_alive);
 
     Seobeo_Handle_Queue(p_cli_handle,
                         (const uint8*)p_response_header,
@@ -252,32 +283,26 @@
                         (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_Web_Header_Generate_KeepAlive(p_response_header,
+                               HTTP_NOT_FOUND,
+                               "text/plain", 0, should_keep_alive);
     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);
+clean_up_arenas:
   if (p_request_arena)
     Dowa_Arena_Free(p_request_arena);
   if (p_response_arena)
     Dowa_Arena_Free(p_response_arena);
-  return;
+  return should_keep_alive;
 }
 
-
 int Seobeo_Web_Header_Parse(Seobeo_Handle *p_handle, Seobeo_Request_Entry **pp_map, Dowa_Arena *p_arena)
 {
   while (1)
@@ -546,8 +571,7 @@
 
       if (fork() == 0)
       {
-        Seobeo_Web_HandleClientRequest(p_cli_handle,
-                                       p_html_cache);
+        Seobeo_Web_ClientHandle_Request(p_cli_handle, p_html_cache, FALSE);
         _exit(0);
       }
     }
@@ -675,10 +699,19 @@
     Seobeo_Request_Entry *p_response_map,
     Dowa_Arena *p_arena)
 {
+  Seobeo_Router_Send_Response_KeepAlive(p_handle, p_response_map, p_arena, FALSE);
+}
+
+void Seobeo_Router_Send_Response_KeepAlive(
+    Seobeo_Handle *p_handle,
+    Seobeo_Request_Entry *p_response_map,
+    Dowa_Arena *p_arena,
+    boolean keep_alive)
+{
   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_Web_Header_Generate_KeepAlive(header, HTTP_INTERNAL_ERROR, "text/plain", 21, keep_alive);
     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);
@@ -716,14 +749,14 @@
   }
 
   char *header = Dowa_Arena_Allocate(p_arena, 4096);
-  Seobeo_Web_Header_Generate(header, status, content_type, body_length);
+  Seobeo_Web_Header_Generate_KeepAlive(header, status, content_type, body_length, keep_alive);
   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") 
+      strstr(p_response_map[i].key, "content-length")
     )
       continue;
 
@@ -735,7 +768,7 @@
   }
 
   Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
-  Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length); 
+  Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length);
   Seobeo_Handle_Flush(p_handle);
 }