package fjord import "base:runtime" import "core:bufio" import "core:bytes" import "core:io" import "core:log" import "core:net" import "core:path/slashpath" import "core:strconv" import "core:strings" import "core:sync" import "core:thread" import "core:unicode" import http "http" Server :: struct($Error_Type: typeid) { endpoint: net.Endpoint, not_found_handler: #type proc( request: ^http.Request, ) -> ( http.Response, Error_Type, ), shutdown: bool, router: Router(Error_Type), work_queue: WorkQueue, worker_count: int, worker_threads: [dynamic]^thread.Thread, } @(private) read_connection :: proc( client_socket: net.TCP_Socket, allocator := context.temp_allocator, ) -> ( data: []byte, err: http.RequestError, ) { TCP_CHUNK_SIZE :: 1024 HTTP_END_SEQUENCE :: []byte{'\r', '\n', '\r', '\n'} buffer: bytes.Buffer bytes.buffer_init_allocator(&buffer, 0, 0, allocator) chunk: [TCP_CHUNK_SIZE]byte for { bytes_read, net_err := net.recv_tcp(client_socket, chunk[:]) if net_err != nil do return data, .NetworkError if bytes_read == 0 do break prev_len := len(buffer.buf) bytes.buffer_write(&buffer, chunk[:bytes_read]) search_start := max(0, prev_len - 3) if bytes.index(buffer.buf[search_start:], HTTP_END_SEQUENCE) != -1 do break } return buffer.buf[:], nil } server_init :: proc( server: ^Server($Error_Type), endpoint: net.Endpoint, allocator := context.allocator, ) { WORKER_COUNT :: 8 QUEUE_CAPACITY :: 252 server^ = Server(Error_Type) { endpoint = endpoint, not_found_handler = proc( request: ^http.Request, ) -> ( http.Response, Error_Type, ) { body := "
Not found
" http_response := http.make_response( .NotFound, transmute([]byte)body, .Html, ) return http_response, nil }, shutdown = false, worker_count = WORKER_COUNT, worker_threads = make([dynamic]^thread.Thread, 0, WORKER_COUNT, allocator), } work_queue_init(&server.work_queue, QUEUE_CAPACITY, allocator) router_init(&server.router, allocator) } server_destroy :: proc(server: ^Server($Error_Type)) { for t in server.worker_threads { thread.destroy(t) } delete(server.worker_threads) work_queue_destroy(&server.work_queue) router_destroy(&server.router) } // This is used instead of default server_make param until compiler bug is fixed: https://github.com/odin-lang/Odin/issues/5792 server_set_not_found_handler :: proc( server: ^Server($Error_Type), not_found_handler: #type proc( request: ^http.Request, ) -> ( http.Response, Error_Type, ), ) { server.not_found_handler = not_found_handler } server_add_route :: proc( server: ^Server($Error_Type), method: http.Method, path: []string, handler: #type proc(request: ^http.Request) -> (http.Response, Error_Type), ) { router_add_route(&server.router, method, path, handler) } server_remove_route :: proc( server: ^Server($Error_Type), method: http.Method, path: []string, ) -> ( ok: bool, ) { return router_remove_route(&server.router, method, path) } server_shutdown :: proc(server: ^Server($Error_Type)) { sync.atomic_store(&server.shutdown, true) } @(private) serve :: proc(server: ^Server($Error_Type), client_socket: net.TCP_Socket) { data, read_err := read_connection(client_socket, context.temp_allocator) if read_err != nil { log.warnf("Failed to read request, reason: %s", read_err) return } http_request, unmarshall_err := http.unmarshall_request( data, context.temp_allocator, ) if unmarshall_err != nil { log.warnf("Failed to unmarshall request, reason: %s", unmarshall_err) return } handler: #type proc(request: ^http.Request) -> (http.Response, Error_Type) ok: bool // todo: should sanitize better path := strings.split(http_request.path, "/", context.temp_allocator)[1:] handler, http_request.path_variables, ok = router_lookup( &server.router, http_request.method, path, context.temp_allocator, ) if !ok do handler = server.not_found_handler http_response, handler_err := handler(&http_request) if handler_err != nil { log.warnf("Handler failed with error: %s", handler_err) return } marshalled_response := http.marshall_response( &http_response, context.temp_allocator, ) net.send_tcp(client_socket, marshalled_response) } @(private) worker_thread :: proc(server: ^Server($Error_Type)) { for { socket, ok := work_queue_dequeue(&server.work_queue) if !ok do break serve(server, socket) net.close(socket) free_all(context.temp_allocator) } } server_listen_and_serve :: proc(server: ^Server($Error_Type)) { server_socket, net_err := net.listen_tcp(server.endpoint) log.assert(net_err == nil, "Couldn't create TCP socket") defer net.close(server_socket) for i in 0 ..< server.worker_count { t := thread.create(proc(t: ^thread.Thread) { server := cast(^Server(Error_Type))t.data worker_thread(server) }) log.assertf(t != nil, "Thread creation failed") t.data = rawptr(server) thread.start(t) append(&server.worker_threads, t) } for !sync.atomic_load(&server.shutdown) { client_socket, _, net_err := net.accept_tcp(server_socket) if net_err != nil { log.warnf("Failed to accept TCP connection, reason: %s", net_err) continue } ok := work_queue_enqueue(&server.work_queue, client_socket) if !ok { log.warn("Work queue full, rejecting connection") net.close(client_socket) } } work_queue_shutdown(&server.work_queue) for t in server.worker_threads { thread.join(t) } }