first commit
This commit is contained in:
429
http.odin
Normal file
429
http.odin
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user