package http import "base:runtime" import "core:log" import "core:net" import "core:strconv" import "core:strings" import "core:text/match" URL :: struct { raw: string, // All other fields are views/slices into this string. scheme: string, host: string, path: string, query: string, } url_parse :: proc(raw: string) -> (url: URL) { url.raw = raw s := raw i := strings.index(s, "://") if i >= 0 { url.scheme = s[:i] s = s[i+3:] } i = strings.index(s, "?") if i != -1 { url.query = s[i+1:] s = s[:i] } i = strings.index(s, "/") if i == -1 { url.host = s } else { url.host = s[:i] url.path = s[i:] } return } Query_Entry :: struct { key, value: string, } query_iter :: proc(query: ^string) -> (entry: Query_Entry, ok: bool) { if len(query) == 0 do return ok = true i := strings.index(query^, "=") if i < 0 { entry.key = query^ query^ = "" return } entry.key = query[:i] query^ = query[i+1:] i = strings.index(query^, "&") if i < 0 { entry.value = query^ query^ = "" return } entry.value = query[:i] query^ = query[i+1:] return } query_get :: proc(url: URL, key: string) -> (val: string, ok: bool) #optional_ok { q := url.query for entry in #force_inline query_iter(&q) { if entry.key == key { return entry.value, true } } return } query_get_percent_decoded :: proc(url: URL, key: string, allocator := context.temp_allocator) -> (val: string, ok: bool) { str := query_get(url, key) or_return return net.percent_decode(str, allocator) } query_get_bool :: proc(url: URL, key: string) -> (result, set: bool) #optional_ok { str := query_get(url, key) or_return set = true switch str { case "", "false", "0", "no": case: result = true } return } query_get_int :: proc(url: URL, key: string, base := 0) -> (result: int, ok: bool, set: bool) { str := query_get(url, key) or_return set = true result, ok = strconv.parse_int(str, base) return } query_get_uint :: proc(url: URL, key: string, base := 0) -> (result: uint, ok: bool, set: bool) { str := query_get(url, key) or_return set = true result, ok = strconv.parse_uint(str, base) return } Route :: struct { handler: Handler, pattern: string, } Router :: struct { allocator: runtime.Allocator, routes: map[Method][dynamic]Route, all: [dynamic]Route, } router_init :: proc(router: ^Router, allocator := context.allocator) { router.allocator = allocator router.routes = make(map[Method][dynamic]Route, len(Method), allocator) } router_destroy :: proc(router: ^Router) { context.allocator = router.allocator for route in router.all { delete(route.pattern) } delete(router.all) for _, routes in router.routes { for route in routes { delete(route.pattern) } delete(routes) } delete(router.routes) } // Returns a handler that matches against the given routes. router_handler :: proc(router: ^Router) -> Handler { h: Handler h.user_data = router h.handle = proc(handler: ^Handler, req: ^Request, res: ^Response) { router := (^Router)(handler.user_data) rline := req.line.(Requestline) if routes_try(router.routes[rline.method], req, res) { return } if routes_try(router.all, req, res) { return } log.infof("no route matched %s %s", method_string(rline.method), rline.target) res.status = .Not_Found respond(res) } return h } route_get :: proc(router: ^Router, pattern: string, handler: Handler) { route_add( router, .Get, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } route_post :: proc(router: ^Router, pattern: string, handler: Handler) { route_add( router, .Post, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } // NOTE: this does not get called when `Server_Opts.redirect_head_to_get` is set to true. route_head :: proc(router: ^Router, pattern: string, handler: Handler) { route_add( router, .Head, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } route_put :: proc(router: ^Router, pattern: string, handler: Handler) { route_add( router, .Put, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } route_patch :: proc(router: ^Router, pattern: string, handler: Handler) { route_add( router, .Patch, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } route_trace :: proc(router: ^Router, pattern: string, handler: Handler) { route_add( router, .Trace, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } route_delete :: proc(router: ^Router, pattern: string, handler: Handler) { route_add( router, .Delete, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } route_connect :: proc(router: ^Router, pattern: string, handler: Handler) { route_add( router, .Connect, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } route_options :: proc(router: ^Router, pattern: string, handler: Handler) { route_add( router, .Options, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } // Adds a catch-all fallback route (all methods, ran if no other routes match). route_all :: proc(router: ^Router, pattern: string, handler: Handler) { if router.all == nil { router.all = make([dynamic]Route, 0, 1, router.allocator) } append( &router.all, Route{handler = handler, pattern = strings.concatenate([]string{"^", pattern, "$"}, router.allocator)}, ) } @(private) route_add :: proc(router: ^Router, method: Method, route: Route) { if method not_in router.routes { router.routes[method] = make([dynamic]Route, router.allocator) } append(&router.routes[method], route) } @(private) routes_try :: proc(routes: [dynamic]Route, req: ^Request, res: ^Response) -> bool { try_captures: [match.MAX_CAPTURES]match.Match = --- for route in routes { n, err := match.find_aux(req.url.path, route.pattern, 0, true, &try_captures) if err != .OK { log.errorf("Error matching route: %v", err) continue } if n > 0 { captures := make([]string, n - 1, context.temp_allocator) for cap, i in try_captures[1:n] { captures[i] = req.url.path[cap.byte_start:cap.byte_end] } req.url_params = captures rh := route.handler rh.handle(&rh, req, res) return true } } return false }