Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_http_router.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
29
30#include <stdio.h>
31#include <stdlib.h>
32#include <string.h>
33#include <unistd.h>
34
35#include "nilorea/n_common.h"
37#include "nilorea/n_log.h"
38
40static int log_level = LOG_ERR;
42static int failures = 0;
43
47static void expect_true(const char* what, int ok) {
48 if (!ok) {
49 n_log(LOG_ERR, "FAIL: %s", what);
50 failures++;
51 } else {
52 n_log(LOG_INFO, "ok: %s", what);
53 }
54}
55
60static int process_args(int argc, char** argv) {
61 int opt = 0;
62
63 while ((opt = getopt(argc, argv, "V:")) != -1) {
64 if (opt == 'V') {
65 log_level = atoi(optarg);
66 } else {
67 return -1;
68 }
69 }
70 return 0;
71}
72
77static int h(void* ctx, const N_ROUTE_MATCH* m) {
78 (void)ctx;
79 (void)m;
80 return 0;
81}
82
85static const N_ROUTE PUBLIC[] = {
86 {N_ROUTE_GET, "/{slug:slug}/", h, "page"},
87 {N_ROUTE_GET, "/", h, "home"},
88 {N_ROUTE_GET, "/page/{n:int6}/", h, "home-paged"},
89 {N_ROUTE_GET, "/category/{slug:slug}/", h, "category"},
90 {N_ROUTE_GET, "/category/{slug:slug}/page/{n:int6}/", h, "category-paged"},
91 {N_ROUTE_GET, "/{y:d4}/{m:d2}/", h, "month"},
92 {N_ROUTE_GET, "/{y:d4}/{m:d2}/{d:d2}/{slug:slug}/", h, "post"},
93 {N_ROUTE_GET, "/feed/", h, "feed"},
94 {N_ROUTE_GET, "/sitemap.xml", h, "sitemap"},
95 {N_ROUTE_GET, "/media/{id:hex16}/{name:seg}", h, "media"},
96 {N_ROUTE_GET, "/media/{id:hex16}/{w:int4}/{name:seg}", h, "variant"},
97 {N_ROUTE_GET, "/wp-content/uploads/{path:rest}", h, "legacy-upload"},
98};
99
101static const N_ROUTE ADMIN[] = {
102 {N_ROUTE_GET, "/admin/login", h, "login-form"},
103 {N_ROUTE_POST, "/admin/login", h, "login"},
104 {N_ROUTE_GET | N_ROUTE_POST, "/admin/posts/{id:int9}", h, "post-edit"},
105 {N_ROUTE_POST, "/admin/posts/{id:int9}/trash", h, "post-trash"},
106};
107
114static void expect_route(const N_ROUTER* r, n_http_req_method method, const char* path, const char* want, N_ROUTE_MATCH* m) {
115 N_ROUTER_RESULT res = n_router_match(r, method, path, strlen(path), m, NULL);
116 int ok = want ? (res == N_ROUTER_FOUND && strcmp((const char*)m->route->data, want) == 0) : res == N_ROUTER_NOT_FOUND;
117 if (!ok) {
118 n_log(LOG_ERR, "FAIL: %s -> result %d, route %s, wanted %s", path, (int)res,
119 res == N_ROUTER_FOUND ? (const char*)m->route->data : "-", want ? want : "not found");
120 failures++;
121 }
122}
123
129static int param_is(const N_ROUTE_MATCH* m, const char* name, const char* want) {
130 size_t len = 0;
131 const char* v = n_route_param(m, name, &len);
132 return v && len == strlen(want) && strcmp(v, want) == 0;
133}
134
136static void test_match(const N_ROUTER* r) {
137 static N_ROUTE_MATCH m;
138 unsigned allowed = 0;
139 char allow[32];
140
141 expect_route(r, N_HTTP_REQ_GET, "/", "home", &m);
142 expect_route(r, N_HTTP_REQ_GET, "/about/", "page", &m);
143 expect_true("slug captured", param_is(&m, "slug", "about"));
144 expect_route(r, N_HTTP_REQ_GET, "/feed/", "feed", &m);
145 expect_route(r, N_HTTP_REQ_GET, "/page/3/", "home-paged", &m);
146 expect_true("int captured", param_is(&m, "n", "3"));
147 expect_route(r, N_HTTP_REQ_GET, "/page/1234567/", NULL, &m);
148 expect_route(r, N_HTTP_REQ_GET, "/category/c_code/", "category", &m);
149 expect_route(r, N_HTTP_REQ_GET, "/category/c_code/page/2/", "category-paged", &m);
150 expect_route(r, N_HTTP_REQ_GET, "/2026/10/", "month", &m);
151 expect_route(r, N_HTTP_REQ_GET, "/2026/10/23/clock_gettime/", "post", &m);
152 expect_true("post placeholders", param_is(&m, "y", "2026") && param_is(&m, "m", "10") && param_is(&m, "d", "23") &&
153 param_is(&m, "slug", "clock_gettime") && m.nb_params == 4);
154 expect_route(r, N_HTTP_REQ_GET, "/2026/1/23/x/", NULL, &m);
155 expect_route(r, N_HTTP_REQ_GET, "/2026/10/23/x", NULL, &m);
156 expect_route(r, N_HTTP_REQ_GET, "/feed", NULL, &m);
157 expect_route(r, N_HTTP_REQ_GET, "/sitemap.xml", "sitemap", &m);
158 expect_route(r, N_HTTP_REQ_GET, "/sitemap.xml/", NULL, &m);
159 expect_route(r, N_HTTP_REQ_GET, "/About/", NULL, &m);
160 expect_route(r, N_HTTP_REQ_GET, "/ab%63/", NULL, &m);
161 expect_route(r, N_HTTP_REQ_GET, "/media/0123456789abcdef/photo%20one.jpg", "media", &m);
162 expect_true("seg decoded", param_is(&m, "name", "photo one.jpg"));
163 expect_route(r, N_HTTP_REQ_GET, "/media/0123456789abcdef/960/photo.webp", "variant", &m);
164 expect_route(r, N_HTTP_REQ_GET, "/media/0123456789ABCDEF/photo.jpg", NULL, &m);
165 expect_route(r, N_HTTP_REQ_GET, "/media/0123456789abcdef/a%2Fb", "media", &m);
166 expect_true("%2F decodes inside a seg value", param_is(&m, "name", "a/b"));
167 expect_route(r, N_HTTP_REQ_GET, "/wp-content/uploads/2019/05/img-300x200.jpg", "legacy-upload", &m);
168 expect_true("rest takes the remainder", param_is(&m, "path", "2019/05/img-300x200.jpg"));
169 expect_route(r, N_HTTP_REQ_GET, "/wp-content/uploads/", NULL, &m);
170 expect_route(r, N_HTTP_REQ_GET, "/wp-content/uploads/caf%C3%A9.png", "legacy-upload", &m);
171 expect_true("rest decoded", param_is(&m, "path", "caf\xc3\xa9.png"));
172 expect_route(r, N_HTTP_REQ_GET, "//", NULL, &m);
173 expect_route(r, N_HTTP_REQ_HEAD, "/feed/", "feed", &m);
174 expect_route(r, N_HTTP_REQ_GET, "/admin/login", "login-form", &m);
175 expect_route(r, N_HTTP_REQ_POST, "/admin/login", "login", &m);
176 expect_route(r, N_HTTP_REQ_POST, "/admin/posts/12", "post-edit", &m);
177 expect_route(r, N_HTTP_REQ_POST, "/admin/posts/12/trash", "post-trash", &m);
178
179 expect_true("POST on a GET route: 405",
180 n_router_match(r, N_HTTP_REQ_POST, "/feed/", 6, &m, &allowed) == N_ROUTER_METHOD_NOT_ALLOWED && allowed == (N_ROUTE_GET | N_ROUTE_HEAD));
181 expect_true("Allow header", n_router_allow_header(allowed, allow, sizeof(allow)) && strcmp(allow, "GET, HEAD") == 0);
182 expect_true("GET on a POST-only route: 405 POST",
183 n_router_match(r, N_HTTP_REQ_GET, "/admin/posts/1/trash", 20, &m, &allowed) == N_ROUTER_METHOD_NOT_ALLOWED &&
184 n_router_allow_header(allowed, allow, sizeof(allow)) && strcmp(allow, "POST") == 0);
185 expect_true("Allow for both", n_router_allow_header(N_ROUTE_GET | N_ROUTE_POST, allow, sizeof(allow)) && strcmp(allow, "GET, HEAD, POST") == 0);
186 expect_true("Allow buffer too small", !n_router_allow_header(N_ROUTE_GET, allow, 4));
187 expect_true("malformed escape: 400", n_router_match(r, N_HTTP_REQ_GET, "/a%zz/", 7, &m, NULL) == N_ROUTER_BAD_REQUEST);
188 expect_true("escape cut short: 400", n_router_match(r, N_HTTP_REQ_GET, "/a%4", 4, &m, NULL) == N_ROUTER_BAD_REQUEST);
189 expect_true("decoded NUL: 400", n_router_match(r, N_HTTP_REQ_GET, "/a%00b/", 7, &m, NULL) == N_ROUTER_BAD_REQUEST);
190 expect_true("ill-formed UTF-8 anywhere: 400", n_router_match(r, N_HTTP_REQ_GET, "/x/%E9/", 7, &m, NULL) == N_ROUTER_BAD_REQUEST);
191 expect_true("UTF-8 split across segments: 400", n_router_match(r, N_HTTP_REQ_GET, "/%C3/%A9", 8, &m, NULL) == N_ROUTER_BAD_REQUEST);
192 expect_true("no leading '/': 400", n_router_match(r, N_HTTP_REQ_GET, "feed/", 5, &m, NULL) == N_ROUTER_BAD_REQUEST);
193 expect_true("empty path: 400", n_router_match(r, N_HTTP_REQ_GET, "", 0, &m, NULL) == N_ROUTER_BAD_REQUEST);
194 expect_true("NULL router: 400", n_router_match(NULL, N_HTTP_REQ_GET, "/", 1, &m, NULL) == N_ROUTER_BAD_REQUEST);
195 expect_true("absent param", n_route_param(&m, "nope", NULL) == NULL);
196 {
197 char deep[200];
198 size_t i, o = 0;
199 for (i = 0; i < 40; i++) {
200 deep[o++] = '/';
201 deep[o++] = 'a';
202 }
203 expect_true("too many segments: 404", n_router_match(r, N_HTTP_REQ_GET, deep, o, &m, NULL) == N_ROUTER_NOT_FOUND);
204 }
205}
206
208static void test_checks(void) {
209 char err[256];
210 N_ROUTER* r = n_router_new();
211 static const N_ROUTE dup[] = {{N_ROUTE_GET, "/a/{x:slug}/", h, NULL}, {N_ROUTE_GET, "/a/{y:slug}/", h, NULL}};
212 static const N_ROUTE dhex[] = {{N_ROUTE_GET, "/b/{x:d4}", h, NULL}, {N_ROUTE_GET, "/b/{y:hex4}", h, NULL}};
213 static const N_ROUTE dlen[] = {{N_ROUTE_GET, "/c/{x:d4}", h, NULL}, {N_ROUTE_GET, "/c/{y:hex16}", h, NULL}};
214 static const N_ROUTE methods[] = {{N_ROUTE_GET, "/d/{x:slug}", h, NULL}, {N_ROUTE_POST, "/d/{y:slug}", h, NULL}};
215 static const N_ROUTE headget[] = {{N_ROUTE_GET, "/e", h, NULL}, {N_ROUTE_HEAD, "/e", h, NULL}};
216
217 n_router_add(r, dup, 2);
218 expect_true("same shape twice is ambiguous", n_router_check(r, err, sizeof(err)) == FALSE && strstr(err, "/a/{x:slug}/") != NULL);
219 n_router_free(&r);
220 r = n_router_new();
221 n_router_add(r, dhex, 2);
222 expect_true("d4 against hex4 is ambiguous", n_router_check(r, err, sizeof(err)) == FALSE);
223 n_router_free(&r);
224 r = n_router_new();
225 n_router_add(r, dlen, 2);
226 expect_true("d4 against hex16 is not", n_router_check(r, err, sizeof(err)) == TRUE && err[0] == '\0');
227 n_router_free(&r);
228 r = n_router_new();
229 n_router_add(r, methods, 2);
230 expect_true("same shape, other method, is not", n_router_check(r, err, sizeof(err)) == TRUE);
231 n_router_free(&r);
232 r = n_router_new();
233 n_router_add(r, headget, 2);
234 expect_true("HEAD route beside a GET one is ambiguous", n_router_check(r, err, sizeof(err)) == FALSE);
235 n_router_free(&r);
236 expect_true("free sets NULL", r == NULL);
237
238 {
239 static const char* bad[] = {
240 "noslash", "/a//b", "/{x", "/{x}y", "/{}", "/{X:seg}", "/{x:what}", "/{x:d0}", "/{x:d19}", "/{x:hex65}",
241 "/{x:int}", "/{x:seg}/{x:seg}", "/{r:rest}/more", "/{r:rest}/", "/a%20b", "/a b", "/a?b",
242 "/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}", "/{abcdefghijklmnopqrstuvwxyz012345}"};
243 size_t i;
244 for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
245 N_ROUTE one = {N_ROUTE_GET, bad[i], h, NULL};
246 r = n_router_new();
247 if (n_router_add(r, &one, 1) != FALSE || n_router_count(r) != 0) {
248 n_log(LOG_ERR, "FAIL: pattern '%s' accepted", bad[i]);
249 failures++;
250 }
251 n_router_free(&r);
252 }
253 }
254 {
255 N_ROUTE nomethod = {0, "/x", h, NULL};
256 N_ROUTE nohandler = {N_ROUTE_GET, "/x", NULL, NULL};
257 N_ROUTE badbit = {1u << 9, "/x", h, NULL};
258 N_ROUTE mixed[] = {{N_ROUTE_GET, "/ok", h, NULL}, {N_ROUTE_GET, "/bad//", h, NULL}};
259 r = n_router_new();
260 expect_true("route without method refused", n_router_add(r, &nomethod, 1) == FALSE);
261 expect_true("route without handler refused", n_router_add(r, &nohandler, 1) == FALSE);
262 expect_true("unknown method bit refused", n_router_add(r, &badbit, 1) == FALSE);
263 expect_true("one bad route: nothing of the table added", n_router_add(r, mixed, 2) == FALSE && n_router_count(r) == 0);
264 expect_true("empty table is fine", n_router_add(r, NULL, 0) == TRUE);
265 n_router_free(&r);
266 }
267}
268
273int main(int argc, char** argv) {
274 N_ROUTER* r = NULL;
275 char err[256];
276
277 if (process_args(argc, argv) != 0) {
278 fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
279 return 1;
280 }
282
283 r = n_router_new();
284 expect_true("public table added", n_router_add(r, PUBLIC, sizeof(PUBLIC) / sizeof(PUBLIC[0])));
285 expect_true("admin table added", n_router_add(r, ADMIN, sizeof(ADMIN) / sizeof(ADMIN[0])));
286 expect_true("route count", n_router_count(r) == 16);
287 expect_true("route access", n_router_route(r, 1) == &PUBLIC[1] && n_router_route(r, 12) == &ADMIN[0] && n_router_route(r, 16) == NULL);
288 expect_true("the blog's tables are not ambiguous", n_router_check(r, err, sizeof(err)) == TRUE);
289 if (err[0])
290 n_log(LOG_ERR, "%s", err);
291 test_match(r);
292 n_router_free(&r);
293 test_checks();
294
295 if (failures) {
296 n_log(LOG_ERR, "ex_http_router: %d failure(s)", failures);
297 return 1;
298 }
299 n_log(LOG_NOTICE, "ex_http_router: all checks passed");
300 return 0;
301}
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static void expect_true(const char *label, int cond)
Definition ex_atlas.c:53
static int log_level
static void test_checks(void)
the start-up checks
static int param_is(const N_ROUTE_MATCH *m, const char *name, const char *want)
param name of the last match equals want
static void test_match(const N_ROUTER *r)
matching on the blog's tables
static int h(void *ctx, const N_ROUTE_MATCH *m)
a handler that does nothing (the test only routes)
static const N_ROUTE PUBLIC[]
the public routes of the Nilorea blog, in a deliberately unhelpful order: the catch-all "/{slug}...
static const N_ROUTE ADMIN[]
a few admin routes
static void expect_route(const N_ROUTER *r, n_http_req_method method, const char *path, const char *want, N_ROUTE_MATCH *m)
route path with method and expect a route data tag (NULL: not found)
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
#define LOG_INFO
informational
Definition n_log.h:106
n_http_req_method
The methods accepted.
Definition n_http_req.h:77
@ N_HTTP_REQ_POST
Definition n_http_req.h:79
@ N_HTTP_REQ_GET
Definition n_http_req.h:78
@ N_HTTP_REQ_HEAD
Definition n_http_req.h:80
size_t nb_params
number of placeholders
const N_ROUTE * route
the route that matched
const void * data
caller data for this route (a policy: auth, CSRF, limits...)
#define N_ROUTE_POST
route accepts POST
N_ROUTER_RESULT
result of n_router_match()
N_ROUTER * n_router_new(void)
new, empty router
int n_router_allow_header(unsigned allowed, char *out, size_t size)
write an Allow header value for a method mask (GET implies HEAD)
N_ROUTER_RESULT n_router_match(const N_ROUTER *r, n_http_req_method method, const char *path, size_t len, N_ROUTE_MATCH *match, unsigned *allowed)
find the route for a method and a path
#define N_ROUTE_HEAD
route accepts HEAD only (rare: GET routes answer HEAD already)
#define N_ROUTE_GET
route accepts GET (and so HEAD)
int n_router_check(const N_ROUTER *r, char *err, size_t errsz)
refuse two routes sharing a method that can match the same path with the same specificity
void n_router_free(N_ROUTER **r)
free a router and set *r to NULL (the route tables are not touched)
size_t n_router_count(const N_ROUTER *r)
number of routes added
const N_ROUTE * n_router_route(const N_ROUTER *r, size_t i)
the i-th route added
int n_router_add(N_ROUTER *r, const N_ROUTE *routes, size_t count)
compile and add a route table; nothing is added when a route is refused
const char * n_route_param(const N_ROUTE_MATCH *m, const char *name, size_t *len)
decoded value of a placeholder
@ N_ROUTER_BAD_REQUEST
the path itself is invalid: 400
@ N_ROUTER_FOUND
a route matched path and method
@ N_ROUTER_NOT_FOUND
no route matches the path: 404 or a fallback
@ N_ROUTER_METHOD_NOT_ALLOWED
the path matches, the method does not: 405
one route of a table
a successful match: the route and its decoded placeholder values
Common headers and low-level functions & define.
a compiled router (opaque)
HTTP router: static route tables, typed placeholders, specificity.
Generic log system.