Mercurial
diff seobeo/s_web.c @ 7:114cad94008f
[Seobeo] Updated to support thread and edge server calls.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Mon, 29 Sep 2025 17:00:38 -0700 |
| parents | |
| children | fb2cff495a60 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/seobeo/s_web.c Mon Sep 29 17:00:38 2025 -0700 @@ -0,0 +1,304 @@ +#include "seobeo/seobeo.h" + +void Seobeo_Web_GenerateResponseHeader(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/2.2 %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_PHandle p_cli_handle, + Dowa_PHashMap p_html_cache) +{ + Dowa_PArena p_response_arena = Dowa_Arena_Create(8192); + Dowa_PHashMap p_req_map = NULL; + + Dowa_PHashEntry entry = NULL; + Dowa_PHashMap p_current = p_html_cache; + char *slash; + + if (!p_response_arena) { perror("Dowa_Arena_Initialize"); goto clean_up; } + + void *p_response_header = Dowa_Arena_Allocate(p_response_arena, (size_t)2048); + if (!p_response_header) { perror("Dowa_Arena_Allocate"); goto clean_up; } + + p_req_map = Dowa_HashMap_Create(32); + if (Seobeo_Web_ParseClientHeader(p_cli_handle, p_req_map) != 0) + { + // malformed request or closed — respond 400 + Seobeo_Web_GenerateResponseHeader(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; + } + + const char *path = (const char*)Dowa_HashMap_Get(p_req_map, "Path"); + + char *file_path = Dowa_Arena_Allocate(p_response_arena, (size_t)512); + + if (!path || strcmp(path, "/") == 0) + { + strcpy(file_path, "index.html"); + } + else + { + size_t L = strlen(path); + // strip leading '/' + 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 + { + // Probably never get here? + strcpy(file_path, path); + } + } + + // printf("\n\nfile_path: %s\n", file_path); + + // Recursively go though the path until it gets to a file + while ((slash = strchr(file_path, '/'))) + { + *slash = '\0'; // e.g. file_path="foo", slash+1="index.html" + char *dir = file_path; // "foo" + file_path = slash + 1; // "index.html" + + p_current = Dowa_HashMap_Get(p_current, dir); + if (!p_current) { perror("No value"); goto clean_up; } + } + + size_t pos = Dowa_HashMap_GetPosition(p_current, file_path); + entry = p_current->entries[pos]; + + // Missing so 404 + if (!entry) + { + Seobeo_Web_GenerateResponseHeader(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; + } + + + const char *mime = "application/octet-stream"; // Default binary + 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"; + + size_t body_size = entry->capacity; + Seobeo_Web_GenerateResponseHeader(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*)entry->buffer, + (uint32)body_size); + Seobeo_Handle_Flush(p_cli_handle); + +clean_up: + Seobeo_Handle_Destroy(p_cli_handle); + Dowa_Arena_Free(p_response_arena); + Dowa_HashMap_Free(p_req_map); // TODO: Maybe initilized hashmap within the Arena? +} + +int Seobeo_Web_ParseClientHeader(Seobeo_PHandle p_handle, Dowa_PHashMap map) +{ + // 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; + + char method[16], path[256], version[16]; + if (sscanf(buf, "%15s %255s %15s", method, path, version) != 3) + { + return -1; + } + + Dowa_HashMap_PushValueWithType(map, "Method", method, strlen(method) + 1, DOWA_HASH_MAP_TYPE_STRING); + Dowa_HashMap_PushValueWithType(map, "Path", path, strlen(path) + 1, DOWA_HASH_MAP_TYPE_STRING); + Dowa_HashMap_PushValueWithType(map, "Version", version, strlen(version) + 1, DOWA_HASH_MAP_TYPE_STRING); + + // 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 = malloc(key_len + 1); + memcpy(key, line, key_len); + key[key_len] = '\0'; + + char *val = malloc(value_len + 1); + memcpy(val, val_start, value_len); + val[value_len] = '\0'; + + Dowa_HashMap_PushValue(map, key, val, value_len + 1); + + free(key); + free(val); + } + + line = next + 2; + } + Seobeo_Handle_Consume(p_handle, (uint32)hdr_len); + + // 4) If Content-Length was provided, read that much body + int content_length_pos = Dowa_HashMap_GetPosition(map, "Content-Length"); + Dowa_PHashEntry p_content_length_entry = map->entries[content_length_pos]; + if (p_content_length_entry) + { + size_t body_len = atoi((char*)p_content_length_entry->buffer); + 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 = malloc(body_len + 1); + memcpy(body, p_handle->read_buffer, body_len); + body[body_len] = '\0'; + + Dowa_HashMap_PushValue(map, "Body", body, body_len + 1); + free(body); + + 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_StartBasicHTTPServer( + const char *folder_path, + const char *port, + Seobeo_ServerMode mode, + int thread_count) +{ + Dowa_PHashMap p_html_cache = Dowa_HashMap_Create(1024); + if (Dowa_HashMap_Cache_Folder(p_html_cache, + folder_path) != 0) + { + perror("Dowa_Cache_Folder"); + return -1; + } + + Seobeo_PHandle p_server_handle = + Seobeo_Stream_Handle_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) + { + struct sigaction sa; + sa.sa_handler = SigchildHandler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction(SIGCHLD, &sa, NULL); + + while (1) { + Seobeo_PHandle cli = + Seobeo_Stream_Handle_Accept(p_server_handle); + if (!cli) continue; + + if (fork() == 0) { + Seobeo_Web_HandleClientRequest(cli, + p_html_cache); + _exit(0); + } + Seobeo_Handle_Destroy(cli); + } + } + + if (mode == SEOBEO_MODE_EDGE) + { + Seobeo_Web_Edge(p_server_handle, thread_count, p_html_cache); + } + + return -1; +}