/*
 * ws protocol handler plugin for "lws login" / auth bouncer
 *
 * Written in 2010-2026 by Andy Green <andy@warmcat.com>
 *
 * This file is made available under the Creative Commons CC0 1.0
 * Universal Public Domain Dedication.
 *
 * This plugin translates an LWS Mount Interceptor into a JWT-driven bouncer
 * depending on lws_jwt_auth. Unauthenticated connections are bounced transparently.
 */

#if !defined(LWS_PLUGIN_STATIC)
#if !defined(LWS_DLL)
#define LWS_DLL
#endif
#if !defined(LWS_INTERNAL)
#define LWS_INTERNAL
#endif
#include <libwebsockets.h>
#endif

#include <stdlib.h>
#include <string.h>
#include <sqlite3.h>

#define LWS_AUTH_MAX_COOKIE_LEN LWS_SSO_MAX_COOKIE

struct login_whitelist {
	lws_dll2_t              list;
	lws_sockaddr46          sa46;
	int                     net_len;
};

struct vhd_login {
	struct lws_context      *context;
	struct lws_vhost        *vhost;
	struct lws_dll2_owner   wl;

	/* PVO settings */
	const char              *cookie_name;
	const char              *service_name;
	const char              *auth_server_url;
	const char              *unauth_protocols;
	int                     min_grant_level;

	char                    db_path[256];
	sqlite3                 *db;
	char                    auth_domain[128];
	char                    cookie_domain[128];
	uint64_t                jwt_validity_secs;

	int                     unauth_allow;

	struct lws_jwk          jwk;
	lws_dll2_owner_t        pending_refresh_list;
};

struct pss_login {
	struct lws_jwt_auth     *ja;
	uint8_t                 whitelist_failed;
	char                    *silent_update_jwt;
	struct lws_spa          *spa;
	struct lws_buflist      *tx_buflist;
	size_t                  tx_remaining; /* body bytes not yet written, so the
					       * FINAL write can be identified
					       * correctly (see WRITEABLE) */
};

struct pending_login_refresh {
	lws_dll2_t              list;
	lws_sorted_usec_list_t  sul;
	struct vhd_login        *vhd;

	struct lws              *wsi_server;
	struct lws              *wsi_client;

	char                    cookie_hdr[1024];

	char                    payload[1024];
	int                     payload_len;
	int                     payload_pos;

	char                    token[2048];
};

static const char * const canned_css =
        ".lws-login-box{position:relative;font-family:-apple-system,system-ui,sans-serif;padding:"
        "16px;border-radius:8px;background:rgba(0,0,0,0.02);border:1px solid "
        "rgba(0,0,0,0.08);display:inline-block;font-size:14px;line-height:1.4;"
        "color:#333;}\n"
        "@media(prefers-color-scheme:dark){.lws-login-box{background:rgba(255,255,"
        "255,0.05);border-color:rgba(255,255,255,0.1);color:#888;}}\n"
        ".lws-login-btn{display:inline-block;padding:8px "
        "16px;background:#007bff;color:#fff!important;text-decoration:none;border-"
        "radius:6px;font-weight:600;font-size:13px;transition:background "
        "0.2s;margin-top:5px;}\n"
        ".lws-login-btn:hover{background:#0056b3;}\n"
        ".lws-login-err{display:inline-block;margin-top:8px;margin-bottom:4px;"
        "padding:6px 10px;background:#ffebee;border-left:3px solid "
        "#f44336;color:#c62828;font-size:13px;font-weight:500;}\n"
        ".lws-login-link{color:#007bff;text-decoration:none;margin-right:12px;font-"
        "weight:500;font-size:13px;transition:opacity 0.2s;}\n"
        ".lws-login-link:hover{opacity:0.8;}\n"
        ".lws-login-logout{color:#f44336;}\n"
        ".lws-login-identity{font-size:16px;margin:0 12px 0 "
        "0;display:inline-block;font-weight:600;}\n"
        ".lws-login-mt{margin-top:10px;}\n"
        ".lws-login-mb{margin-bottom:8px;font-weight:500;}\n"
        ".lws-preauth-banner{position:relative;margin-top:10px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.2);padding:12px;border-radius:8px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 4px 12px rgba(0,0,0,0.15);}"
        ".lws-preauth-info{display:flex;flex-direction:column;}"
        ".lws-preauth-title{font-weight:600;font-size:14px;margin-bottom:4px;}"
        ".lws-preauth-desc{font-size:12px;color:#aaa;}"
        ".lws-preauth-link{background:#28a745;color:#fff!important;text-decoration:none;padding:6px 12px;border-radius:4px;font-weight:600;font-size:13px;transition:background 0.2s;display:flex;align-items:center;gap:8px;}"
        ".lws-preauth-link:hover{background:#218838;}"
        ".pie-timer{width:20px;height:20px;transform:rotate(-90deg);border-radius:50%;}"
        ".pie-timer circle{fill:none;stroke:#fff;stroke-width:10;stroke-dasharray:31.4;transition:stroke-dashoffset 1s linear;}"
        ".lws-login-refgirl{position:absolute;bottom:0px;right:-10px;height:120px;opacity:0.8;pointer-events:none;z-index:1;}"
        ".lws-preauth-widget{display:none;position:absolute;top:0;left:100%;margin-left:15px;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:8px;max-height:200px;overflow-y:auto;background:#2b2d31;box-shadow:0 8px 16px rgba(0,0,0,0.3);z-index:1000;min-width:280px;}"
        ".lws-preauth-widget.active{display:block;}"
        ".lws-login-avatar-admin{color:#007bff;margin-right:6px;display:inline-block;vertical-align:middle;}"
        ".lws-login-avatar-user{color:#333;margin-right:6px;display:inline-block;vertical-align:middle;}"
        "@media(prefers-color-scheme:dark){.lws-login-avatar-user{color:#888;}}";

