diff --git a/http.odin b/http.odin index 729bf24..dd8864e 100644 --- a/http.odin +++ b/http.odin @@ -103,6 +103,14 @@ response_set_body :: proc(resp: ^HTTP_Response, data: []byte) { // Request handler function type Request_Handler :: #type proc(ctx: rawptr, request: ^HTTP_Request, request_alloc: mem.Allocator) -> HTTP_Response +// Parse error enum +Parse_Error :: enum { + None, + Connection_Closed, + Invalid_Request, + Body_Too_Large, +} + // Server configuration Server_Config :: struct { max_body_size: int, // default 100MB @@ -176,6 +184,9 @@ server_start :: proc(server: ^Server) -> bool { server.running = true fmt.printfln("HTTP server listening on %v (thread-per-connection)", server.endpoint) + fmt.printfln(" Max body size: %d MB", server.config.max_body_size / (1024 * 1024)) + fmt.printfln(" Max headers: %d", server.config.max_headers) + fmt.printfln(" Keep-alive: %v", server.config.enable_keep_alive) // Accept loop - spawn a thread for each connection for server.running { @@ -199,7 +210,6 @@ server_start :: proc(server: ^Server) -> bool { t.init_context = context t.data = conn_data thread.start(t) - // Thread will clean itself up when done } else { // Failed to create thread, close connection net.close(conn) @@ -222,7 +232,7 @@ server_stop :: proc(server: ^Server) { // Worker thread procedure connection_worker_thread :: proc(t: ^thread.Thread) { - defer thread.destroy(t) // Clean up thread when done + defer thread.destroy(t) conn_data := cast(^Connection_Task_Data)t.data defer free(conn_data, conn_data.server.allocator) @@ -230,6 +240,15 @@ connection_worker_thread :: proc(t: ^thread.Thread) { handle_connection(conn_data.server, conn_data.conn, conn_data.source) } +// Create error response +make_error_response_simple :: proc(allocator: mem.Allocator, status: HTTP_Status, message: string) -> HTTP_Response { + response := response_init(allocator) + response_set_status(&response, status) + response_add_header(&response, "Content-Type", "text/plain") + response_set_body(&response, transmute([]byte)message) + return response +} + // Handle a single connection handle_connection :: proc(server: ^Server, conn: net.TCP_Socket, source: net.Endpoint) { defer net.close(conn) @@ -253,8 +272,21 @@ handle_connection :: proc(server: ^Server, conn: net.TCP_Socket, source: net.End context.allocator = request_alloc defer context.allocator = old - request, parse_ok := parse_request(conn, request_alloc, server.config) - if !parse_ok { + request, parse_err := parse_request(conn, request_alloc, server.config) + + // Handle parse errors + if parse_err != .None { + #partial switch parse_err { + case .Body_Too_Large: + // Send 413 Payload Too Large + response := make_error_response_simple(request_alloc, .Payload_Too_Large, + fmt.tprintf("Request body exceeds maximum size of %d bytes", server.config.max_body_size)) + send_response(conn, &response, request_alloc) + case .Invalid_Request: + // Send 400 Bad Request + response := make_error_response_simple(request_alloc, .Bad_Request, "Invalid HTTP request") + send_response(conn, &response, request_alloc) + } break } @@ -284,13 +316,13 @@ parse_request :: proc( conn: net.TCP_Socket, allocator: mem.Allocator, config: Server_Config, -) -> (HTTP_Request, bool) { +) -> (HTTP_Request, Parse_Error) { // Read request line and headers buffer := make([]byte, config.read_buffer_size, allocator) bytes_read, read_err := net.recv_tcp(conn, buffer) if read_err != nil || bytes_read == 0 { - return {}, false + return {}, .Connection_Closed } request_data := buffer[:bytes_read] @@ -298,7 +330,7 @@ parse_request :: proc( // Find end of headers (\r\n\r\n) header_end_idx := strings.index(string(request_data), "\r\n\r\n") if header_end_idx < 0 { - return {}, false + return {}, .Invalid_Request } header_section := string(request_data[:header_end_idx]) @@ -307,13 +339,13 @@ parse_request :: proc( // Parse request line lines := strings.split_lines(header_section, allocator) if len(lines) == 0 { - return {}, false + return {}, .Invalid_Request } request_line := lines[0] parts := strings.split(request_line, " ", allocator) if len(parts) < 3 { - return {}, false + return {}, .Invalid_Request } method := method_from_string(parts[0]) @@ -339,6 +371,11 @@ parse_request :: proc( name = strings.clone(name, allocator), value = strings.clone(value, allocator), }) + + // Check max headers limit + if len(headers) > config.max_headers { + return {}, .Invalid_Request + } } // Read body if Content-Length present @@ -348,7 +385,12 @@ parse_request :: proc( if cl, ok := content_length_header.?; ok { content_length := strconv.parse_int(cl) or_else 0 - if content_length > 0 && content_length <= config.max_body_size { + // Check if body size exceeds limit + if content_length > config.max_body_size { + return {}, .Body_Too_Large + } + + if content_length > 0 { // Check if we already have the body in buffer existing_body := request_data[body_start:] @@ -370,7 +412,7 @@ parse_request :: proc( n, err := net.recv_tcp(conn, chunk) if err != nil || n == 0 { - return {}, false + return {}, .Connection_Closed } copy(body[body_written:], chunk[:n]) @@ -386,7 +428,7 @@ parse_request :: proc( path = path, headers = headers[:], body = body, - }, true + }, .None } // Helper to get header from slice