506 lines
12 KiB
Odin
506 lines
12 KiB
Odin
package main
|
|
|
|
import "core:fmt"
|
|
import "core:mem"
|
|
import vmem "core:mem/virtual"
|
|
import "core:net"
|
|
import "core:strings"
|
|
import "core:strconv"
|
|
import "core:thread"
|
|
|
|
// 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
|
|
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
|
|
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,
|
|
}
|
|
}
|
|
|
|
// Connection task data - passed to worker threads
|
|
Connection_Task_Data :: struct {
|
|
server: ^Server,
|
|
conn: net.TCP_Socket,
|
|
source: net.Endpoint,
|
|
}
|
|
|
|
// 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 (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 {
|
|
conn, source, accept_err := net.accept_tcp(socket)
|
|
if accept_err != nil {
|
|
if server.running {
|
|
fmt.eprintfln("Accept error: %v", accept_err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Allocate connection data
|
|
conn_data := new(Connection_Task_Data, server.allocator)
|
|
conn_data.server = server
|
|
conn_data.conn = conn
|
|
conn_data.source = source
|
|
|
|
// Spawn a new thread for this connection
|
|
t := thread.create(connection_worker_thread)
|
|
if t != nil {
|
|
t.init_context = context
|
|
t.data = conn_data
|
|
thread.start(t)
|
|
} else {
|
|
// Failed to create thread, close connection
|
|
net.close(conn)
|
|
free(conn_data, server.allocator)
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
server_stop :: proc(server: ^Server) {
|
|
server.running = false
|
|
|
|
// Close listening socket
|
|
if sock, ok := server.socket.?; ok {
|
|
net.close(sock)
|
|
server.socket = nil
|
|
}
|
|
}
|
|
|
|
// Worker thread procedure
|
|
connection_worker_thread :: proc(t: ^thread.Thread) {
|
|
defer thread.destroy(t)
|
|
|
|
conn_data := cast(^Connection_Task_Data)t.data
|
|
defer free(conn_data, conn_data.server.allocator)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
// Set request arena as context allocator for downstream allocations
|
|
old := context.allocator
|
|
context.allocator = request_alloc
|
|
defer context.allocator = old
|
|
|
|
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
|
|
}
|
|
|
|
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, 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 {}, .Connection_Closed
|
|
}
|
|
|
|
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 {}, .Invalid_Request
|
|
}
|
|
|
|
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 {}, .Invalid_Request
|
|
}
|
|
|
|
request_line := lines[0]
|
|
parts := strings.split(request_line, " ", allocator)
|
|
if len(parts) < 3 {
|
|
return {}, .Invalid_Request
|
|
}
|
|
|
|
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),
|
|
})
|
|
|
|
// Check max headers limit
|
|
if len(headers) > config.max_headers {
|
|
return {}, .Invalid_Request
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
// 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:]
|
|
|
|
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 {}, .Connection_Closed
|
|
}
|
|
|
|
copy(body[body_written:], chunk[:n])
|
|
body_written += n
|
|
remaining -= n
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return HTTP_Request{
|
|
method = method,
|
|
path = path,
|
|
headers = headers[:],
|
|
body = body,
|
|
}, .None
|
|
}
|
|
|
|
// 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"
|
|
}
|