static const char * const canned_js =
        /* lwsLoginSilentRefresh: attempt a side-channel renewal via the BFF.
         * Returns a tri-state so callers can distinguish a hard failure (the
         * long-term login session is genuinely gone -> the auth server returns
         * 401) from a transient failure (network blip, 5xx, captive-portal
         * re-establishing on a freshly-woken device).  Only the hard failure
         * should escalate to the login page; transient failures are
         * retry-worthy because the long-term session may still be valid. */
        "window.lwsLoginSilentRefresh=async function(){"
        "try{"
        "var r=await fetch('.lws-login-refresh',{method:'POST',credentials:'include'});"
        "if(r.status===401)return 'dead';"
        "return r.ok?'ok':'transient';"
        "}catch(e){return 'transient';}"
        "};"
        /* lwsLoginEscalate: give up on silent renewal and send the user to the
         * auth server login page, preserving the current location for the
         * post-login redirect.  Idempotent via a guard flag so multiple
         * concurrent 'dead' results only navigate once. */
        "window.lwsLoginEscalate=function(login_url){"
        "if(window.__lwsLoginEscalated)return;"
        "window.__lwsLoginEscalated=1;"
        "var s=login_url.split('redirect_uri=')[0]+'redirect_uri='+encodeURIComponent(window.location.href);"
        "window.location.href=s;"
        "};"
        "window.renderLwsLoginStatus=async function(d){"
        "window.__lwsLoginDiv=d;"
        "var e=document.getElementById(d);"
        "if(!e)return;"
        "if(window.__lwsLoginInflight)return;"
        "window.__lwsLoginInflight=1;"
        "if(!document.getElementById('lws-login-css')){"
        "var l=document.createElement('link');"
        "l.id='lws-login-css';l.rel='stylesheet';l.href='lws-login.css';"
        "document.head.appendChild(l);"
        "}"
        "try{"
        "let r=await fetch('.lws-login-status');"
        "let st=await r.json();"
        "if(!st.logged_in){"
        "var sr=await window.lwsLoginSilentRefresh();"
        "if(sr==='ok'){"
        "r=await fetch('.lws-login-status');"
        "st=await r.json();"
        "}else if(sr==='dead'){"
        /* long-term login gone: dump to the auth server login page */
        "window.__lwsLoginInflight=0;"
        "window.lwsLoginEscalate(st.login_url);"
        "return;"
        "}"
        "}"
        "var c='<div class=\"lws-login-box\">';"
        "if(st.server_now){"
        "var skew=Math.abs(st.server_now-(Date.now()/1000));"
        "if(skew>300){"
        "c+='<div class=\"lws-login-err\">Warning: Device clock off by '+Math.round(skew/60)+' mins</div><br>';"
        "c+='<img src=\"'+st.auth_server_url+'/refgirl-time.png\" class=\"lws-login-refgirl\">';"
        "}"
        "}"
        "if(st.logged_in){"
        "window.lwsLoginRetry=0;"
        "var u='.lws-login-logout?redirect_uri='+encodeURIComponent(window.location.href);"
        "var a=st.is_admin?'<a class=\"lws-login-link\" href=\"'+st.auth_server_url+'/api/admin\">Admin Console</a>':'';"
        "var av='<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"></path><circle cx=\"12\" cy=\"7\" r=\"4\"></circle></svg>';"
        "var gl=st.is_admin?255:(st.grant_level||0);"
        "var ac=gl>=2?'lws-login-avatar-admin':'lws-login-avatar-user';"
        "c+='<span class=\"'+ac+'\">'+av+'</span>';"
        "c+='<strong class=\"lws-login-identity\">'+st.identity+'</strong><br>';"
        "c+=a+' <a class=\"lws-login-link lws-login-logout\" href=\"'+u+'\">Logout</a>';"
        "if(!st.has_grant&&!st.is_admin)c+='<div class=\"lws-login-err\">login lacks grant</div><br>';"
        "if(st.exp){"
        "var n=st.server_now?st.server_now:(Date.now()/1000);"
        "var m=st.exp-n;"
        /* Schedule a silent refresh before expiry.  The upper bound must be
         * larger than any realistic cookie lifetime, otherwise long-lived
         * sessions (eg the 24h default jwt-validity-secs) never schedule a
         * refresh and the status div goes stale until a manual page reload.
         * 30d (2592000s) is well past any sensible session token lifetime
         * while still guarding against nonsensical multi-year exp values. */
        "if(m>0&&m<2592000){"
        "setTimeout(async function(){"
        "var sr=await window.lwsLoginSilentRefresh();"
        /* 'ok' or 'transient' -> re-render (re-render itself re-checks status
         * and, if still expired, retries silent refresh / escalates).  'dead'
         * -> escalate now rather than waiting for the next render. */
        "if(sr==='dead')window.lwsLoginEscalate(st.login_url);"
        "else window.renderLwsLoginStatus(d);"
        "},(m>60?m-60:0)*1000);"
        "}"
        "}"
        "}else{"
        "var s=st.login_url.split('redirect_uri=')[0]+'redirect_uri='+encodeURIComponent(window.location.href);"
        "c+='<div class=\"lws-login-mb\">Not logged in</div>';"
        "c+='<a class=\"lws-login-btn\" href=\"'+s+'\">Login &rarr;</a>';"
        /* We reach here only when silent refresh returned 'transient' (a hard
         * 'dead' already escalated out of the function above).  Back off and
         * retry; if the long-term session is actually dead, the next
         * renderLwsLoginStatus will get 'dead' from the silent-refresh attempt
         * in the not-logged-in branch and escalate then. */
        "let iv=[1,2,5,10,30,60,300,600,1200,3600];let rt=window.lwsLoginRetry||0;let d_s=iv[rt]||3600;window.lwsLoginRetry=rt+1;"
        "setTimeout(function(){window.renderLwsLoginStatus(d);},d_s*1000);"
        "}"
        "var pc=document.getElementById('lws-preauth-container');"
        "e.innerHTML=c+'</div>';"
        "if(st.logged_in){"
        "if(!pc){pc=document.createElement('div');pc.id='lws-preauth-container';pc.className='lws-preauth-widget';}"
        "e.style.position='relative';e.appendChild(pc);"
        "if(!window.lwsPreauthWS){"
        "var asu=st.auth_server_url || st.login_url.split('/login')[0];"
        "var u=asu.replace('https','wss').replace('http','ws');"
        "var ws=new WebSocket(u,'lws-oauth-preauth');"
        "window.lwsPreauthWS=ws;"
        "var devs={};"
        "function draw(){"
        "var h='';var now=Date.now()/1000;"
        "for(var k in devs){"
        "var d=devs[k];var left=d.expires-now;if(left<=0){delete devs[k];continue;}"
        "var pct=left/300.0;var dash=(1-pct)*31.4;"
        "h+='<div class=\"lws-preauth-banner\"><div class=\"lws-preauth-info\">';"
        "h+='<span class=\"lws-preauth-title\">New Device: '+d.name+'</span>';"
        "h+='<span class=\"lws-preauth-desc\">Requires authorization</span></div>';"
        "h+='<a class=\"lws-preauth-link\" href=\"'+st.auth_server_url+'/?device_code='+d.user_code+'\" target=\"_blank\">';"
        "h+='<svg class=\"pie-timer\" viewBox=\"0 0 10 10\"><circle cx=\"5\" cy=\"5\" r=\"5\" stroke-dashoffset=\"'+dash+'\"></circle></svg>';"
        "h+='Authorize</a></div>';"
        "}"
        "pc.innerHTML=h;"
        "if(Object.keys(devs).length>0){pc.classList.add('active');setTimeout(draw,1000);}else{pc.classList.remove('active');}"
        "}"
        "ws.onmessage=function(e){"
        "var m=JSON.parse(e.data);"
        "if(m.event==='device_joined'){devs[m.serial]=m;draw();}"
        "else if(m.event==='device_left'){delete devs[m.serial];draw();}"
        "};"
        "}"
        "}"
        "window.__lwsLoginInflight=0;"
        "}catch(er){"
        "console.log('lws-login fetch:',er);"
        "window.__lwsLoginInflight=0;"
        "let iv=[1,2,5,10,30,60,300,600,1200,3600];let rt=window.lwsLoginRetry||0;let d_s=iv[rt]||3600;window.lwsLoginRetry=rt+1;"
        "setTimeout(function(){window.renderLwsLoginStatus(d);},d_s*1000);"
        "}"
        "};"
        /* Wake trigger: when the device/tab wakes (eg an Android tablet that
         * slept through its renewal slot), fire a status check on visibility
         * change or bfcache restore so the not-logged-in -> silent-refresh ->
         * escalate flow runs without needing a manual page interaction.
         * Debounced to 1s to coalesce rapid visibility flips, and skipped if a
         * check is already in flight or we've already escalated to login. */
        "window.__lwsLoginWake=function(){"
        "if(window.__lwsLoginEscalated||window.__lwsLoginInflight)return;"
        "if(!window.__lwsLoginDiv)return;"
        "if(window.__lwsLoginWakeTimer)return;"
        "window.__lwsLoginWakeTimer=setTimeout(function(){"
        "window.__lwsLoginWakeTimer=null;"
        "if(!window.__lwsLoginEscalated&&!window.__lwsLoginInflight)"
        "window.renderLwsLoginStatus(window.__lwsLoginDiv);"
        "},1000);"
        "};"
        "document.addEventListener('visibilitychange',function(){"
        "if(!document.hidden)window.__lwsLoginWake();});"
        "window.addEventListener('pageshow',function(ev){"
        /* ev.persisted===true => restored from bfcache (a frozen tab reactivated,
         * eg after the device slept) -> worth a wake check.  A fresh load
         * (persisted===false) already runs its own initial render. */
        "if(ev.persisted)window.__lwsLoginWake();});";

static void
sul_pending_refresh_cb(lws_sorted_usec_list_t *sul)
{
	struct pending_login_refresh *ps = lws_container_of(sul,
					struct pending_login_refresh, sul);

	lwsl_info("%s: auth refresh timed out\n", __func__);
	lws_dll2_remove(&ps->list);
	free(ps);
}

static const char * const param_names[] = {
	"token",
	"target",
};

enum enum_param_names {
	EPN_TOKEN,
	EPN_TARGET,
};

static int
lws_login_ends_with(const char *str, const char *suffix)
{
	size_t len_str = strlen(str);
	size_t len_suffix = strlen(suffix);

	if (len_suffix > len_str)
		return 0;

	return !strcmp(str + len_str - len_suffix, suffix);
}

static int
callback_lws_login_client(struct lws *wsi, enum lws_callback_reasons reason,
			  void *user, void *in, size_t len)
{
	struct pending_login_refresh *ps = (struct pending_login_refresh *)lws_wsi_user(wsi);

	switch (reason) {
	case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: {
		unsigned char **p = (unsigned char **)in;
		unsigned char *end = (*p) + len;
		char clen[16];

		if (!ps)
			break;

		lws_snprintf(clen, sizeof(clen), "%d", ps->payload_len);

		if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
						 (unsigned char *)"application/x-www-form-urlencoded",
						 33, p, end))
			return -1;

		if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_LENGTH,
						 (unsigned char *)clen, (int)strlen(clen), p, end))
			return -1;

		if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_COOKIE,
						 (unsigned char *)ps->cookie_hdr,
						 (int)strlen(ps->cookie_hdr), p, end))
			return -1;

		break;
	}

	case LWS_CALLBACK_CLIENT_HTTP_WRITEABLE: {
		int n;

		if (!ps || ps->payload_pos >= ps->payload_len)
			break;

		n = lws_write(wsi, (unsigned char *)ps->payload + ps->payload_pos,
			      (size_t)(ps->payload_len - ps->payload_pos), LWS_WRITE_HTTP);
		if (n < 0)
			return -1;
		ps->payload_pos += n;

		if (ps->payload_pos < ps->payload_len)
			lws_callback_on_writable(wsi);
		else
			lws_client_http_body_pending(wsi, 0);
		break;
	}

	case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: {
		struct lws_tokenize ts;
		lws_tokenize_elem e;
		int state = 0;

		if (!ps || !in || !len)
			break;

		lws_tokenize_init(&ts, (char *)in, LWS_TOKENIZE_F_DOT_NONTERM | LWS_TOKENIZE_F_MINUS_NONTERM);
		ts.len = len;

		while ((e = lws_tokenize(&ts)) != LWS_TOKZE_ENDED) {
			if (state == 0 && e == LWS_TOKZE_QUOTED_STRING &&
			    ts.token_len == 5 && !strncmp(ts.token, "token", 5)) {
				state = 1;
			} else if (state == 1 && e == LWS_TOKZE_DELIMITER && ts.token[0] == ':') {
				state = 2;
			} else if (state == 2 && e == LWS_TOKZE_QUOTED_STRING) {
				if (ts.token_len < sizeof(ps->token)) {
					lws_strncpy(ps->token, ts.token, ts.token_len + 1);
					lwsl_notice("%s: Extracted OAuth token natively via BFF\n", __func__);
				}
				break;
			}
		}
		break;
	}

	case LWS_CALLBACK_COMPLETED_CLIENT_HTTP: {
		if (!ps)
			break;

		if (ps->wsi_server)
			lws_callback_on_writable(ps->wsi_server);
		ps->wsi_client = NULL;
		break;
	}

	case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
	case LWS_CALLBACK_CLOSED_CLIENT_HTTP: {
		if (!ps)
			break;
		lwsl_notice("%s: client connection closed or errored\n", __func__);
		if (ps->wsi_server && !ps->token[0]) {
			lws_callback_on_writable(ps->wsi_server);
		}
		ps->wsi_client = NULL;
		break;
	}

	default:
		break;
	}

	return 0;
}

