Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_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
27
29
30#include "nilorea/n_common.h"
31#include "nilorea/n_log.h"
32#include "nilorea/n_str.h"
33
34#include <stdio.h>
35#include <stdlib.h>
36#include <string.h>
37
38/* most segments in a pattern or a path the router looks at */
39#define N_ROUTER_SEGS_MAX 32
40
41/* Segment kinds. The value is also the specificity rank: lower wins. */
42typedef enum {
44 _SEG_DIGITS = 1, /* dN */
45 _SEG_HEX = 1, /* hexN, same rank as dN */
48 _SEG_ANY = 4, /* seg */
50} _seg_rank;
51
61
62typedef struct {
64 const char* lit; /* literal text, inside the pattern */
65 size_t lit_len;
66 size_t n; /* N of dN / hexN / intN */
67 int param; /* placeholder index, -1 for a literal */
68} _seg;
69
78
79struct N_ROUTER {
81 size_t nb;
82};
83
84static int _rank(_seg_type t) {
85 switch (t) {
86 case _T_LITERAL:
87 return _SEG_LITERAL;
88 case _T_DIGITS:
89 return _SEG_DIGITS;
90 case _T_HEX:
91 return _SEG_HEX;
92 case _T_INT:
93 return _SEG_INT;
94 case _T_SLUG:
95 return _SEG_SLUG;
96 case _T_ANY:
97 return _SEG_ANY;
98 default:
99 return _SEG_REST;
100 }
101}
102
108 N_ROUTER* r = NULL;
109 Malloc(r, N_ROUTER, 1);
110 return r;
111}
112
118 if (!r || !*r)
119 return;
120 FreeNoLog((*r)->routes);
121 FreeNoLog(*r);
122}
123
124/* Parse "N" of dN / hexN / intN within [lo, hi]. */
125static int _parse_n(const char* s, size_t len, size_t lo, size_t hi, size_t* n) {
126 size_t i;
127 *n = 0;
128 if (len == 0 || len > 2)
129 return 0;
130 for (i = 0; i < len; i++) {
131 if (s[i] < '0' || s[i] > '9')
132 return 0;
133 *n = *n * 10 + (size_t)(s[i] - '0');
134 }
135 return *n >= lo && *n <= hi;
136}
137
138static int _literal_byte_ok(unsigned char c) {
139 return c > 0x20 && c < 0x7f && c != '%' && c != '{' && c != '}' && c != '?' && c != '#';
140}
141
142/* Compile one pattern into c. Returns 1, or 0 with a reason in why. */
143static int _compile(const N_ROUTE* route, _compiled* c, const char** why) {
144 const char* p = route->pattern;
145 size_t len, i = 1;
146 memset(c, 0, sizeof(*c));
147 c->route = route;
148 *why = "";
149 if (!route->methods || !route->handler) {
150 *why = "no method or no handler";
151 return 0;
152 }
153 if (route->methods & ~(unsigned)(N_ROUTE_GET | N_ROUTE_POST | N_ROUTE_HEAD)) {
154 *why = "unknown method bit";
155 return 0;
156 }
157 if (!p || p[0] != '/') {
158 *why = "a pattern starts with '/'";
159 return 0;
160 }
161 len = strlen(p);
162 if (len > N_HTTP_REQ_PATH_MAX) {
163 *why = "pattern too long";
164 return 0;
165 }
166 if (len == 1) {
167 c->trailing_slash = 1;
168 return 1;
169 }
170 while (i < len) {
171 size_t s = i, e;
172 _seg* g;
173 while (i < len && p[i] != '/')
174 i++;
175 e = i;
176 if (e == s) {
177 *why = "empty segment ('//')";
178 return 0;
179 }
180 if (c->nb_segs >= N_ROUTER_SEGS_MAX) {
181 *why = "too many segments";
182 return 0;
183 }
184 if (c->nb_segs > 0 && c->segs[c->nb_segs - 1].type == _T_REST) {
185 *why = "{...:rest} must be the last segment";
186 return 0;
187 }
188 g = &c->segs[c->nb_segs];
189 g->param = -1;
190 if (p[s] == '{') {
191 size_t colon = s + 1, nlen, k, tlen;
192 const char* t;
193 if (p[e - 1] != '}') {
194 *why = "a placeholder fills its whole segment";
195 return 0;
196 }
197 while (colon < e - 1 && p[colon] != ':')
198 colon++;
199 nlen = colon - s - 1;
200 if (nlen == 0 || nlen > N_ROUTE_NAME_MAX) {
201 *why = "placeholder name empty or too long";
202 return 0;
203 }
204 for (k = s + 1; k < colon; k++)
205 if (!((p[k] >= 'a' && p[k] <= 'z') || (p[k] >= '0' && p[k] <= '9') || p[k] == '_')) {
206 *why = "placeholder name is not [a-z0-9_]";
207 return 0;
208 }
209 if (c->nb_params >= N_ROUTE_PARAMS_MAX) {
210 *why = "too many placeholders";
211 return 0;
212 }
213 for (k = 0; k < c->nb_params; k++)
214 if (strlen(c->names[k]) == nlen && memcmp(c->names[k], p + s + 1, nlen) == 0) {
215 *why = "duplicate placeholder name";
216 return 0;
217 }
218 memcpy(c->names[c->nb_params], p + s + 1, nlen);
219 c->names[c->nb_params][nlen] = '\0';
220 t = colon < e - 1 ? p + colon + 1 : "seg}";
221 tlen = colon < e - 1 ? (size_t)(e - 1 - colon - 1) : 3;
222 if (tlen == 4 && memcmp(t, "slug", 4) == 0)
223 g->type = _T_SLUG;
224 else if (tlen == 3 && memcmp(t, "seg", 3) == 0)
225 g->type = _T_ANY;
226 else if (tlen == 4 && memcmp(t, "rest", 4) == 0)
227 g->type = _T_REST;
228 else if (tlen > 1 && t[0] == 'd' && _parse_n(t + 1, tlen - 1, 1, 18, &g->n))
229 g->type = _T_DIGITS;
230 else if (tlen > 3 && memcmp(t, "hex", 3) == 0 && _parse_n(t + 3, tlen - 3, 1, 64, &g->n))
231 g->type = _T_HEX;
232 else if (tlen > 3 && memcmp(t, "int", 3) == 0 && _parse_n(t + 3, tlen - 3, 1, 18, &g->n))
233 g->type = _T_INT;
234 else {
235 *why = "unknown placeholder type";
236 return 0;
237 }
238 g->param = (int)c->nb_params;
239 c->nb_params++;
240 } else {
241 size_t k;
242 for (k = s; k < e; k++)
243 if (!_literal_byte_ok((unsigned char)p[k])) {
244 *why = "literal holds a byte outside visible ASCII, or % { } ? #";
245 return 0;
246 }
247 g->type = _T_LITERAL;
248 g->lit = p + s;
249 g->lit_len = e - s;
250 }
251 c->nb_segs++;
252 if (i < len) {
253 i++; /* skip '/' */
254 if (i == len)
255 c->trailing_slash = 1;
256 }
257 }
258 if (c->nb_segs > 0 && c->segs[c->nb_segs - 1].type == _T_REST && c->trailing_slash) {
259 *why = "{...:rest} takes the trailing '/' itself";
260 return 0;
261 }
262 return 1;
263}
264
272int n_router_add(N_ROUTER* r, const N_ROUTE* routes, size_t count) {
273 _compiled* grown = NULL;
274 size_t i;
275 __n_assert(r, return FALSE);
276 if (count == 0)
277 return TRUE;
278 __n_assert(routes, return FALSE);
279 Malloc(grown, _compiled, r->nb + count);
280 __n_assert(grown, return FALSE);
281 if (r->nb)
282 memcpy(grown, r->routes, r->nb * sizeof(_compiled));
283 for (i = 0; i < count; i++) {
284 const char* why = "";
285 if (!_compile(&routes[i], &grown[r->nb + i], &why)) {
286 n_log(LOG_ERR, "router: route '%s' refused: %s", routes[i].pattern ? routes[i].pattern : "(null)", why);
287 Free(grown);
288 return FALSE;
289 }
290 }
291 FreeNoLog(r->routes);
292 r->routes = grown;
293 r->nb += count;
294 return TRUE;
295}
296
302size_t n_router_count(const N_ROUTER* r) {
303 return r ? r->nb : 0;
304}
305
312const N_ROUTE* n_router_route(const N_ROUTER* r, size_t i) {
313 if (!r || i >= r->nb)
314 return NULL;
315 return r->routes[i].route;
316}
317
318/* Effective methods: GET implies HEAD. */
319static unsigned _methods(unsigned m) {
320 if (m & N_ROUTE_GET)
321 m |= N_ROUTE_HEAD;
322 return m;
323}
324
325/* Can two typed segments of the same rank both match some segment? */
326static int _types_overlap(const _seg* a, const _seg* b) {
327 if (_rank(a->type) == _SEG_DIGITS)
328 return a->n == b->n; /* dN / hexN: equal length, digits are hex */
329 return 1;
330}
331
340int n_router_check(const N_ROUTER* r, char* err, size_t errsz) {
341 size_t i, j, k;
342 if (err && errsz)
343 err[0] = '\0';
344 __n_assert(r, return FALSE);
345 for (i = 0; i < r->nb; i++) {
346 for (j = i + 1; j < r->nb; j++) {
347 const _compiled* a = &r->routes[i];
348 const _compiled* b = &r->routes[j];
349 int ambiguous = 1;
350 if (!(_methods(a->route->methods) & _methods(b->route->methods)))
351 continue;
352 if (a->nb_segs != b->nb_segs || a->trailing_slash != b->trailing_slash)
353 continue;
354 for (k = 0; k < a->nb_segs && ambiguous; k++) {
355 const _seg* x = &a->segs[k];
356 const _seg* y = &b->segs[k];
357 if (_rank(x->type) != _rank(y->type)) {
358 ambiguous = 0; /* the more specific one wins here */
359 } else if (x->type == _T_LITERAL) {
360 ambiguous = x->lit_len == y->lit_len && memcmp(x->lit, y->lit, x->lit_len) == 0;
361 } else {
362 ambiguous = _types_overlap(x, y);
363 }
364 }
365 if (ambiguous) {
366 if (err && errsz)
367 snprintf(err, errsz, "'%s' and '%s' match the same paths with the same specificity", a->route->pattern, b->route->pattern);
368 return FALSE;
369 }
370 }
371 }
372 return TRUE;
373}
374
375/* Does the raw segment [s, s+n) satisfy g? */
376static int _seg_fits(const _seg* g, const char* s, size_t n) {
377 size_t k;
378 switch (g->type) {
379 case _T_LITERAL:
380 return n == g->lit_len && memcmp(s, g->lit, n) == 0;
381 case _T_DIGITS:
382 case _T_INT:
383 if (n == 0 || (g->type == _T_DIGITS ? n != g->n : n > g->n))
384 return 0;
385 for (k = 0; k < n; k++)
386 if (s[k] < '0' || s[k] > '9')
387 return 0;
388 return 1;
389 case _T_HEX:
390 if (n != g->n)
391 return 0;
392 for (k = 0; k < n; k++)
393 if (!((s[k] >= '0' && s[k] <= '9') || (s[k] >= 'a' && s[k] <= 'f')))
394 return 0;
395 return 1;
396 case _T_SLUG:
397 if (n == 0 || n > 120)
398 return 0;
399 for (k = 0; k < n; k++)
400 if (!((s[k] >= 'a' && s[k] <= 'z') || (s[k] >= '0' && s[k] <= '9') || s[k] == '_' || s[k] == '-'))
401 return 0;
402 return 1;
403 case _T_ANY:
404 return n >= 1 && n <= 255;
405 default:
406 return n >= 1;
407 }
408}
409
410static int _hexval(char c) {
411 if (c >= '0' && c <= '9')
412 return c - '0';
413 if (c >= 'a' && c <= 'f')
414 return c - 'a' + 10;
415 if (c >= 'A' && c <= 'F')
416 return c - 'A' + 10;
417 return -1;
418}
419
420/* Strictly percent-decode [s, s+n) into dst (n + 1 bytes). Returns the
421 * decoded length, or (size_t)-1 on a malformed escape, a NUL or bad UTF-8. */
422static size_t _decode(const char* s, size_t n, char* dst) {
423 size_t i, o = 0;
424 for (i = 0; i < n; i++) {
425 char c = s[i];
426 if (c == '%') {
427 int hi, lo;
428 if (n - i < 3 || (hi = _hexval(s[i + 1])) < 0 || (lo = _hexval(s[i + 2])) < 0)
429 return (size_t)-1;
430 c = (char)((hi << 4) | lo);
431 i += 2;
432 }
433 if (c == '\0')
434 return (size_t)-1;
435 dst[o++] = c;
436 }
437 dst[o] = '\0';
438 if (!n_str_utf8_valid(dst, o))
439 return (size_t)-1;
440 return o;
441}
442
443/* Compare the specificity of two matching routes: < 0 when a is more specific. */
444static int _more_specific(const _compiled* a, const _compiled* b) {
445 size_t k, n = a->nb_segs < b->nb_segs ? a->nb_segs : b->nb_segs;
446 for (k = 0; k < n; k++) {
447 int d = _rank(a->segs[k].type) - _rank(b->segs[k].type);
448 if (d)
449 return d;
450 }
451 return 0;
452}
453
454/* Split path into raw segments; returns the count or -1 when there are too many. */
455static int _split(const char* path, size_t len, const char** seg, size_t* seglen, int* trailing) {
456 size_t i = 1;
457 int n = 0;
458 *trailing = 0;
459 if (len == 1)
460 *trailing = 1;
461 while (i < len) {
462 size_t s = i;
463 while (i < len && path[i] != '/')
464 i++;
465 if (n >= N_ROUTER_SEGS_MAX)
466 return -1;
467 seg[n] = path + s;
468 seglen[n] = i - s;
469 n++;
470 if (i < len) {
471 i++;
472 if (i == len)
473 *trailing = 1;
474 }
475 }
476 return n;
477}
478
479/* Does route c match the split path? rest consumes the remainder. */
480static int _route_fits(const _compiled* c, const char** seg, const size_t* seglen, int nseg, int trailing) {
481 size_t k;
482 if (c->nb_segs > 0 && c->segs[c->nb_segs - 1].type == _T_REST) {
483 if ((size_t)nseg < c->nb_segs)
484 return 0;
485 } else if ((size_t)nseg != c->nb_segs || trailing != c->trailing_slash) {
486 return 0;
487 }
488 for (k = 0; k < c->nb_segs; k++) {
489 if (c->segs[k].type == _T_REST)
490 return 1; /* everything from here on, any length >= 1 */
491 if (!_seg_fits(&c->segs[k], seg[k], seglen[k]))
492 return 0;
493 }
494 return 1;
495}
496
507N_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) {
508 const char* seg[N_ROUTER_SEGS_MAX];
509 size_t seglen[N_ROUTER_SEGS_MAX];
510 char scratch[N_HTTP_REQ_PATH_MAX + 1];
511 const _compiled* best = NULL;
512 unsigned path_methods = 0, want = N_HTTP_REQ_METHOD_BIT(method);
513 int nseg, trailing, k;
514 size_t i, o;
515
516 if (allowed)
517 *allowed = 0;
518 if (!r || !path || !match || len == 0 || len > N_HTTP_REQ_PATH_MAX || path[0] != '/')
520 nseg = _split(path, len, seg, seglen, &trailing);
521 if (nseg < 0)
522 return N_ROUTER_NOT_FOUND;
523 /* the whole path must decode cleanly, whatever route it ends up on */
524 for (k = 0; k < nseg; k++)
525 if (_decode(seg[k], seglen[k], scratch) == (size_t)-1)
527 for (i = 0; i < r->nb; i++) {
528 const _compiled* c = &r->routes[i];
529 if (!_route_fits(c, seg, seglen, nseg, trailing))
530 continue;
531 path_methods |= _methods(c->route->methods);
532 if (!(_methods(c->route->methods) & want))
533 continue;
534 if (!best || _more_specific(c, best) < 0)
535 best = c;
536 }
537 if (!best) {
538 if (path_methods) {
539 if (allowed)
540 *allowed = path_methods;
542 }
543 return N_ROUTER_NOT_FOUND;
544 }
545 /* decode the captured values into the match */
546 memset(match, 0, sizeof(*match));
547 match->route = best->route;
548 match->nb_params = best->nb_params;
549 o = 0;
550 for (i = 0; i < best->nb_segs; i++) {
551 const _seg* g = &best->segs[i];
552 const char* s = seg[i];
553 size_t n = seglen[i], d;
554 if (g->param < 0)
555 continue;
556 if (g->type == _T_REST)
557 n = (size_t)(path + len - s); /* remainder, '/' included */
558 d = _decode(s, n, match->buf + o);
559 if (d == (size_t)-1)
560 return N_ROUTER_BAD_REQUEST; /* a %2F-built remainder may still be bad */
561 match->names[g->param] = best->names[g->param];
562 match->values[g->param] = match->buf + o;
563 match->lens[g->param] = d;
564 o += d + 1;
565 }
566 return N_ROUTER_FOUND;
567}
568
576const char* n_route_param(const N_ROUTE_MATCH* m, const char* name, size_t* len) {
577 size_t i;
578 if (len)
579 *len = 0;
580 if (!m || !name)
581 return NULL;
582 for (i = 0; i < m->nb_params; i++)
583 if (m->names[i] && strcmp(m->names[i], name) == 0) {
584 if (len)
585 *len = m->lens[i];
586 return m->values[i];
587 }
588 return NULL;
589}
590
598int n_router_allow_header(unsigned allowed, char* out, size_t size) {
599 char buf[32] = "";
600 allowed = _methods(allowed);
601 if (allowed & N_ROUTE_GET)
602 strcat(buf, "GET");
603 if (allowed & N_ROUTE_HEAD)
604 strcat(buf, buf[0] ? ", HEAD" : "HEAD");
605 if (allowed & N_ROUTE_POST)
606 strcat(buf, buf[0] ? ", POST" : "POST");
607 if (!out || size <= strlen(buf))
608 return FALSE;
609 memcpy(out, buf, strlen(buf) + 1);
610 return TRUE;
611}
LIST * routes
#define FreeNoLog(__ptr)
Free Handler without log.
Definition n_common.h:272
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:204
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:279
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:263
#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
#define N_HTTP_REQ_METHOD_BIT(m)
Bit of a method in n_http_req_limits.methods.
Definition n_http_req.h:84
n_http_req_method
The methods accepted.
Definition n_http_req.h:77
#define N_HTTP_REQ_PATH_MAX
Longest origin-form request target ('/'-prefixed, query included).
Definition n_http_req.h:65
const char * names[8]
placeholder names, in pattern order
const char * pattern
the pattern, see the file documentation
size_t lens[8]
decoded value lengths
const char * values[8]
decoded values (NUL-terminated, inside buf)
n_route_handler handler
what the caller runs on a match
size_t nb_params
number of placeholders
char buf[2048u+8+1]
storage for the decoded values
unsigned methods
accepted methods, an OR of N_ROUTE_GET / N_ROUTE_POST / N_ROUTE_HEAD
const N_ROUTE * route
the route that matched
#define N_ROUTE_POST
route accepts POST
N_ROUTER_RESULT
result of n_router_match()
N_ROUTER * n_router_new(void)
new, empty router
#define N_ROUTE_NAME_MAX
longest placeholder name
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
#define N_ROUTE_PARAMS_MAX
most placeholders in one route
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
int n_str_utf8_valid(const char *s, size_t len)
Test that [s, s+len) is well-formed UTF-8 (RFC 3629): no overlong form, no surrogate,...
Definition n_str.c:1855
Common headers and low-level functions & define.
static int _hexval(char c)
static int _literal_byte_ok(unsigned char c)
_compiled * routes
static int _compile(const N_ROUTE *route, _compiled *c, const char **why)
size_t lit_len
static int _split(const char *path, size_t len, const char **seg, size_t *seglen, int *trailing)
size_t nb
static unsigned _methods(unsigned m)
size_t n
static int _rank(_seg_type t)
size_t nb_segs
_seg_rank
@ _SEG_REST
@ _SEG_LITERAL
@ _SEG_DIGITS
@ _SEG_SLUG
@ _SEG_HEX
@ _SEG_INT
@ _SEG_ANY
_seg_type type
static int _route_fits(const _compiled *c, const char **seg, const size_t *seglen, int nseg, int trailing)
_seg segs[32]
int trailing_slash
static int _more_specific(const _compiled *a, const _compiled *b)
char names[8][31+1]
const N_ROUTE * route
static int _parse_n(const char *s, size_t len, size_t lo, size_t hi, size_t *n)
#define N_ROUTER_SEGS_MAX
static int _seg_fits(const _seg *g, const char *s, size_t n)
_seg_type
@ _T_LITERAL
@ _T_ANY
@ _T_HEX
@ _T_REST
@ _T_SLUG
@ _T_DIGITS
@ _T_INT
const char * lit
static int _types_overlap(const _seg *a, const _seg *b)
int param
static size_t _decode(const char *s, size_t n, char *dst)
size_t nb_params
a compiled router (opaque)
HTTP router: static route tables, typed placeholders, specificity.
Generic log system.
N_STR and string function declaration.