package main import "core:fmt" import "core:mem" import vmem "core:mem/virtual" import "core:net" import "core:strings" import "core:strconv" // HTTP Method enumeration HTTP_Method :: enum { GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH, } method_from_string :: proc(s: string) -> HTTP_Method { switch s { case "GET": return .GET case "POST": return .POST case "PUT": return .PUT case "DELETE": return .DELETE case "OPTIONS": return .OPTIONS case "HEAD": return .HEAD case "PATCH": return .PATCH } return .GET } // HTTP Status codes HTTP_Status :: enum u16 { OK = 200, Created = 201, No_Content = 204, Bad_Request = 400, Unauthorized = 401, Forbidden = 403, Not_Found = 404, Method_Not_Allowed = 405, Conflict = 409, Payload_Too_Large = 413, Internal_Server_Error = 500, Service_Unavailable = 503, } // HTTP Header HTTP_Header :: struct { name: string, value: string, } // HTTP Request HTTP_Request :: struct { method: HTTP_Method, path: string, headers: []HTTP_Header, body: []byte, } // Get header value by name (case-insensitive) request_get_header :: proc(req: ^HTTP_Request, name: string) -> Maybe(string) { for header in req.headers { if strings.equal_fold(header.name, name) { return header.value } } return nil } // HTTP Response HTTP_Response :: struct { status: HTTP_Status, headers: [dynamic]HTTP_Header, body: [dynamic]byte, } response_init :: proc(allocator: mem.Allocator) -> HTTP_Response { return HTTP_Response{ status = .OK, headers = make([dynamic]HTTP_Header, allocator), body = make([dynamic]byte, allocator), } } response_set_status :: proc(resp: ^HTTP_Response, status: HTTP_Status) { resp.status = status } response_add_header :: proc(resp: ^HTTP_Response, name: string, value: string) { append(&resp.headers, HTTP_Header{name = name, value = value}) } response_set_body :: proc(resp: ^HTTP_Response, data: []byte) { clear(&resp.body) append(&resp.body, ..data) } // Request handler function type // Takes context pointer, request, and request-scoped allocator Request_Handler :: #type proc(ctx: rawptr, request: ^HTTP_Request, request_alloc: mem.Allocator) -> HTTP_Response // Server configuration Server_Config :: struct { max_body_size: int, // default 100MB max_headers: int, // default 100 read_buffer_size: int, // default 8KB enable_keep_alive: bool, // default true max_requests_per_connection: int, // default 1000 } default_server_config :: proc() -> Server_Config { return Server_Config{ max_body_size = 100 * 1024 * 1024, max_headers = 100, read_buffer_size = 8 * 1024, enable_keep_alive = true, max_requests_per_connection = 1000, } } // Server Server :: struct { allocator: mem.Allocator, endpoint: net.Endpoint, handler: Request_Handler, handler_ctx: rawptr, config: Server_Config, running: bool, socket: Maybe(net.TCP_Socket), } server_init :: proc( allocator: mem.Allocator, host: string, port: int, handler: Request_Handler, handler_ctx: rawptr, config: Server_Config, ) -> (Server, bool) { endpoint, endpoint_ok := net.parse_endpoint(fmt.tprintf("%s:%d", host, port)) if !endpoint_ok { return {}, false } return Server{ allocator = allocator, endpoint = endpoint, handler = handler, handler_ctx = handler_ctx, config = config, running = false, socket = nil, }, true } server_start :: proc(server: ^Server) -> bool { // Create listening socket socket, socket_err := net.listen_tcp(server.endpoint) if socket_err != nil { fmt.eprintfln("Failed to create listening socket: %v", socket_err) return false } server.socket = socket server.running = true fmt.printfln("HTTP server listening on %v", server.endpoint) // Accept loop for server.running { conn, source, accept_err := net.accept_tcp(socket) if accept_err != nil { if server.running { fmt.eprintfln("Accept error: %v", accept_err) } continue } // Handle connection in separate goroutine would go here // For now, handle synchronously (should spawn thread) handle_connection(server, conn, source) } return true } server_stop :: proc(server: ^Server) { server.running = false if sock, ok := server.socket.?; ok { net.close(sock) server.socket = nil } } // Handle a single connection handle_connection :: proc(server: ^Server, conn: net.TCP_Socket, source: net.Endpoint) { defer net.close(conn) request_count := 0 for request_count < server.config.max_requests_per_connection { request_count += 1 // Growing arena for this request arena: vmem.Arena arena_err := vmem.arena_init_growing(&arena) if arena_err != .None { break } defer vmem.arena_destroy(&arena) request_alloc := vmem.arena_allocator(&arena) // TODO: Double check if we want *all* downstream allocations to use the request arena? old := context.allocator context.allocator = request_alloc defer context.allocator = old request, parse_ok := parse_request(conn, request_alloc, server.config) if !parse_ok { break } response := server.handler(server.handler_ctx, &request, request_alloc) send_ok := send_response(conn, &response, request_alloc) if !send_ok { break } // Check keep-alive keep_alive := request_get_header(&request, "Connection") if ka, ok := keep_alive.?; ok { if !strings.equal_fold(ka, "keep-alive") { break } } else if !server.config.enable_keep_alive { break } // Arena is automatically freed here } } // Parse HTTP request parse_request :: proc( conn: net.TCP_Socket, allocator: mem.Allocator, config: Server_Config, ) -> (HTTP_Request, bool) { // 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 } request_data := buffer[:bytes_read] // 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 } header_section := string(request_data[:header_end_idx]) body_start := header_end_idx + 4 // Parse request line lines := strings.split_lines(header_section, allocator) if len(lines) == 0 { return {}, false } request_line := lines[0] parts := strings.split(request_line, " ", allocator) if len(parts) < 3 { return {}, false } method := method_from_string(parts[0]) path := strings.clone(parts[1], allocator) // Parse headers headers := make([dynamic]HTTP_Header, allocator) for i := 1; i < len(lines); i += 1 { line := lines[i] if len(line) == 0 { continue } colon_idx := strings.index(line, ":") if colon_idx < 0 { continue } name := strings.trim_space(line[:colon_idx]) value := strings.trim_space(line[colon_idx+1:]) append(&headers, HTTP_Header{ name = strings.clone(name, allocator), value = strings.clone(value, allocator), }) } // Read body if Content-Length present body: []byte content_length_header := request_get_header_from_slice(headers[:], "Content-Length") 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 we already have the body in buffer existing_body := request_data[body_start:] if len(existing_body) >= content_length { // Body already in buffer body = make([]byte, content_length, allocator) copy(body, existing_body[:content_length]) } else { // Need to read more body = make([]byte, content_length, allocator) copy(body, existing_body) remaining := content_length - len(existing_body) body_written := len(existing_body) for remaining > 0 { chunk_size := min(remaining, config.read_buffer_size) chunk := make([]byte, chunk_size, allocator) n, err := net.recv_tcp(conn, chunk) if err != nil || n == 0 { return {}, false } copy(body[body_written:], chunk[:n]) body_written += n remaining -= n } } } } return HTTP_Request{ method = method, path = path, headers = headers[:], body = body, }, true } // Helper to get header from slice request_get_header_from_slice :: proc(headers: []HTTP_Header, name: string) -> Maybe(string) { for header in headers { if strings.equal_fold(header.name, name) { return header.value } } return nil } // Send HTTP response send_response :: proc(conn: net.TCP_Socket, resp: ^HTTP_Response, allocator: mem.Allocator) -> bool { // Build response string builder := strings.builder_make(allocator) defer strings.builder_destroy(&builder) // Status line strings.write_string(&builder, "HTTP/1.1 ") strings.write_int(&builder, int(resp.status)) strings.write_string(&builder, " ") strings.write_string(&builder, status_text(resp.status)) strings.write_string(&builder, "\r\n") // Headers response_add_header(resp, "Content-Length", fmt.tprintf("%d", len(resp.body))) for header in resp.headers { strings.write_string(&builder, header.name) strings.write_string(&builder, ": ") strings.write_string(&builder, header.value) strings.write_string(&builder, "\r\n") } // End of headers strings.write_string(&builder, "\r\n") // Send headers header_bytes := transmute([]byte)strings.to_string(builder) _, send_err := net.send_tcp(conn, header_bytes) if send_err != nil { return false } // Send body if len(resp.body) > 0 { _, send_err = net.send_tcp(conn, resp.body[:]) if send_err != nil { return false } } return true } // Get status text for status code status_text :: proc(status: HTTP_Status) -> string { switch status { case .OK: return "OK" case .Created: return "Created" case .No_Content: return "No Content" case .Bad_Request: return "Bad Request" case .Unauthorized: return "Unauthorized" case .Forbidden: return "Forbidden" case .Not_Found: return "Not Found" case .Method_Not_Allowed: return "Method Not Allowed" case .Conflict: return "Conflict" case .Payload_Too_Large: return "Payload Too Large" case .Internal_Server_Error: return "Internal Server Error" case .Service_Unavailable: return "Service Unavailable" } return "Unknown" }