changeset 119:c39582f937e5

[Seobeo Client] Added client side logic which will be used for all my other calls instead of curl.
author June Park <parkjune1995@gmail.com>
date Wed, 07 Jan 2026 16:05:57 -0800
parents 249881ceff7b
children cbbf78b17cfa
files .hgignore seobeo/BUILD seobeo/s_http_client.c seobeo/s_linux_network.c seobeo/s_network.c seobeo/s_web.c seobeo/seobeo.h seobeo/seobeo_internal.h seobeo/tests/seobeo_client_test.c
diffstat 9 files changed, 1313 insertions(+), 486 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Jan 07 13:24:38 2026 -0800
+++ b/.hgignore	Wed Jan 07 16:05:57 2026 -0800
@@ -34,3 +34,6 @@
 
 # Keys and stuff
 .env
+
+# claude config
+.claude/settings.local.json
--- a/seobeo/BUILD	Wed Jan 07 13:24:38 2026 -0800
+++ b/seobeo/BUILD	Wed Jan 07 16:05:57 2026 -0800
@@ -13,7 +13,6 @@
   visibility = ["//visibility:public"],
 )
 
-# Server-only target (no SSL, no OpenSSL dependency) - Production
 alias(
   name = "seobeo_server",
   actual = select({
@@ -24,7 +23,6 @@
   visibility = ["//visibility:public"],
 )
 
-# Server-only target (no SSL, no OpenSSL dependency) - Development with Debug Logs
 alias(
   name = "seobeo_server_dev",
   actual = select({
@@ -38,7 +36,7 @@
 cc_library(
   name = "seobeo_server_macos",
   srcs = [
-    "s_linux_network.c",
+    "s__network.c",
     "s_web.c",
     "s_ssl.c",
     "os/s_macos_edge.c",
@@ -57,7 +55,7 @@
 cc_library(
   name = "seobeo_server_macos_dev",
   srcs = [
-    "s_linux_network.c",
+    "s_network.c",
     "s_web.c",
     "s_ssl.c",
     "os/s_macos_edge.c",
@@ -76,7 +74,7 @@
 cc_library(
   name = "seobeo_server_linux",
   srcs = [
-    "s_linux_network.c",
+    "s_network.c",
     "s_web.c",
     "s_ssl.c",
     "os/s_linux_edge.c",
@@ -95,7 +93,7 @@
 cc_library(
   name = "seobeo_server_linux_dev",
   srcs = [
-    "s_linux_network.c",
+    "s_network.c",
     "s_web.c",
     "s_ssl.c",
     "os/s_linux_edge.c",
@@ -125,9 +123,10 @@
 cc_library(
   name = "seobeo_client_macos",
   srcs = [
-    "s_linux_network.c",
+    "s_network.c",
     "s_web.c",
     "s_ssl.c",
+    "s_http_client.c",
     "snapshot_creator.c",
     "os/s_macos_edge.c",
   ],
@@ -145,9 +144,10 @@
 cc_library(
   name = "seobeo_client_linux",
   srcs = [
-    "s_linux_network.c",
+    "s_network.c",
     "s_web.c",
     "s_ssl.c",
+    "s_http_client.c",
     "snapshot_creator.c",
     "os/s_linux_edge.c",
   ],
@@ -178,3 +178,11 @@
   timeout = "short",
 )
 
+# Examples
+cc_test(
+  name = "seobeo_client_test",
+  srcs = ["tests/seobeo_client_test.c"],
+  deps = [":seobeo_client"],
+  visibility = ["//visibility:public"],
+)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/s_http_client.c	Wed Jan 07 16:05:57 2026 -0800
@@ -0,0 +1,564 @@
+#include "seobeo/seobeo.h"
+#include <ctype.h>
+
+static void Seobeo_Client_Parse_Url(const char *url, char **p_host, 
+    char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena)
+{
+  if (!url)
+    return;
+
+  const char *start = url;
+  *p_use_tls = FALSE;
+
+  if (strncmp(url, "https://", 8) == 0)
+  {
+    *p_use_tls = TRUE;
+    start = url + 8;
+  }
+  else if (strncmp(url, "http://", 7) == 0)
+  {
+    *p_use_tls = FALSE;
+    start = url + 7;
+  }
+
+  const char *slash = strchr(start, '/');
+  const char *colon = strchr(start, ':');
+
+  if (colon && (!slash || colon < slash))
+  {
+    size_t host_len = colon - start;
+    *p_host = Dowa_Arena_Allocate(p_arena, host_len + 1);
+    memcpy(*p_host, start, host_len);
+    (*p_host)[host_len] = '\0';
+
+    const char *port_start = colon + 1;
+    size_t port_len = slash ? (slash - port_start) : strlen(port_start);
+    *p_port = Dowa_Arena_Allocate(p_arena, port_len + 1);
+    memcpy(*p_port, port_start, port_len);
+    (*p_port)[port_len] = '\0';
+  }
+  else
+  {
+    size_t host_len = slash ? (slash - start) : strlen(start);
+    *p_host = Dowa_Arena_Allocate(p_arena, host_len + 1);
+    memcpy(*p_host, start, host_len);
+    (*p_host)[host_len] = '\0';
+
+    *p_port = Dowa_Arena_Allocate(p_arena, 8);
+    strcpy(*p_port, *p_use_tls ? "443" : "80");
+  }
+
+  if (slash)
+  {
+    size_t path_len = strlen(slash);
+    *p_path = Dowa_Arena_Allocate(p_arena, path_len + 1);
+    strcpy(*p_path, slash);
+  }
+  else
+  {
+    *p_path = Dowa_Arena_Allocate(p_arena, 2);
+    strcpy(*p_path, "/");
+  }
+}
+
+Seobeo_Client_Request *Seobeo_Client_Request_Create(const char *url)
+{
+  Seobeo_Client_Request *p_req = malloc(sizeof(Seobeo_Client_Request));
+  if (!p_req)
+    return NULL;
+
+  memset(p_req, 0, sizeof(Seobeo_Client_Request));
+
+  p_req->p_arena = Dowa_Arena_Create(1024 * 1024);
+  if (!p_req->p_arena)
+  {
+    free(p_req);
+    return NULL;
+  }
+
+  size_t url_len = strlen(url);
+  p_req->url = Dowa_Arena_Allocate(p_req->p_arena, url_len + 1);
+  strcpy(p_req->url, url);
+
+  Seobeo_Client_Parse_Url(url, &p_req->host, &p_req->port, &p_req->path, &p_req->use_tls, p_req->p_arena);
+
+  p_req->method = Dowa_Arena_Allocate(p_req->p_arena, 4);
+  strcpy(p_req->method, "GET");
+
+  p_req->follow_redirects = FALSE;
+  p_req->max_redirects = 10;
+
+  return p_req;
+}
+
+void Seobeo_Client_Request_Set_Method(Seobeo_Client_Request *p_req, const char *method)
+{
+  if (!p_req || !method)
+    return;
+
+  size_t len = strlen(method);
+  p_req->method = Dowa_Arena_Allocate(p_req->p_arena, len + 1);
+  strcpy(p_req->method, method);
+}
+
+void Seobeo_Client_Request_Add_Header_Map(Seobeo_Client_Request *p_req, const char *key, const char *value)
+{
+  if (!p_req || !key || !value)
+    return;
+
+  char *key_copy = Dowa_Arena_Allocate(p_req->p_arena, strlen(key) + 1);
+  strcpy(key_copy, key);
+
+  char *value_copy = Dowa_Arena_Allocate(p_req->p_arena, strlen(value) + 1);
+  strcpy(value_copy, value);
+
+  Dowa_HashMap_Push_Arena(p_req->headers_map, key_copy, value_copy, p_req->p_arena);
+}
+
+void Seobeo_Client_Request_Add_Header_Array(Seobeo_Client_Request *p_req, const char *header)
+{
+  if (!p_req || !header)
+    return;
+
+  char *header_copy = Dowa_Arena_Allocate(p_req->p_arena, strlen(header) + 1);
+  strcpy(header_copy, header);
+
+  Dowa_Array_Push_Arena(p_req->headers_array, header_copy, p_req->p_arena);
+}
+
+void Seobeo_Client_Request_Set_Body(Seobeo_Client_Request *p_req, const char *body, size_t length)
+{
+  if (!p_req || !body)
+    return;
+
+  if (length == 0)
+    length = strlen(body);
+
+  p_req->body = Dowa_Arena_Allocate(p_req->p_arena, length + 1);
+  memcpy(p_req->body, body, length);
+  p_req->body[length] = '\0';
+  p_req->body_length = length;
+}
+
+void Seobeo_Client_Request_Set_Follow_Redirects(Seobeo_Client_Request *p_req, boolean follow, int32 max_redirects)
+{
+  if (!p_req)
+    return;
+
+  p_req->follow_redirects = follow;
+  p_req->max_redirects = max_redirects > 0 ? max_redirects : 10;
+}
+
+void Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path)
+{
+  if (!p_req || !path)
+    return;
+
+  size_t len = strlen(path);
+  p_req->download_path = Dowa_Arena_Allocate(p_req->p_arena, len + 1);
+  strcpy(p_req->download_path, path);
+}
+
+static int Seobeo_Client_Build_Request_Header(Seobeo_Client_Request *p_req, char *buffer, size_t buffer_size)
+{
+  int offset = 0;
+
+  offset += snprintf(buffer + offset, buffer_size - offset,
+                     "%s %s HTTP/1.1\r\n"
+                     "Host: %s\r\n",
+                     p_req->method, p_req->path, p_req->host);
+
+  boolean has_content_length = FALSE;
+  boolean has_connection = FALSE;
+
+  if (p_req->headers_map)
+  {
+    size_t count = Dowa_Array_Length(p_req->headers_map);
+    for (size_t i = 0; i < count; i++)
+    {
+      const char *key = p_req->headers_map[i].key;
+      const char *value = p_req->headers_map[i].value;
+
+      if (strcasecmp(key, "content-length") == 0)
+        has_content_length = TRUE;
+      if (strcasecmp(key, "connection") == 0)
+        has_connection = TRUE;
+
+      offset += snprintf(buffer + offset, buffer_size - offset,
+                         "%s: %s\r\n", key, value);
+    }
+  }
+
+  if (p_req->headers_array)
+  {
+    size_t count = Dowa_Array_Length(p_req->headers_array);
+    for (size_t i = 0; i < count; i++)
+    {
+      const char *header = p_req->headers_array[i];
+
+      if (strncasecmp(header, "content-length:", 15) == 0)
+        has_content_length = TRUE;
+      if (strncasecmp(header, "connection:", 11) == 0)
+        has_connection = TRUE;
+
+      offset += snprintf(buffer + offset, buffer_size - offset,
+                         "%s\r\n", header);
+    }
+  }
+
+  if (p_req->body && !has_content_length)
+  {
+    offset += snprintf(buffer + offset, buffer_size - offset,
+                       "Content-Length: %zu\r\n", p_req->body_length);
+  }
+
+  if (!has_connection)
+  {
+    offset += snprintf(buffer + offset, buffer_size - offset,
+                       "Connection: close\r\n");
+  }
+
+  offset += snprintf(buffer + offset, buffer_size - offset, "\r\n");
+
+  return offset;
+}
+
+static Seobeo_Client_Response *Seobeo_Client_Parse_Response(Seobeo_Handle *p_handle, const char *download_path)
+{
+  Seobeo_Client_Response *p_resp = malloc(sizeof(Seobeo_Client_Response));
+  if (!p_resp)
+    return NULL;
+
+  memset(p_resp, 0, sizeof(Seobeo_Client_Response));
+
+  p_resp->p_arena = Dowa_Arena_Create(1024 * 1024);
+  if (!p_resp->p_arena)
+  {
+    free(p_resp);
+    return NULL;
+  }
+
+  while (1)
+  {
+    int r = Seobeo_Handle_Read(p_handle);
+    if (r < 0)
+      return p_resp;
+    if (r == -2)
+      break;
+
+    if (p_handle->read_buffer_len >= 4 && strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL)
+      break;
+
+    if (r == 0)
+      continue;
+  }
+
+  char *buf = (char*)p_handle->read_buffer;
+  char *hdr_end = strstr(buf, "\r\n\r\n");
+  if (!hdr_end)
+    return p_resp;
+
+  size_t hdr_len = hdr_end - buf + 4;
+
+  char version[16];
+  int status_code;
+  char status_text[256];
+  int scan_result = sscanf(buf, "%15s %d %255[^\r\n]", version, &status_code, status_text);
+
+  if (scan_result >= 2)
+  {
+    p_resp->status_code = status_code;
+    if (scan_result >= 3)
+    {
+      size_t len = strlen(status_text);
+      p_resp->status_text = Dowa_Arena_Allocate(p_resp->p_arena, len + 1);
+      strcpy(p_resp->status_text, status_text);
+    }
+  }
+
+  char *line = buf + strlen(version) + 1;
+  while (*line && !isdigit(*line))
+    line++;
+  while (*line && isdigit(*line))
+    line++;
+  while (*line && *line == ' ')
+    line++;
+  while (*line && *line != '\r')
+    line++;
+  line += 2;
+
+  while (line < hdr_end)
+  {
+    char *next = strstr(line, "\r\n");
+    if (!next)
+      break;
+
+    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_resp->p_arena, key_len + 1);
+      memcpy(key, line, key_len);
+      key[key_len] = '\0';
+
+      char *val = Dowa_Arena_Allocate(p_resp->p_arena, value_len + 1);
+      memcpy(val, val_start, value_len);
+      val[value_len] = '\0';
+
+      Dowa_HashMap_Push_Arena(p_resp->headers, key, val, p_resp->p_arena);
+
+      if (strcasecmp(key, "Location") == 0)
+      {
+        p_resp->redirect_url = Dowa_Arena_Allocate(p_resp->p_arena, value_len + 1);
+        strcpy(p_resp->redirect_url, val);
+      }
+    }
+
+    line = next + 2;
+  }
+
+  Seobeo_Handle_Consume(p_handle, (uint32)hdr_len);
+
+  void *p_cl_kv = Dowa_HashMap_Get_Ptr(p_resp->headers, "Content-Length");
+  size_t body_len = 0;
+  if (p_cl_kv)
+  {
+    const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value;
+    body_len = atoi(content_length_str);
+  }
+
+  FILE *p_file = NULL;
+  if (download_path)
+  {
+    p_file = fopen(download_path, "wb");
+    if (!p_file)
+    {
+      Seobeo_Log(SEOBEO_ERROR, "Failed to open file for writing: %s\n", download_path);
+      return p_resp;
+    }
+  }
+
+  if (body_len > 0)
+  {
+    char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, body_len + 1);
+    size_t total_read = 0;
+
+    while (total_read < body_len)
+    {
+      size_t available = p_handle->read_buffer_len;
+      size_t to_copy = (body_len - total_read) < available ? (body_len - total_read) : available;
+
+      if (to_copy > 0)
+      {
+        if (download_path)
+          fwrite(p_handle->read_buffer, 1, to_copy, p_file);
+        else
+          memcpy(body + total_read, p_handle->read_buffer, to_copy);
+
+        total_read += to_copy;
+        Seobeo_Handle_Consume(p_handle, (uint32)to_copy);
+      }
+
+      if (total_read < body_len)
+      {
+        int r = Seobeo_Handle_Read(p_handle);
+        if (r < 0 || r == -2)
+          break;
+        if (r == 0)
+          continue;
+      }
+    }
+
+    if (!download_path)
+    {
+      body[body_len] = '\0';
+      p_resp->body = body;
+      p_resp->body_length = body_len;
+    }
+    else
+    {
+      p_resp->body_length = total_read;
+    }
+  }
+  else
+  {
+    size_t cap = 1024 * 8;
+    size_t used = 0;
+    char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, cap);
+
+    while (1)
+    {
+      int n = Seobeo_Handle_Read(p_handle);
+      if (n > 0)
+      {
+        if (download_path)
+        {
+          fwrite(p_handle->read_buffer, 1, p_handle->read_buffer_len, p_file);
+          used += p_handle->read_buffer_len;
+        }
+        else
+        {
+          if (used + p_handle->read_buffer_len >= cap)
+          {
+            Seobeo_Log(SEOBEO_WARNING, "Response body too large, truncating...\n");
+            break;
+          }
+          memcpy(body + used, p_handle->read_buffer, p_handle->read_buffer_len);
+          used += p_handle->read_buffer_len;
+        }
+        Seobeo_Handle_Consume(p_handle, (uint32)p_handle->read_buffer_len);
+      }
+      else if (n == -2)
+        break;
+      else if (n == 0)
+        continue;
+      else
+        break;
+    }
+
+    if (!download_path)
+    {
+      p_resp->body = body;
+      p_resp->body_length = used;
+      if (used < cap)
+        body[used] = '\0';
+    }
+    else
+    {
+      p_resp->body_length = used;
+    }
+  }
+
+  if (p_file)
+    fclose(p_file);
+
+  return p_resp;
+}
+
+static Seobeo_Client_Response *Seobeo_Client_Execute_Single(Seobeo_Client_Request *p_req)
+{
+  Seobeo_Handle *p_handle = Seobeo_Stream_Handle_Client_Create(p_req->host, p_req->port, p_req->use_tls);
+  if (!p_handle || p_handle->socket < 0)
+  {
+    if (p_handle)
+      Seobeo_Handle_Destroy(p_handle);
+    return NULL;
+  }
+
+  char request_buffer[8192];
+  int request_len = Seobeo_Client_Build_Request_Header(p_req, request_buffer, sizeof(request_buffer));
+
+  Seobeo_Handle_Queue(p_handle, (uint8*)request_buffer, (uint32)request_len);
+
+  if (p_req->body && p_req->body_length > 0)
+    Seobeo_Handle_Queue(p_handle, (uint8*)p_req->body, (uint32)p_req->body_length);
+
+  if (Seobeo_Handle_Flush(p_handle) < 0)
+  {
+    Seobeo_Handle_Destroy(p_handle);
+    return NULL;
+  }
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Parse_Response(p_handle, p_req->download_path);
+
+  Seobeo_Handle_Destroy(p_handle);
+
+  return p_resp;
+}
+
+Seobeo_Client_Response *Seobeo_Client_Request_Execute(Seobeo_Client_Request *p_req)
+{
+  if (!p_req)
+    return NULL;
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Execute_Single(p_req);
+
+  if (!p_resp)
+    return NULL;
+
+  int redirect_count = 0;
+  while (p_req->follow_redirects &&
+         p_resp->redirect_url &&
+         (p_resp->status_code == 301 || p_resp->status_code == 302 ||
+          p_resp->status_code == 303 || p_resp->status_code == 307 ||
+          p_resp->status_code == 308) &&
+         redirect_count < p_req->max_redirects)
+  {
+    char *redirect_url = malloc(strlen(p_resp->redirect_url) + 1);
+    strcpy(redirect_url, p_resp->redirect_url);
+
+    Seobeo_Client_Response_Destroy(p_resp);
+
+    // Relative redirect
+    if (redirect_url[0] == '/')
+    {
+      size_t path_len = strlen(redirect_url);
+      p_req->path = Dowa_Arena_Allocate(p_req->p_arena, path_len + 1);
+      strcpy(p_req->path, redirect_url);
+
+      size_t url_len = strlen(p_req->host) + strlen(p_req->port) + path_len + 16;
+      p_req->url = Dowa_Arena_Allocate(p_req->p_arena, url_len);
+      snprintf(p_req->url, url_len, "%s://%s:%s%s",
+               p_req->use_tls ? "https" : "http",
+               p_req->host, p_req->port, p_req->path);
+    }
+    else
+    {
+      size_t url_len = strlen(redirect_url);
+      p_req->url = Dowa_Arena_Allocate(p_req->p_arena, url_len + 1);
+      strcpy(p_req->url, redirect_url);
+
+      Seobeo_Client_Parse_Url(redirect_url, &p_req->host, &p_req->port, &p_req->path, &p_req->use_tls, p_req->p_arena);
+    }
+
+    free(redirect_url);
+
+    p_resp = Seobeo_Client_Execute_Single(p_req);
+    if (!p_resp)
+      return NULL;
+
+    redirect_count++;
+  }
+
+  return p_resp;
+}
+
+void Seobeo_Client_Request_Destroy(Seobeo_Client_Request *p_req)
+{
+  if (!p_req)
+    return;
+
+  if (p_req->p_arena)
+    Dowa_Arena_Free(p_req->p_arena);
+
+  if (p_req->headers_map)
+    Dowa_HashMap_Free(p_req->headers_map);
+
+  if (p_req->headers_array)
+    Dowa_Array_Free(p_req->headers_array);
+
+  free(p_req);
+}
+
+void Seobeo_Client_Response_Destroy(Seobeo_Client_Response *p_resp)
+{
+  if (!p_resp)
+    return;
+
+  if (p_resp->p_arena)
+    Dowa_Arena_Free(p_resp->p_arena);
+
+  if (p_resp->headers)
+    Dowa_HashMap_Free(p_resp->headers);
+
+  free(p_resp);
+}
--- a/seobeo/s_linux_network.c	Wed Jan 07 13:24:38 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,380 +0,0 @@
-#include "seobeo/seobeo.h"
-
-
-Seobeo_Handle *Seobeo_Stream_Handle_Server_Create(const char *host,  const char* port)
-{
-  Seobeo_Handle *p_handle;
-  struct addrinfo hints, *server_infos, *free_server_info;
-  int32 socket_fd, yes = 1;  // Need this for setsockopt 
-
-  memset(&hints, 0, sizeof hints);
-  hints.ai_family   = AF_UNSPEC;
-  hints.ai_socktype = SOCK_STREAM;
-  hints.ai_protocol = IPPROTO_TCP;
-  hints.ai_flags    = AI_PASSIVE;
-
-  if (getaddrinfo(host, port, &hints, &server_infos) != 0)
-  { perror("getaddrinfo"); return NULL; }
-
-  for
-  (
-    free_server_info = server_infos;
-    free_server_info != NULL;
-    free_server_info = free_server_info->ai_next
-  )
-  {
-    if((socket_fd = socket(free_server_info->ai_family,
-                        free_server_info->ai_socktype, free_server_info->ai_protocol)) == -1)
-    { perror("socket"); continue; }
-
-     if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1)
-     { perror("setsockopt SO_REUSEADDR"); continue; }
-
-#ifdef SO_REUSEPORT
-     // SO_REUSEPORT allows multiple threads/processes to bind to the same port
-     // The kernel will distribute incoming connections among them
-     if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)) == -1)
-     { perror("setsockopt SO_REUSEPORT"); continue; }
-#endif
-
-     if (bind(socket_fd, free_server_info->ai_addr, free_server_info->ai_addrlen) == -1)
-     { perror("v_network: Couldn't make socket non-blocking\n"); continue; }
-
-     break;
-  }
-
-  if (listen(socket_fd, 16) != 0)
-  { 
-    Seobeo_Log(SEOBEO_DEBUG, "Closing socket: %d\n", socket_fd);
-    perror("listen"); close(socket_fd); return NULL; 
-  }
-
-  int flags = fcntl(socket_fd, F_GETFL, 0);
-	if(fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK) != 0) { perror("fcntl"); return NULL; }
-  freeaddrinfo(server_infos);
-
-  p_handle = malloc(sizeof(*p_handle));
-  p_handle->socket = socket_fd;
-  p_handle->type = SEOBEO_STREAM_TYPE_SERVER;
-  p_handle->connected = FALSE;
-
-  p_handle->host = host != NULL ? strdup(host) : "localhost";
-  p_handle->port = strdup(port);
-
-  p_handle->ssl_ctx              = NULL;
-  p_handle->ssl                  = NULL;
-
-
-  p_handle->read_buffer = malloc(sizeof(*p_handle->read_buffer) * INITIAL_BUFFER_CAPACITY);
-  p_handle->read_buffer_capacity = INITIAL_BUFFER_CAPACITY;
-  p_handle->read_buffer_len = 0;
-
-  p_handle->write_buffer = malloc(sizeof(*p_handle->read_buffer) * INITIAL_BUFFER_CAPACITY);
-  p_handle->write_buffer_capacity = INITIAL_BUFFER_CAPACITY;
-  p_handle->write_buffer_len = 0;
-
-  p_handle->destroyed = FALSE;
-
-  return p_handle;
-}
-
-
-Seobeo_Handle *Seobeo_Stream_Handle_Client_Create(const char *host,  const char* port, boolean use_tls)
-{
-  Seobeo_Handle *p_handle;
-  p_handle = malloc(sizeof(*p_handle));
-
-  struct addrinfo hints, *server_infos;
-  int32 socket_fd;  // Need this for setsockopt 
-
-  memset(&hints, 0, sizeof hints);
-  hints.ai_family   = AF_UNSPEC;
-  hints.ai_socktype = SOCK_STREAM;
-
-  if (getaddrinfo(host, port, &hints, &server_infos) != 0)
-  { perror("getaddrinfo"); return NULL; }
-
-
-  if((socket_fd = socket(server_infos->ai_family,
-                      server_infos->ai_socktype, server_infos->ai_protocol)) == -1)
-  { perror("socket"); return NULL; }
-
-  if (connect(socket_fd, server_infos->ai_addr, server_infos->ai_addrlen) != 0)
-  { perror("connect"); return NULL; }
-  freeaddrinfo(server_infos);
-
-  p_handle->socket = socket_fd;
-  p_handle->type = SEOBEO_STREAM_TYPE_CLIENT;
-
-  p_handle->ssl_ctx = NULL;
-  p_handle->ssl = NULL;
-
-  if (use_tls)
-  {
-    if (Seobeo_SSL_Setup_Client(p_handle, host, socket_fd) != 0)
-    {
-      free(p_handle);
-      return NULL;
-    }
-  }
-  p_handle->connected = TRUE;
-
-  p_handle->host = host != NULL ? strdup(host) : "localhost";
-  p_handle->port = strdup(port);
-
-  p_handle->read_buffer = malloc(sizeof(*p_handle->read_buffer) * INITIAL_BUFFER_CAPACITY);
-  p_handle->read_buffer_capacity = INITIAL_BUFFER_CAPACITY;
-  p_handle->read_buffer_len = 0;
-
-  p_handle->write_buffer = malloc(sizeof(*p_handle->read_buffer) * INITIAL_BUFFER_CAPACITY);
-  p_handle->write_buffer_capacity = INITIAL_BUFFER_CAPACITY;
-  p_handle->write_buffer_len = 0;
-
-  p_handle->destroyed = FALSE;
-
-  return p_handle;
-}
-
-Seobeo_Handle *Seobeo_Stream_Handle_Server_Accept(Seobeo_Handle *p_server_handle)
-{
-  struct sockaddr_storage addr;
-  socklen_t addrlen = sizeof addr;
-  char client_inet_addr[INET6_ADDRSTRLEN];
-
-  int client_fd = accept(p_server_handle->socket,
-                         (struct sockaddr*)&addr,
-                         &addrlen);
-  inet_ntop(
-      addr.ss_family,
-      Seobeo_Get_IP4_Or_IP6((struct sockaddr *)&addr),
-      client_inet_addr, sizeof client_inet_addr);
-  if (client_fd == -1) return NULL;
-
-  // Set non blocking...
-  int flags = fcntl(client_fd, F_GETFL, 0);
-  if (flags == -1) return NULL;
-  fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
-
-  Seobeo_Handle *p_client_handle        = malloc(sizeof *p_client_handle);
-
-  p_client_handle->socket               = client_fd;
-  p_client_handle->type                 = SEOBEO_STREAM_TYPE_CLIENT;
-  p_client_handle->connected            = TRUE;
-
-  // TODO: support SSL in the future.
-  p_client_handle->ssl_ctx              = NULL;
-  p_client_handle->ssl                  = NULL;
-
-  p_client_handle->host                 = strdup(client_inet_addr);
-  p_client_handle->port                 = NULL;
-
-  p_client_handle->read_buffer_capacity = p_server_handle->read_buffer_capacity;
-  p_client_handle->read_buffer_len      = 0;
-  p_client_handle->read_buffer          = malloc(p_client_handle->read_buffer_capacity);
-
-  p_client_handle->write_buffer_capacity = p_server_handle->write_buffer_capacity;
-  p_client_handle->write_buffer_len      = 0;
-  p_client_handle->write_buffer          = malloc(p_client_handle->write_buffer_capacity);
-
-  p_client_handle->read_buffer_used      = 0;
-  p_client_handle->file                  = NULL;
-  p_client_handle->text_copy             = NULL;
-  p_client_handle->file_name             = NULL;
-  p_client_handle->destroyed             = FALSE;
-
-  return p_client_handle;
-}
-
-void Seobeo_Handle_Destroy(Seobeo_Handle *p_handle)
-{
-  if (!p_handle) return;
-
-  boolean expected = FALSE;
-  // Need to check
-  if (!atomic_compare_exchange_strong(&p_handle->destroyed, &expected, TRUE))
-  {
-    return;
-  }
-
-  if (p_handle->host) Dowa_Free(p_handle->host);
-  if (p_handle->port) Dowa_Free(p_handle->port);
-
-  Seobeo_SSL_Cleanup(p_handle);
-
-  if (p_handle->socket) {
-    Seobeo_Log(SEOBEO_DEBUG, "Closing handle socket: %d\n", p_handle->socket);
-    close(p_handle->socket);
-  }
-
-  if (p_handle->read_buffer) Dowa_Free(p_handle->read_buffer);
-  if (p_handle->write_buffer) Dowa_Free(p_handle->write_buffer);
-
-  Dowa_Free(p_handle);
-}
-
-
-int32 Seobeo_Handle_Flush(Seobeo_Handle *p_handle)
-{
-  uint32 total = p_handle->write_buffer_len;
-  uint32 sent  = 0;
-
-  Seobeo_Log(SEOBEO_DEBUG, "Write buffer total: %d\n", p_handle->write_buffer_len);
-
-  while (sent < total)
-  {
-    if (p_handle->ssl)
-    {
-      int n = Seobeo_SSL_Write(p_handle, p_handle->write_buffer + sent, total - sent);
-      if (n < 0) return -1;
-      if (n == 0) return 0;  // would block
-      sent += (uint32)n;
-    }else
-    {
-      Seobeo_Log(SEOBEO_DEBUG, "Flushing socket: %d\n", p_handle->socket);
-      ssize_t n = write(
-        p_handle->socket,
-        p_handle->write_buffer + sent,
-        total - sent
-      );
-      if (n < 0) {
-        if (errno == EINTR)  continue;
-        if (errno == EAGAIN) return 1;
-        return -1;
-      }
-      sent += (uint32)n;
-    }
-  }
-
-  p_handle->write_buffer_len = 0;
-  return 0;
-}
-
-int32 Seobeo_Handle_Queue(Seobeo_Handle *p_handle, const uint8 *data, uint32 data_size)
-{
-  if (p_handle->write_buffer_len + data_size > p_handle->write_buffer_capacity)
-  {
-    int32 rc = Seobeo_Handle_Flush(p_handle);
-    if (rc < 0) return -1;
-    if (rc > 0) return 1;
-  }
-
-  if (data_size > p_handle->write_buffer_capacity)
-  {
-    uint32 offset = 0;
-    while (offset < data_size)
-    {
-      ssize_t n = write(p_handle->socket,
-                data + offset,
-                data_size - offset);
-      if (n==0)
-      {
-        // DEBUG
-        Seobeo_Log(SEOBEO_DEBUG, "Write offset: %d\n", offset);
-        break;
-      }
-      if (n < 0)
-      {
-        if (errno == EINTR || errno == EAGAIN)
-        {
-          // DEBUG
-          // printf("Partial write, returning early (offset=%d)\n", offset);
-          continue;
-        }
-        if (errno == EAGAIN) return 1;
-        return -1;
-      }
-      offset += (uint32)n;
-      // DEBUG
-      Seobeo_Log(SEOBEO_DEBUG, "Write completed - offset: %d, data_size: %d\n", offset, data_size);
-    }
-    // DEBUG
-    Seobeo_Log(SEOBEO_DEBUG, "Total bytes written: %d\n", offset);
-    return 0;
-  }
-
-  if (!p_handle)
-  {
-    Seobeo_Log(SEOBEO_ERROR, "p_handle is NULL before memcpy\n");
-    return -1;
-  }
-  
-  if (!p_handle->write_buffer)
-  {
-    Seobeo_Log(SEOBEO_ERROR, "p_handle->write_buffer is NULL (len=%u, size=%u)\n",
-            p_handle->write_buffer_len, data_size);
-    return -1;
-  }
-  
-  Seobeo_Log(SEOBEO_DEBUG, "memcpy -> dest=%p (write_buffer=%p + offset=%u), src=%p, size=%u\n",
-          p_handle->write_buffer + p_handle->write_buffer_len,
-          p_handle->write_buffer,
-          p_handle->write_buffer_len,
-          data,
-          data_size); 
-
-  memcpy(p_handle->write_buffer + p_handle->write_buffer_len,
-         data,
-         data_size);
-  p_handle->write_buffer_len += data_size;
-  return 0;
-}
-
-int32 Seobeo_Handle_Read(Seobeo_Handle *p_handle)
-{
-  int32 read_size;
-  if (!p_handle) return -1;
-
-  // How many bytes we can still read into the buffer
-  uint32 free_space = p_handle->read_buffer_capacity - p_handle->read_buffer_len;
-  if (free_space == 0)
-    return -1;
-
-  if (p_handle->ssl)
-  {
-    read_size = Seobeo_SSL_Read(p_handle, p_handle->read_buffer + p_handle->read_buffer_len, free_space);
-    if (read_size < 0) return read_size;  // -1 for error, -2 for closed
-    if (read_size == 0) return 0;  // would block
-  }
-  else
-  {
-    read_size = (int32)read(p_handle->socket,
-                            p_handle->read_buffer + p_handle->read_buffer_len,
-                            free_space);
-    if (read_size == 0) return -2; 
-    if (read_size < 0)
-    {
-      if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
-      return -1;
-    }
-  }
-
-  p_handle->read_buffer_len += (uint32)read_size;
-  return read_size;
-}
-
-void Seobeo_Handle_Consume(Seobeo_Handle *p_handle, uint32 consumed)
-{
-  if (consumed >= p_handle->read_buffer_len)
-  {
-    p_handle->read_buffer_len = 0;
-    return;
-  }
-
-  // Slide remaining bytes to the front
-  memmove(
-    p_handle->read_buffer,
-    p_handle->read_buffer + consumed,
-    p_handle->read_buffer_len - consumed
-  );
-  p_handle->read_buffer_len -= consumed;
-}
-
-void *Seobeo_Get_IP4_Or_IP6(struct sockaddr *sa)
-{
-  if (sa->sa_family == AF_INET) 
-  {
-    return &(((struct sockaddr_in*)sa)->sin_addr);
-  }
-
-  return &(((struct sockaddr_in6*)sa)->sin6_addr);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/s_network.c	Wed Jan 07 16:05:57 2026 -0800
@@ -0,0 +1,380 @@
+#include "seobeo/seobeo.h"
+
+
+Seobeo_Handle *Seobeo_Stream_Handle_Server_Create(const char *host,  const char* port)
+{
+  Seobeo_Handle *p_handle;
+  struct addrinfo hints, *server_infos, *free_server_info;
+  int32 socket_fd, yes = 1;  // Need this for setsockopt 
+
+  memset(&hints, 0, sizeof hints);
+  hints.ai_family   = AF_UNSPEC;
+  hints.ai_socktype = SOCK_STREAM;
+  hints.ai_protocol = IPPROTO_TCP;
+  hints.ai_flags    = AI_PASSIVE;
+
+  if (getaddrinfo(host, port, &hints, &server_infos) != 0)
+  { perror("getaddrinfo"); return NULL; }
+
+  for
+  (
+    free_server_info = server_infos;
+    free_server_info != NULL;
+    free_server_info = free_server_info->ai_next
+  )
+  {
+    if((socket_fd = socket(free_server_info->ai_family,
+                        free_server_info->ai_socktype, free_server_info->ai_protocol)) == -1)
+    { perror("socket"); continue; }
+
+     if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1)
+     { perror("setsockopt SO_REUSEADDR"); continue; }
+
+#ifdef SO_REUSEPORT
+     // SO_REUSEPORT allows multiple threads/processes to bind to the same port
+     // The kernel will distribute incoming connections among them
+     if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)) == -1)
+     { perror("setsockopt SO_REUSEPORT"); continue; }
+#endif
+
+     if (bind(socket_fd, free_server_info->ai_addr, free_server_info->ai_addrlen) == -1)
+     { perror("v_network: Couldn't make socket non-blocking\n"); continue; }
+
+     break;
+  }
+
+  if (listen(socket_fd, 16) != 0)
+  { 
+    Seobeo_Log(SEOBEO_DEBUG, "Closing socket: %d\n", socket_fd);
+    perror("listen"); close(socket_fd); return NULL; 
+  }
+
+  int flags = fcntl(socket_fd, F_GETFL, 0);
+	if(fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK) != 0) { perror("fcntl"); return NULL; }
+  freeaddrinfo(server_infos);
+
+  p_handle = malloc(sizeof(*p_handle));
+  p_handle->socket = socket_fd;
+  p_handle->type = SEOBEO_STREAM_TYPE_SERVER;
+  p_handle->connected = FALSE;
+
+  p_handle->host = host != NULL ? strdup(host) : "localhost";
+  p_handle->port = strdup(port);
+
+  p_handle->ssl_ctx              = NULL;
+  p_handle->ssl                  = NULL;
+
+
+  p_handle->read_buffer = malloc(sizeof(*p_handle->read_buffer) * INITIAL_BUFFER_CAPACITY);
+  p_handle->read_buffer_capacity = INITIAL_BUFFER_CAPACITY;
+  p_handle->read_buffer_len = 0;
+
+  p_handle->write_buffer = malloc(sizeof(*p_handle->read_buffer) * INITIAL_BUFFER_CAPACITY);
+  p_handle->write_buffer_capacity = INITIAL_BUFFER_CAPACITY;
+  p_handle->write_buffer_len = 0;
+
+  p_handle->destroyed = FALSE;
+
+  return p_handle;
+}
+
+
+Seobeo_Handle *Seobeo_Stream_Handle_Client_Create(const char *host,  const char* port, boolean use_tls)
+{
+  Seobeo_Handle *p_handle;
+  p_handle = malloc(sizeof(*p_handle));
+
+  struct addrinfo hints, *server_infos;
+  int32 socket_fd;  // Need this for setsockopt 
+
+  memset(&hints, 0, sizeof hints);
+  hints.ai_family   = AF_UNSPEC;
+  hints.ai_socktype = SOCK_STREAM;
+
+  if (getaddrinfo(host, port, &hints, &server_infos) != 0)
+  { perror("getaddrinfo"); return NULL; }
+
+
+  if((socket_fd = socket(server_infos->ai_family,
+                      server_infos->ai_socktype, server_infos->ai_protocol)) == -1)
+  { perror("socket"); return NULL; }
+
+  if (connect(socket_fd, server_infos->ai_addr, server_infos->ai_addrlen) != 0)
+  { perror("connect"); return NULL; }
+  freeaddrinfo(server_infos);
+
+  p_handle->socket = socket_fd;
+  p_handle->type = SEOBEO_STREAM_TYPE_CLIENT;
+
+  p_handle->ssl_ctx = NULL;
+  p_handle->ssl = NULL;
+
+  if (use_tls)
+  {
+    if (Seobeo_SSL_Setup_Client(p_handle, host, socket_fd) != 0)
+    {
+      free(p_handle);
+      return NULL;
+    }
+  }
+  p_handle->connected = TRUE;
+
+  p_handle->host = host != NULL ? strdup(host) : "localhost";
+  p_handle->port = strdup(port);
+
+  p_handle->read_buffer = malloc(sizeof(*p_handle->read_buffer) * INITIAL_BUFFER_CAPACITY);
+  p_handle->read_buffer_capacity = INITIAL_BUFFER_CAPACITY;
+  p_handle->read_buffer_len = 0;
+
+  p_handle->write_buffer = malloc(sizeof(*p_handle->read_buffer) * INITIAL_BUFFER_CAPACITY);
+  p_handle->write_buffer_capacity = INITIAL_BUFFER_CAPACITY;
+  p_handle->write_buffer_len = 0;
+
+  p_handle->destroyed = FALSE;
+
+  return p_handle;
+}
+
+Seobeo_Handle *Seobeo_Stream_Handle_Server_Accept(Seobeo_Handle *p_server_handle)
+{
+  struct sockaddr_storage addr;
+  socklen_t addrlen = sizeof addr;
+  char client_inet_addr[INET6_ADDRSTRLEN];
+
+  int client_fd = accept(p_server_handle->socket,
+                         (struct sockaddr*)&addr,
+                         &addrlen);
+  inet_ntop(
+      addr.ss_family,
+      Seobeo_Get_IP4_Or_IP6((struct sockaddr *)&addr),
+      client_inet_addr, sizeof client_inet_addr);
+  if (client_fd == -1) return NULL;
+
+  // Set non blocking...
+  int flags = fcntl(client_fd, F_GETFL, 0);
+  if (flags == -1) return NULL;
+  fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
+
+  Seobeo_Handle *p_client_handle        = malloc(sizeof *p_client_handle);
+
+  p_client_handle->socket               = client_fd;
+  p_client_handle->type                 = SEOBEO_STREAM_TYPE_CLIENT;
+  p_client_handle->connected            = TRUE;
+
+  // TODO: support SSL in the future.
+  p_client_handle->ssl_ctx              = NULL;
+  p_client_handle->ssl                  = NULL;
+
+  p_client_handle->host                 = strdup(client_inet_addr);
+  p_client_handle->port                 = NULL;
+
+  p_client_handle->read_buffer_capacity = p_server_handle->read_buffer_capacity;
+  p_client_handle->read_buffer_len      = 0;
+  p_client_handle->read_buffer          = malloc(p_client_handle->read_buffer_capacity);
+
+  p_client_handle->write_buffer_capacity = p_server_handle->write_buffer_capacity;
+  p_client_handle->write_buffer_len      = 0;
+  p_client_handle->write_buffer          = malloc(p_client_handle->write_buffer_capacity);
+
+  p_client_handle->read_buffer_used      = 0;
+  p_client_handle->file                  = NULL;
+  p_client_handle->text_copy             = NULL;
+  p_client_handle->file_name             = NULL;
+  p_client_handle->destroyed             = FALSE;
+
+  return p_client_handle;
+}
+
+void Seobeo_Handle_Destroy(Seobeo_Handle *p_handle)
+{
+  if (!p_handle) return;
+
+  boolean expected = FALSE;
+  // Need to check
+  if (!atomic_compare_exchange_strong(&p_handle->destroyed, &expected, TRUE))
+  {
+    return;
+  }
+
+  if (p_handle->host) Dowa_Free(p_handle->host);
+  if (p_handle->port) Dowa_Free(p_handle->port);
+
+  Seobeo_SSL_Cleanup(p_handle);
+
+  if (p_handle->socket) {
+    Seobeo_Log(SEOBEO_DEBUG, "Closing handle socket: %d\n", p_handle->socket);
+    close(p_handle->socket);
+  }
+
+  if (p_handle->read_buffer) Dowa_Free(p_handle->read_buffer);
+  if (p_handle->write_buffer) Dowa_Free(p_handle->write_buffer);
+
+  Dowa_Free(p_handle);
+}
+
+
+int32 Seobeo_Handle_Flush(Seobeo_Handle *p_handle)
+{
+  uint32 total = p_handle->write_buffer_len;
+  uint32 sent  = 0;
+
+  Seobeo_Log(SEOBEO_DEBUG, "Write buffer total: %d\n", p_handle->write_buffer_len);
+
+  while (sent < total)
+  {
+    if (p_handle->ssl)
+    {
+      int n = Seobeo_SSL_Write(p_handle, p_handle->write_buffer + sent, total - sent);
+      if (n < 0) return -1;
+      if (n == 0) return 0;  // would block
+      sent += (uint32)n;
+    }else
+    {
+      Seobeo_Log(SEOBEO_DEBUG, "Flushing socket: %d\n", p_handle->socket);
+      ssize_t n = write(
+        p_handle->socket,
+        p_handle->write_buffer + sent,
+        total - sent
+      );
+      if (n < 0) {
+        if (errno == EINTR)  continue;
+        if (errno == EAGAIN) return 1;
+        return -1;
+      }
+      sent += (uint32)n;
+    }
+  }
+
+  p_handle->write_buffer_len = 0;
+  return 0;
+}
+
+int32 Seobeo_Handle_Queue(Seobeo_Handle *p_handle, const uint8 *data, uint32 data_size)
+{
+  if (p_handle->write_buffer_len + data_size > p_handle->write_buffer_capacity)
+  {
+    int32 rc = Seobeo_Handle_Flush(p_handle);
+    if (rc < 0) return -1;
+    if (rc > 0) return 1;
+  }
+
+  if (data_size > p_handle->write_buffer_capacity)
+  {
+    uint32 offset = 0;
+    while (offset < data_size)
+    {
+      ssize_t n = write(p_handle->socket,
+                data + offset,
+                data_size - offset);
+      if (n==0)
+      {
+        // DEBUG
+        Seobeo_Log(SEOBEO_DEBUG, "Write offset: %d\n", offset);
+        break;
+      }
+      if (n < 0)
+      {
+        if (errno == EINTR || errno == EAGAIN)
+        {
+          // DEBUG
+          // printf("Partial write, returning early (offset=%d)\n", offset);
+          continue;
+        }
+        if (errno == EAGAIN) return 1;
+        return -1;
+      }
+      offset += (uint32)n;
+      // DEBUG
+      Seobeo_Log(SEOBEO_DEBUG, "Write completed - offset: %d, data_size: %d\n", offset, data_size);
+    }
+    // DEBUG
+    Seobeo_Log(SEOBEO_DEBUG, "Total bytes written: %d\n", offset);
+    return 0;
+  }
+
+  if (!p_handle)
+  {
+    Seobeo_Log(SEOBEO_ERROR, "p_handle is NULL before memcpy\n");
+    return -1;
+  }
+  
+  if (!p_handle->write_buffer)
+  {
+    Seobeo_Log(SEOBEO_ERROR, "p_handle->write_buffer is NULL (len=%u, size=%u)\n",
+            p_handle->write_buffer_len, data_size);
+    return -1;
+  }
+  
+  Seobeo_Log(SEOBEO_DEBUG, "memcpy -> dest=%p (write_buffer=%p + offset=%u), src=%p, size=%u\n",
+          p_handle->write_buffer + p_handle->write_buffer_len,
+          p_handle->write_buffer,
+          p_handle->write_buffer_len,
+          data,
+          data_size); 
+
+  memcpy(p_handle->write_buffer + p_handle->write_buffer_len,
+         data,
+         data_size);
+  p_handle->write_buffer_len += data_size;
+  return 0;
+}
+
+int32 Seobeo_Handle_Read(Seobeo_Handle *p_handle)
+{
+  int32 read_size;
+  if (!p_handle) return -1;
+
+  // How many bytes we can still read into the buffer
+  uint32 free_space = p_handle->read_buffer_capacity - p_handle->read_buffer_len;
+  if (free_space == 0)
+    return -1;
+
+  if (p_handle->ssl)
+  {
+    read_size = Seobeo_SSL_Read(p_handle, p_handle->read_buffer + p_handle->read_buffer_len, free_space);
+    if (read_size < 0) return read_size;  // -1 for error, -2 for closed
+    if (read_size == 0) return 0;  // would block
+  }
+  else
+  {
+    read_size = (int32)read(p_handle->socket,
+                            p_handle->read_buffer + p_handle->read_buffer_len,
+                            free_space);
+    if (read_size == 0) return -2; 
+    if (read_size < 0)
+    {
+      if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
+      return -1;
+    }
+  }
+
+  p_handle->read_buffer_len += (uint32)read_size;
+  return read_size;
+}
+
+void Seobeo_Handle_Consume(Seobeo_Handle *p_handle, uint32 consumed)
+{
+  if (consumed >= p_handle->read_buffer_len)
+  {
+    p_handle->read_buffer_len = 0;
+    return;
+  }
+
+  // Slide remaining bytes to the front
+  memmove(
+    p_handle->read_buffer,
+    p_handle->read_buffer + consumed,
+    p_handle->read_buffer_len - consumed
+  );
+  p_handle->read_buffer_len -= consumed;
+}
+
+void *Seobeo_Get_IP4_Or_IP6(struct sockaddr *sa)
+{
+  if (sa->sa_family == AF_INET) 
+  {
+    return &(((struct sockaddr_in*)sa)->sin_addr);
+  }
+
+  return &(((struct sockaddr_in6*)sa)->sin6_addr);
+}
--- a/seobeo/s_web.c	Wed Jan 07 13:24:38 2026 -0800
+++ b/seobeo/s_web.c	Wed Jan 07 16:05:57 2026 -0800
@@ -2,33 +2,6 @@
 
 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
