Mercurial
view seobeo/s_web.c @ 90:1aeee370837b
simple locust file to use.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Thu, 01 Jan 2026 14:47:28 -0800 |
| parents | 1ded13720541 |
| children | 19cccf6e866a |
line wrap: on
line source
#include "seobeo/seobeo.h" // Global folder path for serving files 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]; snprintf(full_path, sizeof(full_path), "%s/%s", g_folder_path, file_path); FILE *p_file = fopen(full_path, "rb"); if (!p_file) return NULL; fseek(p_file, 0, SEEK_END); size_t file_size = ftell(p_file); fseek(p_file, 0, SEEK_SET); char *p_content = (char*)malloc(file_size + 1); if (!p_content) { fclose(p_file); return NULL; } fread(p_content, 1, file_size, p_file); p_content[file_size] = '\0'; fclose(p_file); if (p_file_size) *p_file_size = file_size; return p_content; } void Seobeo_Web_Header_Generate( void *buffer, int status, const char *content_type, const int content_length) { const char *status_text; switch(status) { case HTTP_OK: status_text = "OK"; break; case HTTP_CREATED: status_text = "Created"; break; case HTTP_MOVED_PERMANENTLY: status_text = "Moved Permanently"; break; case HTTP_FOUND: status_text = "Found"; break; case HTTP_BAD_REQUEST: status_text = "Bad Request"; break; case HTTP_UNAUTHORIZED: status_text = "Unauthorized"; break; case HTTP_FORBIDDEN: status_text = "Forbidden"; break; case HTTP_NOT_FOUND: status_text = "Not Found"; break; case HTTP_INTERNAL_ERROR: status_text = "Internal Server Error"; break; default: status_text = "Unknown"; break; } sprintf( buffer, "HTTP/1.1 %d %s\r\n" "Content-Type: %s\r\n" "Content-Length: %d\r\n" "Connection: close\r\n" "\r\n", status, status_text, content_type, content_length ); } void Seobeo_Web_HandleClientRequest(Seobeo_Handle *p_cli_handle, Seobeo_Cache_Entry *p_html_cache) { Dowa_Arena *p_request_arena = Dowa_Arena_Create(1*1024*1024); // 1MB for request parsing if (!p_request_arena) { perror("Dowa_Arena_Create request"); goto clean_up; } Dowa_Arena *p_response_arena = Dowa_Arena_Create(5*1024*1024); // 1MB for response if (!p_response_arena) { perror("Dowa_Arena_Create response"); goto clean_up; } void *p_response_header = Dowa_Arena_Allocate(p_response_arena, (size_t)1024*5); // 5Kb if (!p_response_header) { perror("Dowa_Arena_Allocate"); goto clean_up; } // Parse request headers into hashmap using arena Seobeo_Request_Entry *p_req_map = NULL; if (Seobeo_Web_Header_Parse(p_cli_handle, &p_req_map, p_request_arena) != 0) { Seobeo_Web_Header_Generate(p_response_header, HTTP_BAD_REQUEST, "text/plain", 0); Seobeo_Handle_Queue(p_cli_handle, (const uint8*)p_response_header, (uint32)strlen(p_response_header)); Seobeo_Handle_Flush(p_cli_handle); goto clean_up; } // Extract method (GET, POST, etc.) void *p_method_kv = Dowa_HashMap_Get_Ptr(p_req_map, "HTTP_Method"); const char *method = p_method_kv ? ((Seobeo_Request_Entry*)p_method_kv)->value : NULL; if (!method) { Seobeo_Web_Header_Generate(p_response_header, HTTP_BAD_REQUEST, "text/plain", 0); Seobeo_Handle_Queue(p_cli_handle, (const uint8*)p_response_header, (uint32)strlen(p_response_header)); Seobeo_Handle_Flush(p_cli_handle); goto clean_up; } // Extract path void *p_path_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Path"); const char *path = p_path_kv ? ((Seobeo_Request_Entry*)p_path_kv)->value : "/"; // --- Try to match API route first --- Seobeo_Route_Handler handler = Seobeo_Router_Find_Handler(method, path, &p_req_map, p_request_arena); if (handler != NULL) { Seobeo_Request_Entry *p_response_map = handler(p_req_map, p_response_arena); Seobeo_Router_Send_Response(p_cli_handle, p_response_map, p_response_arena); goto clean_up; } // --- Handle different HTTP methods (static files fallback) --- if (strcmp(method, "GET") == 0) { void *p_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Path"); const char *path = p_kv ? ((Seobeo_Request_Entry*)p_kv)->value : NULL; char *file_path = Dowa_Arena_Allocate(p_response_arena, (size_t)5 * 1024); // 5Kb only for path if (!path || strcmp(path, "/") == 0) { strcpy(file_path, "index.html"); } else { size_t L = strlen(path); if (path[0] == '/') { if (strchr(path, '.') == NULL) snprintf(file_path, 512, "%.*s/index.html", (int)(L-1), path+1); else snprintf(file_path, 512, "%.*s", (int)(L-1), path+1); } else strcpy(file_path, path); } // Check if file is in cache, load if not void *p_file_kv = Dowa_HashMap_Get_Ptr(p_html_cache, file_path); const char *file_content = NULL; size_t body_size = 0; if (p_file_kv) { // File is cached file_content = ((Seobeo_Cache_Entry*)p_file_kv)->value; body_size = strlen(file_content); } else { file_content = Seobeo_Web_LoadFile(file_path, &body_size); if (file_content) Dowa_HashMap_Push(p_html_cache, file_path, file_content); } if (!file_content) { Seobeo_Web_Header_Generate(p_response_header, HTTP_NOT_FOUND, "text/html", 0); Seobeo_Handle_Queue(p_cli_handle, (const uint8*)p_response_header, (uint32)strlen(p_response_header)); Seobeo_Handle_Flush(p_cli_handle); goto clean_up; } // Serve static file const char *mime = "application/octet-stream"; if (strstr(file_path, ".html")) mime = "text/html; charset=utf-8"; else if (strstr(file_path, ".css")) mime = "text/css"; else if (strstr(file_path, ".js")) mime = "application/javascript"; else if (strstr(file_path, ".png")) mime = "image/png"; else if (strstr(file_path, ".jpg") || strstr(file_path, ".jpeg")) mime = "image/jpeg"; else if (strstr(file_path, ".gif")) mime = "image/gif"; else if (strstr(file_path, ".svg")) mime = "image/svg+xml"; else if (strstr(file_path, ".ico")) mime = "image/x-icon"; else if (strstr(file_path, ".json")) mime = "application/json"; printf("File path: %s\nBody Size: %zu\n", file_path, body_size); Seobeo_Web_Header_Generate(p_response_header, HTTP_OK, mime, body_size); Seobeo_Handle_Queue(p_cli_handle, (const uint8*)p_response_header, (uint32)strlen(p_response_header)); Seobeo_Handle_Queue(p_cli_handle, (const uint8*)file_content, (uint32)body_size); Seobeo_Handle_Flush(p_cli_handle); printf("DONE\n\n\n"); } else { Seobeo_Web_Header_Generate(p_response_header, HTTP_FORBIDDEN, "text/plain", 0); Seobeo_Handle_Queue(p_cli_handle, (const uint8*)p_response_header, (uint32)strlen(p_response_header)); Seobeo_Handle_Flush(p_cli_handle); } goto clean_up; clean_up: printf("clean up\n\n"); if (p_cli_handle) Seobeo_Handle_Destroy(p_cli_handle); if (p_request_arena) Dowa_Arena_Free(p_request_arena); if (p_response_arena) Dowa_Arena_Free(p_response_arena); return; } int Seobeo_Web_Header_Parse(Seobeo_Handle *p_handle, Seobeo_Request_Entry **pp_map, Dowa_Arena *p_arena) { // 1) Fill read_buffer until we see "\r\n\r\n" while (1) { int r = Seobeo_Handle_Read(p_handle); if (r < 0) return -1; // fatal error if (r == -2) return -2; // connection closed TODO: Add this as part of Handle struct. if (p_handle->read_buffer_len >= 4 && strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL) { break; } if (r == 0) return 1; // EAGAIN, try again later TODO: Add this as part of Handle struct. } // 2) Parse request‐line "METHOD SP PATH SP VERSION CRLF" char *buf = (char*)p_handle->read_buffer; char *hdr_end = strstr(buf, "\r\n\r\n"); size_t hdr_len = hdr_end - buf + 4; // This seems kinda bad ? char method[16], path[256], version[16]; if (sscanf(buf, "%15s %255s %15s", method, path, version) != 3) { return -1; } // Copy strings to arena and store in hashmap char *method_copy = Dowa_Arena_Allocate(p_arena, strlen(method) + 1); if (!method_copy) return -1; strcpy(method_copy, method); char *version_copy = Dowa_Arena_Allocate(p_arena, strlen(version) + 1); if (!version_copy) return -1; strcpy(version_copy, version); Dowa_HashMap_Push_Arena(*pp_map, "HTTP_Method", method_copy, p_arena); Dowa_HashMap_Push_Arena(*pp_map, "Version", version_copy, p_arena); // 1) Separate raw path and query string char *raw_path = path; char *query_start = strchr(raw_path, '?'); char *query_str = NULL; if (query_start) { *query_start = '\0'; // now raw_path ends before '?' query_str = query_start + 1; } // push only the clean path char *path_copy = Dowa_Arena_Allocate(p_arena, strlen(raw_path) + 1); if (!path_copy) return -1; strcpy(path_copy, raw_path); Dowa_HashMap_Push_Arena(*pp_map, "Path", path_copy, p_arena); // 2) If there *is* a query, tokenize into the same map with "query_" prefix if (query_str && *query_str) { char *cur = query_str; while (cur && *cur) { char *next_amp = strchr(cur, '&'); char *pair_end = next_amp ? next_amp : cur + strlen(cur); char *eq = memchr(cur, '=', pair_end - cur); if (eq) { size_t key_len = eq - cur; size_t val_len = pair_end - (eq + 1); char key_buf[256]; snprintf(key_buf, sizeof(key_buf), "query_%.*s", (int)key_len, cur); char *val_copy = Dowa_Arena_Allocate(p_arena, val_len + 1); if (!val_copy) return -1; memcpy(val_copy, eq + 1, val_len); val_copy[val_len] = '\0'; Dowa_HashMap_Push_Arena(*pp_map, key_buf, val_copy, p_arena); } cur = next_amp ? next_amp + 1 : NULL; } } // int qp = Dowa_HashMap_Get_Position(map, "QueryParams"); // Dowa_HashEntry *p_qp_entry = map->entries[qp]; // printf("query param key: %s\n", p_qp_entry->key); // printf("query param value: %s\n",(char *)Dowa_HashMap_Get(p_qp_entry->buffer, "hello")); // 3) Parse each header line until the blank line char *line = buf + strlen(method) + 1 + strlen(path) + 1 + strlen(version) + 2; while (line < hdr_end) { char *next = strstr(line, "\r\n"); if (!next) break; // split at colon char *colon = memchr(line, ':', next - line); if (colon) { size_t key_len = colon - line; size_t value_len = next - colon - 1; char *val_start = colon + 1; if (*val_start == ' ') { val_start++; value_len--; } char *key = Dowa_Arena_Allocate(p_arena, key_len + 1); if (!key) return -1; memcpy(key, line, key_len); key[key_len] = '\0'; char *val = Dowa_Arena_Allocate(p_arena, value_len + 1); if (!val) return -1; memcpy(val, val_start, value_len); val[value_len] = '\0'; // Both key and value are arena-allocated, hashmap will use them Dowa_HashMap_Push_Arena(*pp_map, key, val, p_arena); } line = next + 2; } Seobeo_Handle_Consume(p_handle, (uint32)hdr_len); // 4) If Content-Length was provided, read that much body void *p_cl_kv = Dowa_HashMap_Get_Ptr(*pp_map, "Content-Length"); if (p_cl_kv) { const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value; size_t body_len = atoi(content_length_str); while (p_handle->read_buffer_len < body_len) { int r = Seobeo_Handle_Read(p_handle); if (r < 0) return -1; if (r == 0) return 1; // wait for more data } char *body = Dowa_Arena_Allocate(p_arena, body_len + 1); if (!body) return -1; memcpy(body, p_handle->read_buffer, body_len); body[body_len] = '\0'; // Body is arena-allocated Dowa_HashMap_Push_Arena(*pp_map, "Body", body, p_arena); Seobeo_Handle_Consume(p_handle, (uint32)body_len); } return 0; // success; map now holds Method, Path, Version, headers, and optional Body } // TODO: Do epoll or kqueue depending on the OS. void SigchildHandler(int s) { (void)s; // quiet unused variable warning // waitpid() might overwrite errno, so we save and restore it: int saved_errno = errno; while(waitpid(-1, NULL, WNOHANG) > 0); errno = saved_errno; } int Seobeo_Web_Server_Start( const char *folder_path, const char *port, Seobeo_ServerMode mode, int thread_count) { // Store folder path globally if (folder_path) strncpy(g_folder_path, folder_path, sizeof(g_folder_path) - 1); // Initialize empty cache - files will be loaded on-demand Seobeo_Cache_Entry *p_html_cache = NULL; Seobeo_Handle *p_server_handle = Seobeo_Stream_Handle_Server_Create(NULL, port); if (p_server_handle->socket < 0) return 1; printf("Listening on port %s\n", port); // Fork‐based fallback if (mode == SEOBEO_MODE_FORK) { printf("FORK MODE\n"); struct sigaction sa; sa.sa_handler = SigchildHandler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sigaction(SIGCHLD, &sa, NULL); while (1) { Seobeo_Handle *p_cli_handle = Seobeo_Stream_Handle_Server_Accept(p_server_handle); if (!p_cli_handle) continue; if (fork() == 0) { Seobeo_Web_HandleClientRequest(p_cli_handle, p_html_cache); _exit(0); } } } if (mode == SEOBEO_MODE_EDGE) { printf("EDGE MODE\n"); Seobeo_Web_Edge(p_server_handle, thread_count, p_html_cache); } return -1; } int Seobeo_Web_Client_Get(const char *host, const char *port, const char *path) { Seobeo_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; } // Response size_t cap = 1024*8, used = 0; char *p_request_body = Dowa_Arena_Allocate(p_request_arena, cap); if (!p_request_body) { Seobeo_Handle_Destroy(h); return -1; } while (1) { int n = Seobeo_Handle_Read(h); printf("Size: %d\n", n); if (n > 0) { // TODO: Maybe directly use arena inside of the struct? idk if that is useful or not... memcpy(p_request_body + used, h->read_buffer, h->read_buffer_len); used += h->read_buffer_len; Seobeo_Handle_Consume(h, (uint32)h->read_buffer_len); }else if (n == 0) { // Wait continue; }else if (n == -2) { // Debug // peer closed; we’ve got everything printf("\n\nCLOSED\n\n"); break; }else { Dowa_Arena_Free(p_request_arena); Seobeo_Handle_Destroy(h); return -1; } } // Debug printf("Request body %s", p_request_body); Dowa_Arena_Free(p_request_arena); Seobeo_Handle_Destroy(h); return 0; }