]> PatchyGit - patchyircd.git/commitdiff
add sqlite database support
authorEgor <egor@patchysicp.xyz>
Fri, 16 Jan 2026 15:04:27 +0000 (17:04 +0200)
committerEgor <egor@patchysicp.xyz>
Fri, 16 Jan 2026 15:04:27 +0000 (17:04 +0200)
15 files changed:
Makefile.am
README.md
REUSE.toml
configure.ac
data/.gitignore
lua/irccloud.lua
src/admin.cpp
src/db.cpp [new file with mode: 0644]
src/db.hpp [new file with mode: 0644]
src/dbtool.cpp [new file with mode: 0644]
src/irc.cpp
src/main.cpp
src/passwd.cpp
src/passwd.hpp
src/setpass.cpp [deleted file]

index 3d7481d..346f50c 100644 (file)
@@ -1,11 +1,12 @@
-# Copyright (c) 2025 Egor
+# Copyright (c) 2025-2026 Egor
 # SPDX-License-Identifier: Unlicense
 ACLOCAL_AMFLAGS=--install
-bin_PROGRAMS = ircd setpass
+bin_PROGRAMS = ircd dbtool
 ircd_SOURCES = \
        src/admin.cpp          src/admin.hpp          \
                               src/common.hpp         \
        src/crypt_blowfish.cpp src/crypt_blowfish.hpp \
+       src/db.cpp             src/db.hpp             \
        src/irc.cpp            src/irc.hpp            \
                               src/ircdata.hpp        \
        src/ircfmt.cpp         src/ircfmt.hpp         \
@@ -16,12 +17,14 @@ ircd_SOURCES = \
        src/passwd.cpp         src/passwd.hpp         \
        src/util.cpp           src/util.hpp           \
        src/main.cpp
-setpass_SOURCES = \
+dbtool_SOURCES = \
        src/crypt_blowfish.cpp src/crypt_blowfish.hpp \
        src/passwd.cpp         src/passwd.hpp         \
-       src/setpass.cpp
+       src/util.cpp           src/util.hpp           \
+       src/db.cpp             src/db.hpp             \
+       src/dbtool.cpp
 AM_CXXFLAGS = $(WARNINGCXXFLAGS) $(LIBUV_CFLAGS) $(LIBTLS_CFLAGS) $(LUA_CFLAGS)
-LDADD = $(LIBUV_LIBS) $(LIBTLS_LIBS) $(LUA_LIBS)
+LDADD = $(LIBUV_LIBS) $(LIBTLS_LIBS) $(LUA_LIBS) $(SQLITE3_LIBS)
 
 dist_ircddata_DATA = \
        data/help.example.txt
index ed9ee80..7eb227e 100644 (file)
--- a/README.md
+++ b/README.md
@@ -15,29 +15,51 @@ make install
 
 Some things are configurable via compile-time flags. See the section on paths.
 
-## Config files
-These have to go in datadir (see below).
-
-For TLS you gonna need to place your certificate and private key in
-`cert.pem` and `key.pem` respectively.
-
-`ops.txt`, `users.txt`, `newusers.txt` and `webirc.txt` contain hashed passwords
-(`account:hash`). `ops.txt` is used for OPER command, `users.txt` is used for
-PASS command (which we use for our bespoke user account system). NEWACC command
-adds lines to `newusers.txt`.  To verify a registration, simply copy lines from
-`newusers.txt` to `users.txt`.  You can set password from command line like so
-`./setpass username file.txt` (the file must already exist).
-
-`webirc.txt` is used for authenticating WebSocket proxies and similar with the
+## Database
+`${datadir}/ircd.db` is a sqlite database containing various data. It can be
+manipulated using the included `dbtool`. The `ops`, `users`, `newusers`, and
+`webirc` tables contain hashed password. `ops` is used for OPER command, `users`
+is used for PASS command (which we use for our bespoke user account system).
+NEWACC command adds rows to `newusers`. To verify a registration, first run the
+`dbtool listnewusers` command and note the rowid for the registration, then run
+`dbtool approve [rowid] [ops|users|webirc]`, the last argument being the table
+you want to add the account to. You can set password from command line like so
+`dbtool setpass [username] [ops|users|webirc]`.
+
+`webirc` table is used for authenticating WebSocket proxies and similar with the
 WEBIRC command.
 
 SASL authentication is also supported, including PLAIN (using same credentials
-as PASS), and EXTERNAL which uses `certfp.txt` (`account:fingerprint`). There's
+as PASS), and EXTERNAL which uses `certfp` table. There's
 also irccloud lua module which authenticates using irccloud user ids in
-`irccloud.txt` (`account:userid`).
+`irccloud` table.
 
 By default accounts have ownership of the nick equal to the account name.
