-# 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 \
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
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.
## 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
-version = 1
+version = 1
[[annotations]]
path = [
-# 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])
[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])
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
-# 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
--- Copyright (c) 2024 Egor
+-- Copyright (c) 2024, 2026 Egor
-- SPDX-License-Identifier: GPL-2.0-or-later
-- irccloud authentication support
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
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)
-// Copyright (c) 2023-2025 Egor
+// Copyright (c) 2023-2026 Egor
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common.hpp"
#ifdef HAVE_LUA
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;
}
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+}
// 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"
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);
} 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
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") {
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))
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);
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;
}
}
}
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);
sendbuf(client);
}
}
-
+
// notify the user of their new account name, and update the +r mode
if (willbeloggedin) {
if (newusername)
// 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"
#include "irc.hpp"
#include "job.hpp"
#include "admin.hpp"
+#include "db.hpp"
static int is_running = 1;
fprintf(stderr, "Starting up...\n");
+ auto dblk = db::init();
netpoll::init();
job_init();
#ifdef HAVE_LUA
-// Copyright (c) 2022-2025 Egor
+// Copyright (c) 2022-2026 Egor
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common.hpp"
#include "passwd.hpp"
{
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);
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
-// 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
+++ /dev/null
-// 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;
-}