view seobeo/s_web.c @ 22:947b81010aba

[Dowa & Seobeo] Updated so that Dowa hashmaps can use arena and not be broken. Split up web so taht it can handle different paths. Also fixes issues with hash collisions which was pain in the ass.
author June Park <parkjune1995@gmail.com>
date Tue, 07 Oct 2025 07:11:02 -0700
parents 09def63429b9
children c0f6c8c7829f
line wrap: on
line source

#include "seobeo/seobeo.h"

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
  );
}

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_PHandle p_cli_handle,
                                    Dowa_PHashMap p_html_cache)
{
  Dowa_PHashEntry entry = NULL;
  Dowa_PHashMap p_current = p_html_cache;
  char *slash;

  Dowa_PArena p_response_arena = Dowa_Arena_Create(1*1024*1024);
  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; }

  // Parse request headers into hashmap
  Dowa_PHashMap p_req_map = Dowa_HashMap_Create_With_Arena(100, p_response_arena);
  if (Seobeo_Web_Header_Parse(p_cli_handle, p_req_map) != 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;
  }

  Dowa_HashMap_Print(p_req_map);

  // Extract method (GET, POST, etc.)
  const char *method = (const char*)Dowa_HashMap_Get(p_req_map, "HTTP_Method");
  printf("Method: %s Pointer %p\n\n", method, p_req_map);
  if (!method)
  {
    printf("?? wtf\n\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;
  }

  // --- Separate GET map for caching or routing ---
  Dowa_PHashMap p_get_map = Dowa_HashMap_Create(64);
  if (!p_get_map)
  {
    perror("Dowa_HashMap_Create (p_get_map)");
    goto clean_up;
  }

  // --- Handle different HTTP methods ---
  if (strcmp(method, "GET") == 0)
  {
    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);
      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);
      }
    }

    // Store path for GET handling map
    Dowa_HashMap_Push_Value(p_get_map, "Path", file_path, strlen(file_path) + 1);

    // Walk through nested maps to find content
    while ((slash = strchr(file_path, '/')))
    {
      *slash = '\0';
      char *dir = file_path;
      file_path = slash + 1;

      printf("Directory: %s\n", dir);

      p_current = Dowa_HashMap_Get(p_current, dir);
      if (!p_current)
      {
        fprintf(stderr, "No value in hashmap key: %s\n\n", dir);
        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;
      }
    }

    size_t pos = Dowa_HashMap_Get_Position(p_current, file_path);
    entry = p_current->entries[pos];

    if (!entry)
    {
      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";

    size_t body_size = entry->capacity;
    printf("key: %s\nBody Size: %zu\n", entry->key, 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*)entry->buffer,
                        (uint32)body_size);
    Seobeo_Handle_Flush(p_cli_handle);
  }
  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);
  }

clean_up:
  if (p_cli_handle)
    Seobeo_Handle_Destroy(p_cli_handle);
  if (p_response_arena)
    Dowa_Arena_Destroy(p_response_arena);
}


int Seobeo_Web_Header_Parse(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;

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

  Dowa_HashMap_Push_Value_With_Type(map, "HTTP_Method",  method,  strlen(method)  + 1, DOWA_HASH_MAP_TYPE_STRING);
  printf("Method: %s Pointer %p\n\n", Dowa_HashMap_Get(map, "HTTP_Method"), map);
  Dowa_HashMap_Push_Value_With_Type(map, "Version", version, strlen(version) + 1, DOWA_HASH_MAP_TYPE_STRING);

  // 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
  Dowa_HashMap_Push_Value_With_Type(
    map,
    "Path",
    raw_path,
    strlen(raw_path) + 1,
    DOWA_HASH_MAP_TYPE_STRING);
  
  // 2) If there *is* a query, tokenize into a sub-map
  if (query_str && *query_str)
  {
    // create nested map for GET params
    Dowa_PHashMap p_query_map = Dowa_HashMap_Create_With_Arena(100, map->p_arena);

    char *cur = query_str;
    while (cur && *cur)
    {
      // find the next '&'
      char *next_amp = strchr(cur, '&');
      // if none, treat end-of-string as the boundary
      char *pair_end = next_amp ? next_amp : cur + strlen(cur);
    
      // find '=' in [cur, pair_end)
      char *eq = memchr(cur, '=', pair_end - cur);
      if (eq) {
        size_t key_len = eq - cur;
        size_t val_len = pair_end - (eq + 1);
    
        // extract key
        char key_buf[key_len + 1];
        memcpy(key_buf, cur, key_len);
        key_buf[key_len] = '\0';
    
        // extract value
        char val_buf[val_len + 1];
        memcpy(val_buf, eq + 1, val_len);
        val_buf[val_len] = '\0';
    
        printf("key: '%s', value: '%s'\n", key_buf, val_buf);
        // push into map with strlen(val_buf)+1 to include '\0'
        Dowa_HashMap_Push_Value_With_Type(
          p_query_map,
          key_buf,
          val_buf,
          (uint32_t)(val_len + 1),
          DOWA_HASH_MAP_TYPE_STRING);
      }
    
      // advance past '&' if present, else end loop
      cur = next_amp ? next_amp + 1 : NULL;
    } 
    if (
        Dowa_HashMap_Push_Value_With_Type_NoCopy(
          map,
          "QueryParams",
          p_query_map,
          sizeof(p_query_map),
          DOWA_HASH_MAP_TYPE_HASHMAP) == -1
        )
    {
      printf("Something went wrong...\n\n");
    }
  }

  // int qp = Dowa_HashMap_Get_Position(map, "QueryParams");
  // Dowa_PHashEntry 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 = 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_Push_Value_With_Type(map, key, val, value_len + 1, DOWA_HASH_MAP_TYPE_STRING);

      printf("Capacity: %d, Length: %d ", (int)map->p_arena->capacity, (int)map->p_arena->offset);
      printf("value_len: %d, key: %s value %s position: %d\n\n", (int)value_len + 1, key, val, Dowa_HashMap_Get_Position(map, key));
      printf("Method: %s Position: %d Pointer %p\n\n", Dowa_HashMap_Get(map, "HTTP_Method"), Dowa_HashMap_Get_Position(map, "HTTP_Method"), map);

      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_Get_Position(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_Push_Value(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_Server_Start(
    const char       *folder_path,
    const char       *port,
    Seobeo_ServerMode mode,
    int                thread_count)
{
  Dowa_PHashMap p_html_cache = Dowa_HashMap_Create(200);
  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_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_PHandle cli =
        Seobeo_Stream_Handle_Server_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)
  {
    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_PHandle h = Seobeo_Stream_Handle_Client_Create(host, port, TRUE);
  if (!h || h->socket < 0)
  {
    if (h)
      Seobeo_Handle_Destroy(h);
    return -1;
  }

  Dowa_PArena 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;
}

void Seobeo_Web_SSL_Init()
{
  SSL_load_error_strings();
  OpenSSL_add_ssl_algorithms();
}

void Seobeo_Web_SSL_Cleanup(void)
{
  EVP_cleanup(); // I don't think these are needed...
}