cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
rate_limit_redis.c
Go to the documentation of this file.
1
4#define _POSIX_C_SOURCE 200809L
7#include <arpa/inet.h>
8#include <netinet/in.h>
9#include <pthread.h>
10#include <stdatomic.h>
11#include <errno.h>
12#include <unistd.h>
13#include <stdio.h>
14#include <string.h>
15#include <sys/socket.h>
16
17#define REDIS_RATE_SLOTS 8
18#define REDIS_RATE_PREFIX_MAX 128
19
20typedef struct {
22 int rpm;
23 unsigned timeout_ms;
24 bool fail_closed;
25 char prefix[REDIS_RATE_PREFIX_MAX + 1];
26} redis_rate_cfg;
27
28static redis_rate_cfg rate_cfgs[REDIS_RATE_SLOTS];
29static size_t rate_cfg_count;
30static atomic_long rate_warning_pids[REDIS_RATE_SLOTS];
31static pthread_mutex_t rate_cfg_mutex = PTHREAD_MUTEX_INITIALIZER;
32
33/* A server-side window avoids process clock skew. TTL is set in the same
34 * atomic operation as INCR and is not extended by later or denied requests.
35 * Repair a pre-existing counter without an expiry instead of leaking it. */
36static const char rate_script[] = "local n=redis.call('INCR',KEYS[1]);"
37 "if n==1 or redis.call('PTTL',KEYS[1])<0 then "
38 "redis.call('PEXPIRE',KEYS[1],60000);end;return n";
39
40static bool peer_ip(int fd, char ip[INET6_ADDRSTRLEN]) {
41 struct sockaddr_storage peer = {0};
42 socklen_t size = sizeof(peer);
43 if (getpeername(fd, (struct sockaddr *)&peer, &size) != 0) return false;
44 if (peer.ss_family == AF_INET && size >= sizeof(struct sockaddr_in)) {
45 return inet_ntop(AF_INET, &((struct sockaddr_in *)&peer)->sin_addr, ip, INET6_ADDRSTRLEN) !=
46 NULL;
47 }
48 if (peer.ss_family == AF_INET6 && size >= sizeof(struct sockaddr_in6)) {
49 struct in6_addr *addr = &((struct sockaddr_in6 *)&peer)->sin6_addr;
51 return inet_ntop(AF_INET, (const unsigned char *)addr + 12, ip, INET6_ADDRSTRLEN) !=
52 NULL;
53 }
55 }
56 return false;
57}
58
59static void backend_failure(const redis_rate_cfg *cfg, cwist_http_request *req,
61 size_t slot = (size_t)(cfg - rate_cfgs);
62 long pid = (long)getpid();
63 if (atomic_exchange_explicit(&rate_warning_pids[slot], pid, memory_order_relaxed) != pid) {
65 "[CWIST] Redis rate limiter backend error %d (%s); "
66 "check pool process ownership and Redis connectivity/permissions.\n",
67 code, cfg->fail_closed ? "fail-closed" : "fail-open");
68 }
69 if (!cfg->fail_closed) {
70 next(req, res);
71 return;
72 }
73 res->status_code = 503;
74 cwist_sstring_assign(res->body, "Rate limit backend unavailable");
75}
76
77static void redis_rate_handler(size_t slot, cwist_http_request *req, cwist_http_response *res,
78 cwist_handler_func next) {
79 const redis_rate_cfg *cfg = &rate_cfgs[slot];
80 char ip[INET6_ADDRSTRLEN];
81 if (!peer_ip(req->client_fd, ip)) {
82 backend_failure(cfg, req, res, next, -ENOTCONN);
83 return;
84 }
86 snprintf(key, sizeof(key), "%s%s", cfg->prefix, ip);
87 const void *args[] = {"EVAL", rate_script, "1", key};
88 size_t lengths[] = {4, sizeof(rate_script) - 1, 1, strlen(key)};
91 cfg->timeout_ms, &reply);
92 if (!cwist_error_is_ok(&err) || !reply || reply->type != ':' || reply->integer <= 0) {
93 int code = cwist_error_is_ok(&err) ? -EPROTO : err.error.err_i16;
96 backend_failure(cfg, req, res, next, code);
97 return;
98 }
99 bool allowed = reply->integer <= cfg->rpm;
101 if (allowed) {
102 next(req, res);
103 return;
104 }
105 res->status_code = 429;
106 cwist_sstring_assign(res->body, "Too Many Requests");
107 cwist_http_header_add(&res->headers, "Retry-After", "60");
108}
109
110#define REDIS_RATE_WRAPPER(N) \
111 static void redis_rate_wrap_##N(cwist_http_request *req, cwist_http_response *res, \
112 cwist_handler_func next) { \
113 redis_rate_handler(N, req, res, next); \
114 }
123
127};
128
130 if (!cfg || !cfg->pool || cfg->requests_per_minute <= 0 || cfg->timeout_ms > 60000) return NULL;
131 const char *prefix = cfg->key_prefix ? cfg->key_prefix : "cwist:rl:";
132 if (strnlen(prefix, REDIS_RATE_PREFIX_MAX + 1) > REDIS_RATE_PREFIX_MAX) return NULL;
133 unsigned timeout = cfg->timeout_ms ? cfg->timeout_ms : 50;
134 pthread_mutex_lock(&rate_cfg_mutex);
135 for (size_t i = 0; i < rate_cfg_count; ++i) {
136 const redis_rate_cfg *old = &rate_cfgs[i];
137 if (old->pool == cfg->pool && old->rpm == cfg->requests_per_minute &&
138 old->timeout_ms == timeout && old->fail_closed == cfg->fail_closed &&
139 strcmp(old->prefix, prefix) == 0) {
140 pthread_mutex_unlock(&rate_cfg_mutex);
141 return wrappers[i];
142 }
143 }
144 if (rate_cfg_count == REDIS_RATE_SLOTS) {
145 pthread_mutex_unlock(&rate_cfg_mutex);
146 return NULL;
147 }
148 size_t slot = rate_cfg_count++;
149 rate_cfgs[slot] = (redis_rate_cfg){.pool = cfg->pool,
150 .rpm = cfg->requests_per_minute,
151 .timeout_ms = timeout,
152 .fail_closed = cfg->fail_closed};
153 memcpy(rate_cfgs[slot].prefix, prefix, strlen(prefix) + 1);
154 pthread_mutex_unlock(&rate_cfg_mutex);
155 return wrappers[slot];
156}
Memory allocation wrappers backed by libttak.
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
struct cwist_redis_pool cwist_redis_pool_t
Redis connection pool handle.
Definition cwist_redis.h:24
cwist_error_t cwist_redis_pool_command_argv_reply_timeout(cwist_redis_pool_t *pool, size_t argc, const void *const *argv, const size_t *argv_lens, unsigned timeout_ms, cwist_redis_reply_t **reply)
Run one binary-safe pooled command under a single deadline.
void cwist_redis_reply_free(cwist_redis_reply_t *reply)
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
size_t cap
Definition otel.c:56
cwist_middleware_func cwist_mw_rate_limit_ip_redis(const cwist_rate_limit_redis_cfg_t *cfg)
Shared per-IP 60-second window using one atomic Redis Lua command.
#define REDIS_RATE_WRAPPER(N)
#define REDIS_RATE_PREFIX_MAX
#define REDIS_RATE_SLOTS
Integrated error structure used throughout the system.
Definition cwist_err.h:72
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
Redis-backed rate limiter configuration. Strings are copied.
Definition middleware.h:31
cwist_redis_pool_t * pool
Definition middleware.h:32
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.
void cwist_error_dispose(cwist_error_t *err)
Release resources owned by an error value. Currently only CWIST_ERR_JSON owns memory (the cJSON paylo...
Definition error.c:43