Mercurial
comparison seobeo/s_web.c @ 195:f8f5004a920a
Merging back hg-web-tip
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 27 Jan 2026 06:51:44 -0800 |
| parents | a69485d9f2e1 |
| children |
comparison
equal
deleted
inserted
replaced
| 189:14cc84ba35a0 | 195:f8f5004a920a |
|---|---|
| 149 boolean is_http11 = (strstr(http_version, "1.1") != NULL); | 149 boolean is_http11 = (strstr(http_version, "1.1") != NULL); |
| 150 | 150 |
| 151 void *p_conn_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Connection"); | 151 void *p_conn_kv = Dowa_HashMap_Get_Ptr(p_req_map, "Connection"); |
| 152 const char *conn_header = p_conn_kv ? ((Seobeo_Request_Entry*)p_conn_kv)->value : NULL; | 152 const char *conn_header = p_conn_kv ? ((Seobeo_Request_Entry*)p_conn_kv)->value : NULL; |
| 153 | 153 |
| 154 void *p_real_ip_kv = Dowa_HashMap_Get_Ptr(p_req_map, "X-Real-IP"); | |
| 155 const char *real_ip = p_real_ip_kv ? ((Seobeo_Request_Entry*)p_real_ip_kv)->value : NULL; | |
| 156 if (!real_ip) | |
| 157 real_ip = p_cli_handle->host; | |
| 158 | |
| 154 if (conn_header) | 159 if (conn_header) |
| 155 { | 160 { |
| 156 if (connection_header_contains(conn_header, "close")) | 161 if (connection_header_contains(conn_header, "close")) |
| 157 should_keep_alive = FALSE; | 162 should_keep_alive = FALSE; |
| 158 else if (connection_header_contains(conn_header, "keep-alive")) | 163 else if (connection_header_contains(conn_header, "keep-alive")) |
| 183 goto clean_up_arenas; | 188 goto clean_up_arenas; |
| 184 } | 189 } |
| 185 | 190 |
| 186 // --- Check for WebSocket upgrade request --- | 191 // --- Check for WebSocket upgrade request --- |
| 187 #ifdef SEOBEO_WEBSOCKET_SERVER | 192 #ifdef SEOBEO_WEBSOCKET_SERVER |
| 193 Seobeo_Log(SEOBEO_DEBUG, "Web socket path \n"); | |
| 188 if (Seobeo_WebSocket_Server_Handle_Upgrade(p_cli_handle, p_req_map, path)) | 194 if (Seobeo_WebSocket_Server_Handle_Upgrade(p_cli_handle, p_req_map, path)) |
| 189 { | 195 { |
| 190 Seobeo_Log(SEOBEO_INFO, "WebSocket connection established\n"); | 196 Seobeo_Log(SEOBEO_INFO, "WebSocket connection established\n"); |
| 191 Dowa_Arena_Free(p_request_arena); | 197 Dowa_Arena_Free(p_request_arena); |
| 192 Dowa_Arena_Free(p_response_arena); | 198 Dowa_Arena_Free(p_response_arena); |
| 193 return FALSE; // WebSocket takes over, don't keep-alive in HTTP sense | 199 return FALSE; // WebSocket takes over, don't keep-alive in HTTP sense |
| 194 } | 200 } |
| 195 #endif | 201 #endif |
| 196 | 202 |
| 197 // --- Try to match API route first --- | 203 // --- Try to match streaming route first --- |
| 204 Seobeo_Stream_Handler stream_handler = Seobeo_Router_Find_Stream_Handler(method, path, &p_req_map, p_request_arena); | |
| 205 if (stream_handler != NULL) | |
| 206 { | |
| 207 stream_handler(p_cli_handle, p_req_map, p_response_arena); | |
| 208 goto clean_up_arenas; | |
| 209 } | |
| 210 | |
| 211 // --- Try to match API route --- | |
| 198 Seobeo_Route_Handler handler = Seobeo_Router_Find_Handler(method, path, &p_req_map, p_request_arena); | 212 Seobeo_Route_Handler handler = Seobeo_Router_Find_Handler(method, path, &p_req_map, p_request_arena); |
| 199 if (handler != NULL) | 213 if (handler != NULL) |
| 200 { | 214 { |
| 201 Seobeo_Request_Entry *p_response_map = handler(p_req_map, p_response_arena); | 215 Seobeo_Request_Entry *p_response_map = handler(p_req_map, p_response_arena); |
| 202 Seobeo_Router_Send_Response_KeepAlive(p_cli_handle, p_response_map, p_response_arena, should_keep_alive); | 216 Seobeo_Router_Send_Response_KeepAlive(p_cli_handle, p_response_map, p_response_arena, should_keep_alive); |
| 331 if (p_handle->read_buffer_len >= 4 && | 345 if (p_handle->read_buffer_len >= 4 && |
| 332 strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL) | 346 strstr((char*)p_handle->read_buffer, "\r\n\r\n") != NULL) |
| 333 break; | 347 break; |
| 334 | 348 |
| 335 if (r == 0) | 349 if (r == 0) |
| 336 return 1; // EAGAIN, try again later TODO: Add this as part of Handle struct. | 350 { |
| 351 Seobeo_Log(SEOBEO_INFO, "Waiting?\n"); | |
| 352 continue; // EAGAIN, try again later TODO: Add this as part of Handle struct. | |
| 353 } | |
| 337 } | 354 } |
| 338 | 355 |
| 339 // "METHOD SP PATH SP VERSION CRLF" | 356 // "METHOD SP PATH SP VERSION CRLF" |
| 340 char *buf = (char*)p_handle->read_buffer; | 357 char *buf = (char*)p_handle->read_buffer; |
| 341 char *hdr_end = strstr(buf, "\r\n\r\n"); | 358 char *hdr_end = strstr(buf, "\r\n\r\n"); |
| 446 while (line < hdr_end) | 463 while (line < hdr_end) |
| 447 { | 464 { |
| 448 char *next = strstr(line, "\r\n"); | 465 char *next = strstr(line, "\r\n"); |
| 449 if (!next) break; | 466 if (!next) break; |
| 450 | 467 |
| 451 // split at colon | |
| 452 char *colon = memchr(line, ':', next - line); | 468 char *colon = memchr(line, ':', next - line); |
| 453 if (colon) | 469 if (colon) |
| 454 { | 470 { |
| 455 size_t key_len = colon - line; | 471 size_t key_len = colon - line; |
| 456 size_t value_len = next - colon - 1; | 472 size_t value_len = next - colon - 1; |
| 470 char *val = Dowa_Arena_Allocate(p_arena, value_len + 1); | 486 char *val = Dowa_Arena_Allocate(p_arena, value_len + 1); |
| 471 if (!val) return -1; | 487 if (!val) return -1; |
| 472 memcpy(val, val_start, value_len); | 488 memcpy(val, val_start, value_len); |
| 473 val[value_len] = '\0'; | 489 val[value_len] = '\0'; |
| 474 | 490 |
| 475 // Both key and value are arena-allocated, hashmap will use them | |
| 476 Dowa_HashMap_Push_Arena(*pp_map, key, val, p_arena); | 491 Dowa_HashMap_Push_Arena(*pp_map, key, val, p_arena); |
| 477 } | 492 } |
| 478 | 493 |
| 479 line = next + 2; | 494 line = next + 2; |
| 480 } | 495 } |
| 488 const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value; | 503 const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value; |
| 489 size_t body_len = atoi(content_length_str); | 504 size_t body_len = atoi(content_length_str); |
| 490 | 505 |
| 491 Seobeo_Log(SEOBEO_DEBUG, "Content-Length=%zu, reading body in chunks...\n", body_len); | 506 Seobeo_Log(SEOBEO_DEBUG, "Content-Length=%zu, reading body in chunks...\n", body_len); |
| 492 | 507 |
| 493 // Allocate buffer for entire body | |
| 494 char *body = Dowa_Arena_Allocate(p_arena, body_len + 1); | 508 char *body = Dowa_Arena_Allocate(p_arena, body_len + 1); |
| 495 if (!body) | 509 if (!body) |
| 496 { | 510 { |
| 497 Seobeo_Log(SEOBEO_ERROR, "Failed to allocate %zu bytes for body\n", body_len); | 511 Seobeo_Log(SEOBEO_ERROR, "Failed to allocate %zu bytes for body\n", body_len); |
| 498 return -1; | 512 return -1; |
| 604 /* Router logic */ | 618 /* Router logic */ |
| 605 struct Seobeo_Route_Struct { | 619 struct Seobeo_Route_Struct { |
| 606 char *method; // "GET", "POST", "PUT", "DELETE" | 620 char *method; // "GET", "POST", "PUT", "DELETE" |
| 607 char *path_pattern; // "/v1/users/:id/posts/:post_id" | 621 char *path_pattern; // "/v1/users/:id/posts/:post_id" |
| 608 Seobeo_Route_Handler handler; | 622 Seobeo_Route_Handler handler; |
| 623 Seobeo_Stream_Handler stream_handler; // For streaming responses | |
| 609 | 624 |
| 610 // Pre-parsed path segments for efficient matching | 625 // Pre-parsed path segments for efficient matching |
| 611 char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"] | 626 char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"] |
| 612 boolean *is_param; // [false, false, true, false, true] | 627 boolean *is_param; // [false, false, true, false, true] |
| 613 size_t segment_count; | 628 size_t segment_count; |
| 625 Seobeo_Route route = {0}; | 640 Seobeo_Route route = {0}; |
| 626 | 641 |
| 627 route.method = strdup(method); | 642 route.method = strdup(method); |
| 628 route.path_pattern = strdup(path_pattern); | 643 route.path_pattern = strdup(path_pattern); |
| 629 route.handler = handler; | 644 route.handler = handler; |
| 645 route.stream_handler = NULL; | |
| 646 route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL); | |
| 647 route.segment_count = Dowa_Array_Length(route.path_segments); | |
| 648 route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count); | |
| 649 | |
| 650 for (size_t i = 0; i < route.segment_count; i++) | |
| 651 route.is_param[i] = (route.path_segments[i][0] == ':'); | |
| 652 | |
| 653 Dowa_Array_Push(g_routes, route); | |
| 654 } | |
| 655 | |
| 656 void Seobeo_Router_Register_Stream(const char *method, const char *path_pattern, Seobeo_Stream_Handler handler) | |
| 657 { | |
| 658 Seobeo_Route route = {0}; | |
| 659 | |
| 660 route.method = strdup(method); | |
| 661 route.path_pattern = strdup(path_pattern); | |
| 662 route.handler = NULL; | |
| 663 route.stream_handler = handler; | |
| 630 route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL); | 664 route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL); |
| 631 route.segment_count = Dowa_Array_Length(route.path_segments); | 665 route.segment_count = Dowa_Array_Length(route.path_segments); |
| 632 route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count); | 666 route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count); |
| 633 | 667 |
| 634 for (size_t i = 0; i < route.segment_count; i++) | 668 for (size_t i = 0; i < route.segment_count; i++) |
| 707 } | 741 } |
| 708 | 742 |
| 709 return NULL; | 743 return NULL; |
| 710 } | 744 } |
| 711 | 745 |
| 746 Seobeo_Stream_Handler Seobeo_Router_Find_Stream_Handler( | |
| 747 const char *method, | |
| 748 const char *path, | |
| 749 Seobeo_Request_Entry **pp_request_map, | |
| 750 Dowa_Arena *p_arena) | |
| 751 { | |
| 752 if (g_routes == NULL || method == NULL || path == NULL) | |
| 753 return NULL; | |
| 754 | |
| 755 size_t route_count = Dowa_Array_Length(g_routes); | |
| 756 for (size_t i = 0; i < route_count; i++) | |
| 757 { | |
| 758 Seobeo_Route *route = &g_routes[i]; | |
| 759 if (strcmp(route->method, method) != 0) | |
| 760 continue; | |
| 761 | |
| 762 if (route->stream_handler && match_route_and_extract(route, path, pp_request_map, p_arena)) | |
| 763 return route->stream_handler; | |
| 764 } | |
| 765 | |
| 766 return NULL; | |
| 767 } | |
| 768 | |
| 712 void Seobeo_Router_Send_Response( | 769 void Seobeo_Router_Send_Response( |
| 713 Seobeo_Handle *p_handle, | 770 Seobeo_Handle *p_handle, |
| 714 Seobeo_Request_Entry *p_response_map, | 771 Seobeo_Request_Entry *p_response_map, |
| 715 Dowa_Arena *p_arena) | 772 Dowa_Arena *p_arena) |
| 716 { | 773 { |
| 742 } | 799 } |
| 743 | 800 |
| 744 const char *body = ""; | 801 const char *body = ""; |
| 745 void *p_body_kv = Dowa_HashMap_Get_Ptr(p_response_map, "body"); | 802 void *p_body_kv = Dowa_HashMap_Get_Ptr(p_response_map, "body"); |
| 746 if (p_body_kv) | 803 if (p_body_kv) |
| 747 { | |
| 748 body = ((Seobeo_Request_Entry*)p_body_kv)->value; | 804 body = ((Seobeo_Request_Entry*)p_body_kv)->value; |
| 749 } | |
| 750 | 805 |
| 751 const char *content_type = "text/html"; | 806 const char *content_type = "text/html"; |
| 752 void *p_content_type_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-type"); | 807 void *p_content_type_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-type"); |
| 753 if (p_content_type_kv) | 808 if (p_content_type_kv) |
| 754 { | |
| 755 content_type = ((Seobeo_Request_Entry*)p_content_type_kv)->value; | 809 content_type = ((Seobeo_Request_Entry*)p_content_type_kv)->value; |
| 756 } | 810 |
| 757 | 811 // TODO: Update this to be integer |
| 758 size_t body_length = strlen(body); | 812 size_t body_length; |
| 759 void *p_content_length_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-length"); | 813 void *p_content_length_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-length"); |
| 760 if (p_content_length_kv) | 814 if (p_content_length_kv) |
| 761 { | 815 { |
| 762 const char *content_length_str = ((Seobeo_Request_Entry*)p_content_length_kv)->value; | 816 const char *content_length_str = ((Seobeo_Request_Entry*)p_content_length_kv)->value; |
| 763 body_length = atoi(content_length_str); | 817 body_length = atoi(content_length_str); |
| 764 } | 818 } |
| 819 else | |
| 820 body_length = strlen(body); | |
| 765 | 821 |
| 766 char *header = Dowa_Arena_Allocate(p_arena, 4096); | 822 char *header = Dowa_Arena_Allocate(p_arena, 4096); |
| 767 Seobeo_Web_Header_Generate_KeepAlive(header, status, content_type, body_length, keep_alive); | 823 Seobeo_Web_Header_Generate_KeepAlive(header, status, content_type, body_length, keep_alive); |
| 768 for (int i = 0; i < Dowa_Array_Length(p_response_map); i++) | 824 for (int i = 0; i < Dowa_Array_Length(p_response_map); i++) |
| 769 { | 825 { |
| 780 sprintf(temp, "%s: %s\r\n\r\n", p_response_map[i].key, p_response_map[i].value); | 836 sprintf(temp, "%s: %s\r\n\r\n", p_response_map[i].key, p_response_map[i].value); |
| 781 memcpy(&header[current_header_len - 2 /* \r\n */], temp, strlen(temp)); | 837 memcpy(&header[current_header_len - 2 /* \r\n */], temp, strlen(temp)); |
| 782 free(temp); | 838 free(temp); |
| 783 } | 839 } |
| 784 | 840 |
| 841 printf("hEADER %s\n", header); | |
| 842 | |
| 785 Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header)); | 843 Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header)); |
| 786 Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length); | 844 Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length); |
| 787 Seobeo_Handle_Flush(p_handle); | 845 Seobeo_Handle_Flush(p_handle); |
| 788 } | 846 } |
| 789 | 847 |
| 802 if (route->is_param) free(route->is_param); | 860 if (route->is_param) free(route->is_param); |
| 803 } | 861 } |
| 804 Dowa_Array_Free(g_routes); | 862 Dowa_Array_Free(g_routes); |
| 805 g_routes = NULL; | 863 g_routes = NULL; |
| 806 } | 864 } |
| 865 | |
| 866 // Written by AI. I don't know what it does. | |
| 867 void Seobeo_Url_Decode(char *dst, const char *src) | |
| 868 { | |
| 869 char a, b; | |
| 870 while (*src) { | |
| 871 /* Check if we have a % followed by two valid hex characters */ | |
| 872 if (*src == '%' && src[1] && src[2]) { | |
| 873 a = src[1]; | |
| 874 b = src[2]; | |
| 875 | |
| 876 /* Manual isxdigit check and conversion for 'a' */ | |
| 877 int a_val = -1; | |
| 878 if (a >= '0' && a <= '9') a_val = a - '0'; | |
| 879 else if (a >= 'a' && a <= 'f') a_val = a - 'a' + 10; | |
| 880 else if (a >= 'A' && a <= 'F') a_val = a - 'A' + 10; | |
| 881 | |
| 882 /* Manual isxdigit check and conversion for 'b' */ | |
| 883 int b_val = -1; | |
| 884 if (b >= '0' && b <= '9') b_val = b - '0'; | |
| 885 else if (b >= 'a' && b <= 'f') b_val = b - 'a' + 10; | |
| 886 else if (b >= 'A' && b <= 'F') b_val = b - 'A' + 10; | |
| 887 | |
| 888 /* If both were valid hex, combine them */ | |
| 889 if (a_val != -1 && b_val != -1) { | |
| 890 *dst++ = (char)((a_val << 4) | b_val); | |
| 891 src += 3; | |
| 892 continue; | |
| 893 } | |
| 894 } | |
| 895 | |
| 896 /* Handle '+' as space, otherwise copy character literally */ | |
| 897 if (*src == '+') { | |
| 898 *dst++ = ' '; | |
| 899 } else { | |
| 900 *dst++ = *src; | |
| 901 } | |
| 902 src++; | |
| 903 } | |
| 904 *dst = '\0'; | |
| 905 } |