changeset 173:827c6ac504cd hg-web

Merged in default here.
author MrJuneJune <me@mrjunejune.com>
date Mon, 19 Jan 2026 18:59:10 -0800
parents c033667da5f9 (diff) 0face9898d04 (current diff)
children 1ba8c1df082c
files playground/main.c seobeo/s_web.c seobeo/seobeo.h
diffstat 5 files changed, 143 insertions(+), 445 deletions(-) [+]
line wrap: on
line diff
--- a/hg-web/BUILD	Mon Jan 19 18:56:54 2026 -0800
+++ b/hg-web/BUILD	Mon Jan 19 18:59:10 2026 -0800
@@ -17,9 +17,10 @@
 cc_binary(
   name = "hg_web_server",
   srcs = ["main.c"],
-  deps = ["//seobeo:seobeo_server"],
+  deps = [
+    "//seobeo:seobeo",
+  ],
   data = [":src_files"],
-  defines = ["REPO_ROOT=\\\"\"/home/mrjunejune/zenbu\"\\\""],
 )
 
 bundle(
@@ -33,6 +34,5 @@
   srcs = ["main.c"],
   deps = ["//seobeo:seobeo_tcp_server_ws_debug"],
   data = [":src_files"],
-  defines = ["REPO_ROOT=\\\"\"/home/mrjunejune/zenbu\"\\\""],
 )
 
--- a/hg-web/main.c	Mon Jan 19 18:56:54 2026 -0800
+++ b/hg-web/main.c	Mon Jan 19 18:59:10 2026 -0800
@@ -11,44 +11,17 @@
 #include <netdb.h>
 
 #define HG_SERVE_HOST "127.0.0.1"
-#define HG_SERVE_PORT 4444
+#define HG_SERVE_PORT "4444"
 
 #define MAX_PATH 4096
 
-// TODO: Move this to seobeo....
-// Asked AI to create this lol, probably should learn to decode it myself..
-static void url_decode(char *dst, const char *src)
-{
-  char a, b;
-  while (*src) {
-    if ((*src == '%') &&
-        ((a = src[1]) && (b = src[2])) &&
-        (isxdigit(a) && isxdigit(b))) {
-      if (a >= 'a') a -= 'a'-'A';
-      if (a >= 'A') a -= ('A' - 10);
-      else a -= '0';
-      if (b >= 'a') b -= 'a'-'A';
-      if (b >= 'A') b -= ('A' - 10);
-      else b -= '0';
-      *dst++ = 16*a+b;
-      src+=3;
-    } else if (*src == '+') {
-      *dst++ = ' ';
-      src++;
-    } else {
-      *dst++ = *src++;
-    }
-  }
-  *dst = '\0';
-}
-
 static char* sanitize_path(const char *input_path, Dowa_Arena *arena)
 {
   if (!input_path || strlen(input_path) == 0)
   {
-    char *empty = Dowa_Arena_Allocate(arena, 1);
-    empty[0] = '\0';
-    return empty;
+  char *empty = Dowa_Arena_Allocate(arena, 1);
+  empty[0] = '\0';
+  return empty;
   }
 
   size_t len = strlen(input_path);
@@ -57,168 +30,46 @@
 
   for (size_t i = 0; i < len; i++)
   {
-    if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) {
-      if (i + 1 < len && input_path[i+1] == '.') {
-        // Skip ".."
-        i++;
-        continue;
-      }
-      // Skip "."
-      continue;
+  if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) {
+    if (i + 1 < len && input_path[i+1] == '.') {
+    // Skip ".."
+    i++;
+    continue;
     }
-    result[j++] = input_path[i];
+    // Skip "."
+    continue;
+  }
+  result[j++] = input_path[i];
   }
   result[j] = '\0';
 
   // Remove leading/trailing slashes
   while (result[0] == '/')
-    memmove(result, result + 1, strlen(result));
+  memmove(result, result + 1, strlen(result));
   while (j > 0 && result[j-1] == '/')
