ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
middleware.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file middleware.cpp
6 * @brief Middleware chain + built-in fixed-window rate limiter for PC.
7 *
8 * Split out of protocore.cpp (single-purpose server files). use() registers a middleware;
9 * run_middleware() runs the chain in order (first MW_HALT stops dispatch); enable_rate_limit()
10 * / rate_limit_check() implement a rollover-safe fixed-window counter that answers 429 +
11 * Retry-After past the budget. All four are PC methods over member state - a pure move.
12 */
13
14#include "protocore.h"
15#include "shared_primitives/mime.h" // PC_MIME_TEXT_PLAIN
16#include "shared_primitives/strbuf.h" // pc_sb frame builder
17#include <stdio.h>
18
19// ---------------------------------------------------------------------------
20// Middleware chain + built-in rate limiter
21// ---------------------------------------------------------------------------
22
24{
25 if (mw == nullptr || _middleware_count >= MAX_MIDDLEWARE)
26 {
27 return;
28 }
29 _middleware[_middleware_count++] = mw;
30}
31
32// Run the chain in registration order. The first middleware to return MwResult::MW_HALT
33// stops dispatch; it is responsible for having sent a response.
34bool PC::run_middleware(uint8_t slot_id, HttpReq *req)
35{
36 for (uint8_t i = 0; i < _middleware_count; i++)
37 {
38 // _middleware[i] is never null here: use() is the only writer, and it always stores a
39 // non-null entry together with the count increment that admits index i - a slot below
40 // _middleware_count can never regress to null.
41 if (_middleware[i] && _middleware[i](slot_id, req) == MwResult::MW_HALT) // GCOVR_EXCL_BR_LINE
42 {
43 return true;
44 }
45 }
46 return false;
47}
48
49void PC::enable_rate_limit(uint16_t max_requests, uint32_t window_ms)
50{
51 _rl_max = max_requests;
52 _rl_window_ms = window_ms;
53 _rl_window_start = millis();
54 _rl_count = 0;
55}
56
57// Fixed-window counter. Unsigned subtraction is rollover-safe across the millis()
58// wrap. On the request that tips past _rl_max, reply 429 + Retry-After and stop.
59bool PC::rate_limit_check(uint8_t slot_id)
60{
61 if (_rl_max == 0 || _rl_window_ms == 0)
62 {
63 return false; // disabled
64 }
65
66 uint32_t now = millis();
67 if ((uint32_t)(now - _rl_window_start) >= _rl_window_ms)
68 {
69 _rl_window_start = now; // new window
70 _rl_count = 0;
71 }
72
73 _rl_count++;
74 if (_rl_count <= _rl_max)
75 {
76 return false; // within budget
77 }
78
79 // Over budget: advertise how long until the window resets, then 429.
80 uint32_t elapsed = (uint32_t)(now - _rl_window_start);
81 // The ":0" arm is unreachable: the check above either just reset _rl_window_start to `now`
82 // (elapsed == 0) or left it in place because elapsed was already < _rl_window_ms - either way
83 // elapsed < _rl_window_ms always holds here, so the ">" arm always taken.
84 uint32_t remain_ms = (_rl_window_ms > elapsed) ? (_rl_window_ms - elapsed) : 0; // GCOVR_EXCL_BR_LINE
85 char secs[12];
86 pc_sb sb_secs = {secs, sizeof(secs), 0, true};
87 pc_sb_u32(&sb_secs, (uint32_t)((unsigned long)((remain_ms + 999) / 1000)));
88 if (pc_sb_finish(&sb_secs) == 0)
89 {
90 secs[0] = '\0';
91 }
92 add_response_header(slot_id, "Retry-After", secs);
93 send(slot_id, 429, PC_MIME_TEXT_PLAIN, "Too Many Requests");
94 return true;
95}
void enable_rate_limit(uint16_t max_requests, uint32_t window_ms)
Enable a built-in fixed-window request rate limiter.
void use(Middleware mw)
Register a middleware to run before every request is dispatched.
void add_response_header(uint8_t slot_id, const char *name, const char *value)
Queue a custom response header for the next send on this slot.
Definition response.cpp:336
void send(uint8_t slot_id, int code, const char *content_type, const char *payload)
Send an HTTP response with a body and close the connection.
Shared HTTP Content-Type ("MIME") string constants (one source of truth).
Layer 7 (Application) - public HTTP routing API.
MwResult(* Middleware)(uint8_t slot_id, HttpReq *request)
Composable pre-dispatch middleware (see PC::use()).
Definition protocore.h:158
@ MW_HALT
Stop dispatch; the middleware already sent a response.
#define MAX_MIDDLEWARE
Maximum globally-registered middleware functions.
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
Fully-parsed HTTP/1.1 request.
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30