# HG changeset patch # User June Park # Date 1767830757 28800 # Node ID c39582f937e5c1112234ec7af5080014aee92f44 # Parent 249881ceff7b5d6274f2411ebd2fb4e93388712b [Seobeo Client] Added client side logic which will be used for all my other calls instead of curl. diff -r 249881ceff7b -r c39582f937e5 .hgignore --- 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 diff -r 249881ceff7b -r c39582f937e5 seobeo/BUILD --- 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"], +) + diff -r 249881ceff7b -r c39582f937e5 seobeo/s_http_client.c --- /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 + +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); +} diff -r 249881ceff7b -r c39582f937e5 seobeo/s_linux_network.c --- 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); -} diff -r 249881ceff7b -r c39582f937e5 seobeo/s_network.c --- /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); +} diff -r 249881ceff7b -r c39582f937e5 seobeo/s_web.c --- 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" diff -r 249881ceff7b -r c39582f937e5 seobeo/seobeo.h --- 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 - #include -*/ #include #include #include @@ -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. */ diff -r 249881ceff7b -r c39582f937e5 seobeo/seobeo_internal.h --- 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 diff -r 249881ceff7b -r c39582f937e5 seobeo/tests/seobeo_client_test.c --- /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 +#include + +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; +}