Mercurial
view seobeo/s_web.c @ 21:09def63429b9
[Dowa] Updated the naming scheme and tests.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Mon, 06 Oct 2025 10:57:30 -0700 |
| parents | 875bb6e10db7 |
| children | 947b81010aba |
line wrap: on
line source
#include "seobeo/seobeo.h" 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 ); } 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_PHandle p_cli_handle, Dowa_PHashMap p_html_cache) { Dowa_PHashEntry entry = NULL; Dowa_PHashMap p_current = p_html_cache; char *slash; Dowa_PArena p_response_arena = Dowa_Arena_Create(8192); 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; } Dowa_PHashMap p_req_map = Dowa_HashMap_Create_With_Arena(32, p_response_arena); if (Seobeo_Web_Header_Parse(p_cli_handle, p_req_map) != 0) { // malformed request or closed — respond 400 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; } // DEBUG // Dowa_HashMap_Print(p_req_map); 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); } } // DEBUG // 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" printf("\n\nDirectory: %s\n\n", dir); p_current = Dowa_HashMap_Get(p_current, dir); if (!p_current) { fprintf(stderr, "No value in hashmap key: %s\n\n", dir); 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; } } size_t pos = Dowa_HashMap_Get_Position(p_current, file_path); entry = p_current->entries[pos]; // Missing so 404 if (!entry) { 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; } 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; printf("key: %s\n\n", entry->key); printf("Body Size: %zu\n\n", body_size); Seobeo_Web_Header_Generate(p_response_header, HTTP_OK, mime, body_size); printf("response header: %s, data: %d\n\n", p_response_header, (uint32)strlen(p_response_header)); Seobeo_Handle_Queue(p_cli_handle, (const uint8*)p_response_header, (uint32)strlen(p_response_header)); printf("response body data: %d\n\n", (uint32)body_size); Seobeo_Handle_Queue(p_cli_handle, (const uint8*)entry->buffer, (uint32)body_size); Seobeo_Handle_Flush(p_cli_handle); clean_up: if (p_cli_handle) Seobeo_Handle_Destroy(p_cli_handle); if (p_response_arena) Dowa_Arena_Destroy(p_response_arena); } int Seobeo_Web_Header_Parse(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_Push_Value_With_Type(map, "Method", method, strlen(method) + 1, DOWA_HASH_MAP_TYPE_STRING); Dowa_HashMap_Push_Value_With_Type(map, "Path", path, strlen(path) + 1, DOWA_HASH_MAP_TYPE_STRING); Dowa_HashMap_Push_Value_With_Type(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_Push_Value(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_Get_Position(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_Push_Value(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_Server_Start( const char *folder_path, const char *port, Seobeo_ServerMode mode, int thread_count) { Dowa_PHashMap p_html_cache = Dowa_HashMap_Create(200); 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_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_PHandle cli = Seobeo_Stream_Handle_Server_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) { 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_PHandle h = Seobeo_Stream_Handle_Client_Create(host, port, TRUE); if (!h || h->socket < 0) { if (h) Seobeo_Handle_Destroy(h); return -1; } Dowa_PArena 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); // DEBUG // printf("request: %s\n", (char *)p_request_header); 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_Destroy(p_request_arena); Seobeo_Handle_Destroy(h); return -1; } } printf("%s", p_request_body); Dowa_Arena_Destroy(p_request_arena); Seobeo_Handle_Destroy(h); return 0; } void Seobeo_Web_SSL_Init() { SSL_load_error_strings(); OpenSSL_add_ssl_algorithms(); } void Seobeo_Web_SSL_Cleanup(void) { EVP_cleanup(); // I don't think these are needed... }