package router import "core:fmt" import "core:os" 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, } 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, 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) 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) // 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) { delete(r.mime_map) delete(r.middlewares) delete(r.routes) } add :: proc( r: ^Router, method: http.Method, path: string, h: Route_Handler, kind: Route_Kind = .Exact, user_data: rawptr, ) { append(&r.routes, Route{method = method, kind = kind, path = path, handler = h, user_data = user_data}) } get :: proc(r: ^Router, path: string, h: Route_Handler, kind: Route_Kind = .Exact, user_data: rawptr = nil) { add(r, .Get, path, h, kind, user_data) } post :: proc(r: ^Router, path: string, h: Route_Handler, kind: Route_Kind = .Exact, user_data: rawptr = nil) { add(r, .Post, path, h, kind, user_data) } 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, } static :: proc(r: ^Router, path, filepath: string) { ctx := new(Static_Context) // TODO: cleanup of memory ctx.root, _ = os.get_absolute_path(filepath, context.allocator) ctx.base_path = strings.clone(path) // TODO: cleanup of memory ctx.mime_map = r.mime_map add(r, .Get, path, static_handler, .Prefix, ctx) } 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 } verify_and_get_absolute_filepath :: proc(root, target: string) -> (string, bool) { requested_file, raw_file_path: 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 !strings.has_prefix(requested_file, root) { return "", false } if !os.is_file(requested_file) { return "", false } return requested_file, true } @(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 } defer delete(buf) http.respond(res, .Ok, buf) } @(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) free(ctx) } @(private) build_chain :: proc( final_handler: Route_Handler, final_ctx: rawptr, middlewares: []Middleware, ) -> ( Route_Handler, rawptr, ) { curr_handler := final_handler curr_ctx := final_ctx for m in middlewares { node := new(Chain_Node) node.middleware = m.handler node.middleware_ctx = m.user_data node.next = curr_handler node.next_ctx = curr_ctx curr_handler = chain_node_handler curr_ctx = node } return curr_handler, curr_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 { chain_handler, chain_ctx := build_chain(rt.handler, rt.user_data, r.middlewares[:]) chain_handler(req, res, chain_ctx) 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 { chain_handler, chain_ctx := build_chain(r.routes[best_i].handler, r.routes[best_i].user_data, r.middlewares[:]) chain_handler(req, res, chain_ctx) 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_Kind :: enum { Param, Static, Wildcard, } 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 pattern_parts := strings.split(rt.path[1:], "/") defer delete(pattern_parts) if len(pattern_parts) > len(target_parts) { continue } pattern := make([]Pattern_Elem, len(pattern_parts)) for p, j in pattern_parts { if p[0] == ':' { pattern[j].kind = .Param pattern[j].name = p[1:] } else if p[0] == '*' { if j != len(pattern_parts) - 1 { fmt.eprintf("router: wildcard must be last parameter -> %s\n", rt.path) continue } pattern[j].kind = .Wildcard pattern[j].name = p[1:] } else { pattern[j].kind = .Static pattern[j].name = p } } skip: bool for p, j in pattern { switch p.kind { case .Static: if target_parts[j] != p.name { skip = true break } case .Param: 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: count: int for c, j in req.target[1:] { if c == '/' { count += 1 if count == i { req.params[p.name] = req.target[j + 2:] break } } } } } chain_handler, chain_ctx := build_chain(r.routes[best_i].handler, r.routes[best_i].user_data, r.middlewares[:]) chain_handler(req, res, chain_ctx) delete(best_pattern) return } // TODO: Have a 404 handler http.respond(res, .NotFound, nil) }