-  );
-}
-
 char* Seobeo_Web_LoadFile(const char *file_path, size_t *p_file_size)
 {
   char full_path[1024];
@@ -551,73 +524,6 @@
   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);
-
-  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;
-  }
-
-  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);
-    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...
-      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)
-    {
-      Seobeo_Log(SEOBEO_DEBUG, "Connection closed by client\n");
-      break;
-    }
-    else
-    {
-      Dowa_Arena_Free(p_request_arena);
-      Seobeo_Handle_Destroy(h);
-      return -1;
-    }
-  }
-
-  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"
--- a/seobeo/seobeo.h	Wed Jan 07 13:24:38 2026 -0800
+++ b/seobeo/seobeo.h	Wed Jan 07 16:05:57 2026 -0800
@@ -10,10 +10,6 @@
 
 #include "seobeo/seobeo_internal.h"
 
-/* Included in dowa
-  #include <stdio.h>
-  #include <stdlib.h>
-*/
 #include <stdarg.h>
 #include <unistd.h>
 #include <errno.h>
@@ -70,6 +66,28 @@
 extern int            Seobeo_Web_Server_Start(const char *folder_path, const char *port, Seobeo_ServerMode mode, int thread_count);
 /* Generic HTTP GET Rquest to given host and port with path. It will mimic chrome. */
 extern int            Seobeo_Web_Client_Get(const char *host, const char *port, const char *path);
