27#include <esp_system.h>
36static void sha256_hex(
const uint8_t *data,
size_t len,
char out[65])
47static bool digest_field(
const char *hdr,
const char *key,
char *out,
size_t out_size)
49 size_t klen = strnlen(key, 32);
51 while ((p = strstr(p, key)) !=
nullptr)
53 bool left_ok = (p == hdr) || p[-1] ==
' ' || p[-1] ==
',';
54 const char *after = p + klen;
55 if (!left_ok || *after !=
'=')
76 while (*ve && *ve !=
',' && *ve !=
' ')
81 size_t vlen = (size_t)(ve - vs);
82 if (vlen > out_size - 1)
86 memcpy(out, vs, vlen);
93void PC::regen_digest_secret()
100 static uint32_t counter = 0;
103 for (
int i = 0; i < 4; i++)
105 uint32_t r = esp_random();
106 memcpy(seed + i * 4, &r, 4);
108 uint32_t c = counter;
109 uint32_t t = (uint32_t)millis();
110 memcpy(seed + 16, &c, 4);
111 memcpy(seed + 20, &t, 4);
114 memcpy(_digest_secret, d,
sizeof(_digest_secret));
122static uint32_t digest_nonce_mac(
const uint8_t *secret, uint32_t issue,
char *mac_hex)
124 uint8_t material[20];
125 memcpy(material, secret, 16);
126 memcpy(material + 16, &issue, 4);
128 pc_sha256(material,
sizeof(material), d);
133void PC::make_digest_nonce(
char *out,
size_t cap)
139 digest_nonce_mac(_digest_secret, issue, mac_hex);
140 pc_sb sb_out = {out, cap, 0,
true};
150bool PC::verify_digest_nonce(
const char *nonce,
bool *expired)
154 if (strnlen(nonce, 42) != 8 + 1 + 32 || nonce[8] !=
'.')
164 digest_nonce_mac(_digest_secret, issue, mac_hex);
167 const char *got = nonce + 9;
169 for (
int i = 0; i < 32; i++)
171 diff |= (uint8_t)(mac_hex[i] ^ got[i]);
182void PC::send_unauth(uint8_t slot_id,
const Route *r,
bool stale)
184 if (!pc_conn_active(slot_id))
198 make_digest_nonce(nonce,
sizeof(nonce));
199 pc_sb sb_challenge = {challenge,
sizeof(challenge), 0,
true};
200 pc_sb_put(&sb_challenge,
"WWW-Authenticate: Digest realm=\"");
202 pc_sb_put(&sb_challenge,
"\", qop=\"auth\", algorithm=SHA-256, nonce=\"");
205 pc_sb_put(&sb_challenge, stale ?
", stale=true" :
"");
214 pc_sb sb_challenge2 = {challenge,
sizeof(challenge), 0,
true};
215 pc_sb_put(&sb_challenge2,
"WWW-Authenticate: Basic realm=\"");
216 pc_sb_put(&sb_challenge2, r->auth_realm);
225 const char *cl = pc_resp_conn_hdr(slot_id, &keep);
227 static const char body[] =
"Unauthorized";
229 pc_sb sb_header = {header,
sizeof(header), 0,
true};
230 pc_sb_put(&sb_header,
"HTTP/1.1 401 Unauthorized\r\n");
232 pc_sb_put(&sb_header,
"Content-Type: text/plain\r\nContent-Length: ");
233 pc_sb_i64(&sb_header, (int64_t)((
int)(
sizeof(body) - 1)));
235 pc_sb_put(&sb_header, _cors_enabled ? _cors_header_buf :
"");
252 pc_resp_end(slot_id, 401, (
int)(
sizeof(body) - 1), keep,
true);
258static bool ct_equal(
const void *a,
const void *b,
size_t len)
260 const uint8_t *pa = (
const uint8_t *)a;
261 const uint8_t *pb = (
const uint8_t *)b;
263 for (
size_t i = 0; i < len; i++)
265 diff |= (uint8_t)(pa[i] ^ pb[i]);
270bool PC::check_basic_auth(uint8_t ,
HttpReq *req,
const Route *r)
273 if (!auth_hdr || strncmp(auth_hdr,
"Basic ", 6) != 0)
288 const char *colon = (
const char *)memchr(decoded,
':', n);
294 size_t ulen = (size_t)(colon - (
const char *)decoded);
295 const char *pass = colon + 1;
296 size_t plen = n - (size_t)(pass - (
const char *)decoded);
301 bool user_ok = (ulen == strnlen(r->auth_user,
MAX_AUTH_LEN)) && ct_equal(decoded, r->auth_user, ulen);
302 bool pass_ok = (plen == strnlen(r->auth_pass,
MAX_AUTH_LEN)) && ct_equal(pass, r->auth_pass, plen);
303 return user_ok && pass_ok;
309bool PC::check_digest_auth(uint8_t ,
HttpReq *req,
const Route *r,
bool *stale)
313 const char *hdr = req->authorization;
314 if (strncmp(hdr,
"Digest ", 7) != 0)
318 const char *d = hdr + 7;
328 if (!digest_field(d,
"username", username,
sizeof(username)) || !digest_field(d,
"nonce", nonce,
sizeof(nonce)) ||
329 !digest_field(d,
"uri", uri,
sizeof(uri)) || !digest_field(d,
"qop", qop,
sizeof(qop)) ||
330 !digest_field(d,
"nc", nc,
sizeof(nc)) || !digest_field(d,
"cnonce", cnonce,
sizeof(cnonce)) ||
331 !digest_field(d,
"response", response,
sizeof(response)))
337 if (strcmp(username, r->auth_user) != 0)
344 bool nonce_expired =
false;
345 if (!verify_digest_nonce(nonce, &nonce_expired))
349 if (strcmp(qop,
"auth") != 0)
360 pc_sb sb_target = {target,
sizeof(target), 0,
true};
371 pc_sb sb_target2 = {target,
sizeof(target), 0,
true};
378 if (strcmp(uri, target) != 0)
388 pc_sb sb_tmp = {tmp,
sizeof(tmp), 0,
true};
395 sha256_hex((
const uint8_t *)tmp, (
size_t)n, ha1);
397 char tmp2[
sizeof(uri) + 16];
398 pc_sb sb_tmp2 = {tmp2,
sizeof(tmp2), 0,
true};
403 sha256_hex((
const uint8_t *)tmp2, (
size_t)n, ha2);
405 char tmp3[65 + 48 + 16 + 64 + 8 + 65 + 8];
406 pc_sb sb_tmp3 = {tmp3,
sizeof(tmp3), 0,
true};
419 sha256_hex((
const uint8_t *)tmp3, (
size_t)n, expected);
421 if (strcasecmp(expected, response) != 0)
size_t pc_base64_decode(const char *src, uint8_t *dst, size_t dst_cap)
Decode a null-terminated Base64 string.
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Base-16 conversion between raw bytes and their ASCII digits.
void pc_hex_encode(const uint8_t *in, uint32_t n, char *out, bool upper=false)
Write n bytes of in into out as 2 * n hex characters plus a NUL.
int32_t pc_hex_decode(const char *in, uint32_t hexlen, uint8_t *out, uint32_t out_cap)
Read exactly hexlen hex characters from in into out as hexlen / 2 bytes.
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
void http_reset(uint8_t slot_id)
Reset the HTTP parser for a connection slot.
bool req_is_head(uint8_t slot_id)
True if the request in slot slot_id used the HEAD method (send headers, no body).
Layer 7 (Application) - public HTTP routing API.
#define MAX_QUERY_LEN
Maximum raw query-string length (everything after ?).
#define RESP_HDR_BUF_SIZE
Stack buffer for HTTP response header lines in send() / send_empty() / send_unauth() / serve_file().
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define MAX_AUTH_LEN
Maximum username or password length for HTTP Basic Authentication.
#define PC_DIGEST_NONCE_LIFETIME_MS
Lifetime of a Digest nonce, in milliseconds (default 5 minutes).
Library-private declarations shared between protocore.cpp and the src/server/*.cpp request-handler tr...
void pc_sha256(const uint8_t *data, size_t len, uint8_t digest[PC_SHA256_DIGEST_LEN])
One-shot SHA-256: hash len bytes of data into digest (32 bytes).
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
#define PC_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
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.
void pc_sb_i64(pc_sb *b, int64_t v)
Append v as signed decimal (64-bit), with a leading '-' when negative.
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Fully-parsed HTTP/1.1 request.
char query[MAX_QUERY_LEN]
Raw query string (after ?).
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
char method[PC_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
Internal route entry stored in the routing table.
Bump-append target; ok latches false once an append would overflow cap.
bool pc_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
bool pc_conn_send_flush(uint8_t slot, const void *data, u16_t len)
Send len bytes on slot and flush in a single tcpip_thread round-trip.
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.