diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-02 01:47:15 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-02 01:47:15 +0300 |
| commit | 462e4ac6995760646b53e110f2662a6cd14fc882 (patch) | |
| tree | 3acf0a34361cd4c48dfb27164eadf1d51823c1ca | |
| parent | 5c744e37b6375f3f6842cdf3758a180a649bafea (diff) | |
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 <noreply@anthropic.com>
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | ychat/DOCKER-SQLITE.md | 112 | ||||
| -rw-r--r-- | ychat/Dockerfile.sqlite | 82 | ||||
| -rw-r--r-- | ychat/etc/ychat.conf | 2 | ||||
| -rw-r--r-- | ychat/src/config.h.in | 6 | ||||
| -rwxr-xr-x | ychat/src/configure | 227 | ||||
| -rw-r--r-- | ychat/src/configure.ac | 10 | ||||
| -rw-r--r-- | ychat/src/data/con.cpp | 52 | ||||
| -rw-r--r-- | ychat/src/data/con.h | 8 | ||||
| -rw-r--r-- | ychat/src/data/data.cpp | 202 | ||||
| -rw-r--r-- | ychat/src/data/data.h | 22 | ||||
| -rw-r--r-- | ychat/src/data/data_base.cpp | 10 | ||||
| -rw-r--r-- | ychat/src/glob.h | 8 | ||||
| -rw-r--r-- | ychat/src/mods/html/yc_register.cpp | 2 | ||||
| -rw-r--r-- | ychat/src/msgs.h | 3 | ||||
| -rw-r--r-- | ychat/src/wrap.cpp | 4 | ||||
| -rw-r--r-- | ychat/src/wrap.h | 4 |
17 files changed, 742 insertions, 24 deletions
@@ -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<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/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 @@ </option> <option name="dbname"> <value>ychat_advanced</value> - <descr>Specifies the MySQL database name</descr> + <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> </option> <option name="port"> <value>3306</value> diff --git a/ychat/src/config.h.in b/ychat/src/config.h.in index a17fb04..76abf7b 100644 --- a/ychat/src/config.h.in +++ b/ychat/src/config.h.in @@ -18,6 +18,9 @@ /* Define to 1 if you have the `readline' library (-lreadline). */ #undef HAVE_LIBREADLINE +/* Define to 1 if you have the `sqlite3' library (-lsqlite3). */ +#undef HAVE_LIBSQLITE3 + /* Define to 1 if you have the `ssl' library (-lssl). */ #undef HAVE_LIBSSL @@ -39,6 +42,9 @@ /* Define to 1 if you have the <readline/readline.h> header file. */ #undef HAVE_READLINE_READLINE_H +/* Define to 1 if you have the <sqlite3.h> header file. */ +#undef HAVE_SQLITE3_H + /* Define to 1 if you have the <stdint.h> header file. */ #undef HAVE_STDINT_H diff --git a/ychat/src/configure b/ychat/src/configure index 6f14c7f..582eb51 100755 --- a/ychat/src/configure +++ b/ychat/src/configure @@ -2319,6 +2319,11 @@ if test "${enable_mysqlclient+set}" = set; then enableval=$enable_mysqlclient; fi +# Check whether --enable-sqlite was given. +if test "${enable_sqlite+set}" = set; then + enableval=$enable_sqlite; +fi + header_error() { { echo "$as_me:$LINENO: error: Could not find required header, please check the installation of the required header" >&5 echo "$as_me: error: Could not find required header, please check the installation of the required header" >&2;} @@ -4379,6 +4384,228 @@ fi fi +echo -n "===> Configuring with SQLite " +if test -z $enable_sqlite || test $enable_sqlite != "yes"; then + echo disabled +else + echo enabled + +for ac_header in sqlite3.h +do +as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh` +if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then + { echo "$as_me:$LINENO: checking for $ac_header" >&5 +echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; } +if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then + echo $ECHO_N "(cached) $ECHO_C" >&6 +fi +ac_res=`eval echo '${'$as_ac_Header'}'` + { echo "$as_me:$LINENO: result: $ac_res" >&5 +echo "${ECHO_T}$ac_res" >&6; } +else + # Is the header compilable? +{ echo "$as_me:$LINENO: checking $ac_header usability" >&5 +echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; } +cat >conftest.$ac_ext <<_ACEOF +/* confdefs.h. */ +_ACEOF +cat confdefs.h >>conftest.$ac_ext +cat >>conftest.$ac_ext <<_ACEOF +/* end confdefs.h. */ +$ac_includes_default +#include <$ac_header> +_ACEOF +rm -f conftest.$ac_objext +if { (ac_try="$ac_compile" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5 + (eval "$ac_compile") 2>conftest.er1 + ac_status=$? + grep -v '^ *+' conftest.er1 >conftest.err + rm -f conftest.er1 + cat conftest.err >&5 + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); } && { + test -z "$ac_c_werror_flag" || + test ! -s conftest.err + } && test -s conftest.$ac_objext; then + ac_header_compiler=yes +else + echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_header_compiler=no +fi + +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5 +echo "${ECHO_T}$ac_header_compiler" >&6; } + +# Is the header present? +{ echo "$as_me:$LINENO: checking $ac_header presence" >&5 +echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; } +cat >conftest.$ac_ext <<_ACEOF +/* confdefs.h. */ +_ACEOF +cat confdefs.h >>conftest.$ac_ext +cat >>conftest.$ac_ext <<_ACEOF +/* end confdefs.h. */ +#include <$ac_header> +_ACEOF +if { (ac_try="$ac_cpp conftest.$ac_ext" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5 + (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1 + ac_status=$? + grep -v '^ *+' conftest.er1 >conftest.err + rm -f conftest.er1 + cat conftest.err >&5 + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); } >/dev/null && { + test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" || + test ! -s conftest.err + }; then + ac_header_preproc=yes +else + echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_header_preproc=no +fi + +rm -f conftest.err conftest.$ac_ext +{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5 +echo "${ECHO_T}$ac_header_preproc" >&6; } + +# So? What about this header? +case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in + yes:no: ) + { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5 +echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;} + { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5 +echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;} + ac_header_preproc=yes + ;; + no:yes:* ) + { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5 +echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;} + { echo "$as_me:$LINENO: WARNING: $ac_header: check for missing prerequisite headers?" >&5 +echo "$as_me: WARNING: $ac_header: check for missing prerequisite headers?" >&2;} + { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5 +echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;} + { echo "$as_me:$LINENO: WARNING: $ac_header: section \"Present But Cannot Be Compiled\"" >&5 +echo "$as_me: WARNING: $ac_header: section \"Present But Cannot Be Compiled\"" >&2;} + { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5 +echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;} + { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5 +echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;} + + ;; +esac +{ echo "$as_me:$LINENO: checking for $ac_header" >&5 +echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; } +if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then + echo $ECHO_N "(cached) $ECHO_C" >&6 +else + eval "$as_ac_Header=\$ac_header_preproc" +fi +ac_res=`eval echo '${'$as_ac_Header'}'` + { echo "$as_me:$LINENO: result: $ac_res" >&5 +echo "${ECHO_T}$ac_res" >&6; } + +fi +if test `eval echo '${'$as_ac_Header'}'` = yes; then + cat >>confdefs.h <<_ACEOF +#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1 +_ACEOF + +else + header_error +fi + +done + + +{ echo "$as_me:$LINENO: checking for sqlite3_open in -lsqlite3" >&5 +echo $ECHO_N "checking for sqlite3_open in -lsqlite3... $ECHO_C" >&6; } +if test "${ac_cv_lib_sqlite3_sqlite3_open+set}" = set; then + echo $ECHO_N "(cached) $ECHO_C" >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lsqlite3 $LIBS" +cat >conftest.$ac_ext <<_ACEOF +/* confdefs.h. */ +_ACEOF +cat confdefs.h >>conftest.$ac_ext +cat >>conftest.$ac_ext <<_ACEOF +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char sqlite3_open (); +int +main () +{ +return sqlite3_open (); + ; + return 0; +} +_ACEOF +rm -f conftest.$ac_objext conftest$ac_exeext +if { (ac_try="$ac_link" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5 + (eval "$ac_link") 2>conftest.er1 + ac_status=$? + grep -v '^ *+' conftest.er1 >conftest.err + rm -f conftest.er1 + cat conftest.err >&5 + echo "$as_me:$LINENO: \$? = $ac_status" >&5 + (exit $ac_status); } && { + test -z "$ac_c_werror_flag" || + test ! -s conftest.err + } && test -s conftest$ac_exeext && + $as_test_x conftest$ac_exeext; then + ac_cv_lib_sqlite3_sqlite3_open=yes +else + echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_cv_lib_sqlite3_sqlite3_open=no +fi + +rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ echo "$as_me:$LINENO: result: $ac_cv_lib_sqlite3_sqlite3_open" >&5 +echo "${ECHO_T}$ac_cv_lib_sqlite3_sqlite3_open" >&6; } +if test $ac_cv_lib_sqlite3_sqlite3_open = yes; then + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBSQLITE3 1 +_ACEOF + + LIBS="-lsqlite3 $LIBS" + +else + lib_error +fi + +fi + echo -n "===> Configuring with readline " if test -z $enable_readline || test $enable_readline != "yes"; then echo disabled diff --git a/ychat/src/configure.ac b/ychat/src/configure.ac index 1a11ae8..3284c7d 100644 --- a/ychat/src/configure.ac +++ b/ychat/src/configure.ac @@ -9,6 +9,7 @@ AC_ARG_ENABLE(readline, AC_HELP_STRING([--disable-readline], [Disables readline AC_ARG_ENABLE(readline, AC_HELP_STRING([--disable-readline], [Disables readline support (default=yes)])) AC_ARG_ENABLE(ssl, AC_HELP_STRING([--enable-ssl], [Enable OpenSSL support (default=no)])) AC_ARG_ENABLE(mysqlclient, AC_HELP_STRING([--enable-mysql], [Enable MySQL support (default=no)])) +AC_ARG_ENABLE(sqlite, AC_HELP_STRING([--enable-sqlite], [Enable embedded SQLite support (default=no)])) header_error() AC_MSG_ERROR([Could not find required header, please check the installation of the required header]) lib_error() AC_MSG_ERROR([Library test failed, please check the installation of the required library]) @@ -37,6 +38,15 @@ else AC_CHECK_LIB(mysqlclient, mysql_init, [], [lib_error]) fi +echo -n "===> Configuring with SQLite " +if test -z $enable_sqlite || test $enable_sqlite != "yes"; then + echo disabled +else + echo enabled + AC_CHECK_HEADERS(sqlite3.h, [], [header_error]) + AC_CHECK_LIB(sqlite3, sqlite3_open, [], [lib_error]) +fi + echo -n "===> Configuring with readline " if test -z $enable_readline || test $enable_readline != "yes"; then echo disabled diff --git a/ychat/src/data/con.cpp b/ychat/src/data/con.cpp index c60cd66..6b69487 100644 --- a/ychat/src/data/con.cpp +++ b/ychat/src/data/con.cpp @@ -30,6 +30,56 @@ using namespace std; #ifndef CON_CPP #define CON_CPP +#ifdef USE_SQLITE + +// Schema for the single "user" table this legacy data layer ever queries +// (see etc/ychat.conf's chat.database.mysql* entries for the column list +// the queries are built from: nick password color1 color2 email +// registerdate status). MySQL deployments are expected to have this table +// created out-of-band; SQLite has no such out-of-band step, so create it +// here if missing. +#define SQLITE_USER_TABLE_DDL \ + "CREATE TABLE IF NOT EXISTS user (" \ + "nick TEXT PRIMARY KEY, password TEXT, color1 TEXT, color2 TEXT, " \ + "email TEXT, registerdate TEXT, status TEXT)" + +con::con() +{ + // chat.database.dbname doubles as the SQLite file path in this mode + // (there is no separate server/user/password/port for an embedded db). + string s_path = wrap::CONF->get_elem("chat.database.dbname"); + + while ( sqlite3_open( s_path.c_str(), &p_sqlite ) != SQLITE_OK ) + { + wrap::system_message( SQLITEE1 + s_path + ": " + string( sqlite3_errmsg(p_sqlite) ) ); + sqlite3_close( p_sqlite ); + usleep( 30000000 ); + } + + // Multiple pooled connections open the same file concurrently: WAL lets + // readers and a writer coexist, and the busy timeout makes a writer wait + // for a lock instead of failing immediately with SQLITE_BUSY (this data + // layer has no query-retry logic of its own). + sqlite3_busy_timeout( p_sqlite, 5000 ); + sqlite3_exec( p_sqlite, "PRAGMA journal_mode=WAL", NULL, NULL, NULL ); + sqlite3_exec( p_sqlite, "PRAGMA foreign_keys=ON", NULL, NULL, NULL ); + + char* c_err = NULL; + if ( sqlite3_exec( p_sqlite, SQLITE_USER_TABLE_DDL, NULL, NULL, &c_err ) != SQLITE_OK ) + { + wrap::system_message( SQLITEE2 + string( c_err ? c_err : "unknown error" ) ); + sqlite3_free( c_err ); + } +} + +con::~con() +{ + if ( p_sqlite ) + sqlite3_close( p_sqlite ); +} + +#else + con::con() { p_mysql = mysql_init(NULL); @@ -65,4 +115,6 @@ con::~con() } #endif + +#endif #endif diff --git a/ychat/src/data/con.h b/ychat/src/data/con.h index ca09626..485cd07 100644 --- a/ychat/src/data/con.h +++ b/ychat/src/data/con.h @@ -28,7 +28,11 @@ #ifndef CON_H #define CON_H +#ifdef USE_SQLITE +#include <sqlite3.h> +#else #include <mysql/mysql.h> +#endif #include <iostream> #include "con_base.h" @@ -37,7 +41,11 @@ using namespace std; class con : public con_base { public: +#ifdef USE_SQLITE + sqlite3* p_sqlite; +#else MYSQL* p_mysql; +#endif con( ); ~con( ); }; diff --git a/ychat/src/data/data.cpp b/ychat/src/data/data.cpp index a401dab..4cd0e3b 100644 --- a/ychat/src/data/data.cpp +++ b/ychat/src/data/data.cpp @@ -30,14 +30,196 @@ using namespace std; -data::data() +#ifdef USE_SQLITE + +ychatdb::ychatdb() +{} + +ychatdb::~ychatdb() +{} + +hashmap<string> +ychatdb::select_user_data( string s_user, string s_query) +{ + vector<string> vec_elements; + return select_query( s_query, s_user, vec_elements ); +} + +hashmap<string> +ychatdb::select_query( string s_query, string s_nick, vector<string>& vec_elements ) +{ + hashmap<string> map_ret; + con* p_con = get_con(); + + vec_elements = map_queries[s_query]; + if ( vec_elements.size() == 0 ) + { + push_con( p_con ); + return map_ret; + } + + vector<string>::iterator iter = vec_elements.begin(); + string s_table = *iter; + iter++; + + string s_sql = "SELECT "; + for ( vector<string>::iterator it = iter; it != vec_elements.end(); ) + { + s_sql.append( *it ); + if ( ++it != vec_elements.end() ) + s_sql.append( ", " ); + } + s_sql.append( " FROM " + s_table + " WHERE nick = ?" ); + + print_query( SQLITEQU + s_sql ); + + sqlite3_stmt* p_stmt = NULL; + if ( sqlite3_prepare_v2( p_con->p_sqlite, s_sql.c_str(), -1, &p_stmt, NULL ) == SQLITE_OK ) + { + sqlite3_bind_text( p_stmt, 1, s_nick.c_str(), -1, SQLITE_TRANSIENT ); + + if ( sqlite3_step( p_stmt ) == SQLITE_ROW ) + { + int i_cols = sqlite3_column_count( p_stmt ); + vector<string>::iterator col_iter = iter; + for ( int i = 0; i < i_cols && col_iter != vec_elements.end(); i++, col_iter++ ) + { + const unsigned char* c_val = sqlite3_column_text( p_stmt, i ); + map_ret[*col_iter] = c_val ? string( (const char*) c_val ) : ""; + } + } + } + else + { + wrap::system_message( SQLITEE2 + string( sqlite3_errmsg( p_con->p_sqlite ) ) ); + } + + sqlite3_finalize( p_stmt ); + push_con( p_con ); + return map_ret; +} + +void +ychatdb::insert_user_data( string s_user, string s_query, map<string,string> insert_map ) +{ + insert_query( s_query, insert_map ); +} + +void +ychatdb::insert_query( string s_query, map<string,string> map_insert ) +{ + vector<string> vec_elements = map_queries[s_query]; + if ( vec_elements.size() == 0 ) + return; + + vector<string>::iterator iter = vec_elements.begin(); + string s_table = *iter; + iter++; + + string s_cols, s_placeholders; + for ( vector<string>::iterator it = iter; it != vec_elements.end(); ) + { + s_cols.append( *it ); + s_placeholders.append( "?" ); + if ( ++it != vec_elements.end() ) + { + s_cols.append( ", " ); + s_placeholders.append( ", " ); + } + } + + string s_sql = "INSERT INTO " + s_table + " (" + s_cols + ") VALUES (" + s_placeholders + ")"; + print_query( SQLITEQU + s_sql ); + + con* p_con = get_con(); + sqlite3_stmt* p_stmt = NULL; + + if ( sqlite3_prepare_v2( p_con->p_sqlite, s_sql.c_str(), -1, &p_stmt, NULL ) == SQLITE_OK ) + { + int i = 1; + for ( iter = vec_elements.begin() + 1; iter != vec_elements.end(); iter++, i++ ) + sqlite3_bind_text( p_stmt, i, map_insert[*iter].c_str(), -1, SQLITE_TRANSIENT ); + + if ( sqlite3_step( p_stmt ) != SQLITE_DONE ) + wrap::system_message( SQLITEE2 + string( sqlite3_errmsg( p_con->p_sqlite ) ) ); + } + else + { + wrap::system_message( SQLITEE2 + string( sqlite3_errmsg( p_con->p_sqlite ) ) ); + } + + sqlite3_finalize( p_stmt ); + push_con( p_con ); +} + +void +ychatdb::update_user_data( string s_user, string s_query, hashmap<string> update_map ) +{ + vector<string> vec_elements = map_queries[s_query]; + if ( vec_elements.size() == 0 ) + return; + + vector<string>::iterator iter = vec_elements.begin(); + string s_table = *iter; + iter++; + + string s_sql = "UPDATE " + s_table + " SET "; + vector<string> vec_bind_cols; + bool b_flag = 0; + + for ( ; iter != vec_elements.end(); iter++ ) + { + if ( update_map[*iter] == "" ) // Dont update data if it has not been changed / if its empty! + continue; + + if ( b_flag ) + s_sql.append( ", " ); + + s_sql.append( *iter + "=?" ); + vec_bind_cols.push_back( *iter ); + b_flag = 1; + } + + if ( !b_flag ) + return; + + s_sql.append( " WHERE nick=?" ); + print_query( SQLITEQU + s_sql ); + + con* p_con = get_con(); + sqlite3_stmt* p_stmt = NULL; + + if ( sqlite3_prepare_v2( p_con->p_sqlite, s_sql.c_str(), -1, &p_stmt, NULL ) == SQLITE_OK ) + { + int i = 1; + for ( vector<string>::iterator col_iter = vec_bind_cols.begin(); col_iter != vec_bind_cols.end(); col_iter++, i++ ) + sqlite3_bind_text( p_stmt, i, update_map[*col_iter].c_str(), -1, SQLITE_TRANSIENT ); + + string s_lower_user = tool::to_lower(s_user); + sqlite3_bind_text( p_stmt, i, s_lower_user.c_str(), -1, SQLITE_TRANSIENT ); + + if ( sqlite3_step( p_stmt ) != SQLITE_DONE ) + wrap::system_message( SQLITEE2 + string( sqlite3_errmsg( p_con->p_sqlite ) ) ); + } + else + { + wrap::system_message( SQLITEE2 + string( sqlite3_errmsg( p_con->p_sqlite ) ) ); + } + + sqlite3_finalize( p_stmt ); + push_con( p_con ); +} + +#else + +ychatdb::ychatdb() {} -data::~data() +ychatdb::~ychatdb() |
