diff seobeo/s_web.c @ 96:70401cf61e97

[Seobeo] Added logging.
author June Park <parkjune1995@gmail.com>
date Fri, 02 Jan 2026 19:16:17 -0800
parents 655ea0b661fd
children 65e5a5b89a4e
line wrap: on
line diff
--- a/seobeo/s_web.c	Fri Jan 02 19:11:35 2026 -0800
+++ b/seobeo/s_web.c	Fri Jan 02 19:16:17 2026 -0800
@@ -93,23 +93,22 @@
 void Seobeo_Web_HandleClientRequest(Seobeo_Handle  *p_cli_handle,
                                     Seobeo_Cache_Entry *p_html_cache)
 {
+  Seobeo_Log(SEOBEO_INFO, "Client is from %s\n", p_cli_handle->host);
   Dowa_Arena *p_request_arena = Dowa_Arena_Create(1*1024*1024); // 1MB for request parsing
   if (!p_request_arena) { perror("Dowa_Arena_Create request"); goto clean_up; }
 
-  Dowa_Arena *p_response_arena = Dowa_Arena_Create(5*1024*1024); // 1MB for response
+  Dowa_Arena *p_response_arena = Dowa_Arena_Create(5*1024*1024); // 5MB for response
   if (!p_response_arena) { perror("Dowa_Arena_Create response"); goto clean_up; }
 
   void *p_response_header = Dowa_Arena_Allocate(p_response_arena, (size_t)1024*5); // 5Kb
   if (!p_response_header) { perror("Dowa_Arena_Allocate"); goto clean_up; }
 
-  // Parse request headers into hashmap using arena
   Seobeo_Request_Entry *p_req_map = NULL;
   int parse_result = Seobeo_Web_Header_Parse(p_cli_handle, &p_req_map, p_request_arena);
 
-  // Treat EAGAIN (return code 1) as success - headers were parsed, body may still be coming
   if (parse_result != 0 && parse_result != 1)
   {
-    printf("ERROR: Seobeo_Web_Header_Parse failed with code %d\n", parse_result);
+    Seobeo_Log(SEOBEO_ERROR, "Seobeo_Web_Header_Parse failed with code %d\n", parse_result);
     fflush(stdout);
     Seobeo_Web_Header_Generate(p_response_header,
                                HTTP_BAD_REQUEST,
@@ -121,19 +120,18 @@
     goto clean_up;
   }
 
-  printf("DEBUG: Parse completed with code %d\n", parse_result);
+  Seobeo_Log(SEOBEO_DEBUG, "Parse completed with code %d\n", parse_result);
   fflush(stdout);
 
-  // Extract method (GET, POST, etc.)
   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;
 
-  printf("DEBUG: Parsed request, method=%s\n", method ? method : "NULL");
+  Seobeo_Log(SEOBEO_DEBUG, "Parsed request, method=%s\n", method ? method : "NULL");
   fflush(stdout);
 
   if (!method)
   {
-    printf("ERROR: No HTTP method found in request\n");
+    Seobeo_Log(SEOBEO_ERROR, "No HTTP method found in request\n");
     fflush(stdout);
     Seobeo_Web_Header_Generate(p_response_header,
                                HTTP_BAD_REQUEST,
@@ -225,7 +223,7 @@
     else if (strstr(file_path, ".ico")) mime = "image/x-icon";
     else if (strstr(file_path, ".json")) mime = "application/json";
 
-    printf("File path: %s\nBody Size: %zu\n", file_path, body_size);
+    Seobeo_Log(SEOBEO_DEBUG, "File path: %s\nBody Size: %zu\n", file_path, body_size);
 
     Seobeo_Web_Header_Generate(p_response_header,
                                HTTP_OK,
@@ -239,7 +237,7 @@
                         (const uint8*)file_content,
                         (uint32)body_size);
     Seobeo_Handle_Flush(p_cli_handle);
-    printf("DONE\n\n\n");
+    Seobeo_Log(SEOBEO_DEBUG, "Request handled successfully\n");
   }
   else
   {
@@ -254,7 +252,7 @@
   goto clean_up;
 
 clean_up:
-  printf("clean up\n\n");
+  Seobeo_Log(SEOBEO_INFO, "Clean up all Arenas\n");
   if (p_cli_handle)
     Seobeo_Handle_Destroy(p_cli_handle);
   if (p_request_arena)
@@ -295,16 +293,14 @@
   if (first_line_end)
   {
     size_t first_line_len = first_line_end - buf;
-    printf("DEBUG: Request line (first %zu bytes): '", first_line_len > 200 ? 200 : first_line_len);
-    fwrite(buf, 1, first_line_len > 200 ? 200 : first_line_len, stdout);
-    printf("'\n");
+    Seobeo_Log(SEOBEO_DEBUG, "Request line (first %zu bytes)\n", first_line_len > 200 ? 200 : first_line_len);
     fflush(stdout);
   }
 
   // This seems kinda bad ?
   char method[16], path[256], version[16];
   int scan_result = sscanf(buf, "%15s %255s %15s", method, path, version);
-  printf("DEBUG: sscanf returned %d (method='%s', path='%s', version='%s')\n",
+  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",
@@ -313,29 +309,29 @@
 
   if (scan_result != 3)
   {
-    printf("ERROR: Failed to parse request line\n");
+    Seobeo_Log(SEOBEO_ERROR, "Failed to parse request line\n");
     fflush(stdout);
     return -1;
   }
 
   // Copy strings to arena and store in hashmap
-  printf("DEBUG: Allocating method_copy\n");
+  Seobeo_Log(SEOBEO_DEBUG, "Allocating method_copy\n");
   fflush(stdout);
   char *method_copy = Dowa_Arena_Allocate(p_arena, strlen(method) + 1);
-  if (!method_copy) { printf("ERROR: Failed to allocate method_copy\n"); return -1; }
+  if (!method_copy) { Seobeo_Log(SEOBEO_ERROR, "Failed to allocate method_copy\n"); return -1; }
   strcpy(method_copy, method);
 
-  printf("DEBUG: Allocating version_copy\n");
+  Seobeo_Log(SEOBEO_DEBUG, "Allocating version_copy\n");
   fflush(stdout);
   char *version_copy = Dowa_Arena_Allocate(p_arena, strlen(version) + 1);
-  if (!version_copy) { printf("ERROR: Failed to allocate version_copy\n"); return -1; }
+  if (!version_copy) { Seobeo_Log(SEOBEO_ERROR, "Failed to allocate version_copy\n"); return -1; }
   strcpy(version_copy, version);
 
-  printf("DEBUG: Pushing HTTP_Method and Version to map\n");
+  Seobeo_Log(SEOBEO_DEBUG, "Pushing HTTP_Method and Version to map\n");
   fflush(stdout);
   Dowa_HashMap_Push_Arena(*pp_map, "HTTP_Method", method_copy, p_arena);
   Dowa_HashMap_Push_Arena(*pp_map, "Version", version_copy, p_arena);
-  printf("DEBUG: Map now has %zu entries\n", Dowa_Array_Length(*pp_map));
+  Seobeo_Log(SEOBEO_DEBUG, "Map now has %zu entries\n", Dowa_Array_Length(*pp_map));
   fflush(stdout);
 
   // 1) Separate raw path and query string
@@ -437,14 +433,14 @@
     const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value;
     size_t body_len = atoi(content_length_str);
 
-    printf("DEBUG: Content-Length=%zu, reading body in chunks...\n", body_len);
+    Seobeo_Log(SEOBEO_DEBUG, "Content-Length=%zu, reading body in chunks...\n", body_len);
     fflush(stdout);
 
     // Allocate buffer for entire body
     char *body = Dowa_Arena_Allocate(p_arena, body_len + 1);
     if (!body)
     {
-      printf("ERROR: Failed to allocate %zu bytes for body\n", body_len);
+      Seobeo_Log(SEOBEO_ERROR, "Failed to allocate %zu bytes for body\n", body_len);
       fflush(stdout);
       return -1;
     }
@@ -464,7 +460,7 @@
         total_read += to_copy;
         Seobeo_Handle_Consume(p_handle, (uint32)to_copy);
 
-        printf("DEBUG: Copied %zu bytes, total %zu/%zu\n", to_copy, total_read, body_len);
+        Seobeo_Log(SEOBEO_DEBUG, "Copied %zu bytes, total %zu/%zu\n", to_copy, total_read, body_len);
         fflush(stdout);
       }
 
@@ -474,7 +470,7 @@
         int r = Seobeo_Handle_Read(p_handle);
         if (r < 0)
         {
-          printf("ERROR: Read failed with %d\n", r);
+          Seobeo_Log(SEOBEO_ERROR, "Read failed with %d\n", r);
           fflush(stdout);
           return -1;
         }
@@ -489,7 +485,7 @@
     }
 
     body[body_len] = '\0';
-    printf("DEBUG: Body fully received (%zu bytes)\n", body_len);
+    Seobeo_Log(SEOBEO_DEBUG, "Body fully received (%zu bytes)\n", body_len);
     fflush(stdout);
 
     // Body is arena-allocated
@@ -529,12 +525,12 @@
     Seobeo_Stream_Handle_Server_Create(NULL, port);
   if (p_server_handle->socket < 0) return 1;
 
-  printf("Listening on port %s\n", port);
+  Seobeo_Log(SEOBEO_INFO, "Listening on port %s\n", port);
 
   // Fork‐based fallback
   if (mode == SEOBEO_MODE_FORK)
   {
-    printf("FORK MODE\n");
+    Seobeo_Log(SEOBEO_INFO, "Server mode: FORK\n");
     struct sigaction sa;
     sa.sa_handler = SigchildHandler;
     sigemptyset(&sa.sa_mask);
@@ -558,7 +554,7 @@
 
   if (mode == SEOBEO_MODE_EDGE)
   {
-    printf("EDGE MODE\n");
+    Seobeo_Log(SEOBEO_INFO, "Server mode: EDGE\n");
     Seobeo_Web_Edge(p_server_handle, thread_count, p_html_cache);
   }
 
@@ -600,7 +596,7 @@
   while (1)
   {
     int n = Seobeo_Handle_Read(h);
-    printf("Size: %d\n", n);
+    Seobeo_Log(SEOBEO_DEBUG, "Received size: %d bytes\n", n);
     if (n > 0)
     {
       // TODO: Maybe directly use arena inside of the struct? idk if that is useful or not...
@@ -615,7 +611,7 @@
     {
       // Debug
       // peer closed; we’ve got everything
-      printf("\n\nCLOSED\n\n");
+      Seobeo_Log(SEOBEO_DEBUG, "Connection closed by client\n");
       break;
     }else
     {
@@ -626,8 +622,237 @@
   }
 
   // Debug
-  printf("Request body %s", p_request_body);
+  Seobeo_Log(SEOBEO_DEBUG, "Request body: %s\n", p_request_body);
   Dowa_Arena_Free(p_request_arena);
   Seobeo_Handle_Destroy(h);
   return 0;
 }
+
+/* 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);
+}
+
+// Match route and extract path parameters
+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;
+  }
+
+  // Header 
+  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);
+  }
+
+  // Body 
+  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;
+  }
+
+  // Default text plain
+  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;
+  }
+
+  // Check for custom content-length (for binary data)
+  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);
+  }
+
+  // All other types are default thoguht to be headers.
+  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")  // Skip custom 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);  // Use actual 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;
+}
+
+static char *Seobeo_Log_Level_String(Seobeo_Log_Level level)
+{
+  switch(level) {
+    case SEOBEO_DEBUG:   return "DEBUG";
+    case SEOBEO_INFO:    return "INFO";
+    case SEOBEO_WARNING: return "WARNING";
+    case SEOBEO_ERROR:   return "ERROR";
+    default:          return "INFO";
+  }
+}
+
+int Seobeo_Log(Seobeo_Log_Level level, const char * restrict format, ...)
+{
+  #ifndef SEOBEO_ENABLE_DEBUG
+  if (level == SEOBEO_DEBUG)
+    return 0;
+  #endif
+
+  int result;
+  va_list args;
+  printf("[%s] ", Seobeo_Log_Level_String(level));
+  va_start(args, format);
+  result = vprintf(format, args);
+  va_end(args);
+
+  return result;
+}
+