package fjord
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:unicode"
import http "http"
Server :: struct($Error_Type: typeid) {
endpoint: net.Endpoint,
not_found_handler: #type proc(
request: ^http.Request,
) -> (
http.Response,
Error_Type,
),
router: Router(Error_Type),
shutdown: bool,
}
@(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,
}
router_init(&server.router, allocator)
}
server_destroy :: proc(server: ^Server($Error_Type)) {
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)
}
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, source, net_err := net.accept_tcp(server_socket)
if net_err != nil {
log.warnf("Failed to accept TCP connection, reason: %s", net_err)
continue
}
defer net.close(client_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)
continue
}
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,
)
continue
}
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)
continue
}
marshalled_response := http.marshall_response(
&http_response,
context.temp_allocator,
)
net.send_tcp(client_socket, marshalled_response)
free_all(context.temp_allocator)
}
}