cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
session.c
Go to the documentation of this file.
1
5#define _POSIX_C_SOURCE 200809L
9#include "async_gc.h"
10#include <cjson/cJSON.h>
11#include <cwist/sys/platform.h>
12#ifdef CWIST_BAREMETAL
13#include <cwist/sys/port.h>
14#endif
15#if !defined(CWIST_PLATFORM_HOSTED)
16/* WASM links no OpenSSL: verify cookie signatures with the bundled
17 * header-only SHA-256/HMAC instead. */
19#else
20#include <openssl/hmac.h>
21#include <openssl/evp.h>
22#include <openssl/crypto.h>
23#endif
24#include <string.h>
25#include <stdlib.h>
26#include <stdio.h>
27#include <time.h>
28#include <unistd.h>
29#include <fcntl.h>
30#ifdef __EMSCRIPTEN__
31#include <emscripten.h>
32#endif
33
34#define CWIST_SESSION_DEFAULT_NAME "cwist_session"
35#define CWIST_SESSION_DEFAULT_MAX_AGE 86400
36
37#ifdef __EMSCRIPTEN__
38/* Browser/WASM hosts have no /dev/urandom: draw the session secret from
39 * the host CSPRNG instead (called by generate_secret as a fallback).
40 * Production WASM apps should still pin a secret with
41 * cwist_app_use_session(app, secret) stored in host-side persistence - a
42 * fresh random secret per instance invalidates every existing session
43 * whenever the host drops the WASM instance. */
44EM_JS(int, cwist_session_entropy_js, (char *hex_out, int nbytes), {
45 // clang-format off - JS body, not C: keep make format away from it
46 try {
47 var bytes = new Uint8Array(nbytes);
48 globalThis.crypto.getRandomValues(bytes);
49 var hex = "0123456789abcdef";
50 for (var i = 0; i < nbytes; i++) {
51 HEAPU8[hex_out + 2 * i] = hex.charCodeAt(bytes[i] >> 4);
52 HEAPU8[hex_out + 2 * i + 1] = hex.charCodeAt(bytes[i] & 15);
53 }
54 HEAPU8[hex_out + 2 * nbytes] = 0;
55 return 1;
56 } catch (e) {
57 return 0;
58 }
59 // clang-format on
60});
61#endif
62
63struct cwist_session {
64 cwist_app *app;
66 cwist_query_map *data;
67 bool modified;
68 bool invalidated;
69 bool loaded;
70};
71
72/* --- Base64 helpers (RFC 4648) ------------------------------------------ */
73
74static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
75
83static char *session_base64_encode(const uint8_t *data, size_t len) {
84 size_t out_len = 4 * ((len + 2) / 3);
85 char *out = cwist_alloc(out_len + 1);
86 if (!out) return NULL;
87 for (size_t i = 0, j = 0; i < len; i += 3, j += 4) {
88 uint32_t v = ((uint32_t)data[i]) << 16;
89 if (i + 1 < len) v |= ((uint32_t)data[i + 1]) << 8;
90 if (i + 2 < len) v |= ((uint32_t)data[i + 2]);
91 out[j] = b64[(v >> 18) & 0x3F];
92 out[j + 1] = b64[(v >> 12) & 0x3F];
93 out[j + 2] = (i + 1 < len) ? b64[(v >> 6) & 0x3F] : '=';
94 out[j + 3] = (i + 2 < len) ? b64[v & 0x3F] : '=';
95 }
96 out[out_len] = '\0';
97 return out;
98}
99
109static int base64_decode(const char *in, uint8_t *out, size_t out_len) {
110 size_t in_len = strlen(in);
111 if (in_len % 4 != 0) return -1;
112 size_t pad = 0;
113 if (in_len > 0 && in[in_len - 1] == '=') pad++;
114 if (in_len > 1 && in[in_len - 2] == '=') pad++;
115 size_t len = (in_len / 4) * 3 - pad;
116 if (len > out_len) return -1;
117
118 int decode[256];
119 for (int i = 0; i < 256; i++) decode[i] = -1;
120 for (int i = 0; i < 64; i++) decode[(unsigned char)b64[i]] = i;
121
122 size_t j = 0;
123 for (size_t i = 0; i < in_len; i += 4) {
124 int a = decode[(unsigned char)in[i]];
125 int b = decode[(unsigned char)in[i + 1]];
126 int c = decode[(unsigned char)in[i + 2]];
127 int d = decode[(unsigned char)in[i + 3]];
128 if (a < 0 || b < 0) return -1;
129 uint32_t v = ((uint32_t)a << 18) | ((uint32_t)b << 12);
130 if (c >= 0) v |= ((uint32_t)c << 6);
131 if (d >= 0) v |= (uint32_t)d;
132 out[j++] = (v >> 16) & 0xFF;
133 if (c >= 0 && in[i + 2] != '=') out[j++] = (v >> 8) & 0xFF;
134 if (d >= 0 && in[i + 3] != '=') out[j++] = v & 0xFF;
135 }
136 return (int)j;
137}
138
139/* --- HMAC-SHA256 -------------------------------------------------------- */
140
150static bool session_hmac_sha256(const char *key, size_t key_len, const char *msg, size_t msg_len,
151 uint8_t out[32]) {
152#if !defined(CWIST_PLATFORM_HOSTED)
153 return cwist_hmac_sha256((const uint8_t *)key, key_len, (const uint8_t *)msg, msg_len, out);
154#else
155 unsigned int len = 32;
156 unsigned char *r =
157 HMAC(EVP_sha256(), key, (int)key_len, (const unsigned char *)msg, msg_len, out, &len);
158 return r != NULL && len == 32;
159#endif
160}
161
162/* --- Secret management -------------------------------------------------- */
163
173static char *generate_secret(size_t len) {
174 char *secret = cwist_alloc(len * 2 + 1);
175 if (!secret) return NULL;
176 int fd = open("/dev/urandom", O_RDONLY);
177 if (fd >= 0) {
178 unsigned char *buf CWIST_DEFER_FREE = cwist_alloc(len);
179 if (!buf) {
180 close(fd);
182 return NULL;
183 }
184 ssize_t n = read(fd, buf, len);
185 close(fd);
186 if (n == (ssize_t)len) {
187 static const char hex[] = "0123456789abcdef";
188 for (size_t i = 0; i < len; i++) {
189 secret[i * 2] = hex[buf[i] >> 4];
190 secret[i * 2 + 1] = hex[buf[i] & 0x0F];
191 }
192 secret[len * 2] = '\0';
193 return secret;
194 }
195 }
196#ifdef __wasi__
197 /* WASI preview1 has no /dev/urandom preopened by default, but
198 * wasi-libc maps getentropy() onto __wasi_random_get. */
199 unsigned char *wbuf CWIST_DEFER_FREE = cwist_alloc(len);
200 if (wbuf && getentropy(wbuf, len) == 0) {
201 static const char hex[] = "0123456789abcdef";
202 for (size_t i = 0; i < len; i++) {
203 secret[i * 2] = hex[wbuf[i] >> 4];
204 secret[i * 2 + 1] = hex[wbuf[i] & 0x0F];
205 }
206 secret[len * 2] = '\0';
207 return secret;
208 }
209#endif
210#ifdef CWIST_BAREMETAL
211 unsigned char *pbuf CWIST_DEFER_FREE = cwist_alloc(len);
212 if (pbuf && cwist_port_entropy(pbuf, len) == 0) {
213 static const char hex[] = "0123456789abcdef";
214 for (size_t i = 0; i < len; i++) {
215 secret[i * 2] = hex[pbuf[i] >> 4];
216 secret[i * 2 + 1] = hex[pbuf[i] & 0x0F];
217 }
218 secret[len * 2] = '\0';
219 return secret;
220 }
221#endif
222#ifdef __EMSCRIPTEN__
223 if (fd < 0 && cwist_session_entropy_js(secret, (int)len) == 1) return secret;
224#endif
226 return NULL;
227}
228
239int cwist_app_use_session(cwist_app *app, const char *secret) {
240 if (!app) return -1;
242 if (secret && *secret) {
244 } else {
245 app->session_secret = generate_secret(32);
246 }
247 if (!app->session_secret) return -1;
250 return 0;
251}
252
259void cwist_app_set_session_name(cwist_app *app, const char *name) {
260 if (!app || !name) return;
261 /* Keep the current name if the copy fails rather than leave it NULL. */
262 char *copy = cwist_strdup(name);
263 if (!copy) return;
264 if (app->session_name) cwist_free(app->session_name);
265 app->session_name = copy;
266}
267
275 if (!app || seconds <= 0) return;
277}
278
279/* --- JSON serialization helpers ----------------------------------------- */
280
281typedef struct {
282 cJSON *json;
283} json_ctx_t;
284
292static void add_to_json(const char *key, const char *value, void *ctx) {
293 json_ctx_t *jctx = ctx;
294 cJSON_AddStringToObject(jctx->json, key, value ? value : "");
295}
296
297/* --- Session load/save -------------------------------------------------- */
298
308static bool verify_signature(cwist_app *app, const char *payload_b64, const char *sig_b64) {
309 uint8_t sig[32];
310 if (base64_decode(sig_b64, sig, sizeof(sig)) != 32) return false;
311 uint8_t expected[32];
312 if (!session_hmac_sha256(app->session_secret, strlen(app->session_secret), payload_b64,
314 return false;
315 }
316#if !defined(CWIST_PLATFORM_HOSTED)
317 /* No OpenSSL on WASM: use a portable constant-time loop. */
318 unsigned int diff = 0;
319 for (size_t i = 0; i < 32; i++) diff |= sig[i] ^ expected[i];
320 return diff == 0;
321#else
322 return CRYPTO_memcmp(sig, expected, 32) == 0;
323#endif
324}
325
333static cwist_query_map *parse_payload(const char *payload_b64) {
334 uint8_t payload[8192];
335 int len = base64_decode(payload_b64, payload, sizeof(payload) - 1);
336 if (len < 0) return NULL;
337 payload[len] = '\0';
338 cJSON *json = cJSON_Parse((const char *)payload);
339 if (!json) return NULL;
341 if (!map) {
342 cJSON_Delete(json);
343 return NULL;
344 }
345 cJSON *item = NULL;
346 cJSON_ArrayForEach(item, json) {
347 if (cJSON_IsString(item) && item->string && item->valuestring) {
348 cwist_query_map_set(map, item->string, item->valuestring);
349 }
350 }
351 cJSON_Delete(json);
352 return map;
353}
354
367 cwist_http_response *res) {
368 if (!app || !req || !res) return NULL;
369 if (!app->session_secret && cwist_app_use_session(app, NULL) != 0) return NULL;
370
371 cwist_session_t *session = cwist_alloc(sizeof(*session));
372 if (!session) return NULL;
373 session->app = app;
374 session->req = req;
375 session->data = cwist_query_map_create();
376 session->modified = false;
377 session->invalidated = false;
378 session->loaded = false;
379
381 if (cookies) {
382 const char *cookie_header = cwist_http_header_get(req->headers, "Cookie");
384 const char *raw = cwist_cookie_get(cookies, app->session_name);
385 if (raw) {
386 /* On OOM the cookie is treated as absent: a fresh session. */
387 char *copy = cwist_strdup(raw);
388 char *dot = copy ? strchr(copy, '.') : NULL;
389 if (dot) {
390 *dot = '\0';
391 if (verify_signature(app, copy, dot + 1)) {
392 cwist_query_map_destroy(session->data);
393 session->data = parse_payload(copy);
394 if (!session->data) session->data = cwist_query_map_create();
395 session->loaded = true;
396 }
397 }
398 cwist_free(copy);
399 }
401 }
402
403 req->session = session;
404 return session;
405}
406
414const char *cwist_session_get(cwist_session_t *session, const char *key) {
415 if (!session || !session->data || !key) return NULL;
416 return cwist_query_map_get(session->data, key);
417}
418
428int cwist_session_set(cwist_session_t *session, const char *key, const char *value) {
429 if (!session || !key) return -1;
430 if (!session->data) return -1;
431 cwist_query_map_set(session->data, key, value ? value : "");
432 session->modified = true;
433 return 0;
434}
435
443void cwist_session_delete(cwist_session_t *session, const char *key) {
444 if (!session || !session->data || !key) return;
445 cwist_query_map_delete(session->data, key);
446 session->modified = true;
447}
448
455 if (!session) return;
456 session->invalidated = true;
457 session->modified = true;
458}
459
465 if (!session) return;
466 cwist_query_map_destroy(session->data);
467 cwist_free(session);
468}
469
471 if (!session || !cwist_full_gc_enabled()) return;
472 cwist_http_async_disown_map(session->data);
473 cwist_gc_scope_disown(session);
474 /* app and req are backreferences, not session-owned allocations. */
475}
476
478 if (!session || !res) return -1;
479 if (!session->modified && session->loaded) return 0;
480
481 if (session->invalidated) {
482 cwist_cookie_delete(res, session->app->session_name);
483 return 0;
484 }
485
486 json_ctx_t ctx;
487 ctx.json = cJSON_CreateObject();
488 if (!ctx.json) return -1;
489 cwist_query_map_foreach(session->data, add_to_json, &ctx);
490 char *raw = cJSON_PrintUnformatted(ctx.json);
491 cJSON_Delete(ctx.json);
492 if (!raw) return -1;
493
494 char *payload_b64 = session_base64_encode((const uint8_t *)raw, strlen(raw));
495 free(raw);
496 if (!payload_b64) return -1;
497
498 uint8_t sig[32];
499 if (!session_hmac_sha256(session->app->session_secret, strlen(session->app->session_secret),
502 return -1;
503 }
504 char *sig_b64 = session_base64_encode(sig, sizeof(sig));
505 if (!sig_b64) {
507 return -1;
508 }
509
510 char cookie_value[16384];
514
515 cwist_cookie_options opts = {0};
516 opts.path = "/";
517 opts.max_age_seconds = session->app->session_max_age;
518 opts.http_only = true;
519 opts.same_site = "Lax";
520
521 int rc = cwist_cookie_set(res, session->app->session_name, cookie_value, &opts);
522 return rc;
523}
524
525/* Middleware ------------------------------------------------------------- */
526
535static void cwist_mw_session_handler(cwist_http_request *req, cwist_http_response *res,
536 cwist_handler_func next) {
537 cwist_app *app = req->app;
538 if (!app) {
539 next(req, res);
540 return;
541 }
542 cwist_session_start(app, req, res);
543 next(req, res);
544 if (req->session) {
545 cwist_session_commit(req->session, res);
546 }
547}
548
557 if (app && !app->session_secret) {
559 }
560 CWIST_UNUSED(app);
561 return cwist_mw_session_handler;
562}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
#define CWIST_DEFER_FREE
Attach to a pointer variable's declaration to cwist_free() it automatically when that variable's encl...
Definition alloc.h:101
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
HTTP cookie parsing and construction helpers.
const char * cwist_cookie_get(cwist_query_map *map, const char *name)
Get a cookie value by name from a parsed map.
Definition cookie.c:114
int cwist_cookie_set(cwist_http_response *res, const char *name, const char *value, const cwist_cookie_options *opts)
Add a Set-Cookie header to the response.
Definition cookie.c:119
int cwist_cookie_delete(cwist_http_response *res, const char *name)
Add a Set-Cookie header that deletes a cookie.
Definition cookie.c:167
void cwist_cookie_parse(cwist_query_map *map, const char *header)
Parse a Cookie header value into a query-style map.
Definition cookie.c:80
#define CWIST_UNUSED(x)
Mark an intentionally unused variable to silence compiler warnings.
Definition macros.h:23
mbedTLS TLS backend: OPENSSL_cleanse / CRYPTO_memcmp.
#define CRYPTO_memcmp
Definition crypto.h:17
mbedTLS TLS backend: EVP subset (HMAC digest tags, AES-256-CBC).
#define EVP_sha256
Definition evp.h:25
bool cwist_gc_scope_disown(void *ptr)
Remove ptr from the current thread's pending-sweep list without freeing it, because ownership is bein...
Definition gc.c:766
bool cwist_full_gc_enabled(void)
Check whether full-GC mode is currently enabled.
Definition gc.c:515
mbedTLS TLS backend: HMAC subset (HMAC-SHA256, one-shot + streaming).
#define HMAC
Definition hmac.h:21
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value by key.
Definition http.c:1880
void cwist_query_map_delete(cwist_query_map *map, const char *key)
Delete a key from the map.
Definition query.c:220
const char * cwist_query_map_get(cwist_query_map *map, const char *key)
Get a value by key.
Definition query.c:196
void cwist_query_map_set(cwist_query_map *map, const char *key, const char *value)
Set a key-value pair.
Definition query.c:146
void cwist_query_map_foreach(cwist_query_map *map, cwist_query_map_iter_func cb, void *ctx)
Iterate over all key-value pairs in the map.
Definition query.c:252
cwist_query_map * cwist_query_map_create(void)
Create a new query map.
Definition query.c:54
void cwist_query_map_destroy(cwist_query_map *map)
Destroy a query map.
Definition query.c:81
size_t cap
Definition otel.c:56
Target capability macros shared by all CWIST sources.
Hooks a bare-metal firmware supplies to CWIST (issue #447).
int cwist_port_entropy(void *buf, size_t len)
Fill a buffer with cryptographically secure random bytes.
cwist_session_t * cwist_session_start(cwist_app *app, cwist_http_request *req, cwist_http_response *res)
Create a session for a request, loading data from the signed session cookie if present and valid....
Definition session.c:366
void cwist_session_invalidate(cwist_session_t *session)
Invalidate the session: the session cookie is expired on the next commit. Marks the session as modifi...
Definition session.c:454
void cwist_session_delete(cwist_session_t *session, const char *key)
Delete a key from the session. Marks the session as modified so the change is written back on commit....
Definition session.c:443
#define CWIST_SESSION_DEFAULT_NAME
Definition session.c:34
void cwist_app_set_session_max_age(cwist_app *app, int seconds)
Set the session cookie lifetime in seconds. No-op if app is NULL or seconds is not positive.
Definition session.c:274
void cwist_app_set_session_name(cwist_app *app, const char *name)
Set the session cookie name for an app. Replaces any previously set name. No-op if app or name is NUL...
Definition session.c:259
int cwist_app_use_session(cwist_app *app, const char *secret)
Enable signed session cookies for an app. Installs or replaces the app's session secret....
Definition session.c:239
const char * cwist_session_get(cwist_session_t *session, const char *key)
Get a session value by key.
Definition session.c:414
int cwist_session_set(cwist_session_t *session, const char *key, const char *value)
Set a session value; NULL value stores an empty string. Marks the session as modified so it is writte...
Definition session.c:428
cwist_middleware_func cwist_mw_session(cwist_app *app)
Create the session middleware for an app. Ensures the app has a session secret, generating a random o...
Definition session.c:556
#define CWIST_SESSION_DEFAULT_MAX_AGE
Definition session.c:35
void cwist_session_destroy(cwist_session_t *session)
Free a session and its data. Safe to call with NULL.
Definition session.c:464
void cwist_http_async_disown_session(cwist_session_t *session)
Definition session.c:470
int cwist_session_commit(cwist_session_t *session, cwist_http_response *res)
Persist the session to the response cookie.
Definition session.c:477
Signed client-side session cookie API.
struct cwist_session cwist_session_t
Opaque session handle attached to a request.
Definition session.h:21
Minimal self-contained SHA-256 and HMAC-SHA256 (RFC 4634 style).
Main Application Context.
Definition app.h:155
int session_max_age
Session cookie max-age in seconds.
Definition app.h:207
char * session_secret
Session signing secret (HMAC-SHA256).
Definition app.h:203
char * session_name
Session cookie name.
Definition app.h:205
struct cwist_app * app
Owning app context (if any).
Definition http.h:176
void * session
cwist_session_t pointer set by session middleware.
Definition http.h:185
cwist_http_header_node * headers
Definition http.h:72
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
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