view seobeo/s_web.c @ 60:d64a8c189a77

Merged
author June Park <me@mrjunejune.com>
date Sat, 20 Dec 2025 13:56:01 -0500
parents 84672efec192
children ea9ef388ab97
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)
{
  printf("p_cli_handle: %p", p_cli_handle);
  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;
    return;
  }

  // Dowa_HashMap_Print(p_req_map);

  // Extract method (GET, POST, etc.)
  const char *method = (const char*)Dowa_HashMap_Get(p_req_map, "HTTP_Method");
  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;
  }

  // --- 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);
    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");
  return;
//  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);

      Dowa_Free(key);
      Dowa_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);
    Dowa_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 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_2(p_server_handle, p_html_cache);
    // 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...
}