-    result[--j] = '\0';
+  result[--j] = '\0';
 
   return result;
 }
 
-// Helper to connect to hg serve
-static int hg_proxy_connect(void)
+Seobeo_Client_Response  *hg_proxy_request(
+  const char *method,
+  const char *path,
+  const char *req_body)
 {
-    int sock = socket(AF_INET, SOCK_STREAM, 0);
-    if (sock < 0)
-    {
-        Seobeo_Log(SEOBEO_DEBUG, "Failed to create socket\n");
-        return -1;
-    }
-
-    struct sockaddr_in server_addr;
-    memset(&server_addr, 0, sizeof(server_addr));
-    server_addr.sin_family = AF_INET;
-    server_addr.sin_port = htons(HG_SERVE_PORT);
-    inet_pton(AF_INET, HG_SERVE_HOST, &server_addr.sin_addr);
-
-    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0)
-    {
-        Seobeo_Log(SEOBEO_DEBUG, "Failed to connect to hg serve at %s:%d\n", HG_SERVE_HOST, HG_SERVE_PORT);
-        close(sock);
-        return -1;
-    }
-
-    return sock;
-}
-
-// Generic helper to proxy a request to hg serve and get the response body
-// Returns allocated body on success, NULL on failure
-// out_status and out_content_type are optional output parameters
-// out_body_len returns the actual body length (for binary content)
-static char* hg_proxy_request(
-    const char *method,
-    const char *path,
-    const char *req_body,
-    size_t body_len,
-    char *out_status,       // should be at least 4 bytes
-    char *out_content_type, // should be at least 256 bytes
-    size_t *out_body_len,   // optional: returns actual body length
-    Dowa_Arena *arena)
-{
-    int sock = hg_proxy_connect();
-    if (sock < 0) return NULL;
-
-    // Build HTTP request
-    char http_request[MAX_PATH * 2];
-    snprintf(http_request, sizeof(http_request),
-        "%s %s HTTP/1.1\r\n"
-        "Host: %s:%d\r\n"
-        "Connection: close\r\n"
-        "Accept: application/json, text/plain, */*\r\n"
-        "Content-Length: %zu\r\n"
-        "\r\n",
-        method, path, HG_SERVE_HOST, HG_SERVE_PORT, body_len);
-
-    Seobeo_Log(SEOBEO_DEBUG, "HG Proxy request: %s %s\n", method, path);
-
-    if (send(sock, http_request, strlen(http_request), 0) < 0)
-    {
-        close(sock);
-        return NULL;
-    }
-
-    if (body_len > 0 && req_body)
-    {
-        send(sock, req_body, body_len, 0);
-    }
+  char full_path[MAX_PATH];
+  snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path);
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path);
+  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");
+  if (strcmp(method, "POST"))
+    Seobeo_Client_Request_Set_Method(p_req, "POST");
 
