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 / assets / linux-debian-sid32.svg
Author[]Andy Green <andy@warmcat.com> 2026-09-21 07:06 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-09-21 07:06 UTC
Treec6ff7ec4e41293371fc702ff94e7649b9f9cd5c2   Raw Patch
 
server, web: take the websrv control link socket path from the conf, fixes F-015
server, web: take the websrv control link socket path from the conf, fixes F-015

The sai-web <-> sai-server ss control link listened on the abstract-
namespace socket +@com.warmcat.sai-websrv, hardcoded on both sides.
Abstract sockets have no filesystem representation, so nothing gates
who may connect: any local uid could speak the websrv JSON protocol
directly, which is admin-equivalent (eventdelete, taskreset, taskkill
and taskclone, whose build script a builder then executes verbatim).

Make the lejp conf the single source of truth for where the link lives:
both daemons take an optional "sockpath" pvo in their com-warmcat-sai
protocol section, which must be the same path on both sides.  At
protocol init, each side overlays "+<sockpath>" on to the websrv
streamtype endpoint with lws_ss_policy_overlay() before creating its
stream, so a path-based unix socket is what gets bound and connected.

Nothing else is needed for the permissions.  lws binds the ss server's
socket during protocol init, before it drops privileges, and (as with
any path-based listen socket) gives it the conf uid:gid with mode 0660,
so only sai-server's user and members of its group can connect: the
operator puts the sai-web user in that group.  No runtime directory,
no fixed path in the code, no chown / chmod / stat dance in sai-server.

