From bb8df01e254f7bdb5f518d2714d3e820c2207c62 Mon Sep 17 00:00:00 2001 From: Sadie Powell Date: Thu, 7 Mar 2024 23:18:08 +0000 Subject: Move some modules which can be built by default out of extra. --- modules/CMakeLists.txt | 6 - modules/extra/ldap_authentication.cpp | 313 ---------------------------------- modules/extra/ldap_oper.cpp | 140 --------------- modules/extra/sql_authentication.cpp | 159 ----------------- modules/extra/sql_log.cpp | 108 ------------ modules/extra/sql_oper.cpp | 181 -------------------- modules/ldap_authentication.cpp | 313 ++++++++++++++++++++++++++++++++++ modules/ldap_oper.cpp | 140 +++++++++++++++ modules/sql_authentication.cpp | 159 +++++++++++++++++ modules/sql_log.cpp | 108 ++++++++++++ modules/sql_oper.cpp | 181 ++++++++++++++++++++ 11 files changed, 901 insertions(+), 907 deletions(-) delete mode 100644 modules/extra/ldap_authentication.cpp delete mode 100644 modules/extra/ldap_oper.cpp delete mode 100644 modules/extra/sql_authentication.cpp delete mode 100644 modules/extra/sql_log.cpp delete mode 100644 modules/extra/sql_oper.cpp create mode 100644 modules/ldap_authentication.cpp create mode 100644 modules/ldap_oper.cpp create mode 100644 modules/sql_authentication.cpp create mode 100644 modules/sql_log.cpp create mode 100644 modules/sql_oper.cpp (limited to 'modules') diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 7e1298f53..8485886e2 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -25,12 +25,6 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../conanbuildinfo.cmake") enable_extra("ssl_openssl" "OPENSSL") # this uses Wldap so should always be available copy_extra("ldap") - # these don't actually have extra dependencies, but require a module which does - copy_extra("sql_authentication") - copy_extra("sql_log") - copy_extra("sql_oper") - copy_extra("ldap_authentication") - copy_extra("ldap_oper") # Package extra dlls file(GLOB EXTRA_DLLS "${Anope_SOURCE_DIR}/extradll/bin/*.dll" "${Anope_SOURCE_DIR}/extradll/lib/*.dll") diff --git a/modules/extra/ldap_authentication.cpp b/modules/extra/ldap_authentication.cpp deleted file mode 100644 index 4f4f4f1fc..000000000 --- a/modules/extra/ldap_authentication.cpp +++ /dev/null @@ -1,313 +0,0 @@ -/* - * - * (C) 2011-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" -#include "modules/ldap.h" - -static Module *me; - -static Anope::string basedn; -static Anope::string search_filter; -static Anope::string object_class; -static Anope::string email_attribute; -static Anope::string username_attribute; - -struct IdentifyInfo final -{ - Reference user; - IdentifyRequest *req; - ServiceReference lprov; - bool admin_bind = true; - Anope::string dn; - - IdentifyInfo(User *u, IdentifyRequest *r, ServiceReference &lp) : user(u), req(r), lprov(lp) - { - req->Hold(me); - } - - ~IdentifyInfo() - { - req->Release(me); - } -}; - -class IdentifyInterface final - : public LDAPInterface -{ - IdentifyInfo *ii; - -public: - IdentifyInterface(Module *m, IdentifyInfo *i) : LDAPInterface(m), ii(i) { } - - ~IdentifyInterface() - { - delete ii; - } - - void OnDelete() override - { - delete this; - } - - void OnResult(const LDAPResult &r) override - { - if (!ii->lprov) - return; - - switch (r.type) - { - case QUERY_SEARCH: - { - if (!r.empty()) - { - try - { - const LDAPAttributes &attr = r.get(0); - ii->dn = attr.get("dn"); - Log(LOG_DEBUG) << "ldap_authenticationn: binding as " << ii->dn; - - ii->lprov->Bind(new IdentifyInterface(this->owner, ii), ii->dn, ii->req->GetPassword()); - ii = NULL; - } - catch (const LDAPException &ex) - { - Log(this->owner) << "Error binding after search: " << ex.GetReason(); - } - } - break; - } - case QUERY_BIND: - { - if (ii->admin_bind) - { - Anope::string sf = search_filter.replace_all_cs("%account", ii->req->GetAccount()).replace_all_cs("%object_class", object_class); - try - { - Log(LOG_DEBUG) << "ldap_authentication: searching for " << sf; - ii->lprov->Search(new IdentifyInterface(this->owner, ii), basedn, sf); - ii->admin_bind = false; - ii = NULL; - } - catch (const LDAPException &ex) - { - Log(this->owner) << "Unable to search for " << sf << ": " << ex.GetReason(); - } - } - else - { - NickAlias *na = NickAlias::Find(ii->req->GetAccount()); - if (na == NULL) - { - na = new NickAlias(ii->req->GetAccount(), new NickCore(ii->req->GetAccount())); - na->last_realname = ii->user ? ii->user->realname : ii->req->GetAccount(); - FOREACH_MOD(OnNickRegister, (ii->user, na, ii->req->GetPassword())); - BotInfo *NickServ = Config->GetClient("NickServ"); - if (ii->user && NickServ) - ii->user->SendMessage(NickServ, _("Your account \002%s\002 has been successfully created."), na->nick.c_str()); - } - // encrypt and store the password in the nickcore - Anope::Encrypt(ii->req->GetPassword(), na->nc->pass); - - na->nc->Extend("ldap_authentication_dn", ii->dn); - ii->req->Success(me); - } - break; - } - default: - break; - } - } - - void OnError(const LDAPResult &r) override - { - } -}; - -class OnIdentifyInterface final - : public LDAPInterface -{ - Anope::string uid; - -public: - OnIdentifyInterface(Module *m, const Anope::string &i) : LDAPInterface(m), uid(i) { } - - void OnDelete() override - { - delete this; - } - - void OnResult(const LDAPResult &r) override - { - User *u = User::Find(uid); - - if (!u || !u->Account() || r.empty()) - return; - - try - { - const LDAPAttributes &attr = r.get(0); - Anope::string email = attr.get(email_attribute); - - if (!email.equals_ci(u->Account()->email)) - { - u->Account()->email = email; - BotInfo *NickServ = Config->GetClient("NickServ"); - if (NickServ) - u->SendMessage(NickServ, _("Your email has been updated to \002%s\002"), email.c_str()); - Log(this->owner) << "Updated email address for " << u->nick << " (" << u->Account()->display << ") to " << email; - } - } - catch (const LDAPException &ex) - { - Log(this->owner) << ex.GetReason(); - } - } - - void OnError(const LDAPResult &r) override - { - Log(this->owner) << r.error; - } -}; - -class OnRegisterInterface final - : public LDAPInterface -{ -public: - OnRegisterInterface(Module *m) : LDAPInterface(m) { } - - void OnResult(const LDAPResult &r) override - { - Log(this->owner) << "Successfully added newly created account to LDAP"; - } - - void OnError(const LDAPResult &r) override - { - Log(this->owner) << "Error adding newly created account to LDAP: " << r.getError(); - } -}; - -class ModuleLDAPAuthentication final - : public Module -{ - ServiceReference ldap; - OnRegisterInterface orinterface; - - PrimitiveExtensibleItem dn; - - Anope::string password_attribute; - Anope::string disable_register_reason; - Anope::string disable_email_reason; -public: - ModuleLDAPAuthentication(const Anope::string &modname, const Anope::string &creator) : - Module(modname, creator, EXTRA | VENDOR), ldap("LDAPProvider", "ldap/main"), orinterface(this), - dn(this, "ldap_authentication_dn") - { - me = this; - } - - void Prioritize() override - { - ModuleManager::SetPriority(this, PRIORITY_FIRST); - } - - void OnReload(Configuration::Conf *config) override - { - Configuration::Block *conf = Config->GetModule(this); - - basedn = conf->Get("basedn"); - search_filter = conf->Get("search_filter"); - object_class = conf->Get("object_class"); - username_attribute = conf->Get("username_attribute"); - this->password_attribute = conf->Get("password_attribute"); - email_attribute = conf->Get("email_attribute"); - this->disable_register_reason = conf->Get("disable_register_reason"); - this->disable_email_reason = conf->Get("disable_email_reason"); - } - - EventReturn OnPreCommand(CommandSource &source, Command *command, std::vector ¶ms) override - { - if (!this->disable_register_reason.empty()) - { - if (command->name == "nickserv/register" || command->name == "nickserv/group") - { - source.Reply(this->disable_register_reason); - return EVENT_STOP; - } - } - - if (!email_attribute.empty() && !this->disable_email_reason.empty() && command->name == "nickserv/set/email") - { - source.Reply(this->disable_email_reason); - return EVENT_STOP; - } - - return EVENT_CONTINUE; - } - - void OnCheckAuthentication(User *u, IdentifyRequest *req) override - { - if (!this->ldap) - return; - - auto *ii = new IdentifyInfo(u, req, this->ldap); - this->ldap->BindAsAdmin(new IdentifyInterface(this, ii)); - } - - void OnNickIdentify(User *u) override - { - if (email_attribute.empty() || !this->ldap) - return; - - Anope::string *d = dn.Get(u->Account()); - if (!d || d->empty()) - return; - - this->ldap->Search(new OnIdentifyInterface(this, u->GetUID()), *d, "(" + email_attribute + "=*)"); - } - - void OnNickRegister(User *, NickAlias *na, const Anope::string &pass) override - { - if (!this->disable_register_reason.empty() || !this->ldap) - return; - - this->ldap->BindAsAdmin(NULL); - - LDAPMods attributes; - attributes.resize(4); - - attributes[0].name = "objectClass"; - attributes[0].values.push_back("top"); - attributes[0].values.push_back(object_class); - - attributes[1].name = username_attribute; - attributes[1].values.push_back(na->nick); - - if (!na->nc->email.empty()) - { - attributes[2].name = email_attribute; - attributes[2].values.push_back(na->nc->email); - } - - attributes[3].name = this->password_attribute; - attributes[3].values.push_back(pass); - - Anope::string new_dn = username_attribute + "=" + na->nick + "," + basedn; - this->ldap->Add(&this->orinterface, new_dn, attributes); - } - - void OnPreNickExpire(NickAlias *na, bool &expire) override - { - // We can't let nicks expire if they still have a group or - // there will be a zombie account left over that can't be - // authenticated to. - if (na->nick == na->nc->display && na->nc->aliases->size() > 1) - expire = false; - } -}; - -MODULE_INIT(ModuleLDAPAuthentication) diff --git a/modules/extra/ldap_oper.cpp b/modules/extra/ldap_oper.cpp deleted file mode 100644 index 62749f957..000000000 --- a/modules/extra/ldap_oper.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/* - * - * (C) 2011-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" -#include "modules/ldap.h" - -static std::set my_opers; -static Anope::string opertype_attribute; - -class IdentifyInterface final - : public LDAPInterface -{ - Reference u; - -public: - IdentifyInterface(Module *m, User *user) : LDAPInterface(m), u(user) - { - } - - void OnResult(const LDAPResult &r) override - { - if (!u || !u->Account()) - return; - - NickCore *nc = u->Account(); - - try - { - const LDAPAttributes &attr = r.get(0); - - const Anope::string &opertype = attr.get(opertype_attribute); - - OperType *ot = OperType::Find(opertype); - if (ot != NULL && (nc->o == NULL || ot != nc->o->ot)) - { - Oper *o = nc->o; - if (o != NULL && my_opers.count(o) > 0) - { - my_opers.erase(o); - delete o; - } - o = new Oper(u->nick, ot); - my_opers.insert(o); - nc->o = o; - Log(this->owner) << "Tied " << u->nick << " (" << nc->display << ") to opertype " << ot->GetName(); - } - } - catch (const LDAPException &ex) - { - if (nc->o != NULL) - { - if (my_opers.count(nc->o) > 0) - { - my_opers.erase(nc->o); - delete nc->o; - } - nc->o = NULL; - - Log(this->owner) << "Removed services operator from " << u->nick << " (" << nc->display << ")"; - } - } - } - - void OnError(const LDAPResult &r) override - { - } - - void OnDelete() override - { - delete this; - } -}; - -class LDAPOper final - : public Module -{ - ServiceReference ldap; - - Anope::string binddn; - Anope::string password; - Anope::string basedn; - Anope::string filter; -public: - LDAPOper(const Anope::string &modname, const Anope::string &creator) : - Module(modname, creator, EXTRA | VENDOR), ldap("LDAPProvider", "ldap/main") - { - - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *config = Config->GetModule(this); - - this->binddn = config->Get("binddn"); - this->password = config->Get("password"); - this->basedn = config->Get("basedn"); - this->filter = config->Get("filter"); - opertype_attribute = config->Get("opertype_attribute"); - - for (const auto *oper : my_opers) - delete oper; - my_opers.clear(); - } - - void OnNickIdentify(User *u) override - { - try - { - if (!this->ldap) - throw LDAPException("No LDAP interface. Is ldap loaded and configured correctly?"); - else if (this->basedn.empty() || this->filter.empty() || opertype_attribute.empty()) - throw LDAPException("Could not search LDAP for opertype settings, invalid configuration."); - - if (!this->binddn.empty()) - this->ldap->Bind(NULL, this->binddn.replace_all_cs("%a", u->Account()->display), this->password.c_str()); - this->ldap->Search(new IdentifyInterface(this, u), this->basedn, this->filter.replace_all_cs("%a", u->Account()->display)); - } - catch (const LDAPException &ex) - { - Log() << ex.GetReason(); - } - } - - void OnDelCore(NickCore *nc) override - { - if (nc->o != NULL && my_opers.count(nc->o) > 0) - { - my_opers.erase(nc->o); - delete nc->o; - nc->o = NULL; - } - } -}; - -MODULE_INIT(LDAPOper) diff --git a/modules/extra/sql_authentication.cpp b/modules/extra/sql_authentication.cpp deleted file mode 100644 index 54307496c..000000000 --- a/modules/extra/sql_authentication.cpp +++ /dev/null @@ -1,159 +0,0 @@ -/* - * - * (C) 2012-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" -#include "modules/sql.h" - -static Module *me; - -class SQLAuthenticationResult final - : public SQL::Interface -{ - Reference user; - IdentifyRequest *req; - -public: - SQLAuthenticationResult(User *u, IdentifyRequest *r) : SQL::Interface(me), user(u), req(r) - { - req->Hold(me); - } - - ~SQLAuthenticationResult() - { - req->Release(me); - } - - void OnResult(const SQL::Result &r) override - { - if (r.Rows() == 0) - { - Log(LOG_DEBUG) << "sql_authentication: Unsuccessful authentication for " << req->GetAccount(); - delete this; - return; - } - - Log(LOG_DEBUG) << "sql_authentication: Successful authentication for " << req->GetAccount(); - - Anope::string email; - try - { - email = r.Get(0, "email"); - } - catch (const SQL::Exception &) { } - - NickAlias *na = NickAlias::Find(req->GetAccount()); - BotInfo *NickServ = Config->GetClient("NickServ"); - if (na == NULL) - { - na = new NickAlias(req->GetAccount(), new NickCore(req->GetAccount())); - FOREACH_MOD(OnNickRegister, (user, na, "")); - if (user && NickServ) - user->SendMessage(NickServ, _("Your account \002%s\002 has been successfully created."), na->nick.c_str()); - } - - if (!email.empty() && email != na->nc->email) - { - na->nc->email = email; - if (user && NickServ) - user->SendMessage(NickServ, _("Your email has been updated to \002%s\002."), email.c_str()); - } - - req->Success(me); - delete this; - } - - void OnError(const SQL::Result &r) override - { - Log(this->owner) << "sql_authentication: Error executing query " << r.GetQuery().query << ": " << r.GetError(); - delete this; - } -}; - -class ModuleSQLAuthentication final - : public Module -{ - Anope::string engine; - Anope::string query; - Anope::string disable_reason, disable_email_reason; - - ServiceReference SQL; - -public: - ModuleSQLAuthentication(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) - { - me = this; - - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *config = conf->GetModule(this); - this->engine = config->Get("engine"); - this->query = config->Get("query"); - this->disable_reason = config->Get("disable_reason"); - this->disable_email_reason = config->Get("disable_email_reason"); - - this->SQL = ServiceReference("SQL::Provider", this->engine); - } - - EventReturn OnPreCommand(CommandSource &source, Command *command, std::vector ¶ms) override - { - if (!this->disable_reason.empty() && (command->name == "nickserv/register" || command->name == "nickserv/group")) - { - source.Reply(this->disable_reason); - return EVENT_STOP; - } - - if (!this->disable_email_reason.empty() && command->name == "nickserv/set/email") - { - source.Reply(this->disable_email_reason); - return EVENT_STOP; - } - - return EVENT_CONTINUE; - } - - void OnCheckAuthentication(User *u, IdentifyRequest *req) override - { - if (!this->SQL) - { - Log(this) << "Unable to find SQL engine"; - return; - } - - SQL::Query q(this->query); - q.SetValue("a", req->GetAccount()); - q.SetValue("p", req->GetPassword()); - if (u) - { - q.SetValue("n", u->nick); - q.SetValue("i", u->ip.addr()); - } - else - { - q.SetValue("n", ""); - q.SetValue("i", ""); - } - - - this->SQL->Run(new SQLAuthenticationResult(u, req), q); - - Log(LOG_DEBUG) << "sql_authentication: Checking authentication for " << req->GetAccount(); - } - - void OnPreNickExpire(NickAlias *na, bool &expire) override - { - // We can't let nicks expire if they still have a group or - // there will be a zombie account left over that can't be - // authenticated to. - if (na->nick == na->nc->display && na->nc->aliases->size() > 1) - expire = false; - } -}; - -MODULE_INIT(ModuleSQLAuthentication) diff --git a/modules/extra/sql_log.cpp b/modules/extra/sql_log.cpp deleted file mode 100644 index 7335a5abf..000000000 --- a/modules/extra/sql_log.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * - * (C) 2003-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" -#include "modules/sql.h" - -class SQLLog final - : public Module -{ - std::set inited; - Anope::string table; - -public: - SQLLog(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR | EXTRA) - { - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *config = conf->GetModule(this); - this->table = config->Get("table", "logs"); - } - - void OnLogMessage(LogInfo *li, const Log *l, const Anope::string &msg) override - { - Anope::string ref_name; - ServiceReference SQL; - - for (const auto &target : li->targets) - { - size_t sz = target.find("sql_log:"); - if (!sz) - { - ref_name = target.substr(8); - SQL = ServiceReference("SQL::Provider", ref_name); - break; - } - } - - if (!SQL) - return; - - if (!inited.count(ref_name)) - { - inited.insert(ref_name); - - SQL::Query create("CREATE TABLE IF NOT EXISTS `" + table + "` (" - "`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP," - "`type` varchar(64) NOT NULL," - "`user` varchar(64) NOT NULL," - "`acc` varchar(64) NOT NULL," - "`command` varchar(64) NOT NULL," - "`channel` varchar(64) NOT NULL," - "`msg` text NOT NULL" - ")"); - - SQL->Run(NULL, create); - } - - SQL::Query insert("INSERT INTO `" + table + "` (`type`,`user`,`acc`,`command`,`channel`,`msg`)" - "VALUES (@type@, @user@, @acc@, @command@, @channel@, @msg@)"); - - switch (l->type) - { - case LOG_ADMIN: - insert.SetValue("type", "ADMIN"); - break; - case LOG_OVERRIDE: - insert.SetValue("type", "OVERRIDE"); - break; - case LOG_COMMAND: - insert.SetValue("type", "COMMAND"); - break; - case LOG_SERVER: - insert.SetValue("type", "SERVER"); - break; - case LOG_CHANNEL: - insert.SetValue("type", "CHANNEL"); - break; - case LOG_USER: - insert.SetValue("type", "USER"); - break; - case LOG_MODULE: - insert.SetValue("type", "MODULE"); - break; - case LOG_NORMAL: - insert.SetValue("type", "NORMAL"); - break; - default: - return; - } - - insert.SetValue("user", l->u ? l->u->nick : ""); - insert.SetValue("acc", l->nc ? l->nc->display : ""); - insert.SetValue("command", l->c ? l->c->name : ""); - insert.SetValue("channel", l->ci ? l->ci->name : ""); - insert.SetValue("msg", msg); - - SQL->Run(NULL, insert); - } -}; - -MODULE_INIT(SQLLog) diff --git a/modules/extra/sql_oper.cpp b/modules/extra/sql_oper.cpp deleted file mode 100644 index af2b40346..000000000 --- a/modules/extra/sql_oper.cpp +++ /dev/null @@ -1,181 +0,0 @@ -/* - * - * (C) 2012-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" -#include "modules/sql.h" - -struct SQLOper final - : Oper -{ - SQLOper(const Anope::string &n, OperType *o) : Oper(n, o) { } -}; - -class SQLOperResult final - : public SQL::Interface -{ - Reference user; - - struct SQLOperResultDeleter final - { - SQLOperResult *res; - SQLOperResultDeleter(SQLOperResult *r) : res(r) { } - ~SQLOperResultDeleter() { delete res; } - }; - - void Deoper() - { - if (user->Account() && user->Account()->o && dynamic_cast(user->Account()->o)) - { - delete user->Account()->o; - user->Account()->o = NULL; - - Log(this->owner) << "sql_oper: Removed services operator from " << user->nick << " (" << user->Account()->display << ")"; - - BotInfo *OperServ = Config->GetClient("OperServ"); - user->RemoveMode(OperServ, "OPER"); // Probably not set, just incase - } - } - -public: - SQLOperResult(Module *m, User *u) : SQL::Interface(m), user(u) { } - - void OnResult(const SQL::Result &r) override - { - SQLOperResultDeleter d(this); - - if (!user || !user->Account()) - return; - - if (r.Rows() == 0) - { - Log(LOG_DEBUG) << "sql_oper: Got 0 rows for " << user->nick; - Deoper(); - return; - } - - Anope::string opertype; - try - { - opertype = r.Get(0, "opertype"); - } - catch (const SQL::Exception &) - { - Log(this->owner) << "Expected column named \"opertype\" but one was not found"; - return; - } - - Log(LOG_DEBUG) << "sql_oper: Got result for " << user->nick << ", opertype " << opertype; - - Anope::string modes; - try - { - modes = r.Get(0, "modes"); - } - catch (const SQL::Exception &) - { - // Common case here is an exception, but this probably doesn't get this far often - } - - BotInfo *OperServ = Config->GetClient("OperServ"); - if (opertype.empty()) - { - Deoper(); - return; - } - - OperType *ot = OperType::Find(opertype); - if (ot == NULL) - { - Log(this->owner) << "sql_oper: Oper " << user->nick << " has type " << opertype << ", but this opertype does not exist?"; - return; - } - - if (user->Account()->o && !dynamic_cast(user->Account()->o)) - { - Log(this->owner) << "Oper " << user->Account()->display << " has type " << opertype << ", but is already configured as an oper of type " << user->Account()->o->ot->GetName(); - return; - } - - if (!user->Account()->o || user->Account()->o->ot != ot) - { - Log(this->owner) << "sql_oper: Tieing oper " << user->nick << " to type " << opertype; - - delete user->Account()->o; - user->Account()->o = new SQLOper(user->Account()->display, ot); - } - - if (!user->HasMode("OPER")) - { - IRCD->SendOper(user); - - if (!modes.empty()) - user->SetModes(OperServ, modes); - } - } - - void OnError(const SQL::Result &r) override - { - SQLOperResultDeleter d(this); - Log(this->owner) << "sql_oper: Error executing query " << r.GetQuery().query << ": " << r.GetError(); - } -}; - -class ModuleSQLOper final - : public Module -{ - Anope::string engine; - Anope::string query; - - ServiceReference SQL; - -public: - ModuleSQLOper(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) - { - } - - ~ModuleSQLOper() - { - for (const auto &[_, nc] : *NickCoreList) - { - if (nc->o && dynamic_cast(nc->o)) - { - delete nc->o; - nc->o = NULL; - } - } - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *config = conf->GetModule(this); - - this->engine = config->Get("engine"); - this->query = config->Get("query"); - - this->SQL = ServiceReference("SQL::Provider", this->engine); - } - - void OnNickIdentify(User *u) override - { - if (!this->SQL) - { - Log() << "Unable to find SQL engine"; - return; - } - - SQL::Query q(this->query); - q.SetValue("a", u->Account()->display); - q.SetValue("i", u->ip.addr()); - - this->SQL->Run(new SQLOperResult(this, u), q); - - Log(LOG_DEBUG) << "sql_oper: Checking authentication for " << u->Account()->display; - } -}; - -MODULE_INIT(ModuleSQLOper) diff --git a/modules/ldap_authentication.cpp b/modules/ldap_authentication.cpp new file mode 100644 index 000000000..4f4f4f1fc --- /dev/null +++ b/modules/ldap_authentication.cpp @@ -0,0 +1,313 @@ +/* + * + * (C) 2011-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/ldap.h" + +static Module *me; + +static Anope::string basedn; +static Anope::string search_filter; +static Anope::string object_class; +static Anope::string email_attribute; +static Anope::string username_attribute; + +struct IdentifyInfo final +{ + Reference user; + IdentifyRequest *req; + ServiceReference lprov; + bool admin_bind = true; + Anope::string dn; + + IdentifyInfo(User *u, IdentifyRequest *r, ServiceReference &lp) : user(u), req(r), lprov(lp) + { + req->Hold(me); + } + + ~IdentifyInfo() + { + req->Release(me); + } +}; + +class IdentifyInterface final + : public LDAPInterface +{ + IdentifyInfo *ii; + +public: + IdentifyInterface(Module *m, IdentifyInfo *i) : LDAPInterface(m), ii(i) { } + + ~IdentifyInterface() + { + delete ii; + } + + void OnDelete() override + { + delete this; + } + + void OnResult(const LDAPResult &r) override + { + if (!ii->lprov) + return; + + switch (r.type) + { + case QUERY_SEARCH: + { + if (!r.empty()) + { + try + { + const LDAPAttributes &attr = r.get(0); + ii->dn = attr.get("dn"); + Log(LOG_DEBUG) << "ldap_authenticationn: binding as " << ii->dn; + + ii->lprov->Bind(new IdentifyInterface(this->owner, ii), ii->dn, ii->req->GetPassword()); + ii = NULL; + } + catch (const LDAPException &ex) + { + Log(this->owner) << "Error binding after search: " << ex.GetReason(); + } + } + break; + } + case QUERY_BIND: + { + if (ii->admin_bind) + { + Anope::string sf = search_filter.replace_all_cs("%account", ii->req->GetAccount()).replace_all_cs("%object_class", object_class); + try + { + Log(LOG_DEBUG) << "ldap_authentication: searching for " << sf; + ii->lprov->Search(new IdentifyInterface(this->owner, ii), basedn, sf); + ii->admin_bind = false; + ii = NULL; + } + catch (const LDAPException &ex) + { + Log(this->owner) << "Unable to search for " << sf << ": " << ex.GetReason(); + } + } + else + { + NickAlias *na = NickAlias::Find(ii->req->GetAccount()); + if (na == NULL) + { + na = new NickAlias(ii->req->GetAccount(), new NickCore(ii->req->GetAccount())); + na->last_realname = ii->user ? ii->user->realname : ii->req->GetAccount(); + FOREACH_MOD(OnNickRegister, (ii->user, na, ii->req->GetPassword())); + BotInfo *NickServ = Config->GetClient("NickServ"); + if (ii->user && NickServ) + ii->user->SendMessage(NickServ, _("Your account \002%s\002 has been successfully created."), na->nick.c_str()); + } + // encrypt and store the password in the nickcore + Anope::Encrypt(ii->req->GetPassword(), na->nc->pass); + + na->nc->Extend("ldap_authentication_dn", ii->dn); + ii->req->Success(me); + } + break; + } + default: + break; + } + } + + void OnError(const LDAPResult &r) override + { + } +}; + +class OnIdentifyInterface final + : public LDAPInterface +{ + Anope::string uid; + +public: + OnIdentifyInterface(Module *m, const Anope::string &i) : LDAPInterface(m), uid(i) { } + + void OnDelete() override + { + delete this; + } + + void OnResult(const LDAPResult &r) override + { + User *u = User::Find(uid); + + if (!u || !u->Account() || r.empty()) + return; + + try + { + const LDAPAttributes &attr = r.get(0); + Anope::string email = attr.get(email_attribute); + + if (!email.equals_ci(u->Account()->email)) + { + u->Account()->email = email; + BotInfo *NickServ = Config->GetClient("NickServ"); + if (NickServ) + u->SendMessage(NickServ, _("Your email has been updated to \002%s\002"), email.c_str()); + Log(this->owner) << "Updated email address for " << u->nick << " (" << u->Account()->display << ") to " << email; + } + } + catch (const LDAPException &ex) + { + Log(this->owner) << ex.GetReason(); + } + } + + void OnError(const LDAPResult &r) override + { + Log(this->owner) << r.error; + } +}; + +class OnRegisterInterface final + : public LDAPInterface +{ +public: + OnRegisterInterface(Module *m) : LDAPInterface(m) { } + + void OnResult(const LDAPResult &r) override + { + Log(this->owner) << "Successfully added newly created account to LDAP"; + } + + void OnError(const LDAPResult &r) override + { + Log(this->owner) << "Error adding newly created account to LDAP: " << r.getError(); + } +}; + +class ModuleLDAPAuthentication final + : public Module +{ + ServiceReference ldap; + OnRegisterInterface orinterface; + + PrimitiveExtensibleItem dn; + + Anope::string password_attribute; + Anope::string disable_register_reason; + Anope::string disable_email_reason; +public: + ModuleLDAPAuthentication(const Anope::string &modname, const Anope::string &creator) : + Module(modname, creator, EXTRA | VENDOR), ldap("LDAPProvider", "ldap/main"), orinterface(this), + dn(this, "ldap_authentication_dn") + { + me = this; + } + + void Prioritize() override + { + ModuleManager::SetPriority(this, PRIORITY_FIRST); + } + + void OnReload(Configuration::Conf *config) override + { + Configuration::Block *conf = Config->GetModule(this); + + basedn = conf->Get("basedn"); + search_filter = conf->Get("search_filter"); + object_class = conf->Get("object_class"); + username_attribute = conf->Get("username_attribute"); + this->password_attribute = conf->Get("password_attribute"); + email_attribute = conf->Get("email_attribute"); + this->disable_register_reason = conf->Get("disable_register_reason"); + this->disable_email_reason = conf->Get("disable_email_reason"); + } + + EventReturn OnPreCommand(CommandSource &source, Command *command, std::vector ¶ms) override + { + if (!this->disable_register_reason.empty()) + { + if (command->name == "nickserv/register" || command->name == "nickserv/group") + { + source.Reply(this->disable_register_reason); + return EVENT_STOP; + } + } + + if (!email_attribute.empty() && !this->disable_email_reason.empty() && command->name == "nickserv/set/email") + { + source.Reply(this->disable_email_reason); + return EVENT_STOP; + } + + return EVENT_CONTINUE; + } + + void OnCheckAuthentication(User *u, IdentifyRequest *req) override + { + if (!this->ldap) + return; + + auto *ii = new IdentifyInfo(u, req, this->ldap); + this->ldap->BindAsAdmin(new IdentifyInterface(this, ii)); + } + + void OnNickIdentify(User *u) override + { + if (email_attribute.empty() || !this->ldap) + return; + + Anope::string *d = dn.Get(u->Account()); + if (!d || d->empty()) + return; + + this->ldap->Search(new OnIdentifyInterface(this, u->GetUID()), *d, "(" + email_attribute + "=*)"); + } + + void OnNickRegister(User *, NickAlias *na, const Anope::string &pass) override + { + if (!this->disable_register_reason.empty() || !this->ldap) + return; + + this->ldap->BindAsAdmin(NULL); + + LDAPMods attributes; + attributes.resize(4); + + attributes[0].name = "objectClass"; + attributes[0].values.push_back("top"); + attributes[0].values.push_back(object_class); + + attributes[1].name = username_attribute; + attributes[1].values.push_back(na->nick); + + if (!na->nc->email.empty()) + { + attributes[2].name = email_attribute; + attributes[2].values.push_back(na->nc->email); + } + + attributes[3].name = this->password_attribute; + attributes[3].values.push_back(pass); + + Anope::string new_dn = username_attribute + "=" + na->nick + "," + basedn; + this->ldap->Add(&this->orinterface, new_dn, attributes); + } + + void OnPreNickExpire(NickAlias *na, bool &expire) override + { + // We can't let nicks expire if they still have a group or + // there will be a zombie account left over that can't be + // authenticated to. + if (na->nick == na->nc->display && na->nc->aliases->size() > 1) + expire = false; + } +}; + +MODULE_INIT(ModuleLDAPAuthentication) diff --git a/modules/ldap_oper.cpp b/modules/ldap_oper.cpp new file mode 100644 index 000000000..62749f957 --- /dev/null +++ b/modules/ldap_oper.cpp @@ -0,0 +1,140 @@ +/* + * + * (C) 2011-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/ldap.h" + +static std::set my_opers; +static Anope::string opertype_attribute; + +class IdentifyInterface final + : public LDAPInterface +{ + Reference u; + +public: + IdentifyInterface(Module *m, User *user) : LDAPInterface(m), u(user) + { + } + + void OnResult(const LDAPResult &r) override + { + if (!u || !u->Account()) + return; + + NickCore *nc = u->Account(); + + try + { + const LDAPAttributes &attr = r.get(0); + + const Anope::string &opertype = attr.get(opertype_attribute); + + OperType *ot = OperType::Find(opertype); + if (ot != NULL && (nc->o == NULL || ot != nc->o->ot)) + { + Oper *o = nc->o; + if (o != NULL && my_opers.count(o) > 0) + { + my_opers.erase(o); + delete o; + } + o = new Oper(u->nick, ot); + my_opers.insert(o); + nc->o = o; + Log(this->owner) << "Tied " << u->nick << " (" << nc->display << ") to opertype " << ot->GetName(); + } + } + catch (const LDAPException &ex) + { + if (nc->o != NULL) + { + if (my_opers.count(nc->o) > 0) + { + my_opers.erase(nc->o); + delete nc->o; + } + nc->o = NULL; + + Log(this->owner) << "Removed services operator from " << u->nick << " (" << nc->display << ")"; + } + } + } + + void OnError(const LDAPResult &r) override + { + } + + void OnDelete() override + { + delete this; + } +}; + +class LDAPOper final + : public Module +{ + ServiceReference ldap; + + Anope::string binddn; + Anope::string password; + Anope::string basedn; + Anope::string filter; +public: + LDAPOper(const Anope::string &modname, const Anope::string &creator) : + Module(modname, creator, EXTRA | VENDOR), ldap("LDAPProvider", "ldap/main") + { + + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *config = Config->GetModule(this); + + this->binddn = config->Get("binddn"); + this->password = config->Get("password"); + this->basedn = config->Get("basedn"); + this->filter = config->Get("filter"); + opertype_attribute = config->Get("opertype_attribute"); + + for (const auto *oper : my_opers) + delete oper; + my_opers.clear(); + } + + void OnNickIdentify(User *u) override + { + try + { + if (!this->ldap) + throw LDAPException("No LDAP interface. Is ldap loaded and configured correctly?"); + else if (this->basedn.empty() || this->filter.empty() || opertype_attribute.empty()) + throw LDAPException("Could not search LDAP for opertype settings, invalid configuration."); + + if (!this->binddn.empty()) + this->ldap->Bind(NULL, this->binddn.replace_all_cs("%a", u->Account()->display), this->password.c_str()); + this->ldap->Search(new IdentifyInterface(this, u), this->basedn, this->filter.replace_all_cs("%a", u->Account()->display)); + } + catch (const LDAPException &ex) + { + Log() << ex.GetReason(); + } + } + + void OnDelCore(NickCore *nc) override + { + if (nc->o != NULL && my_opers.count(nc->o) > 0) + { + my_opers.erase(nc->o); + delete nc->o; + nc->o = NULL; + } + } +}; + +MODULE_INIT(LDAPOper) diff --git a/modules/sql_authentication.cpp b/modules/sql_authentication.cpp new file mode 100644 index 000000000..54307496c --- /dev/null +++ b/modules/sql_authentication.cpp @@ -0,0 +1,159 @@ +/* + * + * (C) 2012-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/sql.h" + +static Module *me; + +class SQLAuthenticationResult final + : public SQL::Interface +{ + Reference user; + IdentifyRequest *req; + +public: + SQLAuthenticationResult(User *u, IdentifyRequest *r) : SQL::Interface(me), user(u), req(r) + { + req->Hold(me); + } + + ~SQLAuthenticationResult() + { + req->Release(me); + } + + void OnResult(const SQL::Result &r) override + { + if (r.Rows() == 0) + { + Log(LOG_DEBUG) << "sql_authentication: Unsuccessful authentication for " << req->GetAccount(); + delete this; + return; + } + + Log(LOG_DEBUG) << "sql_authentication: Successful authentication for " << req->GetAccount(); + + Anope::string email; + try + { + email = r.Get(0, "email"); + } + catch (const SQL::Exception &) { } + + NickAlias *na = NickAlias::Find(req->GetAccount()); + BotInfo *NickServ = Config->GetClient("NickServ"); + if (na == NULL) + { + na = new NickAlias(req->GetAccount(), new NickCore(req->GetAccount())); + FOREACH_MOD(OnNickRegister, (user, na, "")); + if (user && NickServ) + user->SendMessage(NickServ, _("Your account \002%s\002 has been successfully created."), na->nick.c_str()); + } + + if (!email.empty() && email != na->nc->email) + { + na->nc->email = email; + if (user && NickServ) + user->SendMessage(NickServ, _("Your email has been updated to \002%s\002."), email.c_str()); + } + + req->Success(me); + delete this; + } + + void OnError(const SQL::Result &r) override + { + Log(this->owner) << "sql_authentication: Error executing query " << r.GetQuery().query << ": " << r.GetError(); + delete this; + } +}; + +class ModuleSQLAuthentication final + : public Module +{ + Anope::string engine; + Anope::string query; + Anope::string disable_reason, disable_email_reason; + + ServiceReference SQL; + +public: + ModuleSQLAuthentication(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) + { + me = this; + + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *config = conf->GetModule(this); + this->engine = config->Get("engine"); + this->query = config->Get("query"); + this->disable_reason = config->Get("disable_reason"); + this->disable_email_reason = config->Get("disable_email_reason"); + + this->SQL = ServiceReference("SQL::Provider", this->engine); + } + + EventReturn OnPreCommand(CommandSource &source, Command *command, std::vector ¶ms) override + { + if (!this->disable_reason.empty() && (command->name == "nickserv/register" || command->name == "nickserv/group")) + { + source.Reply(this->disable_reason); + return EVENT_STOP; + } + + if (!this->disable_email_reason.empty() && command->name == "nickserv/set/email") + { + source.Reply(this->disable_email_reason); + return EVENT_STOP; + } + + return EVENT_CONTINUE; + } + + void OnCheckAuthentication(User *u, IdentifyRequest *req) override + { + if (!this->SQL) + { + Log(this) << "Unable to find SQL engine"; + return; + } + + SQL::Query q(this->query); + q.SetValue("a", req->GetAccount()); + q.SetValue("p", req->GetPassword()); + if (u) + { + q.SetValue("n", u->nick); + q.SetValue("i", u->ip.addr()); + } + else + { + q.SetValue("n", ""); + q.SetValue("i", ""); + } + + + this->SQL->Run(new SQLAuthenticationResult(u, req), q); + + Log(LOG_DEBUG) << "sql_authentication: Checking authentication for " << req->GetAccount(); + } + + void OnPreNickExpire(NickAlias *na, bool &expire) override + { + // We can't let nicks expire if they still have a group or + // there will be a zombie account left over that can't be + // authenticated to. + if (na->nick == na->nc->display && na->nc->aliases->size() > 1) + expire = false; + } +}; + +MODULE_INIT(ModuleSQLAuthentication) diff --git a/modules/sql_log.cpp b/modules/sql_log.cpp new file mode 100644 index 000000000..7335a5abf --- /dev/null +++ b/modules/sql_log.cpp @@ -0,0 +1,108 @@ +/* + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/sql.h" + +class SQLLog final + : public Module +{ + std::set inited; + Anope::string table; + +public: + SQLLog(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR | EXTRA) + { + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *config = conf->GetModule(this); + this->table = config->Get("table", "logs"); + } + + void OnLogMessage(LogInfo *li, const Log *l, const Anope::string &msg) override + { + Anope::string ref_name; + ServiceReference SQL; + + for (const auto &target : li->targets) + { + size_t sz = target.find("sql_log:"); + if (!sz) + { + ref_name = target.substr(8); + SQL = ServiceReference("SQL::Provider", ref_name); + break; + } + } + + if (!SQL) + return; + + if (!inited.count(ref_name)) + { + inited.insert(ref_name); + + SQL::Query create("CREATE TABLE IF NOT EXISTS `" + table + "` (" + "`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP," + "`type` varchar(64) NOT NULL," + "`user` varchar(64) NOT NULL," + "`acc` varchar(64) NOT NULL," + "`command` varchar(64) NOT NULL," + "`channel` varchar(64) NOT NULL," + "`msg` text NOT NULL" + ")"); + + SQL->Run(NULL, create); + } + + SQL::Query insert("INSERT INTO `" + table + "` (`type`,`user`,`acc`,`command`,`channel`,`msg`)" + "VALUES (@type@, @user@, @acc@, @command@, @channel@, @msg@)"); + + switch (l->type) + { + case LOG_ADMIN: + insert.SetValue("type", "ADMIN"); + break; + case LOG_OVERRIDE: + insert.SetValue("type", "OVERRIDE"); + break; + case LOG_COMMAND: + insert.SetValue("type", "COMMAND"); + break; + case LOG_SERVER: + insert.SetValue("type", "SERVER"); + break; + case LOG_CHANNEL: + insert.SetValue("type", "CHANNEL"); + break; + case LOG_USER: + insert.SetValue("type", "USER"); + break; + case LOG_MODULE: + insert.SetValue("type", "MODULE"); + break; + case LOG_NORMAL: + insert.SetValue("type", "NORMAL"); + break; + default: + return; + } + + insert.SetValue("user", l->u ? l->u->nick : ""); + insert.SetValue("acc", l->nc ? l->nc->display : ""); + insert.SetValue("command", l->c ? l->c->name : ""); + insert.SetValue("channel", l->ci ? l->ci->name : ""); + insert.SetValue("msg", msg); + + SQL->Run(NULL, insert); + } +}; + +MODULE_INIT(SQLLog) diff --git a/modules/sql_oper.cpp b/modules/sql_oper.cpp new file mode 100644 index 000000000..af2b40346 --- /dev/null +++ b/modules/sql_oper.cpp @@ -0,0 +1,181 @@ +/* + * + * (C) 2012-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/sql.h" + +struct SQLOper final + : Oper +{ + SQLOper(const Anope::string &n, OperType *o) : Oper(n, o) { } +}; + +class SQLOperResult final + : public SQL::Interface +{ + Reference user; + + struct SQLOperResultDeleter final + { + SQLOperResult *res; + SQLOperResultDeleter(SQLOperResult *r) : res(r) { } + ~SQLOperResultDeleter() { delete res; } + }; + + void Deoper() + { + if (user->Account() && user->Account()->o && dynamic_cast(user->Account()->o)) + { + delete user->Account()->o; + user->Account()->o = NULL; + + Log(this->owner) << "sql_oper: Removed services operator from " << user->nick << " (" << user->Account()->display << ")"; + + BotInfo *OperServ = Config->GetClient("OperServ"); + user->RemoveMode(OperServ, "OPER"); // Probably not set, just incase + } + } + +public: + SQLOperResult(Module *m, User *u) : SQL::Interface(m), user(u) { } + + void OnResult(const SQL::Result &r) override + { + SQLOperResultDeleter d(this); + + if (!user || !user->Account()) + return; + + if (r.Rows() == 0) + { + Log(LOG_DEBUG) << "sql_oper: Got 0 rows for " << user->nick; + Deoper(); + return; + } + + Anope::string opertype; + try + { + opertype = r.Get(0, "opertype"); + } + catch (const SQL::Exception &) + { + Log(this->owner) << "Expected column named \"opertype\" but one was not found"; + return; + } + + Log(LOG_DEBUG) << "sql_oper: Got result for " << user->nick << ", opertype " << opertype; + + Anope::string modes; + try + { + modes = r.Get(0, "modes"); + } + catch (const SQL::Exception &) + { + // Common case here is an exception, but this probably doesn't get this far often + } + + BotInfo *OperServ = Config->GetClient("OperServ"); + if (opertype.empty()) + { + Deoper(); + return; + } + + OperType *ot = OperType::Find(opertype); + if (ot == NULL) + { + Log(this->owner) << "sql_oper: Oper " << user->nick << " has type " << opertype << ", but this opertype does not exist?"; + return; + } + + if (user->Account()->o && !dynamic_cast(user->Account()->o)) + { + Log(this->owner) << "Oper " << user->Account()->display << " has type " << opertype << ", but is already configured as an oper of type " << user->Account()->o->ot->GetName(); + return; + } + + if (!user->Account()->o || user->Account()->o->ot != ot) + { + Log(this->owner) << "sql_oper: Tieing oper " << user->nick << " to type " << opertype; + + delete user->Account()->o; + user->Account()->o = new SQLOper(user->Account()->display, ot); + } + + if (!user->HasMode("OPER")) + { + IRCD->SendOper(user); + + if (!modes.empty()) + user->SetModes(OperServ, modes); + } + } + + void OnError(const SQL::Result &r) override + { + SQLOperResultDeleter d(this); + Log(this->owner) << "sql_oper: Error executing query " << r.GetQuery().query << ": " << r.GetError(); + } +}; + +class ModuleSQLOper final + : public Module +{ + Anope::string engine; + Anope::string query; + + ServiceReference SQL; + +public: + ModuleSQLOper(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) + { + } + + ~ModuleSQLOper() + { + for (const auto &[_, nc] : *NickCoreList) + { + if (nc->o && dynamic_cast(nc->o)) + { + delete nc->o; + nc->o = NULL; + } + } + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *config = conf->GetModule(this); + + this->engine = config->Get("engine"); + this->query = config->Get("query"); + + this->SQL = ServiceReference("SQL::Provider", this->engine); + } + + void OnNickIdentify(User *u) override + { + if (!this->SQL) + { + Log() << "Unable to find SQL engine"; + return; + } + + SQL::Query q(this->query); + q.SetValue("a", u->Account()->display); + q.SetValue("i", u->ip.addr()); + + this->SQL->Run(new SQLOperResult(this, u), q); + + Log(LOG_DEBUG) << "sql_oper: Checking authentication for " << u->Account()->display; + } +}; + +MODULE_INIT(ModuleSQLOper) -- cgit