# HG changeset patch # User June Park # Date 1768012774 28800 # Node ID 6de849867459a3ff26b0494ba8e2dcebd4a2e6c4 # Parent 8e56f800b7e45c3c8a495277ff416b164029984d [HgWeb] Updated logic to use Seobeo Client. diff -r 8e56f800b7e4 -r 6de849867459 hg-web/BUILD --- a/hg-web/BUILD Fri Jan 09 13:45:29 2026 -0800 +++ b/hg-web/BUILD Fri Jan 09 18:39:34 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\"\\\""], ) diff -r 8e56f800b7e4 -r 6de849867459 hg-web/main.c --- a/hg-web/main.c Fri Jan 09 13:45:29 2026 -0800 +++ b/hg-web/main.c Fri Jan 09 18:39:34 2026 -0800 @@ -11,44 +11,17 @@ #include #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/ 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); diff -r 8e56f800b7e4 -r 6de849867459 playground/main.c --- a/playground/main.c Fri Jan 09 13:45:29 2026 -0800 +++ b/playground/main.c Fri Jan 09 18:39:34 2026 -0800 @@ -1,113 +1,20 @@ #include "seobeo/seobeo.h" -void Test_Echo() -{ - printf("\n=== Test: Multiple Messages ===\n"); - - // Seobeo_Client_Request *foo = Seobeo_Client_Request_Create("http://mrjunejune.com/echo"); - // Seobeo_Client_Request_Add_Header_Array(foo, "Upgrade: websocket"); - // Seobeo_Client_Request_Add_Header_Array(foo, "Connection: Upgrade"); - // Seobeo_Client_Request_Add_Header_Array(foo, "Sec-WebSocket-Key: asbc3e12bA"); - // Seobeo_Client_Request_Add_Header_Array(foo, "Sec-WebSocket-Version: 13"); - // Seobeo_Client_Response *foo2 = Seobeo_Client_Request_Execute(foo); - // printf("June: %s\n", foo2->body); - - - Seobeo_WebSocket *p_ws = Seobeo_WebSocket_Connect("ws://mrjunejune.com/echo"); - // Seobeo_WebSocket *p_ws = Seobeo_WebSocket_Connect("ws://localhost:6969/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); } diff -r 8e56f800b7e4 -r 6de849867459 seobeo/s_web.c --- a/seobeo/s_web.c Fri Jan 09 13:45:29 2026 -0800 +++ b/seobeo/s_web.c Fri Jan 09 18:39:34 2026 -0800 @@ -756,5 +756,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'; +} diff -r 8e56f800b7e4 -r 6de849867459 seobeo/seobeo.h --- a/seobeo/seobeo.h Fri Jan 09 13:45:29 2026 -0800 +++ b/seobeo/seobeo.h Fri Jan 09 18:39:34 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 * ------