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) }