Author: Andy Green Date: Sun Sep 06 07:35:36 2026 +0100 web: drop browser-originated watcher_services, harden server web link rx, fixes F-002 An unauthenticated browser could send {"schema":"com.warmcat.sai.watcher_services"} on /sai/browse*: the schema is in the browser rx map and its switch case fell into the generic forward to sai-server over the nailed-up websrv ss link. But sai-server's web link schema map has no entry for it, so the decode failed and websrvss_ws_rx() returned LWSSSSRET_DISCONNECT_ME, tearing down the ss link; the retry policy reconnects after 1/2/3s backoff so a repeat sender keeps the control plane flapping, and admin operations forwarded on it fail while it is down. Watcher services are a server config-file concern (sais_config_watchers()); the schema only ever flows web -> browsers. Give it its own case that logs and drops it, matching the loadreport treatment from F-001. As a second layer, sai-server no longer treats an undecodable message on the web link as fatal: the link carries forwarded browser requests whose content we do not control, so a message that fails to decode is logged, its partial lwsac freed, and skipped, reserving disconnect for protocol-level unrecoverable states. This also fixes a small lwsac leak on the decode-failure path. diff --git a/src/server/s-ws-web.c b/src/server/s-ws-web.c index 0a22b2e..7b3600f 100644 --- a/src/server/s-ws-web.c +++ b/src/server/s-ws-web.c @@ -467,10 +467,19 @@ websrvss_ws_rx(void *userobj, const uint8_t *buf, size_t len, int flags) lws_struct_json_init_parse(&m->ctx, NULL, &a); n = lejp_parse(&m->ctx, (uint8_t *)buf, (int)len); if (n < 0 || !a.dest) { + /* + * This link carries forwarded browser requests whose content + * we do not control, so an undecodable message is skipped, + * not fatal: tearing down the ss link here would take the + * nailed-up control channel away from every connected + * sai-web instance for the retry period. + */ lwsl_hexdump_notice(buf, len); lwsl_notice("%s: notification JSON decode failed '%s'\n", __func__, lejp_error_to_string(n)); - return LWSSSSRET_DISCONNECT_ME; + lwsac_free(&a.ac); + + return 0; } // lwsl_notice("%s: schema idx %d\n", __func__, a.top_schema_index); diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c index 3ac9dc2..7c57ae5 100644 --- a/src/web/w-ws-browser.c +++ b/src/web/w-ws-browser.c @@ -1090,7 +1090,6 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, */ break; - case SAIM_WS_BROWSER_RX_WATCHER_SERVICES: case SAIM_WS_BROWSER_RX_OPENSHELL: case SAIM_WS_BROWSER_RX_CLOSESHELL: case SAIM_WS_BROWSER_RX_PTYDATA: @@ -1106,6 +1105,17 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, lwsl_notice("%s: dropping loadreport from browser\n", __func__); goto ok; + /* + * Watcher services are a server config-file concern; the schema only + * ever flows from us towards browsers. A browser sending one is + * meaningless: drop it locally rather than forward it, sai-server + * does not accept this schema on the web link. + */ + case SAIM_WS_BROWSER_RX_WATCHER_SERVICES: + lwsl_notice("%s: dropping watcher_services from browser\n", + __func__); + goto ok; + default: /* * No schema in the map today reaches here. If one is added