static int
lws_login_jwt_auth_cb(struct lws_jwt_auth *ja, int state, void *user)
{
	if (state == LWS_JWT_AUTH_STATE_EXPIRED)
		lwsl_notice("%s: Session expired naturally\n", __func__);

	return 0;
}

static int
auth_verify_redirect_uri(struct vhd_login *vhd, const char *redirect_uri)
{
	sqlite3_stmt *stmt;
	int valid = 0;

	if (!redirect_uri || !redirect_uri[0])
		return 0;

	if ((char *)strstr(redirect_uri, "../") ||
	    (char *)strstr(redirect_uri, "..%2F") ||
	    (char *)strstr(redirect_uri, "..%2f"))
		return 0;

	if (sqlite3_prepare_v2(vhd->db, "SELECT redirect_uris FROM oauth_clients", -1, &stmt, NULL) == SQLITE_OK) {
		while (!valid && sqlite3_step(stmt) == SQLITE_ROW) {
			const char *uris = (const char *)sqlite3_column_text(stmt, 0);
			if (uris) {
				const char *p = uris;
				while (p && *p) {
					while (*p == ' ' || *p == '\r' || *p == '\n') p++;
					const char *comma = strchr(p, ',');
					size_t len = comma ? lws_ptr_diff_size_t(comma, p) : strlen(p);
					while (len > 0 && (p[len - 1] == ' ' || p[len - 1] == '\r' || p[len - 1] == '\n')) len--;
					while (len > 0 && p[len - 1] == '/') len--;
					if (len > 0) {
						if (!strncasecmp(redirect_uri, p, len)) {
							char next = redirect_uri[len];
							if (next == '\0' || next == '/' || next == '?' || next == '#') {
								valid = 1;
								break;
							}
						}
					}
					p = comma ? comma + 1 : NULL;
				}
			}
		}
		sqlite3_finalize(stmt);
	}
	return valid;
}

static int
simple_response(struct lws *wsi, struct pss_login *pss, const char *msg, const char *mime_type,
	     unsigned int code, unsigned char *start, unsigned char **p, unsigned char *end)
{
        char eb[LWS_PRE + 1024];
	int l = lws_snprintf(eb + LWS_PRE, sizeof(eb) - LWS_PRE, "%s", msg);

	if (lws_add_http_common_headers(wsi, code, mime_type, (lws_filepos_t)l, p, end))
		return -1;

	if (lws_add_http_header_by_name(wsi, (unsigned char *)"cache-control:", (unsigned char *)"no-cache, no-store, must-revalidate", 35, p, end))
		return -1;

        if (lws_finalize_write_http_header(wsi, start, p, end))
                return -1;

	if (lws_buflist_append_segment(&pss->tx_buflist, (unsigned char*)eb + LWS_PRE, (size_t)l) < 0)
		return -1;
	pss->tx_remaining = (size_t)l;

	lws_callback_on_writable(wsi);

	return 0;
}

