package http
import "core:fmt"
import "core:net"
import "core:os"
import "core:slice"
import "core:strings"
import "core:sync"
import "core:sys/posix"
import "core:thread"
import "core:time"
Method :: enum {
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
}
Status_Code :: enum {
Ok = 200,
SeeOther = 303,
Unauthorized = 401,
BadRequest = 400,
Forbidden = 403,
NotFound = 404,
RequestTimeout = 408,
InternalServerError = 500,
NotImplemented = 501,
ServiceUnavailable = 503,
}
Status_Message :: #sparse[Status_Code]string {
.Ok = "200 OK",
.SeeOther = "303 SeeOther",
.Unauthorized = "401 Unauthorized",
.BadRequest = "400 Bad Request",
.Forbidden = "403 Forbidden",
.NotFound = "404 Not Found",
.RequestTimeout = "408 Request Timeout",
.InternalServerError = "500 Internal Server Error",
.NotImplemented = "501 Not Implemented",
.ServiceUnavailable = "503 Service Unavailable",
}
Request :: struct {
_raw_head: []u8,
host: string,
method: Method,
target: string, // TODO: Implement URI type
version: string,
headers: [dynamic]Header,
content_length: int,
body: []u8,
}
Response :: struct {
code: Status_Code,
headers: [dynamic]Header,
body: []u8,
}
Handler :: proc(req: Request, res: ^Response, user_data: rawptr)
// TODO: stream support when body grows very large?
respond :: proc {
respond_empty,
respond_string,
respond_bytes,
//respond_stream,
}
// NOTE: `res.body` is owned by the response, responding again (e.g. a middleware
// rewriting what its `next` produced) frees whatever body was set before.
respond_empty :: proc(res: ^Response, code: Status_Code) {
respond_owned(res, code, nil)
}
respond_string :: proc(res: ^Response, code: Status_Code, str: string) {
respond_owned(res, code, slice.clone(transmute([]u8)str[:]))
}
respond_bytes :: proc(res: ^Response, code: Status_Code, buf: []u8) {
// Clone before freeing, `buf` may well be the current `res.body`
respond_owned(res, code, slice.clone(buf))
}
// Like `respond_bytes` but takes ownership of `buf` instead of cloning it,
// it has to be allocated with `context.allocator`.
respond_owned :: proc(res: ^Response, code: Status_Code, buf: []u8) {
delete(res.body)
res.code = code
res.body = buf
}
/*respond_stream :: proc(res: ^Response, code: Status_Code, ...) {
}*/
MAX_CONNECTIONS :: 128
serve :: proc(endpoint: net.Endpoint, h: proc(req: Request, res: ^Response, user_data: rawptr), data: rawptr) {
posix.signal(.SIGINT, should_quit_handler)
posix.signal(.SIGTERM, should_quit_handler)
listener, err := net.listen_tcp(endpoint)
if err != nil {
fmt.eprintf("listen_tcp failed: %v", err)
os.exit(1)
}
defer net.close(listener)
net.set_option(listener, .Receive_Timeout, time.Millisecond * 10)
fmt.printf("Glaze listening on %s\n", net.to_string(endpoint))
free_all(context.temp_allocator)
active_connections: int
for !SHOULD_QUIT {
client, _, err := net.accept_tcp(listener)
if err != nil {
if err == .Would_Block || err == .Interrupted {
continue
}
fmt.eprintf("accept_tcp failed: %v\n", err)
os.exit(1)
// continue
}
if active_connections >= MAX_CONNECTIONS {
send_response_error(client, .ServiceUnavailable)
net.close(client)
continue
}
sync.atomic_add(&active_connections, 1)
cj := new(Conn_Job)
cj.conn = {
sock = client,
}
cj.handler = h
cj.user_data = data
cj.active_connections = &active_connections
thread.create_and_start_with_poly_data(cj, handle_conn_job, context, self_cleanup = true)
}
}
@(private = "file")
html_escape_for :: proc(c: u8) -> string {
switch c {
case '&':
return "&"
case '<':
return "<"
case '>':
return ">"
case '"':
return """
case '\'':
return "'"
}
return ""
}
// allocates only if escapeable character found
escape_html :: proc(s: string) -> (string, bool) {
// NOTE: All of them are ASCII, so going over bytes leaves any UTF-8 as it was
needs_escaping: bool
for i in 0 ..< len(s) {
if html_escape_for(s[i]) != "" {
needs_escaping = true
break
}
}
if !needs_escaping {
return s, false
}
b: strings.Builder
strings.builder_init(&b, 0, len(s) + 16)
for i in 0 ..< len(s) {
if escaped := html_escape_for(s[i]); escaped != "" {
strings.write_string(&b, escaped)
} else {
strings.write_byte(&b, s[i])
}
}
return strings.to_string(b), true
}