diff seobeo/s_web.c @ 7:114cad94008f

[Seobeo] Updated to support thread and edge server calls.
author June Park <parkjune1995@gmail.com>
date Mon, 29 Sep 2025 17:00:38 -0700
parents
children fb2cff495a60
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/s_web.c	Mon Sep 29 17:00:38 2025 -0700
@@ -0,0 +1,304 @@
+#include "seobeo/seobeo.h"
+
+void Seobeo_Web_GenerateResponseHeader(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/2.2 %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_PHandle p_cli_handle,
+                                    Dowa_PHashMap p_html_cache)
+{
+  Dowa_PArena p_response_arena = Dowa_Arena_Create(8192);
+  Dowa_PHashMap p_req_map = NULL;
+
+  Dowa_PHashEntry entry = NULL;
+  Dowa_PHashMap p_current = p_html_cache;
+  char *slash;
+
+  if (!p_response_arena) { perror("Dowa_Arena_Initialize"); goto clean_up; }
+
+  void *p_response_header = Dowa_Arena_Allocate(p_response_arena, (size_t)2048);
+  if (!p_response_header) { perror("Dowa_Arena_Allocate"); goto clean_up; }
+
+  p_req_map = Dowa_HashMap_Create(32);
+  if (Seobeo_Web_ParseClientHeader(p_cli_handle, p_req_map) != 0)
+  {
+    // malformed request or closed — respond 400
+    Seobeo_Web_GenerateResponseHeader(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;
+  }
+
+  const char *path = (const char*)Dowa_HashMap_Get(p_req_map, "Path");
+
+  char *file_path = Dowa_Arena_Allocate(p_response_arena, (size_t)512);
+
+  if (!path || strcmp(path, "/") == 0)
+  {
+    strcpy(file_path, "index.html");
+  }
+  else
+  {
+    size_t L = strlen(path);
+    // strip leading '/'
+    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
+    {
+      // Probably never get here?
+      strcpy(file_path, path);
+    }
+  }
+
+  // printf("\n\nfile_path: %s\n", file_path);
+
+  // Recursively go though the path until it gets to a file
+  while ((slash = strchr(file_path, '/')))
+  {
+    *slash = '\0'; // e.g. file_path="foo", slash+1="index.html"
+    char *dir     = file_path; // "foo"
+    file_path = slash + 1;  // "index.html"
+
+    p_current = Dowa_HashMap_Get(p_current, dir);
+    if (!p_current) { perror("No value"); goto clean_up; }
+  }
+
+  size_t pos = Dowa_HashMap_GetPosition(p_current, file_path);
+  entry = p_current->entries[pos];
+
+  //  Missing so 404
+  if (!entry)
+  {
+    Seobeo_Web_GenerateResponseHeader(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;
+  }
+
+
+  const char *mime = "application/octet-stream"; // Default binary
+  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";
+
+  size_t body_size = entry->capacity;
+  Seobeo_Web_GenerateResponseHeader(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*)entry->buffer,
+              (uint32)body_size);
+  Seobeo_Handle_Flush(p_cli_handle);
+
+clean_up:
+  Seobeo_Handle_Destroy(p_cli_handle);
+  Dowa_Arena_Free(p_response_arena);
+  Dowa_HashMap_Free(p_req_map); // TODO: Maybe initilized hashmap within the Arena?  
+}
+
+int Seobeo_Web_ParseClientHeader(Seobeo_PHandle p_handle, Dowa_PHashMap map)
+{
+  // 1) Fill read_buffer until we see "\r\n\r\n"
+  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.
+  }
+
+  // 2) Parse request‐line "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;
+
+  char method[16], path[256], version[16];
+  if (sscanf(buf, "%15s %255s %15s", method, path, version) != 3)
+  {
+    return -1;
+  }
+
+  Dowa_HashMap_PushValueWithType(map, "Method",  method,  strlen(method)  + 1, DOWA_HASH_MAP_TYPE_STRING);
+  Dowa_HashMap_PushValueWithType(map, "Path",    path,    strlen(path)    + 1, DOWA_HASH_MAP_TYPE_STRING);
+  Dowa_HashMap_PushValueWithType(map, "Version", version, strlen(version) + 1, DOWA_HASH_MAP_TYPE_STRING);
+
+  // 3) Parse each header line until the blank line
+  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 = malloc(key_len + 1);
+      memcpy(key, line, key_len);
+      key[key_len] = '\0';
+
+      char *val = malloc(value_len + 1);
+      memcpy(val, val_start, value_len);
+      val[value_len] = '\0';
+
+      Dowa_HashMap_PushValue(map, key, val, value_len + 1);
+
+      free(key);
+      free(val);
+    }
+
+    line = next + 2;
+  }
+  Seobeo_Handle_Consume(p_handle, (uint32)hdr_len);
+
+  // 4) If Content-Length was provided, read that much body
+  int content_length_pos = Dowa_HashMap_GetPosition(map, "Content-Length");
+  Dowa_PHashEntry p_content_length_entry = map->entries[content_length_pos];
+  if (p_content_length_entry)
+  {
+    size_t body_len = atoi((char*)p_content_length_entry->buffer);
+    while (p_handle->read_buffer_len < body_len)
+    {
+      int r = Seobeo_Handle_Read(p_handle);
+      if (r < 0)   return -1;
+      if (r ==  0) return 1;       // wait for more data
+    }
+
+    char *body = malloc(body_len + 1);
+    memcpy(body, p_handle->read_buffer, body_len);
+    body[body_len] = '\0';
+
+    Dowa_HashMap_PushValue(map, "Body", body, body_len + 1);
+    free(body);
+
+    Seobeo_Handle_Consume(p_handle, (uint32)body_len);
+  }
+
+  return 0;  // success; map now holds Method, Path, Version, headers, and optional Body
+}
+
+//  TODO: Do epoll or kqueue depending on the OS.
+void SigchildHandler(int s)
+{
+  (void)s; // quiet unused variable warning
+
+  // 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_StartBasicHTTPServer(
+    const char       *folder_path,
+    const char       *port,
+    Seobeo_ServerMode mode,
+    int                thread_count)
+{
+  Dowa_PHashMap p_html_cache = Dowa_HashMap_Create(1024);
+  if (Dowa_HashMap_Cache_Folder(p_html_cache,
+                                folder_path) != 0)
+  {
+    perror("Dowa_Cache_Folder");
+    return -1;
+  }
+
+  Seobeo_PHandle p_server_handle =
+    Seobeo_Stream_Handle_Create(NULL, port);
+  if (p_server_handle->socket < 0) return 1;
+  printf("Listening on port %s\n", port);
+
+  // Fork‐based fallback
+  if (mode == SEOBEO_MODE_FORK)
+  {
+    struct sigaction sa;
+    sa.sa_handler = SigchildHandler;
+    sigemptyset(&sa.sa_mask);
+    sa.sa_flags = SA_RESTART;
+    sigaction(SIGCHLD, &sa, NULL);
+
+    while (1) {
+      Seobeo_PHandle cli =
+        Seobeo_Stream_Handle_Accept(p_server_handle);
+      if (!cli) continue;
+
+      if (fork() == 0) {
+        Seobeo_Web_HandleClientRequest(cli,
+                                       p_html_cache);
+        _exit(0);
+      }
+      Seobeo_Handle_Destroy(cli);
+    }
+  }
+
+  if (mode == SEOBEO_MODE_EDGE)
+  {
+    Seobeo_Web_Edge(p_server_handle, thread_count, p_html_cache);
+  }
+
+  return -1;
+}