Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / src / common / c-sqlite3.c
Author[]Andy Green <andy@warmcat.com> 2026-08-03 14:25 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-08-03 14:28 UTC
Tree8a73440870327d1581e788f3626481c5a3485113   Raw Patch
 
ui12
ui12
diff --git a/assets/sai.js b/assets/sai.js index c696816..75dfba9 100644 --- a/assets/sai.js +++ b/assets/sai.js @@ -398,8 +398,7 @@ const SaiAuthState = { NOT_LOGGED_IN: 0, LOGGED_IN_NO_GRANT: 1, LOGGED_IN_GRANT_USER: 2, // < :2 - LOGGED_IN_GRANT_ADMIN: 3, // >= :2 - PENDING: 4 // backend login-status fetch in flight + LOGGED_IN_GRANT_ADMIN: 3 // >= :2 }; var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, auth_is_admin = 0, auth_grant_level = -1, auth_state = SaiAuthState.NOT_LOGGED_IN, exptimer, auth_user = "", @@ -3693,15 +3692,6 @@ function ws_open_sai() case "com.warmcat.sai.auth_state": console.log("Backend auth_state:", jso.auth_state); - /* - * 4 = PENDING: the backend hasn't resolved the login - * status yet (it fetches it from lws-login async at WS - * establish). Leave the UI as-is; a follow-up message - * will deliver the resolved state. - */ - if (jso.auth_state === 4) { - break; - } if (jso.auth_state === 3) { auth_state = SaiAuthState.LOGGED_IN_GRANT_ADMIN; auth_is_admin = 1; diff --git a/etc-sai-EXAMPLE/web/conf.d/unixskt b/etc-sai-EXAMPLE/web/conf.d/unixskt index e8f11d0..348638c 100644 --- a/etc-sai-EXAMPLE/web/conf.d/unixskt +++ b/etc-sai-EXAMPLE/web/conf.d/unixskt @@ -63,28 +63,28 @@ # "database": "/srv/sai/sai-master", - # sai takes its login state from the lws-login - # interceptor rather than validating any JWT itself. - # At WS establish it performs an internal GET of - # auth-status-url on self-address (its own listener), - # forwarding the browser's Cookie, so lws-login (which - # holds the JWK and grant name) can decide admin or - # not. Both default to the example deployment below. - # - # self-address: "+"-prefixed lws client address of - # sai's own listener; "+" selects a unix socket, the - # rest is the filesystem path. - # - # "self-address": "+/var/run/sai", - # "auth-status-url": "/sai/.lws-login-status", - # - # For the fetch to reach lws-login, the /sai mount - # must have lws-login wired as its interceptor with a - # pmo service-name binding the grant name (eg lws-sai) - # -- that pmo is the single place where the grant name - # lives. See protocol_lws_login.md for the mount - # wiring. Without lws-login, the fetch 404s and sai - # treats the user as not having admin rights. + # sai-web does NO auth of its own: no JWK, no JWT + # validation, no grant logic. It learns the login + # state from the x-lws-login-* headers the lws-login + # interceptor (running in the front-end lwsws + # process) injects and the reverse proxy forwards to + # us. So sai-web's /sai mount in the front-end must + # be a proxy mount with lws-login wired as its + # interceptor, and that interceptor's pmo is the one + # place the grant name (eg lws-sai) lives: + # + # front-end (lwsws) vhost: + # { "mountpoint": "/sai", + # "origin": "http://+/var/run/sai:/sai", + # "interceptor-path": "/lws-login-sai" }, + # { "mountpoint": "/lws-login-sai", + # "origin": "callback://lws-login", + # "protocol": "lws-login", + # "pmo": [{ "service-name": "lws-sai" }] } + # + # sai-web then reads x-lws-login-admin:0/1 at WS + # establish. Without the interceptor, sai-web sees no + # header and treats the user as not having admin rights. # template HTML to use for this vhost. You'd normally # copy this to gitohashi-vhostname.html and modify it diff --git a/src/web/w-comms.c b/src/web/w-comms.c index 3fbfd86..465de59 100644 --- a/src/web/w-comms.c +++ b/src/web/w-comms.c @@ -163,30 +163,12 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, vhd->context = lws_get_context(wsi); vhd->vhost = lws_get_vhost(wsi); - { - const struct lws_protocol_vhost_options *pvo = (const struct lws_protocol_vhost_options *)in; - - /* - * Defaults match the example deployment: sai listens on - * the unix socket /var/run/sai and lws-login serves the - * cooked login status on /sai/.lws-login-status. Both - * are overridable per-vhost. - */ - lws_strncpy(vhd->self_address, "+/var/run/sai", - sizeof(vhd->self_address)); - lws_strncpy(vhd->auth_status_url, "/sai/.lws-login-status", - sizeof(vhd->auth_status_url)); - - while (pvo) { - if (!strcmp(pvo->name, "self-address")) - lws_strncpy(vhd->self_address, pvo->value, - sizeof(vhd->self_address)); - if (!strcmp(pvo->name, "auth-status-url")) - lws_strncpy(vhd->auth_status_url, pvo->value, - sizeof(vhd->auth_status_url)); - pvo = pvo->next; - } - } + /* + * sai-web needs no auth-related pvos: it takes its login state + * from the x-lws-login-* headers the lws-login interceptor (in + * the front-end lwsws) injects and the proxy forwards. See + * LWS_CALLBACK_ESTABLISHED. + */ if (lws_pvo_get_str(in, "database", &vhd->sqlite3_path_lhs)) { lwsl_err("%s: database pvo required\n", __func__); @@ -581,19 +563,25 @@ http_resp: #endif /* - * sai takes its login state from lws-login rather than - * validating any JWT itself. Kick off an async internal - * GET of the cooked login status on our own vhost, - * forwarding the browser's Cookie so lws-login can decide. - * auth_state stays PENDING (=> not admin => fails closed at - * the action gate) until the fetch resolves and pushes the - * result to the browser. + * sai does no JWT/grant validation of its own. The lws-login + * interceptor (in the front-end lwsws process) authenticated + * this WS upgrade and stamped the cooked result as trusted + * headers (x-lws-login-admin: 0/1) which the proxy forwarded + * to us. Read the admin flag; everything else (the action gate + * at w-ws-browser.c) keys off auth_state. If the header is + * absent (no interceptor configured) we fail closed: not admin. */ - pss->auth_state = SAI_AUTH_STATE_PENDING; - if (saiw_auth_fetch_kick(vhd, pss, wsi)) - lwsl_wsi_err(wsi, "unable to start auth-status fetch"); - - lwsl_wsi_notice(wsi, "**** ESTABLISHED WS: auth fetch started (auth_state=%d pending)", (int)pss->auth_state); + { + char admin[8]; + int a = lws_hdr_custom_copy(wsi, admin, sizeof(admin), + "x-lws-login-admin:", 18); + + pss->auth_state = (a == 1 && admin[0] == '1') ? + SAI_AUTH_STATE_LOGGED_IN_GRANT_ADMIN : + SAI_AUTH_STATE_LOGGED_IN_NO_GRANT; + lwsl_wsi_notice(wsi, "ESTABLISHED WS: x-lws-login-admin=%c (auth_state=%d)", + a == 1 ? admin[0] : '-', (int)pss->auth_state); + } if (!memcmp((char *)start, "/sai", 4)) start += 4; @@ -678,25 +666,6 @@ http_resp: saiw_browser_state_changed(pss, 0); lws_dll2_remove(&pss->subs_list); lws_sul_cancel(&pss->sul_logcache); - lws_sul_cancel(&pss->sul_auth); - - /* - * Tear down any in-flight auth fetch: detach it from the pss - * so the client callback can no longer touch the pss, and - * close the client wsi so its CLOSED callback frees the - * pending struct promptly rather than lingering. - */ - if (pss->auth_pending) { - struct lws *cwsi = pss->auth_pending->wsi_client; - - pss->auth_pending->wsi_parent = NULL; - pss->auth_pending = NULL; - - if (cwsi) - lws_set_timeout(cwsi, - PENDING_TIMEOUT_KILLED_BY_PROXY_CLIENT_CLOSE, - LWS_TO_KILL_ASYNC); - } for (n = 0; n < 4; n++) { if (pss->last_bps[n]) @@ -800,254 +769,6 @@ try_to_reuse: return 0; } -/* - * Resolve auth_state from the accumulated .lws-login-status body and push it - * to the browser. Sets ->done so a late timeout/CLOSED can't double-resolve. - * Detaches the pending struct from the pss. The pending struct itself is - * owned by the client wsi (it is the client wsi's userdata) and is freed when - * the client wsi is destroyed -- NOT here -- so the caller may still touch ap - * after this returns; ap->wsi_parent is cleared so a later close callback is a - * no-op for the pss. - */ -static void -saiw_auth_pending_resolve(struct sai_auth_pending *ap, sai_auth_state_t state) -{ - struct pss *pss; - - if (!ap || ap->done) - return; - ap->done = 1; - - if (ap->wsi_parent) { - /* - * wsi_parent's per-session data is the pss; recover it from - * the wsi user_space. pss->auth_pending == ap is the live - * pointer check; if the pss was CLOSED it NULLed - * auth_pending and wsi_parent. - */ - pss = (struct pss *)lws_wsi_user(ap->wsi_parent); - if (pss && pss->auth_pending == ap) { - pss->auth_state = state; - saiw_browser_queue_auth_state(pss); - pss->auth_pending = NULL; - lws_sul_cancel(&pss->sul_auth); - } - } - - ap->wsi_parent = NULL; /* pss side is settled; don't touch it again */ -} - -/* - * Pull the small set of cooked fields we care about out of the body. The body - * is a flat object produced by lws-login's .lws-login-status handler, so - * lws_json_simple_find() is sufficient and avoids a full JSON parser. - */ -static sai_auth_state_t -saiw_auth_state_from_body(const char *body, int len) -{ - const char *v; - size_t alen; - - v = lws_json_simple_find(body, (size_t)len, "\"logged_in\":", &alen); - if (!v || alen != 1 || v[0] != '1') - return SAI_AUTH_STATE_NOT_LOGGED_IN; - - v = lws_json_simple_find(body, (size_t)len, "\"is_admin\":", &alen); - - return (v && alen == 1 && v[0] == '1') ? - SAI_AUTH_STATE_LOGGED_IN_GRANT_ADMIN : - SAI_AUTH_STATE_LOGGED_IN_NO_GRANT; -} - -/* - * SUL timeout: if the fetch hasn't resolved in time, resolve it as no-grant - * (closed) so the browser isn't left in PENDING forever, then close the - * client wsi (whose CLOSED callback frees the pending struct). - */ -static void -saiw_auth_sul_cb(lws_sorted_usec_list_t *sul) -{ - struct pss *pss = lws_container_of(sul, struct pss, sul_auth); - struct sai_auth_pending *ap; - struct lws *cwsi; - - if (!pss) - return; - - ap = pss->auth_pending; - if (!ap) - return; - - lwsl_wsi_err(pss->wsi, "auth-status fetch timed out"); - - cwsi = ap->wsi_client; /* capture before resolve clears wsi_parent */ - saiw_auth_pending_resolve(ap, SAI_AUTH_STATE_LOGGED_IN_NO_GRANT); - - if (cwsi) /* tearing down the client wsi frees ap via CLOSED */ - lws_set_timeout(cwsi, PENDING_TIMEOUT_KILLED_BY_PROXY_CLIENT_CLOSE, - LWS_TO_KILL_ASYNC); -} - -int -saiw_auth_fetch_kick(struct vhd *vhd, struct pss *pss, struct lws *wsi) -{ - struct sai_auth_pending *ap; - struct lws_client_connect_info i; - int n; - - ap = malloc(sizeof(*ap)); - if (!ap) - return 1; - memset(ap, 0, sizeof(*ap)); - ap->wsi_parent = wsi; - - /* - * Copy the browser's raw Cookie header verbatim; we forward it to - * lws-login so IT can validate the session. sai never inspects it. - */ - n = lws_hdr_copy(wsi, ap->cookie_hdr, sizeof(ap->cookie_hdr), - WSI_TOKEN_HTTP_COOKIE); - if (n < 0) - ap->cookie_hdr[0] = '\0'; - - pss->auth_pending = ap; - - lws_sul_schedule(vhd->context, 0, &pss->sul_auth, saiw_auth_sul_cb, - 5 * LWS_US_PER_SEC); - - memset(&i, 0, sizeof(i)); - i.context = vhd->context; - /* - * Bind the client wsi to sai's own vhost explicitly: the protocol - * lookup (lws_vhost_name_to_protocol) runs against wsi->a.vhost, so we - * must use a vhost whose protocol list contains sai_internal_http_client - * (sai registers it on every vhost via pprotocols). Without this lws - * falls back to the "default" / first vhost, which may lack the protocol - * and fall back to the dummy http protocol. - */ - i.vhost = vhd->vhost; - i.address = vhd->self_address; /* "+"-prefixed unix path */ - i.port = 0; /* ignored on the '+' path */ - i.ssl_connection = 0; - i.path = vhd->auth_status_url; - i.host = "localhost"; - i.origin = i.host; - i.method = "GET"; - i.protocol = "sai_internal_http_client"; - i.userdata = ap; /* lws_wsi_user() on client */ - i.pwsi = &ap->wsi_client; - - /* - * lws_client_connect_via_info() returns the new client wsi on success - * (non-NULL) or NULL on synchronous failure. - */ - if (lws_client_connect_via_info(&i)) - return 0; /* connect started: ap now owned by the client wsi */ - - /* synchronous failure to even start: clean up ourselves */ - lws_sul_cancel(&pss->sul_auth); - pss->auth_pending = NULL; - free(ap); - - return 1; -} - -/* - * Internal HTTP client protocol: drives the auth-status GET. Has no - * per-session data of its own; its ->userdata is the struct sai_auth_pending - * set up at connect time. - */ -static int -callback_sai_internal_http_client(struct lws *wsi, - enum lws_callback_reasons reason, - void *user, void *in, size_t len) -{ - struct sai_auth_pending *ap = (struct sai_auth_pending *)lws_wsi_user(wsi); - - switch (reason) { - - case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: { - unsigned char **p = (unsigned char **)in; - unsigned char *end = (*p) + len; - - if (!ap) - break; - - /* forward the browser's Cookie so lws-login can validate it */ - if (ap->cookie_hdr[0] && - lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_COOKIE, - (unsigned char *)ap->cookie_hdr, - (int)strlen(ap->cookie_hdr), p, end)) - return -1; - break; - } - - case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: { - int avail, take; - - if (!ap || ap->done) - break; - - /* accumulate the (small) JSON body */ - avail = (int)sizeof(ap->body) - 1 - ap->body_len; - take = avail < (int)len ? avail : (int)len; - if (take > 0) { - memcpy(ap->body + ap->body_len, in, (size_t)take); - ap->body_len += take; - ap->body[ap->body_len] = '\0'; - } - break; - } - - case LWS_CALLBACK_COMPLETED_CLIENT_HTTP: - if (ap && !ap->done) { - sai_auth_state_t st; - - st = saiw_auth_state_from_body(ap->body, ap->body_len); - lwsl_wsi_notice(ap->wsi_parent, - "auth-status fetch completed: state=%d", (int)st); - saiw_auth_pending_resolve(ap, st); - } - /* - * The transaction is done; close the client wsi. This drives - * CLOSED_CLIENT_HTTP below, which frees ap. - */ - return -1; - - case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: - lwsl_notice("%s: auth-status client connect failed\n", __func__); - /* fall through */ - case LWS_CALLBACK_CLOSED_CLIENT_HTTP: - if (ap) { - if (!ap->done) { - lwsl_wsi_err(ap->wsi_parent, - "auth-status fetch failed/closed"); - saiw_auth_pending_resolve(ap, - SAI_AUTH_STATE_LOGGED_IN_NO_GRANT); - } - /* - * ap is this wsi's userdata; the wsi is going away now, - * so free it. Nothing else references ap once it is - * detached from the pss (resolve clears wsi_parent). - */ - free(ap); - } - break; - - default: - break; - } - - /* - * Do NOT delegate to lws_callback_http_dummy(): that handler is for - * server-side HTTP and proxied client wsis (it asserts the wsi has a - * parent under LWS_WITH_HTTP_PROXY). This is a standalone internal - * HTTP client -- return 0 like lws-login's callback_lws_login_client. - */ - (void)user; - (void)wsi; - return 0; -} const struct lws_protocols protocol_ws = { .name = "com-warmcat-sai", @@ -1055,10 +776,3 @@ const struct lws_protocols protocol_ws = { .per_session_data_size = sizeof(struct pss), .rx_buffer_size = 0, }; - -const struct lws_protocols protocol_sai_internal_http_client = { - .name = "sai_internal_http_client", - .callback = callback_sai_internal_http_client, - .per_session_data_size = 0, - .rx_buffer_size = 0, -}; diff --git a/src/web/w-private.h b/src/web/w-private.h index 29907d4..a86dd3d 100644 --- a/src/web/w-private.h +++ b/src/web/w-private.h @@ -60,51 +60,14 @@ enum { }; typedef enum { - SAI_AUTH_STATE_NOT_LOGGED_IN, /* 0: also the initial pss value */ - SAI_AUTH_STATE_LOGGED_IN_NO_GRANT, /* 1 */ - SAI_AUTH_STATE_LOGGED_IN_GRANT_USER, /* 2: < :2 (unused) */ - SAI_AUTH_STATE_LOGGED_IN_GRANT_ADMIN, /* 3: >= :2 */ - /* - * 4: auth determination is in flight (WS path fetches the cooked - * login status from lws-login asynchronously). The action gate - * treats anything != GRANT_ADMIN as denied, so this fails closed. - * NOT used as a wire value to the browser (we only push a state - * once resolved); defined to keep numeric values stable. - */ - SAI_AUTH_STATE_PENDING + SAI_AUTH_STATE_NOT_LOGGED_IN, + SAI_AUTH_STATE_LOGGED_IN_NO_GRANT, + SAI_AUTH_STATE_LOGGED_IN_GRANT_USER, /* < :2 */ + SAI_AUTH_STATE_LOGGED_IN_GRANT_ADMIN /* >= :2 */ } sai_auth_state_t; -/* - * In-flight WS auth fetch state. - * - * sai's WS path does no JWT/grant logic of its own; instead it fetches the - * cooked login status (".lws-login-status") served by the lws-login - * interceptor on sai's own mount, forwarding the browser's Cookie header so - * lws-login (which holds the JWK, grant name and grant level) can decide. - * - * One of these is heap-allocated per pss at WS ESTABLISH and freed when the - * fetch resolves (success or failure) or when the pss is destroyed. It is - * the ->userdata of the internal client wsi, so the client protocol callback - * recovers it with lws_wsi_user(). - */ -struct sai_auth_pending { - struct lws *wsi_parent; /* the sai WS wsi this is for */ - struct lws *wsi_client; /* lws writes the client wsi here */ - - char cookie_hdr[1024]; /* raw Cookie: value to forward */ - - char body[1024]; /* accumulated response body */ - int body_len; - - char done; /* set when resolved, prevents - * double-resolution if both the - * completion callback and the - * timeout/closed try to finish */ -}; - -struct pss { - struct vhd *vhd; +struct pss { struct vhd *vhd; struct lws *wsi; uint8_t is_gitohashi:1; @@ -196,33 +159,12 @@ struct pss { unsigned int resolved_task_offset:1; uint8_t wants_builder_info; sai_auth_state_t auth_state; - - /* async WS auth-fetch (see struct sai_auth_pending) */ - struct sai_auth_pending *auth_pending; - lws_sorted_usec_list_t sul_auth; }; struct vhd { struct lws_context *context; struct lws_vhost *vhost; - /* - * sai does no JWT/grant validation itself. At WS establish it - * performs an internal HTTP GET of auth_status_url on its own - * (unix-socket) vhost, forwarding the browser's Cookie so the - * lws-login interceptor -- which holds the JWK and grant name -- - * can produce the cooked login status. These pvos tell sai where - * its own vhost is reachable and which path to fetch: - * - * self_address the lws client address of sai's own listener, - * "+"-prefixed for a unix socket, eg - * "+/var/run/sai" (see lws_client_connect_via_info) - * auth_status_url the synthetic path lws-login serves, normally - * "/sai/.lws-login-status" - */ - char self_address[128]; - char auth_status_url[128]; - /* pss lists */ struct lws_dll2_owner browsers; @@ -267,18 +209,8 @@ sai_lws_context_from_json(const char *config_dir, const struct lws_protocols **pprotocols, const char *pol); extern const struct lws_protocols protocol_ws; -extern const struct lws_protocols protocol_sai_internal_http_client; extern const lws_ss_info_t ssi_saiw_websrv; -/* - * Kick off the async internal fetch of the cooked login status from lws-login - * for this WS connection. Sets pss->auth_state to PENDING and arranges for - * saiw_browser_queue_auth_state() to be called when it resolves. Returns 0 - * if the fetch was started. - */ -int -saiw_auth_fetch_kick(struct vhd *vhd, struct pss *pss, struct lws *wsi); - int sai_notification_file_upload_cb(void *data, const char *name, const char *filename, char *buf, int len, @@ -337,14 +269,6 @@ int saiw_ws_browser_queue_REQUIRES_LWS_PRE(struct pss *pss, const void *buf, size_t len, enum lws_write_protocol flags); -/* - * Push a com.warmcat.sai.auth_state message to the browser reflecting the - * pss's current auth_state. Used to notify the browser once the async - * login-status fetch resolves (the browser re-evaluates admin UI on receipt). - */ -void -saiw_browser_queue_auth_state(struct pss *pss); - void saiw_browser_state_changed(struct pss *pss, int established); diff --git a/src/web/w-sai.c b/src/web/w-sai.c index 23ab520..2a7f740 100644 --- a/src/web/w-sai.c +++ b/src/web/w-sai.c @@ -57,8 +57,7 @@ static const char * const default_ss_policy = ; static const struct lws_protocols - *pprotocols[] = { &protocol_ws, - &protocol_sai_internal_http_client, NULL }; + *pprotocols[] = { &protocol_ws, NULL }; static void sigint_handler(int sig) { diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c index d7ca893..f0dbed5 100644 --- a/src/web/w-ws-browser.c +++ b/src/web/w-ws-browser.c @@ -224,24 +224,6 @@ saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(struct vhd *vhd, const void *buf, } lws_end_foreach_dll(p); } -/* - * Push a com.warmcat.sai.auth_state message reflecting pss->auth_state. Used - * to notify the browser once the async login-status fetch resolves; the - * browser re-evaluates admin UI on receipt (sai.js com.warmcat.sai.auth_state). - */ -void -saiw_browser_queue_auth_state(struct pss *pss) -{ - uint8_t buf[LWS_PRE + 128], *start = buf + LWS_PRE, *p = start, - *end = buf + sizeof(buf); - - p += lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), - "{\"schema\":\"com.warmcat.sai.auth_state\",\"auth_state\":%d}", - (int)pss->auth_state); - saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, start, - lws_ptr_diff_size_t(p, start), LWS_WRITE_TEXT); -} - int
Page fetched 0s ago, creation time: 6ms (vhost etag hits: 0%, cache hits: 0%)