-    // Read response
-    int buffer_size = 1024 * 1024 * 5; // 5MB
-    char *response_buf = Dowa_Arena_Allocate(arena, buffer_size);
-    size_t total_read = 0;
-    ssize_t bytes_read;
-
-    while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0)
-    {
-        total_read += bytes_read;
-        if (total_read >= (size_t)(buffer_size - 1)) break;
-    }
-    response_buf[total_read] = '\0';
-    close(sock);
-
-    Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read);
-
-    // Parse response headers - use memmem to handle binary content
-    char *headers_end = NULL;
-    for (size_t i = 0; i + 3 < total_read; i++)
-    {
-        if (response_buf[i] == '\r' && response_buf[i+1] == '\n' &&
-            response_buf[i+2] == '\r' && response_buf[i+3] == '\n')
-        {
-            headers_end = response_buf + i;
-            break;
-        }
-    }
-    if (!headers_end) return NULL;
-
-    // Extract status
-    if (out_status && strncmp(response_buf, "HTTP/", 5) == 0)
-    {
-        char *status_start = strchr(response_buf, ' ');
-        if (status_start)
-        {
-            strncpy(out_status, status_start + 1, 3);
-            out_status[3] = '\0';
-        }
-    }
-
-    // Extract content-type
-    if (out_content_type)
-    {
-        out_content_type[0] = '\0';
-        char *ct_header = strcasestr(response_buf, "Content-Type:");
-        if (ct_header && ct_header < headers_end)
-        {
-            ct_header += 13;
-            while (*ct_header == ' ') ct_header++;
-            char *ct_end = strpbrk(ct_header, "\r\n");
-            if (ct_end)
-            {
-                size_t ct_len = ct_end - ct_header;
-                if (ct_len < 256)
-                {
-                    strncpy(out_content_type, ct_header, ct_len);
-                    out_content_type[ct_len] = '\0';
-                }
-            }
-        }
-    }
-
-    // Return body (copy to fresh allocation for clean pointer)
-    char *body = headers_end + 4;
-    size_t body_size = total_read - (body - response_buf);
-
-    if (out_body_len) *out_body_len = body_size;
-
-    char *result = Dowa_Arena_Allocate(arena, body_size + 1);
-    memcpy(result, body, body_size);
-    result[body_size] = '\0';
-
-    return result;
+  Seobeo_Client_Request_Set_Body(p_req, req_body, 0);
+  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
+  Seobeo_Client_Request_Destroy(p_req);
+  return p_resp;
 }
 
 Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena)
@@ -229,7 +80,7 @@
   const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : "";
 
   char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
-  url_decode(decoded_path, rel_path);
+  Seobeo_Url_Decode(decoded_path, rel_path);
 
   char *safe_path = sanitize_path(decoded_path, arena);
 
@@ -237,18 +88,18 @@
 
   char hg_path[MAX_PATH];
   if (strlen(safe_path) > 0)
-    snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path);
+  snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path);
   else
-    snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json");
+  snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json");
+
+  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL);
 
-  char status[4] = "200";
-  char content_type[256] = "";
-  size_t body_len = 0;
-  char *hg_response = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena);
+  Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length);
 
-  Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%s body_len=%zu\n", status, body_len);
+  char status[4];
+  snprintf(status, 3, "%i", hg_response->status_code);
 
-  if (!hg_response || status[0] != '2')
+  if (hg_response->status_code != 200)
   {
     Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n");
     Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
@@ -256,12 +107,11 @@
     Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena);
     return resp;
   }
-  char *json = hg_response;
 
   Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
   Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-  Dowa_HashMap_Push_Arena(resp, "body", json, arena);
-
+  Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
+  Seobeo_Client_Response_Destroy(hg_response);
   return resp;
 }
 
@@ -272,7 +122,7 @@
   void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path");
   const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : "";
   char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
-  url_decode(decoded_path, rel_path);
+  Seobeo_Url_Decode(decoded_path, rel_path);
   char *safe_path = sanitize_path(decoded_path, arena);
 
   Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path);
@@ -285,18 +135,16 @@
     return resp;
   }
 
-  // Build hg serve URL: /raw-file/tip/<path>
   char hg_path[MAX_PATH];
   snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path);
+  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL);
 
-  char status[4] = "200";
-  char content_type[256] = "";
-  size_t body_len = 0;
-  char *body = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena);
+  Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length);
 
-  Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%s body_len=%zu\n", status, body_len);
+  char status[4];
+  snprintf(status, 3, "%i", hg_response->status_code);
 
-  if (!body)
+  if (!hg_response->body)
   {
     Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
     Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
@@ -304,31 +152,19 @@
     return resp;
   }
 
-  if (status[0] != '2')
+  if (hg_response->status_code != 200)
   {
-    Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error response: %s\n", body);
+    Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error hg_response: %s\n", hg_response->body);
     Dowa_HashMap_Push_Arena(resp, "status", status, arena);
     Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-    // Return actual error from hg serve if available
-    Dowa_HashMap_Push_Arena(resp, "body", body_len > 0 ? body : "File not found", arena);
+    Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
     return resp;
   }
