summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md12
-rw-r--r--ychat/DOCKER-SQLITE.md112
-rw-r--r--ychat/Dockerfile.sqlite82
-rw-r--r--ychat/etc/ychat.conf2
-rw-r--r--ychat/src/config.h.in6
-rwxr-xr-xychat/src/configure227
-rw-r--r--ychat/src/configure.ac10
-rw-r--r--ychat/src/data/con.cpp52
-rw-r--r--ychat/src/data/con.h8
-rw-r--r--ychat/src/data/data.cpp202
-rw-r--r--ychat/src/data/data.h22
-rw-r--r--ychat/src/data/data_base.cpp10
-rw-r--r--ychat/src/glob.h8
-rw-r--r--ychat/src/mods/html/yc_register.cpp2
-rw-r--r--ychat/src/msgs.h3
-rw-r--r--ychat/src/wrap.cpp4
-rw-r--r--ychat/src/wrap.h4
17 files changed, 742 insertions, 24 deletions
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<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()
{}
hashmap<string>
-data::select_user_data( string s_user, string s_query)
+ychatdb::select_user_data( string s_user, string s_query)
{
string s_where_rule = " WHERE nick = \"" + s_user + "\"";
vector<string> vec_elements;
@@ -46,7 +228,7 @@ data::select_user_data( string s_user, string s_query)
}
MYSQL_RES*
-data::select_query( string s_query, string s_where_rule, vector<string>& vec_elements )
+ychatdb::select_query( string s_query, string s_where_rule, vector<string>& vec_elements )
{
con* p_con = get_con();
@@ -85,7 +267,7 @@ data::select_query( string s_query, string s_where_rule, vector<string>& vec_ele
}
hashmap<string>
-data::parse_result( MYSQL_RES* p_result, vector<string>& vec_elements )
+ychatdb::parse_result( MYSQL_RES* p_result, vector<string>& vec_elements )
{
hashmap<string> map_ret;
if ( p_result != NULL )
@@ -104,13 +286,13 @@ data::parse_result( MYSQL_RES* p_result, vector<string>& vec_elements )
}
void
-data::insert_user_data( string s_user, string s_query, map<string,string> insert_map )
+ychatdb::insert_user_data( string s_user, string s_query, map<string,string> insert_map )
{
insert_query( s_query, insert_map );
}
void
-data::insert_query( string s_query, map<string,string> map_insert )
+ychatdb::insert_query( string s_query, map<string,string> map_insert )
{
vector<string> vec_elements = map_queries[s_query];
vector<string>::iterator iter = vec_elements.begin();
@@ -154,7 +336,7 @@ data::insert_query( string s_query, map<string,string> map_insert )
}
void
-data::update_user_data( string s_user, string s_query, hashmap<string> update_map )
+ychatdb::update_user_data( string s_user, string s_query, hashmap<string> update_map )
{
vector<string> vec_elements = map_queries[s_query];
@@ -201,7 +383,7 @@ data::update_user_data( string s_user, string s_query, hashmap<string> update_ma
}
string
-data::secure_query( string s_mysql_query )
+ychatdb::secure_query( string s_mysql_query )
{
// Prevent