-Additional nicks can be added via `nicks.txt` (`account:nick`).
+Additional nicks can be added via `nicks` table.
+
+Please see `dbtool` usage for more information on how to change these
+tables. `dbtool listusers` will show information from `users`, `certfp`,
+`irccloud` and `nicks` tables.
+
+Full database schema:
+```sql
+CREATE TABLE ops(account PRIMARY KEY,hash NOT NULL) WITHOUT ROWID;
+CREATE TABLE users(account PRIMARY KEY,hash NOT NULL) WITHOUT ROWID;
+CREATE TABLE newusers(account NOT NULL,hash NOT NULL,regnuh NOT NULL,regpeer NOT NULL,regtime NOT NULL);
+CREATE TABLE webirc(account PRIMARY KEY,hash NOT NULL) WITHOUT ROWID;
+CREATE TABLE certfp(account NOT NULL,fingerprint PRIMARY KEY) WITHOUT ROWID;
+CREATE TABLE nicks(account NOT NULL,nick PRIMARY KEY) WITHOUT ROWID;
+CREATE TABLE irccloud(account NOT NULL,irccloudid PRIMARY KEY) WITHOUT ROWID;
+CREATE INDEX certfp_account ON certfp(account);
+CREATE INDEX nicks_account ON nicks(account);
+CREATE INDEX irccloud_account ON irccloud(account);
+```
+
+## Config files
+These have to go in datadir (see below).
+
+For TLS you gonna need to place your certificate and private key in
+`cert.pem` and `key.pem` respectively.
 
 `help.txt` file contains the text for the HELP command. You can use the provided
 `help.example.txt` file. Similarly, `motd.txt` contains the MOTD text.
@@ -93,7 +115,7 @@ arguments.
 
 ## Copyright
 Copyright (c) 2022, 2024 Sam Smith
-Copyright (c) 2022-2025 Egor
+Copyright (c) 2022-2026 Egor
 SPDX-License-Identifier: GPL-2.0-or-later
 
 This program is free software; you can redistribute it and/or modify