-
-  // Use content-type from hg serve, or determine from extension
-  const char *final_content_type = content_type;
-  if (strlen(content_type) == 0 || strcmp(content_type, "application/octet-stream") == 0)
-  {
-    final_content_type = "text/plain";
-    if (strstr(safe_path, ".md")) final_content_type = "text/markdown";
-    else if (strstr(safe_path, ".html")) final_content_type = "text/html";
-    else if (strstr(safe_path, ".css")) final_content_type = "text/css";
-    else if (strstr(safe_path, ".js")) final_content_type = "application/javascript";
-    else if (strstr(safe_path, ".json")) final_content_type = "application/json";
-  }
-
-  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
-  Dowa_HashMap_Push_Arena(resp, "content-type", final_content_type, arena);
-  Dowa_HashMap_Push_Arena(resp, "body", body, arena);
+  
+  Dowa_HashMap_Push_Arena(resp, "status", status, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
+  Seobeo_Client_Response_Destroy(hg_response);
 
   return resp;
 }
@@ -339,137 +175,41 @@
 
 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
-    Seobeo_Request_Entry *resp = NULL;
-
-    // Get method
-    void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method");
-    const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET";
-
-    // Get query string
-    void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString");
-    const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : "";
+  Seobeo_Request_Entry *resp = NULL;
 
-    // Get request body for POST
-    void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
-    const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : "";
-    size_t body_len = strlen(req_body);
-
-    Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len);
-
-    // Connect to hg serve
-    int sock = hg_proxy_connect();
-    if (sock < 0)
-    {
-        Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-        Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena);
-        return resp;
-    }
+  void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method");
+  const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET";
 
-    // Build the HTTP request to forward to hg serve
-    char http_request[MAX_PATH * 2];
-    if (strlen(query_string) > 0)
-    {
-        snprintf(http_request, sizeof(http_request),
-            "%s /?%s HTTP/1.1\r\n"
-            "Host: %s:%d\r\n"
-            "Connection: close\r\n"
-            "Content-Length: %zu\r\n"
-            "\r\n",
-            method, query_string, HG_SERVE_HOST, HG_SERVE_PORT, body_len);
-    }
-    else
-    {
-        snprintf(http_request, sizeof(http_request),
-            "%s / HTTP/1.1\r\n"
-            "Host: %s:%d\r\n"
-            "Connection: close\r\n"
-            "Content-Length: %zu\r\n"
-            "\r\n",
-            method, HG_SERVE_HOST, HG_SERVE_PORT, body_len);
-    }
+  void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString");
+  const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : "";
 
-    // Send HTTP request headers
-    if (send(sock, http_request, strlen(http_request), 0) < 0)
-    {
-        Seobeo_Log(SEOBEO_DEBUG, "Failed to send request to hg serve\n");
-        close(sock);
-        Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-        Dowa_HashMap_Push_Arena(resp, "body", "Failed to send to hg serve", arena);
-        return resp;
-    }
+  void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
+  const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : "";
+  size_t body_len = strlen(req_body);
 
-    // Send body if present
-    if (body_len > 0)
-    {
-        send(sock, req_body, body_len, 0);
-    }
-
-    // Read response from hg serve
-    int buffer_size = 1024 * 1024 * 5; // 5MB
-    char *response_buf = Dowa_Arena_Allocate(arena, buffer_size);
-    size_t total_read = 0;
-    ssize_t bytes_read;
-
-    while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0)
-    {
-        total_read += bytes_read;
-        if (total_read >= (size_t)(buffer_size - 1)) break;
-    }
-    response_buf[total_read] = '\0';
-    close(sock);
-
-    Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read);
-
-    // Parse HTTP response - find headers end
-    char *headers_end = strstr(response_buf, "\r\n\r\n");
-    if (!headers_end)
-    {
-        Dowa_HashMap_Push_Arena(resp, "status", "502", arena);
-        Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-        Dowa_HashMap_Push_Arena(resp, "body", "Invalid response from hg serve", arena);
-        return resp;
-    }
+  Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len);
+  Seobeo_Client_Response *hg_response;
+  if (strlen(query_string) > 0)
+  {
+    char temp_path[MAX_PATH];
+    snprintf(temp_path, MAX_PATH, "?%s", query_string);
+    hg_response = hg_proxy_request(method, query_string, req_body);
+  }
+  else
+    hg_response = hg_proxy_request(method, "", req_body);
 