static int
callback_lws_login(struct lws *wsi, enum lws_callback_reasons reason,
		   void *user, void *in, size_t len)
{
	struct vhd_login *vhd = (struct vhd_login *)lws_protocol_vh_priv_get(
			lws_get_vhost(wsi), reason == LWS_CALLBACK_HTTP_INTERCEPTOR_CHECK ? (const struct lws_protocols *)in : lws_get_protocol(wsi));
	struct pss_login *pss = (struct pss_login *)user;
	char buf[LWS_PRE + LWS_SSO_MAX_COOKIE], *p = buf + LWS_PRE, *end = buf + sizeof(buf) - 1;
	const char *cp;

	switch ((int)reason) {
	case LWS_CALLBACK_PROTOCOL_INIT:
		if (lws_cmdline_option_cx(lws_get_context(wsi), "--lws-stub"))
			return 0;
		if (!in)
			return 0;
		vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
				lws_get_protocol(wsi), sizeof(struct vhd_login));
		if (!vhd)
			return -1;

		vhd->context = lws_get_context(wsi);
		vhd->vhost = lws_get_vhost(wsi);

		vhd->cookie_name = "auth_session";
		vhd->service_name = "default-service";
		vhd->min_grant_level = 1;
		lws_strncpy(vhd->auth_domain, "auth.warmcat.com", sizeof(vhd->auth_domain));
		lws_strncpy(vhd->db_path, "/var/db/lws-auth.sqlite3", sizeof(vhd->db_path));
		vhd->jwt_validity_secs = 86400;

		if (lws_pvo_get_str(in, "cookie-name", &vhd->cookie_name))
			lwsl_info("%s: default cookie-name %s\n", __func__, vhd->cookie_name);

		{
			const struct lws_protocol_vhost_options *pvo = (const struct lws_protocol_vhost_options *)in;
			while (pvo) {
				if (!strcmp(pvo->name, "whitelist")) {
					struct login_whitelist *w = malloc(sizeof(*w));
					if (!w) {
						lwsl_err("%s: OOM\n", __func__);
						return -1;
					}
					memset(w, 0, sizeof(*w));
					if (lws_parse_cidr(pvo->value, &w->sa46, &w->net_len) < 0) {
						lwsl_err("%s: invalid whitelist CIDR %s\n", __func__, pvo->value);
						free(w);
						return -1;
					}
					lws_dll2_add_tail(&w->list, &vhd->wl);
				}
				pvo = pvo->next;
			}
		}

		if (lws_pvo_get_str(in, "service-name", &vhd->service_name))
			lwsl_info("%s: default service-name %s\n", __func__, vhd->service_name);

		if (lws_pvo_get_str(in, "auth-server-url", &vhd->auth_server_url)) {
			lwsl_err("%s: auth-server-url PVO is REQUIRED\n", __func__);
			return -1;
		}

		if (!lws_pvo_get_str(in, "min-grant-level", &cp))
			vhd->min_grant_level = atoi(cp);

		if (!lws_pvo_get_str(in, "jwt-jwk", &cp)) {
			if (cp[0] == '{' || lws_jwk_load(&vhd->jwk, cp, NULL, NULL)) {
				if (lws_jwk_import(&vhd->jwk, NULL, NULL, cp, strlen(cp))) {
					lwsl_err("%s: failed to load/import JWK\n", __func__);
					return -1;
				}
			}
		} else {
			lwsl_err("%s: jwt-jwk PVO required\n", __func__);
			return -1;
		}

		vhd->cookie_domain[0] = '\0';
		if (!lws_pvo_get_str(in, "cookie-domain", &cp))
			lws_strncpy(vhd->cookie_domain, cp, sizeof(vhd->cookie_domain));

		if (!lws_pvo_get_str(in, "auth-domain", &cp))
			lws_strncpy(vhd->auth_domain, cp, sizeof(vhd->auth_domain));

		if (!lws_pvo_get_str(in, "jwt-validity-secs", &cp))
			vhd->jwt_validity_secs = (uint64_t)atoll(cp);

		vhd->unauth_allow = 0;
		if (!lws_pvo_get_str(in, "unauth-allow", &cp))
			vhd->unauth_allow = atoi(cp);

		if (!lws_pvo_get_str(in, "unauth-protocols", &vhd->unauth_protocols))
			lwsl_notice("%s: unauth-protocols: %s\n", __func__, vhd->unauth_protocols);

		if (!lws_pvo_get_str(in, "db-path", &cp))
			lws_strncpy(vhd->db_path, cp, sizeof(vhd->db_path));

		if (lws_struct_sq3_open(vhd->context, vhd->db_path, 1, &vhd->db)) {
			lwsl_err("%s: could not open local database at %s. Dynamic grant revocation disabled.\n", __func__, vhd->db_path);
			vhd->db = NULL;
		}


		break;

	case LWS_CALLBACK_PROTOCOL_DESTROY:
		if (vhd) {
			lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, vhd->wl.head) {
				struct login_whitelist *w = lws_container_of(d, struct login_whitelist, list);
				lws_dll2_remove(d);
				free(w);
			} lws_end_foreach_dll_safe(d, d1);
			lws_jwk_destroy(&vhd->jwk);
			if (vhd->db)
				sqlite3_close(vhd->db);
		}
		break;

	case LWS_CALLBACK_USER + 1:
	{
		int level = -1;
		struct lws_jwt_auth *ja;
		const char *service_name;
		const struct lws_http_mount *mount;
		char uri[256];

		vhd = (struct vhd_login *)lws_protocol_vh_priv_get(
				lws_get_vhost(wsi),
				lws_vhost_name_to_protocol(lws_get_vhost(wsi), "lws-login"));

		if (!vhd)
			return 1;

		service_name = vhd->service_name;
		uri[0] = '\0';
		if (lws_hdr_copy(wsi, uri, sizeof(uri), WSI_TOKEN_GET_URI) > 0 ||
		    lws_hdr_copy(wsi, uri, sizeof(uri), WSI_TOKEN_POST_URI) > 0) {
			if (uri[0]) {
				mount = lws_find_mount(wsi, uri, (int)strlen(uri));
				if (mount) {
					if (!lws_pmo_get_str(mount, "service-name", &service_name)) {
						lwsl_info("%s: using service_name %s from target pmo for bypass api\n", __func__, service_name);
					}
#if defined(LWS_WITH_JOSE)
					else if (mount->interceptor_path) {
						const struct lws_http_mount *im = mount;
						while (im && im->interceptor_path) {
							im = lws_find_mount(wsi, im->interceptor_path, (int)strlen(im->interceptor_path));
							if (im && !lws_pmo_get_str(im, "service-name", &service_name)) {
								lwsl_info("%s: using service_name %s from interceptor pmo for bypass api\n", __func__, service_name);
								break;
							}
						}
					}
#endif
				}
			}
		}

		if (!service_name)
			service_name = "";

		ja = lws_jwt_auth_create(wsi, &vhd->jwk, vhd->cookie_name, lws_login_jwt_auth_cb, wsi, NULL);
		if (ja) {
			int epoch_ok = 1;
			if (vhd->db) {
				uint32_t uid = lws_jwt_auth_get_uid(ja);
				if (uid) {
					sqlite3_stmt *stmt;
					if (sqlite3_prepare_v2(vhd->db, "SELECT session_epoch FROM users WHERE uid = ?", -1, &stmt, NULL) == SQLITE_OK) {
						sqlite3_bind_int(stmt, 1, (int)uid);
						if (sqlite3_step(stmt) == SQLITE_ROW) {
							uint32_t db_epoch = (uint32_t)sqlite3_column_int(stmt, 0);
							if (db_epoch != lws_jwt_auth_get_sec(ja))
								epoch_ok = 0;
						}
						sqlite3_finalize(stmt);
					}
				}
			}
			level = lws_jwt_auth_query_grant(ja, service_name);
			lws_jwt_auth_destroy(&ja);
			if (epoch_ok && level >= vhd->min_grant_level)
				return 0; /* Authentic and authorized */
		}
		return 1; /* Unauthenticated */
	}

	case LWS_CALLBACK_HTTP_INTERCEPTOR_CHECK:
	{
		int level = -1;
		struct lws_jwt_auth *ja;
		char uri[256];
		const char *service_name;
		const struct lws_http_mount *mount;

		uri[0] = '\0';
		if (lws_hdr_copy(wsi, uri, sizeof(uri), WSI_TOKEN_GET_URI) > 0 ||
		    lws_hdr_copy(wsi, uri, sizeof(uri), WSI_TOKEN_POST_URI) > 0) {
			if (lws_login_ends_with(uri, "/.lws-login-status") ||
			    lws_login_ends_with(uri, "/lws-login.js") ||
			    lws_login_ends_with(uri, "/lws-login.css") ||
			    lws_login_ends_with(uri, "/.lws-login-sso") ||
			    lws_login_ends_with(uri, "/.lws-login-logout") ||
			    lws_login_ends_with(uri, "/.lws-login-refresh"))
				return 1;
		}

		if (!vhd) {
			lwsl_err("%s: DENYING (vhd is NULL !!! protocol init failed or unconfigured)\n", __func__);
			return 1;
		}

		if (vhd->unauth_protocols) {
			char ws_prot[256];
			if (lws_hdr_copy(wsi, ws_prot, sizeof(ws_prot), WSI_TOKEN_PROTOCOL) > 0) {
				/* simplistic match, sufficient for our usecase but could be tokenized */
				if ((char *)strstr(vhd->unauth_protocols, ws_prot)) {
					lwsl_notice("%s: bypassing interceptor for unauth protocol '%s'\n", __func__, ws_prot);
					return 0;
				}
			}
		}

		service_name = vhd->service_name;
		int unauth_allow = vhd->unauth_allow;
		if (uri[0]) {
			mount = lws_find_mount(wsi, uri, (int)strlen(uri));
			if (mount) {
				const char *pmo_val;
				int target_sn = !lws_pmo_get_str(mount, "service-name", &service_name);
				int target_ua = !lws_pmo_get_str(mount, "unauth-allow", &pmo_val);

				if (target_sn)
					lwsl_info("%s: using service_name %s from target pmo\n", __func__, service_name);
				if (target_ua) {
					unauth_allow = atoi(pmo_val);
					lwsl_info("%s: using unauth_allow %d from target pmo\n", __func__, unauth_allow);
				}

#if defined(LWS_WITH_JOSE)
				if (!target_sn && mount->interceptor_path) {
					const struct lws_http_mount *im = mount;
					while (im && im->interceptor_path) {
						im = lws_find_mount(wsi, im->interceptor_path, (int)strlen(im->interceptor_path));
						if (im && !lws_pmo_get_str(im, "service-name", &service_name)) {
							lwsl_info("%s: using service_name %s from interceptor pmo\n", __func__, service_name);
							break;
						}
					}
				}
				if (!target_ua && mount->interceptor_path) {
					const struct lws_http_mount *im = mount;
					while (im && im->interceptor_path) {
						im = lws_find_mount(wsi, im->interceptor_path, (int)strlen(im->interceptor_path));
						if (im && !lws_pmo_get_str(im, "unauth-allow", &pmo_val)) {
							unauth_allow = atoi(pmo_val);
							lwsl_info("%s: using unauth_allow %d from interceptor pmo\n", __func__, unauth_allow);
							break;
						}
					}
				}
#endif
			}
		}

		if (vhd->wl.count) {
			char ip[64];
			lws_sockaddr46 sa46;
			int match = 0;

			lws_get_peer_simple(wsi, ip, sizeof(ip));
			if (!lws_sa46_parse_numeric_address(ip, &sa46)) {
				lws_start_foreach_dll(struct lws_dll2 *, d, vhd->wl.head) {
					struct login_whitelist *w = lws_container_of(d, struct login_whitelist, list);
					if (!lws_sa46_on_net(&sa46, &w->sa46, w->net_len)) {
						match = 1;
						break;
					}
				} lws_end_foreach_dll(d);
			}

			if (!match) {
				lwsl_info("%s: peer %s failed whitelist\n", __func__, ip);
				return 1; /* Request intercept to serve 403 */
			}
		}

		ja = lws_jwt_auth_create(wsi, &vhd->jwk, vhd->cookie_name, lws_login_jwt_auth_cb, wsi, NULL);
		if (ja) {
			const char *did = lws_jwt_auth_get_did(ja);
			if (did && vhd->db) {
				sqlite3_stmt *stmt;
				int found_device = 0;
				if (sqlite3_prepare_v2(vhd->db, "SELECT 1 FROM devices WHERE device_id = ?", -1, &stmt, NULL) == SQLITE_OK) {
					sqlite3_bind_text(stmt, 1, did, -1, SQLITE_STATIC);
					if (sqlite3_step(stmt) == SQLITE_ROW)
						found_device = 1;
					sqlite3_finalize(stmt);
				}
				if (!found_device) {
					lwsl_notice("%s: Device %s rejected (not found in DB %s), rejecting JWT\n", __func__, did, vhd->db_path);
					lws_jwt_auth_destroy(&ja);
					return 1; /* Request to intercept */
				}
			}

			lwsl_info("%s: Valid cookie found! User authenticated.\n", __func__);
			level = lws_jwt_auth_query_grant(ja, service_name);
			if (level >= vhd->min_grant_level) {
				if (vhd->db) {
					uint32_t uid = lws_jwt_auth_get_uid(ja);
					if (uid) {
						sqlite3_stmt *stmt;
						int mismatch = 0;
						int count_db = 0;

						if (sqlite3_prepare_v2(vhd->db, "SELECT s.name, g.grant_level FROM grants g JOIN services s ON g.service_id = s.service_id WHERE g.uid = ?", -1, &stmt, NULL) == SQLITE_OK) {
							sqlite3_bind_int(stmt, 1, (int)uid);
							while (sqlite3_step(stmt) == SQLITE_ROW) {
								const char *svc_name = (const char *)sqlite3_column_text(stmt, 0);
								int gl = sqlite3_column_int(stmt, 1);
								if (!svc_name) continue;

								count_db++;
								int old_gl = lws_jwt_auth_query_grant(ja, svc_name);
								if (old_gl != gl)
									mismatch = 1;
							}
							sqlite3_finalize(stmt);
						}

						if (sqlite3_prepare_v2(vhd->db, "SELECT session_epoch FROM users WHERE uid = ?", -1, &stmt, NULL) == SQLITE_OK) {
							sqlite3_bind_int(stmt, 1, (int)uid);
							if (sqlite3_step(stmt) == SQLITE_ROW) {
								uint32_t db_epoch = (uint32_t)sqlite3_column_int(stmt, 0);
								if (db_epoch != lws_jwt_auth_get_sec(ja)) {
									lwsl_info("%s: session epoch mismatch! DB=%u JWT=%u\n", __func__, db_epoch, lws_jwt_auth_get_sec(ja));
									mismatch = 1;
								}
							}
							sqlite3_finalize(stmt);
						}

						if (count_db != (int)lws_jwt_auth_count_grants(ja))
							mismatch = 1;

						if (mismatch) {
							lws_jwt_auth_destroy(&ja);
							lwsl_info("%s: Need dynamic JWT rewrite\n", __func__);
							return 1; /* Request to intercept */
						}
					}
				}

				lws_jwt_auth_destroy(&ja);
				lwsl_info("%s: ALLOWING (User has required grant)\n", __func__);
				return 0; /* Let traffic through to the real mount */
			}
			lwsl_info("%s: JWT valid but lacks required %s grant (has %d), INTERCEPTING\n", __func__, service_name, level);
			lws_jwt_auth_destroy(&ja);
		}

		lwsl_info("%s: INTERCEPTING (NO VALID COOKIE FOUND)\n", __func__);

		if (unauth_allow) {
			lwsl_info("%s: ALLOWING UNAUTH (unauth-allow enabled)\n", __func__);
			return 0;
		}

		return 1; /* Unauthorized, intercept */
	}

	case LWS_CALLBACK_HTTP:
	{
		char dest[512], path[256], urlenc_path[512];
		const struct lws_http_mount *mount;
		int whitelist_failed = 0;
		const char *service_name;
		int n;

		if (!vhd)
			return 1;

		service_name = vhd->service_name;

		path[0] = '\0';
		n = lws_hdr_copy(wsi, path, sizeof(path), WSI_TOKEN_GET_URI);
		if (n <= 0)
			n = lws_hdr_copy(wsi, path, sizeof(path), WSI_TOKEN_POST_URI);

		if (n > 0) {
			mount = lws_find_mount(wsi, path, n);
			if (mount) {
				if (!lws_pmo_get_str(mount, "service-name", &service_name))
					lwsl_info("%s: using service_name %s from target pmo\n", __func__, service_name);

#if defined(LWS_WITH_JOSE)
				else if (mount->interceptor_path) {
					const struct lws_http_mount *im = mount;
					while (im && im->interceptor_path) {
						im = lws_find_mount(wsi, im->interceptor_path, (int)strlen(im->interceptor_path));
						if (im && !lws_pmo_get_str(im, "service-name", &service_name)) {
							lwsl_info("%s: using service_name %s from interceptor pmo\n", __func__, service_name);
							break;
						}
					}
				}
#endif
			}
		}

		if (vhd->wl.count) {
			char ip[64];
			lws_sockaddr46 sa46;
			int match = 0;

			lws_get_peer_simple(wsi, ip, sizeof(ip));
			if (!lws_sa46_parse_numeric_address(ip, &sa46)) {
				lws_start_foreach_dll(struct lws_dll2 *, d, vhd->wl.head) {
					struct login_whitelist *w = lws_container_of(d, struct login_whitelist, list);
					if (!lws_sa46_on_net(&sa46, &w->sa46, w->net_len)) {
						match = 1;
						break;
					}
				} lws_end_foreach_dll(d);
			}

			if (!match)
				whitelist_failed = 1;
		}

		if (whitelist_failed)
                  return simple_response(
                      wsi, pss, "Page Unreachable", "text/plain", 403,
                      (unsigned char *)buf + LWS_PRE, (unsigned char **)&p,
                      (unsigned char *)end);

                if (lws_hdr_copy(wsi, path, sizeof(path), WSI_TOKEN_POST_URI) > 0 &&
		    lws_login_ends_with(path, "/.lws-login-sso"))
			return 0; /* Fall through to LWS_CALLBACK_HTTP_BODY */

		if (!pss->ja)
			pss->ja = lws_jwt_auth_create(wsi, &vhd->jwk, vhd->cookie_name, lws_login_jwt_auth_cb, wsi, NULL);

		if (pss->ja) {
			uint64_t exp = lws_jwt_auth_get_exp(pss->ja);
			if (exp && exp < (uint64_t)lws_now_secs()) {
				lwsl_info("%s: JWT expired (exp %llu, now %llu)\n", __func__, (unsigned long long)exp, (unsigned long long)lws_now_secs());
				lws_jwt_auth_destroy(&pss->ja);
			}
		}

		if (pss->ja) {
			int level = lws_jwt_auth_query_grant(pss->ja, service_name);
			if (level >= vhd->min_grant_level) {
				if (vhd->db && !pss->silent_update_jwt) {
					uint32_t uid = lws_jwt_auth_get_uid(pss->ja);
					if (uid) {
						char current_grants[512];
						char *g_p = current_grants;
						char *g_end = current_grants + sizeof(current_grants);
						sqlite3_stmt *stmt;
						int first = 1;
						int mismatch = 0;
						int count_db = 0;

						g_p += lws_snprintf(g_p, lws_ptr_diff_size_t(g_end, g_p), "\"grants\":{");

						if (sqlite3_prepare_v2(vhd->db, "SELECT s.name, g.grant_level FROM grants g JOIN services s ON g.service_id = s.service_id WHERE g.uid = ?", -1, &stmt, NULL) == SQLITE_OK) {
							sqlite3_bind_int(stmt, 1, (int)uid);
							while (sqlite3_step(stmt) == SQLITE_ROW) {
								const char *svc_name = (const char *)sqlite3_column_text(stmt, 0);
								int gl = sqlite3_column_int(stmt, 1);
								if (!svc_name) continue;

								count_db++;
								int old_gl = lws_jwt_auth_query_grant(pss->ja, svc_name);
								if (old_gl != gl)
									mismatch = 1;

								if (!first)
									g_p += lws_snprintf(g_p, lws_ptr_diff_size_t(g_end, g_p), ",");
								first = 0;
								g_p += lws_snprintf(g_p, lws_ptr_diff_size_t(g_end, g_p), "\"%s\":%d", svc_name, gl);
							}
							sqlite3_finalize(stmt);
						}
						g_p += lws_snprintf(g_p, lws_ptr_diff_size_t(g_end, g_p), "}");

						if (count_db != (int)lws_jwt_auth_count_grants(pss->ja))
							mismatch = 1;

						if (mismatch) {
							char temp[1024];
							char out[2048];
							size_t out_len = sizeof(out);
							uint64_t now = (uint64_t)time(NULL);
							uint64_t exp = now + vhd->jwt_validity_secs;
							const char *sub = lws_jwt_auth_get_sub(pss->ja);

							if (!lws_jwt_sign_compact(vhd->context, &vhd->jwk, "ES256",
													  out, &out_len, temp, sizeof(temp),
													  "{\"iss\":\"%s\",\"sub\":\"%s\",\"uid\":%u,"
													  "\"iat\":%llu,\"exp\":%llu,%s}",
													  vhd->auth_domain, sub ? sub : "Unknown", uid,
													  (unsigned long long)now, (unsigned long long)exp,
													  current_grants)) {
								pss->silent_update_jwt = strdup(out);
							}
						}
					}
				}
			}

			if (pss->silent_update_jwt) {
				char cookie[LWS_SSO_MAX_COOKIE], host[128], fq_uri[512];
				const char *h = NULL;

				if (vhd->cookie_domain[0])
					lws_snprintf(cookie, sizeof(cookie), "%s=%s; Path=/; Domain=%s; Max-Age=%llu; HttpOnly; SameSite=Lax; Secure",
							 vhd->cookie_name, pss->silent_update_jwt, vhd->cookie_domain, (unsigned long long)vhd->jwt_validity_secs);
				else
					lws_snprintf(cookie, sizeof(cookie), "%s=%s; Path=/; Max-Age=%llu; HttpOnly; SameSite=Lax; Secure",
							 vhd->cookie_name, pss->silent_update_jwt, (unsigned long long)vhd->jwt_validity_secs);

				path[0] = '\0';
				if (lws_hdr_copy(wsi, path, sizeof(path), WSI_TOKEN_GET_URI) < 0)
					lwsl_debug("%s: URI copy failed\n", __func__);

				host[0] = '\0';
				if (lws_hdr_copy(wsi, host, sizeof(host), WSI_TOKEN_HOST) > 0)
					h = host;
#if defined(LWS_ROLE_H2)
				else if (lws_hdr_copy(wsi, host, sizeof(host), WSI_TOKEN_HTTP_COLON_AUTHORITY) > 0)
					h = host;
#endif
				if (!h) {
					struct lws_vhost *vh = lws_get_vhost(wsi);
					if (vh) {
						const char *vname = lws_get_vhost_name(vh);
						if (vname)
							h = vname;
					}
				}

				{
					const char *scheme = "http";
#if defined(LWS_WITH_CUSTOM_HEADERS)
					char proto[16] = "";

					if (lws_hdr_custom_copy(wsi, proto, sizeof(proto), "x-forwarded-proto:", 18) > 0) {
						if (!strcasecmp(proto, "https"))
							scheme = "https";
					} else
#endif
					if (lws_is_ssl(lws_get_network_wsi(wsi))) {
						scheme = "https";
					}

					lws_snprintf(fq_uri, sizeof(fq_uri), "%s://%s%s",
						     scheme,
						     h ? h : "localhost", path);
				}

				if (lws_add_http_common_headers(wsi, HTTP_STATUS_FOUND, "text/html", 0, (unsigned char **)&p, (unsigned char *)end)) return 1;
				if (lws_add_http_header_by_name(wsi, (unsigned char *)"set-cookie:", (unsigned char *)cookie, (int)strlen(cookie), (unsigned char **)&p, (unsigned char *)end)) return 1;
				if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION, (unsigned char *)fq_uri, (int)strlen(fq_uri), (unsigned char **)&p, (unsigned char *)end)) return 1;
				if (lws_finalize_http_header(wsi, (unsigned char **)&p, (unsigned char *)end)) return 1;

				lws_write(wsi, (unsigned char *)buf + LWS_PRE, lws_ptr_diff_size_t(p, buf + LWS_PRE), LWS_WRITE_HTTP_HEADERS | LWS_WRITE_H2_STREAM_END);

				free(pss->silent_update_jwt);
				pss->silent_update_jwt = NULL;

				return lws_http_transaction_completed(wsi);
			}
		}

		path[0] = '\0';
		if (lws_hdr_copy(wsi, path, sizeof(path), WSI_TOKEN_GET_URI) <= 0)
			if (lws_hdr_copy(wsi, path, sizeof(path), WSI_TOKEN_POST_URI) < 0)
				lwsl_debug("%s: URI copy failed\n", __func__);

		if (lws_login_ends_with(path, "/lws-login.css")) {
			if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, "text/css",
							(lws_filepos_t)strlen(canned_css), (unsigned char **)&p, (unsigned char *)end))
				return 1;
			if (lws_finalize_http_header(wsi, (unsigned char **)&p, (unsigned char *)end))
				return 1;
			lws_write(wsi, (unsigned char *)buf + LWS_PRE, lws_ptr_diff_size_t(p, buf + LWS_PRE), LWS_WRITE_HTTP_HEADERS);
			size_t len = strlen(canned_css);
			int res = lws_buflist_append_segment(&pss->tx_buflist, (const uint8_t *)canned_css, len);
			if (res < 0)
				return -1;
			pss->tx_remaining = len;
			lws_callback_on_writable(wsi);

			return 0;
		}

		if (lws_login_ends_with(path, "/lws-login.js")) {
			if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, "application/javascript",
							(lws_filepos_t)strlen(canned_js), (unsigned char **)&p, (unsigned char *)end))
				return 1;
			if (lws_finalize_http_header(wsi, (unsigned char **)&p, (unsigned char *)end))
				return 1;
			lws_write(wsi, (unsigned char *)buf + LWS_PRE, lws_ptr_diff_size_t(p, buf + LWS_PRE), LWS_WRITE_HTTP_HEADERS);
			size_t len = strlen(canned_js);
			int res = lws_buflist_append_segment(&pss->tx_buflist, (const uint8_t *)canned_js, len);
			if (res < 0)
				return -1;
			pss->tx_remaining = len;
			lws_callback_on_writable(wsi);

			return 0;
		}

		if (lws_login_ends_with(path, "/.lws-login-refresh")) {
			char csrf[64] = {0};
			size_t csrf_len = sizeof(csrf);
			char refresh_session[128] = {0};
			size_t refresh_session_len = sizeof(refresh_session);
			int ck_len;

			ck_len = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_COOKIE);

			if (ck_len > 0) {
				char *cookie = malloc((size_t)ck_len + 1);
				if (cookie) {
					if (lws_hdr_copy(wsi, cookie, ck_len + 1, WSI_TOKEN_HTTP_COOKIE) > 0) {
						/* We just need to know it exists to proceed */
						if (lws_http_cookie_get(wsi, "auth_csrf", csrf, &csrf_len) == 0 &&
						    lws_http_cookie_get(wsi, "auth_refresh_session", refresh_session, &refresh_session_len) == 0 &&
						    csrf[0]) {
							struct pending_login_refresh *ps = malloc(sizeof(*ps));
							if (ps) {
								struct lws_client_connect_info i;
								lws_parse_uri_t *puri;

								memset(ps, 0, sizeof(*ps));
								ps->vhd = vhd;
								ps->wsi_server = wsi;
								lws_strncpy(ps->cookie_hdr, cookie, sizeof(ps->cookie_hdr));

								ps->payload_len = lws_snprintf(ps->payload, sizeof(ps->payload), "csrf_token=%s", csrf);
								ps->payload_pos = 0;

								lws_dll2_add_tail(&ps->list, &vhd->pending_refresh_list);
								lws_sul_schedule(vhd->context, 0, &ps->sul, sul_pending_refresh_cb, 5 * 60 * LWS_US_PER_SEC);

								puri = lws_parse_uri_create(vhd->auth_server_url);
								if (puri) {
									lwsl_notice("%s: Intercepted background refresh request, initiating proxy to %s/api/sso_exchange\n", __func__, vhd->auth_server_url);
									memset(&i, 0, sizeof(i));
									i.context        = vhd->context;
									i.address        = puri->host;
									i.port           = puri->port;
									i.ssl_connection = !strcmp(puri->scheme, "http") ? 0 : LCCSCF_USE_SSL;
									i.path           = "/api/sso_exchange";
									i.host           = i.address;
									i.origin         = i.address;
									i.method         = "POST";
									i.protocol       = "lws_login_client";
									i.pwsi           = &ps->wsi_client;
									i.userdata       = ps;

									lws_client_connect_via_info(&i);
									lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_CONTENT, 30);
									lws_parse_uri_destroy(&puri);

									free(cookie);
									return 0; // Suspend!
								}
								free(ps);
							}
						}
					}
					free(cookie);
				}
			}
			/* Failure or no cookies, 401 Unauthorized */
			lwsl_notice("%s: Missing or malformed cookies for background refresh\n", __func__);
                        return simple_response(wsi, pss, "Missing Authorization", "text/plain",
                                            HTTP_STATUS_UNAUTHORIZED, (unsigned char *)buf + LWS_PRE,
                                            (unsigned char **)&p, (unsigned char *)end);
		}

		char host[128];
		char fq_uri[512];
		const char *h = NULL;

		host[0] = '\0';
		if (lws_hdr_copy(wsi, host, sizeof(host), WSI_TOKEN_HOST) > 0)
			h = host;
