Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / src / web / CMakeLists.txt
Author[]Andy Green <andy@warmcat.com> 2026-10-04 07:40 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-04 20:05 UTC
Tree8241937e4a6b2ce56ab803edf5af5366a3c01c6d   Raw Patch
 
web: "projects" pvo limits which projects a vhost shows
web: "projects" pvo limits which projects a vhost shows

So the events of one sai-server can be offered as different canned views,
let each sai-web vhost list the projects it shows in a new optional
"projects" pvo, as a comma-separated list, eg, "libwebsockets, sai".  Two
otherwise identical vhosts on differently-named unix sockets can then show
different projects, and the front-end proxy mounts pick a view by socket.
Without the pvo a vhost shows everything, as before.

The list is parsed with lws_tokenize (COMMA_SEP_LIST, so whitespace around
names is ignored and misplaced commas are errors) into an lwsac-backed
lws_dll2 list for checks in C, and into a TEMP table on the vhd's own events
db connection, so event queries take a fixed " and repo_name in (...)"
fragment.  TEMP tables are private to their connection, and each vhd opens
its own.

On a restricted vhost the other projects don't exist:

 - overview, project and branch lists, and rss feeds only query visible
   projects
 - taskinfo for a hidden task answers like a deleted event, so no logs or
   artifacts either; artifact downloads 404 and /status badges are unknown
 - task and event change notices from sai-server about hidden events are
   not passed on
 - taskactivity and builder load reports are decoded and serialized again
   without the hidden tasks, instead of being forwarded as they came
 - admin operations naming a hidden task, event or findings repo are
   dropped, and the findings list skips hidden repos

