package router
import "core:fmt"
import "core:os"
import "core:slice"
import "core:strings"
import http "../"
Route_Kind :: enum {
Exact,
Prefix,
Pattern,
}
Route :: struct {
method: http.Method,
kind: Route_Kind,
path: string,
handler: Route_Handler,
user_data: rawptr,
middlewares: []Middleware, // owned by the router, runs after the global middlewares
}
Middleware :: struct {
handler: Middleware_Handler,
user_data: rawptr,
}
Chain_Node :: struct {
middleware: Middleware_Handler,
middleware_ctx: rawptr,
next: Route_Handler,
next_ctx: rawptr,
}
Router :: struct {
routes: [dynamic]Route,
middlewares: [dynamic]Middleware,
statics: [dynamic]^Static_Context,
mime_map: map[string]string,
}
Request :: struct {
using _http_req: http.Request,
rest_path: string,
params: map[string]string,
}
Response :: http.Response
Route_Handler :: proc(req: Request, res: ^Response, ctx: rawptr)
// A pattern is a path of segments: `name` matches itself, `:name` matches any one
// non-empty segment and a trailing `*name` matches whatever is left, which can be
// nothing at all (`/files/*path` matches `/files` and `/files/` as well as `/files/a/b`)
@(private)
Pattern_Kind :: enum {
Static,
Param,
Wildcard,
}
@(private)
segment_kind :: proc(segment: string) -> Pattern_Kind {
if len(segment) > 0 {
switch segment[0] {
case ':':
return .Param
case '*':
return .Wildcard
}
}
return .Static
}
// Empty when the pattern is fine, otherwise what is wrong with it
@(private)
pattern_error :: proc(path: string) -> string {
if len(path) == 0 || path[0] != '/' {
return "pattern must start with a slash"
}
segments := strings.split(path[1:], "/")
defer delete(segments)
for segment, i in segments {
kind := segment_kind(segment)
if kind != .Static && len(segment) == 1 {
return "params and wildcards need a name"
}
if kind == .Wildcard && i != len(segments) - 1 {
return "wildcard must be last"
}
}
return ""
}
Middleware_Handler :: proc(req: Request, res: ^Response, ctx: rawptr, next: Route_Handler, next_ctx: rawptr)
create :: proc() -> Router {
r: Router
r.routes = make([dynamic]Route, 0, 8)
r.middlewares = make([dynamic]Middleware, 0, 8)
r.statics = make([dynamic]^Static_Context, 0, 2)
// TODO: break out into dedicated file to enable clean imports
// Might aswell expose mime handling
r.mime_map = make(map[string]string)
r.mime_map[".html"] = "text/html; charset=utf-8"
r.mime_map[".css"] = "text/css; charset=utf-8"
r.mime_map[".js"] = "application/javascript; charset=utf-8"
r.mime_map[".json"] = "application/json; charset=utf-8"
r.mime_map[".svg"] = "image/svg+xml"
r.mime_map[".png"] = "image/png"
r.mime_map[".jpg"] = "image/jpeg"
r.mime_map[".jpeg"] = "image/jpeg"
r.mime_map[".webp"] = "image/webp"
r.mime_map[".gif"] = "image/gif"
r.mime_map[".ico"] = "image/x-icon"
r.mime_map[".txt"] = "text/plain; charset=utf-8"
return r
}
destroy :: proc(r: ^Router) {
for s in r.statics {
delete(s.root)
delete(s.base_path)
free(s)
}
delete(r.statics)
for rt in r.routes {
delete(rt.middlewares)
}
delete(r.mime_map)
delete(r.middlewares)
delete(r.routes)
}
// Returns false, and doesn't add the route, if `path` is not a valid pattern
add :: proc(
r: ^Router,
method: http.Method,
path: string,
h: Route_Handler,
kind: Route_Kind = .Exact,
user_data: rawptr,
middlewares: []Middleware = nil,
) -> bool {
if kind == .Pattern {
if msg := pattern_error(path); msg != "" {
fmt.eprintf("router: %s -> %s\n", msg, path)
return false
}
}
rt := Route {
method = method,
kind = kind,
path = path,
handler = h,
user_data = user_data,
}
if len(middlewares) > 0 {
rt.middlewares = slice.clone(middlewares)
}
append(&r.routes, rt)
return true
}
get :: proc(
r: ^Router,
path: string,
h: Route_Handler,
kind: Route_Kind = .Exact,
user_data: rawptr = nil,
middlewares: []Middleware = nil,
) -> bool {
return add(r, .Get, path, h, kind, user_data, middlewares)
}
post :: proc(
r: ^Router,
path: string,
h: Route_Handler,
kind: Route_Kind = .Exact,
user_data: rawptr = nil,
middlewares: []Middleware = nil,
) -> bool {
return add(r, .Post, path, h, kind, user_data, middlewares)
}
// Global middleware, runs for every route regardless of when the route was added.
// Middlewares run in the order they are added, first added is outermost:
// global (in `use` order) -> route (in `middlewares` order) -> handler
use :: proc(r: ^Router, h: Middleware_Handler, user_data: rawptr = nil) {
append(&r.middlewares, Middleware{handler = h, user_data = user_data})
}
@(private)
Static_Context :: struct {
root: string,
base_path: string,
mime_map: ^map[string]string, // the router's map, so later additions are seen
}
// Returns false, and serves nothing, if `filepath` isn't a directory that can be resolved
// NOTE: `r` must not be moved after this, the route keeps a pointer into it
static :: proc(r: ^Router, path, filepath: string, middlewares: []Middleware = nil) -> bool {
root, err := os.get_absolute_path(filepath, context.allocator)
if err != nil || !os.is_dir(root) {
// Files are checked against the root, an empty one would let requests reach anywhere
fmt.eprintf("router: static root isn't a directory -> %s\n", filepath)
delete(root)
return false
}
ctx := new(Static_Context)
ctx.root = root
ctx.base_path = strings.clone(path)
ctx.mime_map = &r.mime_map
append(&r.statics, ctx) // freed by `destroy`
return add(r, .Get, path, static_handler, .Prefix, ctx, middlewares)
}
handle :: proc(r: ^Router) -> (http.Handler, rawptr) {
return proc(req: http.Request, res: ^http.Response, ctx: rawptr) {
router_handler(Request{_http_req = req}, res, ctx)
}, r
}
// Returns a path the caller has to free, which is a file below `root`
verify_and_get_absolute_filepath :: proc(root, target: string) -> (string, bool) {
// Would cut the path short once it reaches the OS
if strings.contains_rune(target, 0) {
return "", false
}
raw_file_path, requested_file: string
err: os.Error
raw_file_path, err = os.join_path({root, target}, context.allocator)
if err != nil {
return "", false
}
defer delete(raw_file_path)
requested_file, err = os.get_absolute_path(raw_file_path, context.allocator)
if err != nil {
return "", false
}
if !is_within(root, requested_file) || !os.is_file(requested_file) {
delete(requested_file)
return "", false
}
return requested_file, true
}
// Below `root` as a path, sharing its leading characters isn't enough (/srv/www vs /srv/www-secret)
@(private)
is_within :: proc(root, path: string) -> bool {
if !strings.has_prefix(path, root) || len(path) == len(root) {
return false
}
return strings.has_suffix(root, "/") || path[len(root)] == '/'
}
@(private)
static_handler :: proc(req: Request, res: ^Response, ctx: rawptr) {
static_context := (^Static_Context)(ctx)
if len(req.target) <= len(static_context.base_path) + 1 {
http.respond(res, .NotFound, nil)
return
}
target := string(req.target[len(static_context.base_path) + 1:])
requested_file, ok := verify_and_get_absolute_filepath(static_context.root, target)
if !ok {
http.respond(res, .NotFound, nil)
return
}
defer delete(requested_file)
ext := os.ext(requested_file)
mime: string
mime, ok = static_context.mime_map[ext]
if !ok {
mime = "application/octet-stream"
}
http.headers_add(&res.headers, "content-type", mime)
http.headers_add(&res.headers, "cache-control", "public, max-age=3600")
// TODO: don't read complete file into memory, stream/chunk it
buf, err := os.read_entire_file_from_path(requested_file, context.allocator)
if err != nil {
fmt.eprintf("couldn't read file: %v\n", err)
http.respond(res, .InternalServerError, nil)
return
}
http.respond_owned(res, .Ok, buf)
}
// A chain is one `Chain_Node` per middleware, linked outermost first and ending in the
// route handler. The nodes live in storage owned by `dispatch` and are never freed one
// by one, so a middleware that returns without calling `next` can't leak the rest.
@(private)
chain_node_handler :: proc(req: Request, res: ^Response, ctx: rawptr) {
n := (^Chain_Node)(ctx)
n.middleware(req, res, n.middleware_ctx, n.next, n.next_ctx)
}
// Fills `nodes` (one per middleware, global ones first) and returns the chain's entry point
@(private)
build_chain :: proc(nodes: []Chain_Node, rt: Route, global: []Middleware) -> (Route_Handler, rawptr) {
assert(len(nodes) == len(global) + len(rt.middlewares))
curr_handler := rt.handler
curr_ctx := rt.user_data
i := len(nodes)
// Innermost first, so every node can point at the one below it
for layer in ([2][]Middleware{rt.middlewares, global}) {
#reverse for m in layer {
i -= 1
nodes[i] = Chain_Node {
middleware = m.handler,
middleware_ctx = m.user_data,
next = curr_handler,
next_ctx = curr_ctx,
}
curr_handler = chain_node_handler
curr_ctx = &nodes[i]
}
}
return curr_handler, curr_ctx
}
@(private)
dispatch :: proc(r: ^Router, rt: Route, req: Request, res: ^Response) {
count := len(r.middlewares) + len(rt.middlewares)
// Typical chains fit on the stack, only unusually long ones touch the heap
stack: [8]Chain_Node
nodes := stack[:count] if count <= len(stack) else make([]Chain_Node, count)
defer if count > len(stack) {
delete(nodes)
}
handler, ctx := build_chain(nodes, rt, r.middlewares[:])
handler(req, res, ctx)
}
@(private)
router_handler :: proc(req: Request, res: ^Response, user_data: rawptr) {
r := (^Router)(user_data)
req := req
req.params = make(map[string]string)
defer delete(req.params)
for rt in r.routes {
if rt.kind != .Exact {
continue
}
if rt.method == req.method && rt.path == req.target {
dispatch(r, rt, req, res)
return
}
}
best_i := -1
best_len := -1
for rt, i in r.routes {
if rt.kind != .Prefix {
continue
}
if rt.method != req.method {
// TODO: Support 405 in the future
continue
}
if strings.has_prefix(req.target, rt.path) {
path_len := len(rt.path)
if path_len > best_len {
best_len = path_len
best_i = i
req.rest_path = req.target[len(rt.path):]
}
}
}
if best_i >= 0 {
dispatch(r, r.routes[best_i], req, res)
return
}
// Patterns match whole segments, so the target has to be a path
if len(req.target) == 0 || req.target[0] != '/' {
http.respond(res, .NotFound, nil)
return
}
// TODO: rewrite logic to parse over windows of values instead of splitting
target_parts := strings.split(req.target[1:], "/")
defer delete(target_parts)
Pattern_Elem :: struct {
kind: Pattern_Kind,
name: string,
value: string,
}
best_pattern: []Pattern_Elem
for rt, i in r.routes {
if rt.kind != .Pattern {
continue
}
if rt.method != req.method {
// TODO: Support 405 in the future
continue
}
// TODO: Do all this work at route setup
// NOTE: Skip initial slash, `add` made sure there is one
pattern_parts := strings.split(rt.path[1:], "/")
defer delete(pattern_parts)
// Only a trailing wildcard can match a different number of segments than the pattern has
if segment_kind(pattern_parts[len(pattern_parts) - 1]) == .Wildcard {
if len(target_parts) < len(pattern_parts) - 1 {
continue
}
} else if len(target_parts) != len(pattern_parts) {
continue
}
pattern := make([]Pattern_Elem, len(pattern_parts))
for p, j in pattern_parts {
pattern[j].kind = segment_kind(p)
pattern[j].name = p if pattern[j].kind == .Static else p[1:]
}
skip: bool
for p, j in pattern {
switch p.kind {
case .Static:
skip ||= target_parts[j] != p.name
case .Param:
// A param has to match something
skip ||= target_parts[j] == ""
case .Wildcard:
}
}
if skip {
delete(pattern)
continue
}
if len(pattern) > len(best_pattern) {
best_i = i
if best_pattern != nil {
delete(best_pattern)
}
best_pattern = pattern
} else {
delete(pattern)
}
}
if best_i >= 0 {
for p, i in best_pattern {
#partial switch p.kind {
case .Param:
req.params[p.name] = target_parts[i]
case .Wildcard:
// Everything from this segment on, if the target even gets that far. The parts
// are slices of the target, so their lengths (and the slashes between them)
// say where it starts.
offset := 1
for part in target_parts[:min(i, len(target_parts))] {
offset += len(part) + 1
}
req.params[p.name] = req.target[min(offset, len(req.target)):]
}
}
dispatch(r, r.routes[best_i], req, res)
delete(best_pattern)
return
}
// TODO: Have a 404 handler
http.respond(res, .NotFound, nil)
}