view seobeo/s_web.c @ 71:75de5903355c

Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
author June Park <parkjune1995@gmail.com>
date Sun, 28 Dec 2025 20:34:22 -0800
parents 6626ec933933
children 4532ce6d9eb8
line wrap: on
line source

#include "seobeo/seobeo.h"

// Global folder path for serving files
static char g_folder_path[512] = ".";

int Seobeo_Web_GenerateRequestHeader(void *buffer, const char *host, 
                                     const char *path) 
{
  return sprintf(
    buffer,
    "GET %s HTTP/1.1\r\n"
    "Host: %s\r\n"
    "Connection: close\r\n"
    "accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n"
    "accept-language: en-GB,en;q=0.9,en-US;q=0.8,ko;q=0.7\r\n"
    "if-modified-since: Sat, 02 Aug 2025 22:51:58 GMT\r\n"
    "if-none-match: W/\"688e968e-5700\"\r\n"
    "priority: u=0, i\r\n"
    "sec-ch-ua: \"Chromium\";v=\"140\", \"Not=A?Brand\";v=\"24\", \"Google Chrome\";v=\"140\"\r\n"
    "sec-ch-ua-mobile: 0\r\n"
    "sec-ch-ua-platform: \"macOS\"\r\n"
    "sec-fetch-dest: document\r\n"
    "sec-fetch-mode: navigate\r\n"
    "sec-fetch-site: none\r\n"
    "sec-fetch-user: 1\r\n"
    "upgrade-insecure-requests: 1\r\n"
    "\r\n",
    path, host
  );
}

// Load file from disk and cache it
static 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)
{
  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(1*1024*1024); // 1MB 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;
  if (Seobeo_Web_Header_Parse(p_cli_handle, &p_req_map, p_request_arena) != 0)
  {
    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;
  }

  // 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;

  if (!method)
  {
    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;
  }

  // --- Handle different HTTP methods ---
  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
    {
      // Load from disk and cache
      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;
    }

    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";

    printf("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);
    printf("DONE\n\n\n");
  }
  else if (strcmp(method, "POST") == 0)
  {
    // --- TODO: Add POST logic here ---
    Seobeo_Web_Header_Generate(p_response_header,
                               HTTP_NOT_FOUND,
                               "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);
  }
  else if (strcmp(method, "PUT") == 0)
  {
    // --- TODO: Add PUT logic here ---
    Seobeo_Web_Header_Generate(p_response_header,
                               HTTP_NOT_FOUND,
                               "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);
  }
  else if (strcmp(method, "DELETE") == 0)
  {
    // --- TODO: Add DELETE logic here ---
    Seobeo_Web_Header_Generate(p_response_header,
                               HTTP_NOT_FOUND,
                               "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);
  }
  else
  {
    // Unknown or unsupported method
    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:
  printf("clean up\n\n");
  if (p_cli_handle)
    Seobeo_Handle_Destroy(p_cli_handle);
  if (p_request_arena)
    Dowa_Arena_Destroy(p_request_arena);
  if (p_response_arena)
    Dowa_Arena_Destroy(p_response_arena);
  return;
}


int Seobeo_Web_Header_Parse(Seobeo_Handle *p_handle, Seobeo_Request_Entry **pp_map, Dowa_Arena *p_arena)
{
  // 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;

  // This seems kinda bad ?
  char method[16], path[256], version[16];
  if (sscanf(buf, "%15s %255s %15s", method, path, version) != 3)
  {
    return -1;
  }

  // Copy strings to arena and store in hashmap
  char *method_copy = Dowa_Arena_Allocate(p_arena, strlen(method) + 1);
  if (!method_copy) return -1;
  strcpy(method_copy, method);

  char *version_copy = Dowa_Arena_Allocate(p_arena, strlen(version) + 1);
  if (!version_copy) return -1;
  strcpy(version_copy, version);

  Dowa_HashMap_Push_Arena(*pp_map, "HTTP_Method", method_copy, p_arena);
  Dowa_HashMap_Push_Arena(*pp_map, "Version", version_copy, p_arena);

  // 1) Separate raw path and query string
  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;
  }
  
  // push only the clean path
  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);
  
  // 2) If there *is* a query, tokenize into the same map with "query_" prefix
  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];
        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"));

  // 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 = 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);

  // 4) If Content-Length was provided, read that much 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);
    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 = Dowa_Arena_Allocate(p_arena, body_len + 1);
    if (!body) return -1;
    memcpy(body, p_handle->read_buffer, body_len);
    body[body_len] = '\0';

    // Body is arena-allocated
    Dowa_HashMap_Push_Arena(*pp_map, "Body", body, p_arena);

    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_Server_Start(
    const char       *folder_path,
    const char       *port,
    Seobeo_ServerMode mode,
    int                thread_count)
{
  // Store folder path globally
  if (folder_path)
    strncpy(g_folder_path, folder_path, sizeof(g_folder_path) - 1);

  // Initialize empty cache - files will be loaded on-demand
  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;

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

  // Fork‐based fallback
  if (mode == SEOBEO_MODE_FORK)
  {
    printf("FORK MODE\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)
  {
    printf("EDGE MODE\n");
    Seobeo_Web_Edge(p_server_handle, thread_count, p_html_cache);
  }

  return -1;
}

int Seobeo_Web_Client_Get(const char *host,
                          const char *port,
                          const char *path)
{
  Seobeo_Handle *h = Seobeo_Stream_Handle_Client_Create(host, port, TRUE);
  if (!h || h->socket < 0)
  {
    if (h)
      Seobeo_Handle_Destroy(h);
    return -1;
  }

  Dowa_Arena *p_request_arena = Dowa_Arena_Create(1 * 1024 * 1024);
  if (!p_request_arena) { perror("Dowa_Arena_Create"); return -1; }

  void *p_request_header = Dowa_Arena_Allocate(p_request_arena, 4096);
  if (!p_request_header) { perror("Dowa_Arena_Allocate"); return -1; }

  int request_len = Seobeo_Web_GenerateRequestHeader(p_request_header, host, path);
  // DEBUG
  // printf("request: %s\n", (char *)p_request_header);
  Seobeo_Handle_Queue(h, (uint8 *)p_request_header, (uint32)request_len);
  if (Seobeo_Handle_Flush(h) < 0)
  {
    perror("Seobeo_Handle_Flush");
    Seobeo_Handle_Destroy(h);
    return -1;
  }

  // Response
  size_t cap = 1024*8, used = 0;
  char *p_request_body = Dowa_Arena_Allocate(p_request_arena, cap);
  if (!p_request_body) { Seobeo_Handle_Destroy(h); return -1; }

  while (1)
  {
    int n = Seobeo_Handle_Read(h);
    printf("Size: %d\n", n);
    if (n > 0)
    {
      // TODO: Maybe directly use arena inside of the struct? idk if that is useful or not...
      memcpy(p_request_body + used, h->read_buffer, h->read_buffer_len);
      used += h->read_buffer_len;
      Seobeo_Handle_Consume(h, (uint32)h->read_buffer_len);
    }else if (n == 0)
    {
      // Wait
      continue;
    }else if (n == -2)
    {
      // Debug
      // peer closed; we’ve got everything
      printf("\n\nCLOSED\n\n");
      break;
    }else
    {
      Dowa_Arena_Destroy(p_request_arena);
      Seobeo_Handle_Destroy(h);
      return -1;
    }
  }

  // Debug
  printf("Request body %s", p_request_body);
  Dowa_Arena_Destroy(p_request_arena);
  Seobeo_Handle_Destroy(h);
  return 0;
}