From 67babb244b8ab6fcdfe3748b1c52fcac60eb2ed4 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 2 Jul 2026 10:01:50 +0300 Subject: ychat: remove the no-database build option (task es0) DATABASE/SQLite is no longer optional. src/configure.ac and the generated src/configure now check sqlite3.h/-lsqlite3 unconditionally, the same way pthread/libevent already were, right after those checks (matching order in both files) - there's no --enable-sqlite opt-in any more, and configure aborts via header_error/lib_error if SQLite isn't available rather than silently producing the old in-memory-only, no-account "Mode A" guest chat. --enable-mysql is left alone (pre-existing, separately broken, out of scope - this is about ychat always having *a* database, not about MySQL). With DATABASE guaranteed, the three recent no-DB-build UI special-cases (651f762, 0cdec77, 6c3a65b) are dead code, so they're reverted: deleted html/index_guest.html, reverted html/input.html + src/reqp.cpp to always render a static Options link (dropped the #ifdef DATABASE/%%OPTIONS_LINK%% templating), and the Dockerfile no longer strips register.html/options.html or their .so modules. Unregistered guest chatting itself is untouched - chat.enableguest is a runtime config toggle independent of the compile-time database requirement, and a guest's is_reg is still always false so a guest can never claim operator via chat.defaultop. Consolidated the two Dockerfiles into one (SQLite-backed; deleted Dockerfile.sqlite) and merged DOCKER.md/DOCKER-SQLITE.md into a single DOCKER.md. Updated root README.md and etc/ychat.conf's option descriptions to stop claiming the no-DB build is live/default. Verified in a Rocky Linux 9 podman container: index.html has the password field + Register link, register.html/options.html both resolve, POSTing to register.html creates a SQLite user row, wrong password is rejected and the correct one logs in, the same account's login still works identically after a full container restart with /app/data bind-mounted (persistence), and unregistered guest login still works. Independently reproduced by a fresh-context review agent, which also rebuilt + re-verified the whole flow itself and caught one real (if harmless) issue - the generated src/configure had the SQLite check in a different physical position than configure.ac's - now fixed so both files agree on ordering. Not deployed to f3s: the live cluster still runs the old no-DB image. Rolling this out needs a persistent volume for /app/data and an updated Helm chart - a deliberate follow-up, not done here. Co-Authored-By: Claude Sonnet 5 --- README.md | 60 ++++++++------- ychat/DOCKER-SQLITE.md | 112 ---------------------------- ychat/DOCKER.md | 177 +++++++++++++++++++++++++++++++++++--------- ychat/Dockerfile | 59 ++++++++------- ychat/Dockerfile.sqlite | 82 -------------------- ychat/etc/ychat.conf | 4 +- ychat/html/index_guest.html | 33 --------- ychat/html/input.html | 2 +- ychat/src/chat/chat.cpp | 9 ++- ychat/src/configure | 106 ++++++++++++-------------- ychat/src/configure.ac | 20 ++--- ychat/src/reqp.cpp | 15 ---- 12 files changed, 274 insertions(+), 405 deletions(-) delete mode 100644 ychat/DOCKER-SQLITE.md delete mode 100644 ychat/Dockerfile.sqlite delete mode 100644 ychat/html/index_guest.html diff --git a/README.md b/README.md index 8cbc53e..120f09c 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,13 @@ are kept here as historical/revival code. | Subproject | What it is | Status | |------------|------------|--------| -| [`./ychat`](ychat/) | An HTTP-based web chat server (browsers are the clients; CSS/HTML/JS only). | **Revived & deployed** — Mode A (in-memory guest chat) builds in Docker, runs on the f3s k3s cluster. Mode B (embedded SQLite, real persistent accounts) builds and works locally — see [`ychat/DOCKER-SQLITE.md`](ychat/DOCKER-SQLITE.md) — but isn't deployed. | +| [`./ychat`](ychat/) | An HTTP-based web chat server (browsers are the clients; CSS/HTML/JS only). | **Revived, builds in Docker with a mandatory embedded-SQLite backend** (real, persistent registered accounts) — see [`ychat/DOCKER.md`](ychat/DOCKER.md). Builds and is verified locally; **not yet deployed** to f3s — the live cluster (`https://ychat.f3s.lan.buetow.org/`) still runs an older, in-memory-only, no-database image. | | [`./yhttpd`](yhttpd/) | A tiny standalone http server derived from ychat's socket/threading engine. | Builds and serves reliably in Docker (verified under concurrent load) — not deployed. See [`./yhttpd/DOCKER.md`](yhttpd/DOCKER.md). | | [`./ycurses`](ycurses/) | A curses front-end experiment. | Builds and runs in Docker (a demo, not a service, so nothing to deploy) — see [`./ycurses/BUILD.md`](ycurses/BUILD.md). | The detailed, up-to-date build/deploy notes for the chat live in -[`./ychat/DOCKER.md`](ychat/DOCKER.md) (Mode A) and -[`./ychat/DOCKER-SQLITE.md`](ychat/DOCKER-SQLITE.md) (Mode B). The rest of -this file is a quickstart for running **ychat** locally in Docker and -accessing it. +[`./ychat/DOCKER.md`](ychat/DOCKER.md). The rest of this file is a +quickstart for running **ychat** locally in Docker and accessing it. > The ychat tree has been substantially fixed during this revival (legacy-C++ > build fixes, a from-scratch streaming-chat layer, and a security/bug sweep). @@ -38,15 +36,17 @@ podman build -t ychat:dev . # or: docker build -t ychat:dev . ``` -The build configures ychat with all optional features off (no SSL, no MySQL, -no readline) — this is "Mode A": an **in-memory guest chat with no account -database**. The default chat port is **2000**. +The build configures ychat with SSL and readline off, but a database is not +optional: `./configure` always requires SQLite (`sqlite3.h`/`libsqlite3`), +so registration/login persist in a SQLite file across container restarts. +The default chat port is **2000**. ### 2. Run it ```sh -podman run --rm -p 2000:2000 --name ychat ychat:dev -# or: docker run --rm -p 2000:2000 --name ychat ychat:dev +mkdir -p /tmp/ychat-data && chmod 777 /tmp/ychat-data # see DOCKER.md for why +podman run --rm -p 2000:2000 --name ychat -v /tmp/ychat-data:/app/data:Z ychat:dev +# or: docker run --rm -p 2000:2000 --name ychat -v /tmp/ychat-data:/app/data ychat:dev ``` The server logs to stdout. You should see something like: @@ -62,8 +62,10 @@ Initializing sock events (1) Open http://localhost:2000/ in a browser. -- You'll get the **guest login page** (no password field, no "Register" link — - there is no account database in this build). +- You'll get the full login page (password field + "Register" link). You can + register a nick/password (persisted in the SQLite file under + `/app/data`), or leave the password blank and log in as an unregistered + guest — `chat.enableguest=true` allows that regardless of the database. - Enter any alphanumeric nick (e.g. `alice`), leave the room as `Lounge`, and click **login**. - The chat frameset loads: a streaming message view, the online-user list, and @@ -93,32 +95,38 @@ podman rm -f ychat ## Notes on the local run -- **State is in-memory only.** With no database, all users/sessions/rooms live - in RAM and are wiped on container restart. That's intentional for the - revival; `chat.enableguest=true` lets anyone log in with just a nick. +- **Registered accounts persist; guest sessions don't.** The SQLite file at + `/app/data/ychat.db` (bind-mount it, as above, to survive container + restarts) holds registered users. Sessions/rooms/online-state are still + in-memory, and unregistered `chat.enableguest=true` guest chatters are + wiped on restart same as before — only the accounts table persists. - **Logs** go to `/app/log/` inside the container (`access_log`, `system_log`, `rooms/`). They're an `emptyDir` in k8s and a container-local dir locally, so they don't persist after `rm`. - **Configuration** is `ychat/etc/ychat.conf`, baked into the image at `/app/etc/ychat.conf`. You can override any config key at runtime with `-o ` (the image already does this for - `chat.session.md5hash=false` and `httpd.startsite=index_guest.html`). + `chat.session.md5hash=false` and `chat.database.dbname=data/ychat.db`). Example: `podman run --rm -p 2000:2000 ychat:dev /app/bin/ychat -o chat.idle.timeout 300`. -- **No operator commands for guests.** The default-operator escalation - (`/exec` shell RCE) was removed for security; in this no-DB build there is no - authenticated operator, so privileged commands (`/ko`, `/ban`, `/exec`, …) - are unavailable by design. +- **The `/exec` command module is removed from the image entirely** + (defense-in-depth against its shell-injection RCE), and operator status + via `chat.defaultop` now requires a database-authenticated registered + account — an unregistered guest can never claim it. Other privileged + commands (`/ko`, `/ban`, …) work normally for a registered operator. --- ## Deploying to the f3s k3s cluster -This is covered in detail in [`./ychat/DOCKER.md`](ychat/DOCKER.md). In short: -the image is pushed to the f3s private registry -(`r0.lan.buetow.org:30001/ychat:`), and a Helm chart + ArgoCD Application -in the [`conf` repo](https://codeberg.org/snonux/conf) (path -`f3s/ychat/helm-chart`) deploy it. The LAN URL is -**https://ychat.f3s.lan.buetow.org/**. +This is covered in detail in [`./ychat/DOCKER.md`](ychat/DOCKER.md) (push +steps, Helm chart, ArgoCD Application in the +[`conf` repo](https://codeberg.org/snonux/conf), path +`f3s/ychat/helm-chart`). **The DB-backed build described in this README is +not deployed there yet** — the live LAN URL +(**https://ychat.f3s.lan.buetow.org/**) currently still serves the older, +in-memory-only, no-database image. Rolling out this build needs a persistent +volume for `/app/data` (the existing Helm chart doesn't provision one) and +is a deliberate follow-up, not automatic. --- diff --git a/ychat/DOCKER-SQLITE.md b/ychat/DOCKER-SQLITE.md deleted file mode 100644 index ce82c16..0000000 --- a/ychat/DOCKER-SQLITE.md +++ /dev/null @@ -1,112 +0,0 @@ -# yChat — Mode B: embedded SQLite (real accounts, no MySQL server) - -Mode A (`Dockerfile`, `DOCKER.md`) is an in-memory guest chat with no account -database. This is **Mode B**: `DATABASE` is enabled and backed by an embedded -SQLite file instead of MySQL, so registration/login persist across restarts -without depending on an external database server. - -This is a local proof-of-concept build, **not deployed to f3s** — the live -cluster still runs Mode A (`https://ychat.f3s.lan.buetow.org/`). - -## Build & run (local) - -```sh -cd ychat -podman build -t ychat:sqlite -f Dockerfile.sqlite . -mkdir -p /path/to/data && chmod 777 /path/to/data # see note below -podman run --rm -p 2000:2000 -v /path/to/data:/app/data:Z ychat:sqlite -``` - -Open http://localhost:2000/ — this is the **real** login page (password -field, "Register" link), since `DATABASE` is enabled. Register a nick, -restart the container, log back in with the same password: it works, because -the database is the file at `/app/data/ychat.db` (bind-mounted). - -**Rootless-podman note:** a bind-mounted host directory is usually not -writable by the container's non-root `ychat` user (UID 1000) because of user -namespace remapping. `chmod 777` on the host directory is the quick fix for -local testing; for a real deployment use a named volume or a properly -`chown`ed hostPath/PVC instead. - -## What changed vs. Mode A - -- `src/configure.ac` / `src/configure`: added `--enable-sqlite` (checks for - `sqlite3.h` / `-lsqlite3`), mirroring the existing `--enable-mysql` - machinery. `src/config.h.in`/`src/glob.h`: when both are detected, - `HAVE_SQLITE3_H`+`HAVE_LIBSQLITE3` define `USE_SQLITE` + `DATABASE` - (mirrors the existing `HAVE_MYSQL_MYSQL_H`+`HAVE_LIBMYSQLCLIENT` -> - `USE_MYSQL`+`DATABASE` block). -- `src/data/con.h`/`con.cpp`: `#ifdef USE_SQLITE` branch opens a - `sqlite3*` instead of `MYSQL*`, sets `PRAGMA journal_mode=WAL` + - a busy timeout (multiple pooled connections open the same file - concurrently; this data layer has no query-retry logic of its own, so a - writer needs to wait for a lock rather than fail immediately with - `SQLITE_BUSY`), and runs `CREATE TABLE IF NOT EXISTS user (...)` - - MySQL deployments are expected to have this table created out-of-band; - SQLite has no such step, so bootstrap it on first connect. -- `src/data/data.h`/`data.cpp`: `#ifdef USE_SQLITE` branch rebuilds - `select_user_data`/`insert_user_data`/`update_user_data` on - `sqlite3_prepare_v2`/`sqlite3_bind_text`/`sqlite3_step` (parameterized - queries) instead of hand-built SQL strings. This is safer than the MySQL - path's `secure_query()`, which prevents injection by *transliterating* - `"`/`\` to `'`/`/` rather than escaping them (crude but functional for - MySQL; parameter binding sidesteps the whole class of problem for SQLite, - so there's no SQLite equivalent of `secure_query()`). -- `etc/ychat.conf`: `chat.database.dbname`'s description now notes it - doubles as the SQLite file path in this mode (`serverhost`/`user`/ - `password`/`port` are unused). -- **Renamed `class data` to `class ychatdb`** (`data.h`/`data.cpp`, - `wrap.h`/`wrap.cpp`, `mods/html/yc_register.cpp`): a class literally named - `data` collides with `std::data()` (C++17) under `using namespace std` - - GCC 11 reports "reference to 'data' is ambiguous". This is the *third* - instance of this exact bug class found across this revival (see the - `function`->`mod_func_t` rename in `glob.h`/`modl.cpp`, and - `attributes::set`->`set_attr_flag` in `../ycurses`) - all three are - 1990s/2000s-era C++ that picked short, common names later claimed by the - standard library, invisible until `using namespace std` + a modern - standard collide them. -- **Fixed an infinite-loop OOM in `data_base.cpp`'s query-config parser**: - `unsigned i_pos` truncating `string::npos` (the *exact* same bug class as - the `unsigned`-vs-`size_t` fixes already made across `../yhttpd`) made the - last-token check `i_pos != string::npos` always true, and `i_pos+1` - wrapped back to `0` in 32-bit arithmetic - so the loop never advanced or - terminated, growing a `vector` forever. Fixed to `size_t`. This - had never been hit before: `DATABASE` was never actually compiled - previously (Mode A disables it, and `--enable-mysql` was separately - broken - see below), so this whole code path was completely untested - until Mode B exercised it for the first time. - -## Verified - -Built and run in a Rocky Linux 9 container (matching Mode A's toolchain): -register (`POST register.html`) creates a row in the SQLite `user` table; -login (`POST frameset.html`) with the correct password succeeds (returns the -chat frameset) and with a wrong password is rejected -(`chat.msgs.err.wrongpassword`); a second registered user's login still -works identically after a full `podman restart` (proving the SQLite file -persisted the account, not just an in-memory cache); 15 sequential requests -post-restart all `200`, no crashes/restarts. `update_user_data` -(`savechangednick`, used when a logged-in user changes options) uses the -same prepare/bind/step pattern as the verified insert/select paths but -wasn't independently exercised over HTTP (it needs an authenticated session -cookie) - verified by code inspection only. - -## Known, pre-existing, deliberately NOT fixed - -- **`--enable-mysql` doesn't work**, independent of anything here: - `configure.ac` registers the option as `AC_ARG_ENABLE(mysqlclient, ...)` - (setting `$enable_mysqlclient`) but the help text advertises - `--enable-mysql`, and the actual gating check later tests `$enable_mysql` - - a third, never-set variable. So MySQL support has likely never been - selectable via `./configure` since this script was written. Left alone: - this task is about moving *away* from MySQL, not fixing it. -- **Passwords are stored and compared in plaintext** (`yc_register.cpp`, - `chat.cpp`'s login check) - this predates the SQLite work (same behavior - as the MySQL path) and is a bigger, separate concern than "swap the - database backend"; not addressed here. -- **`data::secure_query()`'s MySQL-only escaping** (`data.cpp`, `#else` - branch) has the same `unsigned i_pos != string::npos` bug as the one - fixed in `data_base.cpp` above. Unreached by this build (`USE_SQLITE` is - defined, so the `#else` branch never compiles here) and MySQL is - unreachable anyway per the point above - not fixed, since fixing dead - code invites bit-rot without a way to verify it. diff --git a/ychat/DOCKER.md b/ychat/DOCKER.md index 2ca751c..3bb6dfd 100644 --- a/ychat/DOCKER.md +++ b/ychat/DOCKER.md @@ -1,25 +1,49 @@ -# yChat — Docker revival (Mode A) +# yChat — Docker revival (embedded SQLite backend) -yChat is a legacy (2007) C++ HTTP chat server. This revival builds it **entirely -in a Docker container** and deploys it to the f3s k3s cluster as an -in-memory guest chat — **no database, no persistent user store**. +yChat is a legacy (2007) C++ HTTP chat server. This revival builds it +**entirely in a Docker container**. A database is no longer optional: the +build always compiles in `DATABASE`, backed by an embedded SQLite file, so +registration and login persist across container restarts. There is no more +"no-DB" build option — see "History" below if you're wondering why some +older commits/docs mention one. ## Build Multi-stage `Dockerfile`: Rocky Linux 9 builder (GCC 11 tolerates the legacy -C++ this tree uses) + slim Rocky 9 runtime. All optional features are OFF: +C++ this tree uses) + slim Rocky 9 runtime. -``` +```sh +cd ychat podman build -t ychat:dev . -podman run --rm -p 2000:2000 ychat:dev +# or: docker build -t ychat:dev . +``` + +## Run + +```sh +mkdir -p /path/to/data && chmod 777 /path/to/data # see rootless-podman note below +podman run --rm -p 2000:2000 -v /path/to/data:/app/data:Z ychat:dev # smoke test (ychat returns proper HTTP/1.1 responses): curl -sS http://127.0.0.1:2000/index.html -o /dev/null -w '%{http_code}\n' ``` +Open http://localhost:2000/ — this is the full login page (password field, +"Register" link). Register a nick, restart the container, log back in with +the same password: it works, because the account lives in the SQLite file at +`/app/data/ychat.db` (bind-mounted, so it survives the container being +recreated). + +**Rootless-podman note:** a bind-mounted host directory is usually not +writable by the container's non-root `ychat` user (UID 1000) because of user +namespace remapping. `chmod 777` on the host directory is the quick fix for +local testing; for a real deployment use a named volume or a properly +`chown`ed hostPath/PVC instead. + ## Legacy-C++ patches applied -The tree does not build on a modern toolchain unmodified. Three minimal, -semantics-preserving patches were made so it builds on Rocky 9 / GCC 11: +The tree does not build on a modern toolchain unmodified. A handful of +minimal, semantics-preserving patches were made so it builds on Rocky 9 / +GCC 11 (see `git log` for the full, ongoing list; highlights): - `src/glob.h`, `src/modl.cpp`: renamed the project's `typedef int function(...)` type to `mod_func_t` — it collided with `std::function` brought in by @@ -29,6 +53,17 @@ semantics-preserving patches were made so it builds on Rocky 9 / GCC 11: - `src/sock/sock.cpp`: reordered `i_server_sock = i_sock;` to *before* the `set_nonblock(i_server_sock)` call — previously `set_nonblock` ran on an uninitialised member, returning EBADF and aborting startup. +- `src/data/data.h`/`data.cpp`, `src/wrap.h`/`wrap.cpp`, + `src/mods/html/yc_register.cpp`: renamed `class data` to `class ychatdb` — + a class literally named `data` collides with `std::data()` (C++17) under + `using namespace std` ("reference to 'data' is ambiguous"). Same bug class + as the `function`→`mod_func_t` rename above. +- `src/data/data_base.cpp`: `unsigned i_pos` → `size_t i_pos` in the + config-query token parser — truncating `string::npos` to 32-bit made the + "no more tokens" check never true and `i_pos+1` wrap back to `0`, so the + loop never terminated (an infinite loop that OOM-killed the container + within seconds of startup, the first time `DATABASE` was ever actually + compiled and run). ## Runtime layout (WORKDIR /app) @@ -36,22 +71,100 @@ semantics-preserving patches were made so it builds on Rocky 9 / GCC 11: |------|---------|-----------| | `bin/ychat` | server binary | no | | `etc/ychat.conf` | config (found via `./etc/` search path) | no | -| `html/` | templates served over HTTP | no | +| `html/` | templates, including `register.html`/`options.html` | no | | `mods/{commands,html}/*.so` | runtime-loadable modules | no | -| `log/` | access/system/room logs | yes (emptyDir in k8s) | - -With MySQL disabled, all user/session/room state is **in-memory only** and is -lost on restart. `chat.enableguest=true` lets guests log in without a DB. - -> Note: ychat now emits proper HTTP/1.1 responses (`HTTP/1.1 200 OK` + headers). -> Earlier in the revival it sent headerless HTTP/0.9-style bodies (the response -> builder left the headers in a local string and never wrote them back to the -> socket buffer); that was fixed in `src/reqp.cpp`. Browsers and `curl` work -> normally now. +| `log/` | access/system/room logs | yes (mount a volume) | +| `data/` | SQLite db file (`ychat.db`) | yes (mount a volume for persistence) | + +## What the database backend looks like + +- `src/configure.ac`/`src/configure`: `sqlite3.h`/`-lsqlite3` are checked + unconditionally (like `pthread`/`libevent`) — there's no `--enable-sqlite` + flag any more, it isn't optional. `--enable-mysql` still exists + (`glob.h`'s `USE_MYSQL`/`USE_SQLITE`/`DATABASE` mechanism supports either + backend) but is a pre-existing, separately-broken code path + (`configure.ac` registers it as `AC_ARG_ENABLE(mysqlclient,...)` but the + gating check tests a different, never-set `$enable_mysql`) that this + revival deliberately leaves alone — the direction is away from MySQL, not + toward fixing it. +- `src/data/con.h`/`con.cpp`: the `USE_SQLITE` branch opens a `sqlite3*`, + sets `PRAGMA journal_mode=WAL` plus a busy timeout (multiple pooled + connections open the same file concurrently; this data layer has no + query-retry logic of its own, so a writer needs to wait for a lock rather + than fail immediately with `SQLITE_BUSY`), and runs + `CREATE TABLE IF NOT EXISTS user (...)` on first connect (SQLite has no + separate schema-provisioning step, unlike a MySQL deployment where the + table is expected to exist already). +- `src/data/data.h`/`data.cpp`: `select_user_data`/`insert_user_data`/ + `update_user_data` use `sqlite3_prepare_v2`/`sqlite3_bind_text`/ + `sqlite3_step` (parameterized queries), which is safer than the MySQL + path's `secure_query()` — that escapes by *transliterating* `"`/`\` to + `'`/`/` rather than properly escaping them. +- `etc/ychat.conf`: `chat.database.dbname` doubles as the SQLite file path + (`serverhost`/`user`/`password`/`port` only matter for a `--enable-mysql` + build). +- Unregistered **guest logins are still a supported runtime feature**, + independent of the database: `chat.enableguest=true` in `ychat.conf` lets + anyone log in with just a nick and no password (see `src/chat/chat.cpp`). + A guest's `is_reg` is always `false`, so a guest can never be granted + operator status via `chat.defaultop` — only a database-authenticated + registered account can. + +> Note: ychat emits proper HTTP/1.1 responses (`HTTP/1.1 200 OK` + headers). +> Earlier in the revival it sent headerless HTTP/0.9-style bodies (the +> response builder left the headers in a local string and never wrote them +> back to the socket buffer); that was fixed in `src/reqp.cpp`. + +## Verified + +Built and run in a Rocky Linux 9 container: register (`POST register.html`) +creates a row in the SQLite `user` table; login (`POST frameset.html`) with +the correct password succeeds (returns the chat frameset) and with a wrong +password is rejected (`chat.msgs.err.wrongpassword`); a registered user's +login still works identically after a full container restart/recreate +(proving the SQLite file persisted the account, not just an in-memory +cache); the input frame shows Colors/Options/Help/Users/Admin/Logout; both +`options.html` and `register.html` resolve (not "Page not found"). + +## Known, pre-existing, deliberately NOT fixed + +- **`--enable-mysql` doesn't work** (see above) — left alone; this revival + moves away from MySQL rather than fixing it. +- **Passwords are stored and compared in plaintext** + (`src/mods/html/yc_register.cpp`, `src/chat/chat.cpp`'s login check) — a + bigger, separate concern than "always require a database backend"; not + addressed here. +- **`data::secure_query()`'s MySQL-only escaping** (`data.cpp`, the + `USE_MYSQL` branch) has the same `unsigned i_pos != string::npos` bug + class fixed in `data_base.cpp` above — unreached by this build + (`USE_SQLITE` is what actually gets compiled) and MySQL is unreachable + anyway per the point above, so not fixed: fixing dead code invites + bit-rot without a way to verify it. + +## History + +Earlier in this revival there were briefly two build modes: an in-memory, +no-database "Mode A" guest chat (the original, minimal revival, and what's +still running live on the f3s k3s cluster as of this writing) and an opt-in +`--enable-sqlite` "Mode B" alongside it (`Dockerfile.sqlite`, +`DOCKER-SQLITE.md`). A later task removed Mode A entirely: `DATABASE` (and +therefore SQLite) is now mandatory, there's only one `Dockerfile`, and the +build-aware UI toggles that existed to make Mode A's lack of a database less +confusing (guest-only login page, hidden Register link, hidden Options menu) +were reverted, since they no longer apply — the full login/registration/ +options UI is always functional now. Unregistered guest chatting itself was +**not** removed — `chat.enableguest` is a runtime config option, not a +compile flag, and stayed as-is. + +**Not yet deployed to f3s**: the live cluster +(`https://ychat.f3s.lan.buetow.org/`) still runs the old no-DB image. Rolling +this DB-backed build out is a separate, deliberate decision (needs a +persistent volume for `/app/data`, updated Helm chart, etc.) — not done as +part of removing the no-DB build option from the codebase. ## Push to the f3s registry -``` +```sh TAG=$(git rev-parse --short HEAD) podman tag ychat:$TAG r0.lan.buetow.org:30001/ychat:$TAG podman tag ychat:latest r0.lan.buetow.org:30001/ychat:latest @@ -68,21 +181,15 @@ Config lives in the `conf` repo (mirrored on the in-cluster git-server): The Deployment pulls `registry.lan.buetow.org:30001/ychat:` (tag matches `appVersion` in `Chart.yaml`). Logs go to an `emptyDir` on `/app/log` — -ephemeral by design (no app state to persist). After chart/app edits push conf: - -``` -git push master master && git push r0 master -``` - -ArgoCD auto-syncs. URLs (LAN wildcard DNS resolves via Pi-hole): - -- https://ychat.f3s.lan.buetow.org -- http://ychat.f3s.buetow.org (via OpenBSD relayd frontend, if wired) +ephemeral by design. The SQLite database at `/app/data` needs a real +persistent volume (PVC) to survive pod rescheduling — the current Helm chart +was written for the no-DB build and doesn't provision one yet; see "History" +above. -## Optional follow-ups (not in scope for Mode A) +## Optional follow-ups (not in scope for this task) - Port to a modern toolchain (`hash_map`→`unordered_map`, fix implicit returns, modernise `configure.ac`) and drop the legacy-C++ patches. -- Mode B: enable MySQL for persistent registered users — requires a - hand-written `CREATE TABLE` schema (none ships in this repo), a MariaDB - StatefulSet + PVC, and a Secret for `chat.database.password`. \ No newline at end of file +- Fix plaintext password storage/comparison. +- Actually deploy this build to f3s (PVC for `/app/data`, updated Helm + chart, ArgoCD sync) — a human decision, not automated here. diff --git a/ychat/Dockerfile b/ychat/Dockerfile index 8403ad3..8934f62 100644 --- a/ychat/Dockerfile +++ b/ychat/Dockerfile @@ -1,15 +1,28 @@ -# yChat revival image — Mode A (no DB, no SSL, no readline) +# yChat revival image — embedded SQLite backend (no MySQL server, no SSL, no +# readline). +# # Multi-stage build on Rocky Linux 9, whose GCC 11 still tolerates the # legacy C++ this 2007 codebase uses (__gnu_cxx::hash_map, ofstream == NULL), -# so the tree builds with ZERO source patches. +# so the tree builds with ZERO source patches beyond the ones already applied +# during this revival (see git log / README.md). +# +# A database is no longer optional: ./configure always requires sqlite3.h + +# libsqlite3 (see src/configure.ac), so DATABASE is unconditionally compiled +# in — the old "Mode A" in-memory-only, no-account guest chat no longer +# exists as a build option. Registration and login persist across restarts +# as long as /app/data is on a persistent volume. Unregistered guest logins +# are still supported as a *runtime* toggle (chat.enableguest=true in +# etc/ychat.conf) — that's independent of the database being compiled in. # # Runtime layout (WORKDIR /app): # bin/ychat server binary # etc/ychat.conf config (found via ./etc/ search path) -# html/ templates (read-only, served over HTTP) +# html/ templates, including register.html/options.html # mods/commands/*.so runtime-loadable command modules # mods/html/*.so runtime-loadable html modules # log/ writable logs (mount a volume here) +# data/ writable SQLite db file (mount a volume here for +# persistence across container restarts) # ---------- builder ---------- FROM rockylinux:9 AS builder @@ -20,16 +33,18 @@ RUN dnf -y install \ autoconf \ automake \ libevent-devel \ + sqlite-devel \ && dnf clean all WORKDIR /build/ychat COPY . . -# Configure with all optional features OFF (Mode A: in-memory guest chat). -# Run the top-level "all" (build modules base) so runtime .so modules are -# produced under /build/ychat/mods/{commands,html}/. +# Configure with SSL/MySQL/readline off; SQLite is required and always on +# (no --enable-sqlite flag any more — see src/configure.ac). Run the +# top-level "all" (build modules base) so runtime .so modules are produced +# under /build/ychat/mods/{commands,html}/. RUN cd src \ - && ./configure --disable-readline --disable-ssl --disable-mysql \ + && ./configure --disable-readline --disable-ssl \ && cd .. \ && make -j"$(nproc)" @@ -39,6 +54,7 @@ FROM rockylinux:9 AS runtime RUN dnf -y install \ libevent \ libstdc++ \ + sqlite-libs \ tzdata \ ca-certificates \ && dnf clean all @@ -57,24 +73,15 @@ COPY --from=builder /build/ychat/mods/ /app/mods/ # Defense-in-depth: the /exec command module does popen() on an attacker- # controlled shell string (host RCE for any operator). It is gated to # operators by permissions and the defaultop grant now requires a -# registered user, but we also physically omit it from the revival image -# so the RCE primitive is absent, not merely unreachable. +# registered (database-authenticated) user, but we also physically omit it +# from the image so the RCE primitive is absent, not merely unreachable. RUN rm -f /app/mods/commands/yc_exec.so -# This is a no-database (guest-mode) build: registration is non-functional -# (yc_register's body is #ifdef DATABASE), so drop the dead register form and -# the register module from the image, and serve the guest login page -# (index_guest.html - no password field, no register link) as the start page. -# The Options menu (options.html + yc_options) is likewise DB-only account -# management (email/password), so drop it too; the Options link in the input -# frame is hidden via the #ifndef DATABASE %%OPTIONS_LINK%% substitution. -RUN rm -f /app/html/register.html /app/mods/html/yc_register.so -RUN rm -f /app/html/options.html /app/mods/html/yc_options.so COPY docker-entrypoint.sh /app/docker-entrypoint.sh COPY etc/ychat.conf /app/etc/ychat.conf -# Writable log dir (entrypoint recreates /app/log/rooms since a volume mount -# on /app/log hides the image's copy). -RUN mkdir -p /app/log/rooms && chown -R ychat:ychat /app +# Writable log + data dirs (entrypoint recreates log/rooms since a volume +# mount on /app/log hides the image's copy; data/ holds the sqlite file). +RUN mkdir -p /app/log/rooms /app/data && chown -R ychat:ychat /app USER 1000:1000 EXPOSE 2000 @@ -83,8 +90,8 @@ ENTRYPOINT ["/app/docker-entrypoint.sh"] # chat.session.md5hash=false: the md5 session-id path does a bad substr() # (the default salt contains chars not in chat.session.validchars, so # s_ret.find(salt) returns npos and the subsequent substr/append corrupts -# the heap and segfaults on login). The 32-char random id is enough for a -# revival guest chat. -# httpd.startsite=index_guest.html: the no-DB guest login page (no password -# field, no register link) - registration isn't possible without a database. -CMD ["/app/bin/ychat", "-o", "chat.session.md5hash", "false", "-o", "httpd.startsite", "index_guest.html"] \ No newline at end of file +# the heap and segfaults on login). The 32-char random id is enough. +# chat.database.dbname=data/ychat.db: SQLite file path (see con.cpp/con.h — +# this config key is reused as a file path instead of a MySQL db name when +# built against SQLite, which is the only backend this image builds). +CMD ["/app/bin/ychat", "-o", "chat.session.md5hash", "false", "-o", "chat.database.dbname", "data/ychat.db"] diff --git a/ychat/Dockerfile.sqlite b/ychat/Dockerfile.sqlite deleted file mode 100644 index bb0665b..0000000 --- a/ychat/Dockerfile.sqlite +++ /dev/null @@ -1,82 +0,0 @@ -# yChat revival image — Mode B (embedded SQLite, no SSL, no readline) -# -# Same Rocky Linux 9 / GCC 11 base as ../Dockerfile (Mode A), but built with -# --enable-sqlite instead of --disable-mysql: this restores real user -# accounts (register/login persist across restarts) without depending on an -# external MySQL server - the whole database is one file under /app/data. -# -# Runtime layout (WORKDIR /app): -# bin/ychat server binary -# etc/ychat.conf config (found via ./etc/ search path) -# html/ templates, INCLUDING register.html/options.html -# (functional now that DATABASE is enabled) -# mods/commands/*.so runtime-loadable command modules -# mods/html/*.so runtime-loadable html modules (yc_register/yc_options -# kept - only Mode A strips them) -# log/ writable logs (mount a volume here) -# data/ writable SQLite db file (mount a volume here for -# persistence across container restarts) - -# ---------- builder ---------- -FROM rockylinux:9 AS builder - -RUN dnf -y install \ - gcc-c++ \ - make \ - autoconf \ - automake \ - libevent-devel \ - sqlite-devel \ - && dnf clean all - -WORKDIR /build/ychat -COPY . . - -RUN cd src \ - && ./configure --disable-readline --disable-ssl --enable-sqlite \ - && cd .. \ - && make -j"$(nproc)" - -# ---------- runtime ---------- -FROM rockylinux:9 AS runtime - -RUN dnf -y install \ - libevent \ - libstdc++ \ - sqlite-libs \ - tzdata \ - ca-certificates \ - && dnf clean all - -# Non-root runtime user. ychat binds port 2000 (unprivileged). -RUN useradd -r -u 1000 -d /app -s /sbin/nologin ychat - -WORKDIR /app - -# Binary -COPY --from=builder /build/ychat/bin/ychat /app/bin/ychat - -# Read-only resources (register.html/options.html and their modules are -# kept - functional in this DB-enabled build, unlike Mode A). -COPY --from=builder /build/ychat/html/ /app/html/ -COPY --from=builder /build/ychat/mods/ /app/mods/ -# Defense-in-depth, same as Mode A: /exec does popen() on an -# attacker-controlled string, physically omit it regardless of DB mode. -RUN rm -f /app/mods/commands/yc_exec.so -COPY docker-entrypoint.sh /app/docker-entrypoint.sh -COPY etc/ychat.conf /app/etc/ychat.conf - -# Writable log + data dirs (entrypoint recreates log/rooms since a volume -# mount on /app/log hides the image's copy; data/ holds the sqlite file). -RUN mkdir -p /app/log/rooms /app/data && chown -R ychat:ychat /app - -USER 1000:1000 -EXPOSE 2000 - -ENTRYPOINT ["/app/docker-entrypoint.sh"] -# chat.session.md5hash=false: same pre-existing salt/substr bug as Mode A -# (unrelated to the database backend). -# chat.database.dbname=data/ychat.db: SQLite file path (see con.cpp/con.h - -# this config key is reused as a file path instead of a MySQL db name when -# built with --enable-sqlite). -CMD ["/app/bin/ychat", "-o", "chat.session.md5hash", "false", "-o", "chat.database.dbname", "data/ychat.db"] diff --git a/ychat/etc/ychat.conf b/ychat/etc/ychat.conf index 0c77c84..5c9e256 100644 --- a/ychat/etc/ychat.conf +++ b/ychat/etc/ychat.conf @@ -7,7 +7,7 @@