Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 6:1e61008b9980 | 7:114cad94008f |
|---|---|
| 1 #include "seobeo/seobeo.h" | |
| 2 | |
| 3 void Seobeo_Web_GenerateResponseHeader(void *buffer, int status, | |
| 4 const char *content_type, const int content_length) | |
| 5 { | |
| 6 const char *status_text; | |
| 7 switch(status) | |
| 8 { | |
| 9 case HTTP_OK: status_text = "OK"; break; | |
| 10 case HTTP_CREATED: status_text = "Created"; break; | |
| 11 case HTTP_MOVED_PERMANENTLY: status_text = "Moved Permanently"; break; | |
| 12 case HTTP_FOUND: status_text = "Found"; break; | |
| 13 case HTTP_BAD_REQUEST: status_text = "Bad Request"; break; | |
| 14 case HTTP_UNAUTHORIZED: status_text = "Unauthorized"; break; | |
| 15 case HTTP_FORBIDDEN: status_text = "Forbidden"; break; | |
| 16 case HTTP_NOT_FOUND: status_text = "Not Found"; break; | |
| 17 case HTTP_INTERNAL_ERROR: status_text = "Internal Server Error"; break; | |
| 18 default: status_text = "Unknown"; break; | |
| 19 } | |
| 20 | |
| 21 sprintf( | |
| 22 buffer, | |
| 23 "HTTP/2.2 %d %s\r\n" | |
| 24 "Content-Type: %s\r\n" | |
| 25 "Content-Length: %d\r\n" | |
| 26 "Connection: close\r\n" | |
| 27 "\r\n", | |
| 28 status, status_text, content_type, content_length | |
| 29 ); | |
| 30 } | |
| 31 | |
| 32 void Seobeo_Web_HandleClientRequest(Seobeo_PHandle p_cli_handle, | |
| 33 Dowa_PHashMap p_html_cache) | |
| 34 { | |
| 35 Dowa_PArena p_response_arena = Dowa_Arena_Create(8192); | |
| 36 Dowa_PHashMap p_req_map = NULL; | |
| 37 | |
| 38 Dowa_PHashEntry entry = NULL; | |
| 39 Dowa_PHashMap p_current = p_html_cache; | |
| 40 char *slash; | |
| 41 | |
| 42 if (!p_response_arena) { perror("Dowa_Arena_Initialize"); goto clean_up; } | |
| 43 | |
| 44 void *p_response_header = Dowa_Arena_Allocate(p_response_arena, (size_t)2048); | |
| 45 if (!p_response_header) { perror("Dowa_Arena_Allocate"); goto clean_up; } | |
| 46 | |
| 47 p_req_map = Dowa_HashMap_Create(32); | |
| 48 if (Seobeo_Web_ParseClientHeader(p_cli_handle, p_req_map) != 0) | |
| 49 { | |
| 50 // malformed request or closed — respond 400 | |
| 51 Seobeo_Web_GenerateResponseHeader(p_response_header, | |
| 52 HTTP_BAD_REQUEST, | |
| 53 "text/plain", 0); | |
| 54 Seobeo_Handle_Queue(p_cli_handle, | |
| 55 (const uint8*)p_response_header, | |
| 56 (uint32)strlen(p_response_header)); | |
| 57 Seobeo_Handle_Flush(p_cli_handle); | |
| 58 goto clean_up; | |
| 59 } | |
| 60 | |
| 61 const char *path = (const char*)Dowa_HashMap_Get(p_req_map, "Path"); | |
| 62 | |
| 63 char *file_path = Dowa_Arena_Allocate(p_response_arena, (size_t)512); | |
| 64 | |
| 65 if (!path || strcmp(path, "/") == 0) | |
| 66 { | |
| 67 strcpy(file_path, "index.html"); | |
| 68 } | |
| 69 else | |
| 70 { | |
| 71 size_t L = strlen(path); | |
| 72 // strip leading '/' | |
| 73 if (path[0] == '/') | |
| 74 { | |
| 75 if (strchr(path, '.') == NULL) | |
| 76 snprintf(file_path, 512, "%.*s/index.html", (int)(L-1), path+1); | |
| 77 else | |
| 78 snprintf(file_path, 512, "%.*s", (int)(L-1), path+1); | |
| 79 } | |
| 80 else | |
| 81 { | |
| 82 // Probably never get here? | |
| 83 strcpy(file_path, path); | |
| 84 } | |
| 85 } | |
| 86 | |
| 87 // printf("\n\nfile_path: %s\n", file_path); | |
| 88 | |
| 89 // Recursively go though the path until it gets to a file | |
| 90 while ((slash = strchr(file_path, '/'))) | |
| 91 { | |
| 92 *slash = '\0'; // e.g. file_path="foo", slash+1="index.html" | |
| 93 char *dir = file_path; // "foo" | |
| 94 file_path = slash + 1; // "index.html" | |
| 95 | |
| 96 p_current = Dowa_HashMap_Get(p_current, dir); | |
| 97 if (!p_current) { perror("No value"); goto clean_up; } | |
| 98 } | |
| 99 | |
| 100 size_t pos = Dowa_HashMap_GetPosition(p_current, file_path); | |
| 101 entry = p_current->entries[pos]; | |
| 102 | |
| 103 // Missing so 404 | |
| 104 if (!entry) | |
| 105 { | |
| 106 Seobeo_Web_GenerateResponseHeader(p_response_header, | |
| 107 HTTP_NOT_FOUND, | |
| 108 "text/html", 0); | |
| 109 Seobeo_Handle_Queue(p_cli_handle, | |
| 110 (const uint8*)p_response_header, | |
| 111 (uint32)strlen(p_response_header)); | |
| 112 Seobeo_Handle_Flush(p_cli_handle); | |
| 113 goto clean_up; | |
| 114 } | |
| 115 | |
| 116 | |
| 117 const char *mime = "application/octet-stream"; // Default binary | |
| 118 if (strstr(file_path, ".html")) mime = "text/html; charset=utf-8"; | |
| 119 else if (strstr(file_path, ".css")) mime = "text/css"; | |
| 120 else if (strstr(file_path, ".js")) mime = "application/javascript"; | |
| 121 else if (strstr(file_path, ".png")) mime = "image/png"; | |
| 122 else if (strstr(file_path, ".jpg") || strstr(file_path, ".jpeg")) mime = "image/jpeg"; | |
| 123 else if (strstr(file_path, ".gif")) mime = "image/gif"; | |
| 124 else if (strstr(file_path, ".svg")) mime = "image/svg+xml"; | |
| 125 else if (strstr(file_path, ".ico")) mime = "image/x-icon"; | |
| 126 else if (strstr(file_path, ".json")) mime = "application/json"; | |
| 127 | |
| 128 size_t body_size = entry->capacity; | |
| 129 Seobeo_Web_GenerateResponseHeader(p_response_header, | |
| 130 HTTP_OK, | |
| 131 mime, | |
| 132 body_size); | |
| 133 Seobeo_Handle_Queue(p_cli_handle, | |
| 134 (const uint8*)p_response_header, | |
| 135 (uint32)strlen(p_response_header)); | |
| 136 Seobeo_Handle_Queue(p_cli_handle, | |
| 137 (const uint8*)entry->buffer, | |
| 138 (uint32)body_size); | |
| 139 Seobeo_Handle_Flush(p_cli_handle); | |
| 140 | |
| 141 clean_up: | |
| 142 Seobeo_Handle_Destroy(p_cli_handle); | |
| 143 Dowa_Arena_Free(p_response_arena); | |
| 144 Dowa_HashMap_Free(p_req_map); // TODO: Maybe initilized hashmap within the Arena? | |
| 145 } | |
| 146 | |
| 147 int Seobeo_Web_ParseClientHeader(Seobeo_PHandle p_handle, Dowa_PHashMap map) | |
| 148 { | |
| 149 // 1) Fill read_buffer until we see "\r\n\r\n" | |
| 150 while (1) | |
| 151 { | |
| 152 int r = Seobeo_Handle_Read(p_handle); | |
| 153 if (r < 0) return -1; // fatal error | |
| 154 if (r == -2) return -2; // connection closed TODO: Add this as part of Handle struct. | |
| 155 | |
| 156 if (p_handle->read_buffer_len >= 4 && | |
| 157 strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL) | |
| 158 { | |
| 159 break; | |
| 160 } | |
| 161 if (r == 0) return 1; // EAGAIN, try again later TODO: Add this as part of Handle struct. | |
| 162 } | |
| 163 | |
| 164 // 2) Parse request‐line "METHOD SP PATH SP VERSION CRLF" | |
| 165 char *buf = (char*)p_handle->read_buffer; | |
| 166 char *hdr_end = strstr(buf, "\r\n\r\n"); | |
| 167 size_t hdr_len = hdr_end - buf + 4; | |
| 168 | |
| 169 char method[16], path[256], version[16]; | |
| 170 if (sscanf(buf, "%15s %255s %15s", method, path, version) != 3) | |
| 171 { | |
| 172 return -1; | |
| 173 } | |
| 174 | |
| 175 Dowa_HashMap_PushValueWithType(map, "Method", method, strlen(method) + 1, DOWA_HASH_MAP_TYPE_STRING); | |
| 176 Dowa_HashMap_PushValueWithType(map, "Path", path, strlen(path) + 1, DOWA_HASH_MAP_TYPE_STRING); | |
| 177 Dowa_HashMap_PushValueWithType(map, "Version", version, strlen(version) + 1, DOWA_HASH_MAP_TYPE_STRING); | |
| 178 | |
| 179 // 3) Parse each header line until the blank line | |
| 180 char *line = buf + strlen(method) + 1 + strlen(path) + 1 + strlen(version) + 2; | |
| 181 while (line < hdr_end) | |
| 182 { | |
| 183 char *next = strstr(line, "\r\n"); | |
| 184 if (!next) break; | |
| 185 | |
| 186 // split at colon | |
| 187 char *colon = memchr(line, ':', next - line); | |
| 188 if (colon) { | |
| 189 size_t key_len = colon - line; | |
| 190 size_t value_len = next - colon - 1; | |
| 191 | |
| 192 char *val_start = colon + 1; | |
| 193 if (*val_start == ' ') | |
| 194 { | |
| 195 val_start++; | |
| 196 value_len--; | |
| 197 } | |
| 198 | |
| 199 char *key = malloc(key_len + 1); | |
| 200 memcpy(key, line, key_len); | |
| 201 key[key_len] = '\0'; | |
| 202 | |
| 203 char *val = malloc(value_len + 1); | |
| 204 memcpy(val, val_start, value_len); | |
| 205 val[value_len] = '\0'; | |
| 206 | |
| 207 Dowa_HashMap_PushValue(map, key, val, value_len + 1); | |
| 208 | |
| 209 free(key); | |
| 210 free(val); | |
| 211 } | |
| 212 | |
| 213 line = next + 2; | |
| 214 } | |
| 215 Seobeo_Handle_Consume(p_handle, (uint32)hdr_len); | |
| 216 | |
| 217 // 4) If Content-Length was provided, read that much body | |
| 218 int content_length_pos = Dowa_HashMap_GetPosition(map, "Content-Length"); | |
| 219 Dowa_PHashEntry p_content_length_entry = map->entries[content_length_pos]; | |
| 220 if (p_content_length_entry) | |
| 221 { | |
| 222 size_t body_len = atoi((char*)p_content_length_entry->buffer); | |
| 223 while (p_handle->read_buffer_len < body_len) | |
| 224 { | |
| 225 int r = Seobeo_Handle_Read(p_handle); | |
| 226 if (r < 0) return -1; | |
| 227 if (r == 0) return 1; // wait for more data | |
| 228 } | |
| 229 | |
| 230 char *body = malloc(body_len + 1); | |
| 231 memcpy(body, p_handle->read_buffer, body_len); | |
| 232 body[body_len] = '\0'; | |
| 233 | |
| 234 Dowa_HashMap_PushValue(map, "Body", body, body_len + 1); | |
| 235 free(body); | |
| 236 | |
| 237 Seobeo_Handle_Consume(p_handle, (uint32)body_len); | |
| 238 } | |
| 239 | |
| 240 return 0; // success; map now holds Method, Path, Version, headers, and optional Body | |
| 241 } | |
| 242 | |
| 243 // TODO: Do epoll or kqueue depending on the OS. | |
| 244 void SigchildHandler(int s) | |
| 245 { | |
| 246 (void)s; // quiet unused variable warning | |
| 247 | |
| 248 // waitpid() might overwrite errno, so we save and restore it: | |
| 249 int saved_errno = errno; | |
| 250 | |
| 251 while(waitpid(-1, NULL, WNOHANG) > 0); | |
| 252 | |
| 253 errno = saved_errno; | |
| 254 } | |
| 255 | |
| 256 int Seobeo_Web_StartBasicHTTPServer( | |
| 257 const char *folder_path, | |
| 258 const char *port, | |
| 259 Seobeo_ServerMode mode, | |
| 260 int thread_count) | |
| 261 { | |
| 262 Dowa_PHashMap p_html_cache = Dowa_HashMap_Create(1024); | |
| 263 if (Dowa_HashMap_Cache_Folder(p_html_cache, | |
| 264 folder_path) != 0) | |
| 265 { | |
| 266 perror("Dowa_Cache_Folder"); | |
| 267 return -1; | |
| 268 } | |
| 269 | |
| 270 Seobeo_PHandle p_server_handle = | |
| 271 Seobeo_Stream_Handle_Create(NULL, port); | |
| 272 if (p_server_handle->socket < 0) return 1; | |
| 273 printf("Listening on port %s\n", port); | |
| 274 | |
| 275 // Fork‐based fallback | |
| 276 if (mode == SEOBEO_MODE_FORK) | |
| 277 { | |
| 278 struct sigaction sa; | |
| 279 sa.sa_handler = SigchildHandler; | |
| 280 sigemptyset(&sa.sa_mask); | |
| 281 sa.sa_flags = SA_RESTART; | |
| 282 sigaction(SIGCHLD, &sa, NULL); | |
| 283 | |
| 284 while (1) { | |
| 285 Seobeo_PHandle cli = | |
| 286 Seobeo_Stream_Handle_Accept(p_server_handle); | |
| 287 if (!cli) continue; | |
| 288 | |
| 289 if (fork() == 0) { | |
| 290 Seobeo_Web_HandleClientRequest(cli, | |
| 291 p_html_cache); | |
| 292 _exit(0); | |
| 293 } | |
| 294 Seobeo_Handle_Destroy(cli); | |
| 295 } | |
| 296 } | |
| 297 | |
| 298 if (mode == SEOBEO_MODE_EDGE) | |
| 299 { | |
| 300 Seobeo_Web_Edge(p_server_handle, thread_count, p_html_cache); | |
| 301 } | |
| 302 | |
| 303 return -1; | |
| 304 } |