From 462e4ac6995760646b53e110f2662a6cd14fc882 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 2 Jul 2026 01:47:15 +0300 Subject: ychat: add Mode B - embedded SQLite backend for real user accounts (task cs0) Adds a second database backend alongside the (already-broken/dormant) MySQL one: --enable-sqlite (configure.ac/configure, mirrors --enable-mysql), USE_SQLITE (glob.h, mirrors USE_MYSQL), and a con/data implementation using sqlite3_prepare_v2/bind/step (parameterized queries - safer than the MySQL path's hand-rolled character-transliteration escaping). chat.database.dbname doubles as the SQLite file path; the "user" table is created on first connect (CREATE TABLE IF NOT EXISTS) since SQLite has no separate schema- provisioning step. New Dockerfile.sqlite (Mode B) builds and runs this in Rocky Linux 9; DOCKER-SQLITE.md documents everything in detail. DATABASE had never actually been compiled before this (Mode A always disables it, and --enable-mysql doesn't work - configure.ac registers it as AC_ARG_ENABLE(mysqlclient,...) but the gating check tests a third, never-set $enable_mysql - left alone, this task is about moving away from MySQL, not fixing it). Getting DATABASE to compile and actually run for the first time surfaced two real, previously-undetectable bugs, both fixed: - class data collided with std::data() (C++17) under "using namespace std" ("reference to 'data' is ambiguous") - renamed to ychatdb throughout (data.h/cpp, wrap.h/cpp, yc_register.cpp). Same bug class as the function->mod_func_t rename already made in glob.h. - data_base.cpp's config-query parser used "unsigned i_pos" for a string::npos comparison - truncating npos to 32-bit makes the "no more tokens" check never true, and i_pos+1 wraps back to 0, so the loop never advances: an infinite loop that OOM-killed the container within seconds of startup. Fixed to size_t (same bug class already fixed repeatedly in ../yhttpd). Verified in Docker: register creates a row, wrong password is rejected, correct password succeeds, and - the actual point of this task - a second user's login still works identically after a full container restart with the db file on a bind-mounted volume, proving persistence. Independently reproduced by a fresh-context review agent, which also rebuilt + re-verified the whole flow itself. Not deployed to f3s - this is a local proof of concept alongside the live Mode A (in-memory guest chat) deployment. Co-Authored-By: Claude Sonnet 5 --- README.md | 12 +- ychat/DOCKER-SQLITE.md | 112 ++++++++++++++++++ ychat/Dockerfile.sqlite | 82 +++++++++++++ ychat/etc/ychat.conf | 2 +- ychat/src/config.h.in | 6 + ychat/src/configure | 227 ++++++++++++++++++++++++++++++++++++ ychat/src/configure.ac | 10 ++ ychat/src/data/con.cpp | 52 +++++++++ ychat/src/data/con.h | 8 ++ ychat/src/data/data.cpp | 202 ++++++++++++++++++++++++++++++-- ychat/src/data/data.h | 22 +++- ychat/src/data/data_base.cpp | 10 +- ychat/src/glob.h | 8 ++ ychat/src/mods/html/yc_register.cpp | 2 +- ychat/src/msgs.h | 3 + ychat/src/wrap.cpp | 4 +- ychat/src/wrap.h | 4 +- 17 files changed, 742 insertions(+), 24 deletions(-) create mode 100644 ychat/DOCKER-SQLITE.md create mode 100644 ychat/Dockerfile.sqlite diff --git a/README.md b/README.md index 23bdb7f..8cbc53e 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,15 @@ 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** — builds in Docker, runs on the f3s k3s cluster. | -| [`./yhttpd`](yhttpd/) | A tiny standalone http server derived from ychat's socket/threading engine. | Unrevived (see its own tree). | -| [`./ycurses`](ycurses/) | A curses front-end experiment. | Unrevived (see its own tree). | +| [`./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. | +| [`./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). The rest of this file is a quickstart -for running **ychat** locally in Docker and accessing it. +[`./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. > 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). diff --git a/ychat/DOCKER-SQLITE.md b/ychat/DOCKER-SQLITE.md new file mode 100644 index 0000000..ce82c16 --- /dev/null +++ b/ychat/DOCKER-SQLITE.md @@ -0,0 +1,112 @@ +# 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/Dockerfile.sqlite b/ychat/Dockerfile.sqlite new file mode 100644 index 0000000..bb0665b --- /dev/null +++ b/ychat/Dockerfile.sqlite @@ -0,0 +1,82 @@ +# 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 698f5cb..0c77c84 100644 --- a/ychat/etc/ychat.conf +++ b/ychat/etc/ychat.conf @@ -135,7 +135,7 @@