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), thread_pool: thread.Pool, } @(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, ) { 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, } THREAD_COUNT :: 8 thread.pool_init(&server.thread_pool, allocator, THREAD_COUNT) thread.pool_start(&server.thread_pool) router_init(&server.router, allocator) } server_destroy :: proc(server: ^Server($Error_Type)) { if sync.atomic_load(&server.thread_pool.is_running) { thread.pool_join(&server.thread_pool) } thread.pool_destroy(&server.thread_pool) 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) } RequestThreadData :: struct($Error_Type: typeid) { server: ^Server(Error_Type), client_socket: net.TCP_Socket, } @(private) request_thread_task :: proc(t: thread.Task) { task_data := cast(^RequestThreadData(any))t.data defer free(task_data, t.allocator) serve(task_data.server, task_data.client_socket) net.close(task_data.client_socket) free_all(context.temp_allocator) } @(private) spawn_request_thread :: proc( server: ^Server($Error_Type), client_socket: net.TCP_Socket, ) { task_data := new(RequestThreadData(Error_Type)) task_data^ = RequestThreadData(Error_Type) { server = server, client_socket = client_socket, } thread.pool_add_task( &server.thread_pool, context.allocator, request_thread_task, rawptr(task_data), ) } 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 !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 } spawn_request_thread(server, client_socket) } thread.pool_finish(&server.thread_pool) thread.pool_join(&server.thread_pool) }