#if defined(LWS_ROLE_H2)
		else if (lws_hdr_copy(wsi, host, sizeof(host), WSI_TOKEN_HTTP_COLON_AUTHORITY) > 0)
			h = host;
#endif

		if (!h) {
			struct lws_vhost *vh = lws_get_vhost(wsi);
			if (vh) {
				const char *vname = lws_get_vhost_name(vh);
				if (vname)
					h = vname;
			}
		}

		{
			const char *scheme = "http";
#if defined(LWS_WITH_CUSTOM_HEADERS)
			char proto[16] = "";

			if (lws_hdr_custom_copy(wsi, proto, sizeof(proto), "x-forwarded-proto:", 18) > 0) {
				if (!strcasecmp(proto, "https"))
					scheme = "https";
			} else
#endif
			if (lws_is_ssl(lws_get_network_wsi(wsi))) {
				scheme = "https";
			}

			lws_snprintf(fq_uri, sizeof(fq_uri), "%s://%s%s",
				     scheme,
				     h ? h : "localhost",
				     path);
		}

		lws_urlencode(urlenc_path, fq_uri, sizeof(urlenc_path));

		size_t asu_len = strlen(vhd->auth_server_url);
		lws_snprintf(dest, sizeof(dest), "%s%s?service_name=%s&redirect_uri=%s",
			vhd->auth_server_url,
			(asu_len > 0 && vhd->auth_server_url[asu_len - 1] == '/') ? "" : "/",
			service_name, urlenc_path);

		if (lws_login_ends_with(path, "/.lws-login-status")) {
			char pl[1024];

			if (pss && pss->ja) {
				const char *sub = lws_jwt_auth_get_sub(pss->ja);
				int level = lws_jwt_auth_query_grant(pss->ja, service_name);
				int is_admin = lws_jwt_auth_query_grant(pss->ja, "*") >= 1;
				int has_grant = level >= 1;
				lws_snprintf(pl, sizeof(pl), "{\"logged_in\":1,\"server_now\":%llu,\"exp\":%llu,\"has_grant\":%d,\"grant_level\":%d,\"identity\":\"%s\",\"auth_server_url\":\"%s\",\"login_url\":\"%s\",\"is_admin\":%d}",
					(unsigned long long)lws_now_secs(), (unsigned long long)lws_jwt_auth_get_exp(pss->ja), has_grant, level, sub ? sub : "Unknown", vhd->auth_server_url ? vhd->auth_server_url : "", dest, is_admin);
			} else
				lws_snprintf(pl, sizeof(pl), "{\"logged_in\":0,\"server_now\":%llu,\"auth_server_url\":\"%s\",\"login_url\":\"%s\"}", (unsigned long long)lws_now_secs(), vhd->auth_server_url ? vhd->auth_server_url : "", dest);

                        return simple_response(wsi, pss, pl, "application/json",
                                               HTTP_STATUS_OK, (unsigned char *)buf + LWS_PRE, (unsigned char **)&p,
                                               (unsigned char *)end);
		}

		if (lws_login_ends_with(path, "/.lws-login-logout")) {
			char redirect_uri[512];
			char u[1024];
			char cookie_hdr1[256], cookie_hdr1_host[256];
			char exp[64];
			time_t t = 0;
#if defined(WIN32) || defined(_WIN32)
			struct tm tmp;
			struct tm *tm = gmtime_s(&tmp, &t) == 0 ? &tmp : NULL;
#else
			struct tm tmp;
			struct tm *tm = gmtime_r(&t, &tmp);
#endif
			if (tm)
				strftime(exp, sizeof(exp), "%a, %d %b %Y %H:%M:%S GMT", tm);
			else
				exp[0] = '\0';

			redirect_uri[0] = '\0';
			if (lws_get_urlarg_by_name_safe(wsi, "redirect_uri=", redirect_uri, sizeof(redirect_uri)) >= 0)
				lws_urldecode(redirect_uri, redirect_uri, sizeof(redirect_uri));
			if (!redirect_uri[0])
				lws_strncpy(redirect_uri, "/", sizeof(redirect_uri));

			if (vhd->cookie_domain[0])
				lws_snprintf(cookie_hdr1, sizeof(cookie_hdr1), "%s=; Path=/; Domain=%s; Expires=%s; Max-Age=0; HttpOnly; SameSite=Lax; Secure", vhd->cookie_name, vhd->cookie_domain, exp);
			else
				lws_snprintf(cookie_hdr1, sizeof(cookie_hdr1), "%s=; Path=/; Expires=%s; Max-Age=0; HttpOnly; SameSite=Lax; Secure", vhd->cookie_name, exp);

			lws_snprintf(cookie_hdr1_host, sizeof(cookie_hdr1_host), "%s=; Path=/; Expires=%s; Max-Age=0; HttpOnly; SameSite=Lax; Secure", vhd->cookie_name, exp);

			char urlenc_path[512];
			lws_urlencode(urlenc_path, redirect_uri, sizeof(urlenc_path));

			if (vhd->auth_server_url && vhd->auth_server_url[0]) {
				lws_snprintf(u, sizeof(u), "%s/api/logout?redirect_uri=%s", vhd->auth_server_url, urlenc_path);
			} else {
				/* Fallback if auth_server_url somehow missing */
				lws_strncpy(u, redirect_uri, sizeof(u));
			}

			char html[1024];
			int html_len = lws_snprintf(html, sizeof(html),
				"<html lang=\"en\"><head><meta http-equiv=\"refresh\" content=\"0; url=%s\"></head><body>Redirecting to <a href=\"%s\">%s</a></body></html>",
				u, u, u);
			if (lws_buflist_append_segment(&pss->tx_buflist, (uint8_t *)html, (size_t)html_len) < 0) return -1;
			pss->tx_remaining = (size_t)html_len;

			if (lws_add_http_common_headers(wsi, HTTP_STATUS_SEE_OTHER, "text/html", (unsigned int)html_len, (unsigned char **)&p, (unsigned char *)end)) return 1;
			if (lws_add_http_header_by_name(wsi, (unsigned char *)"set-cookie:", (unsigned char *)cookie_hdr1, (int)strlen(cookie_hdr1), (unsigned char **)&p, (unsigned char *)end)) return 1;
			if (lws_add_http_header_by_name(wsi, (unsigned char *)"set-cookie:", (unsigned char *)cookie_hdr1_host, (int)strlen(cookie_hdr1_host), (unsigned char **)&p, (unsigned char *)end)) return 1;
			if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION, (unsigned char *)u, (int)strlen(u), (unsigned char **)&p, (unsigned char *)end)) return 1;
			goto fin_hdrs;
		}

		lwsl_info("%s: bouncing unauth to %s\n", __func__, dest);

		char html[1024];
		int html_len = lws_snprintf(html, sizeof(html),
			"<html lang=\"en\"><head><meta http-equiv=\"refresh\" content=\"0; url=%s\"></head><body>Redirecting to <a href=\"%s\">%s</a></body></html>",
			dest, dest, dest);
		if (lws_buflist_append_segment(&pss->tx_buflist, (uint8_t *)html, (size_t)html_len) < 0) return -1;
		pss->tx_remaining = (size_t)html_len;

		if (lws_add_http_common_headers(wsi, HTTP_STATUS_SEE_OTHER, "text/html", (unsigned int)html_len, (unsigned char **)&p, (unsigned char *)end))
			return 1;

		if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION,
				(unsigned char *)dest, (int)strlen(dest), (unsigned char **)&p, (unsigned char *)end))
			return 1;
 fin_hdrs:
		if (lws_finalize_http_header(wsi, (unsigned char **)&p, (unsigned char *)end))
			return 1;

		lws_write(wsi, (unsigned char *)buf + LWS_PRE, lws_ptr_diff_size_t(p, buf + LWS_PRE), LWS_WRITE_HTTP_HEADERS);
		lws_callback_on_writable(wsi);
		return 0;
	}

	case LWS_CALLBACK_HTTP_BODY:
	{
		char path[256];
		path[0] = '\0';
		if (lws_hdr_copy(wsi, path, sizeof(path), WSI_TOKEN_POST_URI) < 0)
			lwsl_debug("%s: URI copy failed\n", __func__);

		if (lws_login_ends_with(path, "/.lws-login-sso")) {
			if (!pss->spa) {
				pss->spa = lws_spa_create(wsi, param_names,
							  LWS_ARRAY_SIZE(param_names),
							  LWS_SSO_MAX_COOKIE, NULL, NULL);
				if (!pss->spa)
					return -1;
			}
			if (lws_spa_process(pss->spa, (const char *)in, (int)len)) {
				lws_spa_finalize(pss->spa);
				return -1;
			}
		}
		return 0;
	}

	case LWS_CALLBACK_HTTP_BODY_COMPLETION:
	{
		char path[256];

		if (!vhd)
			return 1;

		path[0] = '\0';
		if (lws_hdr_copy(wsi, path, sizeof(path), WSI_TOKEN_POST_URI) < 0)
			lwsl_debug("%s: URI copy failed\n", __func__);

		if (lws_login_ends_with(path, "/.lws-login-sso")) {
			if (pss->spa) {
				lws_spa_finalize(pss->spa);
				const char *token = lws_spa_get_string(pss->spa, EPN_TOKEN);
				const char *target = lws_spa_get_string(pss->spa, EPN_TARGET);

				char origin[128], referer[256];
				const char *chk_url = NULL;
				int has_origin = lws_hdr_copy(wsi, origin, sizeof(origin), WSI_TOKEN_ORIGIN) > 0;
				int has_referer = lws_hdr_copy(wsi, referer, sizeof(referer), WSI_TOKEN_HTTP_REFERER) > 0;

				if (has_origin && strcmp(origin, "null")) {
					chk_url = origin;
				} else if (has_referer) {
					chk_url = referer;
				}

				if (token && vhd && vhd->auth_server_url && chk_url) {
					lws_parse_uri_t *puri_auth = lws_parse_uri_create(vhd->auth_server_url);
					lws_parse_uri_t *puri_chk = lws_parse_uri_create(chk_url);

					if (puri_auth && puri_chk) {
						if (strcmp(puri_auth->scheme, puri_chk->scheme) ||
						    strcasecmp(puri_auth->host, puri_chk->host) ||
						    puri_auth->port != puri_chk->port) {
							lwsl_err("%s: blocking SSO CSRF from origin/referer %s (expected %s)\n",
								 __func__, chk_url, vhd->auth_server_url);
							token = NULL;
						} else {
							lwsl_notice("%s: allowing SSO request matching auth server origin %s\n",
								    __func__, chk_url);
						}
					}
					if (puri_auth) lws_parse_uri_destroy(&puri_auth);
					if (puri_chk) lws_parse_uri_destroy(&puri_chk);
				}

				if (token && target && vhd) {
					char temp[2048], out[2048];
					size_t out_len = sizeof(out);

					/* Ensure signature is authentic using broad algorithms. */
					if (!lws_jwt_signed_validate(vhd->context, &vhd->jwk, "ES256,ES384,ES512,RS256,RS384,RS512",
								    token, strlen(token), temp, sizeof(temp), out, &out_len)) {
						pss->silent_update_jwt = strdup(token);
					}
				}

				const char *final_target = "/";
				if (target && target[0]) {
					if (target[0] == '/' && target[1] != '/') {
						final_target = target;
					} else {
						lws_parse_uri_t *puri_tgt = lws_parse_uri_create(target);
						if (puri_tgt) {
							char host[128] = "";
							const char *h = NULL;
							if (lws_hdr_copy(wsi, host, sizeof(host), WSI_TOKEN_HOST) > 0)
								h = host;
#if defined(LWS_ROLE_H2)
							else if (lws_hdr_copy(wsi, host, sizeof(host), WSI_TOKEN_HTTP_COLON_AUTHORITY) > 0)
								h = host;
#endif
							if (!h) {
								struct lws_vhost *vh = lws_get_vhost(wsi);
								if (vh) h = lws_get_vhost_name(vh);
							}
							if (h && !strcasecmp(puri_tgt->host, h)) {
								final_target = target;
							} else if (vhd->db) {
								if (auth_verify_redirect_uri(vhd, target)) {
									final_target = target;
								} else {
									lwsl_err("%s: untrusted absolute target %s\n", __func__, target);
								}
							}
							lws_parse_uri_destroy(&puri_tgt);
						}
					}
				}

				if (pss->silent_update_jwt && final_target) {
					char cookie[LWS_SSO_MAX_COOKIE];
					if (vhd->cookie_domain[0]) {
						lws_snprintf(cookie, sizeof(cookie), "%s=%s; Path=/; Domain=%s; Max-Age=%llu; HttpOnly; SameSite=Lax; Secure",
							     vhd->cookie_name, pss->silent_update_jwt, vhd->cookie_domain, (unsigned long long)vhd->jwt_validity_secs);
					} else {
						lws_snprintf(cookie, sizeof(cookie), "%s=%s; Path=/; Max-Age=%llu; HttpOnly; SameSite=Lax; Secure",
							     vhd->cookie_name, pss->silent_update_jwt, (unsigned long long)vhd->jwt_validity_secs);
					}

					if (lws_add_http_common_headers(wsi, HTTP_STATUS_FOUND, "text/html", 0, (unsigned char **)&p, (unsigned char *)end)) return 1;
					if (lws_add_http_header_by_name(wsi, (unsigned char *)"set-cookie:", (unsigned char *)cookie, (int)strlen(cookie), (unsigned char **)&p, (unsigned char *)end)) return 1;
					if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION, (unsigned char *)final_target, (int)strlen(final_target), (unsigned char **)&p, (unsigned char *)end)) return 1;
					if (lws_finalize_http_header(wsi, (unsigned char **)&p, (unsigned char *)end)) return 1;
					lws_write(wsi, (unsigned char *)buf + LWS_PRE, lws_ptr_diff_size_t(p, buf + LWS_PRE), LWS_WRITE_HTTP_HEADERS | LWS_WRITE_H2_STREAM_END);

					return lws_http_transaction_completed(wsi);
				} else {
					const char *err = "Invalid SSO Token";
					int err_len = (int)strlen(err);
					if (lws_add_http_common_headers(wsi, HTTP_STATUS_FORBIDDEN, "text/plain", (lws_filepos_t)err_len, (unsigned char **)&p, (unsigned char *)end)) return 1;
					if (lws_finalize_http_header(wsi, (unsigned char **)&p, (unsigned char *)end)) return 1;
					lws_write(wsi, (unsigned char *)buf + LWS_PRE, lws_ptr_diff_size_t(p, buf + LWS_PRE), LWS_WRITE_HTTP_HEADERS);
					int res = lws_buflist_append_segment(&pss->tx_buflist, (const uint8_t *)err, (size_t)err_len);
					if (res < 0) return -1;
					pss->tx_remaining = (size_t)err_len;
					lws_callback_on_writable(wsi);
					return 0;
				}
			}
		}
		return 0;
	}

	case LWS_CALLBACK_HTTP_WRITEABLE:
	{
		unsigned char buf[LWS_SSO_MAX_COOKIE + LWS_PRE], *p = buf + LWS_PRE, *end = buf + sizeof(buf) - 1;
		struct pending_login_refresh *ps = NULL;

		lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
					   lws_dll2_get_head(&vhd->pending_refresh_list)) {
			struct pending_login_refresh *s = lws_container_of(d, struct pending_login_refresh, list);

			if (s->wsi_server == wsi) {
				ps = s;
				break;
			}
		} lws_end_foreach_dll_safe(d, d1);

		if (ps) {
			if (ps->token[0]) {
				char cookie[LWS_SSO_MAX_COOKIE];
				int n;

				if (vhd->cookie_domain[0]) {
					n = lws_snprintf(cookie, sizeof(cookie), "%s=%s; Path=/; Domain=%s; Max-Age=%llu; HttpOnly; SameSite=Lax; Secure",
							 vhd->cookie_name, ps->token, vhd->cookie_domain, (unsigned long long)vhd->jwt_validity_secs);
				} else {
					n = lws_snprintf(cookie, sizeof(cookie), "%s=%s; Path=/; Max-Age=%llu; HttpOnly; SameSite=Lax; Secure",
							 vhd->cookie_name, ps->token, (unsigned long long)vhd->jwt_validity_secs);
				}

				if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, "application/json", 13, (unsigned char **)&p, (unsigned char *)end)) return 1;
				if (lws_add_http_header_by_name(wsi, (unsigned char *)"set-cookie:", (unsigned char *)cookie, n, (unsigned char **)&p, (unsigned char *)end)) return 1;
				if (lws_finalize_http_header(wsi, (unsigned char **)&p, (unsigned char *)end)) return 1;

				lws_write(wsi, buf + LWS_PRE, lws_ptr_diff_size_t(p, buf + LWS_PRE), LWS_WRITE_HTTP_HEADERS);
				if (lws_buflist_append_segment(&pss->tx_buflist, (const uint8_t *)"{\"success\":1}", 13) < 0) return -1;
				pss->tx_remaining = 13;
				lwsl_notice("%s: Successfully issued refreshed token to browser via BFF\n", __func__);
			} else {
				if (lws_add_http_common_headers(wsi, HTTP_STATUS_UNAUTHORIZED, "application/json", 13, (unsigned char **)&p, (unsigned char *)end)) return 1;
				if (lws_finalize_http_header(wsi, (unsigned char **)&p, (unsigned char *)end)) return 1;
				lws_write(wsi, buf + LWS_PRE, lws_ptr_diff_size_t(p, buf + LWS_PRE), LWS_WRITE_HTTP_HEADERS);
				if (lws_buflist_append_segment(&pss->tx_buflist, (const uint8_t *)"{\"success\":0}", 13) < 0) return -1;
				pss->tx_remaining = 13;
				lwsl_notice("%s: BFF SSO Exchange denied by Server\n", __func__);
			}

			lws_sul_cancel(&ps->sul);
			lws_dll2_remove(&ps->list);
			free(ps);

			lws_callback_on_writable(wsi);

			return 0;
		}

		if (!pss || !pss->tx_buflist)
			break;

		uint8_t *pout;
		size_t bytes = lws_buflist_next_segment_len(&pss->tx_buflist, &pout);

		if (!bytes)
			break;

		/*
		 * We must set LWS_WRITE_HTTP_FINAL on the write that sends the last
		 * bytes of the body, so that under HTTP/2 the END_STREAM flag lands on
		 * the final DATA frame.  Comparing chunk to lws_buflist_total_len() is
		 * wrong once a segment is partly consumed: total_len() sums each
		 * segment's original len without subtracting pos, so the second chunk
		 * of a body larger than the write buffer never compares equal, FINAL
		 * is never set, and browsers report NS_ERROR_NET_PARTIAL_TRANSFER.
		 * struct lws_buflist is opaque to plugins, so we track the remaining
		 * body length in pss->tx_remaining and mark FINAL when this chunk
		 * brings it to zero.
		 */
		size_t chunk = bytes;
		if (chunk > sizeof(buf) - LWS_PRE)
			chunk = sizeof(buf) - LWS_PRE;

		memcpy(p, pout, chunk);

		int flags = (chunk >= pss->tx_remaining) ?
				LWS_WRITE_HTTP_FINAL : LWS_WRITE_HTTP;

		int m = lws_write(wsi, p, (unsigned int)chunk, (enum lws_write_protocol)flags);
		if (m < 0) return -1;

		pss->tx_remaining -= (size_t)m;
		lws_buflist_use_segment(&pss->tx_buflist, (size_t)m);

		if (lws_buflist_next_segment_len(&pss->tx_buflist, &pout)) {
			lws_callback_on_writable(wsi);
			return 0;
		}

		return lws_http_transaction_completed(wsi);
	}

		case LWS_CALLBACK_CLOSED_HTTP:
		case LWS_CALLBACK_CLOSED:
			if (pss && pss->tx_buflist) {
				lws_buflist_destroy_all_segments(&pss->tx_buflist);
				pss->tx_remaining = 0;
			}

		if (pss && pss->ja)
			lws_jwt_auth_destroy(&pss->ja);

                if (pss && pss->silent_update_jwt) {
			free(pss->silent_update_jwt);
			pss->silent_update_jwt = NULL;
		}

		if (pss && pss->spa) {
			lws_spa_destroy(pss->spa);
			pss->spa = NULL;
		}

		if (vhd) {
			lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
						   lws_dll2_get_head(&vhd->pending_refresh_list)) {
				struct pending_login_refresh *s = lws_container_of(d, struct pending_login_refresh, list);

                                if (s->wsi_server == wsi) {
					s->wsi_server = NULL;
					lwsl_notice("%s: cleared dangling wsi_server from pending refresh\n", __func__);
				}
			} lws_end_foreach_dll_safe(d, d1);
		}
		break;

	default:
		break;
	}

	return 0;
}

#define LWS_PLUGIN_PROTOCOL_LWS_LOGIN \
	{ \
		"lws-login", \
		callback_lws_login, \
		sizeof(struct pss_login), \
		1024, 0, NULL, 0 \
	}

#if !defined (LWS_PLUGIN_STATIC)

LWS_VISIBLE const struct lws_protocols protocols[] = {
	LWS_PLUGIN_PROTOCOL_LWS_LOGIN,
	{
		.name = "lws_login_client",
		.callback = callback_lws_login_client,
		.per_session_data_size = 0,
		.rx_buffer_size = 0,
		.id = 0,
		.user = NULL,
		.tx_packet_size = 0
	}
};

LWS_VISIBLE const lws_plugin_protocol_t lws_login = {
	.hdr = {
		.name = "lws login",
		._class = "lws_protocol_plugin",
		.lws_build_hash = LWS_BUILD_HASH,
		.api_magic = LWS_PLUGIN_API_MAGIC
	},

	.protocols = protocols,
	.count_protocols = LWS_ARRAY_SIZE(protocols),
	.extensions = NULL,
	.count_extensions = 0,
};

#endif