+
+// --- HTTP Client (curl-like API) --- //
+/* Create a new HTTP client request with the given URL. */
+extern Seobeo_Client_Request  *Seobeo_Client_Request_Create(const char *url);
+/* Set HTTP method (GET, POST, PUT, DELETE, etc.). Default is GET. */
+extern void                    Seobeo_Client_Request_Set_Method(Seobeo_Client_Request *p_req, const char *method);
+/* Add a header using key-value pairs (stored in HashMap). */
+extern void                    Seobeo_Client_Request_Add_Header_Map(Seobeo_Client_Request *p_req, const char *key, const char *value);
+/* Add a header as a string "Key: Value" (stored in Array). */
+extern void                    Seobeo_Client_Request_Add_Header_Array(Seobeo_Client_Request *p_req, const char *header);
+/* Set request body with given data and length. */
+extern void                    Seobeo_Client_Request_Set_Body(Seobeo_Client_Request *p_req, const char *body, size_t length);
+/* Enable/disable following redirects with max redirect count. Default is FALSE with 10 max. */
+extern void                    Seobeo_Client_Request_Set_Follow_Redirects(Seobeo_Client_Request *p_req, boolean follow, int32 max_redirects);
+/* Set download path to save response body to file instead of memory. */
+extern void                    Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path);
+/* Execute the HTTP request and return response. */
+extern Seobeo_Client_Response *Seobeo_Client_Request_Execute(Seobeo_Client_Request *p_req);
+/* Destroy request and free all resources. */
+extern void                    Seobeo_Client_Request_Destroy(Seobeo_Client_Request *p_req);
+/* Destroy response and free all resources. */
+extern void                    Seobeo_Client_Response_Destroy(Seobeo_Client_Response *p_resp);
 /* Initialize the router system (called automatically by Seobeo_Web_Server_Start) */
 extern void           Seobeo_Router_Init();
 /* Register an API route handler. Call before starting server. */