-    // Extract status code from first line (e.g., "HTTP/1.1 200 OK")
-    char status_code[4] = "200";
-    if (strncmp(response_buf, "HTTP/", 5) == 0)
-    {
-        char *status_start = strchr(response_buf, ' ');
-        if (status_start)
-        {
-            strncpy(status_code, status_start + 1, 3);
-            status_code[3] = '\0';
-        }
-    }
+  Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length);
+
+  Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type");
 
-    // Extract content-type from headers
-    const char *content_type = "application/mercurial-0.1";
-    char *ct_header = strcasestr(response_buf, "Content-Type:");
-    if (ct_header && ct_header < headers_end)
-    {
-        ct_header += 13; // Skip "Content-Type:"
-        while (*ct_header == ' ') ct_header++;
-        char *ct_end = strpbrk(ct_header, "\r\n");
-        if (ct_end)
-        {
-            size_t ct_len = ct_end - ct_header;
-            char *ct_copy = Dowa_Arena_Allocate(arena, ct_len + 1);
-            strncpy(ct_copy, ct_header, ct_len);
-            ct_copy[ct_len] = '\0';
-            content_type = ct_copy;
-        }
-    }
+  char status[4];
+  snprintf(status, 3, "%i", hg_response->status_code);
 
-    // Body starts after \r\n\r\n
-    char *body = headers_end + 4;
+  Dowa_HashMap_Push_Arena(resp, "status", status, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", kv ? kv->value : "", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
 
-    Dowa_HashMap_Push_Arena(resp, "status", status_code, arena);
-    Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena);
-    Dowa_HashMap_Push_Arena(resp, "body", body, arena);
-
-    return resp;
+  return resp;
 }
 
 int main(void) {
@@ -483,7 +223,6 @@
   Seobeo_Router_Register("POST", "/repo", ApiHgWireProtocol);
 
   printf("Starting on Port 6970...\n");
-  printf("Repository: %s\n", REPO_ROOT);
 
   int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4);
 
--- a/playground/main.c	Mon Jan 19 18:56:54 2026 -0800
+++ b/playground/main.c	Mon Jan 19 18:59:10 2026 -0800
@@ -1,102 +1,20 @@
 #include "seobeo/seobeo.h"
 
