diff --git a/assets/sai.js b/assets/sai.js
index 69cf70b..0c81bb4 100644
--- a/assets/sai.js
+++ b/assets/sai.js
@@ -398,7 +398,8 @@ const SaiAuthState = {
NOT_LOGGED_IN: 0,
LOGGED_IN_NO_GRANT: 1,
LOGGED_IN_GRANT_USER: 2, // < :2
- LOGGED_IN_GRANT_ADMIN: 3 // >= :2
+ LOGGED_IN_GRANT_ADMIN: 3, // >= :2
+ PENDING: 4 // backend login-status fetch in flight
};
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 = "",
@@ -3170,21 +3171,30 @@ function ws_open_sai()
location.reload();
break;
- case "com.warmcat.sai.auth_state":
- console.log("Backend auth_state:", jso.auth_state);
- if (jso.auth_state === 3) {
- auth_state = SaiAuthState.LOGGED_IN_GRANT_ADMIN;
- auth_is_admin = 1;
- } else if (jso.auth_state === 2) {
- auth_state = SaiAuthState.LOGGED_IN_GRANT_USER;
- auth_is_admin = 0;
- } else if (jso.auth_state === 1) {
- auth_state = SaiAuthState.LOGGED_IN_NO_GRANT;
- auth_is_admin = 0;
- } else {
- auth_state = SaiAuthState.NOT_LOGGED_IN;
- auth_is_admin = 0;
- }
+ 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;
+ } else if (jso.auth_state === 2) {
+ auth_state = SaiAuthState.LOGGED_IN_GRANT_USER;
+ auth_is_admin = 0;
+ } else if (jso.auth_state === 1) {
+ auth_state = SaiAuthState.LOGGED_IN_NO_GRANT;
+ auth_is_admin = 0;
+ } else {
+ auth_state = SaiAuthState.NOT_LOGGED_IN;
+ auth_is_admin = 0;
+ }
const statusContainer = document.getElementById('lws-login-status-container');
if (statusContainer) {
statusContainer.classList.remove('grant-admin', 'grant-user', 'grant-none');
diff --git a/etc-sai-EXAMPLE/web/conf.d/unixskt b/etc-sai-EXAMPLE/web/conf.d/unixskt
index 42500f2..e8f11d0 100644
--- a/etc-sai-EXAMPLE/web/conf.d/unixskt
+++ b/etc-sai-EXAMPLE/web/conf.d/unixskt
@@ -63,7 +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.
# 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 36d2497..fcb52e5 100644
--- a/src/web/w-comms.c
+++ b/src/web/w-comms.c
@@ -165,24 +165,27 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,
{
const struct lws_protocol_vhost_options *pvo = (const struct lws_protocol_vhost_options *)in;
- const char *jwk_path = "/etc/sai/web/auth.jwk";
- lws_strncpy(vhd->cookie_name, "auth_session", sizeof(vhd->cookie_name));
+ /*
+ * 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, "jwt-auth-jwk-path"))
- jwk_path = pvo->value;
- if (!strcmp(pvo->name, "cookie-name"))
- lws_strncpy(vhd->cookie_name, pvo->value, sizeof(vhd->cookie_name));
+ 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;
}
- lws_strncpy(vhd->jwk_path, jwk_path, sizeof(vhd->jwk_path));
- if (!lws_jwk_load(&vhd->jwk, vhd->jwk_path, NULL, NULL)) {
- vhd->has_jwk = 1;
- lwsl_notice("Loaded JWT jwk from %s\n", vhd->jwk_path);
- } else {
- lwsl_err("FAILED to load JWT jwk from %s\n", vhd->jwk_path);
- }
}
if (lws_pvo_get_str(in, "database", &vhd->sqlite3_path_lhs)) {
@@ -570,61 +573,20 @@ http_resp:
}
#endif
- pss->auth_state = SAI_AUTH_STATE_NOT_LOGGED_IN;
- if (vhd->has_jwk) {
-#if defined(LWS_WITH_JOSE)
- const char *reason = "unknown";
- struct lws_jwt_auth *ja = lws_jwt_auth_create(wsi, &vhd->jwk, vhd->cookie_name, NULL, NULL, &reason);
- if (ja) {
- int grant = (int)lws_jwt_auth_query_grant(ja, "com.warmcat.sai");
- int grant_all = (int)lws_jwt_auth_query_grant(ja, "*");
- int max_grant = grant > grant_all ? grant : grant_all;
-
- /*
- * Security: enforce JWT expiry at request
- * time. lws's proactive SUL expiry timer
- * invokes a callback we registered as
- * NULL, so on its own it does nothing.
- * A stolen cookie that has already
- * expired would otherwise still
- * authorize privileged operations for
- * the life of this WS. Reject if the
- * token's exp is in the past.
- */
- uint64_t exp = lws_jwt_auth_get_exp(ja);
- if (exp && exp < (uint64_t)lws_now_secs()) {
- lwsl_wsi_err(wsi, "Rejecting WS: JWT expired (exp %llu, now %llu)",
- (unsigned long long)exp,
- (unsigned long long)lws_now_secs());
- } else {
- if (max_grant >= 0) {
- pss->auth_state = SAI_AUTH_STATE_LOGGED_IN_GRANT_ADMIN;
- lwsl_wsi_notice(wsi, "Authorized WebSocket connection (admin/grant, max_grant: %d)", max_grant);
- } else {
- pss->auth_state = SAI_AUTH_STATE_LOGGED_IN_NO_GRANT;
- lwsl_wsi_err(wsi, "JWT validation passed, but no grant found (grant: %d, grant_all: %d)", grant, grant_all);
- }
- }
-
- lws_jwt_auth_destroy(&ja);
- } else {
- if (reason && !strcmp(reason, "Cookie not found")) {
- char cookies[512];
- int n = lws_hdr_copy(wsi, cookies, sizeof(cookies), WSI_TOKEN_HTTP_COOKIE);
- if (n > 0)
- lwsl_wsi_err(wsi, "JWT auth failed (cookie: '%s'): Cookie not found (available cookies: '%s') (using JWK from %s)", vhd->cookie_name, cookies, vhd->jwk_path);
- else
- lwsl_wsi_err(wsi, "JWT auth failed (cookie: '%s'): Cookie not found (no cookies sent by browser) (using JWK from %s)", vhd->cookie_name, vhd->jwk_path);
- } else {
- lwsl_wsi_err(wsi, "JWT auth failed (cookie: '%s'): %s (using JWK from %s)", vhd->cookie_name, reason ? reason : "unknown", vhd->jwk_path);
- }
- }
-#endif
- } else {
- lwsl_wsi_err(wsi, "Cannot validate JWT because no JWK was loaded (expected at %s)", vhd->jwk_path);
- }
+ /*
+ * 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.
+ */
+ 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: final auth_state=%d (has_jwk=%d, cookie_name='%s')", (int)pss->auth_state, vhd->has_jwk, vhd->cookie_name);
+ lwsl_wsi_notice(wsi, "**** ESTABLISHED WS: auth fetch started (auth_state=%d pending)", (int)pss->auth_state);
if (!memcmp((char *)start, "/sai", 4))
start += 4;
@@ -709,6 +671,25 @@ 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])
@@ -812,9 +793,265 @@ 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",
.callback = w_callback_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 5d0b19a..b9950bc 100644
--- a/src/web/w-private.h
+++ b/src/web/w-private.h
@@ -60,12 +60,48 @@ enum {
};
typedef enum {
- 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_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_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;
@@ -136,16 +172,32 @@ 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;
- struct lws_jwk jwk;
- int has_jwk;
- char cookie_name[64];
- char jwk_path[256];
+ /*
+ * 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;
@@ -191,8 +243,18 @@ 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,
@@ -251,6 +313,14 @@ 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 2a7f740..23ab520 100644
--- a/src/web/w-sai.c
+++ b/src/web/w-sai.c
@@ -57,7 +57,8 @@ static const char * const default_ss_policy =
;
static const struct lws_protocols
- *pprotocols[] = { &protocol_ws, NULL };
+ *pprotocols[] = { &protocol_ws,
+ &protocol_sai_internal_http_client, NULL };
static void sigint_handler(int sig)
{
diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c
index 79ee52e..6240ab2 100644
--- a/src/web/w-ws-browser.c
+++ b/src/web/w-ws-browser.c
@@ -210,6 +210,24 @@ 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