summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-02 10:01:50 +0300
committerPaul Buetow <paul@buetow.org>2026-07-02 10:01:50 +0300
commit67babb244b8ab6fcdfe3748b1c52fcac60eb2ed4 (patch)
tree88c40aff603237cb5eacc9fbc76de8b56d7aaff1
parent462e4ac6995760646b53e110f2662a6cd14fc882 (diff)
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 <noreply@anthropic.com>
-rw-r--r--README.md60
-rw-r--r--ychat/DOCKER-SQLITE.md112
-rw-r--r--ychat/DOCKER.md177
-rw-r--r--ychat/Dockerfile59
-rw-r--r--ychat/Dockerfile.sqlite82
-rw-r--r--ychat/etc/ychat.conf4
-rw-r--r--ychat/html/index_guest.html33
-rw-r--r--ychat/html/input.html2
-rw-r--r--ychat/src/chat/chat.cpp9
-rwxr-xr-xychat/src/configure106
-rw-r--r--ychat/src/configure.ac20
-rw-r--r--ychat/src/reqp.cpp15
12 files changed, 274 insertions, 405 deletions
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/<room>`). 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 <key> <value>` (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:<tag>`), 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<string>` 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>` (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 @@
</option>
<option name="enableguest">
<value>true</value>
- <descr>Is set to true if guest chatters are allowed. If database support is disabled this option HAS to be enabled otherwise no login will work because all chatter are treated as guests if there is no database!</descr>
+ <descr>Is set to true if unregistered nicks may log in as guest chatters (no password required). A database backend is always compiled in, so this is independent of that; set to false to require every login to be a registered account</descr>
</option>
<option name="defaultrang">
<value>3</value>
@@ -135,7 +135,7 @@
</option>
<option name="dbname">
<value>ychat_advanced</value>
- <descr>Specifies the MySQL database name. If built with --enable-sqlite instead of --enable-mysql, this is reused as the SQLite database file path (e.g. var/ychat.db); serverhost/user/password/port are ignored in that mode</descr>
+ <descr>SQLite database file path (e.g. data/ychat.db) - SQLite is the default and only always-available backend. If built with --enable-mysql instead, this is the MySQL database name and serverhost/user/password/port apply</descr>
</option>
<option name="port">
<value>3306</value>
diff --git a/ychat/html/index_guest.html b/ychat/html/index_guest.html
deleted file mode 100644
index 4a6ed93..0000000
--- a/ychat/html/index_guest.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
-<html>
-<head>
- <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
- <title>%%ychat.version%%</title>
- <link rel=stylesheet href="style.css" type="text/css" />
-</head>
-<body>
-<h1>%%ychat.version%%</h1>
-<div>%%INFO%%</div>
-<form action="frameset.html" method="POST">
- <input type="hidden" name="event" value="login" />
- <div>Enter your nick:</div>
- <div>
- <input class="text" type="text" name="nick" value="%%nick%%" maxlength="%%chat.maxlength.username%%" accesskey="n" />
- </div>
- <br />
- <div>Enter your room:</div>
- <div>
- <input class="text" type="text" name="room" value="%%chat.defaultroom%%" maxlength="%%chat.maxlength.roomn