Without a conf sockpath, both sides fall back to the old abstract name
and warn at startup, so existing deployments keep working unchanged
until the operator adds the pvo to both confs.  The example confs and
README document the pvo and the group membership it needs.
diff --git a/README.md b/README.md index ab255b1..36ff504 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,40 @@ the browser, with JWT-authentication for manual job control. most cases spend most of their time idle, this enables a very good optimization of average power down to nearly zero. +## sai-web <-> sai-server control link ("sockpath") + +sai-web is the only thing that talks to sai-server on behalf of browsers, +over a unix socket ws link that sai-server serves and sai-web connects to. +Everything the UI can do with admin rights arrives at sai-server over this +link and is trusted (deleting events, resetting and cloning tasks, whose +build scripts the builders then run), so who can connect to the socket is who +has admin on the CI. + +Both daemons take the socket path from the optional `sockpath` pvo in the +`vhosts|ws-protocols|com-warmcat-sai` section of their conf, and it must be +the same on both sides: + +``` + "sockpath": "/var/run/sai-websrv", +``` + +sai-server binds it during protocol init, before dropping privileges, and lws +gives the socket sai-server's conf `uid`:`gid` with mode 0660 (the same way it +treats any path-based listen socket). So only that user and members of that +group can connect: put the user sai-web runs as in sai-server's group, eg, +with the example confs (sai-server runs as `apache`, sai-web as `sai`) + +``` +# usermod -a -G apache sai +``` + +If `sockpath` is not set on a side, that side falls back to the +abstract-namespace socket `@com.warmcat.sai-websrv` that older confs used, and +warns at startup: abstract sockets have no filesystem permissions, so any +local user on the host can connect to the link. Existing deployments keep +working unchanged, but should add the pvo to both confs and restart both +daemons (sai-server first, sai-web reconnects by itself). + ## Build flow and support for embedded ![build flow](READMEs/sai-build-test-flow.png) diff --git a/etc-sai-EXAMPLE/server/conf.d/mydomain.com b/etc-sai-EXAMPLE/server/conf.d/mydomain.com index a4cec3b..7269381 100644 --- a/etc-sai-EXAMPLE/server/conf.d/mydomain.com +++ b/etc-sai-EXAMPLE/server/conf.d/mydomain.com @@ -74,6 +74,21 @@ # "notification-key": "51b3ee2f06ef2a893cfe901972bd13065d7dbae4cf087b396ee38e7bf78f79a6", + # Unix socket sai-server serves its control link for + # sai-web on. The link is admin-equivalent, so give it + # a filesystem path: lws binds it before dropping + # privileges and gives it the uid:gid from the global + # conf with mode 0660, so only that user and group can + # connect. The user sai-web runs as must be that user + # or a member of that group. Set the same path as + # "sockpath" in sai-web's conf. + # + # If unset, the abstract-namespace socket + # @com.warmcat.sai-websrv is used, which any local user + # can connect to. + # + "sockpath": "/var/run/sai-websrv", + # auth jwk path # You can generate a suitable key like this # diff --git a/etc-sai-EXAMPLE/web/conf.d/unixskt b/etc-sai-EXAMPLE/web/conf.d/unixskt index 4615307..2a9e006 100644 --- a/etc-sai-EXAMPLE/web/conf.d/unixskt +++ b/etc-sai-EXAMPLE/web/conf.d/unixskt @@ -63,6 +63,20 @@ # "database": "/srv/sai/sai-master", + # Unix socket sai-server serves its control link on: + # the same "sockpath" as in sai-server's conf. It is + # owned by sai-server's uid:gid with mode 0660, so the + # user sai-web runs as (see ../conf) must be that user + # or a member of that group, eg, + # + # usermod -a -G apache sai + # + # If unset, the abstract-namespace socket + # @com.warmcat.sai-websrv is used, which any local user + # can connect to. + # + "sockpath": "/var/run/sai-websrv", + # 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 diff --git a/src/common/include/private.h b/src/common/include/private.h index fbd2787..9836180 100644 --- a/src/common/include/private.h +++ b/src/common/include/private.h @@ -48,6 +48,19 @@ #define UDS_PATHNAME_RESPROXY "/var/run/com.warmcat.com.saib.resproxy" #endif +/* + * The sai-web <-> sai-server ss control link ("websrv"). The link is + * admin-equivalent (eventdelete, taskreset, taskclone...), so it should be + * served on a path-based unix socket that filesystem permissions gate: both + * daemons take the path from the "sockpath" pvo in their lejp conf, which is + * the single source of truth for it. + * + * Without a conf sockpath, both sides fall back to this abstract-namespace + * name, which older confs relied on. Any local uid can connect to an + * abstract socket (F-015), so both daemons warn when they end up here. + */ +#define SAI_WEBSRV_UDS_DEFAULT "@com.warmcat.sai-websrv" + #define SAI_BUILDER_INSTANCE_LIMIT 256 struct sai_plat; diff --git a/src/server/s-comms.c b/src/server/s-comms.c index afdb1cc..b456bc2 100644 --- a/src/server/s-comms.c +++ b/src/server/s-comms.c @@ -264,7 +264,40 @@ s_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, "CREATE UNIQUE INDEX IF NOT EXISTS name_idx ON builders (name)", "create builder name index"); - lwsl_notice("%s: creating server stream\n", __func__); + /* + * Where we serve the websrv control link for sai-web... it is + * admin-equivalent, so it wants to be a path-based unix + * socket that filesystem permissions gate. lws binds it + * during protocol init, before dropping privileges, and + * gives it the conf uid:gid with mode 0660: only that user + * and group can connect. Without a conf sockpath, fall back + * to the legacy abstract-namespace name, which any local + * user can connect to. + */ + if (lws_pvo_get_str(in, "sockpath", &vhd->websrv_sockpath)) { + vhd->websrv_sockpath = SAI_WEBSRV_UDS_DEFAULT; + lwsl_warn("%s: no \"sockpath\" pvo: serving the" + " admin control link on abstract socket %s," + " which any local user can connect to." + " Set \"sockpath\" to a filesystem path" + " in both the sai-server and sai-web confs\n", + __func__, vhd->websrv_sockpath); + } else { + char pol[256]; + + lws_snprintf(pol, sizeof(pol), + "{\"s\":[{\"websrv\":{\"endpoint\":\"+%s\"}}]}", + vhd->websrv_sockpath); + + if (lws_ss_policy_overlay(vhd->context, pol) < 0) { + lwsl_err("%s: unable to apply sockpath %s\n", + __func__, vhd->websrv_sockpath); + return -1; + } + } + + lwsl_notice("%s: creating server stream on %s\n", __func__, + vhd->websrv_sockpath); if (lws_ss_create(vhd->context, 0, &ssi_server, vhd, &vhd->h_ss_websrv, NULL, NULL)) { diff --git a/src/server/s-private.h b/src/server/s-private.h index f57b4f8..edf6736 100644 --- a/src/server/s-private.h +++ b/src/server/s-private.h @@ -264,6 +264,7 @@ struct vhd { lws_usec_t last_check_abandoned_tasks; const char *notification_key; + const char *websrv_sockpath; /* control link uds we serve */ unsigned int task_abandoned_timeout_mins; /* diff --git a/src/server/s-sai.c b/src/server/s-sai.c index 0a35bc6..41901e8 100644 --- a/src/server/s-sai.c +++ b/src/server/s-sai.c @@ -36,6 +36,10 @@ struct lws_context *context; * The pass up authenticated browser task and event redo requests, and receive * information about updates to tasks and events that they might have clients * that are watching. + * + * The endpoint here is only the fallback: the comms protocol init overlays + * the "sockpath" from the conf on to it before creating the stream, see + * SAI_WEBSRV_UDS_DEFAULT. */ @@ -45,7 +49,7 @@ static const char * const default_ss_policy = /* uds link between web and server pieces */ "{\"websrv\": {" "\"server\":" "true," - "\"endpoint\":" "\"+@com.warmcat.sai-websrv\"," + "\"endpoint\":" "\"+" SAI_WEBSRV_UDS_DEFAULT "\"," "\"protocol\":" "\"ws\"" "}}" "]" diff --git a/src/web/w-comms.c b/src/web/w-comms.c index d3c6395..0280d50 100644 --- a/src/web/w-comms.c +++ b/src/web/w-comms.c @@ -274,6 +274,37 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, return -1; } + /* + * Where to find sai-server's control link... the same + * "sockpath" as in sai-server's conf. Without one, fall back + * to the legacy abstract-namespace name sai-server also falls + * back to. + * + * The streamtype is nailed_up, so lws_ss_create() connects + * immediately: the conf sockpath has to be overlaid on to the + * policy endpoint before we get there. + */ + if (lws_pvo_get_str(in, "sockpath", &vhd->sockpath)) { + vhd->sockpath = SAI_WEBSRV_UDS_DEFAULT; + lwsl_warn("%s: no \"sockpath\" pvo: connecting to" + " sai-server's control link on abstract" + " socket %s; set \"sockpath\" to the same" + " filesystem path in both confs\n", + __func__, vhd->sockpath); + } else { + char pol[256]; + + lws_snprintf(pol, sizeof(pol), + "{\"s\":[{\"websrv\":{\"endpoint\":\"+%s\"}}]}", + vhd->sockpath); + + if (lws_ss_policy_overlay(vhd->context, pol) < 0) { + lwsl_err("%s: unable to apply sockpath %s\n", + __func__, vhd->sockpath); + return -1; + } + } + lws_snprintf((char *)buf, sizeof(buf), "%s-events.sqlite3", vhd->sqlite3_path_lhs); @@ -334,6 +365,7 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, /* * Reach out to the sai-server part over the SS ws websrv link + * (nailed_up: this connects, using the sockpath overlaid above) */ if (lws_ss_create(lws_get_context(wsi), 0, &ssi_saiw_websrv, vhd, @@ -344,10 +376,6 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, return 1; } - if (lws_ss_set_metadata(vhd->h_ss_websrv, "sockpath", - "@com.warmcat.sai-websrv", 23)) - lwsl_warn("%s: unable to set metadata\n", __func__); - r = lws_ss_client_connect(vhd->h_ss_websrv) ? -1 : 0; if (r) diff --git a/src/web/w-private.h b/src/web/w-private.h index 47a7590..b0dccd7 100644 --- a/src/web/w-private.h +++ b/src/web/w-private.h @@ -210,6 +210,7 @@ struct vhd { struct lws_ss_handle *h_ss_websrv; /* client */ const char *sqlite3_path_lhs; + const char *sockpath; /* sai-server control link uds */ lws_dll2_owner_t sqlite3_cache; /* sais_sqlite_cache_t */ lws_dll2_owner_t tasklog_cache; diff --git a/src/web/w-sai.c b/src/web/w-sai.c index a85c8d3..c7da9f5 100644 --- a/src/web/w-sai.c +++ b/src/web/w-sai.c @@ -42,14 +42,15 @@ static const char * const default_ss_policy = "}]," "\"s\": [" /* - * Unix Domain Socket connections to sai-server webevents + * Unix Domain Socket connection to sai-server's control link. + * + * The endpoint here is only the fallback: protocol init + * overlays the "sockpath" from the conf on to it before + * creating the stream, see SAI_WEBSRV_UDS_DEFAULT. */ "{\"websrv\": {" - "\"endpoint\":" "\"+${sockpath}\"," + "\"endpoint\":" "\"+" SAI_WEBSRV_UDS_DEFAULT "\"," "\"protocol\":" "\"ws\"," - "\"metadata\": [" - "{\"sockpath\": \"\"}" - "]," "\"retry\": \"default\"," "\"nailed_up\":" "true" "}}"
Page fetched 0s ago, creation time: 6ms (vhost etag hits: 0%, cache hits: 0%)