From 7ac1fe58478d58e2480b6919c4abf3a82929169c Mon Sep 17 00:00:00 2001 From: Sadie Powell Date: Wed, 24 Jan 2024 12:01:50 +0000 Subject: Rename several modules to remove the m_ prefix. --- modules/extra/ldap.cpp | 702 ++++++++++++++++++++++++++++++++ modules/extra/ldap_authentication.cpp | 308 ++++++++++++++ modules/extra/ldap_oper.cpp | 140 +++++++ modules/extra/m_ldap.cpp | 702 -------------------------------- modules/extra/m_ldap_authentication.cpp | 308 -------------- modules/extra/m_ldap_oper.cpp | 140 ------- modules/extra/m_mysql.cpp | 569 -------------------------- modules/extra/m_regex_pcre2.cpp | 91 ----- modules/extra/m_regex_posix.cpp | 82 ---- modules/extra/m_regex_tre.cpp | 83 ---- modules/extra/m_sql_authentication.cpp | 150 ------- modules/extra/m_sql_log.cpp | 108 ----- modules/extra/m_sql_oper.cpp | 181 -------- modules/extra/m_sqlite.cpp | 340 ---------------- modules/extra/m_ssl_gnutls.cpp | 643 ----------------------------- modules/extra/m_ssl_openssl.cpp | 445 -------------------- modules/extra/mysql.cpp | 569 ++++++++++++++++++++++++++ modules/extra/regex_pcre2.cpp | 91 +++++ modules/extra/regex_posix.cpp | 82 ++++ modules/extra/regex_tre.cpp | 83 ++++ modules/extra/sql_authentication.cpp | 150 +++++++ modules/extra/sql_log.cpp | 108 +++++ modules/extra/sql_oper.cpp | 181 ++++++++ modules/extra/sqlite.cpp | 340 ++++++++++++++++ modules/extra/ssl_gnutls.cpp | 643 +++++++++++++++++++++++++++++ modules/extra/ssl_openssl.cpp | 445 ++++++++++++++++++++ 26 files changed, 3842 insertions(+), 3842 deletions(-) create mode 100644 modules/extra/ldap.cpp create mode 100644 modules/extra/ldap_authentication.cpp create mode 100644 modules/extra/ldap_oper.cpp delete mode 100644 modules/extra/m_ldap.cpp delete mode 100644 modules/extra/m_ldap_authentication.cpp delete mode 100644 modules/extra/m_ldap_oper.cpp delete mode 100644 modules/extra/m_mysql.cpp delete mode 100644 modules/extra/m_regex_pcre2.cpp delete mode 100644 modules/extra/m_regex_posix.cpp delete mode 100644 modules/extra/m_regex_tre.cpp delete mode 100644 modules/extra/m_sql_authentication.cpp delete mode 100644 modules/extra/m_sql_log.cpp delete mode 100644 modules/extra/m_sql_oper.cpp delete mode 100644 modules/extra/m_sqlite.cpp delete mode 100644 modules/extra/m_ssl_gnutls.cpp delete mode 100644 modules/extra/m_ssl_openssl.cpp create mode 100644 modules/extra/mysql.cpp create mode 100644 modules/extra/regex_pcre2.cpp create mode 100644 modules/extra/regex_posix.cpp create mode 100644 modules/extra/regex_tre.cpp create mode 100644 modules/extra/sql_authentication.cpp create mode 100644 modules/extra/sql_log.cpp create mode 100644 modules/extra/sql_oper.cpp create mode 100644 modules/extra/sqlite.cpp create mode 100644 modules/extra/ssl_gnutls.cpp create mode 100644 modules/extra/ssl_openssl.cpp (limited to 'modules/extra') diff --git a/modules/extra/ldap.cpp b/modules/extra/ldap.cpp new file mode 100644 index 000000000..31e712662 --- /dev/null +++ b/modules/extra/ldap.cpp @@ -0,0 +1,702 @@ +/* + * + * (C) 2011-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +/* RequiredLibraries: ldap_r|ldap,lber */ + +#include "module.h" +#include "modules/ldap.h" + +#ifdef _WIN32 +# include +# include +# include +# define LDAP_OPT_SUCCESS LDAP_SUCCESS +# define LDAP_OPT_NETWORK_TIMEOUT LDAP_OPT_SEND_TIMEOUT +# define LDAP_STR(X) const_cast((X).c_str()) +# define LDAP_SASL_SIMPLE static_cast(0) +# define LDAP_TIME(X) reinterpret_cast(&(X)) +# define ldap_first_message ldap_first_entry +# define ldap_next_message ldap_next_entry +# define ldap_unbind_ext(LDAP, UNUSED1, UNUSED2) ldap_unbind(LDAP) +# pragma comment(lib, "Wldap32.lib") +# pragma comment(lib, "Wininet.lib") +#else +# include +# define LDAP_STR(X) ((X).c_str()) +# define LDAP_TIME(X) (&(X)) +#endif + +#if defined LDAP_API_FEATURE_X_OPENLDAP_REENTRANT && !LDAP_API_FEATURE_X_OPENLDAP_REENTRANT +# error Anope requires OpenLDAP to be built as reentrant. +#endif + + +class LDAPService; +static Pipe *me; + +class LDAPRequest +{ +public: + LDAPService *service; + LDAPInterface *inter; + LDAPMessage *message = nullptr; /* message returned by ldap_ */ + LDAPResult *result = nullptr; /* final result */ + struct timeval tv; + QueryType type = QUERY_UNKNOWN; + + LDAPRequest(LDAPService *s, LDAPInterface *i) + : service(s) + , inter(i) + { + tv.tv_sec = 0; + tv.tv_usec = 100000; + } + + virtual ~LDAPRequest() + { + delete result; + if (inter != NULL) + inter->OnDelete(); + if (message != NULL) + ldap_msgfree(message); + } + + virtual int run() = 0; +}; + +class LDAPBind final + : public LDAPRequest +{ + Anope::string who, pass; + +public: + LDAPBind(LDAPService *s, LDAPInterface *i, const Anope::string &w, const Anope::string &p) + : LDAPRequest(s, i) + , who(w) + , pass(p) + { + type = QUERY_BIND; + } + + int run() override; +}; + +class LDAPSearchRequest final + : public LDAPRequest +{ + Anope::string base; + Anope::string filter; + +public: + LDAPSearchRequest(LDAPService *s, LDAPInterface *i, const Anope::string &b, const Anope::string &f) + : LDAPRequest(s, i) + , base(b) + , filter(f) + { + type = QUERY_SEARCH; + } + + int run() override; +}; + +class LDAPAdd final + : public LDAPRequest +{ + Anope::string dn; + LDAPMods attributes; + +public: + LDAPAdd(LDAPService *s, LDAPInterface *i, const Anope::string &d, const LDAPMods &attr) + : LDAPRequest(s, i) + , dn(d) + , attributes(attr) + { + type = QUERY_ADD; + } + + int run() override; +}; + +class LDAPDel final + : public LDAPRequest +{ + Anope::string dn; + +public: + LDAPDel(LDAPService *s, LDAPInterface *i, const Anope::string &d) + : LDAPRequest(s, i) + , dn(d) + { + type = QUERY_DELETE; + } + + int run() override; +}; + +class LDAPModify final + : public LDAPRequest +{ + Anope::string base; + LDAPMods attributes; + +public: + LDAPModify(LDAPService *s, LDAPInterface *i, const Anope::string &b, const LDAPMods &attr) + : LDAPRequest(s, i) + , base(b) + , attributes(attr) + { + type = QUERY_MODIFY; + } + + int run() override; +}; + +class LDAPService final + : public LDAPProvider + , public Thread + , public Condition +{ + Anope::string server; + Anope::string admin_binddn; + Anope::string admin_pass; + + LDAP *con; + + time_t last_connect = 0; + +public: + static LDAPMod **BuildMods(const LDAPMods &attributes) + { + LDAPMod **mods = new LDAPMod*[attributes.size() + 1]; + memset(mods, 0, sizeof(LDAPMod*) * (attributes.size() + 1)); + for (unsigned x = 0; x < attributes.size(); ++x) + { + const LDAPModification &l = attributes[x]; + mods[x] = new LDAPMod(); + + if (l.op == LDAPModification::LDAP_ADD) + mods[x]->mod_op = LDAP_MOD_ADD; + else if (l.op == LDAPModification::LDAP_DEL) + mods[x]->mod_op = LDAP_MOD_DELETE; + else if (l.op == LDAPModification::LDAP_REPLACE) + mods[x]->mod_op = LDAP_MOD_REPLACE; + else if (l.op != 0) + throw LDAPException("Unknown LDAP operation"); + mods[x]->mod_type = strdup(l.name.c_str()); + mods[x]->mod_values = new char*[l.values.size() + 1]; + memset(mods[x]->mod_values, 0, sizeof(char *) * (l.values.size() + 1)); + for (unsigned j = 0, c = 0; j < l.values.size(); ++j) + if (!l.values[j].empty()) + mods[x]->mod_values[c++] = strdup(l.values[j].c_str()); + } + return mods; + } + + static void FreeMods(LDAPMod **mods) + { + for (int i = 0; mods[i] != NULL; ++i) + { + free(mods[i]->mod_type); + for (int j = 0; mods[i]->mod_values[j] != NULL; ++j) + free(mods[i]->mod_values[j]); + delete [] mods[i]->mod_values; + } + delete [] mods; + } + +private: +#ifdef _WIN32 + // Windows LDAP does not implement this so we need to do it. + int ldap_initialize(LDAP** ldap, const char* url) + { + URL_COMPONENTS urlComponents; + memset(&urlComponents, 0, sizeof(urlComponents)); + urlComponents.dwStructSize = sizeof(urlComponents); + + urlComponents.lpszScheme = new char[8]; + urlComponents.dwSchemeLength = 8; + + urlComponents.lpszHostName = new char[1024]; + urlComponents.dwHostNameLength = 1024; + + if (!InternetCrackUrlA(url, 0, 0, &urlComponents)) + { + delete[] urlComponents.lpszScheme; + delete[] urlComponents.lpszHostName; + return LDAP_CONNECT_ERROR; // Malformed url. + } + + unsigned long port = 389; // Default plaintext port. + bool secure = false; // LDAP defaults to plaintext. + if (urlComponents.dwSchemeLength > 0) + { + const Anope::string scheme(urlComponents.lpszScheme); + if (scheme.equals_ci("ldaps")) + { + port = 636; // Default encrypted port. + secure = true; + } + else if (!scheme.equals_ci("ldap")) + { + delete[] urlComponents.lpszScheme; + delete[] urlComponents.lpszHostName; + return LDAP_CONNECT_ERROR; // Invalid protocol. + } + } + + if (urlComponents.nPort > 0) + { + port = urlComponents.nPort; + } + + *ldap = ldap_sslinit(urlComponents.lpszHostName, port, secure); + delete[] urlComponents.lpszScheme; + delete[] urlComponents.lpszHostName; + if (!*ldap) + { + return LdapGetLastError(); // Something went wrong, find out what. + } + + // We're connected to the LDAP server! + return LDAP_SUCCESS; + } +#endif + + void Connect() + { + int i = ldap_initialize(&this->con, this->server.c_str()); + if (i != LDAP_SUCCESS) + throw LDAPException("Unable to connect to LDAP service " + this->name + ": " + ldap_err2string(i)); + + const int version = LDAP_VERSION3; + i = ldap_set_option(this->con, LDAP_OPT_PROTOCOL_VERSION, &version); + if (i != LDAP_OPT_SUCCESS) + throw LDAPException("Unable to set protocol version for " + this->name + ": " + ldap_err2string(i)); + + const struct timeval tv = { 0, 0 }; + i = ldap_set_option(this->con, LDAP_OPT_NETWORK_TIMEOUT, &tv); + if (i != LDAP_OPT_SUCCESS) + throw LDAPException("Unable to set timeout for " + this->name + ": " + ldap_err2string(i)); + } + + void Reconnect() + { + /* Only try one connect a minute. It is an expensive blocking operation */ + if (last_connect > Anope::CurTime - 60) + throw LDAPException("Unable to connect to LDAP service " + this->name + ": reconnecting too fast"); + last_connect = Anope::CurTime; + + ldap_unbind_ext(this->con, NULL, NULL); + + Connect(); + } + + void QueueRequest(LDAPRequest *r) + { + this->Lock(); + this->queries.push_back(r); + this->Wakeup(); + this->Unlock(); + } + +public: + typedef std::vector query_queue; + query_queue queries, results; + Mutex process_mutex; /* held when processing requests not in either queue */ + + LDAPService(Module *o, const Anope::string &n, const Anope::string &s, const Anope::string &b, const Anope::string &p) : LDAPProvider(o, n), server(s), admin_binddn(b), admin_pass(p) + { + Connect(); + } + + ~LDAPService() + { + /* At this point the thread has stopped so we don't need to hold process_mutex */ + + this->Lock(); + + for (auto *req : this->queries) + { + /* queries have no results yet */ + req->result = new LDAPResult(); + req->result->type = req->type; + req->result->error = "LDAP Interface is going away"; + if (req->inter) + req->inter->OnError(*req->result); + + delete req; + } + this->queries.clear(); + + for (const auto *req : this->queries) + { + /* even though this may have already finished successfully we return that it didn't */ + req->result->error = "LDAP Interface is going away"; + if (req->inter) + req->inter->OnError(*req->result); + + delete req; + } + + this->Unlock(); + + ldap_unbind_ext(this->con, NULL, NULL); + } + + void BindAsAdmin(LDAPInterface *i) override + { + this->Bind(i, this->admin_binddn, this->admin_pass); + } + + void Bind(LDAPInterface *i, const Anope::string &who, const Anope::string &pass) override + { + auto *b = new LDAPBind(this, i, who, pass); + QueueRequest(b); + } + + void Search(LDAPInterface *i, const Anope::string &base, const Anope::string &filter) override + { + if (i == NULL) + throw LDAPException("No interface"); + + auto *s = new LDAPSearchRequest(this, i, base, filter); + QueueRequest(s); + } + + void Add(LDAPInterface *i, const Anope::string &dn, LDAPMods &attributes) override + { + auto *add = new LDAPAdd(this, i, dn, attributes); + QueueRequest(add); + } + + void Del(LDAPInterface *i, const Anope::string &dn) override + { + auto *del = new LDAPDel(this, i, dn); + QueueRequest(del); + } + + void Modify(LDAPInterface *i, const Anope::string &base, LDAPMods &attributes) override + { + auto *mod = new LDAPModify(this, i, base, attributes); + QueueRequest(mod); + } + +private: + void BuildReply(int res, LDAPRequest *req) + { + LDAPResult *ldap_result = req->result = new LDAPResult(); + req->result->type = req->type; + + if (res != LDAP_SUCCESS) + { + ldap_result->error = ldap_err2string(res); + return; + } + + if (req->message == NULL) + { + return; + } + + /* a search result */ + + for (LDAPMessage *cur = ldap_first_message(this->con, req->message); cur; cur = ldap_next_message(this->con, cur)) + { + LDAPAttributes attributes; + + char *dn = ldap_get_dn(this->con, cur); + if (dn != NULL) + { + attributes["dn"].push_back(dn); + ldap_memfree(dn); + dn = NULL; + } + + BerElement *ber = NULL; + + for (char *attr = ldap_first_attribute(this->con, cur, &ber); attr; attr = ldap_next_attribute(this->con, cur, ber)) + { + berval **vals = ldap_get_values_len(this->con, cur, attr); + int count = ldap_count_values_len(vals); + + std::vector attrs; + for (int j = 0; j < count; ++j) + attrs.push_back(vals[j]->bv_val); + attributes[attr] = attrs; + + ldap_value_free_len(vals); + ldap_memfree(attr); + } + + if (ber != NULL) + ber_free(ber, 0); + + ldap_result->messages.push_back(attributes); + } + } + + void SendRequests() + { + process_mutex.Lock(); + + query_queue q; + this->Lock(); + queries.swap(q); + this->Unlock(); + + if (q.empty()) + { + process_mutex.Unlock(); + return; + } + + for (auto *req : q) + { + int ret = req->run(); + + if (ret == LDAP_SERVER_DOWN || ret == LDAP_TIMEOUT) + { + /* try again */ + try + { + Reconnect(); + } + catch (const LDAPException &) + { + } + + ret = req->run(); + } + + BuildReply(ret, req); + + this->Lock(); + results.push_back(req); + this->Unlock(); + } + + me->Notify(); + + process_mutex.Unlock(); + } + +public: + void Run() override + { + while (!this->GetExitState()) + { + this->Lock(); + /* Queries can be non empty if one is pushed during SendRequests() */ + if (queries.empty()) + this->Wait(); + this->Unlock(); + + SendRequests(); + } + } + + LDAP* GetConnection() + { + return con; + } +}; + +class ModuleLDAP final + : public Module + , public Pipe +{ + std::map LDAPServices; + +public: + + ModuleLDAP(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) + { + me = this; + } + + ~ModuleLDAP() + { + for (std::map::iterator it = this->LDAPServices.begin(); it != this->LDAPServices.end(); ++it) + { + it->second->SetExitState(); + it->second->Wakeup(); + it->second->Join(); + delete it->second; + } + LDAPServices.clear(); + } + + void OnReload(Configuration::Conf *config) override + { + Configuration::Block *conf = config->GetModule(this); + + for (std::map::iterator it = this->LDAPServices.begin(); it != this->LDAPServices.end();) + { + const Anope::string &cname = it->first; + LDAPService *s = it->second; + int i; + + ++it; + + for (i = 0; i < conf->CountBlock("ldap"); ++i) + if (conf->GetBlock("ldap", i)->Get("name", "ldap/main") == cname) + break; + + if (i == conf->CountBlock("ldap")) + { + Log(LOG_NORMAL, "ldap") << "LDAP: Removing server connection " << cname; + + s->SetExitState(); + s->Wakeup(); + s->Join(); + delete s; + this->LDAPServices.erase(cname); + } + } + + for (int i = 0; i < conf->CountBlock("ldap"); ++i) + { + Configuration::Block *ldap = conf->GetBlock("ldap", i); + + const Anope::string &connname = ldap->Get("name", "ldap/main"); + + if (this->LDAPServices.find(connname) == this->LDAPServices.end()) + { + const Anope::string &server = ldap->Get("server", "127.0.0.1"); + const Anope::string &admin_binddn = ldap->Get("admin_binddn"); + const Anope::string &admin_password = ldap->Get("admin_password"); + + try + { + auto *ss = new LDAPService(this, connname, server, admin_binddn, admin_password); + ss->Start(); + this->LDAPServices.emplace(connname, ss); + + Log(LOG_NORMAL, "ldap") << "LDAP: Successfully initialized server " << connname << " (" << server << ")"; + } + catch (const LDAPException &ex) + { + Log(LOG_NORMAL, "ldap") << "LDAP: " << ex.GetReason(); + } + } + } + } + + void OnModuleUnload(User *, Module *m) override + { + for (std::map::iterator it = this->LDAPServices.begin(); it != this->LDAPServices.end(); ++it) + { + LDAPService *s = it->second; + + s->process_mutex.Lock(); + s->Lock(); + + for (unsigned int i = s->queries.size(); i > 0; --i) + { + LDAPRequest *req = s->queries[i - 1]; + LDAPInterface *li = req->inter; + + if (li && li->owner == m) + { + s->queries.erase(s->queries.begin() + i - 1); + delete req; + } + } + for (unsigned int i = s->results.size(); i > 0; --i) + { + LDAPRequest *req = s->results[i - 1]; + LDAPInterface *li = req->inter; + + if (li && li->owner == m) + { + s->results.erase(s->results.begin() + i - 1); + delete req; + } + } + + s->Unlock(); + s->process_mutex.Unlock(); + } + } + + void OnNotify() override + { + for (std::map::iterator it = this->LDAPServices.begin(); it != this->LDAPServices.end(); ++it) + { + LDAPService *s = it->second; + + LDAPService::query_queue results; + s->Lock(); + results.swap(s->results); + s->Unlock(); + + for (const auto *req : results) + { + LDAPInterface *li = req->inter; + LDAPResult *r = req->result; + + if (li != NULL) + { + if (!r->getError().empty()) + { + Log(this) << "Error running LDAP query: " << r->getError(); + li->OnError(*r); + } + else + li->OnResult(*r); + } + + delete req; + } + } + } +}; + +int LDAPBind::run() +{ + berval cred; + cred.bv_val = strdup(pass.c_str()); + cred.bv_len = pass.length(); + + int i = ldap_sasl_bind_s(service->GetConnection(), LDAP_STR(who), LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL); + + free(cred.bv_val); + + return i; +} + +int LDAPSearchRequest::run() +{ + return ldap_search_ext_s(service->GetConnection(), LDAP_STR(base), LDAP_SCOPE_SUBTREE, LDAP_STR(filter), NULL, 0, NULL, NULL, LDAP_TIME(tv), 0, &message); +} + +int LDAPAdd::run() +{ + LDAPMod **mods = LDAPService::BuildMods(attributes); + int i = ldap_add_ext_s(service->GetConnection(), LDAP_STR(dn), mods, NULL, NULL); + LDAPService::FreeMods(mods); + return i; +} + +int LDAPDel::run() +{ + return ldap_delete_ext_s(service->GetConnection(), LDAP_STR(dn), NULL, NULL); +} + +int LDAPModify::run() +{ + LDAPMod **mods = LDAPService::BuildMods(attributes); + int i = ldap_modify_ext_s(service->GetConnection(), LDAP_STR(base), mods, NULL, NULL); + LDAPService::FreeMods(mods); + return i; +} + +MODULE_INIT(ModuleLDAP) diff --git a/modules/extra/ldap_authentication.cpp b/modules/extra/ldap_authentication.cpp new file mode 100644 index 000000000..c78d2ae25 --- /dev/null +++ b/modules/extra/ldap_authentication.cpp @@ -0,0 +1,308 @@ +/* + * + * (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"); + + if (!email_attribute.empty()) + /* Don't complain to users about how they need to update their email, we will do it for them */ + config->GetModule("nickserv")->Set("forceemail", "false"); + } + + 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); + } +}; + +MODULE_INIT(ModuleLDAPAuthentication) diff --git a/modules/extra/ldap_oper.cpp b/modules/extra/ldap_oper.cpp new file mode 100644 index 000000000..62749f957 --- /dev/null +++ b/modules/extra/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/extra/m_ldap.cpp b/modules/extra/m_ldap.cpp deleted file mode 100644 index 31e712662..000000000 --- a/modules/extra/m_ldap.cpp +++ /dev/null @@ -1,702 +0,0 @@ -/* - * - * (C) 2011-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - * - * Based on the original code of Epona by Lara. - * Based on the original code of Services by Andy Church. - */ - -/* RequiredLibraries: ldap_r|ldap,lber */ - -#include "module.h" -#include "modules/ldap.h" - -#ifdef _WIN32 -# include -# include -# include -# define LDAP_OPT_SUCCESS LDAP_SUCCESS -# define LDAP_OPT_NETWORK_TIMEOUT LDAP_OPT_SEND_TIMEOUT -# define LDAP_STR(X) const_cast((X).c_str()) -# define LDAP_SASL_SIMPLE static_cast(0) -# define LDAP_TIME(X) reinterpret_cast(&(X)) -# define ldap_first_message ldap_first_entry -# define ldap_next_message ldap_next_entry -# define ldap_unbind_ext(LDAP, UNUSED1, UNUSED2) ldap_unbind(LDAP) -# pragma comment(lib, "Wldap32.lib") -# pragma comment(lib, "Wininet.lib") -#else -# include -# define LDAP_STR(X) ((X).c_str()) -# define LDAP_TIME(X) (&(X)) -#endif - -#if defined LDAP_API_FEATURE_X_OPENLDAP_REENTRANT && !LDAP_API_FEATURE_X_OPENLDAP_REENTRANT -# error Anope requires OpenLDAP to be built as reentrant. -#endif - - -class LDAPService; -static Pipe *me; - -class LDAPRequest -{ -public: - LDAPService *service; - LDAPInterface *inter; - LDAPMessage *message = nullptr; /* message returned by ldap_ */ - LDAPResult *result = nullptr; /* final result */ - struct timeval tv; - QueryType type = QUERY_UNKNOWN; - - LDAPRequest(LDAPService *s, LDAPInterface *i) - : service(s) - , inter(i) - { - tv.tv_sec = 0; - tv.tv_usec = 100000; - } - - virtual ~LDAPRequest() - { - delete result; - if (inter != NULL) - inter->OnDelete(); - if (message != NULL) - ldap_msgfree(message); - } - - virtual int run() = 0; -}; - -class LDAPBind final - : public LDAPRequest -{ - Anope::string who, pass; - -public: - LDAPBind(LDAPService *s, LDAPInterface *i, const Anope::string &w, const Anope::string &p) - : LDAPRequest(s, i) - , who(w) - , pass(p) - { - type = QUERY_BIND; - } - - int run() override; -}; - -class LDAPSearchRequest final - : public LDAPRequest -{ - Anope::string base; - Anope::string filter; - -public: - LDAPSearchRequest(LDAPService *s, LDAPInterface *i, const Anope::string &b, const Anope::string &f) - : LDAPRequest(s, i) - , base(b) - , filter(f) - { - type = QUERY_SEARCH; - } - - int run() override; -}; - -class LDAPAdd final - : public LDAPRequest -{ - Anope::string dn; - LDAPMods attributes; - -public: - LDAPAdd(LDAPService *s, LDAPInterface *i, const Anope::string &d, const LDAPMods &attr) - : LDAPRequest(s, i) - , dn(d) - , attributes(attr) - { - type = QUERY_ADD; - } - - int run() override; -}; - -class LDAPDel final - : public LDAPRequest -{ - Anope::string dn; - -public: - LDAPDel(LDAPService *s, LDAPInterface *i, const Anope::string &d) - : LDAPRequest(s, i) - , dn(d) - { - type = QUERY_DELETE; - } - - int run() override; -}; - -class LDAPModify final - : public LDAPRequest -{ - Anope::string base; - LDAPMods attributes; - -public: - LDAPModify(LDAPService *s, LDAPInterface *i, const Anope::string &b, const LDAPMods &attr) - : LDAPRequest(s, i) - , base(b) - , attributes(attr) - { - type = QUERY_MODIFY; - } - - int run() override; -}; - -class LDAPService final - : public LDAPProvider - , public Thread - , public Condition -{ - Anope::string server; - Anope::string admin_binddn; - Anope::string admin_pass; - - LDAP *con; - - time_t last_connect = 0; - -public: - static LDAPMod **BuildMods(const LDAPMods &attributes) - { - LDAPMod **mods = new LDAPMod*[attributes.size() + 1]; - memset(mods, 0, sizeof(LDAPMod*) * (attributes.size() + 1)); - for (unsigned x = 0; x < attributes.size(); ++x) - { - const LDAPModification &l = attributes[x]; - mods[x] = new LDAPMod(); - - if (l.op == LDAPModification::LDAP_ADD) - mods[x]->mod_op = LDAP_MOD_ADD; - else if (l.op == LDAPModification::LDAP_DEL) - mods[x]->mod_op = LDAP_MOD_DELETE; - else if (l.op == LDAPModification::LDAP_REPLACE) - mods[x]->mod_op = LDAP_MOD_REPLACE; - else if (l.op != 0) - throw LDAPException("Unknown LDAP operation"); - mods[x]->mod_type = strdup(l.name.c_str()); - mods[x]->mod_values = new char*[l.values.size() + 1]; - memset(mods[x]->mod_values, 0, sizeof(char *) * (l.values.size() + 1)); - for (unsigned j = 0, c = 0; j < l.values.size(); ++j) - if (!l.values[j].empty()) - mods[x]->mod_values[c++] = strdup(l.values[j].c_str()); - } - return mods; - } - - static void FreeMods(LDAPMod **mods) - { - for (int i = 0; mods[i] != NULL; ++i) - { - free(mods[i]->mod_type); - for (int j = 0; mods[i]->mod_values[j] != NULL; ++j) - free(mods[i]->mod_values[j]); - delete [] mods[i]->mod_values; - } - delete [] mods; - } - -private: -#ifdef _WIN32 - // Windows LDAP does not implement this so we need to do it. - int ldap_initialize(LDAP** ldap, const char* url) - { - URL_COMPONENTS urlComponents; - memset(&urlComponents, 0, sizeof(urlComponents)); - urlComponents.dwStructSize = sizeof(urlComponents); - - urlComponents.lpszScheme = new char[8]; - urlComponents.dwSchemeLength = 8; - - urlComponents.lpszHostName = new char[1024]; - urlComponents.dwHostNameLength = 1024; - - if (!InternetCrackUrlA(url, 0, 0, &urlComponents)) - { - delete[] urlComponents.lpszScheme; - delete[] urlComponents.lpszHostName; - return LDAP_CONNECT_ERROR; // Malformed url. - } - - unsigned long port = 389; // Default plaintext port. - bool secure = false; // LDAP defaults to plaintext. - if (urlComponents.dwSchemeLength > 0) - { - const Anope::string scheme(urlComponents.lpszScheme); - if (scheme.equals_ci("ldaps")) - { - port = 636; // Default encrypted port. - secure = true; - } - else if (!scheme.equals_ci("ldap")) - { - delete[] urlComponents.lpszScheme; - delete[] urlComponents.lpszHostName; - return LDAP_CONNECT_ERROR; // Invalid protocol. - } - } - - if (urlComponents.nPort > 0) - { - port = urlComponents.nPort; - } - - *ldap = ldap_sslinit(urlComponents.lpszHostName, port, secure); - delete[] urlComponents.lpszScheme; - delete[] urlComponents.lpszHostName; - if (!*ldap) - { - return LdapGetLastError(); // Something went wrong, find out what. - } - - // We're connected to the LDAP server! - return LDAP_SUCCESS; - } -#endif - - void Connect() - { - int i = ldap_initialize(&this->con, this->server.c_str()); - if (i != LDAP_SUCCESS) - throw LDAPException("Unable to connect to LDAP service " + this->name + ": " + ldap_err2string(i)); - - const int version = LDAP_VERSION3; - i = ldap_set_option(this->con, LDAP_OPT_PROTOCOL_VERSION, &version); - if (i != LDAP_OPT_SUCCESS) - throw LDAPException("Unable to set protocol version for " + this->name + ": " + ldap_err2string(i)); - - const struct timeval tv = { 0, 0 }; - i = ldap_set_option(this->con, LDAP_OPT_NETWORK_TIMEOUT, &tv); - if (i != LDAP_OPT_SUCCESS) - throw LDAPException("Unable to set timeout for " + this->name + ": " + ldap_err2string(i)); - } - - void Reconnect() - { - /* Only try one connect a minute. It is an expensive blocking operation */ - if (last_connect > Anope::CurTime - 60) - throw LDAPException("Unable to connect to LDAP service " + this->name + ": reconnecting too fast"); - last_connect = Anope::CurTime; - - ldap_unbind_ext(this->con, NULL, NULL); - - Connect(); - } - - void QueueRequest(LDAPRequest *r) - { - this->Lock(); - this->queries.push_back(r); - this->Wakeup(); - this->Unlock(); - } - -public: - typedef std::vector query_queue; - query_queue queries, results; - Mutex process_mutex; /* held when processing requests not in either queue */ - - LDAPService(Module *o, const Anope::string &n, const Anope::string &s, const Anope::string &b, const Anope::string &p) : LDAPProvider(o, n), server(s), admin_binddn(b), admin_pass(p) - { - Connect(); - } - - ~LDAPService() - { - /* At this point the thread has stopped so we don't need to hold process_mutex */ - - this->Lock(); - - for (auto *req : this->queries) - { - /* queries have no results yet */ - req->result = new LDAPResult(); - req->result->type = req->type; - req->result->error = "LDAP Interface is going away"; - if (req->inter) - req->inter->OnError(*req->result); - - delete req; - } - this->queries.clear(); - - for (const auto *req : this->queries) - { - /* even though this may have already finished successfully we return that it didn't */ - req->result->error = "LDAP Interface is going away"; - if (req->inter) - req->inter->OnError(*req->result); - - delete req; - } - - this->Unlock(); - - ldap_unbind_ext(this->con, NULL, NULL); - } - - void BindAsAdmin(LDAPInterface *i) override - { - this->Bind(i, this->admin_binddn, this->admin_pass); - } - - void Bind(LDAPInterface *i, const Anope::string &who, const Anope::string &pass) override - { - auto *b = new LDAPBind(this, i, who, pass); - QueueRequest(b); - } - - void Search(LDAPInterface *i, const Anope::string &base, const Anope::string &filter) override - { - if (i == NULL) - throw LDAPException("No interface"); - - auto *s = new LDAPSearchRequest(this, i, base, filter); - QueueRequest(s); - } - - void Add(LDAPInterface *i, const Anope::string &dn, LDAPMods &attributes) override - { - auto *add = new LDAPAdd(this, i, dn, attributes); - QueueRequest(add); - } - - void Del(LDAPInterface *i, const Anope::string &dn) override - { - auto *del = new LDAPDel(this, i, dn); - QueueRequest(del); - } - - void Modify(LDAPInterface *i, const Anope::string &base, LDAPMods &attributes) override - { - auto *mod = new LDAPModify(this, i, base, attributes); - QueueRequest(mod); - } - -private: - void BuildReply(int res, LDAPRequest *req) - { - LDAPResult *ldap_result = req->result = new LDAPResult(); - req->result->type = req->type; - - if (res != LDAP_SUCCESS) - { - ldap_result->error = ldap_err2string(res); - return; - } - - if (req->message == NULL) - { - return; - } - - /* a search result */ - - for (LDAPMessage *cur = ldap_first_message(this->con, req->message); cur; cur = ldap_next_message(this->con, cur)) - { - LDAPAttributes attributes; - - char *dn = ldap_get_dn(this->con, cur); - if (dn != NULL) - { - attributes["dn"].push_back(dn); - ldap_memfree(dn); - dn = NULL; - } - - BerElement *ber = NULL; - - for (char *attr = ldap_first_attribute(this->con, cur, &ber); attr; attr = ldap_next_attribute(this->con, cur, ber)) - { - berval **vals = ldap_get_values_len(this->con, cur, attr); - int count = ldap_count_values_len(vals); - - std::vector attrs; - for (int j = 0; j < count; ++j) - attrs.push_back(vals[j]->bv_val); - attributes[attr] = attrs; - - ldap_value_free_len(vals); - ldap_memfree(attr); - } - - if (ber != NULL) - ber_free(ber, 0); - - ldap_result->messages.push_back(attributes); - } - } - - void SendRequests() - { - process_mutex.Lock(); - - query_queue q; - this->Lock(); - queries.swap(q); - this->Unlock(); - - if (q.empty()) - { - process_mutex.Unlock(); - return; - } - - for (auto *req : q) - { - int ret = req->run(); - - if (ret == LDAP_SERVER_DOWN || ret == LDAP_TIMEOUT) - { - /* try again */ - try - { - Reconnect(); - } - catch (const LDAPException &) - { - } - - ret = req->run(); - } - - BuildReply(ret, req); - - this->Lock(); - results.push_back(req); - this->Unlock(); - } - - me->Notify(); - - process_mutex.Unlock(); - } - -public: - void Run() override - { - while (!this->GetExitState()) - { - this->Lock(); - /* Queries can be non empty if one is pushed during SendRequests() */ - if (queries.empty()) - this->Wait(); - this->Unlock(); - - SendRequests(); - } - } - - LDAP* GetConnection() - { - return con; - } -}; - -class ModuleLDAP final - : public Module - , public Pipe -{ - std::map LDAPServices; - -public: - - ModuleLDAP(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) - { - me = this; - } - - ~ModuleLDAP() - { - for (std::map::iterator it = this->LDAPServices.begin(); it != this->LDAPServices.end(); ++it) - { - it->second->SetExitState(); - it->second->Wakeup(); - it->second->Join(); - delete it->second; - } - LDAPServices.clear(); - } - - void OnReload(Configuration::Conf *config) override - { - Configuration::Block *conf = config->GetModule(this); - - for (std::map::iterator it = this->LDAPServices.begin(); it != this->LDAPServices.end();) - { - const Anope::string &cname = it->first; - LDAPService *s = it->second; - int i; - - ++it; - - for (i = 0; i < conf->CountBlock("ldap"); ++i) - if (conf->GetBlock("ldap", i)->Get("name", "ldap/main") == cname) - break; - - if (i == conf->CountBlock("ldap")) - { - Log(LOG_NORMAL, "ldap") << "LDAP: Removing server connection " << cname; - - s->SetExitState(); - s->Wakeup(); - s->Join(); - delete s; - this->LDAPServices.erase(cname); - } - } - - for (int i = 0; i < conf->CountBlock("ldap"); ++i) - { - Configuration::Block *ldap = conf->GetBlock("ldap", i); - - const Anope::string &connname = ldap->Get("name", "ldap/main"); - - if (this->LDAPServices.find(connname) == this->LDAPServices.end()) - { - const Anope::string &server = ldap->Get("server", "127.0.0.1"); - const Anope::string &admin_binddn = ldap->Get("admin_binddn"); - const Anope::string &admin_password = ldap->Get("admin_password"); - - try - { - auto *ss = new LDAPService(this, connname, server, admin_binddn, admin_password); - ss->Start(); - this->LDAPServices.emplace(connname, ss); - - Log(LOG_NORMAL, "ldap") << "LDAP: Successfully initialized server " << connname << " (" << server << ")"; - } - catch (const LDAPException &ex) - { - Log(LOG_NORMAL, "ldap") << "LDAP: " << ex.GetReason(); - } - } - } - } - - void OnModuleUnload(User *, Module *m) override - { - for (std::map::iterator it = this->LDAPServices.begin(); it != this->LDAPServices.end(); ++it) - { - LDAPService *s = it->second; - - s->process_mutex.Lock(); - s->Lock(); - - for (unsigned int i = s->queries.size(); i > 0; --i) - { - LDAPRequest *req = s->queries[i - 1]; - LDAPInterface *li = req->inter; - - if (li && li->owner == m) - { - s->queries.erase(s->queries.begin() + i - 1); - delete req; - } - } - for (unsigned int i = s->results.size(); i > 0; --i) - { - LDAPRequest *req = s->results[i - 1]; - LDAPInterface *li = req->inter; - - if (li && li->owner == m) - { - s->results.erase(s->results.begin() + i - 1); - delete req; - } - } - - s->Unlock(); - s->process_mutex.Unlock(); - } - } - - void OnNotify() override - { - for (std::map::iterator it = this->LDAPServices.begin(); it != this->LDAPServices.end(); ++it) - { - LDAPService *s = it->second; - - LDAPService::query_queue results; - s->Lock(); - results.swap(s->results); - s->Unlock(); - - for (const auto *req : results) - { - LDAPInterface *li = req->inter; - LDAPResult *r = req->result; - - if (li != NULL) - { - if (!r->getError().empty()) - { - Log(this) << "Error running LDAP query: " << r->getError(); - li->OnError(*r); - } - else - li->OnResult(*r); - } - - delete req; - } - } - } -}; - -int LDAPBind::run() -{ - berval cred; - cred.bv_val = strdup(pass.c_str()); - cred.bv_len = pass.length(); - - int i = ldap_sasl_bind_s(service->GetConnection(), LDAP_STR(who), LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL); - - free(cred.bv_val); - - return i; -} - -int LDAPSearchRequest::run() -{ - return ldap_search_ext_s(service->GetConnection(), LDAP_STR(base), LDAP_SCOPE_SUBTREE, LDAP_STR(filter), NULL, 0, NULL, NULL, LDAP_TIME(tv), 0, &message); -} - -int LDAPAdd::run() -{ - LDAPMod **mods = LDAPService::BuildMods(attributes); - int i = ldap_add_ext_s(service->GetConnection(), LDAP_STR(dn), mods, NULL, NULL); - LDAPService::FreeMods(mods); - return i; -} - -int LDAPDel::run() -{ - return ldap_delete_ext_s(service->GetConnection(), LDAP_STR(dn), NULL, NULL); -} - -int LDAPModify::run() -{ - LDAPMod **mods = LDAPService::BuildMods(attributes); - int i = ldap_modify_ext_s(service->GetConnection(), LDAP_STR(base), mods, NULL, NULL); - LDAPService::FreeMods(mods); - return i; -} - -MODULE_INIT(ModuleLDAP) diff --git a/modules/extra/m_ldap_authentication.cpp b/modules/extra/m_ldap_authentication.cpp deleted file mode 100644 index 1b8318d7e..000000000 --- a/modules/extra/m_ldap_authentication.cpp +++ /dev/null @@ -1,308 +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) << "m_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) << "m_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("m_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, "m_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"); - - if (!email_attribute.empty()) - /* Don't complain to users about how they need to update their email, we will do it for them */ - config->GetModule("nickserv")->Set("forceemail", "false"); - } - - 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); - } -}; - -MODULE_INIT(ModuleLDAPAuthentication) diff --git a/modules/extra/m_ldap_oper.cpp b/modules/extra/m_ldap_oper.cpp deleted file mode 100644 index 866ed0110..000000000 --- a/modules/extra/m_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 m_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/m_mysql.cpp b/modules/extra/m_mysql.cpp deleted file mode 100644 index d4be01f88..000000000 --- a/modules/extra/m_mysql.cpp +++ /dev/null @@ -1,569 +0,0 @@ -/* - * - * (C) 2010-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -/* RequiredLibraries: mysqlclient */ -/* RequiredWindowsLibraries: libmysql */ - -#include "module.h" -#include "modules/sql.h" - -#ifdef WIN32 -# include -#else -# include -#endif - -using namespace SQL; - -/** Non blocking threaded MySQL API, based loosely from InspIRCd's m_mysql.cpp - * - * This module spawns a single thread that is used to execute blocking MySQL queries. - * When a module requests a query to be executed it is added to a list for the thread - * (which never stops looping and sleeping) to pick up and execute, the result of which - * is inserted in to another queue to be picked up by the main thread. The main thread - * uses Pipe to become notified through the socket engine when there are results waiting - * to be sent back to the modules requesting the query - */ - -class MySQLService; - -/** A query request - */ -struct QueryRequest final -{ - /* The connection to the database */ - MySQLService *service; - /* The interface to use once we have the result to send the data back */ - Interface *sqlinterface; - /* The actual query */ - Query query; - - QueryRequest(MySQLService *s, Interface *i, const Query &q) : service(s), sqlinterface(i), query(q) { } -}; - -/** A query result */ -struct QueryResult final -{ - /* The interface to send the data back on */ - Interface *sqlinterface; - /* The result */ - Result result; - - QueryResult(Interface *i, Result &r) : sqlinterface(i), result(r) { } -}; - -/** A MySQL result - */ -class MySQLResult final - : public Result -{ - MYSQL_RES *res = nullptr; - -public: - MySQLResult(unsigned int i, const Query &q, const Anope::string &fq, MYSQL_RES *r) : Result(i, q, fq), res(r) - { - unsigned num_fields = res ? mysql_num_fields(res) : 0; - - /* It is not thread safe to log anything here using Log(this->owner) now :( */ - - if (!num_fields) - return; - - for (MYSQL_ROW row; (row = mysql_fetch_row(res));) - { - MYSQL_FIELD *fields = mysql_fetch_fields(res); - - if (fields) - { - std::map items; - - for (unsigned field_count = 0; field_count < num_fields; ++field_count) - { - Anope::string column = (fields[field_count].name ? fields[field_count].name : ""); - Anope::string data = (row[field_count] ? row[field_count] : ""); - - items[column] = data; - } - - this->entries.push_back(items); - } - } - } - - MySQLResult(const Query &q, const Anope::string &fq, const Anope::string &err) : Result(0, q, fq, err) - { - } - - ~MySQLResult() - { - if (this->res) - mysql_free_result(this->res); - } -}; - -/** A MySQL connection, there can be multiple - */ -class MySQLService final - : public Provider -{ - std::map > active_schema; - - Anope::string database; - Anope::string server; - Anope::string user; - Anope::string password; - unsigned int port; - - MYSQL *sql = nullptr; - - /** Escape a query. - * Note the mutex must be held! - */ - Anope::string Escape(const Anope::string &query); - -public: - /* Locked by the SQL thread when a query is pending on this database, - * prevents us from deleting a connection while a query is executing - * in the thread - */ - Mutex Lock; - - MySQLService(Module *o, const Anope::string &n, const Anope::string &d, const Anope::string &s, const Anope::string &u, const Anope::string &p, unsigned int po); - - ~MySQLService(); - - void Run(Interface *i, const Query &query) override; - - Result RunQuery(const Query &query) override; - - std::vector CreateTable(const Anope::string &table, const Data &data) override; - - Query BuildInsert(const Anope::string &table, unsigned int id, Data &data) override; - - Query GetTables(const Anope::string &prefix) override; - - void Connect(); - - bool CheckConnection(); - - Anope::string BuildQuery(const Query &q); - - Anope::string FromUnixtime(time_t) override; -}; - -/** The SQL thread used to execute queries - */ -class DispatcherThread final - : public Thread - , public Condition -{ -public: - DispatcherThread() : Thread() { } - - void Run() override; -}; - -class ModuleSQL; -static ModuleSQL *me; - -class ModuleSQL final - : public Module - , public Pipe -{ - /* SQL connections */ - std::map MySQLServices; -public: - /* Pending query requests */ - std::deque QueryRequests; - /* Pending finished requests with results */ - std::deque FinishedRequests; - /* The thread used to execute queries */ - DispatcherThread *DThread; - - ModuleSQL(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) - { - me = this; - - - DThread = new DispatcherThread(); - DThread->Start(); - } - - ~ModuleSQL() - { - for (std::map::iterator it = this->MySQLServices.begin(); it != this->MySQLServices.end(); ++it) - delete it->second; - MySQLServices.clear(); - - DThread->SetExitState(); - DThread->Wakeup(); - DThread->Join(); - delete DThread; - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *config = conf->GetModule(this); - - for (std::map::iterator it = this->MySQLServices.begin(); it != this->MySQLServices.end();) - { - const Anope::string &cname = it->first; - MySQLService *s = it->second; - int i; - - ++it; - - for (i = 0; i < config->CountBlock("mysql"); ++i) - if (config->GetBlock("mysql", i)->Get("name", "mysql/main") == cname) - break; - - if (i == config->CountBlock("mysql")) - { - Log(LOG_NORMAL, "mysql") << "MySQL: Removing server connection " << cname; - - delete s; - this->MySQLServices.erase(cname); - } - } - - for (int i = 0; i < config->CountBlock("mysql"); ++i) - { - Configuration::Block *block = config->GetBlock("mysql", i); - const Anope::string &connname = block->Get("name", "mysql/main"); - - if (this->MySQLServices.find(connname) == this->MySQLServices.end()) - { - const Anope::string &database = block->Get("database", "anope"); - const Anope::string &server = block->Get("server", "127.0.0.1"); - const Anope::string &user = block->Get("username", "anope"); - const Anope::string &password = block->Get("password"); - unsigned int port = block->Get("port", "3306"); - - try - { - auto *ss = new MySQLService(this, connname, database, server, user, password, port); - this->MySQLServices.emplace(connname, ss); - - Log(LOG_NORMAL, "mysql") << "MySQL: Successfully connected to server " << connname << " (" << server << ")"; - } - catch (const SQL::Exception &ex) - { - Log(LOG_NORMAL, "mysql") << "MySQL: " << ex.GetReason(); - } - } - } - } - - void OnModuleUnload(User *, Module *m) override - { - this->DThread->Lock(); - - for (unsigned i = this->QueryRequests.size(); i > 0; --i) - { - QueryRequest &r = this->QueryRequests[i - 1]; - - if (r.sqlinterface && r.sqlinterface->owner == m) - { - if (i == 1) - { - r.service->Lock.Lock(); - r.service->Lock.Unlock(); - } - - this->QueryRequests.erase(this->QueryRequests.begin() + i - 1); - } - } - - this->DThread->Unlock(); - - this->OnNotify(); - } - - void OnNotify() override - { - this->DThread->Lock(); - std::deque finishedRequests = this->FinishedRequests; - this->FinishedRequests.clear(); - this->DThread->Unlock(); - - for (const auto &qr : finishedRequests) - { - if (!qr.sqlinterface) - throw SQL::Exception("NULL qr.sqlinterface in MySQLPipe::OnNotify() ?"); - - if (qr.result.GetError().empty()) - qr.sqlinterface->OnResult(qr.result); - else - qr.sqlinterface->OnError(qr.result); - } - } -}; - -MySQLService::MySQLService(Module *o, const Anope::string &n, const Anope::string &d, const Anope::string &s, const Anope::string &u, const Anope::string &p, unsigned int po) - : Provider(o, n) - , database(d) - , server(s) - , user(u) - , password(p) - , port(po) -{ - Connect(); -} - -MySQLService::~MySQLService() -{ - me->DThread->Lock(); - this->Lock.Lock(); - mysql_close(this->sql); - this->sql = NULL; - - for (unsigned i = me->QueryRequests.size(); i > 0; --i) - { - QueryRequest &r = me->QueryRequests[i - 1]; - - if (r.service == this) - { - if (r.sqlinterface) - r.sqlinterface->OnError(Result(0, r.query, "SQL Interface is going away")); - me->QueryRequests.erase(me->QueryRequests.begin() + i - 1); - } - } - this->Lock.Unlock(); - me->DThread->Unlock(); -} - -void MySQLService::Run(Interface *i, const Query &query) -{ - me->DThread->Lock(); - me->QueryRequests.push_back(QueryRequest(this, i, query)); - me->DThread->Unlock(); - me->DThread->Wakeup(); -} - -Result MySQLService::RunQuery(const Query &query) -{ - this->Lock.Lock(); - - Anope::string real_query = this->BuildQuery(query); - - if (this->CheckConnection() && !mysql_real_query(this->sql, real_query.c_str(), real_query.length())) - { - MYSQL_RES *res = mysql_store_result(this->sql); - unsigned int id = mysql_insert_id(this->sql); - - /* because we enabled CLIENT_MULTI_RESULTS in our options - * a multiple statement or a procedure call can return - * multiple result sets. - * we must process them all before the next query. - */ - - while (!mysql_next_result(this->sql)) - mysql_free_result(mysql_store_result(this->sql)); - - this->Lock.Unlock(); - return MySQLResult(id, query, real_query, res); - } - else - { - Anope::string error = mysql_error(this->sql); - this->Lock.Unlock(); - return MySQLResult(query, real_query, error); - } -} - -std::vector MySQLService::CreateTable(const Anope::string &table, const Data &data) -{ - std::vector queries; - std::set &known_cols = this->active_schema[table]; - - if (known_cols.empty()) - { - Log(LOG_DEBUG) << "m_mysql: Fetching columns for " << table; - - Result columns = this->RunQuery("SHOW COLUMNS FROM `" + table + "`"); - for (int i = 0; i < columns.Rows(); ++i) - { - const Anope::string &column = columns.Get(i, "Field"); - - Log(LOG_DEBUG) << "m_mysql: Column #" << i << " for " << table << ": " << column; - known_cols.insert(column); - } - } - - if (known_cols.empty()) - { - Anope::string query_text = "CREATE TABLE `" + table + "` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT," - " `timestamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"; - for (const auto &[column, _] : data.data) - { - known_cols.insert(column); - - query_text += ", `" + column + "` "; - if (data.GetType(column) == Serialize::Data::DT_INT) - query_text += "int(11)"; - else - query_text += "text"; - } - query_text += ", PRIMARY KEY (`id`), KEY `timestamp_idx` (`timestamp`))"; - queries.push_back(query_text); - } - else - { - for (const auto &[column, _] : data.data) - { - if (known_cols.count(column) > 0) - continue; - - known_cols.insert(column); - - Anope::string query_text = "ALTER TABLE `" + table + "` ADD `" + column + "` "; - if (data.GetType(column) == Serialize::Data::DT_INT) - query_text += "int(11)"; - else - query_text += "text"; - - queries.push_back(query_text); - } - } - - return queries; -} - -Query MySQLService::BuildInsert(const Anope::string &table, unsigned int id, Data &data) -{ - /* Empty columns not present in the data set */ - for (const auto &known_col : this->active_schema[table]) - { - if (known_col != "id" && known_col != "timestamp" && data.data.count(known_col) == 0) - data[known_col] << ""; - } - - Anope::string query_text = "INSERT INTO `" + table + "` (`id`"; - - for (const auto &[field, _] : data.data) - query_text += ",`" + field + "`"; - query_text += ") VALUES (" + stringify(id); - for (const auto &[field, _] : data.data) - query_text += ",@" + field + "@"; - query_text += ") ON DUPLICATE KEY UPDATE "; - for (const auto &[field, _] : data.data) - query_text += "`" + field + "`=VALUES(`" + field + "`),"; - query_text.erase(query_text.end() - 1); - - Query query(query_text); - for (auto &[field, value] : data.data) - { - Anope::string buf; - *value >> buf; - - bool escape = true; - if (buf.empty()) - { - buf = "NULL"; - escape = false; - } - - query.SetValue(field, buf, escape); - } - - return query; -} - -Query MySQLService::GetTables(const Anope::string &prefix) -{ - return Query("SHOW TABLES LIKE '" + prefix + "%';"); -} - -void MySQLService::Connect() -{ - this->sql = mysql_init(this->sql); - - const unsigned int timeout = 1; - mysql_options(this->sql, MYSQL_OPT_CONNECT_TIMEOUT, reinterpret_cast(&timeout)); - - bool connect = mysql_real_connect(this->sql, this->server.c_str(), this->user.c_str(), this->password.c_str(), this->database.c_str(), this->port, NULL, CLIENT_MULTI_RESULTS); - - if (!connect) - throw SQL::Exception("Unable to connect to MySQL service " + this->name + ": " + mysql_error(this->sql)); - - Log(LOG_DEBUG) << "Successfully connected to MySQL service " << this->name << " at " << this->server << ":" << this->port; -} - - -bool MySQLService::CheckConnection() -{ - if (!this->sql || mysql_ping(this->sql)) - { - try - { - this->Connect(); - } - catch (const SQL::Exception &) - { - return false; - } - } - - return true; -} - -Anope::string MySQLService::Escape(const Anope::string &query) -{ - std::vector buffer(query.length() * 2 + 1); - mysql_real_escape_string(this->sql, &buffer[0], query.c_str(), query.length()); - return &buffer[0]; -} - -Anope::string MySQLService::BuildQuery(const Query &q) -{ - Anope::string real_query = q.query; - - for (const auto &[name, value] : q.parameters) - real_query = real_query.replace_all_cs("@" + name + "@", (value.escape ? ("'" + this->Escape(value.data) + "'") : value.data)); - - return real_query; -} - -Anope::string MySQLService::FromUnixtime(time_t t) -{ - return "FROM_UNIXTIME(" + stringify(t) + ")"; -} - -void DispatcherThread::Run() -{ - this->Lock(); - - while (!this->GetExitState()) - { - if (!me->QueryRequests.empty()) - { - QueryRequest &r = me->QueryRequests.front(); - this->Unlock(); - - Result sresult = r.service->RunQuery(r.query); - - this->Lock(); - if (!me->QueryRequests.empty() && me->QueryRequests.front().query == r.query) - { - if (r.sqlinterface) - me->FinishedRequests.push_back(QueryResult(r.sqlinterface, sresult)); - me->QueryRequests.pop_front(); - } - } - else - { - if (!me->FinishedRequests.empty()) - me->Notify(); - this->Wait(); - } - } - - this->Unlock(); -} - -MODULE_INIT(ModuleSQL) diff --git a/modules/extra/m_regex_pcre2.cpp b/modules/extra/m_regex_pcre2.cpp deleted file mode 100644 index ef2786823..000000000 --- a/modules/extra/m_regex_pcre2.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/* - * - * (C) 2012-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -/* RequiredLibraries: pcre2-8 */ -/* RequiredWindowsLibraries: pcre2-8 */ - -#include "module.h" - -#define PCRE2_CODE_UNIT_WIDTH 8 -#include - -class PCRERegex final - : public Regex -{ - pcre2_code *regex; - -public: - PCRERegex(const Anope::string &expr) : Regex(expr) - { - int errcode; - PCRE2_SIZE erroffset; - this->regex = pcre2_compile(reinterpret_cast(expr.c_str()), expr.length(), PCRE2_CASELESS, &errcode, &erroffset, NULL); - - if (!this->regex) - { - PCRE2_UCHAR error[128]; - pcre2_get_error_message(errcode, error, sizeof error); - throw RegexException("Error in regex " + expr + " at offset " + stringify(erroffset) + ": " + reinterpret_cast(error)); - } - } - - ~PCRERegex() - { - pcre2_code_free(this->regex); - } - - bool Matches(const Anope::string &str) - { - pcre2_match_data *unused = pcre2_match_data_create_from_pattern(this->regex, NULL); - int result = pcre2_match(regex, reinterpret_cast(str.c_str()), str.length(), 0, 0, unused, NULL); - pcre2_match_data_free(unused); - return result >= 0; - } -}; - -class PCRERegexProvider final - : public RegexProvider -{ -public: - PCRERegexProvider(Module *creator) : RegexProvider(creator, "regex/pcre") { } - - Regex *Compile(const Anope::string &expression) override - { - return new PCRERegex(expression); - } -}; - -class ModuleRegexPCRE final - : public Module -{ - PCRERegexProvider pcre_regex_provider; - -public: - ModuleRegexPCRE(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), - pcre_regex_provider(this) - { - this->SetPermanent(true); - } - - ~ModuleRegexPCRE() - { - for (auto *xlm : XLineManager::XLineManagers) - { - for (auto *x : xlm->GetList()) - { - if (x->regex && dynamic_cast(x->regex)) - { - delete x->regex; - x->regex = NULL; - } - } - } - } -}; - -MODULE_INIT(ModuleRegexPCRE) diff --git a/modules/extra/m_regex_posix.cpp b/modules/extra/m_regex_posix.cpp deleted file mode 100644 index cef4486f9..000000000 --- a/modules/extra/m_regex_posix.cpp +++ /dev/null @@ -1,82 +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 -#include - -class POSIXRegex final - : public Regex -{ - regex_t regbuf; - -public: - POSIXRegex(const Anope::string &expr) : Regex(expr) - { - int err = regcomp(&this->regbuf, expr.c_str(), REG_EXTENDED | REG_NOSUB | REG_ICASE); - if (err) - { - char buf[BUFSIZE]; - regerror(err, &this->regbuf, buf, sizeof(buf)); - regfree(&this->regbuf); - throw RegexException("Error in regex " + expr + ": " + buf); - } - } - - ~POSIXRegex() - { - regfree(&this->regbuf); - } - - bool Matches(const Anope::string &str) - { - return regexec(&this->regbuf, str.c_str(), 0, NULL, 0) == 0; - } -}; - -class POSIXRegexProvider final - : public RegexProvider -{ -public: - POSIXRegexProvider(Module *creator) : RegexProvider(creator, "regex/posix") { } - - Regex *Compile(const Anope::string &expression) override - { - return new POSIXRegex(expression); - } -}; - -class ModuleRegexPOSIX final - : public Module -{ - POSIXRegexProvider posix_regex_provider; - -public: - ModuleRegexPOSIX(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), - posix_regex_provider(this) - { - this->SetPermanent(true); - } - - ~ModuleRegexPOSIX() - { - for (auto *xlm : XLineManager::XLineManagers) - { - for (auto *x : xlm->GetList()) - { - if (x->regex && dynamic_cast(x->regex)) - { - delete x->regex; - x->regex = NULL; - } - } - } - } -}; - -MODULE_INIT(ModuleRegexPOSIX) diff --git a/modules/extra/m_regex_tre.cpp b/modules/extra/m_regex_tre.cpp deleted file mode 100644 index 18485507a..000000000 --- a/modules/extra/m_regex_tre.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/* - * - * (C) 2012-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -/* RequiredLibraries: tre */ - -#include "module.h" -#include - -class TRERegex final - : public Regex -{ - regex_t regbuf; - -public: - TRERegex(const Anope::string &expr) : Regex(expr) - { - int err = regcomp(&this->regbuf, expr.c_str(), REG_EXTENDED | REG_NOSUB); - if (err) - { - char buf[BUFSIZE]; - regerror(err, &this->regbuf, buf, sizeof(buf)); - regfree(&this->regbuf); - throw RegexException("Error in regex " + expr + ": " + buf); - } - } - - ~TRERegex() - { - regfree(&this->regbuf); - } - - bool Matches(const Anope::string &str) - { - return regexec(&this->regbuf, str.c_str(), 0, NULL, 0) == 0; - } -}; - -class TRERegexProvider final - : public RegexProvider -{ -public: - TRERegexProvider(Module *creator) : RegexProvider(creator, "regex/tre") { } - - Regex *Compile(const Anope::string &expression) override - { - return new TRERegex(expression); - } -}; - -class ModuleRegexTRE final - : public Module -{ - TRERegexProvider tre_regex_provider; - -public: - ModuleRegexTRE(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), - tre_regex_provider(this) - { - this->SetPermanent(true); - } - - ~ModuleRegexTRE() - { - for (auto *xlm : XLineManager::XLineManagers) - { - for (auto *x : xlm->GetList()) - { - if (x->regex && dynamic_cast(x->regex)) - { - delete x->regex; - x->regex = NULL; - } - } - } - } -}; - -MODULE_INIT(ModuleRegexTRE) diff --git a/modules/extra/m_sql_authentication.cpp b/modules/extra/m_sql_authentication.cpp deleted file mode 100644 index 8d60f12ea..000000000 --- a/modules/extra/m_sql_authentication.cpp +++ /dev/null @@ -1,150 +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) << "m_sql_authentication: Unsuccessful authentication for " << req->GetAccount(); - delete this; - return; - } - - Log(LOG_DEBUG) << "m_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) << "m_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) << "m_sql_authentication: Checking authentication for " << req->GetAccount(); - } -}; - -MODULE_INIT(ModuleSQLAuthentication) diff --git a/modules/extra/m_sql_log.cpp b/modules/extra/m_sql_log.cpp deleted file mode 100644 index 7335a5abf..000000000 --- a/modules/extra/m_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/m_sql_oper.cpp b/modules/extra/m_sql_oper.cpp deleted file mode 100644 index 7642bab5f..000000000 --- a/modules/extra/m_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) << "m_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) << "m_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) << "m_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) << "m_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) << "m_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) << "m_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) << "m_sql_oper: Checking authentication for " << u->Account()->display; - } -}; - -MODULE_INIT(ModuleSQLOper) diff --git a/modules/extra/m_sqlite.cpp b/modules/extra/m_sqlite.cpp deleted file mode 100644 index f11c0bf09..000000000 --- a/modules/extra/m_sqlite.cpp +++ /dev/null @@ -1,340 +0,0 @@ -/* - * - * (C) 2011-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -/* RequiredLibraries: sqlite3 */ -/* RequiredWindowsLibraries: sqlite3 */ - -#include "module.h" -#include "modules/sql.h" -#include - -using namespace SQL; - -/* SQLite3 API, based from InspIRCd */ - -/** A SQLite result - */ -class SQLiteResult final - : public Result -{ -public: - SQLiteResult(unsigned int i, const Query &q, const Anope::string &fq) : Result(i, q, fq) - { - } - - SQLiteResult(const Query &q, const Anope::string &fq, const Anope::string &err) : Result(0, q, fq, err) - { - } - - void AddRow(const std::map &data) - { - this->entries.push_back(data); - } -}; - -/** A SQLite database, there can be multiple - */ -class SQLiteService final - : public Provider -{ - std::map > active_schema; - - Anope::string database; - - sqlite3 *sql = nullptr; - - Anope::string Escape(const Anope::string &query); - -public: - SQLiteService(Module *o, const Anope::string &n, const Anope::string &d); - - ~SQLiteService(); - - void Run(Interface *i, const Query &query) override; - - Result RunQuery(const Query &query) override; - - std::vector CreateTable(const Anope::string &table, const Data &data) override; - - Query BuildInsert(const Anope::string &table, unsigned int id, Data &data) override; - - Query GetTables(const Anope::string &prefix) override; - - Anope::string BuildQuery(const Query &q); - - Anope::string FromUnixtime(time_t) override; -}; - -class ModuleSQLite final - : public Module -{ - /* SQL connections */ - std::map SQLiteServices; -public: - ModuleSQLite(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) - { - } - - ~ModuleSQLite() - { - for (std::map::iterator it = this->SQLiteServices.begin(); it != this->SQLiteServices.end(); ++it) - delete it->second; - SQLiteServices.clear(); - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *config = conf->GetModule(this); - - for (std::map::iterator it = this->SQLiteServices.begin(); it != this->SQLiteServices.end();) - { - const Anope::string &cname = it->first; - SQLiteService *s = it->second; - int i, num; - ++it; - - for (i = 0, num = config->CountBlock("sqlite"); i < num; ++i) - if (config->GetBlock("sqlite", i)->Get("name", "sqlite/main") == cname) - break; - - if (i == num) - { - Log(LOG_NORMAL, "sqlite") << "SQLite: Removing server connection " << cname; - - delete s; - this->SQLiteServices.erase(cname); - } - } - - for (int i = 0; i < config->CountBlock("sqlite"); ++i) - { - Configuration::Block *block = config->GetBlock("sqlite", i); - Anope::string connname = block->Get("name", "sqlite/main"); - - if (this->SQLiteServices.find(connname) == this->SQLiteServices.end()) - { - Anope::string database = Anope::DataDir + "/" + block->Get("database", "anope"); - - try - { - auto *ss = new SQLiteService(this, connname, database); - this->SQLiteServices[connname] = ss; - - Log(LOG_NORMAL, "sqlite") << "SQLite: Successfully added database " << database; - } - catch (const SQL::Exception &ex) - { - Log(LOG_NORMAL, "sqlite") << "SQLite: " << ex.GetReason(); - } - } - } - } -}; - -SQLiteService::SQLiteService(Module *o, const Anope::string &n, const Anope::string &d) -: Provider(o, n), database(d) -{ - int db = sqlite3_open_v2(database.c_str(), &this->sql, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0); - if (db != SQLITE_OK) - { - Anope::string exstr = "Unable to open SQLite database " + database; - if (this->sql) - { - exstr += ": "; - exstr += sqlite3_errmsg(this->sql); - sqlite3_close(this->sql); - } - throw SQL::Exception(exstr); - } -} - -SQLiteService::~SQLiteService() -{ - sqlite3_interrupt(this->sql); - sqlite3_close(this->sql); -} - -void SQLiteService::Run(Interface *i, const Query &query) -{ - Result res = this->RunQuery(query); - if (!res.GetError().empty()) - i->OnError(res); - else - i->OnResult(res); -} - -Result SQLiteService::RunQuery(const Query &query) -{ - Anope::string real_query = this->BuildQuery(query); - sqlite3_stmt *stmt; - int err = sqlite3_prepare_v2(this->sql, real_query.c_str(), real_query.length(), &stmt, NULL); - if (err != SQLITE_OK) - return SQLiteResult(query, real_query, sqlite3_errmsg(this->sql)); - - std::vector columns; - int cols = sqlite3_column_count(stmt); - columns.resize(cols); - for (int i = 0; i < cols; ++i) - columns[i] = sqlite3_column_name(stmt, i); - - SQLiteResult result(0, query, real_query); - - while ((err = sqlite3_step(stmt)) == SQLITE_ROW) - { - std::map items; - for (int i = 0; i < cols; ++i) - { - const char *data = reinterpret_cast(sqlite3_column_text(stmt, i)); - if (data && *data) - items[columns[i]] = data; - } - result.AddRow(items); - } - - result.id = sqlite3_last_insert_rowid(this->sql); - - sqlite3_finalize(stmt); - - if (err != SQLITE_DONE) - return SQLiteResult(query, real_query, sqlite3_errmsg(this->sql)); - - return std::move(result); -} - -std::vector SQLiteService::CreateTable(const Anope::string &table, const Data &data) -{ - std::vector queries; - std::set &known_cols = this->active_schema[table]; - - if (known_cols.empty()) - { - Log(LOG_DEBUG) << "m_sqlite: Fetching columns for " << table; - - Result columns = this->RunQuery("PRAGMA table_info(" + table + ")"); - for (int i = 0; i < columns.Rows(); ++i) - { - const Anope::string &column = columns.Get(i, "name"); - - Log(LOG_DEBUG) << "m_sqlite: Column #" << i << " for " << table << ": " << column; - known_cols.insert(column); - } - } - - if (known_cols.empty()) - { - Anope::string query_text = "CREATE TABLE `" + table + "` (`id` INTEGER PRIMARY KEY, `timestamp` timestamp DEFAULT CURRENT_TIMESTAMP"; - - for (const auto &[column, _] : data.data) - { - known_cols.insert(column); - - query_text += ", `" + column + "` "; - if (data.GetType(column) == Serialize::Data::DT_INT) - query_text += "int(11)"; - else - query_text += "text"; - } - - query_text += ")"; - - queries.push_back(query_text); - - query_text = "CREATE UNIQUE INDEX `" + table + "_id_idx` ON `" + table + "` (`id`)"; - queries.push_back(query_text); - - query_text = "CREATE INDEX `" + table + "_timestamp_idx` ON `" + table + "` (`timestamp`)"; - queries.push_back(query_text); - - query_text = "CREATE TRIGGER `" + table + "_trigger` AFTER UPDATE ON `" + table + "` FOR EACH ROW BEGIN UPDATE `" + table + "` SET `timestamp` = CURRENT_TIMESTAMP WHERE `id` = `old.id`; end;"; - queries.push_back(query_text); - } - else - { - for (const auto &[column, _] : data.data) - { - if (known_cols.count(column) > 0) - continue; - - known_cols.insert(column); - - Anope::string query_text = "ALTER TABLE `" + table + "` ADD `" + column + "` "; - if (data.GetType(column) == Serialize::Data::DT_INT) - query_text += "int(11)"; - else - query_text += "text"; - - queries.push_back(query_text); - } - } - - return queries; -} - -Query SQLiteService::BuildInsert(const Anope::string &table, unsigned int id, Data &data) -{ - /* Empty columns not present in the data set */ - for (const auto &known_col : this->active_schema[table]) - { - if (known_col != "id" && known_col != "timestamp" && data.data.count(known_col) == 0) - data[known_col] << ""; - } - - Anope::string query_text = "REPLACE INTO `" + table + "` ("; - if (id > 0) - query_text += "`id`,"; - for (const auto &[field, _] : data.data) - query_text += "`" + field + "`,"; - query_text.erase(query_text.length() - 1); - query_text += ") VALUES ("; - if (id > 0) - query_text += stringify(id) + ","; - for (const auto &[field, _] : data.data) - query_text += "@" + field + "@,"; - query_text.erase(query_text.length() - 1); - query_text += ")"; - - Query query(query_text); - for (auto &[field, value] : data.data) - { - Anope::string buf; - *value >> buf; - query.SetValue(field, buf); - } - - return query; -} - -Query SQLiteService::GetTables(const Anope::string &prefix) -{ - return Query("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '" + prefix + "%';"); -} - -Anope::string SQLiteService::Escape(const Anope::string &query) -{ - char *e = sqlite3_mprintf("%q", query.c_str()); - Anope::string buffer = e; - sqlite3_free(e); - return buffer; -} - -Anope::string SQLiteService::BuildQuery(const Query &q) -{ - Anope::string real_query = q.query; - - for (const auto &[name, value] : q.parameters) - real_query = real_query.replace_all_cs("@" + name + "@", (value.escape ? ("'" + this->Escape(value.data) + "'") : value.data)); - - return real_query; -} - -Anope::string SQLiteService::FromUnixtime(time_t t) -{ - return "datetime('" + stringify(t) + "', 'unixepoch')"; -} - -MODULE_INIT(ModuleSQLite) diff --git a/modules/extra/m_ssl_gnutls.cpp b/modules/extra/m_ssl_gnutls.cpp deleted file mode 100644 index 28310471a..000000000 --- a/modules/extra/m_ssl_gnutls.cpp +++ /dev/null @@ -1,643 +0,0 @@ -/* - * - * (C) 2014 Attila Molnar - * (C) 2014-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -/* RequiredLibraries: gnutls */ -/* RequiredWindowsLibraries: libgnutls-30 */ - -#include "module.h" -#include "modules/ssl.h" - -#include -#include -#include - -class GnuTLSModule; -static GnuTLSModule *me; - -namespace GnuTLS { class X509CertCredentials; } - -class MySSLService final - : public SSLService -{ -public: - MySSLService(Module *o, const Anope::string &n); - - /** Initialize a socket to use SSL - * @param s The socket - */ - void Init(Socket *s) override; -}; - -class SSLSocketIO final - : public SocketIO -{ -public: - gnutls_session_t sess = nullptr; - GnuTLS::X509CertCredentials* mycreds; - - /** Constructor - */ - SSLSocketIO(); - - /** Really receive something from the buffer - * @param s The socket - * @param buf The buf to read to - * @param sz How much to read - * @return Number of bytes received - */ - int Recv(Socket *s, char *buf, size_t sz) override; - - /** Write something to the socket - * @param s The socket - * @param buf The data to write - * @param size The length of the data - */ - int Send(Socket *s, const char *buf, size_t sz) override; - - /** Accept a connection from a socket - * @param s The socket - * @return The new socket - */ - ClientSocket *Accept(ListenSocket *s) override; - - /** Finished accepting a connection from a socket - * @param s The socket - * @return SF_ACCEPTED if accepted, SF_ACCEPTING if still in process, SF_DEAD on error - */ - SocketFlag FinishAccept(ClientSocket *cs) override; - - /** Connect the socket - * @param s THe socket - * @param target IP to connect to - * @param port to connect to - */ - void Connect(ConnectionSocket *s, const Anope::string &target, int port) override; - - /** Called to potentially finish a pending connection - * @param s The socket - * @return SF_CONNECTED on success, SF_CONNECTING if still pending, and SF_DEAD on error. - */ - SocketFlag FinishConnect(ConnectionSocket *s) override; - - /** Called when the socket is destructing - */ - void Destroy() override; -}; - -namespace GnuTLS -{ - class Init final - { - public: - Init() { gnutls_global_init(); } - ~Init() { gnutls_global_deinit(); } - }; - - /** Used to create a gnutls_datum_t* from an Anope::string - */ - class Datum final - { - gnutls_datum_t datum; - - public: - Datum(const Anope::string &dat) - { - datum.data = reinterpret_cast(const_cast(dat.data())); - datum.size = static_cast(dat.length()); - } - - const gnutls_datum_t *get() const { return &datum; } - }; - - class DHParams final - { - gnutls_dh_params_t dh_params = nullptr; - - public: - void Import(const Anope::string &dhstr) - { - if (dh_params != NULL) - { - gnutls_dh_params_deinit(dh_params); - dh_params = NULL; - } - - int ret = gnutls_dh_params_init(&dh_params); - if (ret < 0) - throw ConfigException("Unable to initialize DH parameters"); - - ret = gnutls_dh_params_import_pkcs3(dh_params, Datum(dhstr).get(), GNUTLS_X509_FMT_PEM); - if (ret < 0) - { - gnutls_dh_params_deinit(dh_params); - dh_params = NULL; - throw ConfigException("Unable to import DH parameters"); - } - } - - ~DHParams() - { - if (dh_params) - gnutls_dh_params_deinit(dh_params); - } - - gnutls_dh_params_t get() const { return dh_params; } - }; - - class X509Key final - { - /** Ensure that the key is deinited in case the constructor of X509Key throws - */ - class RAIIKey final - { - public: - gnutls_x509_privkey_t key; - - RAIIKey() - { - int ret = gnutls_x509_privkey_init(&key); - if (ret < 0) - throw ConfigException("gnutls_x509_privkey_init() failed"); - } - - ~RAIIKey() - { - gnutls_x509_privkey_deinit(key); - } - } key; - - public: - /** Import */ - X509Key(const Anope::string &keystr) - { - int ret = gnutls_x509_privkey_import(key.key, Datum(keystr).get(), GNUTLS_X509_FMT_PEM); - if (ret < 0) - throw ConfigException("Error loading private key: " + Anope::string(gnutls_strerror(ret))); - } - - gnutls_x509_privkey_t& get() { return key.key; } - }; - - class X509CertList final - { - std::vector certs; - - public: - /** Import */ - X509CertList(const Anope::string &certstr) - { - unsigned int certcount = 3; - certs.resize(certcount); - Datum datum(certstr); - - int ret = gnutls_x509_crt_list_import(raw(), &certcount, datum.get(), GNUTLS_X509_FMT_PEM, GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED); - if (ret == GNUTLS_E_SHORT_MEMORY_BUFFER) - { - // the buffer wasn't big enough to hold all certs but gnutls changed certcount to the number of available certs, - // try again with a bigger buffer - certs.resize(certcount); - ret = gnutls_x509_crt_list_import(raw(), &certcount, datum.get(), GNUTLS_X509_FMT_PEM, GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED); - } - - if (ret < 0) - throw ConfigException("Unable to load certificates" + Anope::string(gnutls_strerror(ret))); - - // Resize the vector to the actual number of certs because we rely on its size being correct - // when deallocating the certs - certs.resize(certcount); - } - - ~X509CertList() - { - for (std::vector::iterator i = certs.begin(); i != certs.end(); ++i) - gnutls_x509_crt_deinit(*i); - } - - gnutls_x509_crt_t* raw() { return &certs[0]; } - unsigned int size() const { return certs.size(); } - }; - - class X509CertCredentials final - { - unsigned int refcount = 0; - gnutls_certificate_credentials_t cred; - DHParams dh; - - static Anope::string LoadFile(const Anope::string &filename) - { - std::ifstream ifs(filename.c_str()); - const Anope::string ret((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - return ret; - } - - static int cert_callback(gnutls_session_t sess, const gnutls_datum_t* req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t* sign_algos, int sign_algos_length, gnutls_retr2_st* st); - - public: - X509CertList certs; - X509Key key; - - X509CertCredentials(const Anope::string &certfile, const Anope::string &keyfile) - : certs(LoadFile(certfile)), key(LoadFile(keyfile)) - { - if (gnutls_certificate_allocate_credentials(&cred) < 0) - throw ConfigException("Cannot allocate certificate credentials"); - - int ret = gnutls_certificate_set_x509_key(cred, certs.raw(), certs.size(), key.get()); - if (ret < 0) - { - gnutls_certificate_free_credentials(cred); - throw ConfigException("Unable to set cert/key pair"); - } - - gnutls_certificate_set_retrieve_function(cred, cert_callback); - } - - ~X509CertCredentials() - { - gnutls_certificate_free_credentials(cred); - } - - void SetupSession(gnutls_session_t sess) - { - gnutls_credentials_set(sess, GNUTLS_CRD_CERTIFICATE, cred); - gnutls_set_default_priority(sess); - } - - void SetDH(const Anope::string &dhfile) - { - const Anope::string dhdata = LoadFile(dhfile); - dh.Import(dhdata); - gnutls_certificate_set_dh_params(cred, dh.get()); - } - - bool HasDH() const - { - return (dh.get() != NULL); - } - - void incrref() { refcount++; } - void decrref() { if (!--refcount) delete this; } - }; -} - -class GnuTLSModule final - : public Module -{ - GnuTLS::Init libinit; - -public: - GnuTLS::X509CertCredentials *cred = nullptr; - MySSLService service; - - GnuTLSModule(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), service(this, "ssl") - { - me = this; - this->SetPermanent(true); - } - - ~GnuTLSModule() - { - for (std::map::const_iterator it = SocketEngine::Sockets.begin(), it_end = SocketEngine::Sockets.end(); it != it_end;) - { - Socket *s = it->second; - ++it; - - if (dynamic_cast(s->io)) - delete s; - } - - if (cred) - cred->decrref(); - } - - static void CheckFile(const Anope::string &filename) - { - if (!Anope::IsFile(filename.c_str())) - { - Log() << "File does not exist: " << filename; - throw ConfigException("Error loading certificate/private key"); - } - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *config = conf->GetModule(this); - - const Anope::string certfile = config->Get("cert", "data/fullchain.pem"); - const Anope::string keyfile = config->Get("key", "data/privkey.pem"); - const Anope::string dhfile = config->Get("dh", "data/dhparams.pem"); - - CheckFile(certfile); - CheckFile(keyfile); - - GnuTLS::X509CertCredentials *newcred = new GnuTLS::X509CertCredentials(certfile, keyfile); - - // DH params is not mandatory - if (Anope::IsFile(dhfile.c_str())) - { - try - { - newcred->SetDH(dhfile); - } - catch (...) - { - delete newcred; - throw; - } - Log(LOG_DEBUG) << "m_ssl_gnutls: Successfully loaded DH parameters from " << dhfile; - } - - if (cred) - cred->decrref(); - cred = newcred; - cred->incrref(); - - Log(LOG_DEBUG) << "m_ssl_gnutls: Successfully loaded certificate " << certfile << " and private key " << keyfile; - } - - void OnPreServerConnect() override - { - Configuration::Block *config = Config->GetBlock("uplink", Anope::CurrentUplink); - - if (config->Get("ssl")) - { - this->service.Init(UplinkSock); - } - } -}; - -MySSLService::MySSLService(Module *o, const Anope::string &n) : SSLService(o, n) -{ -} - -void MySSLService::Init(Socket *s) -{ - if (s->io != &NormalSocketIO) - throw CoreException("Socket initializing SSL twice"); - - s->io = new SSLSocketIO(); -} - -int SSLSocketIO::Recv(Socket *s, char *buf, size_t sz) -{ - int ret = gnutls_record_recv(this->sess, buf, sz); - - if (ret > 0) - TotalRead += ret; - else if (ret < 0) - { - switch (ret) - { - case GNUTLS_E_AGAIN: - case GNUTLS_E_INTERRUPTED: - SocketEngine::SetLastError(EAGAIN); - break; - default: - if (s == UplinkSock) - { - // Log and fake an errno because this is a fatal error on the uplink socket - Log() << "SSL error: " << gnutls_strerror(ret); - } - SocketEngine::SetLastError(ECONNRESET); - } - } - - return ret; -} - -int SSLSocketIO::Send(Socket *s, const char *buf, size_t sz) -{ - int ret = gnutls_record_send(this->sess, buf, sz); - - if (ret > 0) - TotalWritten += ret; - else - { - switch (ret) - { - case 0: - case GNUTLS_E_AGAIN: - case GNUTLS_E_INTERRUPTED: - SocketEngine::SetLastError(EAGAIN); - break; - default: - if (s == UplinkSock) - { - // Log and fake an errno because this is a fatal error on the uplink socket - Log() << "SSL error: " << gnutls_strerror(ret); - } - SocketEngine::SetLastError(ECONNRESET); - } - } - - return ret; -} - -ClientSocket *SSLSocketIO::Accept(ListenSocket *s) -{ - if (s->io == &NormalSocketIO) - throw SocketException("Attempting to accept on uninitialized socket with SSL"); - - sockaddrs conaddr; - - socklen_t size = sizeof(conaddr); - int newsock = accept(s->GetFD(), &conaddr.sa, &size); - -#ifndef INVALID_SOCKET - const int INVALID_SOCKET = -1; -#endif - - if (newsock < 0 || newsock == INVALID_SOCKET) - throw SocketException("Unable to accept connection: " + Anope::LastError()); - - ClientSocket *newsocket = s->OnAccept(newsock, conaddr); - me->service.Init(newsocket); - SSLSocketIO *io = anope_dynamic_static_cast(newsocket->io); - - if (gnutls_init(&io->sess, GNUTLS_SERVER) != GNUTLS_E_SUCCESS) - throw SocketException("Unable to initialize SSL socket"); - - me->cred->SetupSession(io->sess); - gnutls_transport_set_ptr(io->sess, reinterpret_cast(newsock)); - - newsocket->flags[SF_ACCEPTING] = true; - this->FinishAccept(newsocket); - - return newsocket; -} - -SocketFlag SSLSocketIO::FinishAccept(ClientSocket *cs) -{ - if (cs->io == &NormalSocketIO) - throw SocketException("Attempting to finish connect uninitialized socket with SSL"); - else if (cs->flags[SF_ACCEPTED]) - return SF_ACCEPTED; - else if (!cs->flags[SF_ACCEPTING]) - throw SocketException("SSLSocketIO::FinishAccept called for a socket not accepted nor accepting?"); - - SSLSocketIO *io = anope_dynamic_static_cast(cs->io); - - int ret = gnutls_handshake(io->sess); - if (ret < 0) - { - if (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) - { - // gnutls_handshake() wants to read or write again; - // if gnutls_record_get_direction() returns 0 it wants to read, otherwise it wants to write. - if (gnutls_record_get_direction(io->sess) == 0) - { - SocketEngine::Change(cs, false, SF_WRITABLE); - SocketEngine::Change(cs, true, SF_READABLE); - } - else - { - SocketEngine::Change(cs, true, SF_WRITABLE); - SocketEngine::Change(cs, false, SF_READABLE); - } - return SF_ACCEPTING; - } - else - { - cs->OnError(Anope::string(gnutls_strerror(ret))); - cs->flags[SF_DEAD] = true; - cs->flags[SF_ACCEPTING] = false; - return SF_DEAD; - } - } - else - { - cs->flags[SF_ACCEPTED] = true; - cs->flags[SF_ACCEPTING] = false; - SocketEngine::Change(cs, false, SF_WRITABLE); - SocketEngine::Change(cs, true, SF_READABLE); - cs->OnAccept(); - return SF_ACCEPTED; - } -} - -void SSLSocketIO::Connect(ConnectionSocket *s, const Anope::string &target, int port) -{ - if (s->io == &NormalSocketIO) - throw SocketException("Attempting to connect uninitialized socket with SSL"); - - s->flags[SF_CONNECTING] = s->flags[SF_CONNECTED] = false; - - s->conaddr.pton(s->GetFamily(), target, port); - int c = connect(s->GetFD(), &s->conaddr.sa, s->conaddr.size()); - if (c == -1) - { - if (Anope::LastErrorCode() != EINPROGRESS) - { - s->OnError(Anope::LastError()); - s->flags[SF_DEAD] = true; - return; - } - else - { - SocketEngine::Change(s, true, SF_WRITABLE); - s->flags[SF_CONNECTING] = true; - return; - } - } - else - { - s->flags[SF_CONNECTING] = true; - this->FinishConnect(s); - } -} - -SocketFlag SSLSocketIO::FinishConnect(ConnectionSocket *s) -{ - if (s->io == &NormalSocketIO) - throw SocketException("Attempting to finish connect uninitialized socket with SSL"); - else if (s->flags[SF_CONNECTED]) - return SF_CONNECTED; - else if (!s->flags[SF_CONNECTING]) - throw SocketException("SSLSocketIO::FinishConnect called for a socket not connected nor connecting?"); - - SSLSocketIO *io = anope_dynamic_static_cast(s->io); - - if (io->sess == NULL) - { - if (gnutls_init(&io->sess, GNUTLS_CLIENT) != GNUTLS_E_SUCCESS) - throw SocketException("Unable to initialize SSL socket"); - me->cred->SetupSession(io->sess); - gnutls_transport_set_ptr(io->sess, reinterpret_cast(s->GetFD())); - } - - int ret = gnutls_handshake(io->sess); - if (ret < 0) - { - if (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) - { - // gnutls_handshake() wants to read or write again; - // if gnutls_record_get_direction() returns 0 it wants to read, otherwise it wants to write. - if (gnutls_record_get_direction(io->sess) == 0) - { - SocketEngine::Change(s, false, SF_WRITABLE); - SocketEngine::Change(s, true, SF_READABLE); - } - else - { - SocketEngine::Change(s, true, SF_WRITABLE); - SocketEngine::Change(s, false, SF_READABLE); - } - - return SF_CONNECTING; - } - else - { - s->OnError(Anope::string(gnutls_strerror(ret))); - s->flags[SF_CONNECTING] = false; - s->flags[SF_DEAD] = true; - return SF_DEAD; - } - } - else - { - s->flags[SF_CONNECTING] = false; - s->flags[SF_CONNECTED] = true; - SocketEngine::Change(s, false, SF_WRITABLE); - SocketEngine::Change(s, true, SF_READABLE); - s->OnConnect(); - return SF_CONNECTED; - } -} - -void SSLSocketIO::Destroy() -{ - if (this->sess) - { - gnutls_bye(this->sess, GNUTLS_SHUT_WR); - gnutls_deinit(this->sess); - } - - mycreds->decrref(); - - delete this; -} - -SSLSocketIO::SSLSocketIO() : mycreds(me->cred) -{ - mycreds->incrref(); -} - -int GnuTLS::X509CertCredentials::cert_callback(gnutls_session_t sess, const gnutls_datum_t* req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t* sign_algos, int sign_algos_length, gnutls_retr2_st* st) -{ - st->cert_type = GNUTLS_CRT_X509; - st->key_type = GNUTLS_PRIVKEY_X509; - st->ncerts = me->cred->certs.size(); - st->cert.x509 = me->cred->certs.raw(); - st->key.x509 = me->cred->key.get(); - st->deinit_all = 0; - - return 0; -} - -MODULE_INIT(GnuTLSModule) diff --git a/modules/extra/m_ssl_openssl.cpp b/modules/extra/m_ssl_openssl.cpp deleted file mode 100644 index 8d4aca9ba..000000000 --- a/modules/extra/m_ssl_openssl.cpp +++ /dev/null @@ -1,445 +0,0 @@ -/* - * - * (C) 2010-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -/* RequiredLibraries: ssl,crypto */ -/* RequiredWindowsLibraries: libssl,libcrypto */ - -#include "module.h" -#include "modules/ssl.h" - -#define OPENSSL_API_COMPAT 0x10100000L -#define OPENSSL_NO_DEPRECATED - -#include -#include -#include -#include -#include - -static SSL_CTX *server_ctx, *client_ctx; - -class MySSLService final - : public SSLService -{ -public: - MySSLService(Module *o, const Anope::string &n); - - /** Initialize a socket to use SSL - * @param s The socket - */ - void Init(Socket *s) override; -}; - -class SSLSocketIO final - : public SocketIO -{ -public: - /* The SSL socket for this socket */ - SSL *sslsock; - - /** Constructor - */ - SSLSocketIO(); - - /** Really receive something from the buffer - * @param s The socket - * @param buf The buf to read to - * @param sz How much to read - * @return Number of bytes received - */ - int Recv(Socket *s, char *buf, size_t sz) override; - - /** Write something to the socket - * @param s The socket - * @param buf The data to write - * @param size The length of the data - */ - int Send(Socket *s, const char *buf, size_t sz) override; - - /** Accept a connection from a socket - * @param s The socket - * @return The new socket - */ - ClientSocket *Accept(ListenSocket *s) override; - - /** Finished accepting a connection from a socket - * @param s The socket - * @return SF_ACCEPTED if accepted, SF_ACCEPTING if still in process, SF_DEAD on error - */ - SocketFlag FinishAccept(ClientSocket *cs) override; - - /** Connect the socket - * @param s THe socket - * @param target IP to connect to - * @param port to connect to - */ - void Connect(ConnectionSocket *s, const Anope::string &target, int port) override; - - /** Called to potentially finish a pending connection - * @param s The socket - * @return SF_CONNECTED on success, SF_CONNECTING if still pending, and SF_DEAD on error. - */ - SocketFlag FinishConnect(ConnectionSocket *s) override; - - /** Called when the socket is destructing - */ - void Destroy() override; -}; - -class SSLModule; -static SSLModule *me; -class SSLModule final - : public Module -{ - Anope::string certfile, keyfile; - -public: - MySSLService service; - - SSLModule(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), service(this, "ssl") - { - me = this; - - this->SetPermanent(true); - - OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS, nullptr); - - client_ctx = SSL_CTX_new(TLS_client_method()); - server_ctx = SSL_CTX_new(TLS_server_method()); - - if (!client_ctx || !server_ctx) - throw ModuleException("Error initializing SSL CTX"); - - long opts = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION | SSL_OP_CIPHER_SERVER_PREFERENCE; - SSL_CTX_set_options(client_ctx, opts); - SSL_CTX_set_options(server_ctx, opts); - - SSL_CTX_set_mode(client_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); - SSL_CTX_set_mode(server_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); - - Anope::string context_name = "Anope"; - SSL_CTX_set_session_id_context(client_ctx, reinterpret_cast(context_name.c_str()), context_name.length()); - SSL_CTX_set_session_id_context(server_ctx, reinterpret_cast(context_name.c_str()), context_name.length()); - } - - ~SSLModule() - { - for (std::map::const_iterator it = SocketEngine::Sockets.begin(), it_end = SocketEngine::Sockets.end(); it != it_end;) - { - Socket *s = it->second; - ++it; - - if (dynamic_cast(s->io)) - delete s; - } - - SSL_CTX_free(client_ctx); - SSL_CTX_free(server_ctx); - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *config = conf->GetModule(this); - - this->certfile = config->Get("cert", "data/fullchain.pem"); - this->keyfile = config->Get("key", "data/privkey.pem"); - - if (Anope::IsFile(this->certfile.c_str())) - { - if (!SSL_CTX_use_certificate_chain_file(client_ctx, this->certfile.c_str()) || !SSL_CTX_use_certificate_chain_file(server_ctx, this->certfile.c_str())) - throw ConfigException("Error loading certificate"); - else - Log(LOG_DEBUG) << "m_ssl_openssl: Successfully loaded certificate " << this->certfile; - } - else - Log() << "Unable to open certificate " << this->certfile; - - if (Anope::IsFile(this->keyfile.c_str())) - { - if (!SSL_CTX_use_PrivateKey_file(client_ctx, this->keyfile.c_str(), SSL_FILETYPE_PEM) || !SSL_CTX_use_PrivateKey_file(server_ctx, this->keyfile.c_str(), SSL_FILETYPE_PEM)) - throw ConfigException("Error loading private key"); - else - Log(LOG_DEBUG) << "m_ssl_openssl: Successfully loaded private key " << this->keyfile; - } - else - { - if (Anope::IsFile(this->certfile.c_str())) - throw ConfigException("Error loading private key " + this->keyfile + " - file not found"); - else - Log() << "Unable to open private key " << this->keyfile; - } - - // Allow disabling old versions of TLS - if (config->Get("tlsv10", "false")) - { - SSL_CTX_clear_options(client_ctx, SSL_OP_NO_TLSv1); - SSL_CTX_clear_options(server_ctx, SSL_OP_NO_TLSv1); - } - else - { - SSL_CTX_set_options(client_ctx, SSL_OP_NO_TLSv1); - SSL_CTX_set_options(server_ctx, SSL_OP_NO_TLSv1); - } - - if (config->Get("tlsv11", "true")) - { - SSL_CTX_clear_options(client_ctx, SSL_OP_NO_TLSv1_1); - SSL_CTX_clear_options(server_ctx, SSL_OP_NO_TLSv1_1); - } - else - { - SSL_CTX_set_options(client_ctx, SSL_OP_NO_TLSv1_1); - SSL_CTX_set_options(server_ctx, SSL_OP_NO_TLSv1_1); - } - - if (config->Get("tlsv12", "true")) - { - SSL_CTX_clear_options(client_ctx, SSL_OP_NO_TLSv1_2); - SSL_CTX_clear_options(server_ctx, SSL_OP_NO_TLSv1_2); - } - else - { - SSL_CTX_set_options(client_ctx, SSL_OP_NO_TLSv1_2); - SSL_CTX_set_options(server_ctx, SSL_OP_NO_TLSv1_2); - } - } - - void OnPreServerConnect() override - { - Configuration::Block *config = Config->GetBlock("uplink", Anope::CurrentUplink); - - if (config->Get("ssl")) - { - this->service.Init(UplinkSock); - } - } -}; - -MySSLService::MySSLService(Module *o, const Anope::string &n) : SSLService(o, n) -{ -} - -void MySSLService::Init(Socket *s) -{ - if (s->io != &NormalSocketIO) - throw CoreException("Socket initializing SSL twice"); - - s->io = new SSLSocketIO(); -} - -SSLSocketIO::SSLSocketIO() -{ - this->sslsock = NULL; -} - -int SSLSocketIO::Recv(Socket *s, char *buf, size_t sz) -{ - int i = SSL_read(this->sslsock, buf, sz); - if (i > 0) - TotalRead += i; - else if (i < 0) - { - int err = SSL_get_error(this->sslsock, i); - switch (err) - { - case SSL_ERROR_WANT_READ: - case SSL_ERROR_WANT_WRITE: - SocketEngine::SetLastError(EAGAIN); - } - } - - return i; -} - -int SSLSocketIO::Send(Socket *s, const char *buf, size_t sz) -{ - int i = SSL_write(this->sslsock, buf, sz); - if (i > 0) - TotalWritten += i; - else if (i < 0) - { - int err = SSL_get_error(this->sslsock, i); - switch (err) - { - case SSL_ERROR_WANT_READ: - case SSL_ERROR_WANT_WRITE: - SocketEngine::SetLastError(EAGAIN); - } - } - return i; -} - -ClientSocket *SSLSocketIO::Accept(ListenSocket *s) -{ - if (s->io == &NormalSocketIO) - throw SocketException("Attempting to accept on uninitialized socket with SSL"); - - sockaddrs conaddr; - - socklen_t size = sizeof(conaddr); - int newsock = accept(s->GetFD(), &conaddr.sa, &size); - -#ifndef INVALID_SOCKET - const int INVALID_SOCKET = -1; -#endif - - if (newsock < 0 || newsock == INVALID_SOCKET) - throw SocketException("Unable to accept connection: " + Anope::LastError()); - - ClientSocket *newsocket = s->OnAccept(newsock, conaddr); - me->service.Init(newsocket); - SSLSocketIO *io = anope_dynamic_static_cast(newsocket->io); - - io->sslsock = SSL_new(server_ctx); - if (!io->sslsock) - throw SocketException("Unable to initialize SSL socket"); - - SSL_set_accept_state(io->sslsock); - - if (!SSL_set_fd(io->sslsock, newsocket->GetFD())) - throw SocketException("Unable to set SSL fd"); - - newsocket->flags[SF_ACCEPTING] = true; - this->FinishAccept(newsocket); - - return newsocket; -} - -SocketFlag SSLSocketIO::FinishAccept(ClientSocket *cs) -{ - if (cs->io == &NormalSocketIO) - throw SocketException("Attempting to finish connect uninitialized socket with SSL"); - else if (cs->flags[SF_ACCEPTED]) - return SF_ACCEPTED; - else if (!cs->flags[SF_ACCEPTING]) - throw SocketException("SSLSocketIO::FinishAccept called for a socket not accepted nor accepting?"); - - SSLSocketIO *io = anope_dynamic_static_cast(cs->io); - - int ret = SSL_accept(io->sslsock); - if (ret <= 0) - { - int error = SSL_get_error(io->sslsock, ret); - if (ret == -1 && (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE)) - { - SocketEngine::Change(cs, error == SSL_ERROR_WANT_WRITE, SF_WRITABLE); - SocketEngine::Change(cs, error == SSL_ERROR_WANT_READ, SF_READABLE); - return SF_ACCEPTING; - } - else - { - cs->OnError(ERR_error_string(ERR_get_error(), NULL)); - cs->flags[SF_DEAD] = true; - cs->flags[SF_ACCEPTING] = false; - return SF_DEAD; - } - } - else - { - cs->flags[SF_ACCEPTED] = true; - cs->flags[SF_ACCEPTING] = false; - SocketEngine::Change(cs, false, SF_WRITABLE); - SocketEngine::Change(cs, true, SF_READABLE); - cs->OnAccept(); - return SF_ACCEPTED; - } -} - -void SSLSocketIO::Connect(ConnectionSocket *s, const Anope::string &target, int port) -{ - if (s->io == &NormalSocketIO) - throw SocketException("Attempting to connect uninitialized socket with SSL"); - - s->flags[SF_CONNECTING] = s->flags[SF_CONNECTED] = false; - - s->conaddr.pton(s->GetFamily(), target, port); - int c = connect(s->GetFD(), &s->conaddr.sa, s->conaddr.size()); - if (c == -1) - { - if (Anope::LastErrorCode() != EINPROGRESS) - { - s->OnError(Anope::LastError()); - s->flags[SF_DEAD] = true; - return; - } - else - { - SocketEngine::Change(s, true, SF_WRITABLE); - s->flags[SF_CONNECTING] = true; - return; - } - } - else - { - s->flags[SF_CONNECTING] = true; - this->FinishConnect(s); - } -} - -SocketFlag SSLSocketIO::FinishConnect(ConnectionSocket *s) -{ - if (s->io == &NormalSocketIO) - throw SocketException("Attempting to finish connect uninitialized socket with SSL"); - else if (s->flags[SF_CONNECTED]) - return SF_CONNECTED; - else if (!s->flags[SF_CONNECTING]) - throw SocketException("SSLSocketIO::FinishConnect called for a socket not connected nor connecting?"); - - SSLSocketIO *io = anope_dynamic_static_cast(s->io); - - if (io->sslsock == NULL) - { - io->sslsock = SSL_new(client_ctx); - if (!io->sslsock) - throw SocketException("Unable to initialize SSL socket"); - - if (!SSL_set_fd(io->sslsock, s->GetFD())) - throw SocketException("Unable to set SSL fd"); - } - - int ret = SSL_connect(io->sslsock); - if (ret <= 0) - { - int error = SSL_get_error(io->sslsock, ret); - if (ret == -1 && (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE)) - { - SocketEngine::Change(s, error == SSL_ERROR_WANT_WRITE, SF_WRITABLE); - SocketEngine::Change(s, error == SSL_ERROR_WANT_READ, SF_READABLE); - return SF_CONNECTING; - } - else - { - s->OnError(ERR_error_string(ERR_get_error(), NULL)); - s->flags[SF_CONNECTING] = false; - s->flags[SF_DEAD] = true; - return SF_DEAD; - } - } - else - { - s->flags[SF_CONNECTING] = false; - s->flags[SF_CONNECTED] = true; - SocketEngine::Change(s, false, SF_WRITABLE); - SocketEngine::Change(s, true, SF_READABLE); - s->OnConnect(); - return SF_CONNECTED; - } -} - -void SSLSocketIO::Destroy() -{ - if (this->sslsock) - { - SSL_shutdown(this->sslsock); - SSL_free(this->sslsock); - } - - delete this; -} - -MODULE_INIT(SSLModule) diff --git a/modules/extra/mysql.cpp b/modules/extra/mysql.cpp new file mode 100644 index 000000000..f1baff7b9 --- /dev/null +++ b/modules/extra/mysql.cpp @@ -0,0 +1,569 @@ +/* + * + * (C) 2010-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +/* RequiredLibraries: mysqlclient */ +/* RequiredWindowsLibraries: libmysql */ + +#include "module.h" +#include "modules/sql.h" + +#ifdef WIN32 +# include +#else +# include +#endif + +using namespace SQL; + +/** Non blocking threaded MySQL API, based loosely from InspIRCd's m_mysql.cpp + * + * This module spawns a single thread that is used to execute blocking MySQL queries. + * When a module requests a query to be executed it is added to a list for the thread + * (which never stops looping and sleeping) to pick up and execute, the result of which + * is inserted in to another queue to be picked up by the main thread. The main thread + * uses Pipe to become notified through the socket engine when there are results waiting + * to be sent back to the modules requesting the query + */ + +class MySQLService; + +/** A query request + */ +struct QueryRequest final +{ + /* The connection to the database */ + MySQLService *service; + /* The interface to use once we have the result to send the data back */ + Interface *sqlinterface; + /* The actual query */ + Query query; + + QueryRequest(MySQLService *s, Interface *i, const Query &q) : service(s), sqlinterface(i), query(q) { } +}; + +/** A query result */ +struct QueryResult final +{ + /* The interface to send the data back on */ + Interface *sqlinterface; + /* The result */ + Result result; + + QueryResult(Interface *i, Result &r) : sqlinterface(i), result(r) { } +}; + +/** A MySQL result + */ +class MySQLResult final + : public Result +{ + MYSQL_RES *res = nullptr; + +public: + MySQLResult(unsigned int i, const Query &q, const Anope::string &fq, MYSQL_RES *r) : Result(i, q, fq), res(r) + { + unsigned num_fields = res ? mysql_num_fields(res) : 0; + + /* It is not thread safe to log anything here using Log(this->owner) now :( */ + + if (!num_fields) + return; + + for (MYSQL_ROW row; (row = mysql_fetch_row(res));) + { + MYSQL_FIELD *fields = mysql_fetch_fields(res); + + if (fields) + { + std::map items; + + for (unsigned field_count = 0; field_count < num_fields; ++field_count) + { + Anope::string column = (fields[field_count].name ? fields[field_count].name : ""); + Anope::string data = (row[field_count] ? row[field_count] : ""); + + items[column] = data; + } + + this->entries.push_back(items); + } + } + } + + MySQLResult(const Query &q, const Anope::string &fq, const Anope::string &err) : Result(0, q, fq, err) + { + } + + ~MySQLResult() + { + if (this->res) + mysql_free_result(this->res); + } +}; + +/** A MySQL connection, there can be multiple + */ +class MySQLService final + : public Provider +{ + std::map > active_schema; + + Anope::string database; + Anope::string server; + Anope::string user; + Anope::string password; + unsigned int port; + + MYSQL *sql = nullptr; + + /** Escape a query. + * Note the mutex must be held! + */ + Anope::string Escape(const Anope::string &query); + +public: + /* Locked by the SQL thread when a query is pending on this database, + * prevents us from deleting a connection while a query is executing + * in the thread + */ + Mutex Lock; + + MySQLService(Module *o, const Anope::string &n, const Anope::string &d, const Anope::string &s, const Anope::string &u, const Anope::string &p, unsigned int po); + + ~MySQLService(); + + void Run(Interface *i, const Query &query) override; + + Result RunQuery(const Query &query) override; + + std::vector CreateTable(const Anope::string &table, const Data &data) override; + + Query BuildInsert(const Anope::string &table, unsigned int id, Data &data) override; + + Query GetTables(const Anope::string &prefix) override; + + void Connect(); + + bool CheckConnection(); + + Anope::string BuildQuery(const Query &q); + + Anope::string FromUnixtime(time_t) override; +}; + +/** The SQL thread used to execute queries + */ +class DispatcherThread final + : public Thread + , public Condition +{ +public: + DispatcherThread() : Thread() { } + + void Run() override; +}; + +class ModuleSQL; +static ModuleSQL *me; + +class ModuleSQL final + : public Module + , public Pipe +{ + /* SQL connections */ + std::map MySQLServices; +public: + /* Pending query requests */ + std::deque QueryRequests; + /* Pending finished requests with results */ + std::deque FinishedRequests; + /* The thread used to execute queries */ + DispatcherThread *DThread; + + ModuleSQL(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) + { + me = this; + + + DThread = new DispatcherThread(); + DThread->Start(); + } + + ~ModuleSQL() + { + for (std::map::iterator it = this->MySQLServices.begin(); it != this->MySQLServices.end(); ++it) + delete it->second; + MySQLServices.clear(); + + DThread->SetExitState(); + DThread->Wakeup(); + DThread->Join(); + delete DThread; + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *config = conf->GetModule(this); + + for (std::map::iterator it = this->MySQLServices.begin(); it != this->MySQLServices.end();) + { + const Anope::string &cname = it->first; + MySQLService *s = it->second; + int i; + + ++it; + + for (i = 0; i < config->CountBlock("mysql"); ++i) + if (config->GetBlock("mysql", i)->Get("name", "mysql/main") == cname) + break; + + if (i == config->CountBlock("mysql")) + { + Log(LOG_NORMAL, "mysql") << "MySQL: Removing server connection " << cname; + + delete s; + this->MySQLServices.erase(cname); + } + } + + for (int i = 0; i < config->CountBlock("mysql"); ++i) + { + Configuration::Block *block = config->GetBlock("mysql", i); + const Anope::string &connname = block->Get("name", "mysql/main"); + + if (this->MySQLServices.find(connname) == this->MySQLServices.end()) + { + const Anope::string &database = block->Get("database", "anope"); + const Anope::string &server = block->Get("server", "127.0.0.1"); + const Anope::string &user = block->Get("username", "anope"); + const Anope::string &password = block->Get("password"); + unsigned int port = block->Get("port", "3306"); + + try + { + auto *ss = new MySQLService(this, connname, database, server, user, password, port); + this->MySQLServices.emplace(connname, ss); + + Log(LOG_NORMAL, "mysql") << "MySQL: Successfully connected to server " << connname << " (" << server << ")"; + } + catch (const SQL::Exception &ex) + { + Log(LOG_NORMAL, "mysql") << "MySQL: " << ex.GetReason(); + } + } + } + } + + void OnModuleUnload(User *, Module *m) override + { + this->DThread->Lock(); + + for (unsigned i = this->QueryRequests.size(); i > 0; --i) + { + QueryRequest &r = this->QueryRequests[i - 1]; + + if (r.sqlinterface && r.sqlinterface->owner == m) + { + if (i == 1) + { + r.service->Lock.Lock(); + r.service->Lock.Unlock(); + } + + this->QueryRequests.erase(this->QueryRequests.begin() + i - 1); + } + } + + this->DThread->Unlock(); + + this->OnNotify(); + } + + void OnNotify() override + { + this->DThread->Lock(); + std::deque finishedRequests = this->FinishedRequests; + this->FinishedRequests.clear(); + this->DThread->Unlock(); + + for (const auto &qr : finishedRequests) + { + if (!qr.sqlinterface) + throw SQL::Exception("NULL qr.sqlinterface in MySQLPipe::OnNotify() ?"); + + if (qr.result.GetError().empty()) + qr.sqlinterface->OnResult(qr.result); + else + qr.sqlinterface->OnError(qr.result); + } + } +}; + +MySQLService::MySQLService(Module *o, const Anope::string &n, const Anope::string &d, const Anope::string &s, const Anope::string &u, const Anope::string &p, unsigned int po) + : Provider(o, n) + , database(d) + , server(s) + , user(u) + , password(p) + , port(po) +{ + Connect(); +} + +MySQLService::~MySQLService() +{ + me->DThread->Lock(); + this->Lock.Lock(); + mysql_close(this->sql); + this->sql = NULL; + + for (unsigned i = me->QueryRequests.size(); i > 0; --i) + { + QueryRequest &r = me->QueryRequests[i - 1]; + + if (r.service == this) + { + if (r.sqlinterface) + r.sqlinterface->OnError(Result(0, r.query, "SQL Interface is going away")); + me->QueryRequests.erase(me->QueryRequests.begin() + i - 1); + } + } + this->Lock.Unlock(); + me->DThread->Unlock(); +} + +void MySQLService::Run(Interface *i, const Query &query) +{ + me->DThread->Lock(); + me->QueryRequests.push_back(QueryRequest(this, i, query)); + me->DThread->Unlock(); + me->DThread->Wakeup(); +} + +Result MySQLService::RunQuery(const Query &query) +{ + this->Lock.Lock(); + + Anope::string real_query = this->BuildQuery(query); + + if (this->CheckConnection() && !mysql_real_query(this->sql, real_query.c_str(), real_query.length())) + { + MYSQL_RES *res = mysql_store_result(this->sql); + unsigned int id = mysql_insert_id(this->sql); + + /* because we enabled CLIENT_MULTI_RESULTS in our options + * a multiple statement or a procedure call can return + * multiple result sets. + * we must process them all before the next query. + */ + + while (!mysql_next_result(this->sql)) + mysql_free_result(mysql_store_result(this->sql)); + + this->Lock.Unlock(); + return MySQLResult(id, query, real_query, res); + } + else + { + Anope::string error = mysql_error(this->sql); + this->Lock.Unlock(); + return MySQLResult(query, real_query, error); + } +} + +std::vector MySQLService::CreateTable(const Anope::string &table, const Data &data) +{ + std::vector queries; + std::set &known_cols = this->active_schema[table]; + + if (known_cols.empty()) + { + Log(LOG_DEBUG) << "mysql: Fetching columns for " << table; + + Result columns = this->RunQuery("SHOW COLUMNS FROM `" + table + "`"); + for (int i = 0; i < columns.Rows(); ++i) + { + const Anope::string &column = columns.Get(i, "Field"); + + Log(LOG_DEBUG) << "mysql: Column #" << i << " for " << table << ": " << column; + known_cols.insert(column); + } + } + + if (known_cols.empty()) + { + Anope::string query_text = "CREATE TABLE `" + table + "` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT," + " `timestamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"; + for (const auto &[column, _] : data.data) + { + known_cols.insert(column); + + query_text += ", `" + column + "` "; + if (data.GetType(column) == Serialize::Data::DT_INT) + query_text += "int(11)"; + else + query_text += "text"; + } + query_text += ", PRIMARY KEY (`id`), KEY `timestamp_idx` (`timestamp`))"; + queries.push_back(query_text); + } + else + { + for (const auto &[column, _] : data.data) + { + if (known_cols.count(column) > 0) + continue; + + known_cols.insert(column); + + Anope::string query_text = "ALTER TABLE `" + table + "` ADD `" + column + "` "; + if (data.GetType(column) == Serialize::Data::DT_INT) + query_text += "int(11)"; + else + query_text += "text"; + + queries.push_back(query_text); + } + } + + return queries; +} + +Query MySQLService::BuildInsert(const Anope::string &table, unsigned int id, Data &data) +{ + /* Empty columns not present in the data set */ + for (const auto &known_col : this->active_schema[table]) + { + if (known_col != "id" && known_col != "timestamp" && data.data.count(known_col) == 0) + data[known_col] << ""; + } + + Anope::string query_text = "INSERT INTO `" + table + "` (`id`"; + + for (const auto &[field, _] : data.data) + query_text += ",`" + field + "`"; + query_text += ") VALUES (" + stringify(id); + for (const auto &[field, _] : data.data) + query_text += ",@" + field + "@"; + query_text += ") ON DUPLICATE KEY UPDATE "; + for (const auto &[field, _] : data.data) + query_text += "`" + field + "`=VALUES(`" + field + "`),"; + query_text.erase(query_text.end() - 1); + + Query query(query_text); + for (auto &[field, value] : data.data) + { + Anope::string buf; + *value >> buf; + + bool escape = true; + if (buf.empty()) + { + buf = "NULL"; + escape = false; + } + + query.SetValue(field, buf, escape); + } + + return query; +} + +Query MySQLService::GetTables(const Anope::string &prefix) +{ + return Query("SHOW TABLES LIKE '" + prefix + "%';"); +} + +void MySQLService::Connect() +{ + this->sql = mysql_init(this->sql); + + const unsigned int timeout = 1; + mysql_options(this->sql, MYSQL_OPT_CONNECT_TIMEOUT, reinterpret_cast(&timeout)); + + bool connect = mysql_real_connect(this->sql, this->server.c_str(), this->user.c_str(), this->password.c_str(), this->database.c_str(), this->port, NULL, CLIENT_MULTI_RESULTS); + + if (!connect) + throw SQL::Exception("Unable to connect to MySQL service " + this->name + ": " + mysql_error(this->sql)); + + Log(LOG_DEBUG) << "Successfully connected to MySQL service " << this->name << " at " << this->server << ":" << this->port; +} + + +bool MySQLService::CheckConnection() +{ + if (!this->sql || mysql_ping(this->sql)) + { + try + { + this->Connect(); + } + catch (const SQL::Exception &) + { + return false; + } + } + + return true; +} + +Anope::string MySQLService::Escape(const Anope::string &query) +{ + std::vector buffer(query.length() * 2 + 1); + mysql_real_escape_string(this->sql, &buffer[0], query.c_str(), query.length()); + return &buffer[0]; +} + +Anope::string MySQLService::BuildQuery(const Query &q) +{ + Anope::string real_query = q.query; + + for (const auto &[name, value] : q.parameters) + real_query = real_query.replace_all_cs("@" + name + "@", (value.escape ? ("'" + this->Escape(value.data) + "'") : value.data)); + + return real_query; +} + +Anope::string MySQLService::FromUnixtime(time_t t) +{ + return "FROM_UNIXTIME(" + stringify(t) + ")"; +} + +void DispatcherThread::Run() +{ + this->Lock(); + + while (!this->GetExitState()) + { + if (!me->QueryRequests.empty()) + { + QueryRequest &r = me->QueryRequests.front(); + this->Unlock(); + + Result sresult = r.service->RunQuery(r.query); + + this->Lock(); + if (!me->QueryRequests.empty() && me->QueryRequests.front().query == r.query) + { + if (r.sqlinterface) + me->FinishedRequests.push_back(QueryResult(r.sqlinterface, sresult)); + me->QueryRequests.pop_front(); + } + } + else + { + if (!me->FinishedRequests.empty()) + me->Notify(); + this->Wait(); + } + } + + this->Unlock(); +} + +MODULE_INIT(ModuleSQL) diff --git a/modules/extra/regex_pcre2.cpp b/modules/extra/regex_pcre2.cpp new file mode 100644 index 000000000..ef2786823 --- /dev/null +++ b/modules/extra/regex_pcre2.cpp @@ -0,0 +1,91 @@ +/* + * + * (C) 2012-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +/* RequiredLibraries: pcre2-8 */ +/* RequiredWindowsLibraries: pcre2-8 */ + +#include "module.h" + +#define PCRE2_CODE_UNIT_WIDTH 8 +#include + +class PCRERegex final + : public Regex +{ + pcre2_code *regex; + +public: + PCRERegex(const Anope::string &expr) : Regex(expr) + { + int errcode; + PCRE2_SIZE erroffset; + this->regex = pcre2_compile(reinterpret_cast(expr.c_str()), expr.length(), PCRE2_CASELESS, &errcode, &erroffset, NULL); + + if (!this->regex) + { + PCRE2_UCHAR error[128]; + pcre2_get_error_message(errcode, error, sizeof error); + throw RegexException("Error in regex " + expr + " at offset " + stringify(erroffset) + ": " + reinterpret_cast(error)); + } + } + + ~PCRERegex() + { + pcre2_code_free(this->regex); + } + + bool Matches(const Anope::string &str) + { + pcre2_match_data *unused = pcre2_match_data_create_from_pattern(this->regex, NULL); + int result = pcre2_match(regex, reinterpret_cast(str.c_str()), str.length(), 0, 0, unused, NULL); + pcre2_match_data_free(unused); + return result >= 0; + } +}; + +class PCRERegexProvider final + : public RegexProvider +{ +public: + PCRERegexProvider(Module *creator) : RegexProvider(creator, "regex/pcre") { } + + Regex *Compile(const Anope::string &expression) override + { + return new PCRERegex(expression); + } +}; + +class ModuleRegexPCRE final + : public Module +{ + PCRERegexProvider pcre_regex_provider; + +public: + ModuleRegexPCRE(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), + pcre_regex_provider(this) + { + this->SetPermanent(true); + } + + ~ModuleRegexPCRE() + { + for (auto *xlm : XLineManager::XLineManagers) + { + for (auto *x : xlm->GetList()) + { + if (x->regex && dynamic_cast(x->regex)) + { + delete x->regex; + x->regex = NULL; + } + } + } + } +}; + +MODULE_INIT(ModuleRegexPCRE) diff --git a/modules/extra/regex_posix.cpp b/modules/extra/regex_posix.cpp new file mode 100644 index 000000000..cef4486f9 --- /dev/null +++ b/modules/extra/regex_posix.cpp @@ -0,0 +1,82 @@ +/* + * + * (C) 2012-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include +#include + +class POSIXRegex final + : public Regex +{ + regex_t regbuf; + +public: + POSIXRegex(const Anope::string &expr) : Regex(expr) + { + int err = regcomp(&this->regbuf, expr.c_str(), REG_EXTENDED | REG_NOSUB | REG_ICASE); + if (err) + { + char buf[BUFSIZE]; + regerror(err, &this->regbuf, buf, sizeof(buf)); + regfree(&this->regbuf); + throw RegexException("Error in regex " + expr + ": " + buf); + } + } + + ~POSIXRegex() + { + regfree(&this->regbuf); + } + + bool Matches(const Anope::string &str) + { + return regexec(&this->regbuf, str.c_str(), 0, NULL, 0) == 0; + } +}; + +class POSIXRegexProvider final + : public RegexProvider +{ +public: + POSIXRegexProvider(Module *creator) : RegexProvider(creator, "regex/posix") { } + + Regex *Compile(const Anope::string &expression) override + { + return new POSIXRegex(expression); + } +}; + +class ModuleRegexPOSIX final + : public Module +{ + POSIXRegexProvider posix_regex_provider; + +public: + ModuleRegexPOSIX(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), + posix_regex_provider(this) + { + this->SetPermanent(true); + } + + ~ModuleRegexPOSIX() + { + for (auto *xlm : XLineManager::XLineManagers) + { + for (auto *x : xlm->GetList()) + { + if (x->regex && dynamic_cast(x->regex)) + { + delete x->regex; + x->regex = NULL; + } + } + } + } +}; + +MODULE_INIT(ModuleRegexPOSIX) diff --git a/modules/extra/regex_tre.cpp b/modules/extra/regex_tre.cpp new file mode 100644 index 000000000..18485507a --- /dev/null +++ b/modules/extra/regex_tre.cpp @@ -0,0 +1,83 @@ +/* + * + * (C) 2012-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +/* RequiredLibraries: tre */ + +#include "module.h" +#include + +class TRERegex final + : public Regex +{ + regex_t regbuf; + +public: + TRERegex(const Anope::string &expr) : Regex(expr) + { + int err = regcomp(&this->regbuf, expr.c_str(), REG_EXTENDED | REG_NOSUB); + if (err) + { + char buf[BUFSIZE]; + regerror(err, &this->regbuf, buf, sizeof(buf)); + regfree(&this->regbuf); + throw RegexException("Error in regex " + expr + ": " + buf); + } + } + + ~TRERegex() + { + regfree(&this->regbuf); + } + + bool Matches(const Anope::string &str) + { + return regexec(&this->regbuf, str.c_str(), 0, NULL, 0) == 0; + } +}; + +class TRERegexProvider final + : public RegexProvider +{ +public: + TRERegexProvider(Module *creator) : RegexProvider(creator, "regex/tre") { } + + Regex *Compile(const Anope::string &expression) override + { + return new TRERegex(expression); + } +}; + +class ModuleRegexTRE final + : public Module +{ + TRERegexProvider tre_regex_provider; + +public: + ModuleRegexTRE(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), + tre_regex_provider(this) + { + this->SetPermanent(true); + } + + ~ModuleRegexTRE() + { + for (auto *xlm : XLineManager::XLineManagers) + { + for (auto *x : xlm->GetList()) + { + if (x->regex && dynamic_cast(x->regex)) + { + delete x->regex; + x->regex = NULL; + } + } + } + } +}; + +MODULE_INIT(ModuleRegexTRE) diff --git a/modules/extra/sql_authentication.cpp b/modules/extra/sql_authentication.cpp new file mode 100644 index 000000000..bb2f82410 --- /dev/null +++ b/modules/extra/sql_authentication.cpp @@ -0,0 +1,150 @@ +/* + * + * (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(); + } +}; + +MODULE_INIT(ModuleSQLAuthentication) diff --git a/modules/extra/sql_log.cpp b/modules/extra/sql_log.cpp new file mode 100644 index 000000000..7335a5abf --- /dev/null +++ b/modules/extra/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/extra/sql_oper.cpp b/modules/extra/sql_oper.cpp new file mode 100644 index 000000000..af2b40346 --- /dev/null +++ b/modules/extra/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) diff --git a/modules/extra/sqlite.cpp b/modules/extra/sqlite.cpp new file mode 100644 index 000000000..a3d2f7757 --- /dev/null +++ b/modules/extra/sqlite.cpp @@ -0,0 +1,340 @@ +/* + * + * (C) 2011-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +/* RequiredLibraries: sqlite3 */ +/* RequiredWindowsLibraries: sqlite3 */ + +#include "module.h" +#include "modules/sql.h" +#include + +using namespace SQL; + +/* SQLite3 API, based from InspIRCd */ + +/** A SQLite result + */ +class SQLiteResult final + : public Result +{ +public: + SQLiteResult(unsigned int i, const Query &q, const Anope::string &fq) : Result(i, q, fq) + { + } + + SQLiteResult(const Query &q, const Anope::string &fq, const Anope::string &err) : Result(0, q, fq, err) + { + } + + void AddRow(const std::map &data) + { + this->entries.push_back(data); + } +}; + +/** A SQLite database, there can be multiple + */ +class SQLiteService final + : public Provider +{ + std::map > active_schema; + + Anope::string database; + + sqlite3 *sql = nullptr; + + Anope::string Escape(const Anope::string &query); + +public: + SQLiteService(Module *o, const Anope::string &n, const Anope::string &d); + + ~SQLiteService(); + + void Run(Interface *i, const Query &query) override; + + Result RunQuery(const Query &query) override; + + std::vector CreateTable(const Anope::string &table, const Data &data) override; + + Query BuildInsert(const Anope::string &table, unsigned int id, Data &data) override; + + Query GetTables(const Anope::string &prefix) override; + + Anope::string BuildQuery(const Query &q); + + Anope::string FromUnixtime(time_t) override; +}; + +class ModuleSQLite final + : public Module +{ + /* SQL connections */ + std::map SQLiteServices; +public: + ModuleSQLite(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) + { + } + + ~ModuleSQLite() + { + for (std::map::iterator it = this->SQLiteServices.begin(); it != this->SQLiteServices.end(); ++it) + delete it->second; + SQLiteServices.clear(); + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *config = conf->GetModule(this); + + for (std::map::iterator it = this->SQLiteServices.begin(); it != this->SQLiteServices.end();) + { + const Anope::string &cname = it->first; + SQLiteService *s = it->second; + int i, num; + ++it; + + for (i = 0, num = config->CountBlock("sqlite"); i < num; ++i) + if (config->GetBlock("sqlite", i)->Get("name", "sqlite/main") == cname) + break; + + if (i == num) + { + Log(LOG_NORMAL, "sqlite") << "SQLite: Removing server connection " << cname; + + delete s; + this->SQLiteServices.erase(cname); + } + } + + for (int i = 0; i < config->CountBlock("sqlite"); ++i) + { + Configuration::Block *block = config->GetBlock("sqlite", i); + Anope::string connname = block->Get("name", "sqlite/main"); + + if (this->SQLiteServices.find(connname) == this->SQLiteServices.end()) + { + Anope::string database = Anope::DataDir + "/" + block->Get("database", "anope"); + + try + { + auto *ss = new SQLiteService(this, connname, database); + this->SQLiteServices[connname] = ss; + + Log(LOG_NORMAL, "sqlite") << "SQLite: Successfully added database " << database; + } + catch (const SQL::Exception &ex) + { + Log(LOG_NORMAL, "sqlite") << "SQLite: " << ex.GetReason(); + } + } + } + } +}; + +SQLiteService::SQLiteService(Module *o, const Anope::string &n, const Anope::string &d) +: Provider(o, n), database(d) +{ + int db = sqlite3_open_v2(database.c_str(), &this->sql, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0); + if (db != SQLITE_OK) + { + Anope::string exstr = "Unable to open SQLite database " + database; + if (this->sql) + { + exstr += ": "; + exstr += sqlite3_errmsg(this->sql); + sqlite3_close(this->sql); + } + throw SQL::Exception(exstr); + } +} + +SQLiteService::~SQLiteService() +{ + sqlite3_interrupt(this->sql); + sqlite3_close(this->sql); +} + +void SQLiteService::Run(Interface *i, const Query &query) +{ + Result res = this->RunQuery(query); + if (!res.GetError().empty()) + i->OnError(res); + else + i->OnResult(res); +} + +Result SQLiteService::RunQuery(const Query &query) +{ + Anope::string real_query = this->BuildQuery(query); + sqlite3_stmt *stmt; + int err = sqlite3_prepare_v2(this->sql, real_query.c_str(), real_query.length(), &stmt, NULL); + if (err != SQLITE_OK) + return SQLiteResult(query, real_query, sqlite3_errmsg(this->sql)); + + std::vector columns; + int cols = sqlite3_column_count(stmt); + columns.resize(cols); + for (int i = 0; i < cols; ++i) + columns[i] = sqlite3_column_name(stmt, i); + + SQLiteResult result(0, query, real_query); + + while ((err = sqlite3_step(stmt)) == SQLITE_ROW) + { + std::map items; + for (int i = 0; i < cols; ++i) + { + const char *data = reinterpret_cast(sqlite3_column_text(stmt, i)); + if (data && *data) + items[columns[i]] = data; + } + result.AddRow(items); + } + + result.id = sqlite3_last_insert_rowid(this->sql); + + sqlite3_finalize(stmt); + + if (err != SQLITE_DONE) + return SQLiteResult(query, real_query, sqlite3_errmsg(this->sql)); + + return std::move(result); +} + +std::vector SQLiteService::CreateTable(const Anope::string &table, const Data &data) +{ + std::vector queries; + std::set &known_cols = this->active_schema[table]; + + if (known_cols.empty()) + { + Log(LOG_DEBUG) << "sqlite: Fetching columns for " << table; + + Result columns = this->RunQuery("PRAGMA table_info(" + table + ")"); + for (int i = 0; i < columns.Rows(); ++i) + { + const Anope::string &column = columns.Get(i, "name"); + + Log(LOG_DEBUG) << "sqlite: Column #" << i << " for " << table << ": " << column; + known_cols.insert(column); + } + } + + if (known_cols.empty()) + { + Anope::string query_text = "CREATE TABLE `" + table + "` (`id` INTEGER PRIMARY KEY, `timestamp` timestamp DEFAULT CURRENT_TIMESTAMP"; + + for (const auto &[column, _] : data.data) + { + known_cols.insert(column); + + query_text += ", `" + column + "` "; + if (data.GetType(column) == Serialize::Data::DT_INT) + query_text += "int(11)"; + else + query_text += "text"; + } + + query_text += ")"; + + queries.push_back(query_text); + + query_text = "CREATE UNIQUE INDEX `" + table + "_id_idx` ON `" + table + "` (`id`)"; + queries.push_back(query_text); + + query_text = "CREATE INDEX `" + table + "_timestamp_idx` ON `" + table + "` (`timestamp`)"; + queries.push_back(query_text); + + query_text = "CREATE TRIGGER `" + table + "_trigger` AFTER UPDATE ON `" + table + "` FOR EACH ROW BEGIN UPDATE `" + table + "` SET `timestamp` = CURRENT_TIMESTAMP WHERE `id` = `old.id`; end;"; + queries.push_back(query_text); + } + else + { + for (const auto &[column, _] : data.data) + { + if (known_cols.count(column) > 0) + continue; + + known_cols.insert(column); + + Anope::string query_text = "ALTER TABLE `" + table + "` ADD `" + column + "` "; + if (data.GetType(column) == Serialize::Data::DT_INT) + query_text += "int(11)"; + else + query_text += "text"; + + queries.push_back(query_text); + } + } + + return queries; +} + +Query SQLiteService::BuildInsert(const Anope::string &table, unsigned int id, Data &data) +{ + /* Empty columns not present in the data set */ + for (const auto &known_col : this->active_schema[table]) + { + if (known_col != "id" && known_col != "timestamp" && data.data.count(known_col) == 0) + data[known_col] << ""; + } + + Anope::string query_text = "REPLACE INTO `" + table + "` ("; + if (id > 0) + query_text += "`id`,"; + for (const auto &[field, _] : data.data) + query_text += "`" + field + "`,"; + query_text.erase(query_text.length() - 1); + query_text += ") VALUES ("; + if (id > 0) + query_text += stringify(id) + ","; + for (const auto &[field, _] : data.data) + query_text += "@" + field + "@,"; + query_text.erase(query_text.length() - 1); + query_text += ")"; + + Query query(query_text); + for (auto &[field, value] : data.data) + { + Anope::string buf; + *value >> buf; + query.SetValue(field, buf); + } + + return query; +} + +Query SQLiteService::GetTables(const Anope::string &prefix) +{ + return Query("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '" + prefix + "%';"); +} + +Anope::string SQLiteService::Escape(const Anope::string &query) +{ + char *e = sqlite3_mprintf("%q", query.c_str()); + Anope::string buffer = e; + sqlite3_free(e); + return buffer; +} + +Anope::string SQLiteService::BuildQuery(const Query &q) +{ + Anope::string real_query = q.query; + + for (const auto &[name, value] : q.parameters) + real_query = real_query.replace_all_cs("@" + name + "@", (value.escape ? ("'" + this->Escape(value.data) + "'") : value.data)); + + return real_query; +} + +Anope::string SQLiteService::FromUnixtime(time_t t) +{ + return "datetime('" + stringify(t) + "', 'unixepoch')"; +} + +MODULE_INIT(ModuleSQLite) diff --git a/modules/extra/ssl_gnutls.cpp b/modules/extra/ssl_gnutls.cpp new file mode 100644 index 000000000..fdf5b1bff --- /dev/null +++ b/modules/extra/ssl_gnutls.cpp @@ -0,0 +1,643 @@ +/* + * + * (C) 2014 Attila Molnar + * (C) 2014-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +/* RequiredLibraries: gnutls */ +/* RequiredWindowsLibraries: libgnutls-30 */ + +#include "module.h" +#include "modules/ssl.h" + +#include +#include +#include + +class GnuTLSModule; +static GnuTLSModule *me; + +namespace GnuTLS { class X509CertCredentials; } + +class MySSLService final + : public SSLService +{ +public: + MySSLService(Module *o, const Anope::string &n); + + /** Initialize a socket to use SSL + * @param s The socket + */ + void Init(Socket *s) override; +}; + +class SSLSocketIO final + : public SocketIO +{ +public: + gnutls_session_t sess = nullptr; + GnuTLS::X509CertCredentials* mycreds; + + /** Constructor + */ + SSLSocketIO(); + + /** Really receive something from the buffer + * @param s The socket + * @param buf The buf to read to + * @param sz How much to read + * @return Number of bytes received + */ + int Recv(Socket *s, char *buf, size_t sz) override; + + /** Write something to the socket + * @param s The socket + * @param buf The data to write + * @param size The length of the data + */ + int Send(Socket *s, const char *buf, size_t sz) override; + + /** Accept a connection from a socket + * @param s The socket + * @return The new socket + */ + ClientSocket *Accept(ListenSocket *s) override; + + /** Finished accepting a connection from a socket + * @param s The socket + * @return SF_ACCEPTED if accepted, SF_ACCEPTING if still in process, SF_DEAD on error + */ + SocketFlag FinishAccept(ClientSocket *cs) override; + + /** Connect the socket + * @param s THe socket + * @param target IP to connect to + * @param port to connect to + */ + void Connect(ConnectionSocket *s, const Anope::string &target, int port) override; + + /** Called to potentially finish a pending connection + * @param s The socket + * @return SF_CONNECTED on success, SF_CONNECTING if still pending, and SF_DEAD on error. + */ + SocketFlag FinishConnect(ConnectionSocket *s) override; + + /** Called when the socket is destructing + */ + void Destroy() override; +}; + +namespace GnuTLS +{ + class Init final + { + public: + Init() { gnutls_global_init(); } + ~Init() { gnutls_global_deinit(); } + }; + + /** Used to create a gnutls_datum_t* from an Anope::string + */ + class Datum final + { + gnutls_datum_t datum; + + public: + Datum(const Anope::string &dat) + { + datum.data = reinterpret_cast(const_cast(dat.data())); + datum.size = static_cast(dat.length()); + } + + const gnutls_datum_t *get() const { return &datum; } + }; + + class DHParams final + { + gnutls_dh_params_t dh_params = nullptr; + + public: + void Import(const Anope::string &dhstr) + { + if (dh_params != NULL) + { + gnutls_dh_params_deinit(dh_params); + dh_params = NULL; + } + + int ret = gnutls_dh_params_init(&dh_params); + if (ret < 0) + throw ConfigException("Unable to initialize DH parameters"); + + ret = gnutls_dh_params_import_pkcs3(dh_params, Datum(dhstr).get(), GNUTLS_X509_FMT_PEM); + if (ret < 0) + { + gnutls_dh_params_deinit(dh_params); + dh_params = NULL; + throw ConfigException("Unable to import DH parameters"); + } + } + + ~DHParams() + { + if (dh_params) + gnutls_dh_params_deinit(dh_params); + } + + gnutls_dh_params_t get() const { return dh_params; } + }; + + class X509Key final + { + /** Ensure that the key is deinited in case the constructor of X509Key throws + */ + class RAIIKey final + { + public: + gnutls_x509_privkey_t key; + + RAIIKey() + { + int ret = gnutls_x509_privkey_init(&key); + if (ret < 0) + throw ConfigException("gnutls_x509_privkey_init() failed"); + } + + ~RAIIKey() + { + gnutls_x509_privkey_deinit(key); + } + } key; + + public: + /** Import */ + X509Key(const Anope::string &keystr) + { + int ret = gnutls_x509_privkey_import(key.key, Datum(keystr).get(), GNUTLS_X509_FMT_PEM); + if (ret < 0) + throw ConfigException("Error loading private key: " + Anope::string(gnutls_strerror(ret))); + } + + gnutls_x509_privkey_t& get() { return key.key; } + }; + + class X509CertList final + { + std::vector certs; + + public: + /** Import */ + X509CertList(const Anope::string &certstr) + { + unsigned int certcount = 3; + certs.resize(certcount); + Datum datum(certstr); + + int ret = gnutls_x509_crt_list_import(raw(), &certcount, datum.get(), GNUTLS_X509_FMT_PEM, GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED); + if (ret == GNUTLS_E_SHORT_MEMORY_BUFFER) + { + // the buffer wasn't big enough to hold all certs but gnutls changed certcount to the number of available certs, + // try again with a bigger buffer + certs.resize(certcount); + ret = gnutls_x509_crt_list_import(raw(), &certcount, datum.get(), GNUTLS_X509_FMT_PEM, GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED); + } + + if (ret < 0) + throw ConfigException("Unable to load certificates" + Anope::string(gnutls_strerror(ret))); + + // Resize the vector to the actual number of certs because we rely on its size being correct + // when deallocating the certs + certs.resize(certcount); + } + + ~X509CertList() + { + for (std::vector::iterator i = certs.begin(); i != certs.end(); ++i) + gnutls_x509_crt_deinit(*i); + } + + gnutls_x509_crt_t* raw() { return &certs[0]; } + unsigned int size() const { return certs.size(); } + }; + + class X509CertCredentials final + { + unsigned int refcount = 0; + gnutls_certificate_credentials_t cred; + DHParams dh; + + static Anope::string LoadFile(const Anope::string &filename) + { + std::ifstream ifs(filename.c_str()); + const Anope::string ret((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + return ret; + } + + static int cert_callback(gnutls_session_t sess, const gnutls_datum_t* req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t* sign_algos, int sign_algos_length, gnutls_retr2_st* st); + + public: + X509CertList certs; + X509Key key; + + X509CertCredentials(const Anope::string &certfile, const Anope::string &keyfile) + : certs(LoadFile(certfile)), key(LoadFile(keyfile)) + { + if (gnutls_certificate_allocate_credentials(&cred) < 0) + throw ConfigException("Cannot allocate certificate credentials"); + + int ret = gnutls_certificate_set_x509_key(cred, certs.raw(), certs.size(), key.get()); + if (ret < 0) + { + gnutls_certificate_free_credentials(cred); + throw ConfigException("Unable to set cert/key pair"); + } + + gnutls_certificate_set_retrieve_function(cred, cert_callback); + } + + ~X509CertCredentials() + { + gnutls_certificate_free_credentials(cred); + } + + void SetupSession(gnutls_session_t sess) + { + gnutls_credentials_set(sess, GNUTLS_CRD_CERTIFICATE, cred); + gnutls_set_default_priority(sess); + } + + void SetDH(const Anope::string &dhfile) + { + const Anope::string dhdata = LoadFile(dhfile); + dh.Import(dhdata); + gnutls_certificate_set_dh_params(cred, dh.get()); + } + + bool HasDH() const + { + return (dh.get() != NULL); + } + + void incrref() { refcount++; } + void decrref() { if (!--refcount) delete this; } + }; +} + +class GnuTLSModule final + : public Module +{ + GnuTLS::Init libinit; + +public: + GnuTLS::X509CertCredentials *cred = nullptr; + MySSLService service; + + GnuTLSModule(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), service(this, "ssl") + { + me = this; + this->SetPermanent(true); + } + + ~GnuTLSModule() + { + for (std::map::const_iterator it = SocketEngine::Sockets.begin(), it_end = SocketEngine::Sockets.end(); it != it_end;) + { + Socket *s = it->second; + ++it; + + if (dynamic_cast(s->io)) + delete s; + } + + if (cred) + cred->decrref(); + } + + static void CheckFile(const Anope::string &filename) + { + if (!Anope::IsFile(filename.c_str())) + { + Log() << "File does not exist: " << filename; + throw ConfigException("Error loading certificate/private key"); + } + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *config = conf->GetModule(this); + + const Anope::string certfile = config->Get("cert", "data/fullchain.pem"); + const Anope::string keyfile = config->Get("key", "data/privkey.pem"); + const Anope::string dhfile = config->Get("dh", "data/dhparams.pem"); + + CheckFile(certfile); + CheckFile(keyfile); + + GnuTLS::X509CertCredentials *newcred = new GnuTLS::X509CertCredentials(certfile, keyfile); + + // DH params is not mandatory + if (Anope::IsFile(dhfile.c_str())) + { + try + { + newcred->SetDH(dhfile); + } + catch (...) + { + delete newcred; + throw; + } + Log(LOG_DEBUG) << "ssl_gnutls: Successfully loaded DH parameters from " << dhfile; + } + + if (cred) + cred->decrref(); + cred = newcred; + cred->incrref(); + + Log(LOG_DEBUG) << "ssl_gnutls: Successfully loaded certificate " << certfile << " and private key " << keyfile; + } + + void OnPreServerConnect() override + { + Configuration::Block *config = Config->GetBlock("uplink", Anope::CurrentUplink); + + if (config->Get("ssl")) + { + this->service.Init(UplinkSock); + } + } +}; + +MySSLService::MySSLService(Module *o, const Anope::string &n) : SSLService(o, n) +{ +} + +void MySSLService::Init(Socket *s) +{ + if (s->io != &NormalSocketIO) + throw CoreException("Socket initializing SSL twice"); + + s->io = new SSLSocketIO(); +} + +int SSLSocketIO::Recv(Socket *s, char *buf, size_t sz) +{ + int ret = gnutls_record_recv(this->sess, buf, sz); + + if (ret > 0) + TotalRead += ret; + else if (ret < 0) + { + switch (ret) + { + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + SocketEngine::SetLastError(EAGAIN); + break; + default: + if (s == UplinkSock) + { + // Log and fake an errno because this is a fatal error on the uplink socket + Log() << "SSL error: " << gnutls_strerror(ret); + } + SocketEngine::SetLastError(ECONNRESET); + } + } + + return ret; +} + +int SSLSocketIO::Send(Socket *s, const char *buf, size_t sz) +{ + int ret = gnutls_record_send(this->sess, buf, sz); + + if (ret > 0) + TotalWritten += ret; + else + { + switch (ret) + { + case 0: + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + SocketEngine::SetLastError(EAGAIN); + break; + default: + if (s == UplinkSock) + { + // Log and fake an errno because this is a fatal error on the uplink socket + Log() << "SSL error: " << gnutls_strerror(ret); + } + SocketEngine::SetLastError(ECONNRESET); + } + } + + return ret; +} + +ClientSocket *SSLSocketIO::Accept(ListenSocket *s) +{ + if (s->io == &NormalSocketIO) + throw SocketException("Attempting to accept on uninitialized socket with SSL"); + + sockaddrs conaddr; + + socklen_t size = sizeof(conaddr); + int newsock = accept(s->GetFD(), &conaddr.sa, &size); + +#ifndef INVALID_SOCKET + const int INVALID_SOCKET = -1; +#endif + + if (newsock < 0 || newsock == INVALID_SOCKET) + throw SocketException("Unable to accept connection: " + Anope::LastError()); + + ClientSocket *newsocket = s->OnAccept(newsock, conaddr); + me->service.Init(newsocket); + SSLSocketIO *io = anope_dynamic_static_cast(newsocket->io); + + if (gnutls_init(&io->sess, GNUTLS_SERVER) != GNUTLS_E_SUCCESS) + throw SocketException("Unable to initialize SSL socket"); + + me->cred->SetupSession(io->sess); + gnutls_transport_set_ptr(io->sess, reinterpret_cast(newsock)); + + newsocket->flags[SF_ACCEPTING] = true; + this->FinishAccept(newsocket); + + return newsocket; +} + +SocketFlag SSLSocketIO::FinishAccept(ClientSocket *cs) +{ + if (cs->io == &NormalSocketIO) + throw SocketException("Attempting to finish connect uninitialized socket with SSL"); + else if (cs->flags[SF_ACCEPTED]) + return SF_ACCEPTED; + else if (!cs->flags[SF_ACCEPTING]) + throw SocketException("SSLSocketIO::FinishAccept called for a socket not accepted nor accepting?"); + + SSLSocketIO *io = anope_dynamic_static_cast(cs->io); + + int ret = gnutls_handshake(io->sess); + if (ret < 0) + { + if (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) + { + // gnutls_handshake() wants to read or write again; + // if gnutls_record_get_direction() returns 0 it wants to read, otherwise it wants to write. + if (gnutls_record_get_direction(io->sess) == 0) + { + SocketEngine::Change(cs, false, SF_WRITABLE); + SocketEngine::Change(cs, true, SF_READABLE); + } + else + { + SocketEngine::Change(cs, true, SF_WRITABLE); + SocketEngine::Change(cs, false, SF_READABLE); + } + return SF_ACCEPTING; + } + else + { + cs->OnError(Anope::string(gnutls_strerror(ret))); + cs->flags[SF_DEAD] = true; + cs->flags[SF_ACCEPTING] = false; + return SF_DEAD; + } + } + else + { + cs->flags[SF_ACCEPTED] = true; + cs->flags[SF_ACCEPTING] = false; + SocketEngine::Change(cs, false, SF_WRITABLE); + SocketEngine::Change(cs, true, SF_READABLE); + cs->OnAccept(); + return SF_ACCEPTED; + } +} + +void SSLSocketIO::Connect(ConnectionSocket *s, const Anope::string &target, int port) +{ + if (s->io == &NormalSocketIO) + throw SocketException("Attempting to connect uninitialized socket with SSL"); + + s->flags[SF_CONNECTING] = s->flags[SF_CONNECTED] = false; + + s->conaddr.pton(s->GetFamily(), target, port); + int c = connect(s->GetFD(), &s->conaddr.sa, s->conaddr.size()); + if (c == -1) + { + if (Anope::LastErrorCode() != EINPROGRESS) + { + s->OnError(Anope::LastError()); + s->flags[SF_DEAD] = true; + return; + } + else + { + SocketEngine::Change(s, true, SF_WRITABLE); + s->flags[SF_CONNECTING] = true; + return; + } + } + else + { + s->flags[SF_CONNECTING] = true; + this->FinishConnect(s); + } +} + +SocketFlag SSLSocketIO::FinishConnect(ConnectionSocket *s) +{ + if (s->io == &NormalSocketIO) + throw SocketException("Attempting to finish connect uninitialized socket with SSL"); + else if (s->flags[SF_CONNECTED]) + return SF_CONNECTED; + else if (!s->flags[SF_CONNECTING]) + throw SocketException("SSLSocketIO::FinishConnect called for a socket not connected nor connecting?"); + + SSLSocketIO *io = anope_dynamic_static_cast(s->io); + + if (io->sess == NULL) + { + if (gnutls_init(&io->sess, GNUTLS_CLIENT) != GNUTLS_E_SUCCESS) + throw SocketException("Unable to initialize SSL socket"); + me->cred->SetupSession(io->sess); + gnutls_transport_set_ptr(io->sess, reinterpret_cast(s->GetFD())); + } + + int ret = gnutls_handshake(io->sess); + if (ret < 0) + { + if (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) + { + // gnutls_handshake() wants to read or write again; + // if gnutls_record_get_direction() returns 0 it wants to read, otherwise it wants to write. + if (gnutls_record_get_direction(io->sess) == 0) + { + SocketEngine::Change(s, false, SF_WRITABLE); + SocketEngine::Change(s, true, SF_READABLE); + } + else + { + SocketEngine::Change(s, true, SF_WRITABLE); + SocketEngine::Change(s, false, SF_READABLE); + } + + return SF_CONNECTING; + } + else + { + s->OnError(Anope::string(gnutls_strerror(ret))); + s->flags[SF_CONNECTING] = false; + s->flags[SF_DEAD] = true; + return SF_DEAD; + } + } + else + { + s->flags[SF_CONNECTING] = false; + s->flags[SF_CONNECTED] = true; + SocketEngine::Change(s, false, SF_WRITABLE); + SocketEngine::Change(s, true, SF_READABLE); + s->OnConnect(); + return SF_CONNECTED; + } +} + +void SSLSocketIO::Destroy() +{ + if (this->sess) + { + gnutls_bye(this->sess, GNUTLS_SHUT_WR); + gnutls_deinit(this->sess); + } + + mycreds->decrref(); + + delete this; +} + +SSLSocketIO::SSLSocketIO() : mycreds(me->cred) +{ + mycreds->incrref(); +} + +int GnuTLS::X509CertCredentials::cert_callback(gnutls_session_t sess, const gnutls_datum_t* req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t* sign_algos, int sign_algos_length, gnutls_retr2_st* st) +{ + st->cert_type = GNUTLS_CRT_X509; + st->key_type = GNUTLS_PRIVKEY_X509; + st->ncerts = me->cred->certs.size(); + st->cert.x509 = me->cred->certs.raw(); + st->key.x509 = me->cred->key.get(); + st->deinit_all = 0; + + return 0; +} + +MODULE_INIT(GnuTLSModule) diff --git a/modules/extra/ssl_openssl.cpp b/modules/extra/ssl_openssl.cpp new file mode 100644 index 000000000..a052752ae --- /dev/null +++ b/modules/extra/ssl_openssl.cpp @@ -0,0 +1,445 @@ +/* + * + * (C) 2010-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +/* RequiredLibraries: ssl,crypto */ +/* RequiredWindowsLibraries: libssl,libcrypto */ + +#include "module.h" +#include "modules/ssl.h" + +#define OPENSSL_API_COMPAT 0x10100000L +#define OPENSSL_NO_DEPRECATED + +#include +#include +#include +#include +#include + +static SSL_CTX *server_ctx, *client_ctx; + +class MySSLService final + : public SSLService +{ +public: + MySSLService(Module *o, const Anope::string &n); + + /** Initialize a socket to use SSL + * @param s The socket + */ + void Init(Socket *s) override; +}; + +class SSLSocketIO final + : public SocketIO +{ +public: + /* The SSL socket for this socket */ + SSL *sslsock; + + /** Constructor + */ + SSLSocketIO(); + + /** Really receive something from the buffer + * @param s The socket + * @param buf The buf to read to + * @param sz How much to read + * @return Number of bytes received + */ + int Recv(Socket *s, char *buf, size_t sz) override; + + /** Write something to the socket + * @param s The socket + * @param buf The data to write + * @param size The length of the data + */ + int Send(Socket *s, const char *buf, size_t sz) override; + + /** Accept a connection from a socket + * @param s The socket + * @return The new socket + */ + ClientSocket *Accept(ListenSocket *s) override; + + /** Finished accepting a connection from a socket + * @param s The socket + * @return SF_ACCEPTED if accepted, SF_ACCEPTING if still in process, SF_DEAD on error + */ + SocketFlag FinishAccept(ClientSocket *cs) override; + + /** Connect the socket + * @param s THe socket + * @param target IP to connect to + * @param port to connect to + */ + void Connect(ConnectionSocket *s, const Anope::string &target, int port) override; + + /** Called to potentially finish a pending connection + * @param s The socket + * @return SF_CONNECTED on success, SF_CONNECTING if still pending, and SF_DEAD on error. + */ + SocketFlag FinishConnect(ConnectionSocket *s) override; + + /** Called when the socket is destructing + */ + void Destroy() override; +}; + +class SSLModule; +static SSLModule *me; +class SSLModule final + : public Module +{ + Anope::string certfile, keyfile; + +public: + MySSLService service; + + SSLModule(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), service(this, "ssl") + { + me = this; + + this->SetPermanent(true); + + OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS, nullptr); + + client_ctx = SSL_CTX_new(TLS_client_method()); + server_ctx = SSL_CTX_new(TLS_server_method()); + + if (!client_ctx || !server_ctx) + throw ModuleException("Error initializing SSL CTX"); + + long opts = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION | SSL_OP_CIPHER_SERVER_PREFERENCE; + SSL_CTX_set_options(client_ctx, opts); + SSL_CTX_set_options(server_ctx, opts); + + SSL_CTX_set_mode(client_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + SSL_CTX_set_mode(server_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + + Anope::string context_name = "Anope"; + SSL_CTX_set_session_id_context(client_ctx, reinterpret_cast(context_name.c_str()), context_name.length()); + SSL_CTX_set_session_id_context(server_ctx, reinterpret_cast(context_name.c_str()), context_name.length()); + } + + ~SSLModule() + { + for (std::map::const_iterator it = SocketEngine::Sockets.begin(), it_end = SocketEngine::Sockets.end(); it != it_end;) + { + Socket *s = it->second; + ++it; + + if (dynamic_cast(s->io)) + delete s; + } + + SSL_CTX_free(client_ctx); + SSL_CTX_free(server_ctx); + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *config = conf->GetModule(this); + + this->certfile = config->Get("cert", "data/fullchain.pem"); + this->keyfile = config->Get("key", "data/privkey.pem"); + + if (Anope::IsFile(this->certfile.c_str())) + { + if (!SSL_CTX_use_certificate_chain_file(client_ctx, this->certfile.c_str()) || !SSL_CTX_use_certificate_chain_file(server_ctx, this->certfile.c_str())) + throw ConfigException("Error loading certificate"); + else + Log(LOG_DEBUG) << "ssl_openssl: Successfully loaded certificate " << this->certfile; + } + else + Log() << "Unable to open certificate " << this->certfile; + + if (Anope::IsFile(this->keyfile.c_str())) + { + if (!SSL_CTX_use_PrivateKey_file(client_ctx, this->keyfile.c_str(), SSL_FILETYPE_PEM) || !SSL_CTX_use_PrivateKey_file(server_ctx, this->keyfile.c_str(), SSL_FILETYPE_PEM)) + throw ConfigException("Error loading private key"); + else + Log(LOG_DEBUG) << "ssl_openssl: Successfully loaded private key " << this->keyfile; + } + else + { + if (Anope::IsFile(this->certfile.c_str())) + throw ConfigException("Error loading private key " + this->keyfile + " - file not found"); + else + Log() << "Unable to open private key " << this->keyfile; + } + + // Allow disabling old versions of TLS + if (config->Get("tlsv10", "false")) + { + SSL_CTX_clear_options(client_ctx, SSL_OP_NO_TLSv1); + SSL_CTX_clear_options(server_ctx, SSL_OP_NO_TLSv1); + } + else + { + SSL_CTX_set_options(client_ctx, SSL_OP_NO_TLSv1); + SSL_CTX_set_options(server_ctx, SSL_OP_NO_TLSv1); + } + + if (config->Get("tlsv11", "true")) + { + SSL_CTX_clear_options(client_ctx, SSL_OP_NO_TLSv1_1); + SSL_CTX_clear_options(server_ctx, SSL_OP_NO_TLSv1_1); + } + else + { + SSL_CTX_set_options(client_ctx, SSL_OP_NO_TLSv1_1); + SSL_CTX_set_options(server_ctx, SSL_OP_NO_TLSv1_1); + } + + if (config->Get("tlsv12", "true")) + { + SSL_CTX_clear_options(client_ctx, SSL_OP_NO_TLSv1_2); + SSL_CTX_clear_options(server_ctx, SSL_OP_NO_TLSv1_2); + } + else + { + SSL_CTX_set_options(client_ctx, SSL_OP_NO_TLSv1_2); + SSL_CTX_set_options(server_ctx, SSL_OP_NO_TLSv1_2); + } + } + + void OnPreServerConnect() override + { + Configuration::Block *config = Config->GetBlock("uplink", Anope::CurrentUplink); + + if (config->Get("ssl")) + { + this->service.Init(UplinkSock); + } + } +}; + +MySSLService::MySSLService(Module *o, const Anope::string &n) : SSLService(o, n) +{ +} + +void MySSLService::Init(Socket *s) +{ + if (s->io != &NormalSocketIO) + throw CoreException("Socket initializing SSL twice"); + + s->io = new SSLSocketIO(); +} + +SSLSocketIO::SSLSocketIO() +{ + this->sslsock = NULL; +} + +int SSLSocketIO::Recv(Socket *s, char *buf, size_t sz) +{ + int i = SSL_read(this->sslsock, buf, sz); + if (i > 0) + TotalRead += i; + else if (i < 0) + { + int err = SSL_get_error(this->sslsock, i); + switch (err) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + SocketEngine::SetLastError(EAGAIN); + } + } + + return i; +} + +int SSLSocketIO::Send(Socket *s, const char *buf, size_t sz) +{ + int i = SSL_write(this->sslsock, buf, sz); + if (i > 0) + TotalWritten += i; + else if (i < 0) + { + int err = SSL_get_error(this->sslsock, i); + switch (err) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + SocketEngine::SetLastError(EAGAIN); + } + } + return i; +} + +ClientSocket *SSLSocketIO::Accept(ListenSocket *s) +{ + if (s->io == &NormalSocketIO) + throw SocketException("Attempting to accept on uninitialized socket with SSL"); + + sockaddrs conaddr; + + socklen_t size = sizeof(conaddr); + int newsock = accept(s->GetFD(), &conaddr.sa, &size); + +#ifndef INVALID_SOCKET + const int INVALID_SOCKET = -1; +#endif + + if (newsock < 0 || newsock == INVALID_SOCKET) + throw SocketException("Unable to accept connection: " + Anope::LastError()); + + ClientSocket *newsocket = s->OnAccept(newsock, conaddr); + me->service.Init(newsocket); + SSLSocketIO *io = anope_dynamic_static_cast(newsocket->io); + + io->sslsock = SSL_new(server_ctx); + if (!io->sslsock) + throw SocketException("Unable to initialize SSL socket"); + + SSL_set_accept_state(io->sslsock); + + if (!SSL_set_fd(io->sslsock, newsocket->GetFD())) + throw SocketException("Unable to set SSL fd"); + + newsocket->flags[SF_ACCEPTING] = true; + this->FinishAccept(newsocket); + + return newsocket; +} + +SocketFlag SSLSocketIO::FinishAccept(ClientSocket *cs) +{ + if (cs->io == &NormalSocketIO) + throw SocketException("Attempting to finish connect uninitialized socket with SSL"); + else if (cs->flags[SF_ACCEPTED]) + return SF_ACCEPTED; + else if (!cs->flags[SF_ACCEPTING]) + throw SocketException("SSLSocketIO::FinishAccept called for a socket not accepted nor accepting?"); + + SSLSocketIO *io = anope_dynamic_static_cast(cs->io); + + int ret = SSL_accept(io->sslsock); + if (ret <= 0) + { + int error = SSL_get_error(io->sslsock, ret); + if (ret == -1 && (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE)) + { + SocketEngine::Change(cs, error == SSL_ERROR_WANT_WRITE, SF_WRITABLE); + SocketEngine::Change(cs, error == SSL_ERROR_WANT_READ, SF_READABLE); + return SF_ACCEPTING; + } + else + { + cs->OnError(ERR_error_string(ERR_get_error(), NULL)); + cs->flags[SF_DEAD] = true; + cs->flags[SF_ACCEPTING] = false; + return SF_DEAD; + } + } + else + { + cs->flags[SF_ACCEPTED] = true; + cs->flags[SF_ACCEPTING] = false; + SocketEngine::Change(cs, false, SF_WRITABLE); + SocketEngine::Change(cs, true, SF_READABLE); + cs->OnAccept(); + return SF_ACCEPTED; + } +} + +void SSLSocketIO::Connect(ConnectionSocket *s, const Anope::string &target, int port) +{ + if (s->io == &NormalSocketIO) + throw SocketException("Attempting to connect uninitialized socket with SSL"); + + s->flags[SF_CONNECTING] = s->flags[SF_CONNECTED] = false; + + s->conaddr.pton(s->GetFamily(), target, port); + int c = connect(s->GetFD(), &s->conaddr.sa, s->conaddr.size()); + if (c == -1) + { + if (Anope::LastErrorCode() != EINPROGRESS) + { + s->OnError(Anope::LastError()); + s->flags[SF_DEAD] = true; + return; + } + else + { + SocketEngine::Change(s, true, SF_WRITABLE); + s->flags[SF_CONNECTING] = true; + return; + } + } + else + { + s->flags[SF_CONNECTING] = true; + this->FinishConnect(s); + } +} + +SocketFlag SSLSocketIO::FinishConnect(ConnectionSocket *s) +{ + if (s->io == &NormalSocketIO) + throw SocketException("Attempting to finish connect uninitialized socket with SSL"); + else if (s->flags[SF_CONNECTED]) + return SF_CONNECTED; + else if (!s->flags[SF_CONNECTING]) + throw SocketException("SSLSocketIO::FinishConnect called for a socket not connected nor connecting?"); + + SSLSocketIO *io = anope_dynamic_static_cast(s->io); + + if (io->sslsock == NULL) + { + io->sslsock = SSL_new(client_ctx); + if (!io->sslsock) + throw SocketException("Unable to initialize SSL socket"); + + if (!SSL_set_fd(io->sslsock, s->GetFD())) + throw SocketException("Unable to set SSL fd"); + } + + int ret = SSL_connect(io->sslsock); + if (ret <= 0) + { + int error = SSL_get_error(io->sslsock, ret); + if (ret == -1 && (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE)) + { + SocketEngine::Change(s, error == SSL_ERROR_WANT_WRITE, SF_WRITABLE); + SocketEngine::Change(s, error == SSL_ERROR_WANT_READ, SF_READABLE); + return SF_CONNECTING; + } + else + { + s->OnError(ERR_error_string(ERR_get_error(), NULL)); + s->flags[SF_CONNECTING] = false; + s->flags[SF_DEAD] = true; + return SF_DEAD; + } + } + else + { + s->flags[SF_CONNECTING] = false; + s->flags[SF_CONNECTED] = true; + SocketEngine::Change(s, false, SF_WRITABLE); + SocketEngine::Change(s, true, SF_READABLE); + s->OnConnect(); + return SF_CONNECTED; + } +} + +void SSLSocketIO::Destroy() +{ + if (this->sslsock) + { + SSL_shutdown(this->sslsock); + SSL_free(this->sslsock); + } + + delete this; +} + +MODULE_INIT(SSLModule) -- cgit