index d31b545..f2102d2 100644 (file)
@@ -1,4 +1,4 @@
-version = 1 
+version = 1
 
 [[annotations]]
 path = [
index e87fd75..777f905 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2025 Egor
+# Copyright (c) 2025-2026 Egor
 # SPDX-License-Identifier: Unlicense
 AC_PREREQ([2.71])
 AC_INIT([PatchyIRCD], [0.0.1], [bugs@patchysicp.xyz])
@@ -48,7 +48,7 @@ AC_ARG_ENABLE([pch],
        [AS_HELP_STRING([--enable-pch], [enable precompiled headers])])
 AM_CONDITIONAL([ENABLE_PCH], [test "x$enable_pch" = xyes])
 AS_IF([test "x$enable_pch" = xyes],
-         [AS_CASE(["$CXX"], [*clang*], 
+         [AS_CASE(["$CXX"], [*clang*],
                           [AC_SUBST([PCHEXT], [pch])
                           AC_SUBST([PCHOPT], ['" -include-pch %s.pch\n"'])],
                           [AC_SUBST([PCHEXT], [gch])
@@ -57,5 +57,6 @@ AS_IF([test "x$enable_pch" = xyes],
 PKG_CHECK_MODULES(LIBUV, [libuv])
 PKG_HAVE_DEFINE_WITH_MODULES(LIBTLS, [libtls])
 PKG_HAVE_DEFINE_WITH_MODULES(LUA, [lua5.4])
+PKG_CHECK_MODULES(SQLITE3, [sqlite3])
 
 AC_OUTPUT
index 3162d3c..e54037a 100644 (file)
@@ -1,16 +1,19 @@
-# Copyright (c) 2022-2025 Egor
+# Copyright (c) 2022-2026 Egor
 # SPDX-License-Identifier: Unlicense
-/ops.txt
 /init.txt
 /chanlog.txt
 /cert.pem
 /key.pem
-/users.txt
-/newusers.txt
 /help.txt
 /motd.txt
+/admin.txt
+/ircd.db
+
+# old pre-db stuff
+/ops.txt
+/users.txt
+/newusers.txt
 /certfp.txt
 /nicks.txt
 /irccloud.txt
 /webirc.txt
-/admin.txt
index 1a83568..9026397 100644 (file)
@@ -1,4 +1,4 @@
--- Copyright (c) 2024 Egor
+-- Copyright (c) 2024, 2026 Egor
 -- SPDX-License-Identifier: GPL-2.0-or-later
 -- irccloud authentication support
 local function parseipv6(s)
@@ -9,7 +9,7 @@ local function parseipv6(s)
        local part2 = {}
        local part = part1
        local i = 1
-       repeat 
+       repeat
                local j = s:find(":", i, true) or 0
                local sub = s:sub(i, j-1)
                if sub == "" then
@@ -43,6 +43,6 @@ end
 hook_add("newuser", "irccloud_auth", function(hook, cl1, cl2, chan, txt)
        local netid, ifaceid = parseipv6(cl1.peername)
        if netid and isirccloudnetwork(netid) then
-               client_chghost(cl1, db_read_rhs(tostring(ifaceid), "irccloud.txt"))
+               client_chghost(cl1, db_read_rhs(tostring(ifaceid), "irccloud"))
        end
 end)
index 7c24cab..53c5cf8 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (c) 2023-2025 Egor
+// Copyright (c) 2023-2026 Egor
 // SPDX-License-Identifier: GPL-2.0-or-later
 #include "common.hpp"
 #ifdef HAVE_LUA
@@ -322,7 +322,7 @@ static int fn_rpl_whoisspecial(lua_State *L) {
 static int fn_db_read_rhs(lua_State *L) {
        const char *key = luaL_checkstring(L, 1);
        const char *fn = luaL_checkstring(L, 2);
-       const char *res = db_read_rhs(key, getdatafile(fn));
+       const char *res = db_read_rhs(key, fn);
        lua_pushstring(L, res);
        return 1;
 }
diff --git a/src/db.cpp b/src/db.cpp
new file mode 100644 (file)
index 0000000..cb060cd
--- /dev/null
@@ -0,0 +1,125 @@
+// Copyright (c) 2026 Egor
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "common.hpp"
+#include "db.hpp"
+#include "util.hpp"
+#include <sqlite3.h>
+namespace db {
+Database::Database(const char *fn) :db(0) {
+       sqlite3_open(fn, &db);
+}
+Database::~Database() {
+       if (db) {
+               sqlite3_close_v2(db);
+               db = 0;
+       }
+}
+Statement Database::operator()(std::string_view sv) {
+       Statement stmt;
+       sqlite3_prepare_v2(db, sv.data(), (int)sv.size(), &stmt.stmt, 0);
+       // EXPLAIN QUERY PLAN
+#if 0
+       sqlite3_stmt_explain(stmt.stmt, 2);
+       while (stmt.step()) {
+               auto id = stmt.num(0);
+               auto parent = stmt.num(1);
+               auto detail = stmt.text(3);
+               fprintf(stderr, "%d <- %d %*s\n", (int)parent, (int)id, (int)detail.size(), detail.data());
+       }
+       stmt.reset();
+       sqlite3_stmt_explain(stmt.stmt, 0);
+#endif
+       return stmt;
+}
+Statement::~Statement() {
+       if (stmt) {
+               sqlite3_finalize(stmt);
+               stmt = 0;
+       }
+}
+void Statement::reset() {
+       sqlite3_reset(stmt);
+}
+bool Statement::step() {
+       return sqlite3_step(stmt) == SQLITE_ROW;
+}
+void Statement::bind(int i, std::string_view text) {
+       sqlite3_bind_text64(stmt, i, text.data(), text.size(), SQLITE_TRANSIENT, SQLITE_UTF8);
+}
+void Statement::bind(int i, int64_t num) {
+       sqlite3_bind_int64(stmt, i, num);
+}
+std::string_view Statement::text(int i) {
+       auto p = sqlite3_column_text(stmt, i);
+       int len = sqlite3_column_bytes(stmt, i);
+       return std::string_view(reinterpret_cast<const char *>(p), len);
+}
+int64_t Statement::num(int i) {
+       return sqlite3_column_int64(stmt, i);
+}
+Database disk{}, memory{};
+static void errorLogCallback(void *pArg, int iErrCode, const char *zMsg){
+       (void)pArg;
+       fprintf(stderr, "(%d) %s\n", iErrCode, zMsg);
+}
+InitLock init() {
+       sqlite3_config(SQLITE_CONFIG_LOG, errorLogCallback, nullptr);
+       disk = Database(getdatafile("ircd.db"));
+       memory = Database(":memory:");
+
+#define CREATE_TABLE(loc, tbl) \
+       db::loc("CREATE TABLE IF NOT EXISTS " tbl ";").step()
+#define CREATE_INDEX(loc, tbl) \
+       db::loc("CREATE INDEX IF NOT EXISTS " tbl ";").step()
+       CREATE_TABLE(disk, "ops(account PRIMARY KEY,hash NOT NULL) WITHOUT ROWID");
+       CREATE_TABLE(disk, "users(account PRIMARY KEY,hash NOT NULL) WITHOUT ROWID");
+       CREATE_TABLE(disk, "newusers(account NOT NULL,hash NOT NULL,regnuh NOT NULL,regpeer NOT NULL,regtime NOT NULL)");
+       CREATE_TABLE(disk, "webirc(account PRIMARY KEY,hash NOT NULL) WITHOUT ROWID");
+       CREATE_TABLE(disk, "certfp(account NOT NULL,fingerprint PRIMARY KEY) WITHOUT ROWID");
+       CREATE_TABLE(disk, "nicks(account NOT NULL,nick PRIMARY KEY) WITHOUT ROWID");
+       CREATE_TABLE(disk, "irccloud(account NOT NULL,irccloudid PRIMARY KEY) WITHOUT ROWID");
+       CREATE_INDEX(disk, "certfp_account ON certfp(account)");
+       CREATE_INDEX(disk, "nicks_account ON nicks(account)");
+       CREATE_INDEX(disk, "irccloud_account ON irccloud(account)");
+
+       return InitLock{};
+}
+InitLock::~InitLock() {
+       disk = Database();
+       memory = Database();
+}
+}
+
+// deprecated old APIs
+static char *db_read_internal(const char *key, db::Statement stmt) {
+       static char buf[1024];
+       stmt.bind(1, key);
+       if (!stmt.step())
+               return nullptr;
+       auto sv = stmt.text(0);
+       if (sv.size()+1 > 1024)
+               return nullptr;
+       memcpy(buf, sv.data(), sv.size());
+       buf[sv.size()] = '\0';
+       return buf;
+}
+char *password_get(const char *key, const char *fn) {
+       if (fn == "ops"sv)
+               return db_read_internal(key, db::disk("SELECT hash FROM ops WHERE account = ?;"));
+       else if (fn == "users"sv)
+               return db_read_internal(key, db::disk("SELECT hash FROM users WHERE account = ?;"));
+       else if (fn == "webirc"sv)
+               return db_read_internal(key, db::disk("SELECT hash FROM webirc WHERE account = ?;"));
+       else
+               return nullptr;
+}
+const char *db_read_rhs(const char *key, const char *fn) {
+       if (fn == "certfp"sv)
+               return db_read_internal(key, db::disk("SELECT account FROM certfp WHERE fingerprint = ?;"));
+       else if (fn == "nicks"sv)
+               return db_read_internal(key, db::disk("SELECT account FROM nicks WHERE nick = ?;"));
+       else if (fn == "irccloud"sv)
+               return db_read_internal(key, db::disk("SELECT account FROM irccloud WHERE irccloudid = ?;"));
+       else
+               return nullptr;
+}
diff --git a/src/db.hpp b/src/db.hpp
new file mode 100644 (file)
index 0000000..0bf2d58
--- /dev/null
@@ -0,0 +1,66 @@
+// Copyright (c) 2026 Egor
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef DB_H_
+#define DB_H_
+typedef struct sqlite3 sqlite3;
+typedef struct sqlite3_stmt sqlite3_stmt;
+#include <string_view>
+#include <utility>
+namespace db {
+       struct Statement;
+       struct Database {
+               sqlite3 *db;
+               Database() :db(0) {};
+               explicit Database(const char *fn);
+               ~Database();
+               Database(const Database&) = delete;
+               Database& operator=(const Database&) = delete;
+               Database(Database&& other) { db = std::exchange(other.db, nullptr); }
+               Database& operator=(Database&& other) {
+                       swap(*this, other);
+                       return *this;
+               }
+               friend void swap(Database &lhs, Database &rhs) {
+                       using std::swap;
+                       swap(lhs.db, rhs.db);
+               }
+               Statement operator()(std::string_view sv);
+       };
+       struct Statement {
+               sqlite3_stmt *stmt;
+               Statement() :stmt(0) {};
+               ~Statement();
+               Statement(const Statement&) = delete;
+               Statement& operator=(const Statement&) = delete;
+               Statement(Statement&& other) { stmt = std::exchange(other.stmt, nullptr); }
+               Statement& operator=(Statement&& other) {
+                       swap(*this, other);
+                       return *this;
+               }
+               friend void swap(Statement &lhs, Statement &rhs) {
+                       using std::swap;
+                       swap(lhs.stmt, rhs.stmt);
+               }
+               void reset();
+               bool step();
+               void bind(int i, std::string_view text);
+               void bind(int i, int64_t num);
+               std::string_view text(int i);
+               int64_t num(int i);
+       };
+       extern Database disk, memory;
+       class InitLock {
+               friend InitLock init();
+               InitLock() = default;
+       public:
+               ~InitLock();
+               InitLock(const InitLock&) = delete;
+               InitLock& operator=(const InitLock&) = delete;
+       };
+       [[nodiscard]] InitLock init();
+}
+
+// deprecated old APIs from passwd.hpp
+char *password_get(const char *key, const char *fn);
+const char *db_read_rhs(const char *key, const char *fn);
+#endif
diff --git a/src/dbtool.cpp b/src/dbtool.cpp
new file mode 100644 (file)
index 0000000..f4e44b5
--- /dev/null
@@ -0,0 +1,262 @@
+// Copyright (c) 2022-2023, 2025-2026 Egor
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "common.hpp"
+#include "db.hpp"
+#include "passwd.hpp"
+#include "util.hpp"
+#include <stdio.h>
+#include <functional>
+#include <iostream>
+static const char *argv0 = nullptr;
+static void usage() {
+       printf("Usage: %s import\n", argv0);
+       printf("       %s setpass [username] [ops|users|webirc]\n", argv0);
+       printf("       %s listnewusers\n", argv0);
+       printf("       %s approve [rowid] [ops|users|webirc]\n", argv0);
+       printf("       %s listusers\n", argv0);
+       printf("       %s listops\n", argv0);
+       printf("       %s listwebirc\n", argv0);
+       printf("       %s deleterow [rowid] [ops|users|newusers|webirc|certfp|nicks|irccloud]\n", argv0);
+       printf("       %s addcertfp [account] [certfp]\n", argv0);
+       printf("       %s addnick [account] [nick]\n", argv0);
+       printf("       %s addirccloud [account] [irccloudid]\n", argv0);
+}
+static void parse_old_db(const char *filename, std::function<void(std::string_view, std::string_view)> cb) {
+       FILE *fi = fopen(filename, "rbe");
+       if (!fi)
+               return;
+       char buf[1024];
+       while (fgets(buf, 1024, fi)) {
+               size_t len = strlen(buf);
+               if (len && buf[len-1] == '\n')
+                       buf[len-1] = '\0';
+               char *rhs = strchr(buf, ':');
+               if (rhs) {
+                       std::string_view key(buf, rhs);
+                       std::string_view value(rhs+1);
+                       cb(key, value);
+               }
+       }
+       fclose(fi);
+}
+static void import_table(db::Statement stmt, const char *filename) {
+       parse_old_db(filename, [&](auto k, auto v) {
+               stmt.reset();
+               stmt.bind(1, k);
+               stmt.bind(2, v);
+               stmt.step();
+       });
+}
+static int import_db() {
+#define IMPORT(x) \
+       import_table(db::disk("INSERT INTO " x " VALUES(?, ?);"), \
+                       getdatafile(x ".txt"))
+       IMPORT("ops");
+       IMPORT("users");
+       import_table(db::disk("INSERT INTO newusers VALUES(?, ?, '(legacy)', '(legacy)', unixepoch());"), getdatafile("newusers.txt"));
+       IMPORT("webirc");
+       IMPORT("certfp");
+       IMPORT("nicks");
+       IMPORT("irccloud");
+       return 0;
+}
+static char *read_password(const char *prompt)
+{
+       static char buffer[256];
+       struct termios oldattr, newattr;
+
+       if (tcgetattr(0, &oldattr) == -1)
+               return 0;
+       newattr = oldattr;
+       newattr.c_lflag &= ~(ECHO|ISIG);
+       tcsetattr(0, TCSAFLUSH, &newattr);
+
+       write(0, prompt, strlen(prompt));
+       ssize_t n = read(0, buffer, 256);
+       if (n < 0)
+               n = 0;
+       else if (n > 0 && buffer[n-1] == '\n')
+               n--;
+       buffer[n] = '\0';
+
+       tcsetattr(0, TCSAFLUSH, &oldattr);
+       write(0, "\n", 1);
+
+       return n ? buffer : 0;
+}
+static int setpass(const char *account, const char *table) {
+       char *pass = read_password("password: ");
+       if (!pass)
+               return 1;
+       char *hash = password_hash(pass);
+       db::Statement stmt;
+       if (table == "ops"sv) {
+               stmt = db::disk("INSERT OR REPLACE INTO ops(account, hash) VALUES(?, ?);");
+       } else if (table == "users"sv) {
+               stmt = db::disk("INSERT OR REPLACE INTO users(account, hash) VALUES(?, ?);");
+       } else if (table == "webirc"sv) {
+               stmt = db::disk("INSERT OR REPLACE INTO webirc(account, hash) VALUES(?, ?);");
+       } else {
+               usage();
+               return 1;
+       }
+       stmt.bind(1, account);
+       stmt.bind(2, hash);
+       stmt.step();
+       return 0;
+}
+static void showuser(std::string_view account) {
+       std::cout << account << '\n';
+       // hash
+       auto stmt = db::disk("SELECT 1 FROM users WHERE account = ?;");
+       stmt.bind(1, account);
+       if (stmt.step())
+               std::cout << "\thas password\n";
+       // certfp
+       stmt = db::disk("SELECT fingerprint FROM certfp WHERE account = ?;");
+       stmt.bind(1, account);
+       while (stmt.step())
+               std::cout << "\tcertfp: " << stmt.text(0) << '\n';
+       // nicks
+       stmt = db::disk("SELECT nick FROM nicks WHERE account = ?;");
+       stmt.bind(1, account);
+       while (stmt.step())
+               std::cout << "\tnick: " << stmt.text(0) << '\n';
+       // irccloud
+       stmt = db::disk("SELECT irccloudid FROM irccloud WHERE account = ?;");
+       stmt.bind(1, account);
+       while (stmt.step())
+               std::cout << "\tirccloud: " << stmt.text(0) << '\n';
+}
+int main(int argc, const char *argv[]) {
+       argv0 = argv[0];
+       if (argc < 2) {
+               usage();
+               return 1;
+       }
+       auto dblk = db::init();
+       if (argv[1] == "import"sv) {
+               return import_db();
+       } else if (argv[1] == "setpass"sv) {
+               if (argc < 4) {
+                       usage();
+                       return 1;
+               }
+               return setpass(argv[2], argv[3]);
+       } else if (argv[1] == "listnewusers"sv) {
+               auto stmt = db::disk("SELECT rowid, account, regnuh, regpeer, datetime(regtime, 'unixepoch') FROM newusers;");
+               while (stmt.step()) {
+                       auto rowid = stmt.num(0);
+                       auto account = stmt.text(1);
+                       auto regnuh = stmt.text(2);
+                       auto regpeer = stmt.text(3);
+                       auto regtime = stmt.text(4);
+                       std::cout << rowid << ' ' << account << ' ' << regnuh << ' ' << regpeer << ' ' << regtime << '\n';
+               }
+       } else if (argv[1] == "approve"sv) {
+               if (argc < 4) {
+                       usage();
+                       return 1;
+               }
+               std::string_view rowid = argv[2];
+               std::string_view table = argv[3];
+
+               db::Statement stmt;
+               if (table == "ops"sv) {
+                       stmt = db::disk("INSERT OR REPLACE INTO ops(account, hash) VALUES(?, ?);");
+               } else if (table == "users"sv) {
+                       stmt = db::disk("INSERT OR REPLACE INTO users(account, hash) VALUES(?, ?);");
+               } else if (table == "webirc"sv) {
+                       stmt = db::disk("INSERT OR REPLACE INTO webirc(account, hash) VALUES(?, ?);");
+               } else {
+                       usage();
+                       return 1;
+               }
+
+               auto stmt2 = db::disk("SELECT account, hash FROM newusers WHERE rowid = ?;");
+               stmt2.bind(1, rowid);
+               if (stmt2.step()) {
+                       stmt.bind(1, stmt2.text(0));
+                       stmt.bind(2, stmt2.text(1));
+                       stmt.step();
+               } else {
+                       fprintf(stderr, "no such row\n");
+               }
+       } else if (argv[1] == "listusers"sv) {
+               auto stmt = db::disk(R"(
+                               SELECT account
+                               FROM users
+                               FULL JOIN certfp USING(account)
+                               FULL JOIN irccloud USING(account)
+                               FULL JOIN nicks USING(account)
+                               GROUP BY account;)");
+               while (stmt.step())
+                       showuser(stmt.text(0));
+       } else if (argv[1] == "listops"sv) {
+               auto stmt = db::disk("SELECT account FROM ops;");
+               while (stmt.step())
+                       std::cout << stmt.text(0) << '\n';
+       } else if (argv[1] == "listwebirc"sv) {
+               auto stmt = db::disk("SELECT account FROM webirc;");
+               while (stmt.step())
+                       std::cout << stmt.text(0) << '\n';
+       } else if (argv[1] == "deleterow"sv) {
+               if (argc < 3) {
+                       usage();
+                       return 1;
+               }
+               std::string_view rowid = argv[2];
+               std::string_view table = argv[3];
+               db::Statement stmt;
+               if (table == "ops")
+                       stmt = db::disk("DELETE FROM ops WHERE account = ?;");
+               else if (table == "users")
+                       stmt = db::disk("DELETE FROM users WHERE account = ?;");
+               else if (table == "newusers")
+                       stmt = db::disk("DELETE FROM newusers WHERE rowid = ?;");
+               else if (table == "webirc")
+                       stmt = db::disk("DELETE FROM webirc WHERE account = ?;");
+               else if (table == "certfp")
+                       stmt = db::disk("DELETE FROM certfp WHERE fingerprint = ?;");
+               else if (table == "nicks")
+                       stmt = db::disk("DELETE FROM nicks WHERE nick = ?;");
+               else if (table == "irccloud")
+                       stmt = db::disk("DELETE FROM irccloud WHERE irccloudid = ?;");
+               else {
+                       usage();
+                       return 1;
+               }
+               stmt.bind(1, rowid);
+               stmt.step();
+       } else if (argv[1] == "addcertfp"sv) {
+               if (argc < 3) {
+                       usage();
+                       return 1;
+               }
+               auto stmt = db::disk("INSERT INTO certfp VALUES(?, ?);");
+               stmt.bind(1, argv[2]);
+               stmt.bind(2, argv[3]);
+               stmt.step();
+       } else if (argv[1] == "addnick"sv) {
+               if (argc < 3) {
+                       usage();
+                       return 1;
+               }
+               auto stmt = db::disk("INSERT INTO nicks VALUES(?, ?);");
+               stmt.bind(1, argv[2]);
+               stmt.bind(2, argv[3]);
+               stmt.step();
+       } else if (argv[1] == "addirccloud"sv) {
+               if (argc < 3) {
+                       usage();
+                       return 1;
+               }
+               auto stmt = db::disk("INSERT INTO irccloud VALUES(?, ?);");
+               stmt.bind(1, argv[2]);
+               stmt.bind(2, argv[3]);
+               stmt.step();
+       } else {
+               usage();
+       }
+       return 0;
+}
index b726126..082b0c1 100644 (file)
@@ -1,5 +1,5 @@
 // Copyright (c) 2022, 2024 Sam Smith
-// Copyright (c) 2022-2025 Egor
+// Copyright (c) 2022-2026 Egor
 // SPDX-License-Identifier: GPL-2.0-or-later
 #include "common.hpp"
 #include "irc.hpp"
@@ -446,7 +446,7 @@ int irc_process_line(Client *client, char *source, std::string_view command, int
                                        char *password = strchr(p, '\0')+1;
                                        if (password > client->saslbuf + client->saslbufLen)
                                                goto saslerr;
-                                       job_password_check(client, username, password, getdatafile("users.txt"), cont_sasl);
+                                       job_password_check(client, username, password, "users", cont_sasl);
                                        // we'll clear the whole buffer later, this is just in case username alloc fails
                                        memset(password, 0, strlen(password));
                                        client->saslusername = mystrdup(username);
@@ -456,7 +456,7 @@ int irc_process_line(Client *client, char *source, std::string_view command, int
                                } else {
 #ifdef HAVE_LIBTLS
                                        if (client->cctx && tls_peer_cert_provided(client->cctx)) {
-                                               username = db_read_rhs(tls_peer_cert_hash(client->cctx), getdatafile("certfp.txt"));
+                                               username = db_read_rhs(tls_peer_cert_hash(client->cctx), "certfp");
                                                res = -!username;
                                        }
 #endif
@@ -540,7 +540,7 @@ saslcleanup:
                                        memcpy(client->webirchostname+len, ".xmpp", sizeof(".xmpp"));
                                }
                        }
-                       job_password_check(client, args[1], args[0], getdatafile("webirc.txt"), cont_webirc);
+                       job_password_check(client, args[1], args[0], "webirc", cont_webirc);
                        memset(args[0], 0, strlen(args[0]));
                }
        } MATCH_COMMAND("OPER") {
@@ -549,7 +549,7 @@ saslcleanup:
                if (argc < 2)
                        return err_needmoreparams(client, command), sendbuf(client), 0;
                stringlower(args[0]);
-               job_password_check(client, args[0], args[1], getdatafile("ops.txt"), cont_oper);
+               job_password_check(client, args[0], args[1], "ops", cont_oper);
                memset(args[1], 0, strlen(args[1]));
        } MATCH_COMMAND("NEWACC") {
                if (!is_registered(client))
@@ -561,11 +561,14 @@ saslcleanup:
                if (!hash)
                        return 0; // TODO: error message
                memset(args[1], 0, strlen(args[1]));
-               FILE *f = fopen(getdatafile("newusers.txt"), "abe");
-               if (!f)
-                       return 0; // TODO: error message
-               fprintf(f, "%s:%s\n", args[0], hash);
-               fclose(f);
+               {
+                       auto stmt = db::disk("INSERT INTO newusers(account, hash, regnuh, regpeer, regtime) VALUES(?, ?, ?, ?, unixepoch());");
+                       stmt.bind(1, args[0]);
+                       stmt.bind(2, hash);
+                       stmt.bind(3, client->nuh);
+                       stmt.bind(4, client->peername);
+                       stmt.step();
+               }
                msg_notice_server(hostname, client->nick,
                                "Your information has been recorded. Contact IRCops for assistance with registration.");
                sendbuf(client);
@@ -577,7 +580,7 @@ saslcleanup:
                        stringlower(args[0]);
                        desired_nick = args[0];
                        if (strcmp(desired_nick, client->username) != 0) {
-                               const char *username = db_read_rhs(desired_nick, getdatafile("nicks.txt"));
+                               const char *username = db_read_rhs(desired_nick, "nicks");
                                if (!username || strcmp(username, client->username) != 0)
                                        return err_nicknameinuse(client, desired_nick), sendbuf(client), 0;
                        }
@@ -1256,7 +1259,7 @@ void newUser(Client *client) {
                }
        }
        if (client->username[0] == '~' && client->pass_reg) {
-               job_password_check(client, client->username+1, client->pass_reg, getdatafile("users.txt"), cont_pass);
+               job_password_check(client, client->username+1, client->pass_reg, "users", cont_pass);
                client->passwait = 1;
                memset(client->pass_reg, 0, strlen(client->pass_reg));
                free(client->pass_reg);
@@ -1481,7 +1484,7 @@ void client_chghost(Client *client, const char *newusername, const char *newhost
                                sendbuf(client);
                }
        }
-       
+
        // notify the user of their new account name, and update the +r mode
        if (willbeloggedin) {
                if (newusername)
index acab933..b9171bd 100644 (file)
@@ -1,5 +1,5 @@
 // Copyright (c) 2022 Sam Smith
-// Copyright (c) 2022-2025 Egor
+// Copyright (c) 2022-2026 Egor
 // SPDX-License-Identifier: GPL-2.0-or-later
 #include "common.hpp"
 #include "net.hpp"
@@ -7,6 +7,7 @@
 #include "irc.hpp"
 #include "job.hpp"
 #include "admin.hpp"
+#include "db.hpp"
 
 static int is_running = 1;
 
@@ -211,6 +212,7 @@ int main(int argc, char *argv[])
 
        fprintf(stderr, "Starting up...\n");
 
+       auto dblk = db::init();
        netpoll::init();
        job_init();
 #ifdef HAVE_LUA
index b67a9c1..1ab9034 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2025 Egor
+// Copyright (c) 2022-2026 Egor
 // SPDX-License-Identifier: GPL-2.0-or-later
 #include "common.hpp"
 #include "passwd.hpp"
@@ -44,75 +44,7 @@ char *password_hash(const char *pw)
 {
        return password_hash2(pw, 2, 12);
 }
-int password_set(const char *key, const char *pw, const char *fn, const char *tmpfn)
-{
-       FILE *fi = fopen(fn, "rbe");
-       if (!fi)
-               return -1;
-       FILE *fo = fopen(tmpfn, "wbe");
-       if (!fo) {
-               fclose(fi);
-               return -1;
-       }
-
-       size_t keylen = strlen(key);
-       char buf[1024];
-       while (fgets(buf, 1024, fi)) {
-               if (buf[keylen] != ':' || memcmp(buf, key, keylen) != 0) {
-                       if (fputs(buf, fo) == -1) {
-                               fclose(fi);
-                               fclose(fo);
-                               remove(tmpfn);
-                               return -1;
-                       }
-               }
-       }
-       fclose(fi);
-
-       char *hash = password_hash(pw);
-       if (!hash) {
-               fclose(fo);
-               remove(tmpfn);
-               return -1;
-       }
-
-       if (fprintf(fo, "%s:%s\n", key, hash) == -1) {
-               memset(hash, 0, strlen(hash));
-               fclose(fo);
-               remove(tmpfn);
-               return -1;
-       }
-       memset(hash, 0, strlen(hash));
-       if (fclose(fo) == EOF) {
-               remove(tmpfn);
-               return -1;
-       }
-       if (rename(tmpfn, fn) == -1)
-               return -1;
-       return 0;
-}
-char *password_get(const char *key, const char *fn)
-{
-       // NOTE: there are probably a lot of timing leaks in here
-       FILE *fi = fopen(fn, "rbe");
-       if (!fi)
-               return 0;
-       size_t keylen = strlen(key);
-       static char buf[1024];
-       while (fgets(buf, 1024, fi)) {
-               if (buf[keylen] == ':' && memcmp(buf, key, keylen) == 0) {
-                       size_t len = strlen(buf);
-                       if (buf[len-1] == '\n')
-                               buf[len-1] = '\0';
-                       fclose(fi);
-                       memset(buf+len, 0, 1024-len);
-                       return &buf[keylen+1];
-               }
-       }
-       fclose(fi);
-       memset(buf, 0, 1024);
-       return 0;
-}
+#if 0
 int password_check(const char *key, const char *pw, const char *fn)
 {
        char *salt = password_get(key, fn);
@@ -127,26 +59,4 @@ int password_check(const char *key, const char *pw, const char *fn)
                return -1;
        }
 }
-const char *db_read_rhs(const char *key, const char *fn) {
-       FILE *fi = fopen(fn, "rbe");
-       if (!fi)
-               return 0;
-       size_t keylen = strlen(key);
-       static char buf[1024];
-       while (fgets(buf, 1024, fi)) {
-               char *rhs = strchr(buf, ':');
-               if (rhs) {
-                       rhs++;
-                       size_t len = strlen(rhs);
-                       if (rhs[len-1] == '\n')
-                               rhs[--len] = '\0';
-                       if (keylen == len && memcmp(rhs, key, keylen) == 0) {
-                               rhs[-1] = '\0';
-                               fclose(fi);
-                               return buf;
-                       }
-               }
-       }
-       fclose(fi);
-       return 0;
-}
+#endif
index bd27035..690c2af 100644 (file)
@@ -1,12 +1,10 @@
-// Copyright (c) 2022-2024 Egor
+// Copyright (c) 2022-2024, 2026 Egor
 // SPDX-License-Identifier: GPL-2.0-or-later
 #ifndef PASSWD_H_
 #define PASSWD_H_
+#include "db.hpp"
 char *password_crypt(const char *key, const char *setting);
 char *password_hash2(const char *pw, int algo, int strength);
 char *password_hash(const char *pw);
-int password_set(const char *key, const char *pw, const char *fn, const char *tmpfn);
-char *password_get(const char *key, const char *fn);
-int password_check(const char *key, const char *pw, const char *fn);
-const char *db_read_rhs(const char *key, const char *fn);
+//int password_check(const char *key, const char *pw, const char *fn);
 #endif
diff --git a/src/setpass.cpp b/src/setpass.cpp
deleted file mode 100644 (file)
index 819ace6..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) 2022-2023, 2025 Egor
-// SPDX-License-Identifier: GPL-2.0-or-later
-#include "common.hpp"
-#include "passwd.hpp"
-
-static char *read_password(const char *prompt)
-{
-       static char buffer[256];
-       struct termios oldattr, newattr;
-
-       if (tcgetattr(0, &oldattr) == -1)
-               return 0;
-       newattr = oldattr;
-       newattr.c_lflag &= ~(ECHO|ISIG);
-       tcsetattr(0, TCSAFLUSH, &newattr);
-
-       write(0, prompt, strlen(prompt));
-       ssize_t n = read(0, buffer, 256);
-       if (n < 0)
-               n = 0;
-       else if (n > 0 && buffer[n-1] == '\n')
-               n--;
-       buffer[n] = '\0';
-
-       tcsetattr(0, TCSAFLUSH, &oldattr);
-       write(0, "\n", 1);
-
-       return n ? buffer : 0;
-}
-
-int main(int argc, char **argv)
-{
-       if (argc < 3) {
-               fprintf(stderr, "Usage: %s [username] [datafile.txt]\n", argv[0]);
-               return 1;
-       }
-       char *pass = read_password("password: ");
-       if (!pass)
-               return 1;
-       if (password_set(argv[1], pass, argv[2], "setpass.tmp") == -1) {
-               fprintf(stderr, "There was an error. Does %s exist?\n", argv[2]);
-               return 1;
-       }
-       return 0;
-}