--- a/seobeo/seobeo_internal.h	Wed Jan 07 13:24:38 2026 -0800
+++ b/seobeo/seobeo_internal.h	Wed Jan 07 16:05:57 2026 -0800
@@ -91,4 +91,54 @@
 extern void          *Seobeo_Web_Edge_Worker(void *vargs);
 extern void           Seobeo_Web_Edge(Seobeo_Handle *p_server_handle, int thread_count, Seobeo_Cache_Entry *p_html_cache);
 
+// --- HTTP Client Types --- //
+typedef struct {
+  char    *method;
+  char    *url;
+  char    *host;
+  char    *port;
+  char    *path;
+  boolean  use_tls;
+
+  // Headers can be either HashMap or Array
+  Seobeo_Request_Entry *headers_map;
+  char                **headers_array;
+
+  char    *body;
+  size_t   body_length;
+
+  boolean  follow_redirects;
+  int32    max_redirects;
+
+  char    *download_path;
+
+  Dowa_Arena *p_arena;
+} Seobeo_Client_Request;
+
+typedef struct {
+  int32    status_code;
+  char    *status_text;
+
+  Seobeo_Request_Entry *headers;
+
+  char    *body;
+  size_t   body_length;
+
+  char    *redirect_url;
+
+  Dowa_Arena *p_arena;
+} Seobeo_Client_Response;
+
+// --- HTTP Client Functions --- //
+extern Seobeo_Client_Request  *Seobeo_Client_Request_Create(const char *url);
+extern void                    Seobeo_Client_Request_Set_Method(Seobeo_Client_Request *p_req, const char *method);
+extern void                    Seobeo_Client_Request_Add_Header_Map(Seobeo_Client_Request *p_req, const char *key, const char *value);
+extern void                    Seobeo_Client_Request_Add_Header_Array(Seobeo_Client_Request *p_req, const char *header);
+extern void                    Seobeo_Client_Request_Set_Body(Seobeo_Client_Request *p_req, const char *body, size_t length);
+extern void                    Seobeo_Client_Request_Set_Follow_Redirects(Seobeo_Client_Request *p_req, boolean follow, int32 max_redirects);
+extern void                    Seobeo_Client_Request_Set_Download_Path(Seobeo_Client_Request *p_req, const char *path);
+extern Seobeo_Client_Response *Seobeo_Client_Request_Execute(Seobeo_Client_Request *p_req);
+extern void                    Seobeo_Client_Request_Destroy(Seobeo_Client_Request *p_req);
+extern void                    Seobeo_Client_Response_Destroy(Seobeo_Client_Response *p_resp);
+
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/tests/seobeo_client_test.c	Wed Jan 07 16:05:57 2026 -0800
@@ -0,0 +1,278 @@
+#include "seobeo/seobeo.h"
+#include <stdio.h>
+#include <stdlib.h>
+
+void Test_Simple_Get()
+{
+  printf("\n=== Test 1: Simple GET Request ===\n");
+
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create("https://httpbin.org/get");
+  if (!p_req)
+  {
+    printf("Failed to create request\n");
+    return;
+  }
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+  if (p_resp)
+  {
+    printf("Status: %d %s\n", p_resp->status_code, p_resp->status_text ? p_resp->status_text : "");
+    printf("Body length: %zu bytes\n", p_resp->body_length);
+    if (p_resp->body && p_resp->body_length > 0)
+    {
+      printf("Body preview: %.200s...\n", p_resp->body);
+    }
+    Seobeo_Client_Response_Destroy(p_resp);
+  }
+  else
+  {
+    printf("Request failed\n");
+  }
+
+  Seobeo_Client_Request_Destroy(p_req);
+}
+
+void Test_Custom_Headers_HashMap()
+{
+  printf("\n=== Test 2: Custom Headers using HashMap ===\n");
+
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create("https://httpbin.org/headers");
+  if (!p_req)
+  {
+    printf("Failed to create request\n");
+    return;
+  }
+
+  Seobeo_Client_Request_Add_Header_Map(p_req, "User-Agent", "Seobeo/1.0");
+  Seobeo_Client_Request_Add_Header_Map(p_req, "Accept", "application/json");
+  Seobeo_Client_Request_Add_Header_Map(p_req, "X-Custom-Header", "CustomValue");
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+  if (p_resp)
+  {
+    printf("Status: %d\n", p_resp->status_code);
+    if (p_resp->body)
+    {
+      printf("Response:\n%s\n", p_resp->body);
+    }
+    Seobeo_Client_Response_Destroy(p_resp);
+  }
+  else
+  {
+    printf("Request failed\n");
+  }
+
+  Seobeo_Client_Request_Destroy(p_req);
+}
+
+void Test_Custom_Headers_Array()
+{
+  printf("\n=== Test 3: Custom Headers using Array ===\n");
+
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create("https://httpbin.org/headers");
+  if (!p_req)
+  {
+    printf("Failed to create request\n");
+    return;
+  }
+
+  Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0 (Array Mode)");
+  Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json");
+  Seobeo_Client_Request_Add_Header_Array(p_req, "X-Test-Header: TestValue");
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+  if (p_resp)
+  {
+    printf("Status: %d\n", p_resp->status_code);
+    if (p_resp->body)
+    {
+      printf("Response:\n%s\n", p_resp->body);
+    }
+    Seobeo_Client_Response_Destroy(p_resp);
+  }
+  else
+  {
+    printf("Request failed\n");
+  }
+
+  Seobeo_Client_Request_Destroy(p_req);
+}
+
+void Test_Post_With_Body()
+{
+  printf("\n=== Test 4: POST Request with Body ===\n");
+
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create("https://httpbin.org/post");
+  if (!p_req)
+  {
+    printf("Failed to create request\n");
+    return;
+  }
+
+  Seobeo_Client_Request_Set_Method(p_req, "POST");
+  Seobeo_Client_Request_Add_Header_Map(p_req, "Content-Type", "application/json");
+
+  const char *json_body = "{\"name\": \"Seobeo\", \"version\": \"1.0\", \"message\": \"Hello from Seobeo HTTP Client!\"}";
+  Seobeo_Client_Request_Set_Body(p_req, json_body, 0);
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+  if (p_resp)
+  {
+    printf("Status: %d\n", p_resp->status_code);
+    if (p_resp->body)
+    {
+      printf("Response:\n%s\n", p_resp->body);
+    }
+    Seobeo_Client_Response_Destroy(p_resp);
+  }
+  else
+  {
+    printf("Request failed\n");
+  }
+
+  Seobeo_Client_Request_Destroy(p_req);
+}
+
+void Test_Follow_Redirects()
+{
+  printf("\n=== Test 5: Following Redirects ===\n");
+
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create("https://httpbin.org/redirect/2");
+  if (!p_req)
+  {
+    printf("Failed to create request\n");
+    return;
+  }
+
+  Seobeo_Client_Request_Set_Follow_Redirects(p_req, TRUE, 10);
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+  if (p_resp)
+  {
+    printf("Final Status: %d\n", p_resp->status_code);
+    printf("Body length: %zu bytes\n", p_resp->body_length);
+    Seobeo_Client_Response_Destroy(p_resp);
+  }
+  else
+  {
+    printf("Request failed\n");
+  }
+
+  Seobeo_Client_Request_Destroy(p_req);
+}
+
+void Test_Download_File()
+{
+  printf("\n=== Test 6: Download File ===\n");
+
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create("https://mrjunejune.com/public/epi_favicon.svg");
+  if (!p_req)
+  {
+    printf("Failed to create request\n");
+    return;
+  }
+
+  Seobeo_Client_Request_Set_Download_Path(p_req, "downloaded_example.html");
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+  if (p_resp)
+  {
+    printf("Status: %d\n", p_resp->status_code);
+    printf("Downloaded %zu bytes to 'downloaded_example.html'\n", p_resp->body_length);
+    Seobeo_Client_Response_Destroy(p_resp);
+  }
+  else
+  {
+    printf("Request failed\n");
+  }
+
+  Seobeo_Client_Request_Destroy(p_req);
+}
+
+void Test_Mixed_Headers()
+{
+  printf("\n=== Test 7: Mixed HashMap and Array Headers ===\n");
+
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create("https://httpbin.org/headers");
+  if (!p_req)
+  {
+    printf("Failed to create request\n");
+    return;
+  }
+
+  Seobeo_Client_Request_Add_Header_Map(p_req, "User-Agent", "Seobeo/1.0");
+  Seobeo_Client_Request_Add_Header_Map(p_req, "Accept", "application/json");
+
+  Seobeo_Client_Request_Add_Header_Array(p_req, "X-Custom-1: Value1");
+  Seobeo_Client_Request_Add_Header_Array(p_req, "X-Custom-2: Value2");
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+  if (p_resp)
+  {
+    printf("Status: %d\n", p_resp->status_code);
+    if (p_resp->body)
+    {
+      printf("Response:\n%s\n", p_resp->body);
+    }
+    Seobeo_Client_Response_Destroy(p_resp);
+  }
+  else
+  {
+    printf("Request failed\n");
+  }
+
+  Seobeo_Client_Request_Destroy(p_req);
+}
+
+void Test_Response_Headers()
+{
+  printf("\n=== Test 8: Accessing Response Headers ===\n");
+
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create("https://httpbin.org/get");
+  if (!p_req)
+  {
+    printf("Failed to create request\n");
+    return;
+  }
+
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+  if (p_resp)
+  {
+    printf("Status: %d %s\n", p_resp->status_code, p_resp->status_text ? p_resp->status_text : "");
+
+    if (p_resp->headers)
+    {
+      printf("\nResponse Headers:\n");
+      size_t count = Dowa_Array_Length(p_resp->headers);
+      for (size_t i = 0; i < count; i++)
+      {
+        printf("  %s: %s\n", p_resp->headers[i].key, p_resp->headers[i].value);
+      }
+    }
+
+    Seobeo_Client_Response_Destroy(p_resp);
+  }
+  else
+  {
+    printf("Request failed\n");
+  }
+
+  Seobeo_Client_Request_Destroy(p_req);
+}
+
+int main()
+{
+  printf("=== Seobeo HTTP Client Tests ===\n");
+
+  Test_Simple_Get();
+  Test_Custom_Headers_HashMap();
+  Test_Custom_Headers_Array();
+  Test_Post_With_Body();
+  Test_Follow_Redirects();
+  Test_Download_File();
+  Test_Mixed_Headers();
+  Test_Response_Headers();
+
+  printf("\n=== All Tests Completed ===\n");
+  return 0;
+}