first commit

This commit is contained in:
2026-02-15 08:55:22 -05:00
commit 677bbb4028
21 changed files with 7320 additions and 0 deletions

429
http.odin Normal file
View File

@@ -0,0 +1,429 @@
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"
}