From 65266f09f539688a71664af6fbb7699f3655a7a2 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 16 Jan 2026 17:04:27 +0200 Subject: [PATCH] add sqlite database support --- Makefile.am | 13 ++- README.md | 58 +++++++---- REUSE.toml | 2 +- configure.ac | 5 +- data/.gitignore | 13 ++- lua/irccloud.lua | 6 +- src/admin.cpp | 4 +- src/db.cpp | 125 ++++++++++++++++++++++ src/db.hpp | 66 ++++++++++++ src/dbtool.cpp | 262 +++++++++++++++++++++++++++++++++++++++++++++++ src/irc.cpp | 29 +++--- src/main.cpp | 4 +- src/passwd.cpp | 96 +---------------- src/passwd.hpp | 8 +- src/setpass.cpp | 45 -------- 15 files changed, 543 insertions(+), 193 deletions(-) create mode 100644 src/db.cpp create mode 100644 src/db.hpp create mode 100644 src/dbtool.cpp delete mode 100644 src/setpass.cpp diff --git a/Makefile.am b/Makefile.am index 3d7481d..346f50c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/README.md b/README.md index ed9ee80..7eb227e 100644 --- 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 diff --git a/REUSE.toml b/REUSE.toml index d31b545..f2102d2 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -1,4 +1,4 @@ -version = 1 +version = 1 [[annotations]] path = [ diff --git a/configure.ac b/configure.ac index e87fd75..777f905 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/data/.gitignore b/data/.gitignore index 3162d3c..e54037a 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -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 diff --git a/lua/irccloud.lua b/lua/irccloud.lua index 1a83568..9026397 100644 --- a/lua/irccloud.lua +++ b/lua/irccloud.lua @@ -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) diff --git a/src/admin.cpp b/src/admin.cpp index 7c24cab..53c5cf8 100644 --- a/src/admin.cpp +++ b/src/admin.cpp @@ -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 index 0000000..cb060cd --- /dev/null +++ b/src/db.cpp @@ -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 +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(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 index 0000000..0bf2d58 --- /dev/null +++ b/src/db.hpp @@ -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 +#include +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 index 0000000..f4e44b5 --- /dev/null +++ b/src/dbtool.cpp @@ -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 +#include +#include +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 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; +} diff --git a/src/irc.cpp b/src/irc.cpp index b726126..082b0c1 100644 --- a/src/irc.cpp +++ b/src/irc.cpp @@ -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) diff --git a/src/main.cpp b/src/main.cpp index acab933..b9171bd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 diff --git a/src/passwd.cpp b/src/passwd.cpp index b67a9c1..1ab9034 100644 --- a/src/passwd.cpp +++ b/src/passwd.cpp @@ -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 diff --git a/src/passwd.hpp b/src/passwd.hpp index bd27035..690c2af 100644 --- a/src/passwd.hpp +++ b/src/passwd.hpp @@ -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 index 819ace6..0000000 --- a/src/setpass.cpp +++ /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; -} -- 2.56.0