-void Test_Echo()
-{
-  printf("\n=== Test: Multiple Messages ===\n");
-  Seobeo_WebSocket *p_ws = Seobeo_WebSocket_Connect("wss://mrjunejune.com/echo");
-  if (!p_ws)
-  {
-    printf("Failed to connect\n");
-    return;
-  }
-
-  const char *messages[] = {
-    "Message 1",
-    "Message 2",
-    "Message 3"
-  };
-
-  for (int i = 0; i < 3; i++)
-  {
-    printf("Sending: %s\n", messages[i]);
-    Seobeo_WebSocket_Send_Text(p_ws, messages[i]);
-    usleep(100000);
-  }
-
-  printf("Receiving responses...\n");
-  int received = 0;
-  int attempts = 0;
-
-  while (received < 3 && attempts < 200)
-  {
-    Seobeo_WebSocket_Message *p_msg = Seobeo_WebSocket_Receive(p_ws);
-    if (p_msg)
-    {
-      if (p_msg->opcode == SEOBEO_WS_OPCODE_TEXT)
-      {
-        printf("Response %d: %.*s\n", received + 1, (int)p_msg->length, (char*)p_msg->data);
-        received++;
-      }
-      Seobeo_WebSocket_Message_Destroy(p_msg);
-    }
-
-    usleep(10000);
-    attempts++;
-  }
-  printf("Received %d/%d messages\n", received, 3);
-  Seobeo_WebSocket_Destroy(p_ws);
-}
-
-void Test_Chat()
-{
-  printf("\n=== Test: Multiple Messages ===\n");
-
-  Seobeo_WebSocket *p_ws = Seobeo_WebSocket_Connect("ws://127.0.0.1:8080/chat");
-  if (!p_ws)
-  {
-    printf("Failed to connect\n");
-    return;
-  }
-
-  const char *messages[] = {
-    "Message 1",
-    "Message 2",
-    "Message 3"
-  };
-
-  for (int i = 0; i < 3; i++)
-  {
-    printf("Sending: %s\n", messages[i]);
-    Seobeo_WebSocket_Send_Text(p_ws, messages[i]);
-    usleep(100000);
-  }
-
-  printf("Receiving responses...\n");
-  int received = 0;
-  int attempts = 0;
-
-  while (received < 3 && attempts < 200)
-  {
-    Seobeo_WebSocket_Message *p_msg = Seobeo_WebSocket_Receive(p_ws);
-    if (p_msg)
-    {
-      if (p_msg->opcode == SEOBEO_WS_OPCODE_TEXT)
-      {
-        printf("Response %d: %.*s\n", received + 1, (int)p_msg->length, (char*)p_msg->data);
-        received++;
-      }
-      Seobeo_WebSocket_Message_Destroy(p_msg);
-    }
-
-    usleep(10000);
-    attempts++;
-  }
-  printf("Received %d/%d messages\n", received, 3);
-  Seobeo_WebSocket_Destroy(p_ws);
-}
-
 int main(int argc, char *argv[])
 {
-
-  Test_Echo();
+  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create("http://127.0.0.1:4444/file/tip?style=json");
+  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);
 }
--- a/seobeo/s_web.c	Mon Jan 19 18:56:54 2026 -0800
+++ b/seobeo/s_web.c	Mon Jan 19 18:59:10 2026 -0800
@@ -757,5 +757,43 @@
   g_routes = NULL;
 }
 
-// Logging functions moved to s_logging.c
+// Written by AI. I don't know what it does.
+void Seobeo_Url_Decode(char *dst, const char *src)
+{
+  char a, b;
+  while (*src) {
+    /* Check if we have a % followed by two valid hex characters */
+    if (*src == '%' && src[1] && src[2]) {
+      a = src[1];
+      b = src[2];
+
+      /* Manual isxdigit check and conversion for 'a' */
+      int a_val = -1;
+      if (a >= '0' && a <= '9')    a_val = a - '0';
+      else if (a >= 'a' && a <= 'f') a_val = a - 'a' + 10;
+      else if (a >= 'A' && a <= 'F') a_val = a - 'A' + 10;
 
+      /* Manual isxdigit check and conversion for 'b' */
+      int b_val = -1;
+      if (b >= '0' && b <= '9')    b_val = b - '0';
+      else if (b >= 'a' && b <= 'f') b_val = b - 'a' + 10;
+      else if (b >= 'A' && b <= 'F') b_val = b - 'A' + 10;
+
+      /* If both were valid hex, combine them */
+      if (a_val != -1 && b_val != -1) {
+        *dst++ = (char)((a_val << 4) | b_val);
+        src += 3;
+        continue;
+      }
+    }
+
+    /* Handle '+' as space, otherwise copy character literally */
+    if (*src == '+') {
+      *dst++ = ' ';
+    } else {
+      *dst++ = *src;
+    }
+    src++;
+  }
+  *dst = '\0';
+}
--- a/seobeo/seobeo.h	Mon Jan 19 18:56:54 2026 -0800
+++ b/seobeo/seobeo.h	Mon Jan 19 18:59:10 2026 -0800
@@ -89,6 +89,9 @@
 /* Destroy response and free all resources. */
 extern void                    Seobeo_Client_Response_Destroy(Seobeo_Client_Response *p_resp);
 
+// --- HTTP Web related helper functions --- //
+extern void    Seobeo_Url_Decode(char *dst, const char *src);
+
 /**
  * WebSocket Client API
  * ------