🍯 Glaze

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 "&lt;"
	case '>':
		return "&gt;"
	case '"':
		return "&quot;"
	case '\'':
		return "&#39;"
	}
	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
}