cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
middleware.c
Go to the documentation of this file.
1#define _POSIX_C_SOURCE 200809L
4#include <cwist/core/macros.h>
7#include <stdarg.h>
8#include <stdio.h>
9#include <stdlib.h>
10#include <string.h>
11#include <strings.h>
12#include <time.h>
13#include <sys/time.h>
14#include <unistd.h>
15#include <pthread.h>
16#include "../../net/http/async_internal.h"
17#include "log_async.h"
18
26static unsigned int rid_seed = 0;
27
35static void generate_request_id(char id[17]) {
36 static const char charset[] = "abcdefghijklmnopqrstuvwxyz0123456789";
37
38 pthread_mutex_lock(&rid_mutex);
39 if (rid_seed == 0) rid_seed = (unsigned int)time(NULL) ^ (unsigned int)pthread_self();
40 unsigned int seed = rid_seed++;
41 pthread_mutex_unlock(&rid_mutex);
42
43 for (int i = 0; i < 16; i++) {
44 id[i] = charset[rand_r(&seed) % (sizeof(charset) - 1)];
45 }
46 id[16] = '\0';
47}
48
56 cwist_handler_func next) {
57 const char *header_name = "X-Request-Id";
58 const char *rid = cwist_http_header_get(req->headers, header_name);
59 char generated[17];
60
61 if (!rid) {
62 generate_request_id(generated);
63 rid = generated;
65 }
66
67 /* header_add copies the value, so neither the request's own header nor
68 * the stack buffer has to outlive this call. */
70
71 next(req, res);
72}
73
83
84/* --- Access Log Middleware --- */
85
87
95static void access_log_printf(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
96static void access_log_printf(const char *fmt, ...) {
97 char stack_buf[1024];
98 char *line = stack_buf;
100 va_start(args, fmt);
101 int n = vsnprintf(stack_buf, sizeof(stack_buf), fmt, args);
102 va_end(args);
103 if (n < 0) return;
104 if ((size_t)n >= sizeof(stack_buf)) {
105 line = cwist_alloc((size_t)n + 1);
106 if (!line) return;
107 va_start(args, fmt);
108 vsnprintf(line, (size_t)n + 1, fmt, args);
109 va_end(args);
110 }
111 if (!cwist_log_async_submit(STDOUT_FILENO, line, (size_t)n)) {
112 pthread_mutex_lock(&log_mutex);
113 fwrite(line, 1, (size_t)n, stdout);
114 pthread_mutex_unlock(&log_mutex);
115 }
116 if (line != stack_buf) cwist_free(line);
117}
118
126static void format_clf_time(char *buf, size_t len, const struct timeval *tv) {
127 struct tm tm;
128 localtime_r(&tv->tv_sec, &tm);
129 char tzbuf[8];
130 strftime(tzbuf, sizeof(tzbuf), "%z", &tm);
131 strftime(buf, len, "%d/%b/%Y:%H:%M:%S", &tm);
132 size_t pos = strlen(buf);
133 snprintf(buf + pos, len - pos, " %s", tzbuf);
134}
135
143static void format_iso8601_time(char *buf, size_t len, const struct timeval *tv) {
144 struct tm tm;
145 gmtime_r(&tv->tv_sec, &tm);
146 strftime(buf, len, "%Y-%m-%dT%H:%M:%S", &tm);
147 size_t pos = strlen(buf);
148 snprintf(buf + pos, len - pos, ".%03ldZ", tv->tv_usec / 1000);
149}
150
164static const char *access_log_request_id(cwist_http_request *req, cwist_http_response *res) {
166 return cwist_http_header_get(headers, "X-Request-Id");
167}
168
179static void access_log_status_bytes(const cwist_http_response *res, const char *unknown,
180 char *status, size_t status_len, char *bytes,
181 size_t bytes_len) {
182 if (cwist_async_res_deferred(res)) {
183 snprintf(status, status_len, "%s", unknown);
184 snprintf(bytes, bytes_len, "%s", unknown);
185 return;
186 }
187 snprintf(status, status_len, "%d", (int)res->status_code);
188 snprintf(bytes, bytes_len, "%zu", res->body ? res->body->size : (size_t)0);
189}
190
198static void cwist_mw_access_log_common_handler(cwist_http_request *req, cwist_http_response *res,
199 cwist_handler_func next) {
200 struct timeval start, end;
202 next(req, res);
204
205 const char *rid = access_log_request_id(req, res);
207 const char *ip_str = ip ? ip->data : "-";
208
209 char time_buf[64];
210 format_clf_time(time_buf, sizeof(time_buf), &end);
211
212 char status[16], res_bytes[32];
213 access_log_status_bytes(res, "-", status, sizeof(status), res_bytes, sizeof(res_bytes));
214
215 access_log_printf("%s - %s [%s] \"%s %s %s\" %s %s\n", ip_str, rid ? rid : "-", time_buf,
216 cwist_http_method_to_string(req->method), req->path->data,
217 req->version ? req->version->data : "HTTP/1.1", status, res_bytes);
218
219 if (ip) cwist_sstring_destroy(ip);
220}
221
229static void cwist_mw_access_log_combined_handler(cwist_http_request *req, cwist_http_response *res,
230 cwist_handler_func next) {
231 struct timeval start, end;
233 next(req, res);
235
236 const char *rid = access_log_request_id(req, res);
238 const char *ip_str = ip ? ip->data : "-";
239
240 char time_buf[64];
241 format_clf_time(time_buf, sizeof(time_buf), &end);
242
243 const char *referer = cwist_http_header_get(req->headers, "Referer");
244 const char *user_agent = cwist_http_header_get(req->headers, "User-Agent");
245 char status[16], res_bytes[32];
246 access_log_status_bytes(res, "-", status, sizeof(status), res_bytes, sizeof(res_bytes));
247
248 access_log_printf("%s - %s [%s] \"%s %s %s\" %s %s \"%s\" \"%s\"\n", ip_str, rid ? rid : "-",
249 time_buf, cwist_http_method_to_string(req->method), req->path->data,
250 req->version ? req->version->data : "HTTP/1.1", status, res_bytes,
251 referer ? referer : "-", user_agent ? user_agent : "-");
252
253 if (ip) cwist_sstring_destroy(ip);
254}
255
263static void cwist_mw_access_log_json_handler(cwist_http_request *req, cwist_http_response *res,
264 cwist_handler_func next) {
265 struct timeval start, end;
267 next(req, res);
269 long msec = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000;
270
271 const char *rid = access_log_request_id(req, res);
273 const char *ip_str = ip ? ip->data : "-";
274
275 char time_buf[64];
276 format_iso8601_time(time_buf, sizeof(time_buf), &end);
277
278 size_t req_bytes = req->body ? req->body->size : 0;
279 char status[16], res_bytes[32];
280 access_log_status_bytes(res, "null", status, sizeof(status), res_bytes, sizeof(res_bytes));
281
282 access_log_printf(
283 "{\"time\":\"%s\",\"client\":\"%s\",\"rid\":\"%s\",\"method\":\"%s\",\"path\":\"%s\",\"protocol\":\"%s\",\"status\":%s,\"duration_ms\":%ld,\"req_bytes\":%zu,\"res_bytes\":%s}\n",
284 time_buf, ip_str, rid ? rid : "-", cwist_http_method_to_string(req->method),
285 req->path->data, req->version ? req->version->data : "HTTP/1.1", status, msec, req_bytes,
286 res_bytes);
287
288 if (ip) cwist_sstring_destroy(ip);
289}
290
297 switch (format) {
298 case CWIST_LOG_COMMON: return cwist_mw_access_log_common_handler;
299 case CWIST_LOG_COMBINED: return cwist_mw_access_log_combined_handler;
300 case CWIST_LOG_JSON: return cwist_mw_access_log_json_handler;
301 default: return cwist_mw_access_log_common_handler;
302 }
303}
304
305/* --- Metrics Middleware --- */
306
308
328
336
337/* --- Rate Limiter Middleware --- */
338
339#include <ttak/limit/limit.h>
340
341typedef struct {
342 char ip[46];
343 ttak_token_bucket_t bucket;
344 bool active;
345} ip_bucket_t;
346
347#define MAX_IP_BUCKETS 1024
348static ip_bucket_t ip_buckets[MAX_IP_BUCKETS];
349static int ip_bucket_count = 0;
351
365static void cwist_mw_rate_limit_ip_handler(cwist_http_request *req, cwist_http_response *res,
366 cwist_handler_func next, int rpm) {
367 if (rpm <= 0) rpm = 60;
368
370 if (!ip) {
371 next(req, res);
372 return;
373 }
374
375 pthread_mutex_lock(&rate_mutex);
376 ip_bucket_t *found = NULL;
377 for (int i = 0; i < ip_bucket_count; i++) {
378 if (ip_buckets[i].active && strcmp(ip_buckets[i].ip, ip->data) == 0) {
379 found = &ip_buckets[i];
380 break;
381 }
382 }
383
384 if (!found && ip_bucket_count < MAX_IP_BUCKETS) {
385 found = &ip_buckets[ip_bucket_count++];
386 strncpy(found->ip, ip->data, sizeof(found->ip) - 1);
387 found->ip[sizeof(found->ip) - 1] = '\0';
388 found->active = true;
389 double rate = rpm / 60.0;
390 ttak_token_bucket_init(&found->bucket, rate, (double)rpm);
391 }
392
393 /* Fail-open: if the IP table is full we cannot track this client, so
394 * allow the request rather than permanently blocking unknown IPs (which
395 * would let an attacker exhaust the 1024 slots with spoofed addresses
396 * and then deny service to all legitimate users). */
397 bool allowed = (found == NULL);
398 if (found) {
399 allowed = ttak_token_bucket_consume(&found->bucket, 1.0);
400 }
402 pthread_mutex_unlock(&rate_mutex);
403
404 if (!allowed) {
405 res->status_code = 429;
406 cwist_sstring_assign(res->body, "Too Many Requests");
407 char retry_buf[16];
408 snprintf(retry_buf, sizeof(retry_buf), "%d", 60 / rpm > 0 ? 60 / rpm : 1);
409 cwist_http_header_add(&res->headers, "Retry-After", retry_buf);
410 return;
411 }
412
413 next(req, res);
414}
415
416#define CWIST_RATE_LIMIT_MAX_CFGS 8
417
418typedef struct {
419 int rpm;
420} rate_limit_cfg_t;
421
422static rate_limit_cfg_t s_rate_cfgs[CWIST_RATE_LIMIT_MAX_CFGS];
423static int s_rate_cfg_count = 0;
424static pthread_mutex_t s_rate_cfg_mutex = PTHREAD_MUTEX_INITIALIZER;
425
426#define CWIST_RATE_LIMIT_DEFINE_WRAPPER(N) \
427 static void cwist_mw_rate_limit_wrap_##N(cwist_http_request *req, cwist_http_response *res, \
428 cwist_handler_func next) { \
429 cwist_mw_rate_limit_ip_handler(req, res, next, s_rate_cfgs[N].rpm); \
430 }
431
440
445};
446
451 pthread_mutex_lock(&rate_mutex);
452 ip_bucket_count = 0;
453 pthread_mutex_unlock(&rate_mutex);
454}
455
468 if (requests_per_minute <= 0) requests_per_minute = 60;
469
470 pthread_mutex_lock(&s_rate_cfg_mutex);
471
472 for (int i = 0; i < s_rate_cfg_count; i++) {
473 if (s_rate_cfgs[i].rpm == requests_per_minute) {
474 pthread_mutex_unlock(&s_rate_cfg_mutex);
475 return s_rate_wrappers[i];
476 }
477 }
478
479 if (s_rate_cfg_count >= CWIST_RATE_LIMIT_MAX_CFGS) {
480 pthread_mutex_unlock(&s_rate_cfg_mutex);
481 fprintf(stderr, "[CWIST] cwist_mw_rate_limit_ip: maximum config slots exceeded\n");
482 return s_rate_wrappers[0];
483 }
484
485 int slot = s_rate_cfg_count++;
486 s_rate_cfgs[slot].rpm = requests_per_minute;
487
488 pthread_mutex_unlock(&s_rate_cfg_mutex);
489 return s_rate_wrappers[slot];
490}
491
492/* --- CORS Middleware --- */
493
501 cwist_handler_func next) {
502 // Add standard CORS headers
503 cwist_http_header_add(&res->headers, "Access-Control-Allow-Origin", "*");
504
505 // Handle Preflight (OPTIONS)
506 if (req->method == CWIST_HTTP_OPTIONS) {
507 cwist_http_header_add(&res->headers, "Access-Control-Allow-Methods",
508 "GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD");
509 cwist_http_header_add(&res->headers, "Access-Control-Allow-Headers",
510 "Content-Type, Authorization, X-Request-Id");
511 cwist_http_header_add(&res->headers, "Access-Control-Max-Age", "86400"); // 24 hours
512
513 res->status_code = CWIST_HTTP_NO_CONTENT; // 204 No Content
514 // Short-circuit: do not call next()
515 return;
516 }
517
518 next(req, res);
519}
520
528
529/* --- JWT Authentication Middleware --- */
530
531/*
532 * The decoded claims of a request reach downstream handlers through a
533 * per-thread stack of contexts, one per JWT middleware currently running,
534 * each tagged with its request. req->private_data is not used: inside the
535 * app's middleware chain it holds the chain's own state, which next() and
536 * the final route handler read.
537 *
538 * Each context is stack-allocated in the middleware frame and is only valid
539 * while the rest of the chain runs synchronously inside next(); claims are
540 * destroyed when next() returns, so no pointer to them escapes to
541 * asynchronous code.
542 */
543
544typedef struct cwist_jwt_ctx {
545 const cwist_http_request *req;
546 cwist_jwt_claims *claims;
547 struct cwist_jwt_ctx *outer;
549
550#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L)
552#elif defined(__GNUC__) || defined(__clang__)
554#endif
555
556#ifdef CWIST_JWT
565 cwist_handler_func next, const char *secret) {
566 /* Extract Bearer token from Authorization header */
567 char *auth_header = cwist_http_header_get(req->headers, "Authorization");
568 if (!auth_header) {
570 cwist_sstring_assign(res->body, "{\"error\":\"Missing Authorization header\"}");
571 cwist_http_header_add(&res->headers, "Content-Type", "application/json");
572 return;
573 }
574
575 /* Expect "Bearer <token>" (scheme is case-insensitive per RFC 6750) */
576 if (strncasecmp(auth_header, "Bearer ", 7) != 0) {
578 cwist_sstring_assign(res->body, "{\"error\":\"Invalid Authorization scheme\"}");
579 cwist_http_header_add(&res->headers, "Content-Type", "application/json");
580 return;
581 }
582
583 const char *token = auth_header + 7;
584 while (*token == ' ' || *token == '\t') token++;
585
587 if (!claims) {
589 cwist_sstring_assign(res->body, "{\"error\":\"Invalid or expired token\"}");
590 cwist_http_header_add(&res->headers, "Content-Type", "application/json");
591 return;
592 }
593
594 /* Publish the claims to downstream handlers on this thread for the
595 * duration of the rest of the chain. */
596 cwist_jwt_ctx_t ctx = {.req = req, .claims = claims, .outer = t_jwt_ctx};
597 t_jwt_ctx = &ctx;
598
599 if (next) next(req, res);
600
601 t_jwt_ctx = ctx.outer;
603}
604
605typedef struct {
606 const char *secret;
608
609/* We keep a small static table of registered secrets. A server rarely needs
610 * more than a handful of distinct signing keys. 8 slots cover the common case
611 * (e.g. one per audience / service) without dynamic allocation. If more are
612 * needed, increase CWIST_JWT_MAX_SECRETS and add the corresponding wrapper. */
613#define CWIST_JWT_MAX_SECRETS 8
614
616static int s_jwt_cfg_count = 0;
618
619/* One wrapper function per registered secret slot, because
620 * cwist_middleware_func is a plain function pointer that cannot carry the
621 * secret itself. */
622#define CWIST_JWT_DEFINE_WRAPPER(N) \
623 static void cwist_mw_jwt_wrap_##N(cwist_http_request *req, cwist_http_response *res, \
624 cwist_handler_func next) { \
625 cwist_mw_jwt_auth_handler(req, res, next, s_jwt_cfgs[N].secret); \
626 }
627
636
640};
641#endif /* CWIST_JWT */
642
650#ifdef CWIST_JWT
651 if (!secret) return NULL;
652
654
655 /* Reuse existing slot for the same secret */
656 for (int i = 0; i < s_jwt_cfg_count; i++) {
657 if (s_jwt_cfgs[i].secret && strcmp(s_jwt_cfgs[i].secret, secret) == 0) {
659 return s_jwt_wrappers[i];
660 }
661 }
662
665 fprintf(stderr, "[CWIST] cwist_mw_jwt_auth: maximum secret slots exceeded\n");
666 return NULL;
667 }
668
669 int slot = s_jwt_cfg_count++;
670 s_jwt_cfgs[slot].secret = secret;
671
673 return s_jwt_wrappers[slot];
674#else
675 /* JWT compiled out (CWIST_JWT=0): no wrapper can verify tokens. */
676 (void)secret;
677 return NULL;
678#endif
679}
680
688 if (!req) return NULL;
689 for (const cwist_jwt_ctx_t *ctx = t_jwt_ctx; ctx; ctx = ctx->outer) {
690 if (ctx->req == req) return ctx->claims;
691 }
692 return NULL;
693}
__attribute__((constructor))
Definition alloc.c:64
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
bool cwist_async_res_deferred(const cwist_http_response *res)
Definition async_tls.c:71
Convenience size-conversion and utility macros shared across CWIST.
#define CWIST_UNUSED(x)
Mark an intentionally unused variable to silence compiler warnings.
Definition macros.h:23
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
void cwist_sstring_destroy(cwist_sstring *str)
Destroy an sstring instance.
Definition sstring.c:823
Simple JSON string builder utility.
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value by key.
Definition http.c:1880
@ CWIST_HTTP_UNAUTHORIZED
Definition http.h:32
@ CWIST_HTTP_NO_CONTENT
Definition http.h:30
@ CWIST_HTTP_OPTIONS
Definition http.h:23
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
Definition http.c:1861
const char * cwist_http_method_to_string(cwist_http_method_t method)
Convert method enum to string.
Definition http.c:1554
void cwist_jwt_claims_destroy(cwist_jwt_claims *claims)
Free a claims object.
Definition jwt.c:779
struct cwist_jwt_claims cwist_jwt_claims
Opaque JWT claims object returned after successful verification.
Definition jwt.h:25
cwist_jwt_claims * cwist_jwt_verify(const char *token, const char *secret)
Verify a JWT token and return the decoded claims.
Definition jwt.c:748
bool cwist_log_async_submit(int fd, const char *line, size_t len)
Queue one complete line for fd (STDOUT_FILENO or STDERR_FILENO).
Definition log_async.c:263
Internal hook from the log call sites into the async log writer.
Production-grade metrics engine for CWIST.
cwist_metrics_registry_t * cwist_metrics_registry(void)
Obtain the global registry, creating it on first call.
Definition metrics.c:112
@ CWIST_METRIC_REQUESTS_TOTAL
Definition metrics.h:58
@ CWIST_METRIC_REQUEST_DURATION_NS
Definition metrics.h:59
struct cwist_metrics_registry cwist_metrics_registry_t
Global metrics registry (singleton, lazy-initialised).
Definition metrics.h:87
void cwist_metric_inc(cwist_metrics_registry_t *reg, cwist_metric_id_t id)
Increment a counter metric by 1.
Definition metrics.c:179
void cwist_metric_add(cwist_metrics_registry_t *reg, cwist_metric_id_t id, uintmax_t delta)
Increment a counter metric by delta.
Definition metrics.c:194
void cwist_mw_request_id_handler(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next)
Attach an X-Request-Id header to both the request and the response.
Definition middleware.c:55
struct cwist_jwt_ctx cwist_jwt_ctx_t
cwist_middleware_func cwist_mw_metrics(void)
Return the built-in metrics collection middleware.
Definition middleware.c:333
#define CWIST_RATE_LIMIT_DEFINE_WRAPPER(N)
Definition middleware.c:426
cwist_middleware_func cwist_mw_jwt_auth(const char *secret)
Register a JWT secret in a static slot and return the matching middleware wrapper.
Definition middleware.c:649
cwist_middleware_func cwist_mw_request_id(const char *header_name)
Return the built-in request ID middleware.
Definition middleware.c:79
const cwist_jwt_claims * cwist_mw_jwt_get_claims(const cwist_http_request *req)
Retrieve the claims the JWT middleware decoded for a request.
Definition middleware.c:687
cwist_middleware_func cwist_mw_cors(void)
Return the built-in permissive CORS middleware.
Definition middleware.c:525
void cwist_mw_cors_handler(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next)
Inject permissive CORS headers and short-circuit preflight requests.
Definition middleware.c:500
void cwist_mw_rate_limit_reset(void)
Reset all per-IP rate-limiter buckets, clearing tracked clients.
Definition middleware.c:450
#define CWIST_RATE_LIMIT_MAX_CFGS
Definition middleware.c:416
void cwist_mw_metrics_handler(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next)
Increment request counter and observe duration for Prometheus metrics.
Definition middleware.c:315
cwist_middleware_func cwist_mw_rate_limit_ip(int requests_per_minute)
Return the built-in per-IP rate-limiter middleware.
Definition middleware.c:467
cwist_middleware_func cwist_mw_access_log(cwist_log_format_t format)
Return the built-in access-log middleware.
Definition middleware.c:296
#define MAX_IP_BUCKETS
Definition middleware.c:347
cwist_log_format_t
Access log middleware output formats.
Definition middleware.h:16
@ CWIST_LOG_COMBINED
Definition middleware.h:16
@ CWIST_LOG_JSON
Definition middleware.h:16
@ CWIST_LOG_COMMON
Definition middleware.h:16
HTTP Protocol Definitions and Helpers.
cwist_sstring * cwist_get_client_ip_from_fd(int fd)
Resolve the peer IP address for a connected client socket.
Definition http.c:2141
size_t cap
Definition otel.c:56
Linked list for headers to handle multiple headers easily.
Definition http.h:59
cwist_sstring * body
Definition http.h:73
cwist_http_header_node * headers
Definition http.h:72
cwist_http_method_t method
Definition http.h:66
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
cwist_sstring * body
Definition http.h:87
cwist_http_status_t status_code
Definition http.h:84
cwist_http_header_node * headers
Definition http.h:86
size_t size
Definition sstring.h:20
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
void(* cwist_handler_func)(cwist_http_request *req, cwist_http_response *res)
Function pointer type for HTTP route handlers.
Definition app.h:32
void(* cwist_middleware_func)(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next)
Middleware type that receives req/res pair and the next stage in the chain.
Definition app.h:115
Built-in middleware factories.