A pvo that doesn't parse or lists nothing fails PROTOCOL_INIT, which leaves
the vhost serving nothing rather than everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/README.md b/README.md index dd00400..4799dc4 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,49 @@ 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). +## Showing only some projects on a sai-web vhost ("projects") + +By default a sai-web vhost shows every project sai-server has built. The +optional `projects` pvo in the vhost's `com-warmcat-sai` section limits it to +the listed projects (repo names), as a comma-separated list: + +``` + "projects": "libwebsockets, sai", +``` + +Whitespace around the names is ignored; names can use `A-Z a-z 0-9 _ - .`. +On that vhost, everything behaves as if the other projects don't +exist: the event list, project and branch lists, live updates, task logs and +artifacts, the builders' lists of what they are building, the rss feeds, +`/status` badges and findings. Admin actions on tasks and events of other +projects are dropped. The builders themselves are shared, so they and their +load are shown as usual. A `projects` pvo that can't be parsed, or that lists +nothing, stops the protocol coming up on that vhost rather than showing it +everything. + +To offer different canned views of the one sai-server, give sai-web two (or +more) otherwise identical vhosts, each with its own `name`, its own unix socket +`interface` path and its own `projects`, eg, `/var/run/sai` showing everything +and `/var/run/sai-lws` showing only libwebsockets. Then point each front-end +proxy's `/sai` mount at the socket for the view it should show. The web UI +expects to be at `/sai`, so the views go on different front-end vhosts +(hostnames) rather than on different paths of one, eg, on one front-end vhost + +``` + { "mountpoint": "/sai", + "origin": "http://+/var/run/sai:/sai", ... } +``` + +and on another + +``` + { "mountpoint": "/sai", + "origin": "http://+/var/run/sai-lws:/sai", ... } +``` + +Each sai-web vhost makes its own control link to sai-server, so they need the +same `database` and `sockpath`. + ## RSS feed of build events sai-web serves a public RSS 2.0 feed of the latest 10 events at diff --git a/etc-sai-EXAMPLE/web/conf.d/unixskt b/etc-sai-EXAMPLE/web/conf.d/unixskt index 2a9e006..50efb3a 100644 --- a/etc-sai-EXAMPLE/web/conf.d/unixskt +++ b/etc-sai-EXAMPLE/web/conf.d/unixskt @@ -77,6 +77,22 @@ # "sockpath": "/var/run/sai-websrv", + # Optional: the projects (repo names) this vhost shows, + # as a comma-separated list. Browsers, rss feeds, + # status badges and artifact links on this vhost then + # behave as if no other project exists. Without it, + # the vhost shows every project. + # + # To offer different canned views of the one sai-server, + # copy this whole vhost with its own "name", a different + # unix socket "interface" path and different "projects", + # keeping "database" and "sockpath" the same. Then + # point each front-end vhost's /sai proxy mount at the + # socket for the view it should show (the UI expects to + # be at /sai, so views go on different hostnames). + # + #"projects": "libwebsockets, sai", + # 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/web/CMakeLists.txt b/src/web/CMakeLists.txt index f14432a..2a6cc5e 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -10,6 +10,7 @@ set(SRCS w-ws-server.c w-ws-browser.c w-findings.c + w-visible.c ../common/c-utils.c ../common/c-pool.c ../common/c-conf.c diff --git a/src/web/w-artifact.c b/src/web/w-artifact.c index 3732e61..8fb6b1d 100644 --- a/src/web/w-artifact.c +++ b/src/web/w-artifact.c @@ -90,6 +90,12 @@ saiw_get_blob(struct vhd *vhd, const char *url, sqlite3 **pdb, return -1; } + if (!saiw_event_visible(vhd, task_uuid)) { + lwsl_info("%s: task's project not shown on this vhost\n", + __func__); + return -1; + } + sai_task_uuid_to_event_uuid(event_uuid, task_uuid); /* open the event-specific database object */ diff --git a/src/web/w-comms.c b/src/web/w-comms.c index aa151b5..72eebcd 100644 --- a/src/web/w-comms.c +++ b/src/web/w-comms.c @@ -97,6 +97,10 @@ sai_get_head_status(struct vhd *vhd, const char *projname) sai_event_t *e; int state; + if (!saiw_project_visible(vhd, projname)) + /* as if we never heard of it */ + return -1; + /* * The newest event of the named project decides it. Ad-hoc events * are scratch builds seeded by an admin; they don't say anything @@ -378,6 +382,16 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, sai_sqlite3_statement(vhd->pdb, "CREATE TABLE IF NOT EXISTS saiweb_state (key TEXT PRIMARY KEY, val INTEGER);", "create saiweb_state"); + + /* + * Which projects this vhost shows, if it doesn't show them + * all. Failing here leaves the vhost without a vhd, which + * serves nothing, rather than serving every project. + */ + if (saiw_visible_init(vhd, in)) { + lws_struct_sq3_close(&vhd->pdb); + return -1; + } { sqlite3_stmt *stmt; @@ -422,6 +436,7 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, goto passthru; saiw_event_db_close_all_now(vhd); lws_struct_sq3_close(&vhd->pdb); + saiw_visible_destroy(vhd); goto passthru; /* diff --git a/src/web/w-findings.c b/src/web/w-findings.c index 0a68310..77f0cf2 100644 --- a/src/web/w-findings.c +++ b/src/web/w-findings.c @@ -112,7 +112,8 @@ saiw_browser_send_findings(struct vhd *vhd, struct pss *pss) int first = 1; sqlite3 *pdb; - if (!repo || !pool || saiw_findings_open(vhd, repo, pool, &pdb)) + if (!repo || !pool || !saiw_project_visible(vhd, repo) || + saiw_findings_open(vhd, repo, pool, &pdb)) continue; p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), diff --git a/src/web/w-private.h b/src/web/w-private.h index a6f067d..6d1b4b4 100644 --- a/src/web/w-private.h +++ b/src/web/w-private.h @@ -244,8 +244,18 @@ struct vhd { lws_dll2_owner_t sqlite3_cache; /* sais_sqlite_cache_t */ lws_dll2_owner_t rss_waiters; /* pss held on long poll */ lws_dll2_owner_t tasklog_cache; + + /* + * The projects this vhost shows, from the "projects" pvo; if empty, + * it shows all of them. See w-visible.c + */ + lws_dll2_owner_t visible_projects; + struct lwsac *ac_visible; }; +/* nonzero if a "projects" pvo limits the projects this vhost shows */ +#define saiw_restricted(_vhd) (!!(_vhd)->visible_projects.count) + typedef struct saiw_websrv { struct lws_ss_handle *ss; void *opaque_data; @@ -379,6 +389,28 @@ saiw_browser_broadcast_queue_power_history(struct vhd *vhd, struct pss *pss); extern const lws_struct_map_t lsm_schema_pcon_energy[]; +/* w-visible.c */ + +int +saiw_visible_init(struct vhd *vhd, void *pvo); + +void +saiw_visible_destroy(struct vhd *vhd); + +int +saiw_project_visible(struct vhd *vhd, const char *project); + +/* uuid may be an event's, or one of its tasks' */ +int +saiw_event_visible(struct vhd *vhd, const char *uuid); + +/* + * " and ..." restricting a query on vhd->pdb's events table to the visible + * projects, or "" if they all are + */ +const char * +saiw_visible_sql(struct vhd *vhd); + /* w-findings.c, for admins only */ int diff --git a/src/web/w-rss.c b/src/web/w-rss.c index 003d1d0..e0f7acd 100644 --- a/src/web/w-rss.c +++ b/src/web/w-rss.c @@ -249,27 +249,30 @@ static int saiw_feed_query(struct vhd *vhd, struct pss *pss, sai_feed_t *f, struct lwsac **ac) { + struct lws_genhash_ctx hc; + sqlite3_stmt *sm = NULL; + uint8_t digest[32]; + sai_feed_item_t *it; + const char *ref; + int rc, ret = 1; + char q[512]; + + memset(f, 0, sizeof(*f)); + /* * The project and branch come from the request url, so they must be * bound rather than go via lws_struct_sq3_deserialize(), whose filter - * text is spliced into the sql verbatim. + * text is spliced into the sql verbatim. Only the fixed fragment + * limiting it to the projects this vhost shows is spliced in. */ - static const char * const q = + lws_snprintf(q, sizeof(q), "SELECT uuid, repo_name, ref, hash, created, state, " "ifnull(adhoc,0), repo_fetchurl, weburl FROM events " - "WHERE state != ?3 AND " + "WHERE state != ?3%s AND " "(?1 IS NULL OR repo_name = ?1) AND " "(?2 IS NULL OR ref = ?2 OR ref = 'refs/heads/' || ?2) AND " "(?5 IS NULL OR repo_fetchurl = ?5) " - "ORDER BY created DESC LIMIT ?4"; - struct lws_genhash_ctx hc; - uint8_t digest[32]; - sqlite3_stmt *sm = NULL; - sai_feed_item_t *it; - const char *ref; - int rc, ret = 1; - - memset(f, 0, sizeof(*f)); + "ORDER BY created DESC LIMIT ?4", saiw_visible_sql(vhd)); if (lws_genhash_init(&hc, LWS_GENHASH_TYPE_SHA256)) return 1; diff --git a/src/web/w-visible.c b/src/web/w-visible.c new file mode 100644 index 0000000..09a4889 --- /dev/null +++ b/src/web/w-visible.c @@ -0,0 +1,230 @@ +/* + * Sai web - ./src/web/w-visible.c + * + * Copyright (C) 2019 - 2026 Andy Green <andy@warmcat.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation: + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + * + * Which projects a vhost shows + * + * A sai-web vhost can be given a "projects" pvo listing the projects (repo + * names) it shows, so the events of one sai-server can be offered as different + * canned views, eg, on differently-named unix sockets for the front-end proxy + * to mount. Without the pvo, the vhost shows every project. + * + * The list is kept twice from the one parse: as a list for checks done in C, + * and as a TEMP table on the vhd's own connection to the events db, so the sql + * listing events can be scoped by a fixed fragment. TEMP tables only exist on + * the connection that made them, so each vhost's list is private to it. + * + * Everything that tells browsers about, or acts on, a project goes through + * these, so that a restricted vhost behaves as if the other projects don't + * exist. + */ + +#include <libwebsockets.h> +#include <string.h> + +#include "w-private.h" + +#define SAIW_VISIBLE_SQL \ + " and repo_name in (select name from temp.saiw_visible)" + +typedef struct saiw_visible_project { + lws_dll2_t list; /* vhd->visible_projects */ + + /* name over-allocated here */ +} saiw_visible_project_t; + +static const char * +saiw_visible_name(const saiw_visible_project_t *vp) +{ + return (const char *)&vp[1]; +} + +static int +saiw_visible_listed(struct vhd *vhd, const char *project) +{ + lws_start_foreach_dll(struct lws_dll2 *, p, + vhd->visible_projects.head) { + saiw_visible_project_t *vp = lws_container_of(p, + saiw_visible_project_t, list); + + if (!strcmp(saiw_visible_name(vp), project)) + return 1; + } lws_end_foreach_dll(p); + + return 0; +} + +int +saiw_project_visible(struct vhd *vhd, const char *project) +{ + return !saiw_restricted(vhd) || saiw_visible_listed(vhd, project); +} + +int +saiw_event_visible(struct vhd *vhd, const char *uuid) +{ + char event_uuid[33]; + sqlite3_stmt *sm; + int r = 0; + + if (!saiw_restricted(vhd)) + return 1; + + /* a task uuid starts with its event's uuid */ + if (strlen(uuid) < 32) + return 0; + sai_task_uuid_to_event_uuid(event_uuid, uuid); + + if (sqlite3_prepare_v2(vhd->pdb, "SELECT 1 FROM events WHERE uuid = ?" + SAIW_VISIBLE_SQL, -1, &sm, NULL) != SQLITE_OK) { + lwsl_err("%s: prepare failed: %s\n", __func__, + sqlite3_errmsg(vhd->pdb)); + return 0; + } + + sqlite3_bind_text(sm, 1, event_uuid, -1, SQLITE_TRANSIENT); + r = sqlite3_step(sm) == SQLITE_ROW; + sqlite3_finalize(sm); + + return r; +} + +const char * +saiw_visible_sql(struct vhd *vhd) +{ + return saiw_restricted(vhd) ? SAIW_VISIBLE_SQL : ""; +} + +static int +saiw_visible_add(struct vhd *vhd, const char *name) +{ + saiw_visible_project_t *vp; + size_t len = strlen(name); + sqlite3_stmt *sm; + + if (!len || saiw_visible_listed(vhd, name)) + /* listed twice is the same as once */ + return 0; + + vp = lwsac_use_zero(&vhd->ac_visible, sizeof(*vp) + len + 1, 512); + if (!vp) + return 1; + memcpy((char *)&vp[1], name, len + 1); + + if (sqlite3_prepare_v2(vhd->pdb, "INSERT INTO temp.saiw_visible " + "(name) VALUES (?)", -1, &sm, NULL) != SQLITE_OK) + return 1; + sqlite3_bind_text(sm, 1, name, -1, SQLITE_TRANSIENT); + if (sai_sqlite3_step_done(vhd->pdb, sm, "list visible project")) + return 1; + + lws_dll2_add_tail(&vp->list, &vhd->visible_projects); + + return 0; +} + +/* + * Parse the "projects" pvo, a comma-separated list, eg, + * + * "projects": "libwebsockets, sai", + * + * Whitespace around the names is ignored. Names may use A-Z a-z 0-9 _ - and . + * + * Call after vhd->pdb is open. Returns 0 if OK, including when there is no + * pvo. If there is one but it can't be understood or lists nothing, it fails + * rather than show this vhost everything. + */ + +int +saiw_visible_init(struct vhd *vhd, void *pvo) +{ + struct lws_tokenize ts; + const char *list; + char name[65]; + + lws_dll2_owner_clear(&vhd->visible_projects); + + if (lws_pvo_get_str(pvo, "projects", &list)) + return 0; + + if (sai_sqlite3_statement(vhd->pdb, "CREATE TEMP TABLE IF NOT EXISTS " + "saiw_visible (name TEXT PRIMARY KEY);", + "create saiw_visible")) + return 1; + + lws_tokenize_init(&ts, list, LWS_TOKENIZE_F_COMMA_SEP_LIST | + LWS_TOKENIZE_F_MINUS_NONTERM | + LWS_TOKENIZE_F_DOT_NONTERM | + LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_NO_FLOATS); + + do { + ts.e = (int8_t)lws_tokenize(&ts); + + switch (ts.e) { + case LWS_TOKZE_ENDED: + break; + + case LWS_TOKZE_TOKEN: + if (lws_tokenize_cstr(&ts, name, sizeof(name))) { + lwsl_err("%s: project name too long\n", + __func__); + goto bail; + } + if (saiw_visible_add(vhd, name)) + goto bail; + break; + + case LWS_TOKZE_DELIMITER: + /* COMMA_SEP_LIST makes sure commas are where they go */ + if (*ts.token == ',') + break; + goto bail; + + default: + /* eg, missing or doubled commas, or bad characters */ + goto bail; + } + } while (ts.e > 0); + + if (!saiw_restricted(vhd)) { + lwsl_err("%s: \"projects\" pvo lists no projects\n", __func__); + return 1; + } + + lwsl_notice("%s: vhost %s shows %u projects: %s\n", __func__, + lws_get_vhost_name(vhd->vhost), + (unsigned int)vhd->visible_projects.count, list); + + return 0; + +bail: + lwsl_err("%s: unable to use \"projects\" pvo '%s'\n", __func__, list); + saiw_visible_destroy(vhd); + + return 1; +} + +void +saiw_visible_destroy(struct vhd *vhd) +{ + /* the TEMP table goes with the db connection */ + lws_dll2_owner_clear(&vhd->visible_projects); + lwsac_free(&vhd->ac_visible); +} diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c index 48daf3d..98e0c18 100644 --- a/src/web/w-ws-browser.c +++ b/src/web/w-ws-browser.c @@ -533,9 +533,13 @@ saiw_pss_schedule_taskinfo(struct pss *pss, const char *task_uuid, int logsub, i goto bail; } - /* open the event-specific database object */ + /* + * Open the event-specific database object... a task in a project this + * vhost doesn't show is treated the same as one whose event is gone + */ - if (sai_event_db_ensure_open(pss->vhd->context, &pss->vhd->sqlite3_cache, + if (!saiw_event_visible(pss->vhd, event_uuid) || + sai_event_db_ensure_open(pss->vhd->context, &pss->vhd->sqlite3_cache, pss->vhd->sqlite3_path_lhs, event_uuid, 0, &pdb)) { uint8_t buf[LWS_PRE + 128]; int n1 = lws_snprintf((char *)buf + LWS_PRE, sizeof(buf) - LWS_PRE, @@ -877,6 +881,51 @@ saiw_event_state_change(struct vhd *vhd, const char *event_uuid) } /* + * Nonzero if the browser message, which has decoded into dest, is about + * something in a project this vhost shows, or about nothing project-specific + * at all (eg, builders). Taskinfo and eventinfo don't need checking here, + * the replies to them are already limited to the visible projects. + */ + +static int +saiw_rx_visible(struct vhd *vhd, int schema_idx, void *dest) +{ + switch (schema_idx) { + case SAIM_WS_BROWSER_RX_TASKRESET: + case SAIM_WS_BROWSER_RX_TASKREMOVEALLTRIES: + case SAIM_WS_BROWSER_RX_TASKREBUILDLASTSTEP: + case SAIM_WS_BROWSER_RX_EVENTRESET: + case SAIM_WS_BROWSER_RX_EVENTDELETE: + case SAIM_WS_BROWSER_RX_CLONEINFO: + return saiw_event_visible(vhd, + ((sai_browse_rx_evinfo_t *)dest)->event_hash); + case SAIM_WS_BROWSER_RX_TASKCANCEL: + return saiw_event_visible(vhd, ((sai_cancel_t *)dest)->task_uuid); + case SAIM_WS_BROWSER_RX_PLATRESET: + return saiw_event_visible(vhd, + ((sai_browse_rx_platreset_t *)dest)->event_uuid); + case SAIM_WS_BROWSER_RX_OPENSHELL: + return saiw_event_visible(vhd, + ((sai_openshell_t *)dest)->task_uuid); + case SAIM_WS_BROWSER_RX_CLOSESHELL: + return saiw_event_visible(vhd, + ((sai_closeshell_t *)dest)->task_uuid); + case SAIM_WS_BROWSER_RX_PTYDATA: + return saiw_event_visible(vhd, + ((sai_ptydata_t *)dest)->task_uuid); + case SAIM_WS_BROWSER_RX_TASKCLONE: + return saiw_event_visible(vhd, + ((sai_browse_rx_taskclone_t *)dest)->seed_uuid); + case SAIM_WS_BROWSER_RX_FINDINGGET: + case SAIM_WS_BROWSER_RX_FINDINGSET: + return saiw_project_visible(vhd, + ((sai_findingset_t *)dest)->repo); + } + + return 1; +} + +/* * sai-web has sent us a request for either overview, or data on a specific * task */ @@ -948,6 +997,16 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, goto soft_error; } + /* + * Nobody, admin or not, can act on projects this vhost doesn't show: + * for it, they don't exist. Drop it, the UI never sends these. + */ + if (!saiw_rx_visible(vhd, a.top_schema_index, a.dest)) { + lwsl_notice("%s: dropping schema %d for a project not shown " + "on this vhost\n", __func__, a.top_schema_index); + goto ok; + } + switch (a.top_schema_index) { case SAIM_WS_BROWSER_RX_BUILDER_VISIBILITY: @@ -1049,16 +1108,18 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, sqlite3_stmt *stmt = NULL; uint8_t buf[LWS_PRE + 4096], *start = buf + LWS_PRE, *p = start, *end = buf + sizeof(buf); - char esc[96]; + char esc[96], q[256]; /* first_elem = first array entry (omit leading comma); * sent_any = have we already tx'd a ws fragment of this msg */ int first_elem = 1, sent_any = 0, rc; - if (sqlite3_prepare_v2(vhd->pdb, + lws_snprintf(q, sizeof(q), "SELECT repo_name, MAX(created) AS mc FROM events " - "WHERE state != ? GROUP BY repo_name " - "ORDER BY mc DESC", - -1, &stmt, NULL) != SQLITE_OK) { + "WHERE state != ?%s GROUP BY repo_name " + "ORDER BY mc DESC", saiw_visible_sql(vhd)); + + if (sqlite3_prepare_v2(vhd->pdb, q, -1, &stmt, NULL) != + SQLITE_OK) { lwsl_notice("%s: projlist prepare failed\n", __func__); goto soft_error; } @@ -1127,7 +1188,7 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, sqlite3_stmt *stmt = NULL; uint8_t buf[LWS_PRE + 4096], *start = buf + LWS_PRE, *p = start, *end = buf + sizeof(buf); - char esc[96], pesc[96]; + char esc[96], pesc[96], q[512]; int first_elem = 1, sent_any = 0, rc; lws_dll2_owner_clear(&owner); @@ -1139,16 +1200,23 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, */ lws_sql_purify(pesc, bl->project, sizeof(pesc) - 1); - if (sqlite3_prepare_v2(vhd->pdb, + /* + * A project this vhost doesn't show has no branches; the + * subquery only looks at the project the outer one found. + */ + lws_snprintf(q, sizeof(q), "SELECT ref, " "(SELECT e2.state FROM events e2 " " WHERE e2.ref = events.ref " " AND e2.repo_name = ? AND e2.state != ? " " ORDER BY e2.created DESC LIMIT 1) AS ls " "FROM events " - "WHERE state != ? AND repo_name = ? " + "WHERE state != ? AND repo_name = ?%s " "GROUP BY ref ORDER BY MAX(created) DESC", - -1, &stmt, NULL) != SQLITE_OK) { + saiw_visible_sql(vhd)); + + if (sqlite3_prepare_v2(vhd->pdb, q, -1, &stmt, NULL) != + SQLITE_OK) { lwsl_notice("%s: branchlist prepare failed\n", __func__); goto soft_error; @@ -1795,7 +1863,7 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss) { char buf[4096 + LWS_PRE], *start = buf + LWS_PRE, *p = start, *end = buf + sizeof(buf); - char esc[256], filt[256], subsequent; + char esc[256], filt[448], subsequent; struct lwsac *task_ac = NULL, *ac = NULL; lws_dll2_owner_t task_owner, owner; unsigned int task_index = 0; @@ -1840,7 +1908,7 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss) } if (ev_created > 0) { unsigned int events_newer = 0; - lws_snprintf(q, sizeof(q), "SELECT COUNT(*) FROM events WHERE state != %d AND created > %llu", SAIES_DELETED, (unsigned long long)ev_created); + lws_snprintf(q, sizeof(q), "SELECT COUNT(*) FROM events WHERE state != %d AND created > %llu%s", SAIES_DELETED, (unsigned long long)ev_created, saiw_visible_sql(vhd)); if (sqlite3_prepare_v2(vhd->pdb, q, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { events_newer = (unsigned int)sqlite3_column_int(stmt, 0); @@ -1852,6 +1920,12 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss) pss->resolved_task_offset = 1; } + /* + * The vhost may only show some projects: that clause goes right after + * the first one, so if anything is going to be truncated off the end + * of filt, it isn't that. + */ + if (pss->specific_project[0]) { /* * gitohashi /git/<project> URL-locked mode: lock to that one @@ -1859,8 +1933,8 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss) */ lws_sql_purify(esc, pss->specific_project, sizeof(esc) - 1); lws_snprintf(filt, sizeof(filt), - " and state != %d and repo_name='%s'", - SAIES_DELETED, esc); + " and state != %d%s and repo_name='%s'", + SAIES_DELETED, saiw_visible_sql(vhd), esc); n = -1; } else { size_t fl; @@ -1870,8 +1944,8 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss) * " and ..." clauses here; the COUNT query below skips the * leading " and " with filt + 5 and uses the rest verbatim. */ - lws_snprintf(filt, sizeof(filt), " and state != %d", - SAIES_DELETED); + lws_snprintf(filt, sizeof(filt), " and state != %d%s", + SAIES_DELETED, saiw_visible_sql(vhd)); /* * A specific event selection (com.warmcat.sai.eventinfo from @@ -1915,7 +1989,7 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss) unsigned int total_events = 0; { - char q[256]; + char q[64 + sizeof(filt)]; sqlite3_stmt *stmt; lws_snprintf(q, sizeof(q), "SELECT COUNT(*) FROM events WHERE %s", filt + 5); if (sqlite3_prepare_v2(vhd->pdb, q, -1, &stmt, NULL) == SQLITE_OK) { diff --git a/src/web/w-ws-server.c b/src/web/w-ws-server.c index 5d7a2c3..859b9cc 100644 --- a/src/web/w-ws-server.c +++ b/src/web/w-ws-server.c @@ -37,6 +37,32 @@ static lws_struct_map_t lsm_websrv_evinfo[] = { }; /* + * sai-server's periodic list of the tasks that are building, and how + * recently each one produced logs. We only need to decode it to remove the + * tasks a vhost doesn't show, see saiw_reissue_activity() + */ + +typedef struct saiw_activity { + lws_dll2_t list; + char uuid[65]; + int cat; +} saiw_activity_t; + +typedef struct saiw_activities { + lws_dll2_owner_t activity; +} saiw_activities_t; + +static const lws_struct_map_t lsm_websrv_activity[] = { + LSM_CARRAY (saiw_activity_t, uuid, "uuid"), + LSM_SIGNED (saiw_activity_t, cat, "cat"), +}; + +static const lws_struct_map_t lsm_websrv_activities[] = { + LSM_LIST (saiw_activities_t, activity, saiw_activity_t, list, + NULL, lsm_websrv_activity, "activity"), +}; + +/* * (Structs and maps removed - now in common/include/private.h and common/struct-metadata.c) */ @@ -52,7 +78,7 @@ const lws_struct_map_t lsm_schema_json_map[] = { /* shares struct */ "sai-tasklogs"), LSM_SCHEMA (sai_load_report_t, NULL, lsm_load_report_members, "com.warmcat.sai.loadreport"), - LSM_SCHEMA (sai_browse_rx_evinfo_t, NULL, lsm_websrv_evinfo, + LSM_SCHEMA (saiw_activities_t, NULL, lsm_websrv_activities, "com.warmcat.sai.taskactivity"), LSM_SCHEMA (sai_build_metric_t, NULL, lsm_build_metric, "com.warmcat.sai.build-metric"), @@ -146,6 +172,98 @@ saiw_pty_accum(saiw_websrv_t *m, const uint8_t *frag, size_t len) return 0; } +/* + * A vhost that only shows some projects can't pass on messages that mention + * tasks in the others as they came. Instead it serializes the decoded + * object, with those tasks removed, from the same schema and queues it to its + * browsers (only ones showing builders, if builder_info). + */ + +static void +saiw_reissue_to_browsers(struct vhd *vhd, int schema_idx, void *obj, + int builder_info) +{ + uint8_t buf[LWS_PRE + 2048], *start = buf + LWS_PRE; + lws_struct_serialize_t *js; + int r, fi = 1; + size_t w; + + js = lws_struct_json_serialize_create(&lsm_schema_json_map[schema_idx], + 1, 0, obj); + if (!js) + return; + + do { + r = (int)lws_struct_json_serialize(js, start, + sizeof(buf) - LWS_PRE, &w); + if (r == LSJS_RESULT_ERROR) { + lwsl_err("%s: unable to serialize schema %d\n", + __func__, schema_idx); + break; + } + + lws_start_foreach_dll(struct lws_dll2 *, p, vhd->browsers.head) { + struct pss *pss = lws_container_of(p, struct pss, same); + + if (!builder_info || + (!pss->is_gitohashi && pss->wants_builder_info)) + saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, + start, w, lws_write_ws_flags( + LWS_WRITE_TEXT, fi, + r == LSJS_RESULT_FINISH)); + } lws_end_foreach_dll(p); + + fi = 0; + } while (r == LSJS_RESULT_CONTINUE); + + lws_struct_json_serialize_destroy(&js); +} + +static void +saiw_reissue_activity(struct vhd *vhd, saiw_activities_t *acts) +{ + char last[33] = ""; + int last_vis = 0; + + /* the list comes grouped by event, so check each event once */ + + lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1, + acts->activity.head) { + saiw_activity_t *act = lws_container_of(p, saiw_activity_t, + list); + + if (strncmp(act->uuid, last, 32)) { + last_vis = saiw_event_visible(vhd, act->uuid); + lws_strnncpy(last, act->uuid, 32, sizeof(last)); + } + + if (!last_vis) + lws_dll2_remove(&act->list); + } lws_end_foreach_dll_safe(p, p1); + + saiw_reissue_to_browsers(vhd, SAIS_WS_WEBSRV_RX_TASKACTIVITY, acts, 0); +} + +static void +saiw_reissue_loadreport(struct vhd *vhd, sai_load_report_t *lr) +{ + /* + * The builders and their load are shared by every project, only the + * tasks they're building belong to one + */ + + lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1, + lr->active_tasks.head) { + sai_active_task_info_t *ati = lws_container_of(p, + sai_active_task_info_t, list); + + if (!saiw_project_visible(vhd, ati->repo_name)) + lws_dll2_remove(&ati->list); + } lws_end_foreach_dll_safe(p, p1); + + saiw_reissue_to_browsers(vhd, SAIS_WS_WEBSRV_RX_LOADREPORT, lr, 1); +} + static int saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags) { @@ -192,7 +310,13 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags) switch (m->a.top_schema_index) { case SAIS_WS_WEBSRV_RX_TASKACTIVITY: { - uint8_t *tmp = malloc(LWS_PRE + rem); + uint8_t *tmp; + + if (saiw_restricted(vhd)) + /* reissued filtered when complete */ + break; + + tmp = malloc(LWS_PRE + rem); if (tmp) { memcpy(tmp + LWS_PRE, p, rem); saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, tmp + LWS_PRE, rem, @@ -213,7 +337,13 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags) break; case SAIS_WS_WEBSRV_RX_LOADREPORT: { - uint8_t *tmp = malloc(LWS_PRE + rem); + uint8_t *tmp; + + if (saiw_restricted(vhd)) + /* reissued filtered when complete */ + break; + + tmp = malloc(LWS_PRE + rem); if (tmp) { memcpy(tmp + LWS_PRE, p, rem); lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->browsers.head) { @@ -239,10 +369,26 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags) sai_browse_rx_evinfo_t *ei; + /* + * This vhost's browsers hear nothing about events in projects + * it doesn't show + */ + if ((m->a.top_schema_index == SAIS_WS_WEBSRV_RX_TASKCHANGE || + m->a.top_schema_index == SAIS_WS_WEBSRV_RX_EVENTCHANGE) && + m->a.dest && !saiw_event_visible(vhd, + ((sai_browse_rx_evinfo_t *)m->a.dest)->event_hash)) + goto cleanup_parse_allocs; + switch (m->a.top_schema_index) { + case SAIS_WS_WEBSRV_RX_TASKACTIVITY: + if (saiw_restricted(vhd)) { + if (m->a.dest) + saiw_reissue_activity(vhd, m->a.dest); + break; + } + /* fallthru */ case SAIS_WS_WEBSRV_RX_TASKCHANGE: case SAIS_WS_WEBSRV_RX_EVENTCHANGE: - case SAIS_WS_WEBSRV_RX_TASKACTIVITY: { uint8_t *tmp = malloc(LWS_PRE + consumed); if (tmp) { @@ -293,7 +439,15 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags) } case SAIS_WS_WEBSRV_RX_LOADREPORT: { - uint8_t *tmp = malloc(LWS_PRE + consumed); + uint8_t *tmp; + + if (saiw_restricted(vhd)) { + if (m->a.dest) + saiw_reissue_loadreport(vhd, m->a.dest); + break; + } + + tmp = malloc(LWS_PRE + consumed); if (tmp) { memcpy(tmp + LWS_PRE, p, consumed); lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->browsers.head) {
Page fetched 0s ago, creation time: 6ms (vhost etag hits: 0%, cache hits: 0%)