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 }