enforce body size

This commit is contained in:
2026-02-17 14:54:29 -05:00
parent 6bc1a03347
commit a7f2a5ab59

View File

@@ -103,6 +103,14 @@ response_set_body :: proc(resp: ^HTTP_Response, data: []byte) {
// Request handler function type // Request handler function type
Request_Handler :: #type proc(ctx: rawptr, request: ^HTTP_Request, request_alloc: mem.Allocator) -> HTTP_Response 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 configuration
Server_Config :: struct { Server_Config :: struct {
max_body_size: int, // default 100MB max_body_size: int, // default 100MB
@@ -176,6 +184,9 @@ server_start :: proc(server: ^Server) -> bool {
server.running = true server.running = true
fmt.printfln("HTTP server listening on %v (thread-per-connection)", server.endpoint) 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 // Accept loop - spawn a thread for each connection
for server.running { for server.running {
@@ -199,7 +210,6 @@ server_start :: proc(server: ^Server) -> bool {
t.init_context = context t.init_context = context
t.data = conn_data t.data = conn_data
thread.start(t) thread.start(t)
// Thread will clean itself up when done
} else { } else {
// Failed to create thread, close connection // Failed to create thread, close connection
net.close(conn) net.close(conn)
@@ -222,7 +232,7 @@ server_stop :: proc(server: ^Server) {
// Worker thread procedure // Worker thread procedure
connection_worker_thread :: proc(t: ^thread.Thread) { 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 conn_data := cast(^Connection_Task_Data)t.data
defer free(conn_data, conn_data.server.allocator) 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) 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 a single connection
handle_connection :: proc(server: ^Server, conn: net.TCP_Socket, source: net.Endpoint) { handle_connection :: proc(server: ^Server, conn: net.TCP_Socket, source: net.Endpoint) {
defer net.close(conn) defer net.close(conn)
@@ -253,8 +272,21 @@ handle_connection :: proc(server: ^Server, conn: net.TCP_Socket, source: net.End
context.allocator = request_alloc context.allocator = request_alloc
defer context.allocator = old defer context.allocator = old
request, parse_ok := parse_request(conn, request_alloc, server.config) request, parse_err := parse_request(conn, request_alloc, server.config)
if !parse_ok {
// 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 break
} }
@@ -284,13 +316,13 @@ parse_request :: proc(
conn: net.TCP_Socket, conn: net.TCP_Socket,
allocator: mem.Allocator, allocator: mem.Allocator,
config: Server_Config, config: Server_Config,
) -> (HTTP_Request, bool) { ) -> (HTTP_Request, Parse_Error) {
// Read request line and headers // Read request line and headers
buffer := make([]byte, config.read_buffer_size, allocator) buffer := make([]byte, config.read_buffer_size, allocator)
bytes_read, read_err := net.recv_tcp(conn, buffer) bytes_read, read_err := net.recv_tcp(conn, buffer)
if read_err != nil || bytes_read == 0 { if read_err != nil || bytes_read == 0 {
return {}, false return {}, .Connection_Closed
} }
request_data := buffer[:bytes_read] request_data := buffer[:bytes_read]
@@ -298,7 +330,7 @@ parse_request :: proc(
// Find end of headers (\r\n\r\n) // Find end of headers (\r\n\r\n)
header_end_idx := strings.index(string(request_data), "\r\n\r\n") header_end_idx := strings.index(string(request_data), "\r\n\r\n")
if header_end_idx < 0 { if header_end_idx < 0 {
return {}, false return {}, .Invalid_Request
} }
header_section := string(request_data[:header_end_idx]) header_section := string(request_data[:header_end_idx])
@@ -307,13 +339,13 @@ parse_request :: proc(
// Parse request line // Parse request line
lines := strings.split_lines(header_section, allocator) lines := strings.split_lines(header_section, allocator)
if len(lines) == 0 { if len(lines) == 0 {
return {}, false return {}, .Invalid_Request
} }
request_line := lines[0] request_line := lines[0]
parts := strings.split(request_line, " ", allocator) parts := strings.split(request_line, " ", allocator)
if len(parts) < 3 { if len(parts) < 3 {
return {}, false return {}, .Invalid_Request
} }
method := method_from_string(parts[0]) method := method_from_string(parts[0])
@@ -339,6 +371,11 @@ parse_request :: proc(
name = strings.clone(name, allocator), name = strings.clone(name, allocator),
value = strings.clone(value, 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 // Read body if Content-Length present
@@ -348,7 +385,12 @@ parse_request :: proc(
if cl, ok := content_length_header.?; ok { if cl, ok := content_length_header.?; ok {
content_length := strconv.parse_int(cl) or_else 0 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 // Check if we already have the body in buffer
existing_body := request_data[body_start:] existing_body := request_data[body_start:]
@@ -370,7 +412,7 @@ parse_request :: proc(
n, err := net.recv_tcp(conn, chunk) n, err := net.recv_tcp(conn, chunk)
if err != nil || n == 0 { if err != nil || n == 0 {
return {}, false return {}, .Connection_Closed
} }
copy(body[body_written:], chunk[:n]) copy(body[body_written:], chunk[:n])
@@ -386,7 +428,7 @@ parse_request :: proc(
path = path, path = path,
headers = headers[:], headers = headers[:],
body = body, body = body,
}, true }, .None
} }
// Helper to get header from slice // Helper to get header from slice