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. --- .github/workflows/ci-linux.yml | 4 +- .gitignore | 27 +- data/anope.example.conf | 6 +- data/chanstats.example.conf | 2 +- data/modules.example.conf | 122 ++-- data/operserv.example.conf | 2 +- data/stats.standalone.example.conf | 4 +- docs/Changes.conf | 1 + docs/INSTALL | 2 +- docs/INSTALL.fr | 2 +- docs/WIN32.txt | 2 +- docs/XMLRPC/XMLRPC | 2 +- modules/CMakeLists.txt | 20 +- modules/dns.cpp | 1126 +++++++++++++++++++++++++++++++ modules/dnsbl.cpp | 196 ++++++ 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 ++++++++++++ modules/helpchan.cpp | 33 + modules/httpd.cpp | 469 +++++++++++++ modules/m_dns.cpp | 1126 ------------------------------- modules/m_dnsbl.cpp | 196 ------ modules/m_helpchan.cpp | 33 - modules/m_httpd.cpp | 469 ------------- modules/m_proxyscan.cpp | 388 ----------- modules/m_redis.cpp | 618 ----------------- modules/m_regex_stdlib.cpp | 107 --- modules/m_rewrite.cpp | 185 ----- modules/m_sasl.cpp | 414 ------------ modules/m_xmlrpc.cpp | 229 ------- modules/m_xmlrpc_main.cpp | 306 --------- modules/proxyscan.cpp | 388 +++++++++++ modules/redis.cpp | 618 +++++++++++++++++ modules/regex_stdlib.cpp | 107 +++ modules/rewrite.cpp | 185 +++++ modules/sasl.cpp | 414 ++++++++++++ modules/stats/chanstats.cpp | 660 ++++++++++++++++++ modules/stats/cs_fantasy_stats.cpp | 6 +- modules/stats/cs_fantasy_top.cpp | 6 +- modules/stats/irc2sql/irc2sql.cpp | 2 +- modules/stats/irc2sql/irc2sql.h | 4 +- modules/stats/m_chanstats.cpp | 660 ------------------ modules/webcpanel/webcpanel.cpp | 2 +- modules/xmlrpc.cpp | 229 +++++++ modules/xmlrpc_main.cpp | 306 +++++++++ 68 files changed, 8681 insertions(+), 8681 deletions(-) create mode 100644 modules/dns.cpp create mode 100644 modules/dnsbl.cpp 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 create mode 100644 modules/helpchan.cpp create mode 100644 modules/httpd.cpp delete mode 100644 modules/m_dns.cpp delete mode 100644 modules/m_dnsbl.cpp delete mode 100644 modules/m_helpchan.cpp delete mode 100644 modules/m_httpd.cpp delete mode 100644 modules/m_proxyscan.cpp delete mode 100644 modules/m_redis.cpp delete mode 100644 modules/m_regex_stdlib.cpp delete mode 100644 modules/m_rewrite.cpp delete mode 100644 modules/m_sasl.cpp delete mode 100644 modules/m_xmlrpc.cpp delete mode 100644 modules/m_xmlrpc_main.cpp create mode 100644 modules/proxyscan.cpp create mode 100644 modules/redis.cpp create mode 100644 modules/regex_stdlib.cpp create mode 100644 modules/rewrite.cpp create mode 100644 modules/sasl.cpp create mode 100644 modules/stats/chanstats.cpp delete mode 100644 modules/stats/m_chanstats.cpp create mode 100644 modules/xmlrpc.cpp create mode 100644 modules/xmlrpc_main.cpp diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index e30226cdc..8aebe5b4e 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -28,9 +28,9 @@ jobs: ninja-build - name: Enable extras run: | - for MODULE in m_ldap.cpp m_ldap_authentication.cpp m_ldap_oper.cpp m_mysql.cpp m_regex_pcre2.cpp m_regex_posix.cpp m_regex_tre.cpp m_sql_authentication.cpp m_sql_log.cpp m_sql_oper.cpp m_sqlite.cpp m_ssl_gnutls.cpp m_ssl_openssl.cpp + for MODULE in ldap ldap_authentication ldap_oper mysql regex_pcre2 regex_posix regex_tre sql_authentication sql_log sql_oper sqlite ssl_gnutls ssl_openssl do - ln -s ${{ github.workspace }}/modules/extra/$MODULE ${{ github.workspace }}/modules + ln -s ${{ github.workspace }}/modules/extra/$MODULE.cpp ${{ github.workspace }}/modules done - name: Run CMake run: | diff --git a/.gitignore b/.gitignore index 4e0d9badf..1495a75a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,19 @@ build/ config.cache include/sysconf.h -modules/m_ldap.cpp -modules/m_ldap_authentication.cpp -modules/m_ldap_oper.cpp -modules/m_mysql.cpp -modules/m_regex_pcre.cpp -modules/m_regex_pcre2.cpp -modules/m_regex_posix.cpp -modules/m_regex_tre.cpp -modules/m_sql_authentication.cpp -modules/m_sql_log.cpp -modules/m_sql_oper.cpp -modules/m_sqlite.cpp -modules/m_ssl_gnutls.cpp -modules/m_ssl_openssl.cpp +modules/ldap.cpp +modules/ldap_authentication.cpp +modules/ldap_oper.cpp +modules/mysql.cpp +modules/regex_pcre2.cpp +modules/regex_posix.cpp +modules/regex_tre.cpp +modules/sql_authentication.cpp +modules/sql_log.cpp +modules/sql_oper.cpp +modules/sqlite.cpp +modules/ssl_gnutls.cpp +modules/ssl_openssl.cpp modules/stats run/ *.mo diff --git a/data/anope.example.conf b/data/anope.example.conf index 62eaa8154..393eeaa46 100644 --- a/data/anope.example.conf +++ b/data/anope.example.conf @@ -1199,7 +1199,7 @@ module * db_redis. * * This module allows using Redis (https://redis.io/) as a database backend. - * This module requires that m_redis is loaded and configured properly. + * This module requires that redis is loaded and configured properly. * * Redis 2.8 supports keyspace notifications which allows Redis to push notifications * to Anope about outside modifications to the database. This module supports this and @@ -1211,7 +1211,7 @@ module name = "db_redis" /* - * Redis database to use. This must be configured with m_redis. + * Redis database to use. This must be configured with redis. */ engine = "redis/main" } @@ -1225,7 +1225,7 @@ module * * Without any encryption modules loaded users will not be able to authenticate unless * there is another module loaded that provides authentication checking, such as - * m_ldap_authentication or m_sql_authentication. + * ldap_authentication or sql_authentication. * * With enc_none, passwords will be stored in plain text, allowing for passwords * to be recovered later but it isn't secure and therefore is not recommended. diff --git a/data/chanstats.example.conf b/data/chanstats.example.conf index 338b93306..31e6897a6 100644 --- a/data/chanstats.example.conf +++ b/data/chanstats.example.conf @@ -5,7 +5,7 @@ module { - name = "m_chanstats" + name = "chanstats" /* * The name of this engine. diff --git a/data/modules.example.conf b/data/modules.example.conf index fb4f6a6d9..d60c83cce 100644 --- a/data/modules.example.conf +++ b/data/modules.example.conf @@ -19,14 +19,14 @@ module { name = "help" } /* - * m_dns + * dns * * Adds support for the DNS protocol. By itself this module does nothing useful, - * but other modules such as m_dnsbl and os_dns require this. + * but other modules such as dnsbl and os_dns require this. */ #module { - name = "m_dns" + name = "dns" /* * The nameserver to use for resolving hostnames, must be an IP or a resolver configuration file. @@ -80,7 +80,7 @@ module { name = "help" } } /* - * m_dnsbl + * dnsbl * * Allows configurable DNS blacklists to check connecting users against. If a user * is found on the blacklist they will be immediately banned. This is a crucial module @@ -88,7 +88,7 @@ module { name = "help" } */ #module { - name = "m_dnsbl" + name = "dnsbl" /* * If set, Anope will check clients against the DNSBLs when services connect to its uplink. @@ -179,19 +179,19 @@ module { name = "help" } } /* - * m_helpchan + * helpchan * * Gives users who are op in the specified help channel usermode +h (helpop). */ #module { - name = "m_helpchan" + name = "helpchan" helpchannel = "#help" } /* - * m_httpd + * httpd * * Allows services to serve web pages. By itself, this module does nothing useful. * @@ -200,7 +200,7 @@ module { name = "help" } */ #module { - name = "m_httpd" + name = "httpd" httpd { @@ -233,13 +233,13 @@ module { name = "help" } } /* - * m_ldap [EXTRA] + * ldap [EXTRA] * * This module allows other modules to use LDAP. By itself, this module does nothing useful. */ #module { - name = "m_ldap" + name = "ldap" ldap { @@ -254,14 +254,14 @@ module { name = "help" } } /* - * m_ldap_authentication [EXTRA] + * ldap_authentication [EXTRA] * * This module allows many commands such as IDENTIFY, RELEASE, RECOVER, GHOST, etc. use - * LDAP to authenticate users. Requires m_ldap. + * LDAP to authenticate users. Requires ldap. */ #module { - name = "m_ldap_authentication" + name = "ldap_authentication" /* * The distinguished name used for searching for users's accounts. @@ -314,16 +314,16 @@ module { name = "help" } } /* - * m_ldap_oper [EXTRA] + * ldap_oper [EXTRA] * * This module dynamically ties users to Anope opertypes when they identify - * via LDAP group membership. Requires m_ldap. + * via LDAP group membership. Requires ldap. * * Note that this doesn't give the user privileges on the IRCd, only in Anope. */ #module { - name = "m_ldap_oper" + name = "ldap_oper" /* * An optional binddn to use when searching for groups. @@ -355,13 +355,13 @@ module { name = "help" } } /* - * m_mysql [EXTRA] + * mysql [EXTRA] * * This module allows other modules to use MySQL. */ #module { - name = "m_mysql" + name = "mysql" mysql { @@ -376,13 +376,13 @@ module { name = "help" } } /* - * m_redis + * redis * * This module allows other modules to use Redis. */ #module { - name = "m_redis" + name = "redis" /* A redis database */ redis @@ -401,29 +401,29 @@ module { name = "help" } } /* - * m_regex_pcre2 [EXTRA] + * regex_pcre2 [EXTRA] * * Provides the regex engine regex/pcre, which uses version 2 of the Perl Compatible Regular * Expressions library. */ -#module { name = "m_regex_pcre2" } +#module { name = "regex_pcre2" } /* - * m_regex_posix [EXTRA] + * regex_posix [EXTRA] * * Provides the regex engine regex/posix, which uses the POSIX compliant regular expressions. */ -#module { name = "m_regex_posix" } +#module { name = "regex_posix" } /* - * m_regex_stdlib + * regex_stdlib * * Provides the regex engine regex/stdlib, which uses the regular expression library that is part of * the C++ standard library. */ module { - name = "m_regex_stdlib" + name = "regex_stdlib" /* * The syntax scheme to use. Can be set to awk to use the regular expression grammar used by the @@ -439,23 +439,23 @@ module } /* - * m_regex_tre [EXTRA] + * regex_tre [EXTRA] * * Provides the regex engine regex/tre, which uses the TRE regex library. */ -#module { name = "m_regex_tre" } +#module { name = "regex_tre" } /* - * m_rewrite + * rewrite * * Allows rewriting commands sent to/from clients. */ -#module { name = "m_rewrite" } +#module { name = "rewrite" } #command { service = "ChanServ"; name = "CLEAR"; command = "rewrite" - /* Enable m_rewrite. */ + /* Enable rewrite. */ rewrite = true /* Source message to match. A $ can be used to match anything. */ @@ -476,7 +476,7 @@ module } /* - * m_proxyscan + * proxyscan * * This module allows you to scan connecting clients for open proxies. * Note that using this will allow users to get the IP of your services. @@ -490,7 +490,7 @@ module */ #module { - name = "m_proxyscan" + name = "proxyscan" /* * The target IP services tells the proxy to connect back to. This must be a publicly @@ -558,29 +558,29 @@ module } /* - * m_sasl + * sasl * * Some IRCds allow "SASL" authentication to let users identify to services * during the IRCd user registration process. If this module is loaded, Anope will allow * authenticating users through this mechanism. Supported mechanisms are: * PLAIN, EXTERNAL. */ -module { name = "m_sasl" } +module { name = "sasl" } /* - * m_ssl_gnutls [EXTRA] + * ssl_gnutls [EXTRA] * * This module provides SSL services to Anope using GnuTLS, for example to * connect to the uplink server(s) via SSL. * - * You may only load either m_ssl_gnutls or m_ssl_openssl, but not both. + * You may only load either ssl_gnutls or ssl_openssl, but not both. */ #module { - name = "m_ssl_gnutls" + name = "ssl_gnutls" /* - * An optional certificate and key for m_ssl_gnutls to give to the uplink. + * An optional certificate and key for ssl_gnutls to give to the uplink. * * You can generate your own certificate and key pair by using: * @@ -606,20 +606,20 @@ module { name = "m_sasl" } } /* - * m_ssl_openssl [EXTRA] + * ssl_openssl [EXTRA] * * This module provides SSL services to Anope using OpenSSL, for example to * connect to the uplink server(s) via SSL. * - * You may only load either m_ssl_openssl or m_ssl_gnutls, but not both. + * You may only load either ssl_openssl or ssl_gnutls, but not both. * */ #module { - name = "m_ssl_openssl" + name = "ssl_openssl" /* - * An optional certificate and key for m_ssl_openssl to give to the uplink. + * An optional certificate and key for ssl_openssl to give to the uplink. * * You can generate your own certificate and key pair by using: * @@ -640,16 +640,16 @@ module { name = "m_sasl" } } /* - * m_sql_authentication [EXTRA] + * sql_authentication [EXTRA] * * This module allows authenticating users against an external SQL database using a custom * query. */ #module { - name = "m_sql_authentication" + name = "sql_authentication" - /* SQL engine to use. Should be configured elsewhere with m_mysql, m_sqlite, etc. */ + /* SQL engine to use. Should be configured elsewhere with mysql, sqlite, etc. */ engine = "mysql/main" /* Query to execute to authenticate. A non empty result from this query is considered a success, @@ -691,7 +691,7 @@ module { name = "m_sasl" } } /* - * m_sql_log [EXTRA] + * sql_log [EXTRA] * * This module adds an additional target option to log{} blocks * that allows logging Service's logs to SQL. To log to SQL, add @@ -708,19 +708,19 @@ module { name = "m_sasl" } * it if it doesn't exist. This module does not create any indexes (keys) * on the table and it is recommended you add them yourself as necessary. */ -#module { name = "m_sql_log" } +#module { name = "sql_log" } /* - * m_sql_oper [EXTRA] + * sql_oper [EXTRA] * * This module allows granting users services operator privileges and possibly IRC Operator * privileges based on an external SQL database using a custom query. */ #module { - name = "m_sql_oper" + name = "sql_oper" - /* SQL engine to use. Should be configured elsewhere with m_mysql, m_sqlite, etc. */ + /* SQL engine to use. Should be configured elsewhere with mysql, sqlite, etc. */ engine = "mysql/main" /* Query to execute to determine if a user should have operator privileges. @@ -737,13 +737,13 @@ module { name = "m_sasl" } } /* - * m_sqlite [EXTRA] + * sqlite [EXTRA] * * This module allows other modules to use SQLite. */ #module { - name = "m_sqlite" + name = "sqlite" /* A SQLite database */ sqlite @@ -763,7 +763,7 @@ module { name = "m_sasl" } * as they could over IRC. If you are using the default configuration you should be able to access * this panel by visiting http://127.0.0.1:8080 in your web browser from the machine Anope is running on. * - * This module requires m_httpd. + * This module requires httpd. */ #module { @@ -780,23 +780,23 @@ module { name = "m_sasl" } } /* - * m_xmlrpc + * xmlrpc * * Allows remote applications (websites) to execute queries in real time to retrieve data from Anope. * By itself this module does nothing, but allows other modules (m_xmlrpc_main) to receive and send XMLRPC queries. */ #module { - name = "m_xmlrpc" + name = "xmlrpc" - /* Web service to use. Requires m_httpd. */ + /* Web service to use. Requires httpd. */ server = "httpd/main" } /* - * m_xmlrpc_main + * xmlrpc_main * * Adds the main XMLRPC core functions. - * Requires m_xmlrpc. + * Requires xmlrpc. */ -#module { name = "m_xmlrpc_main" } +#module { name = "xmlrpc_main" } diff --git a/data/operserv.example.conf b/data/operserv.example.conf index d4cfeda0b..e23efbb47 100644 --- a/data/operserv.example.conf +++ b/data/operserv.example.conf @@ -267,7 +267,7 @@ command { service = "OperServ"; name = "CHANKILL"; command = "operserv/chankill" * * Provides the command operserv/dns. * - * This module requires that m_dns is loaded. + * This module requires that dns is loaded. * * This module allows controlling a DNS zone. This is useful for * controlling what servers users are placed on for load balancing, diff --git a/data/stats.standalone.example.conf b/data/stats.standalone.example.conf index bd40723e8..51698e795 100644 --- a/data/stats.standalone.example.conf +++ b/data/stats.standalone.example.conf @@ -483,13 +483,13 @@ log /* * [REQUIRED] MySQL Database configuration. * - * m_mysql + * mysql * * This module allows other modules to use MySQL. */ module { - name = "m_mysql" + name = "mysql" mysql { diff --git a/docs/Changes.conf b/docs/Changes.conf index 8e6da9fcf..eae81a617 100644 --- a/docs/Changes.conf +++ b/docs/Changes.conf @@ -4,6 +4,7 @@ Added module:tlsv10 from m_ssl_openssl. Added module:tlsv11 from m_ssl_openssl. Added module:tlsv12 from m_ssl_openssl. Removed module:sslv3 from m_ssl_openssl. +Removed the m_ prefix from the names of the chanstats, dns, dnsbl, helpchan, httpd, ldap, ldap_oper, mysql, proxyscan, redis, regex_pcre2, regex_posix, regex_stdlib, regex_tre, rewrite, sasl, sql_log, sql_oper, sqlite, ssl_gnutls, ssl_openssl, xmlrpc, and xmlrpc_main modules. Anope Version 2.1.1 ------------------- diff --git a/docs/INSTALL b/docs/INSTALL index cbd170804..2a643429a 100644 --- a/docs/INSTALL +++ b/docs/INSTALL @@ -37,7 +37,7 @@ Note: You should also read the README and FAQ files! Next, unpack the package in your home directory, and go into the created directory. - If there are any extra modules you want to enable, such as m_mysql, run + If there are any extra modules you want to enable, such as mysql, run the 'extras' script to enable them. If you do not know you can come back later and enable them. diff --git a/docs/INSTALL.fr b/docs/INSTALL.fr index f80aeb9d6..bdfbccc53 100644 --- a/docs/INSTALL.fr +++ b/docs/INSTALL.fr @@ -37,7 +37,7 @@ Note : Vous devrez également lire les fichiers README et FAQ ! Ensuite, décompressez le package dans votre répertoire home, et allez dans le répértoire qui vient d'être créé. - Si il y a des modules facultatifs que vous voulez activer comme m_mysql, + Si il y a des modules facultatifs que vous voulez activer comme mysql, exécuter le script 'extras' pour les activer. Si vous ne savez pas, vous pouvez les activer plus tard. diff --git a/docs/WIN32.txt b/docs/WIN32.txt index 104ef7441..bf7153010 100644 --- a/docs/WIN32.txt +++ b/docs/WIN32.txt @@ -70,7 +70,7 @@ Anope for Windows If you cannot find whats causing the error, please visit our forums or our IRC Support channel for assistance. - Some Anope modules require third party libraries, such as m_mysql and + Some Anope modules require third party libraries, such as mysql and the SSL modules. If these libraries are installed in nonstandard locations, cmake will probably not find them and should be told where they are by passing their location to Config. diff --git a/docs/XMLRPC/XMLRPC b/docs/XMLRPC/XMLRPC index 52f2248a6..7a64556db 100644 --- a/docs/XMLRPC/XMLRPC +++ b/docs/XMLRPC/XMLRPC @@ -1,7 +1,7 @@ Starting with Anope 1.9.4 XMLRPC using PHP's xmlrpc_encode_request and xmlrpc_decode functions is supported. This allows external applications, such as websites, to execute remote procedure calls to Anope in real time. -Currently there are 5 supported XMLRPC calls, provided by m_xmlrpc_main: +Currently there are 5 supported XMLRPC calls, provided by xmlrpc_main: checkAuthentication - Takes two parameters, an account name and a password. Checks if the account name is valid and the password is correct for the account name, useful for making login pages on websites. diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 8f9455572..df45c211d 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -19,18 +19,18 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../conanbuildinfo.cmake") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/extra/${NAME}.cpp" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}") endfunction() - enable_extra("m_mysql" "LIBMYSQLCLIENT") - enable_extra("m_regex_pcre2" "PCRE2") - enable_extra("m_sqlite" "SQLITE3") - enable_extra("m_ssl_openssl" "OPENSSL") + enable_extra("mysql" "LIBMYSQLCLIENT") + enable_extra("regex_pcre2" "PCRE2") + enable_extra("sqlite" "SQLITE3") + enable_extra("ssl_openssl" "OPENSSL") # this uses Wldap so should always be available - copy_extra("m_ldap") + copy_extra("ldap") # these don't actually have extra dependencies, but require a module which does - copy_extra("m_sql_authentication") - copy_extra("m_sql_log") - copy_extra("m_sql_oper") - copy_extra("m_ldap_authentication") - copy_extra("m_ldap_oper") + copy_extra("sql_authentication") + copy_extra("sql_log") + copy_extra("sql_oper") + copy_extra("ldap_authentication") + copy_extra("ldap_oper") # Package extra dlls file(GLOB EXTRA_DLLS "${Anope_SOURCE_DIR}/extradll/bin/*.dll" "${Anope_SOURCE_DIR}/extradll/lib/*.dll") diff --git a/modules/dns.cpp b/modules/dns.cpp new file mode 100644 index 000000000..a75ca6820 --- /dev/null +++ b/modules/dns.cpp @@ -0,0 +1,1126 @@ +/* + * + * (C) 2003-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. + */ + +#include "module.h" +#include "modules/dns.h" + +using namespace DNS; + +namespace +{ + Anope::string admin, nameservers; + int refresh; + time_t timeout; +} + +/** A full packet sent or received to/from the nameserver + */ +class Packet final + : public Query +{ + static bool IsValidName(const Anope::string &name) + { + return name.find_first_not_of("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-") == Anope::string::npos; + } + + void PackName(unsigned char *output, unsigned short output_size, unsigned short &pos, const Anope::string &name) + { + if (pos + name.length() + 2 > output_size) + throw SocketException("Unable to pack name"); + + Log(LOG_DEBUG_2) << "Resolver: PackName packing " << name; + + sepstream sep(name, '.'); + Anope::string token; + + while (sep.GetToken(token)) + { + output[pos++] = token.length(); + memcpy(&output[pos], token.c_str(), token.length()); + pos += token.length(); + } + + output[pos++] = 0; + } + + Anope::string UnpackName(const unsigned char *input, unsigned short input_size, unsigned short &pos) + { + Anope::string name; + unsigned short pos_ptr = pos, lowest_ptr = input_size; + bool compressed = false; + + if (pos_ptr >= input_size) + throw SocketException("Unable to unpack name - no input"); + + while (input[pos_ptr] > 0) + { + unsigned short offset = input[pos_ptr]; + + if (offset & POINTER) + { + if ((offset & POINTER) != POINTER) + throw SocketException("Unable to unpack name - bogus compression header"); + if (pos_ptr + 1 >= input_size) + throw SocketException("Unable to unpack name - bogus compression header"); + + /* Place pos at the second byte of the first (farthest) compression pointer */ + if (compressed == false) + { + ++pos; + compressed = true; + } + + pos_ptr = (offset & LABEL) << 8 | input[pos_ptr + 1]; + + /* Pointers can only go back */ + if (pos_ptr >= lowest_ptr) + throw SocketException("Unable to unpack name - bogus compression pointer"); + lowest_ptr = pos_ptr; + } + else + { + if (pos_ptr + offset + 1 >= input_size) + throw SocketException("Unable to unpack name - offset too large"); + if (!name.empty()) + name += "."; + for (unsigned i = 1; i <= offset; ++i) + name += input[pos_ptr + i]; + + pos_ptr += offset + 1; + if (compressed == false) + /* Move up pos */ + pos = pos_ptr; + } + } + + /* +1 pos either to one byte after the compression pointer or one byte after the ending \0 */ + ++pos; + + /* Empty names are valid (root domain) */ + + Log(LOG_DEBUG_2) << "Resolver: UnpackName successfully unpacked " << name; + + return name; + } + + Question UnpackQuestion(const unsigned char *input, unsigned short input_size, unsigned short &pos) + { + Question question; + + question.name = this->UnpackName(input, input_size, pos); + + if (pos + 4 > input_size) + throw SocketException("Unable to unpack question"); + + if (!IsValidName(question.name)) + throw SocketException("Invalid question name"); + + question.type = static_cast(input[pos] << 8 | input[pos + 1]); + pos += 2; + + question.qclass = input[pos] << 8 | input[pos + 1]; + pos += 2; + + return question; + } + + ResourceRecord UnpackResourceRecord(const unsigned char *input, unsigned short input_size, unsigned short &pos) + { + ResourceRecord record = static_cast(this->UnpackQuestion(input, input_size, pos)); + + if (pos + 6 > input_size) + throw SocketException("Unable to unpack resource record"); + + record.ttl = (input[pos] << 24) | (input[pos + 1] << 16) | (input[pos + 2] << 8) | input[pos + 3]; + pos += 4; + + //record.rdlength = input[pos] << 8 | input[pos + 1]; + pos += 2; + + switch (record.type) + { + case QUERY_A: + { + if (pos + 4 > input_size) + throw SocketException("Unable to unpack resource record"); + + in_addr a; + a.s_addr = input[pos] | (input[pos + 1] << 8) | (input[pos + 2] << 16) | (input[pos + 3] << 24); + pos += 4; + + sockaddrs addrs; + addrs.ntop(AF_INET, &a); + if (!addrs.valid()) + throw SocketException("Invalid IP"); + + record.rdata = addrs.addr(); + break; + } + case QUERY_AAAA: + { + if (pos + 16 > input_size) + throw SocketException("Unable to unpack resource record"); + + in6_addr a; + for (int j = 0; j < 16; ++j) + a.s6_addr[j] = input[pos + j]; + pos += 16; + + sockaddrs addrs; + addrs.ntop(AF_INET6, &a); + if (!addrs.valid()) + throw SocketException("Invalid IP"); + + record.rdata = addrs.addr(); + break; + } + case QUERY_CNAME: + case QUERY_PTR: + { + record.rdata = this->UnpackName(input, input_size, pos); + + if (!IsValidName(record.rdata)) + throw SocketException("Invalid cname/ptr record data"); + + break; + } + default: + break; + } + + Log(LOG_DEBUG_2) << "Resolver: " << record.name << " -> " << record.rdata; + + return record; + } + +public: + static const int POINTER = 0xC0; + static const int LABEL = 0x3F; + static const int HEADER_LENGTH = 12; + + Manager *manager; + /* Source or destination of the packet */ + sockaddrs addr; + /* ID for this packet */ + unsigned short id = 0; + /* Flags on the packet */ + unsigned short flags = 0; + + Packet(Manager *m, sockaddrs *a) : manager(m) + { + if (a) + addr = *a; + } + + void Fill(const unsigned char *input, const unsigned short len) + { + if (len < HEADER_LENGTH) + throw SocketException("Unable to fill packet"); + + unsigned short packet_pos = 0; + + this->id = (input[packet_pos] << 8) | input[packet_pos + 1]; + packet_pos += 2; + + this->flags = (input[packet_pos] << 8) | input[packet_pos + 1]; + packet_pos += 2; + + unsigned short qdcount = (input[packet_pos] << 8) | input[packet_pos + 1]; + packet_pos += 2; + + unsigned short ancount = (input[packet_pos] << 8) | input[packet_pos + 1]; + packet_pos += 2; + + unsigned short nscount = (input[packet_pos] << 8) | input[packet_pos + 1]; + packet_pos += 2; + + unsigned short arcount = (input[packet_pos] << 8) | input[packet_pos + 1]; + packet_pos += 2; + + Log(LOG_DEBUG_2) << "Resolver: qdcount: " << qdcount << " ancount: " << ancount << " nscount: " << nscount << " arcount: " << arcount; + + for (unsigned i = 0; i < qdcount; ++i) + this->questions.push_back(this->UnpackQuestion(input, len, packet_pos)); + + for (unsigned i = 0; i < ancount; ++i) + this->answers.push_back(this->UnpackResourceRecord(input, len, packet_pos)); + + try + { + for (unsigned i = 0; i < nscount; ++i) + this->authorities.push_back(this->UnpackResourceRecord(input, len, packet_pos)); + + for (unsigned i = 0; i < arcount; ++i) + this->additional.push_back(this->UnpackResourceRecord(input, len, packet_pos)); + } + catch (const SocketException &ex) + { + Log(LOG_DEBUG_2) << "Unable to parse ns/ar records: " << ex.GetReason(); + } + } + + unsigned short Pack(unsigned char *output, unsigned short output_size) + { + if (output_size < HEADER_LENGTH) + throw SocketException("Unable to pack packet"); + + unsigned short pos = 0; + + output[pos++] = this->id >> 8; + output[pos++] = this->id & 0xFF; + output[pos++] = this->flags >> 8; + output[pos++] = this->flags & 0xFF; + output[pos++] = this->questions.size() >> 8; + output[pos++] = this->questions.size() & 0xFF; + output[pos++] = this->answers.size() >> 8; + output[pos++] = this->answers.size() & 0xFF; + output[pos++] = this->authorities.size() >> 8; + output[pos++] = this->authorities.size() & 0xFF; + output[pos++] = this->additional.size() >> 8; + output[pos++] = this->additional.size() & 0xFF; + + for (auto &q : this->questions) + { + if (q.type == QUERY_PTR) + { + sockaddrs ip(q.name); + if (!ip.valid()) + throw SocketException("Invalid IP"); + + switch (ip.family()) + { + case AF_INET6: + q.name = ip.reverse() + ".ip6.arpa"; + break; + case AF_INET: + q.name = ip.reverse() + ".in-addr.arpa"; + break; + default: + throw SocketException("Unsupported IP Family"); + } + } + + this->PackName(output, output_size, pos, q.name); + + if (pos + 4 >= output_size) + throw SocketException("Unable to pack packet"); + + short s = htons(q.type); + memcpy(&output[pos], &s, 2); + pos += 2; + + s = htons(q.qclass); + memcpy(&output[pos], &s, 2); + pos += 2; + } + + std::vector types[] = { this->answers, this->authorities, this->additional }; + for (auto &type : types) + { + for (const auto &rr : type) + { + this->PackName(output, output_size, pos, rr.name); + + if (pos + 8 >= output_size) + throw SocketException("Unable to pack packet"); + + short s = htons(rr.type); + memcpy(&output[pos], &s, 2); + pos += 2; + + s = htons(rr.qclass); + memcpy(&output[pos], &s, 2); + pos += 2; + + long l = htonl(rr.ttl); + memcpy(&output[pos], &l, 4); + pos += 4; + + switch (rr.type) + { + case QUERY_A: + { + if (pos + 6 > output_size) + throw SocketException("Unable to pack packet"); + + sockaddrs a(rr.rdata); + if (!a.valid()) + throw SocketException("Invalid IP"); + + s = htons(4); + memcpy(&output[pos], &s, 2); + pos += 2; + + memcpy(&output[pos], &a.sa4.sin_addr, 4); + pos += 4; + break; + } + case QUERY_AAAA: + { + if (pos + 18 > output_size) + throw SocketException("Unable to pack packet"); + + sockaddrs a(rr.rdata); + if (!a.valid()) + throw SocketException("Invalid IP"); + + s = htons(16); + memcpy(&output[pos], &s, 2); + pos += 2; + + memcpy(&output[pos], &a.sa6.sin6_addr, 16); + pos += 16; + break; + } + case QUERY_NS: + case QUERY_CNAME: + case QUERY_PTR: + { + if (pos + 2 >= output_size) + throw SocketException("Unable to pack packet"); + + unsigned short packet_pos_save = pos; + pos += 2; + + this->PackName(output, output_size, pos, rr.rdata); + + s = htons(pos - packet_pos_save - 2); + memcpy(&output[packet_pos_save], &s, 2); + break; + } + case QUERY_SOA: + { + if (pos + 2 >= output_size) + throw SocketException("Unable to pack packet"); + + unsigned short packet_pos_save = pos; + pos += 2; + + std::vector ns; + spacesepstream(nameservers).GetTokens(ns); + this->PackName(output, output_size, pos, !ns.empty() ? ns[0] : ""); + this->PackName(output, output_size, pos, admin.replace_all_cs('@', '.')); + + if (pos + 20 >= output_size) + throw SocketException("Unable to pack SOA"); + + l = htonl(manager->GetSerial()); + memcpy(&output[pos], &l, 4); + pos += 4; + + l = htonl(refresh); // Refresh + memcpy(&output[pos], &l, 4); + pos += 4; + + l = htonl(refresh); // Retry + memcpy(&output[pos], &l, 4); + pos += 4; + + l = htonl(604800); // Expire + memcpy(&output[pos], &l, 4); + pos += 4; + + l = htonl(0); // Minimum + memcpy(&output[pos], &l, 4); + pos += 4; + + s = htons(pos - packet_pos_save - 2); + memcpy(&output[packet_pos_save], &s, 2); + + break; + } + default: + break; + } + } + } + + return pos; + } +}; + +namespace DNS +{ + class ReplySocket + : public virtual Socket + { + public: + ~ReplySocket() override = default; + virtual void Reply(Packet *p) = 0; + }; +} + +/* Listens for TCP requests */ +class TCPSocket final + : public ListenSocket +{ + Manager *manager; + +public: + /* A TCP client */ + class Client final + : public ClientSocket + , public Timer + , public ReplySocket + { + Manager *manager; + Packet *packet = nullptr; + unsigned char packet_buffer[524]; + int length = 0; + + public: + Client(Manager *m, TCPSocket *l, int fd, const sockaddrs &addr) : Socket(fd, l->GetFamily()), ClientSocket(l, addr), Timer(5), + manager(m) + { + Log(LOG_DEBUG_2) << "Resolver: New client from " << addr.addr(); + } + + ~Client() override + { + Log(LOG_DEBUG_2) << "Resolver: Exiting client from " << clientaddr.addr(); + delete packet; + } + + /* Times out after a few seconds */ + void Tick(time_t) override { } + + void Reply(Packet *p) override + { + delete packet; + packet = p; + SocketEngine::Change(this, true, SF_WRITABLE); + } + + bool ProcessRead() override + { + Log(LOG_DEBUG_2) << "Resolver: Reading from DNS TCP socket"; + + int i = recv(this->GetFD(), reinterpret_cast(packet_buffer) + length, sizeof(packet_buffer) - length, 0); + if (i <= 0) + return false; + + length += i; + + unsigned short want_len = packet_buffer[0] << 8 | packet_buffer[1]; + if (length >= want_len + 2) + { + int len = length - 2; + length -= want_len + 2; + return this->manager->HandlePacket(this, packet_buffer + 2, len, NULL); + } + return true; + } + + bool ProcessWrite() override + { + Log(LOG_DEBUG_2) << "Resolver: Writing to DNS TCP socket"; + + if (packet != NULL) + { + try + { + unsigned char buffer[65535]; + unsigned short len = packet->Pack(buffer + 2, sizeof(buffer) - 2); + + short s = htons(len); + memcpy(buffer, &s, 2); + len += 2; + + send(this->GetFD(), reinterpret_cast(buffer), len, 0); + } + catch (const SocketException &) { } + + delete packet; + packet = NULL; + } + + SocketEngine::Change(this, false, SF_WRITABLE); + return true; /* Do not return false here, bind is unhappy we close the connection so soon after sending */ + } + }; + + TCPSocket(Manager *m, const Anope::string &ip, int port) : Socket(-1, ip.find(':') == Anope::string::npos ? AF_INET : AF_INET6), ListenSocket(ip, port, ip.find(':') != Anope::string::npos), manager(m) { } + + ClientSocket *OnAccept(int fd, const sockaddrs &addr) override + { + return new Client(this->manager, this, fd, addr); + } +}; + +/* Listens for UDP requests */ +class UDPSocket final + : public ReplySocket +{ + Manager *manager; + std::deque packets; + +public: + UDPSocket(Manager *m, const Anope::string &ip, int port) : Socket(-1, ip.find(':') == Anope::string::npos ? AF_INET : AF_INET6, SOCK_DGRAM), manager(m) { } + + ~UDPSocket() override + { + for (const auto *packet : packets) + delete packet; + } + + void Reply(Packet *p) override + { + packets.push_back(p); + SocketEngine::Change(this, true, SF_WRITABLE); + } + + std::deque& GetPackets() { return packets; } + + bool ProcessRead() override + { + Log(LOG_DEBUG_2) << "Resolver: Reading from DNS UDP socket"; + + unsigned char packet_buffer[524]; + sockaddrs from_server; + socklen_t x = sizeof(from_server); + int length = recvfrom(this->GetFD(), reinterpret_cast(&packet_buffer), sizeof(packet_buffer), 0, &from_server.sa, &x); + return this->manager->HandlePacket(this, packet_buffer, length, &from_server); + } + + bool ProcessWrite() override + { + Log(LOG_DEBUG_2) << "Resolver: Writing to DNS UDP socket"; + + Packet *r = !packets.empty() ? packets.front() : NULL; + if (r != NULL) + { + try + { + unsigned char buffer[524]; + unsigned short len = r->Pack(buffer, sizeof(buffer)); + + sendto(this->GetFD(), reinterpret_cast(buffer), len, 0, &r->addr.sa, r->addr.size()); + } + catch (const SocketException &) { } + + delete r; + packets.pop_front(); + } + + if (packets.empty()) + SocketEngine::Change(this, false, SF_WRITABLE); + + return true; + } +}; + +class NotifySocket final + : public Socket +{ + Packet *packet; +public: + NotifySocket(int family, Packet *p) : Socket(-1, family, SOCK_DGRAM), packet(p) + { + SocketEngine::Change(this, false, SF_READABLE); + SocketEngine::Change(this, true, SF_WRITABLE); + } + + bool ProcessWrite() override + { + if (!packet) + return false; + + Log(LOG_DEBUG_2) << "Resolver: Notifying slave " << packet->addr.addr(); + + try + { + unsigned char buffer[524]; + unsigned short len = packet->Pack(buffer, sizeof(buffer)); + + sendto(this->GetFD(), reinterpret_cast(buffer), len, 0, &packet->addr.sa, packet->addr.size()); + } + catch (const SocketException &) { } + + delete packet; + packet = NULL; + + return false; + } +}; + +class MyManager final + : public Manager + , public Timer +{ + uint32_t serial; + + typedef std::unordered_map cache_map; + cache_map cache; + + TCPSocket *tcpsock = nullptr; + UDPSocket *udpsock = nullptr; + + bool listen = false; + sockaddrs addrs; + + std::vector > notify; +public: + std::map requests; + + MyManager(Module *creator) : Manager(creator), Timer(300, Anope::CurTime, true), serial(Anope::CurTime), cur_id(rand()) + { + } + + ~MyManager() override + { + delete udpsock; + delete tcpsock; + + for (std::map::iterator it = this->requests.begin(), it_end = this->requests.end(); it != it_end;) + { + Request *request = it->second; + ++it; + + Query rr(*request); + rr.error = ERROR_UNKNOWN; + request->OnError(&rr); + + delete request; + } + this->requests.clear(); + + this->cache.clear(); + } + + void SetIPPort(const Anope::string &nameserver, const Anope::string &ip, unsigned short port, std::vector > n) + { + delete udpsock; + delete tcpsock; + + udpsock = NULL; + tcpsock = NULL; + + try + { + this->addrs.pton(nameserver.find(':') != Anope::string::npos ? AF_INET6 : AF_INET, nameserver, 53); + + udpsock = new UDPSocket(this, ip, port); + + if (!ip.empty()) + { + udpsock->Bind(ip, port); + tcpsock = new TCPSocket(this, ip, port); + listen = true; + } + } + catch (const SocketException &ex) + { + Log() << "Unable to bind dns to " << ip << ":" << port << ": " << ex.GetReason(); + } + + notify = n; + } + +private: + unsigned short cur_id; + + unsigned short GetID() + { + if (this->udpsock->GetPackets().size() == 65535) + throw SocketException("DNS queue full"); + + do + cur_id = (cur_id + 1) & 0xFFFF; + while (!cur_id || this->requests.count(cur_id)); + + return cur_id; + } + +public: + void Process(Request *req) override + { + Log(LOG_DEBUG_2) << "Resolver: Processing request to lookup " << req->name << ", of type " << req->type; + + if (req->use_cache && this->CheckCache(req)) + { + Log(LOG_DEBUG_2) << "Resolver: Using cached result"; + delete req; + return; + } + + if (!this->udpsock) + throw SocketException("No dns socket"); + + req->id = GetID(); + this->requests[req->id] = req; + + req->SetSecs(timeout); + + auto *p = new Packet(this, &this->addrs); + p->flags = QUERYFLAGS_RD; + p->id = req->id; + p->questions.push_back(*req); + + this->udpsock->Reply(p); + } + + void RemoveRequest(Request *req) override + { + this->requests.erase(req->id); + } + + bool HandlePacket(ReplySocket *s, const unsigned char *const packet_buffer, int length, sockaddrs *from) override + { + if (length < Packet::HEADER_LENGTH) + return true; + + Packet recv_packet(this, from); + + try + { + recv_packet.Fill(packet_buffer, length); + } + catch (const SocketException &ex) + { + Log(LOG_DEBUG_2) << ex.GetReason(); + return true; + } + + if (!(recv_packet.flags & QUERYFLAGS_QR)) + { + if (!listen) + return true; + else if (recv_packet.questions.empty()) + { + Log(LOG_DEBUG_2) << "Resolver: Received a question with no questions?"; + return true; + } + + auto *packet = new Packet(recv_packet); + packet->flags |= QUERYFLAGS_QR; /* This is a response */ + packet->flags |= QUERYFLAGS_AA; /* And we are authoritative */ + + packet->answers.clear(); + packet->authorities.clear(); + packet->additional.clear(); + + for (auto &q : recv_packet.questions) + { + if (q.type == QUERY_AXFR || q.type == QUERY_SOA) + { + ResourceRecord rr(q.name, QUERY_SOA); + packet->answers.push_back(rr); + + if (q.type == QUERY_AXFR) + { + Anope::string token; + spacesepstream sep(nameservers); + while (sep.GetToken(token)) + { + ResourceRecord rr2(q.name, QUERY_NS); + rr2.rdata = token; + packet->answers.push_back(rr2); + } + } + break; + } + } + + FOREACH_MOD(OnDnsRequest, (recv_packet, packet)); + + for (auto &q : recv_packet.questions) + { + if (q.type == QUERY_AXFR) + { + ResourceRecord rr(q.name, QUERY_SOA); + packet->answers.push_back(rr); + break; + } + } + + if (packet->answers.empty() && packet->authorities.empty() && packet->additional.empty() && packet->error == ERROR_NONE) + packet->error = ERROR_REFUSED; // usually safe, won't cause an NXDOMAIN to get cached + + s->Reply(packet); + return true; + } + + if (from == NULL) + { + Log(LOG_DEBUG_2) << "Resolver: Received an answer over TCP. This is not supported."; + return true; + } + else if (this->addrs != *from) + { + Log(LOG_DEBUG_2) << "Resolver: Received an answer from the wrong nameserver, Bad NAT or DNS forging attempt? '" << this->addrs.addr() << "' != '" << from->addr() << "'"; + return true; + } + + std::map::iterator it = this->requests.find(recv_packet.id); + if (it == this->requests.end()) + { + Log(LOG_DEBUG_2) << "Resolver: Received an answer for something we didn't request"; + return true; + } + Request *request = it->second; + + if (recv_packet.flags & QUERYFLAGS_OPCODE) + { + Log(LOG_DEBUG_2) << "Resolver: Received a nonstandard query"; + recv_packet.error = ERROR_NONSTANDARD_QUERY; + request->OnError(&recv_packet); + } + else if (recv_packet.flags & QUERYFLAGS_RCODE) + { + Error error = ERROR_UNKNOWN; + + switch (recv_packet.flags & QUERYFLAGS_RCODE) + { + case 1: + Log(LOG_DEBUG_2) << "Resolver: format error"; + error = ERROR_FORMAT_ERROR; + break; + case 2: + Log(LOG_DEBUG_2) << "Resolver: server error"; + error = ERROR_SERVER_FAILURE; + break; + case 3: + Log(LOG_DEBUG_2) << "Resolver: domain not found"; + error = ERROR_DOMAIN_NOT_FOUND; + break; + case 4: + Log(LOG_DEBUG_2) << "Resolver: not implemented"; + error = ERROR_NOT_IMPLEMENTED; + break; + case 5: + Log(LOG_DEBUG_2) << "Resolver: refused"; + error = ERROR_REFUSED; + break; + default: + break; + } + + recv_packet.error = error; + request->OnError(&recv_packet); + } + else if (recv_packet.questions.empty() || recv_packet.answers.empty()) + { + Log(LOG_DEBUG_2) << "Resolver: No resource records returned"; + recv_packet.error = ERROR_NO_RECORDS; + request->OnError(&recv_packet); + } + else + { + Log(LOG_DEBUG_2) << "Resolver: Lookup complete for " << request->name; + request->OnLookupComplete(&recv_packet); + this->AddCache(recv_packet); + } + + delete request; + return true; + } + + void UpdateSerial() override + { + serial = Anope::CurTime; + } + + void Notify(const Anope::string &zone) override + { + /* notify slaves of the update */ + for (const auto &[ip, port] : notify) + { + sockaddrs addr; + addr.pton(ip.find(':') != Anope::string::npos ? AF_INET6 : AF_INET, ip, port); + if (!addr.valid()) + return; + + auto *packet = new Packet(this, &addr); + packet->flags = QUERYFLAGS_AA | QUERYFLAGS_OPCODE_NOTIFY; + try + { + packet->id = GetID(); + } + catch (const SocketException &) + { + delete packet; + continue; + } + + packet->questions.emplace_back(zone, QUERY_SOA); + + new NotifySocket(ip.find(':') == Anope::string::npos ? AF_INET : AF_INET6, packet); + } + } + + uint32_t GetSerial() const override + { + return serial; + } + + void Tick(time_t now) override + { + Log(LOG_DEBUG_2) << "Resolver: Purging DNS cache"; + + for (cache_map::iterator it = this->cache.begin(), it_next; it != this->cache.end(); it = it_next) + { + const Query &q = it->second; + const ResourceRecord &req = q.answers[0]; + it_next = it; + ++it_next; + + if (req.created + static_cast(req.ttl) < now) + this->cache.erase(it); + } + } + +private: + /** Add a record to the dns cache + * @param r The record + */ + void AddCache(Query &r) + { + const ResourceRecord &rr = r.answers[0]; + Log(LOG_DEBUG_3) << "Resolver cache: added cache for " << rr.name << " -> " << rr.rdata << ", ttl: " << rr.ttl; + this->cache[r.questions[0]] = r; + } + + /** Check the DNS cache to see if request can be handled by a cached result + * @return true if a cached result was found. + */ + bool CheckCache(Request *request) + { + cache_map::iterator it = this->cache.find(*request); + if (it != this->cache.end()) + { + Query &record = it->second; + Log(LOG_DEBUG_3) << "Resolver: Using cached result for " << request->name; + request->OnLookupComplete(&record); + return true; + } + + return false; + } + +}; + +class ModuleDNS final + : public Module +{ + MyManager manager; + + Anope::string nameserver; + Anope::string ip; + int port; + + std::vector > notify; + +public: + ModuleDNS(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), manager(this) + { + + } + + ~ModuleDNS() override + { + 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) || dynamic_cast(s)) + delete s; + } + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *block = conf->GetModule(this); + + nameserver = block->Get("nameserver", "127.0.0.1"); + timeout = block->Get("timeout", "5"); + ip = block->Get("ip", "0.0.0.0"); + port = block->Get("port", "53"); + admin = block->Get("admin", "admin@example.com"); + nameservers = block->Get("nameservers", "ns1.example.com"); + refresh = block->Get("refresh", "3600"); + + for (int i = 0; i < block->CountBlock("notify"); ++i) + { + Configuration::Block *n = block->GetBlock("notify", i); + Anope::string nip = n->Get("ip"); + short nport = n->Get("port"); + + notify.emplace_back(nip, nport); + } + + if (Anope::IsFile(nameserver)) + { + std::ifstream f(nameserver.c_str()); + bool success = false; + + if (f.is_open()) + { + for (Anope::string server; std::getline(f, server.str());) + { + if (server.find("nameserver") == 0) + { + size_t i = server.find_first_of("123456789"); + if (i != Anope::string::npos) + { + if (server.substr(i).is_pos_number_only()) + { + nameserver = server.substr(i); + Log(LOG_DEBUG) << "Nameserver set to " << nameserver; + success = true; + break; + } + } + } + } + + f.close(); + } + + if (!success) + { + Log() << "Unable to find nameserver, defaulting to 127.0.0.1"; + nameserver = "127.0.0.1"; + } + } + + try + { + this->manager.SetIPPort(nameserver, ip, port, notify); + } + catch (const SocketException &ex) + { + throw ModuleException(ex.GetReason()); + } + } + + void OnModuleUnload(User *u, Module *m) override + { + for (std::map::iterator it = this->manager.requests.begin(), it_end = this->manager.requests.end(); it != it_end;) + { + unsigned short id = it->first; + Request *req = it->second; + ++it; + + if (req->creator == m) + { + Query rr(*req); + rr.error = ERROR_UNLOADED; + req->OnError(&rr); + + delete req; + this->manager.requests.erase(id); + } + } + } +}; + +MODULE_INIT(ModuleDNS) diff --git a/modules/dnsbl.cpp b/modules/dnsbl.cpp new file mode 100644 index 000000000..af4d686e1 --- /dev/null +++ b/modules/dnsbl.cpp @@ -0,0 +1,196 @@ +/* + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/dns.h" + +using namespace DNS; + +static ServiceReference akills("XLineManager", "xlinemanager/sgline"); +static ServiceReference dnsmanager("DNS::Manager", "dns/manager"); + +struct Blacklist final +{ + struct Reply final + { + int code = 0; + Anope::string reason; + bool allow_account = false; + + Reply() = default; + }; + + Anope::string name; + time_t bantime = 0; + Anope::string reason; + std::vector replies; + + const Reply *Find(int code) + { + for (const auto &reply : replies) + if (reply.code == code) + return &reply; + return NULL; + } +}; + +class DNSBLResolver final + : public Request +{ + Reference user; + Blacklist blacklist; + bool add_to_akill; + +public: + DNSBLResolver(Module *c, User *u, const Blacklist &b, const Anope::string &host, bool add_akill) : Request(dnsmanager, c, host, QUERY_A, true), user(u), blacklist(b), add_to_akill(add_akill) { } + + void OnLookupComplete(const Query *record) override + { + if (!user || user->Quitting()) + return; + + const ResourceRecord &ans_record = record->answers[0]; + // Replies should be in 127.0.0.0/8 + if (ans_record.rdata.find("127.") != 0) + return; + + sockaddrs sresult; + sresult.pton(AF_INET, ans_record.rdata); + int result = sresult.sa4.sin_addr.s_addr >> 24; + + const Blacklist::Reply *reply = blacklist.Find(result); + if (!blacklist.replies.empty() && !reply) + return; + + if (reply && reply->allow_account && user->Account()) + return; + + Anope::string reason = this->blacklist.reason, addr = user->ip.addr(); + reason = reason.replace_all_cs("%n", user->nick); + reason = reason.replace_all_cs("%u", user->GetIdent()); + reason = reason.replace_all_cs("%g", user->realname); + reason = reason.replace_all_cs("%h", user->host); + reason = reason.replace_all_cs("%i", addr); + reason = reason.replace_all_cs("%r", reply ? reply->reason : ""); + reason = reason.replace_all_cs("%N", Config->GetBlock("networkinfo")->Get("networkname")); + + BotInfo *OperServ = Config->GetClient("OperServ"); + Log(creator, "dnsbl", OperServ) << user->GetMask() << " (" << addr << ") appears in " << this->blacklist.name; + auto *x = new XLine("*@" + addr, OperServ ? OperServ->nick : "dnsbl", Anope::CurTime + this->blacklist.bantime, reason, XLineManager::GenerateUID()); + if (this->add_to_akill && akills) + { + akills->AddXLine(x); + akills->Send(NULL, x); + } + else + { + IRCD->SendAkill(NULL, x); + delete x; + } + } +}; + +class ModuleDNSBL final + : public Module +{ + std::vector blacklists; + std::set exempts; + bool check_on_connect; + bool check_on_netburst; + bool add_to_akill; + +public: + ModuleDNSBL(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR | EXTRA) + { + + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *block = conf->GetModule(this); + this->check_on_connect = block->Get("check_on_connect"); + this->check_on_netburst = block->Get("check_on_netburst"); + this->add_to_akill = block->Get("add_to_akill", "yes"); + + this->blacklists.clear(); + for (int i = 0; i < block->CountBlock("blacklist"); ++i) + { + Configuration::Block *bl = block->GetBlock("blacklist", i); + Blacklist blacklist; + + blacklist.name = bl->Get("name"); + if (blacklist.name.empty()) + continue; + blacklist.bantime = bl->Get("time", "4h"); + blacklist.reason = bl->Get("reason"); + + for (int j = 0; j < bl->CountBlock("reply"); ++j) + { + Configuration::Block *reply = bl->GetBlock("reply", j); + Blacklist::Reply r; + + r.code = reply->Get("code"); + r.reason = reply->Get("reason"); + r.allow_account = reply->Get("allow_account"); + + blacklist.replies.push_back(r); + } + + this->blacklists.push_back(blacklist); + } + + this->exempts.clear(); + for (int i = 0; i < block->CountBlock("exempt"); ++i) + { + Configuration::Block *bl = block->GetBlock("exempt", i); + this->exempts.insert(bl->Get("ip")); + } + } + + void OnUserConnect(User *user, bool &exempt) override + { + if (exempt || user->Quitting() || (!this->check_on_connect && !Me->IsSynced()) || !dnsmanager) + return; + + if (!this->check_on_netburst && !user->server->IsSynced()) + return; + + if (!user->ip.valid()) + /* User doesn't have a valid IP (spoof/etc) */ + return; + + if (this->blacklists.empty()) + return; + + if (this->exempts.count(user->ip.addr())) + { + Log(LOG_DEBUG) << "User " << user->nick << " is exempt from dnsbl check - ip: " << user->ip.addr(); + return; + } + + Anope::string reverse = user->ip.reverse(); + + for (const auto &b : this->blacklists) + { + Anope::string dnsbl_host = reverse + "." + b.name; + DNSBLResolver *res = NULL; + try + { + res = new DNSBLResolver(this, user, b, dnsbl_host, this->add_to_akill); + dnsmanager->Process(res); + } + catch (const SocketException &ex) + { + delete res; + Log(this) << ex.GetReason(); + } + } + } +}; + +MODULE_INIT(ModuleDNSBL) 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) diff --git a/modules/helpchan.cpp b/modules/helpchan.cpp new file mode 100644 index 000000000..061388dfe --- /dev/null +++ b/modules/helpchan.cpp @@ -0,0 +1,33 @@ +/* + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" + +class HelpChannel final + : public Module +{ +public: + HelpChannel(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR) + { + } + + EventReturn OnChannelModeSet(Channel *c, MessageSource &, ChannelMode *mode, const Anope::string ¶m) override + { + if (mode->name == "OP" && c && c->ci && c->name.equals_ci(Config->GetModule(this)->Get("helpchannel"))) + { + User *u = User::Find(param); + + if (u && c->ci->AccessFor(u).HasPriv("OPME")) + u->SetMode(Config->GetClient("OperServ"), "HELPOP"); + } + + return EVENT_CONTINUE; + } +}; + +MODULE_INIT(HelpChannel) diff --git a/modules/httpd.cpp b/modules/httpd.cpp new file mode 100644 index 000000000..c80507621 --- /dev/null +++ b/modules/httpd.cpp @@ -0,0 +1,469 @@ +/* + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/httpd.h" +#include "modules/ssl.h" + +static Anope::string BuildDate() +{ + char timebuf[64]; + struct tm *tm = localtime(&Anope::CurTime); + strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %H:%M:%S %Z", tm); + return timebuf; +} + +static Anope::string GetStatusFromCode(HTTPError err) +{ + switch (err) + { + case HTTP_ERROR_OK: + return "200 OK"; + case HTTP_FOUND: + return "302 Found"; + case HTTP_BAD_REQUEST: + return "400 Bad Request"; + case HTTP_PAGE_NOT_FOUND: + return "404 Not Found"; + case HTTP_NOT_SUPPORTED: + return "505 HTTP Version Not Supported"; + } + + return "501 Not Implemented"; +} + +class MyHTTPClient final + : public HTTPClient +{ + HTTPProvider *provider; + HTTPMessage message; + bool header_done = false, served = false; + Anope::string page_name; + Reference page; + Anope::string ip; + + unsigned content_length = 0; + + enum + { + ACTION_NONE, + ACTION_GET, + ACTION_POST + } action = ACTION_NONE; + + void Serve() + { + if (this->served) + return; + this->served = true; + + if (!this->page) + { + this->SendError(HTTP_PAGE_NOT_FOUND, "Page not found"); + return; + } + + if (std::find(this->provider->ext_ips.begin(), this->provider->ext_ips.end(), this->ip) != this->provider->ext_ips.end()) + { + for (auto &token : this->provider->ext_headers) + { + if (this->message.headers.count(token)) + { + this->ip = this->message.headers[token]; + Log(LOG_DEBUG, "httpd") << "httpd: IP for connection " << this->GetFD() << " changed to " << this->ip; + break; + } + } + } + + Log(LOG_DEBUG, "httpd") << "httpd: Serving page " << this->page_name << " to " << this->ip; + + HTTPReply reply; + reply.content_type = this->page->GetContentType(); + + if (this->page->OnRequest(this->provider, this->page_name, this, this->message, reply)) + this->SendReply(&reply); + } + +public: + time_t created; + + MyHTTPClient(HTTPProvider *l, int f, const sockaddrs &a) : Socket(f, l->GetFamily()), HTTPClient(l, f, a), provider(l), ip(a.addr()), created(Anope::CurTime) + { + Log(LOG_DEBUG, "httpd") << "Accepted connection " << f << " from " << a.addr(); + } + + ~MyHTTPClient() override + { + Log(LOG_DEBUG, "httpd") << "Closing connection " << this->GetFD() << " from " << this->ip; + } + + /* Close connection once all data is written */ + bool ProcessWrite() override + { + return !BinarySocket::ProcessWrite() || this->write_buffer.empty() ? false : true; + } + + const Anope::string GetIP() override + { + return this->ip; + } + + bool Read(const char *buffer, size_t l) override + { + message.content.append(buffer, l); + + for (size_t nl; !this->header_done && (nl = message.content.find('\n')) != Anope::string::npos;) + { + Anope::string token = message.content.substr(0, nl).trim(); + message.content = message.content.substr(nl + 1); + + if (token.empty()) + this->header_done = true; + else + this->Read(token); + } + + if (!this->header_done) + return true; + + if (this->message.content.length() >= this->content_length) + { + sepstream sep(this->message.content, '&'); + Anope::string token; + + while (sep.GetToken(token)) + { + size_t sz = token.find('='); + if (sz == Anope::string::npos || !sz || sz + 1 >= token.length()) + continue; + this->message.post_data[token.substr(0, sz)] = HTTPUtils::URLDecode(token.substr(sz + 1)); + Log(LOG_DEBUG_2) << "HTTP POST from " << this->clientaddr.addr() << ": " << token.substr(0, sz) << ": " << this->message.post_data[token.substr(0, sz)]; + } + + this->Serve(); + } + + return true; + } + + bool Read(const Anope::string &buf) + { + Log(LOG_DEBUG_2) << "HTTP from " << this->clientaddr.addr() << ": " << buf; + + if (this->action == ACTION_NONE) + { + std::vector params; + spacesepstream(buf).GetTokens(params); + + if (params.empty() || (params[0] != "GET" && params[0] != "POST")) + { + this->SendError(HTTP_BAD_REQUEST, "Unknown operation"); + return true; + } + + if (params.size() != 3) + { + this->SendError(HTTP_BAD_REQUEST, "Invalid parameters"); + return true; + } + + if (params[0] == "GET") + this->action = ACTION_GET; + else if (params[0] == "POST") + this->action = ACTION_POST; + + Anope::string targ = params[1]; + size_t q = targ.find('?'); + if (q != Anope::string::npos) + { + sepstream sep(targ.substr(q + 1), '&'); + targ = targ.substr(0, q); + + Anope::string token; + while (sep.GetToken(token)) + { + size_t sz = token.find('='); + if (sz == Anope::string::npos || !sz || sz + 1 >= token.length()) + continue; + this->message.get_data[token.substr(0, sz)] = HTTPUtils::URLDecode(token.substr(sz + 1)); + } + } + + this->page = this->provider->FindPage(targ); + this->page_name = targ; + } + else if (buf.find_ci("Cookie: ") == 0) + { + spacesepstream sep(buf.substr(8)); + Anope::string token; + + while (sep.GetToken(token)) + { + size_t sz = token.find('='); + if (sz == Anope::string::npos || !sz || sz + 1 >= token.length()) + continue; + size_t end = token.length() - (sz + 1); + if (!sep.StreamEnd()) + --end; // Remove trailing ; + this->message.cookies[token.substr(0, sz)] = token.substr(sz + 1, end); + } + } + else if (buf.find_ci("Content-Length: ") == 0) + { + try + { + this->content_length = convertTo(buf.substr(16)); + } + catch (const ConvertException &ex) { } + } + else if (buf.find(':') != Anope::string::npos) + { + size_t sz = buf.find(':'); + if (sz + 2 < buf.length()) + this->message.headers[buf.substr(0, sz)] = buf.substr(sz + 2); + } + + return true; + } + + void SendError(HTTPError err, const Anope::string &msg) override + { + HTTPReply h; + + h.error = err; + + h.Write(msg); + + this->SendReply(&h); + } + + void SendReply(HTTPReply *msg) override + { + this->WriteClient("HTTP/1.1 " + GetStatusFromCode(msg->error)); + this->WriteClient("Date: " + BuildDate()); + this->WriteClient("Server: Anope-" + Anope::VersionShort()); + if (msg->content_type.empty()) + this->WriteClient("Content-Type: text/html"); + else + this->WriteClient("Content-Type: " + msg->content_type); + this->WriteClient("Content-Length: " + stringify(msg->length)); + + for (const auto &cookie : msg->cookies) + { + Anope::string buf = "Set-Cookie:"; + + for (const auto &[name, value] : cookie) + buf += " " + name + "=" + value + ";"; + + buf.erase(buf.length() - 1); + + this->WriteClient(buf); + } + + for (auto &[name, value] : msg->headers) + this->WriteClient(name + ": " + value); + + this->WriteClient("Connection: Close"); + this->WriteClient(""); + + for (auto *d : msg->out) + { + this->Write(d->buf, d->len); + + delete d; + } + + msg->out.clear(); + } +}; + +class MyHTTPProvider final + : public HTTPProvider + , public Timer +{ + int timeout; + std::map pages; + std::list > clients; + +public: + MyHTTPProvider(Module *c, const Anope::string &n, const Anope::string &i, const unsigned short p, const int t, bool s) : Socket(-1, i.find(':') == Anope::string::npos ? AF_INET : AF_INET6), HTTPProvider(c, n, i, p, s), Timer(c, 10, Anope::CurTime, true), timeout(t) { } + + void Tick(time_t) override + { + while (!this->clients.empty()) + { + Reference& c = this->clients.front(); + if (c && c->created + this->timeout >= Anope::CurTime) + break; + + delete c; + this->clients.pop_front(); + } + } + + ClientSocket* OnAccept(int fd, const sockaddrs &addr) override + { + auto *c = new MyHTTPClient(this, fd, addr); + this->clients.emplace_back(c); + return c; + } + + bool RegisterPage(HTTPPage *page) override + { + return this->pages.emplace(page->GetURL(), page).second; + } + + void UnregisterPage(HTTPPage *page) override + { + this->pages.erase(page->GetURL()); + } + + HTTPPage* FindPage(const Anope::string &pname) override + { + if (this->pages.count(pname) == 0) + return NULL; + return this->pages[pname]; + } +}; + +class HTTPD final + : public Module +{ + ServiceReference sslref; + std::map providers; +public: + HTTPD(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), sslref("SSLService", "ssl") + { + + } + + ~HTTPD() override + { + 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) || dynamic_cast(s)) + delete s; + } + + this->providers.clear(); + } + + void OnReload(Configuration::Conf *config) override + { + Configuration::Block *conf = config->GetModule(this); + std::set existing; + + for (int i = 0; i < conf->CountBlock("httpd"); ++i) + { + Configuration::Block *block = conf->GetBlock("httpd", i); + + + const Anope::string &hname = block->Get("name", "httpd/main"); + existing.insert(hname); + + Anope::string ip = block->Get("ip"); + int port = block->Get("port", "8080"); + int timeout = block->Get("timeout", "30"); + bool ssl = block->Get("ssl", "no"); + Anope::string ext_ip = block->Get("extforward_ip"); + Anope::string ext_header = block->Get("extforward_header"); + + if (ip.empty()) + { + Log(this) << "You must configure a bind IP for HTTP server " << hname; + continue; + } + else if (port <= 0 || port > 65535) + { + Log(this) << "You must configure a (valid) listen port for HTTP server " << hname; + continue; + } + + MyHTTPProvider *p; + if (this->providers.count(hname) == 0) + { + try + { + p = new MyHTTPProvider(this, hname, ip, port, timeout, ssl); + if (ssl && sslref) + sslref->Init(p); + } + catch (const SocketException &ex) + { + Log(this) << "Unable to create HTTP server " << hname << ": " << ex.GetReason(); + continue; + } + this->providers[hname] = p; + + Log(this) << "Created HTTP server " << hname; + } + else + { + p = this->providers[hname]; + + if (p->GetIP() != ip || p->GetPort() != port) + { + delete p; + this->providers.erase(hname); + + Log(this) << "Changing HTTP server " << hname << " to " << ip << ":" << port; + + try + { + p = new MyHTTPProvider(this, hname, ip, port, timeout, ssl); + if (ssl && sslref) + sslref->Init(p); + } + catch (const SocketException &ex) + { + Log(this) << "Unable to create HTTP server " << hname << ": " << ex.GetReason(); + continue; + } + + this->providers[hname] = p; + } + } + + + spacesepstream(ext_ip).GetTokens(p->ext_ips); + spacesepstream(ext_header).GetTokens(p->ext_headers); + } + + for (std::map::iterator it = this->providers.begin(), it_end = this->providers.end(); it != it_end;) + { + HTTPProvider *p = it->second; + ++it; + + if (existing.count(p->name) == 0) + { + Log(this) << "Removing HTTP server " << p->name; + this->providers.erase(p->name); + delete p; + } + } + } + + void OnModuleLoad(User *u, Module *m) override + { + for (auto &[_, p] : this->providers) + { + if (p->IsSSL() && sslref) + try + { + sslref->Init(p); + } + catch (const CoreException &) { } // Throws on reinitialization + } + } +}; + +MODULE_INIT(HTTPD) diff --git a/modules/m_dns.cpp b/modules/m_dns.cpp deleted file mode 100644 index a75ca6820..000000000 --- a/modules/m_dns.cpp +++ /dev/null @@ -1,1126 +0,0 @@ -/* - * - * (C) 2003-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. - */ - -#include "module.h" -#include "modules/dns.h" - -using namespace DNS; - -namespace -{ - Anope::string admin, nameservers; - int refresh; - time_t timeout; -} - -/** A full packet sent or received to/from the nameserver - */ -class Packet final - : public Query -{ - static bool IsValidName(const Anope::string &name) - { - return name.find_first_not_of("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-") == Anope::string::npos; - } - - void PackName(unsigned char *output, unsigned short output_size, unsigned short &pos, const Anope::string &name) - { - if (pos + name.length() + 2 > output_size) - throw SocketException("Unable to pack name"); - - Log(LOG_DEBUG_2) << "Resolver: PackName packing " << name; - - sepstream sep(name, '.'); - Anope::string token; - - while (sep.GetToken(token)) - { - output[pos++] = token.length(); - memcpy(&output[pos], token.c_str(), token.length()); - pos += token.length(); - } - - output[pos++] = 0; - } - - Anope::string UnpackName(const unsigned char *input, unsigned short input_size, unsigned short &pos) - { - Anope::string name; - unsigned short pos_ptr = pos, lowest_ptr = input_size; - bool compressed = false; - - if (pos_ptr >= input_size) - throw SocketException("Unable to unpack name - no input"); - - while (input[pos_ptr] > 0) - { - unsigned short offset = input[pos_ptr]; - - if (offset & POINTER) - { - if ((offset & POINTER) != POINTER) - throw SocketException("Unable to unpack name - bogus compression header"); - if (pos_ptr + 1 >= input_size) - throw SocketException("Unable to unpack name - bogus compression header"); - - /* Place pos at the second byte of the first (farthest) compression pointer */ - if (compressed == false) - { - ++pos; - compressed = true; - } - - pos_ptr = (offset & LABEL) << 8 | input[pos_ptr + 1]; - - /* Pointers can only go back */ - if (pos_ptr >= lowest_ptr) - throw SocketException("Unable to unpack name - bogus compression pointer"); - lowest_ptr = pos_ptr; - } - else - { - if (pos_ptr + offset + 1 >= input_size) - throw SocketException("Unable to unpack name - offset too large"); - if (!name.empty()) - name += "."; - for (unsigned i = 1; i <= offset; ++i) - name += input[pos_ptr + i]; - - pos_ptr += offset + 1; - if (compressed == false) - /* Move up pos */ - pos = pos_ptr; - } - } - - /* +1 pos either to one byte after the compression pointer or one byte after the ending \0 */ - ++pos; - - /* Empty names are valid (root domain) */ - - Log(LOG_DEBUG_2) << "Resolver: UnpackName successfully unpacked " << name; - - return name; - } - - Question UnpackQuestion(const unsigned char *input, unsigned short input_size, unsigned short &pos) - { - Question question; - - question.name = this->UnpackName(input, input_size, pos); - - if (pos + 4 > input_size) - throw SocketException("Unable to unpack question"); - - if (!IsValidName(question.name)) - throw SocketException("Invalid question name"); - - question.type = static_cast(input[pos] << 8 | input[pos + 1]); - pos += 2; - - question.qclass = input[pos] << 8 | input[pos + 1]; - pos += 2; - - return question; - } - - ResourceRecord UnpackResourceRecord(const unsigned char *input, unsigned short input_size, unsigned short &pos) - { - ResourceRecord record = static_cast(this->UnpackQuestion(input, input_size, pos)); - - if (pos + 6 > input_size) - throw SocketException("Unable to unpack resource record"); - - record.ttl = (input[pos] << 24) | (input[pos + 1] << 16) | (input[pos + 2] << 8) | input[pos + 3]; - pos += 4; - - //record.rdlength = input[pos] << 8 | input[pos + 1]; - pos += 2; - - switch (record.type) - { - case QUERY_A: - { - if (pos + 4 > input_size) - throw SocketException("Unable to unpack resource record"); - - in_addr a; - a.s_addr = input[pos] | (input[pos + 1] << 8) | (input[pos + 2] << 16) | (input[pos + 3] << 24); - pos += 4; - - sockaddrs addrs; - addrs.ntop(AF_INET, &a); - if (!addrs.valid()) - throw SocketException("Invalid IP"); - - record.rdata = addrs.addr(); - break; - } - case QUERY_AAAA: - { - if (pos + 16 > input_size) - throw SocketException("Unable to unpack resource record"); - - in6_addr a; - for (int j = 0; j < 16; ++j) - a.s6_addr[j] = input[pos + j]; - pos += 16; - - sockaddrs addrs; - addrs.ntop(AF_INET6, &a); - if (!addrs.valid()) - throw SocketException("Invalid IP"); - - record.rdata = addrs.addr(); - break; - } - case QUERY_CNAME: - case QUERY_PTR: - { - record.rdata = this->UnpackName(input, input_size, pos); - - if (!IsValidName(record.rdata)) - throw SocketException("Invalid cname/ptr record data"); - - break; - } - default: - break; - } - - Log(LOG_DEBUG_2) << "Resolver: " << record.name << " -> " << record.rdata; - - return record; - } - -public: - static const int POINTER = 0xC0; - static const int LABEL = 0x3F; - static const int HEADER_LENGTH = 12; - - Manager *manager; - /* Source or destination of the packet */ - sockaddrs addr; - /* ID for this packet */ - unsigned short id = 0; - /* Flags on the packet */ - unsigned short flags = 0; - - Packet(Manager *m, sockaddrs *a) : manager(m) - { - if (a) - addr = *a; - } - - void Fill(const unsigned char *input, const unsigned short len) - { - if (len < HEADER_LENGTH) - throw SocketException("Unable to fill packet"); - - unsigned short packet_pos = 0; - - this->id = (input[packet_pos] << 8) | input[packet_pos + 1]; - packet_pos += 2; - - this->flags = (input[packet_pos] << 8) | input[packet_pos + 1]; - packet_pos += 2; - - unsigned short qdcount = (input[packet_pos] << 8) | input[packet_pos + 1]; - packet_pos += 2; - - unsigned short ancount = (input[packet_pos] << 8) | input[packet_pos + 1]; - packet_pos += 2; - - unsigned short nscount = (input[packet_pos] << 8) | input[packet_pos + 1]; - packet_pos += 2; - - unsigned short arcount = (input[packet_pos] << 8) | input[packet_pos + 1]; - packet_pos += 2; - - Log(LOG_DEBUG_2) << "Resolver: qdcount: " << qdcount << " ancount: " << ancount << " nscount: " << nscount << " arcount: " << arcount; - - for (unsigned i = 0; i < qdcount; ++i) - this->questions.push_back(this->UnpackQuestion(input, len, packet_pos)); - - for (unsigned i = 0; i < ancount; ++i) - this->answers.push_back(this->UnpackResourceRecord(input, len, packet_pos)); - - try - { - for (unsigned i = 0; i < nscount; ++i) - this->authorities.push_back(this->UnpackResourceRecord(input, len, packet_pos)); - - for (unsigned i = 0; i < arcount; ++i) - this->additional.push_back(this->UnpackResourceRecord(input, len, packet_pos)); - } - catch (const SocketException &ex) - { - Log(LOG_DEBUG_2) << "Unable to parse ns/ar records: " << ex.GetReason(); - } - } - - unsigned short Pack(unsigned char *output, unsigned short output_size) - { - if (output_size < HEADER_LENGTH) - throw SocketException("Unable to pack packet"); - - unsigned short pos = 0; - - output[pos++] = this->id >> 8; - output[pos++] = this->id & 0xFF; - output[pos++] = this->flags >> 8; - output[pos++] = this->flags & 0xFF; - output[pos++] = this->questions.size() >> 8; - output[pos++] = this->questions.size() & 0xFF; - output[pos++] = this->answers.size() >> 8; - output[pos++] = this->answers.size() & 0xFF; - output[pos++] = this->authorities.size() >> 8; - output[pos++] = this->authorities.size() & 0xFF; - output[pos++] = this->additional.size() >> 8; - output[pos++] = this->additional.size() & 0xFF; - - for (auto &q : this->questions) - { - if (q.type == QUERY_PTR) - { - sockaddrs ip(q.name); - if (!ip.valid()) - throw SocketException("Invalid IP"); - - switch (ip.family()) - { - case AF_INET6: - q.name = ip.reverse() + ".ip6.arpa"; - break; - case AF_INET: - q.name = ip.reverse() + ".in-addr.arpa"; - break; - default: - throw SocketException("Unsupported IP Family"); - } - } - - this->PackName(output, output_size, pos, q.name); - - if (pos + 4 >= output_size) - throw SocketException("Unable to pack packet"); - - short s = htons(q.type); - memcpy(&output[pos], &s, 2); - pos += 2; - - s = htons(q.qclass); - memcpy(&output[pos], &s, 2); - pos += 2; - } - - std::vector types[] = { this->answers, this->authorities, this->additional }; - for (auto &type : types) - { - for (const auto &rr : type) - { - this->PackName(output, output_size, pos, rr.name); - - if (pos + 8 >= output_size) - throw SocketException("Unable to pack packet"); - - short s = htons(rr.type); - memcpy(&output[pos], &s, 2); - pos += 2; - - s = htons(rr.qclass); - memcpy(&output[pos], &s, 2); - pos += 2; - - long l = htonl(rr.ttl); - memcpy(&output[pos], &l, 4); - pos += 4; - - switch (rr.type) - { - case QUERY_A: - { - if (pos + 6 > output_size) - throw SocketException("Unable to pack packet"); - - sockaddrs a(rr.rdata); - if (!a.valid()) - throw SocketException("Invalid IP"); - - s = htons(4); - memcpy(&output[pos], &s, 2); - pos += 2; - - memcpy(&output[pos], &a.sa4.sin_addr, 4); - pos += 4; - break; - } - case QUERY_AAAA: - { - if (pos + 18 > output_size) - throw SocketException("Unable to pack packet"); - - sockaddrs a(rr.rdata); - if (!a.valid()) - throw SocketException("Invalid IP"); - - s = htons(16); - memcpy(&output[pos], &s, 2); - pos += 2; - - memcpy(&output[pos], &a.sa6.sin6_addr, 16); - pos += 16; - break; - } - case QUERY_NS: - case QUERY_CNAME: - case QUERY_PTR: - { - if (pos + 2 >= output_size) - throw SocketException("Unable to pack packet"); - - unsigned short packet_pos_save = pos; - pos += 2; - - this->PackName(output, output_size, pos, rr.rdata); - - s = htons(pos - packet_pos_save - 2); - memcpy(&output[packet_pos_save], &s, 2); - break; - } - case QUERY_SOA: - { - if (pos + 2 >= output_size) - throw SocketException("Unable to pack packet"); - - unsigned short packet_pos_save = pos; - pos += 2; - - std::vector ns; - spacesepstream(nameservers).GetTokens(ns); - this->PackName(output, output_size, pos, !ns.empty() ? ns[0] : ""); - this->PackName(output, output_size, pos, admin.replace_all_cs('@', '.')); - - if (pos + 20 >= output_size) - throw SocketException("Unable to pack SOA"); - - l = htonl(manager->GetSerial()); - memcpy(&output[pos], &l, 4); - pos += 4; - - l = htonl(refresh); // Refresh - memcpy(&output[pos], &l, 4); - pos += 4; - - l = htonl(refresh); // Retry - memcpy(&output[pos], &l, 4); - pos += 4; - - l = htonl(604800); // Expire - memcpy(&output[pos], &l, 4); - pos += 4; - - l = htonl(0); // Minimum - memcpy(&output[pos], &l, 4); - pos += 4; - - s = htons(pos - packet_pos_save - 2); - memcpy(&output[packet_pos_save], &s, 2); - - break; - } - default: - break; - } - } - } - - return pos; - } -}; - -namespace DNS -{ - class ReplySocket - : public virtual Socket - { - public: - ~ReplySocket() override = default; - virtual void Reply(Packet *p) = 0; - }; -} - -/* Listens for TCP requests */ -class TCPSocket final - : public ListenSocket -{ - Manager *manager; - -public: - /* A TCP client */ - class Client final - : public ClientSocket - , public Timer - , public ReplySocket - { - Manager *manager; - Packet *packet = nullptr; - unsigned char packet_buffer[524]; - int length = 0; - - public: - Client(Manager *m, TCPSocket *l, int fd, const sockaddrs &addr) : Socket(fd, l->GetFamily()), ClientSocket(l, addr), Timer(5), - manager(m) - { - Log(LOG_DEBUG_2) << "Resolver: New client from " << addr.addr(); - } - - ~Client() override - { - Log(LOG_DEBUG_2) << "Resolver: Exiting client from " << clientaddr.addr(); - delete packet; - } - - /* Times out after a few seconds */ - void Tick(time_t) override { } - - void Reply(Packet *p) override - { - delete packet; - packet = p; - SocketEngine::Change(this, true, SF_WRITABLE); - } - - bool ProcessRead() override - { - Log(LOG_DEBUG_2) << "Resolver: Reading from DNS TCP socket"; - - int i = recv(this->GetFD(), reinterpret_cast(packet_buffer) + length, sizeof(packet_buffer) - length, 0); - if (i <= 0) - return false; - - length += i; - - unsigned short want_len = packet_buffer[0] << 8 | packet_buffer[1]; - if (length >= want_len + 2) - { - int len = length - 2; - length -= want_len + 2; - return this->manager->HandlePacket(this, packet_buffer + 2, len, NULL); - } - return true; - } - - bool ProcessWrite() override - { - Log(LOG_DEBUG_2) << "Resolver: Writing to DNS TCP socket"; - - if (packet != NULL) - { - try - { - unsigned char buffer[65535]; - unsigned short len = packet->Pack(buffer + 2, sizeof(buffer) - 2); - - short s = htons(len); - memcpy(buffer, &s, 2); - len += 2; - - send(this->GetFD(), reinterpret_cast(buffer), len, 0); - } - catch (const SocketException &) { } - - delete packet; - packet = NULL; - } - - SocketEngine::Change(this, false, SF_WRITABLE); - return true; /* Do not return false here, bind is unhappy we close the connection so soon after sending */ - } - }; - - TCPSocket(Manager *m, const Anope::string &ip, int port) : Socket(-1, ip.find(':') == Anope::string::npos ? AF_INET : AF_INET6), ListenSocket(ip, port, ip.find(':') != Anope::string::npos), manager(m) { } - - ClientSocket *OnAccept(int fd, const sockaddrs &addr) override - { - return new Client(this->manager, this, fd, addr); - } -}; - -/* Listens for UDP requests */ -class UDPSocket final - : public ReplySocket -{ - Manager *manager; - std::deque packets; - -public: - UDPSocket(Manager *m, const Anope::string &ip, int port) : Socket(-1, ip.find(':') == Anope::string::npos ? AF_INET : AF_INET6, SOCK_DGRAM), manager(m) { } - - ~UDPSocket() override - { - for (const auto *packet : packets) - delete packet; - } - - void Reply(Packet *p) override - { - packets.push_back(p); - SocketEngine::Change(this, true, SF_WRITABLE); - } - - std::deque& GetPackets() { return packets; } - - bool ProcessRead() override - { - Log(LOG_DEBUG_2) << "Resolver: Reading from DNS UDP socket"; - - unsigned char packet_buffer[524]; - sockaddrs from_server; - socklen_t x = sizeof(from_server); - int length = recvfrom(this->GetFD(), reinterpret_cast(&packet_buffer), sizeof(packet_buffer), 0, &from_server.sa, &x); - return this->manager->HandlePacket(this, packet_buffer, length, &from_server); - } - - bool ProcessWrite() override - { - Log(LOG_DEBUG_2) << "Resolver: Writing to DNS UDP socket"; - - Packet *r = !packets.empty() ? packets.front() : NULL; - if (r != NULL) - { - try - { - unsigned char buffer[524]; - unsigned short len = r->Pack(buffer, sizeof(buffer)); - - sendto(this->GetFD(), reinterpret_cast(buffer), len, 0, &r->addr.sa, r->addr.size()); - } - catch (const SocketException &) { } - - delete r; - packets.pop_front(); - } - - if (packets.empty()) - SocketEngine::Change(this, false, SF_WRITABLE); - - return true; - } -}; - -class NotifySocket final - : public Socket -{ - Packet *packet; -public: - NotifySocket(int family, Packet *p) : Socket(-1, family, SOCK_DGRAM), packet(p) - { - SocketEngine::Change(this, false, SF_READABLE); - SocketEngine::Change(this, true, SF_WRITABLE); - } - - bool ProcessWrite() override - { - if (!packet) - return false; - - Log(LOG_DEBUG_2) << "Resolver: Notifying slave " << packet->addr.addr(); - - try - { - unsigned char buffer[524]; - unsigned short len = packet->Pack(buffer, sizeof(buffer)); - - sendto(this->GetFD(), reinterpret_cast(buffer), len, 0, &packet->addr.sa, packet->addr.size()); - } - catch (const SocketException &) { } - - delete packet; - packet = NULL; - - return false; - } -}; - -class MyManager final - : public Manager - , public Timer -{ - uint32_t serial; - - typedef std::unordered_map cache_map; - cache_map cache; - - TCPSocket *tcpsock = nullptr; - UDPSocket *udpsock = nullptr; - - bool listen = false; - sockaddrs addrs; - - std::vector > notify; -public: - std::map requests; - - MyManager(Module *creator) : Manager(creator), Timer(300, Anope::CurTime, true), serial(Anope::CurTime), cur_id(rand()) - { - } - - ~MyManager() override - { - delete udpsock; - delete tcpsock; - - for (std::map::iterator it = this->requests.begin(), it_end = this->requests.end(); it != it_end;) - { - Request *request = it->second; - ++it; - - Query rr(*request); - rr.error = ERROR_UNKNOWN; - request->OnError(&rr); - - delete request; - } - this->requests.clear(); - - this->cache.clear(); - } - - void SetIPPort(const Anope::string &nameserver, const Anope::string &ip, unsigned short port, std::vector > n) - { - delete udpsock; - delete tcpsock; - - udpsock = NULL; - tcpsock = NULL; - - try - { - this->addrs.pton(nameserver.find(':') != Anope::string::npos ? AF_INET6 : AF_INET, nameserver, 53); - - udpsock = new UDPSocket(this, ip, port); - - if (!ip.empty()) - { - udpsock->Bind(ip, port); - tcpsock = new TCPSocket(this, ip, port); - listen = true; - } - } - catch (const SocketException &ex) - { - Log() << "Unable to bind dns to " << ip << ":" << port << ": " << ex.GetReason(); - } - - notify = n; - } - -private: - unsigned short cur_id; - - unsigned short GetID() - { - if (this->udpsock->GetPackets().size() == 65535) - throw SocketException("DNS queue full"); - - do - cur_id = (cur_id + 1) & 0xFFFF; - while (!cur_id || this->requests.count(cur_id)); - - return cur_id; - } - -public: - void Process(Request *req) override - { - Log(LOG_DEBUG_2) << "Resolver: Processing request to lookup " << req->name << ", of type " << req->type; - - if (req->use_cache && this->CheckCache(req)) - { - Log(LOG_DEBUG_2) << "Resolver: Using cached result"; - delete req; - return; - } - - if (!this->udpsock) - throw SocketException("No dns socket"); - - req->id = GetID(); - this->requests[req->id] = req; - - req->SetSecs(timeout); - - auto *p = new Packet(this, &this->addrs); - p->flags = QUERYFLAGS_RD; - p->id = req->id; - p->questions.push_back(*req); - - this->udpsock->Reply(p); - } - - void RemoveRequest(Request *req) override - { - this->requests.erase(req->id); - } - - bool HandlePacket(ReplySocket *s, const unsigned char *const packet_buffer, int length, sockaddrs *from) override - { - if (length < Packet::HEADER_LENGTH) - return true; - - Packet recv_packet(this, from); - - try - { - recv_packet.Fill(packet_buffer, length); - } - catch (const SocketException &ex) - { - Log(LOG_DEBUG_2) << ex.GetReason(); - return true; - } - - if (!(recv_packet.flags & QUERYFLAGS_QR)) - { - if (!listen) - return true; - else if (recv_packet.questions.empty()) - { - Log(LOG_DEBUG_2) << "Resolver: Received a question with no questions?"; - return true; - } - - auto *packet = new Packet(recv_packet); - packet->flags |= QUERYFLAGS_QR; /* This is a response */ - packet->flags |= QUERYFLAGS_AA; /* And we are authoritative */ - - packet->answers.clear(); - packet->authorities.clear(); - packet->additional.clear(); - - for (auto &q : recv_packet.questions) - { - if (q.type == QUERY_AXFR || q.type == QUERY_SOA) - { - ResourceRecord rr(q.name, QUERY_SOA); - packet->answers.push_back(rr); - - if (q.type == QUERY_AXFR) - { - Anope::string token; - spacesepstream sep(nameservers); - while (sep.GetToken(token)) - { - ResourceRecord rr2(q.name, QUERY_NS); - rr2.rdata = token; - packet->answers.push_back(rr2); - } - } - break; - } - } - - FOREACH_MOD(OnDnsRequest, (recv_packet, packet)); - - for (auto &q : recv_packet.questions) - { - if (q.type == QUERY_AXFR) - { - ResourceRecord rr(q.name, QUERY_SOA); - packet->answers.push_back(rr); - break; - } - } - - if (packet->answers.empty() && packet->authorities.empty() && packet->additional.empty() && packet->error == ERROR_NONE) - packet->error = ERROR_REFUSED; // usually safe, won't cause an NXDOMAIN to get cached - - s->Reply(packet); - return true; - } - - if (from == NULL) - { - Log(LOG_DEBUG_2) << "Resolver: Received an answer over TCP. This is not supported."; - return true; - } - else if (this->addrs != *from) - { - Log(LOG_DEBUG_2) << "Resolver: Received an answer from the wrong nameserver, Bad NAT or DNS forging attempt? '" << this->addrs.addr() << "' != '" << from->addr() << "'"; - return true; - } - - std::map::iterator it = this->requests.find(recv_packet.id); - if (it == this->requests.end()) - { - Log(LOG_DEBUG_2) << "Resolver: Received an answer for something we didn't request"; - return true; - } - Request *request = it->second; - - if (recv_packet.flags & QUERYFLAGS_OPCODE) - { - Log(LOG_DEBUG_2) << "Resolver: Received a nonstandard query"; - recv_packet.error = ERROR_NONSTANDARD_QUERY; - request->OnError(&recv_packet); - } - else if (recv_packet.flags & QUERYFLAGS_RCODE) - { - Error error = ERROR_UNKNOWN; - - switch (recv_packet.flags & QUERYFLAGS_RCODE) - { - case 1: - Log(LOG_DEBUG_2) << "Resolver: format error"; - error = ERROR_FORMAT_ERROR; - break; - case 2: - Log(LOG_DEBUG_2) << "Resolver: server error"; - error = ERROR_SERVER_FAILURE; - break; - case 3: - Log(LOG_DEBUG_2) << "Resolver: domain not found"; - error = ERROR_DOMAIN_NOT_FOUND; - break; - case 4: - Log(LOG_DEBUG_2) << "Resolver: not implemented"; - error = ERROR_NOT_IMPLEMENTED; - break; - case 5: - Log(LOG_DEBUG_2) << "Resolver: refused"; - error = ERROR_REFUSED; - break; - default: - break; - } - - recv_packet.error = error; - request->OnError(&recv_packet); - } - else if (recv_packet.questions.empty() || recv_packet.answers.empty()) - { - Log(LOG_DEBUG_2) << "Resolver: No resource records returned"; - recv_packet.error = ERROR_NO_RECORDS; - request->OnError(&recv_packet); - } - else - { - Log(LOG_DEBUG_2) << "Resolver: Lookup complete for " << request->name; - request->OnLookupComplete(&recv_packet); - this->AddCache(recv_packet); - } - - delete request; - return true; - } - - void UpdateSerial() override - { - serial = Anope::CurTime; - } - - void Notify(const Anope::string &zone) override - { - /* notify slaves of the update */ - for (const auto &[ip, port] : notify) - { - sockaddrs addr; - addr.pton(ip.find(':') != Anope::string::npos ? AF_INET6 : AF_INET, ip, port); - if (!addr.valid()) - return; - - auto *packet = new Packet(this, &addr); - packet->flags = QUERYFLAGS_AA | QUERYFLAGS_OPCODE_NOTIFY; - try - { - packet->id = GetID(); - } - catch (const SocketException &) - { - delete packet; - continue; - } - - packet->questions.emplace_back(zone, QUERY_SOA); - - new NotifySocket(ip.find(':') == Anope::string::npos ? AF_INET : AF_INET6, packet); - } - } - - uint32_t GetSerial() const override - { - return serial; - } - - void Tick(time_t now) override - { - Log(LOG_DEBUG_2) << "Resolver: Purging DNS cache"; - - for (cache_map::iterator it = this->cache.begin(), it_next; it != this->cache.end(); it = it_next) - { - const Query &q = it->second; - const ResourceRecord &req = q.answers[0]; - it_next = it; - ++it_next; - - if (req.created + static_cast(req.ttl) < now) - this->cache.erase(it); - } - } - -private: - /** Add a record to the dns cache - * @param r The record - */ - void AddCache(Query &r) - { - const ResourceRecord &rr = r.answers[0]; - Log(LOG_DEBUG_3) << "Resolver cache: added cache for " << rr.name << " -> " << rr.rdata << ", ttl: " << rr.ttl; - this->cache[r.questions[0]] = r; - } - - /** Check the DNS cache to see if request can be handled by a cached result - * @return true if a cached result was found. - */ - bool CheckCache(Request *request) - { - cache_map::iterator it = this->cache.find(*request); - if (it != this->cache.end()) - { - Query &record = it->second; - Log(LOG_DEBUG_3) << "Resolver: Using cached result for " << request->name; - request->OnLookupComplete(&record); - return true; - } - - return false; - } - -}; - -class ModuleDNS final - : public Module -{ - MyManager manager; - - Anope::string nameserver; - Anope::string ip; - int port; - - std::vector > notify; - -public: - ModuleDNS(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), manager(this) - { - - } - - ~ModuleDNS() override - { - 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) || dynamic_cast(s)) - delete s; - } - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *block = conf->GetModule(this); - - nameserver = block->Get("nameserver", "127.0.0.1"); - timeout = block->Get("timeout", "5"); - ip = block->Get("ip", "0.0.0.0"); - port = block->Get("port", "53"); - admin = block->Get("admin", "admin@example.com"); - nameservers = block->Get("nameservers", "ns1.example.com"); - refresh = block->Get("refresh", "3600"); - - for (int i = 0; i < block->CountBlock("notify"); ++i) - { - Configuration::Block *n = block->GetBlock("notify", i); - Anope::string nip = n->Get("ip"); - short nport = n->Get("port"); - - notify.emplace_back(nip, nport); - } - - if (Anope::IsFile(nameserver)) - { - std::ifstream f(nameserver.c_str()); - bool success = false; - - if (f.is_open()) - { - for (Anope::string server; std::getline(f, server.str());) - { - if (server.find("nameserver") == 0) - { - size_t i = server.find_first_of("123456789"); - if (i != Anope::string::npos) - { - if (server.substr(i).is_pos_number_only()) - { - nameserver = server.substr(i); - Log(LOG_DEBUG) << "Nameserver set to " << nameserver; - success = true; - break; - } - } - } - } - - f.close(); - } - - if (!success) - { - Log() << "Unable to find nameserver, defaulting to 127.0.0.1"; - nameserver = "127.0.0.1"; - } - } - - try - { - this->manager.SetIPPort(nameserver, ip, port, notify); - } - catch (const SocketException &ex) - { - throw ModuleException(ex.GetReason()); - } - } - - void OnModuleUnload(User *u, Module *m) override - { - for (std::map::iterator it = this->manager.requests.begin(), it_end = this->manager.requests.end(); it != it_end;) - { - unsigned short id = it->first; - Request *req = it->second; - ++it; - - if (req->creator == m) - { - Query rr(*req); - rr.error = ERROR_UNLOADED; - req->OnError(&rr); - - delete req; - this->manager.requests.erase(id); - } - } - } -}; - -MODULE_INIT(ModuleDNS) diff --git a/modules/m_dnsbl.cpp b/modules/m_dnsbl.cpp deleted file mode 100644 index 47aba3876..000000000 --- a/modules/m_dnsbl.cpp +++ /dev/null @@ -1,196 +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/dns.h" - -using namespace DNS; - -static ServiceReference akills("XLineManager", "xlinemanager/sgline"); -static ServiceReference dnsmanager("DNS::Manager", "dns/manager"); - -struct Blacklist final -{ - struct Reply final - { - int code = 0; - Anope::string reason; - bool allow_account = false; - - Reply() = default; - }; - - Anope::string name; - time_t bantime = 0; - Anope::string reason; - std::vector replies; - - const Reply *Find(int code) - { - for (const auto &reply : replies) - if (reply.code == code) - return &reply; - return NULL; - } -}; - -class DNSBLResolver final - : public Request -{ - Reference user; - Blacklist blacklist; - bool add_to_akill; - -public: - DNSBLResolver(Module *c, User *u, const Blacklist &b, const Anope::string &host, bool add_akill) : Request(dnsmanager, c, host, QUERY_A, true), user(u), blacklist(b), add_to_akill(add_akill) { } - - void OnLookupComplete(const Query *record) override - { - if (!user || user->Quitting()) - return; - - const ResourceRecord &ans_record = record->answers[0]; - // Replies should be in 127.0.0.0/8 - if (ans_record.rdata.find("127.") != 0) - return; - - sockaddrs sresult; - sresult.pton(AF_INET, ans_record.rdata); - int result = sresult.sa4.sin_addr.s_addr >> 24; - - const Blacklist::Reply *reply = blacklist.Find(result); - if (!blacklist.replies.empty() && !reply) - return; - - if (reply && reply->allow_account && user->Account()) - return; - - Anope::string reason = this->blacklist.reason, addr = user->ip.addr(); - reason = reason.replace_all_cs("%n", user->nick); - reason = reason.replace_all_cs("%u", user->GetIdent()); - reason = reason.replace_all_cs("%g", user->realname); - reason = reason.replace_all_cs("%h", user->host); - reason = reason.replace_all_cs("%i", addr); - reason = reason.replace_all_cs("%r", reply ? reply->reason : ""); - reason = reason.replace_all_cs("%N", Config->GetBlock("networkinfo")->Get("networkname")); - - BotInfo *OperServ = Config->GetClient("OperServ"); - Log(creator, "dnsbl", OperServ) << user->GetMask() << " (" << addr << ") appears in " << this->blacklist.name; - auto *x = new XLine("*@" + addr, OperServ ? OperServ->nick : "m_dnsbl", Anope::CurTime + this->blacklist.bantime, reason, XLineManager::GenerateUID()); - if (this->add_to_akill && akills) - { - akills->AddXLine(x); - akills->Send(NULL, x); - } - else - { - IRCD->SendAkill(NULL, x); - delete x; - } - } -}; - -class ModuleDNSBL final - : public Module -{ - std::vector blacklists; - std::set exempts; - bool check_on_connect; - bool check_on_netburst; - bool add_to_akill; - -public: - ModuleDNSBL(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR | EXTRA) - { - - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *block = conf->GetModule(this); - this->check_on_connect = block->Get("check_on_connect"); - this->check_on_netburst = block->Get("check_on_netburst"); - this->add_to_akill = block->Get("add_to_akill", "yes"); - - this->blacklists.clear(); - for (int i = 0; i < block->CountBlock("blacklist"); ++i) - { - Configuration::Block *bl = block->GetBlock("blacklist", i); - Blacklist blacklist; - - blacklist.name = bl->Get("name"); - if (blacklist.name.empty()) - continue; - blacklist.bantime = bl->Get("time", "4h"); - blacklist.reason = bl->Get("reason"); - - for (int j = 0; j < bl->CountBlock("reply"); ++j) - { - Configuration::Block *reply = bl->GetBlock("reply", j); - Blacklist::Reply r; - - r.code = reply->Get("code"); - r.reason = reply->Get("reason"); - r.allow_account = reply->Get("allow_account"); - - blacklist.replies.push_back(r); - } - - this->blacklists.push_back(blacklist); - } - - this->exempts.clear(); - for (int i = 0; i < block->CountBlock("exempt"); ++i) - { - Configuration::Block *bl = block->GetBlock("exempt", i); - this->exempts.insert(bl->Get("ip")); - } - } - - void OnUserConnect(User *user, bool &exempt) override - { - if (exempt || user->Quitting() || (!this->check_on_connect && !Me->IsSynced()) || !dnsmanager) - return; - - if (!this->check_on_netburst && !user->server->IsSynced()) - return; - - if (!user->ip.valid()) - /* User doesn't have a valid IP (spoof/etc) */ - return; - - if (this->blacklists.empty()) - return; - - if (this->exempts.count(user->ip.addr())) - { - Log(LOG_DEBUG) << "User " << user->nick << " is exempt from dnsbl check - ip: " << user->ip.addr(); - return; - } - - Anope::string reverse = user->ip.reverse(); - - for (const auto &b : this->blacklists) - { - Anope::string dnsbl_host = reverse + "." + b.name; - DNSBLResolver *res = NULL; - try - { - res = new DNSBLResolver(this, user, b, dnsbl_host, this->add_to_akill); - dnsmanager->Process(res); - } - catch (const SocketException &ex) - { - delete res; - Log(this) << ex.GetReason(); - } - } - } -}; - -MODULE_INIT(ModuleDNSBL) diff --git a/modules/m_helpchan.cpp b/modules/m_helpchan.cpp deleted file mode 100644 index 061388dfe..000000000 --- a/modules/m_helpchan.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/* - * - * (C) 2003-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" - -class HelpChannel final - : public Module -{ -public: - HelpChannel(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR) - { - } - - EventReturn OnChannelModeSet(Channel *c, MessageSource &, ChannelMode *mode, const Anope::string ¶m) override - { - if (mode->name == "OP" && c && c->ci && c->name.equals_ci(Config->GetModule(this)->Get("helpchannel"))) - { - User *u = User::Find(param); - - if (u && c->ci->AccessFor(u).HasPriv("OPME")) - u->SetMode(Config->GetClient("OperServ"), "HELPOP"); - } - - return EVENT_CONTINUE; - } -}; - -MODULE_INIT(HelpChannel) diff --git a/modules/m_httpd.cpp b/modules/m_httpd.cpp deleted file mode 100644 index 53e7c51a9..000000000 --- a/modules/m_httpd.cpp +++ /dev/null @@ -1,469 +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/httpd.h" -#include "modules/ssl.h" - -static Anope::string BuildDate() -{ - char timebuf[64]; - struct tm *tm = localtime(&Anope::CurTime); - strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %H:%M:%S %Z", tm); - return timebuf; -} - -static Anope::string GetStatusFromCode(HTTPError err) -{ - switch (err) - { - case HTTP_ERROR_OK: - return "200 OK"; - case HTTP_FOUND: - return "302 Found"; - case HTTP_BAD_REQUEST: - return "400 Bad Request"; - case HTTP_PAGE_NOT_FOUND: - return "404 Not Found"; - case HTTP_NOT_SUPPORTED: - return "505 HTTP Version Not Supported"; - } - - return "501 Not Implemented"; -} - -class MyHTTPClient final - : public HTTPClient -{ - HTTPProvider *provider; - HTTPMessage message; - bool header_done = false, served = false; - Anope::string page_name; - Reference page; - Anope::string ip; - - unsigned content_length = 0; - - enum - { - ACTION_NONE, - ACTION_GET, - ACTION_POST - } action = ACTION_NONE; - - void Serve() - { - if (this->served) - return; - this->served = true; - - if (!this->page) - { - this->SendError(HTTP_PAGE_NOT_FOUND, "Page not found"); - return; - } - - if (std::find(this->provider->ext_ips.begin(), this->provider->ext_ips.end(), this->ip) != this->provider->ext_ips.end()) - { - for (auto &token : this->provider->ext_headers) - { - if (this->message.headers.count(token)) - { - this->ip = this->message.headers[token]; - Log(LOG_DEBUG, "httpd") << "m_httpd: IP for connection " << this->GetFD() << " changed to " << this->ip; - break; - } - } - } - - Log(LOG_DEBUG, "httpd") << "m_httpd: Serving page " << this->page_name << " to " << this->ip; - - HTTPReply reply; - reply.content_type = this->page->GetContentType(); - - if (this->page->OnRequest(this->provider, this->page_name, this, this->message, reply)) - this->SendReply(&reply); - } - -public: - time_t created; - - MyHTTPClient(HTTPProvider *l, int f, const sockaddrs &a) : Socket(f, l->GetFamily()), HTTPClient(l, f, a), provider(l), ip(a.addr()), created(Anope::CurTime) - { - Log(LOG_DEBUG, "httpd") << "Accepted connection " << f << " from " << a.addr(); - } - - ~MyHTTPClient() override - { - Log(LOG_DEBUG, "httpd") << "Closing connection " << this->GetFD() << " from " << this->ip; - } - - /* Close connection once all data is written */ - bool ProcessWrite() override - { - return !BinarySocket::ProcessWrite() || this->write_buffer.empty() ? false : true; - } - - const Anope::string GetIP() override - { - return this->ip; - } - - bool Read(const char *buffer, size_t l) override - { - message.content.append(buffer, l); - - for (size_t nl; !this->header_done && (nl = message.content.find('\n')) != Anope::string::npos;) - { - Anope::string token = message.content.substr(0, nl).trim(); - message.content = message.content.substr(nl + 1); - - if (token.empty()) - this->header_done = true; - else - this->Read(token); - } - - if (!this->header_done) - return true; - - if (this->message.content.length() >= this->content_length) - { - sepstream sep(this->message.content, '&'); - Anope::string token; - - while (sep.GetToken(token)) - { - size_t sz = token.find('='); - if (sz == Anope::string::npos || !sz || sz + 1 >= token.length()) - continue; - this->message.post_data[token.substr(0, sz)] = HTTPUtils::URLDecode(token.substr(sz + 1)); - Log(LOG_DEBUG_2) << "HTTP POST from " << this->clientaddr.addr() << ": " << token.substr(0, sz) << ": " << this->message.post_data[token.substr(0, sz)]; - } - - this->Serve(); - } - - return true; - } - - bool Read(const Anope::string &buf) - { - Log(LOG_DEBUG_2) << "HTTP from " << this->clientaddr.addr() << ": " << buf; - - if (this->action == ACTION_NONE) - { - std::vector params; - spacesepstream(buf).GetTokens(params); - - if (params.empty() || (params[0] != "GET" && params[0] != "POST")) - { - this->SendError(HTTP_BAD_REQUEST, "Unknown operation"); - return true; - } - - if (params.size() != 3) - { - this->SendError(HTTP_BAD_REQUEST, "Invalid parameters"); - return true; - } - - if (params[0] == "GET") - this->action = ACTION_GET; - else if (params[0] == "POST") - this->action = ACTION_POST; - - Anope::string targ = params[1]; - size_t q = targ.find('?'); - if (q != Anope::string::npos) - { - sepstream sep(targ.substr(q + 1), '&'); - targ = targ.substr(0, q); - - Anope::string token; - while (sep.GetToken(token)) - { - size_t sz = token.find('='); - if (sz == Anope::string::npos || !sz || sz + 1 >= token.length()) - continue; - this->message.get_data[token.substr(0, sz)] = HTTPUtils::URLDecode(token.substr(sz + 1)); - } - } - - this->page = this->provider->FindPage(targ); - this->page_name = targ; - } - else if (buf.find_ci("Cookie: ") == 0) - { - spacesepstream sep(buf.substr(8)); - Anope::string token; - - while (sep.GetToken(token)) - { - size_t sz = token.find('='); - if (sz == Anope::string::npos || !sz || sz + 1 >= token.length()) - continue; - size_t end = token.length() - (sz + 1); - if (!sep.StreamEnd()) - --end; // Remove trailing ; - this->message.cookies[token.substr(0, sz)] = token.substr(sz + 1, end); - } - } - else if (buf.find_ci("Content-Length: ") == 0) - { - try - { - this->content_length = convertTo(buf.substr(16)); - } - catch (const ConvertException &ex) { } - } - else if (buf.find(':') != Anope::string::npos) - { - size_t sz = buf.find(':'); - if (sz + 2 < buf.length()) - this->message.headers[buf.substr(0, sz)] = buf.substr(sz + 2); - } - - return true; - } - - void SendError(HTTPError err, const Anope::string &msg) override - { - HTTPReply h; - - h.error = err; - - h.Write(msg); - - this->SendReply(&h); - } - - void SendReply(HTTPReply *msg) override - { - this->WriteClient("HTTP/1.1 " + GetStatusFromCode(msg->error)); - this->WriteClient("Date: " + BuildDate()); - this->WriteClient("Server: Anope-" + Anope::VersionShort()); - if (msg->content_type.empty()) - this->WriteClient("Content-Type: text/html"); - else - this->WriteClient("Content-Type: " + msg->content_type); - this->WriteClient("Content-Length: " + stringify(msg->length)); - - for (const auto &cookie : msg->cookies) - { - Anope::string buf = "Set-Cookie:"; - - for (const auto &[name, value] : cookie) - buf += " " + name + "=" + value + ";"; - - buf.erase(buf.length() - 1); - - this->WriteClient(buf); - } - - for (auto &[name, value] : msg->headers) - this->WriteClient(name + ": " + value); - - this->WriteClient("Connection: Close"); - this->WriteClient(""); - - for (auto *d : msg->out) - { - this->Write(d->buf, d->len); - - delete d; - } - - msg->out.clear(); - } -}; - -class MyHTTPProvider final - : public HTTPProvider - , public Timer -{ - int timeout; - std::map pages; - std::list > clients; - -public: - MyHTTPProvider(Module *c, const Anope::string &n, const Anope::string &i, const unsigned short p, const int t, bool s) : Socket(-1, i.find(':') == Anope::string::npos ? AF_INET : AF_INET6), HTTPProvider(c, n, i, p, s), Timer(c, 10, Anope::CurTime, true), timeout(t) { } - - void Tick(time_t) override - { - while (!this->clients.empty()) - { - Reference& c = this->clients.front(); - if (c && c->created + this->timeout >= Anope::CurTime) - break; - - delete c; - this->clients.pop_front(); - } - } - - ClientSocket* OnAccept(int fd, const sockaddrs &addr) override - { - auto *c = new MyHTTPClient(this, fd, addr); - this->clients.emplace_back(c); - return c; - } - - bool RegisterPage(HTTPPage *page) override - { - return this->pages.emplace(page->GetURL(), page).second; - } - - void UnregisterPage(HTTPPage *page) override - { - this->pages.erase(page->GetURL()); - } - - HTTPPage* FindPage(const Anope::string &pname) override - { - if (this->pages.count(pname) == 0) - return NULL; - return this->pages[pname]; - } -}; - -class HTTPD final - : public Module -{ - ServiceReference sslref; - std::map providers; -public: - HTTPD(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), sslref("SSLService", "ssl") - { - - } - - ~HTTPD() override - { - 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) || dynamic_cast(s)) - delete s; - } - - this->providers.clear(); - } - - void OnReload(Configuration::Conf *config) override - { - Configuration::Block *conf = config->GetModule(this); - std::set existing; - - for (int i = 0; i < conf->CountBlock("httpd"); ++i) - { - Configuration::Block *block = conf->GetBlock("httpd", i); - - - const Anope::string &hname = block->Get("name", "httpd/main"); - existing.insert(hname); - - Anope::string ip = block->Get("ip"); - int port = block->Get("port", "8080"); - int timeout = block->Get("timeout", "30"); - bool ssl = block->Get("ssl", "no"); - Anope::string ext_ip = block->Get("extforward_ip"); - Anope::string ext_header = block->Get("extforward_header"); - - if (ip.empty()) - { - Log(this) << "You must configure a bind IP for HTTP server " << hname; - continue; - } - else if (port <= 0 || port > 65535) - { - Log(this) << "You must configure a (valid) listen port for HTTP server " << hname; - continue; - } - - MyHTTPProvider *p; - if (this->providers.count(hname) == 0) - { - try - { - p = new MyHTTPProvider(this, hname, ip, port, timeout, ssl); - if (ssl && sslref) - sslref->Init(p); - } - catch (const SocketException &ex) - { - Log(this) << "Unable to create HTTP server " << hname << ": " << ex.GetReason(); - continue; - } - this->providers[hname] = p; - - Log(this) << "Created HTTP server " << hname; - } - else - { - p = this->providers[hname]; - - if (p->GetIP() != ip || p->GetPort() != port) - { - delete p; - this->providers.erase(hname); - - Log(this) << "Changing HTTP server " << hname << " to " << ip << ":" << port; - - try - { - p = new MyHTTPProvider(this, hname, ip, port, timeout, ssl); - if (ssl && sslref) - sslref->Init(p); - } - catch (const SocketException &ex) - { - Log(this) << "Unable to create HTTP server " << hname << ": " << ex.GetReason(); - continue; - } - - this->providers[hname] = p; - } - } - - - spacesepstream(ext_ip).GetTokens(p->ext_ips); - spacesepstream(ext_header).GetTokens(p->ext_headers); - } - - for (std::map::iterator it = this->providers.begin(), it_end = this->providers.end(); it != it_end;) - { - HTTPProvider *p = it->second; - ++it; - - if (existing.count(p->name) == 0) - { - Log(this) << "Removing HTTP server " << p->name; - this->providers.erase(p->name); - delete p; - } - } - } - - void OnModuleLoad(User *u, Module *m) override - { - for (auto &[_, p] : this->providers) - { - if (p->IsSSL() && sslref) - try - { - sslref->Init(p); - } - catch (const CoreException &) { } // Throws on reinitialization - } - } -}; - -MODULE_INIT(HTTPD) diff --git a/modules/m_proxyscan.cpp b/modules/m_proxyscan.cpp deleted file mode 100644 index 279169bfb..000000000 --- a/modules/m_proxyscan.cpp +++ /dev/null @@ -1,388 +0,0 @@ -/* - * - * (C) 2003-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" - -struct ProxyCheck final -{ - std::set types; - std::vector ports; - time_t duration; - Anope::string reason; -}; - -static Anope::string ProxyCheckString; -static Anope::string target_ip; -static unsigned short target_port; -static bool add_to_akill; - -class ProxyCallbackListener final - : public ListenSocket -{ - class ProxyCallbackClient final - : public ClientSocket - , public BufferedSocket - { - public: - ProxyCallbackClient(ListenSocket *l, int f, const sockaddrs &a) : Socket(f, l->GetFamily()), ClientSocket(l, a), BufferedSocket() - { - } - - void OnAccept() override - { - this->Write(ProxyCheckString); - } - - bool ProcessWrite() override - { - return !BufferedSocket::ProcessWrite() || this->write_buffer.empty() ? false : true; - } - }; - -public: - ProxyCallbackListener(const Anope::string &b, int p) : Socket(-1, b.find(':') == Anope::string::npos ? AF_INET : AF_INET6), ListenSocket(b, p, false) - { - } - - ClientSocket *OnAccept(int fd, const sockaddrs &addr) override - { - return new ProxyCallbackClient(this, fd, addr); - } -}; - -class ProxyConnect - : public ConnectionSocket -{ - static ServiceReference akills; - -public: - static std::set proxies; - - ProxyCheck proxy; - unsigned short port; - time_t created; - - ProxyConnect(ProxyCheck &p, unsigned short po) : Socket(-1), ConnectionSocket(), proxy(p), - port(po), created(Anope::CurTime) - { - proxies.insert(this); - } - - ~ProxyConnect() override - { - proxies.erase(this); - } - - void OnConnect() override = 0; - virtual const Anope::string GetType() const = 0; - -protected: - void Ban() - { - Anope::string reason = this->proxy.reason; - - reason = reason.replace_all_cs("%t", this->GetType()); - reason = reason.replace_all_cs("%i", this->conaddr.addr()); - reason = reason.replace_all_cs("%p", stringify(this->conaddr.port())); - - BotInfo *OperServ = Config->GetClient("OperServ"); - Log(OperServ) << "PROXYSCAN: Open " << this->GetType() << " proxy found on " << this->conaddr.str() << " (" << reason << ")"; - auto *x = new XLine("*@" + this->conaddr.addr(), OperServ ? OperServ->nick : "", Anope::CurTime + this->proxy.duration, reason, XLineManager::GenerateUID()); - if (add_to_akill && akills) - { - akills->AddXLine(x); - akills->Send(NULL, x); - } - else - { - if (IRCD->CanSZLine) - IRCD->SendSZLine(NULL, x); - else - IRCD->SendAkill(NULL, x); - delete x; - } - } -}; -ServiceReference ProxyConnect::akills("XLineManager", "xlinemanager/sgline"); -std::set ProxyConnect::proxies; - -class HTTPProxyConnect final - : public ProxyConnect - , public BufferedSocket -{ -public: - HTTPProxyConnect(ProxyCheck &p, unsigned short po) : Socket(-1), ProxyConnect(p, po), BufferedSocket() - { - } - - void OnConnect() override - { - this->Write("CONNECT %s:%d HTTP/1.0", target_ip.c_str(), target_port); - this->Write("Content-Length: 0"); - this->Write("Connection: close"); - this->Write(""); - } - - const Anope::string GetType() const override - { - return "HTTP"; - } - - bool ProcessRead() override - { - bool b = BufferedSocket::ProcessRead(); - if (this->GetLine() == ProxyCheckString) - { - this->Ban(); - return false; - } - return b; - } -}; - -class SOCKS5ProxyConnect final - : public ProxyConnect - , public BinarySocket -{ -public: - SOCKS5ProxyConnect(ProxyCheck &p, unsigned short po) : Socket(-1), ProxyConnect(p, po), BinarySocket() - { - } - - void OnConnect() override - { - sockaddrs target_addr; - char buf[4 + sizeof(target_addr.sa4.sin_addr.s_addr) + sizeof(target_addr.sa4.sin_port)]; - int ptr = 0; - target_addr.pton(AF_INET, target_ip, target_port); - if (!target_addr.valid()) - return; - - buf[ptr++] = 5; // Version - buf[ptr++] = 1; // # of methods - buf[ptr++] = 0; // No authentication - - this->Write(buf, ptr); - - ptr = 1; - buf[ptr++] = 1; // Connect - buf[ptr++] = 0; // Reserved - buf[ptr++] = 1; // IPv4 - memcpy(buf + ptr, &target_addr.sa4.sin_addr.s_addr, sizeof(target_addr.sa4.sin_addr.s_addr)); - ptr += sizeof(target_addr.sa4.sin_addr.s_addr); - memcpy(buf + ptr, &target_addr.sa4.sin_port, sizeof(target_addr.sa4.sin_port)); - ptr += sizeof(target_addr.sa4.sin_port); - - this->Write(buf, ptr); - } - - const Anope::string GetType() const override - { - return "SOCKS5"; - } - - bool Read(const char *buffer, size_t l) override - { - if (l >= ProxyCheckString.length() && !strncmp(buffer, ProxyCheckString.c_str(), ProxyCheckString.length())) - { - this->Ban(); - return false; - } - return true; - } -}; - -class ModuleProxyScan final - : public Module -{ - Anope::string listen_ip; - unsigned short listen_port; - Anope::string con_notice, con_source; - std::vector proxyscans; - - ProxyCallbackListener *listener; - - class ConnectionTimeout final - : public Timer - { - public: - ConnectionTimeout(Module *c, long timeout) : Timer(c, timeout, Anope::CurTime, true) - { - } - - void Tick(time_t) override - { - for (std::set::iterator it = ProxyConnect::proxies.begin(), it_end = ProxyConnect::proxies.end(); it != it_end;) - { - ProxyConnect *p = *it; - ++it; - - if (p->created + this->GetSecs() < Anope::CurTime) - delete p; - } - } - } connectionTimeout; - -public: - ModuleProxyScan(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), - connectionTimeout(this, 5) - { - - - this->listener = NULL; - } - - ~ModuleProxyScan() override - { - for (std::set::iterator it = ProxyConnect::proxies.begin(), it_end = ProxyConnect::proxies.end(); it != it_end;) - { - ProxyConnect *p = *it; - ++it; - delete p; - } - - for (std::map::const_iterator it = SocketEngine::Sockets.begin(), it_end = SocketEngine::Sockets.end(); it != it_end;) - { - Socket *s = it->second; - ++it; - - ClientSocket *cs = dynamic_cast(s); - if (cs != NULL && cs->ls == this->listener) - delete s; - } - - delete this->listener; - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *config = Config->GetModule(this); - - Anope::string s_target_ip = config->Get("target_ip"); - if (s_target_ip.empty()) - throw ConfigException(this->name + " target_ip may not be empty"); - - int s_target_port = config->Get("target_port", "-1"); - if (s_target_port <= 0) - throw ConfigException(this->name + " target_port may not be empty and must be a positive number"); - - Anope::string s_listen_ip = config->Get("listen_ip"); - if (s_listen_ip.empty()) - throw ConfigException(this->name + " listen_ip may not be empty"); - - int s_listen_port = config->Get("listen_port", "-1"); - if (s_listen_port <= 0) - throw ConfigException(this->name + " listen_port may not be empty and must be a positive number"); - - target_ip = s_target_ip; - target_port = s_target_port; - this->listen_ip = s_listen_ip; - this->listen_port = s_listen_port; - this->con_notice = config->Get("connect_notice"); - this->con_source = config->Get("connect_source"); - add_to_akill = config->Get("add_to_akill", "true"); - this->connectionTimeout.SetSecs(config->Get("timeout", "5s")); - - ProxyCheckString = Config->GetBlock("networkinfo")->Get("networkname") + " proxy check"; - delete this->listener; - this->listener = NULL; - try - { - this->listener = new ProxyCallbackListener(this->listen_ip, this->listen_port); - } - catch (const SocketException &ex) - { - throw ConfigException("m_proxyscan: " + ex.GetReason()); - } - - this->proxyscans.clear(); - for (int i = 0; i < config->CountBlock("proxyscan"); ++i) - { - Configuration::Block *block = config->GetBlock("proxyscan", i); - ProxyCheck p; - Anope::string token; - - commasepstream sep(block->Get("type")); - while (sep.GetToken(token)) - { - if (!token.equals_ci("HTTP") && !token.equals_ci("SOCKS5")) - continue; - p.types.insert(token); - } - if (p.types.empty()) - continue; - - commasepstream sep2(block->Get("port")); - while (sep2.GetToken(token)) - { - try - { - unsigned short port = convertTo(token); - p.ports.push_back(port); - } - catch (const ConvertException &) { } - } - if (p.ports.empty()) - continue; - - p.duration = block->Get("time", "4h"); - p.reason = block->Get("reason"); - if (p.reason.empty()) - continue; - - this->proxyscans.push_back(p); - } - } - - void OnUserConnect(User *user, bool &exempt) override - { - if (exempt || user->Quitting() || !Me->IsSynced() || !user->server->IsSynced()) - return; - - /* At this time we only support IPv4 */ - if (!user->ip.valid() || user->ip.sa.sa_family != AF_INET) - /* User doesn't have a valid IPv4 IP (ipv6/spoof/etc) */ - return; - - if (!this->con_notice.empty() && !this->con_source.empty()) - { - BotInfo *bi = BotInfo::Find(this->con_source, true); - if (bi) - user->SendMessage(bi, this->con_notice); - } - - for (unsigned i = this->proxyscans.size(); i > 0; --i) - { - ProxyCheck &p = this->proxyscans[i - 1]; - - for (const auto &type : p.types) - { - for (const auto port : p.ports) - { - try - { - ProxyConnect *con = NULL; - if (type.equals_ci("HTTP")) - con = new HTTPProxyConnect(p, port); - else if (type.equals_ci("SOCKS5")) - con = new SOCKS5ProxyConnect(p, port); - else - continue; - con->Connect(user->ip.addr(), port); - } - catch (const SocketException &ex) - { - Log(LOG_DEBUG) << "m_proxyscan: " << ex.GetReason(); - } - } - } - } - } -}; - -MODULE_INIT(ModuleProxyScan) diff --git a/modules/m_redis.cpp b/modules/m_redis.cpp deleted file mode 100644 index 6aa62bb29..000000000 --- a/modules/m_redis.cpp +++ /dev/null @@ -1,618 +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/redis.h" - -using namespace Redis; - -class MyRedisService; - -class RedisSocket final - : public BinarySocket - , public ConnectionSocket -{ - size_t ParseReply(Reply &r, const char *buf, size_t l); -public: - MyRedisService *provider; - std::deque interfaces; - std::map subinterfaces; - - RedisSocket(MyRedisService *pro, bool v6) : Socket(-1, v6 ? AF_INET6 : AF_INET), provider(pro) { } - - ~RedisSocket() override; - - void OnConnect() override; - void OnError(const Anope::string &error) override; - - bool Read(const char *buffer, size_t l) override; -}; - -class Transaction final - : public Interface -{ -public: - std::deque interfaces; - - Transaction(Module *creator) : Interface(creator) { } - - ~Transaction() override - { - for (auto *iface : interfaces) - { - if (!iface) - continue; - - iface->OnError("Interface going away"); - } - } - - void OnResult(const Reply &r) override - { - /* This is a multi bulk reply of the results of the queued commands - * in this transaction - */ - - Log(LOG_DEBUG_2) << "redis: transaction complete with " << r.multi_bulk.size() << " results"; - - for (auto *result : r.multi_bulk) - { - if (interfaces.empty()) - break; - - Interface *inter = interfaces.front(); - interfaces.pop_front(); - - if (inter) - inter->OnResult(*result); - } - } -}; - -class MyRedisService final - : public Provider -{ -public: - Anope::string host; - int port; - unsigned db; - - RedisSocket *sock = nullptr, *sub = nullptr; - - Transaction ti; - bool in_transaction = false; - - MyRedisService(Module *c, const Anope::string &n, const Anope::string &h, int p, unsigned d) : Provider(c, n), host(h), port(p), db(d), ti(c) - { - sock = new RedisSocket(this, host.find(':') != Anope::string::npos); - sock->Connect(host, port); - - sub = new RedisSocket(this, host.find(':') != Anope::string::npos); - sub->Connect(host, port); - } - - ~MyRedisService() override - { - if (sock) - { - sock->flags[SF_DEAD] = true; - sock->provider = NULL; - } - - if (sub) - { - sub->flags[SF_DEAD] = true; - sub->provider = NULL; - } - } - -private: - inline void Pack(std::vector &buffer, const char *buf, size_t sz = 0) - { - if (!sz) - sz = strlen(buf); - - size_t old_size = buffer.size(); - buffer.resize(old_size + sz); - std::copy(buf, buf + sz, buffer.begin() + old_size); - } - - void Send(RedisSocket *s, Interface *i, const std::vector > &args) - { - std::vector buffer; - - Pack(buffer, "*"); - Pack(buffer, stringify(args.size()).c_str()); - Pack(buffer, "\r\n"); - - for (const auto &[key, value] : args) - { - Pack(buffer, "$"); - Pack(buffer, stringify(value).c_str()); - Pack(buffer, "\r\n"); - - Pack(buffer, key, value); - Pack(buffer, "\r\n"); - } - - if (buffer.empty()) - return; - - s->Write(&buffer[0], buffer.size()); - if (in_transaction) - { - ti.interfaces.push_back(i); - s->interfaces.push_back(NULL); // For the +Queued response - } - else - s->interfaces.push_back(i); - } - -public: - bool IsSocketDead() override - { - return this->sock && this->sock->flags[SF_DEAD]; - } - - void SendCommand(RedisSocket *s, Interface *i, const std::vector &cmds) - { - std::vector > args; - for (const auto &cmd : cmds) - args.emplace_back(cmd.c_str(), cmd.length()); - this->Send(s, i, args); - } - - void SendCommand(RedisSocket *s, Interface *i, const Anope::string &str) - { - std::vector args; - spacesepstream(str).GetTokens(args); - this->SendCommand(s, i, args); - } - - void Send(Interface *i, const std::vector > &args) - { - if (!sock) - { - sock = new RedisSocket(this, host.find(':') != Anope::string::npos); - sock->Connect(host, port); - } - - this->Send(sock, i, args); - } - - void SendCommand(Interface *i, const std::vector &cmds) override - { - std::vector > args; - for (const auto &cmd : cmds) - args.emplace_back(cmd.c_str(), cmd.length()); - this->Send(i, args); - } - - void SendCommand(Interface *i, const Anope::string &str) override - { - std::vector args; - spacesepstream(str).GetTokens(args); - this->SendCommand(i, args); - } - -public: - bool BlockAndProcess() override - { - if (!this->sock->ProcessWrite()) - this->sock->flags[SF_DEAD] = true; - this->sock->SetBlocking(true); - if (!this->sock->ProcessRead()) - this->sock->flags[SF_DEAD] = true; - this->sock->SetBlocking(false); - return !this->sock->interfaces.empty(); - } - - void Subscribe(Interface *i, const Anope::string &pattern) override - { - if (sub == NULL) - { - sub = new RedisSocket(this, host.find(':') != Anope::string::npos); - sub->Connect(host, port); - } - - std::vector args; - args.emplace_back("PSUBSCRIBE"); - args.push_back(pattern); - this->SendCommand(sub, NULL, args); - - sub->subinterfaces[pattern] = i; - } - - void Unsubscribe(const Anope::string &pattern) override - { - if (sub) - sub->subinterfaces.erase(pattern); - } - - void StartTransaction() override - { - if (in_transaction) - throw CoreException(); - - this->SendCommand(NULL, "MULTI"); - in_transaction = true; - } - - void CommitTransaction() override - { - /* The result of the transaction comes back to the reply of EXEC as a multi bulk. - * The reply to the individual commands that make up the transaction when executed - * is a simple +QUEUED - */ - in_transaction = false; - this->SendCommand(&this->ti, "EXEC"); - } -}; - -RedisSocket::~RedisSocket() -{ - if (provider) - { - if (provider->sock == this) - provider->sock = NULL; - else if (provider->sub == this) - provider->sub = NULL; - } - - for (auto *iface : interfaces) - { - if (!iface) - continue; - - iface->OnError("Interface going away"); - } -} - -void RedisSocket::OnConnect() -{ - Log() << "redis: Successfully connected to " << provider->name << (this == this->provider->sub ? " (sub)" : ""); - - this->provider->SendCommand(NULL, "CLIENT SETNAME Anope"); - this->provider->SendCommand(NULL, "SELECT " + stringify(provider->db)); - - if (this != this->provider->sub) - { - this->provider->SendCommand(this, NULL, "CONFIG SET notify-keyspace-events KA"); - } -} - -void RedisSocket::OnError(const Anope::string &error) -{ - Log() << "redis: Error on " << provider->name << (this == this->provider->sub ? " (sub)" : "") << ": " << error; -} - -size_t RedisSocket::ParseReply(Reply &r, const char *buffer, size_t l) -{ - size_t used = 0; - - if (!l) - return used; - - if (r.type == Reply::MULTI_BULK) - goto multi_bulk_cont; - - switch (*buffer) - { - case '+': - { - Anope::string reason(buffer, 1, l - 1); - size_t nl = reason.find("\r\n"); - Log(LOG_DEBUG_2) << "redis: status ok: " << reason.substr(0, nl); - if (nl != Anope::string::npos) - { - r.type = Reply::OK; - used = 1 + nl + 2; - } - break; - } - case '-': - { - Anope::string reason(buffer, 1, l - 1); - size_t nl = reason.find("\r\n"); - Log(LOG_DEBUG) << "redis: status error: " << reason.substr(0, nl); - if (nl != Anope::string::npos) - { - r.type = Reply::NOT_OK; - used = 1 + nl + 2; - } - break; - } - case ':': - { - Anope::string ibuf(buffer, 1, l - 1); - size_t nl = ibuf.find("\r\n"); - if (nl != Anope::string::npos) - { - try - { - r.i = convertTo(ibuf.substr(0, nl)); - } - catch (const ConvertException &) { } - - r.type = Reply::INT; - used = 1 + nl + 2; - } - break; - } - case '$': - { - Anope::string reply(buffer + 1, l - 1); - /* This assumes one bulk can always fit in our recv buffer */ - size_t nl = reply.find("\r\n"); - if (nl != Anope::string::npos) - { - int len; - try - { - len = convertTo(reply.substr(0, nl)); - if (len >= 0) - { - if (1 + nl + 2 + len + 2 <= l) - { - used = 1 + nl + 2 + len + 2; - r.bulk = reply.substr(nl + 2, len); - r.type = Reply::BULK; - } - } - else - { - used = 1 + nl + 2 + 2; - r.type = Reply::BULK; - } - } - catch (const ConvertException &) { } - } - break; - } - multi_bulk_cont: - case '*': - { - if (r.type != Reply::MULTI_BULK) - { - Anope::string reply(buffer + 1, l - 1); - size_t nl = reply.find("\r\n"); - if (nl != Anope::string::npos) - { - r.type = Reply::MULTI_BULK; - try - { - r.multi_bulk_size = convertTo(reply.substr(0, nl)); - } - catch (const ConvertException &) { } - - used = 1 + nl + 2; - } - else - break; - } - else if (r.multi_bulk_size >= 0 && r.multi_bulk.size() == static_cast(r.multi_bulk_size)) - { - /* This multi bulk is already complete, so check the sub bulks */ - for (auto &bulk : r.multi_bulk) - if (bulk->type == Reply::MULTI_BULK) - ParseReply(*bulk, buffer + used, l - used); - break; - } - - for (int i = r.multi_bulk.size(); i < r.multi_bulk_size; ++i) - { - auto *reply = new Reply(); - size_t u = ParseReply(*reply, buffer + used, l - used); - if (!u) - { - Log(LOG_DEBUG) << "redis: ran out of data to parse"; - delete reply; - break; - } - r.multi_bulk.push_back(reply); - used += u; - } - break; - } - default: - Log(LOG_DEBUG) << "redis: unknown reply " << *buffer; - } - - return used; -} - -bool RedisSocket::Read(const char *buffer, size_t l) -{ - static std::vector save; - std::vector copy; - - if (!save.empty()) - { - std::copy(buffer, buffer + l, std::back_inserter(save)); - - copy = save; - - buffer = ©[0]; - l = copy.size(); - } - - while (l) - { - static Reply r; - - size_t used = this->ParseReply(r, buffer, l); - if (!used) - { - Log(LOG_DEBUG) << "redis: used == 0 ?"; - r.Clear(); - break; - } - else if (used > l) - { - Log(LOG_DEBUG) << "redis: used > l ?"; - r.Clear(); - break; - } - - /* Full result is not here yet */ - if (r.type == Reply::MULTI_BULK && static_cast(r.multi_bulk_size) != r.multi_bulk.size()) - { - buffer += used; - l -= used; - break; - } - - if (this == provider->sub) - { - if (r.multi_bulk.size() == 4) - { - /* pmessage - * pattern subscribed to - * __keyevent@0__:set - * key - */ - std::map::iterator it = this->subinterfaces.find(r.multi_bulk[1]->bulk); - if (it != this->subinterfaces.end()) - it->second->OnResult(r); - } - } - else - { - if (this->interfaces.empty()) - { - Log(LOG_DEBUG) << "redis: no interfaces?"; - } - else - { - Interface *i = this->interfaces.front(); - this->interfaces.pop_front(); - - if (i) - { - if (r.type != Reply::NOT_OK) - i->OnResult(r); - else - i->OnError(r.bulk); - } - } - } - - buffer += used; - l -= used; - - r.Clear(); - } - - if (l) - { - save.resize(l); - std::copy(buffer, buffer + l, save.begin()); - } - else - std::vector().swap(save); - - return true; -} - - -class ModuleRedis final - : public Module -{ - std::map services; - -public: - ModuleRedis(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) - { - } - - ~ModuleRedis() override - { - for (auto &[_, p] : services) - { - delete p->sock; - p->sock = NULL; - - delete p->sub; - p->sub = NULL; - - delete p; - } - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *block = conf->GetModule(this); - std::vector new_services; - - for (int i = 0; i < block->CountBlock("redis"); ++i) - { - Configuration::Block *redis = block->GetBlock("redis", i); - - const Anope::string &n = redis->Get("name"), - &ip = redis->Get("ip"); - int port = redis->Get("port"); - unsigned db = redis->Get("db"); - - delete services[n]; - services[n] = new MyRedisService(this, n, ip, port, db); - new_services.push_back(n); - } - - for (std::map::iterator it = services.begin(); it != services.end();) - { - Provider *p = it->second; - ++it; - - if (std::find(new_services.begin(), new_services.end(), p->name) == new_services.end()) - delete it->second; - } - } - - void OnModuleUnload(User *, Module *m) override - { - for (auto &[_, p] : services) - { - if (p->sock) - for (unsigned i = p->sock->interfaces.size(); i > 0; --i) - { - Interface *inter = p->sock->interfaces[i - 1]; - - if (inter && inter->owner == m) - { - inter->OnError(m->name + " being unloaded"); - p->sock->interfaces.erase(p->sock->interfaces.begin() + i - 1); - } - } - - if (p->sub) - for (unsigned i = p->sub->interfaces.size(); i > 0; --i) - { - Interface *inter = p->sub->interfaces[i - 1]; - - if (inter && inter->owner == m) - { - inter->OnError(m->name + " being unloaded"); - p->sub->interfaces.erase(p->sub->interfaces.begin() + i - 1); - } - } - - for (unsigned i = p->ti.interfaces.size(); i > 0; --i) - { - Interface *inter = p->ti.interfaces[i - 1]; - - if (inter && inter->owner == m) - { - inter->OnError(m->name + " being unloaded"); - p->ti.interfaces.erase(p->ti.interfaces.begin() + i - 1); - } - } - } - } -}; - -MODULE_INIT(ModuleRedis) diff --git a/modules/m_regex_stdlib.cpp b/modules/m_regex_stdlib.cpp deleted file mode 100644 index d759cc8fd..000000000 --- a/modules/m_regex_stdlib.cpp +++ /dev/null @@ -1,107 +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 - -class StdLibRegex final - : public Regex -{ -private: - std::regex regex; - -public: - StdLibRegex(const Anope::string &expr, std::regex::flag_type type) - : Regex(expr) - { - try - { - regex.assign(expr.str(), type | std::regex::optimize); - } - catch (const std::regex_error& error) - { - throw RegexException("Error in regex " + expr + ": " + error.what()); - } - } - - bool Matches(const Anope::string &str) - { - return std::regex_search(str.str(), regex); - } -}; - -class StdLibRegexProvider final - : public RegexProvider -{ -public: - std::regex_constants::syntax_option_type type; - - StdLibRegexProvider(Module *creator) - : RegexProvider(creator, "regex/stdlib") - { - } - - Regex *Compile(const Anope::string &expression) override - { - return new StdLibRegex(expression, type); - } -}; - -class ModuleRegexStdLib final - : public Module -{ -private: - StdLibRegexProvider stdlib_regex_provider; - -public: - ModuleRegexStdLib(const Anope::string &modname, const Anope::string &creator) - : Module(modname, creator, EXTRA | VENDOR) - , stdlib_regex_provider(this) - { - this->SetPermanent(true); - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *block = conf->GetModule(this); - - const Anope::string syntax = block->Get("syntax", "ecmascript"); - if (syntax == "awk") - stdlib_regex_provider.type = std::regex::awk; - else if (syntax == "basic") - stdlib_regex_provider.type = std::regex::basic; - else if (syntax == "ecmascript") - stdlib_regex_provider.type = std::regex::ECMAScript; - else if (syntax == "egrep") - stdlib_regex_provider.type = std::regex::egrep; - else if (syntax == "extended") - stdlib_regex_provider.type = std::regex::extended; - else if (syntax == "grep") - stdlib_regex_provider.type = std::regex::grep; - else - throw ConfigException(this->name + ": syntax must be set to awk, basic, ecmascript, egrep, extended, or grep."); - } - - ~ModuleRegexStdLib() - { - 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(ModuleRegexStdLib) diff --git a/modules/m_rewrite.cpp b/modules/m_rewrite.cpp deleted file mode 100644 index 59560227b..000000000 --- a/modules/m_rewrite.cpp +++ /dev/null @@ -1,185 +0,0 @@ -/* - * - * (C) 2003-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" - -struct Rewrite final -{ - Anope::string client, source_message, target_message, desc; - - bool Matches(const std::vector &message) - { - std::vector sm; - spacesepstream(this->source_message).GetTokens(sm); - - for (unsigned i = 0; i < sm.size(); ++i) - if (i >= message.size() || (sm[i] != "$" && !sm[i].equals_ci(message[i]))) - return false; - - return true; - } - - Anope::string Process(CommandSource &source, const std::vector ¶ms) - { - spacesepstream sep(this->target_message); - Anope::string token, message; - - while (sep.GetToken(token)) - { - if (token[0] != '$') - message += " " + token; - else if (token == "$me") - message += " " + source.GetNick(); - else - { - int num = -1, end = -1; - try - { - Anope::string num_str = token.substr(1); - size_t hy = num_str.find('-'); - if (hy == Anope::string::npos) - { - num = convertTo(num_str); - end = num + 1; - } - else - { - num = convertTo(num_str.substr(0, hy)); - if (hy == num_str.length() - 1) - end = params.size(); - else - end = convertTo(num_str.substr(hy + 1)) + 1; - } - } - catch (const ConvertException &) - { - continue; - } - - for (int i = num; i < end && static_cast(i) < params.size(); ++i) - message += " " + params[i]; - } - } - - message.trim(); - return message; - } - - static std::vector rewrites; - - static Rewrite *Find(const Anope::string &client, const Anope::string &cmd) - { - for (auto &r : rewrites) - { - if ((client.empty() || r.client.equals_ci(client)) && (r.source_message.equals_ci(cmd) || r.source_message.find_ci(cmd + " ") == 0)) - return &r; - } - - return NULL; - } - - static Rewrite *Match(const Anope::string &client, const std::vector ¶ms) - { - for (auto &r : rewrites) - { - if ((client.empty() || r.client.equals_ci(client)) && r.Matches(params)) - return &r; - } - - return NULL; - } -}; - -std::vector Rewrite::rewrites; - -class RewriteCommand final - : public Command -{ -public: - RewriteCommand(Module *creator) : Command(creator, "rewrite", 0, 0) { } - - void Execute(CommandSource &source, const std::vector ¶ms) override - { - std::vector full_params = params; - full_params.insert(full_params.begin(), source.command); - - Rewrite *r = Rewrite::Match(!source.c ? source.service->nick : "", full_params); - if (r != NULL) - { - Anope::string new_message = r->Process(source, full_params); - Log(LOG_DEBUG) << "m_rewrite: Rewrote '" << source.command << (!params.empty() ? " " + params[0] : "") << "' to '" << new_message << "' using '" << r->source_message << "'"; - source.service = BotInfo::Find(r->client, true); - if (!source.service) - return; - Command::Run(source, new_message); - } - else - Log() << "m_rewrite: Unable to rewrite '" << source.command << (!params.empty() ? " " + params[0] : "") << "'"; - } - - void OnServHelp(CommandSource &source) override - { - Rewrite *r = Rewrite::Find(!source.c ? source.service->nick : "", source.command); - if (r != NULL && !r->desc.empty()) - { - this->SetDesc(r->desc); - Command::OnServHelp(source); - } - } - - bool OnHelp(CommandSource &source, const Anope::string &subcommand) override - { - Rewrite *r = Rewrite::Find(!source.c ? source.service->nick : "", source.command); - if (r != NULL && !r->desc.empty()) - { - source.Reply(r->desc); - size_t sz = r->target_message.find(' '); - source.Reply(_("This command is an alias to the command %s."), sz != Anope::string::npos ? r->target_message.substr(0, sz).c_str() : r->target_message.c_str()); - return true; - } - return false; - } -}; - -class ModuleRewrite final - : public Module -{ - RewriteCommand cmdrewrite; - -public: - ModuleRewrite(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR | EXTRA), cmdrewrite(this) - { - } - - void OnReload(Configuration::Conf *conf) override - { - Rewrite::rewrites.clear(); - - for (int i = 0; i < conf->CountBlock("command"); ++i) - { - Configuration::Block *block = conf->GetBlock("command", i); - - if (!block->Get("rewrite")) - continue; - - Rewrite rw; - - rw.client = block->Get("service"); - rw.source_message = block->Get("rewrite_source"); - rw.target_message = block->Get("rewrite_target"); - rw.desc = block->Get("rewrite_description"); - - if (rw.client.empty() || rw.source_message.empty() || rw.target_message.empty()) - continue; - - Rewrite::rewrites.push_back(rw); - } - } -}; - -MODULE_INIT(ModuleRewrite) diff --git a/modules/m_sasl.cpp b/modules/m_sasl.cpp deleted file mode 100644 index 298ad0e75..000000000 --- a/modules/m_sasl.cpp +++ /dev/null @@ -1,414 +0,0 @@ -/* - * - * (C) 2014-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" -#include "modules/sasl.h" -#include "modules/ns_cert.h" - -using namespace SASL; - -class Plain final - : public Mechanism -{ -public: - Plain(Module *o) : Mechanism(o, "PLAIN") { } - - void ProcessMessage(Session *sess, const SASL::Message &m) override - { - if (m.type == "S") - { - sasl->SendMessage(sess, "C", "+"); - } - else if (m.type == "C") - { - // message = [authzid] UTF8NUL authcid UTF8NUL passwd - Anope::string message; - Anope::B64Decode(m.data, message); - - size_t zcsep = message.find('\0'); - if (zcsep == Anope::string::npos) - { - sasl->Fail(sess); - delete sess; - return; - } - - size_t cpsep = message.find('\0', zcsep + 1); - if (cpsep == Anope::string::npos) - { - sasl->Fail(sess); - delete sess; - return; - } - - Anope::string authzid = message.substr(0, zcsep); - Anope::string authcid = message.substr(zcsep + 1, cpsep - zcsep - 1); - - // We don't support having an authcid that is different to the authzid. - if (!authzid.empty() && authzid != authcid) - { - sasl->Fail(sess); - delete sess; - return; - } - - Anope::string passwd = message.substr(cpsep + 1); - - if (authcid.empty() || passwd.empty() || !IRCD->IsNickValid(authcid) || passwd.find_first_of("\r\n\0") != Anope::string::npos) - { - sasl->Fail(sess); - delete sess; - return; - } - - SASL::IdentifyRequest *req = new SASL::IdentifyRequest(this->owner, m.source, authcid, passwd, sess->hostname, sess->ip); - FOREACH_MOD(OnCheckAuthentication, (NULL, req)); - req->Dispatch(); - } - } -}; - -class External final - : public Mechanism -{ - ServiceReference certs; - - struct Session final - : SASL::Session - { - Anope::string cert; - - Session(Mechanism *m, const Anope::string &u) : SASL::Session(m, u) { } - }; - -public: - External(Module *o) : Mechanism(o, "EXTERNAL"), certs("CertService", "certs") - { - if (!IRCD || !IRCD->CanCertFP) - throw ModuleException("No CertFP"); - } - - Session* CreateSession(const Anope::string &uid) override - { - return new Session(this, uid); - } - - void ProcessMessage(SASL::Session *sess, const SASL::Message &m) override - { - Session *mysess = anope_dynamic_static_cast(sess); - - if (m.type == "S") - { - mysess->cert = m.ext; - - sasl->SendMessage(sess, "C", "+"); - } - else if (m.type == "C") - { - if (!certs || mysess->cert.empty()) - { - sasl->Fail(sess); - delete sess; - return; - } - - Anope::string user = "A user"; - if (!mysess->hostname.empty() && !mysess->ip.empty()) - user = mysess->hostname + " (" + mysess->ip + ")"; - - NickCore *nc = certs->FindAccountFromCert(mysess->cert); - if (!nc || nc->HasExt("NS_SUSPENDED") || nc->HasExt("UNCONFIRMED")) - { - Log(this->owner, "sasl", Config->GetClient("NickServ")) << user << " failed to identify using certificate " << mysess->cert << " using SASL EXTERNAL"; - sasl->Fail(sess); - delete sess; - return; - } - - Log(this->owner, "sasl", Config->GetClient("NickServ")) << user << " identified to account " << nc->display << " using SASL EXTERNAL"; - sasl->Succeed(sess, nc); - delete sess; - } - } -}; - -class Anonymous final - : public Mechanism -{ -public: - Anonymous(Module *o) : Mechanism(o, "ANONYMOUS") { } - - void ProcessMessage(Session *sess, const SASL::Message &m) override - { - if (!IRCD->CanSVSLogout && !User::Find(sess->uid)) - { - // This IRCd can't log users out yet. - sasl->Fail(sess); - delete sess; - return; - } - - if (m.type == "S") - { - sasl->SendMessage(sess, "C", "+"); - } - else if (m.type == "C") - { - Anope::string decoded; - Anope::B64Decode(m.data, decoded); - - Anope::string user = "A user"; - if (!sess->hostname.empty() && !sess->ip.empty()) - user = sess->hostname + " (" + sess->ip + ")"; - if (!decoded.empty()) - user += " [" + decoded + "]"; - - Log(this->owner, "sasl", Config->GetClient("NickServ")) << user << " unidentified using SASL ANONYMOUS"; - sasl->Succeed(sess, nullptr); - } - } -}; - -class SASLService final - : public SASL::Service - , public Timer -{ - std::map sessions; - -public: - SASLService(Module *o) : SASL::Service(o), Timer(o, 60, Anope::CurTime, true) { } - - ~SASLService() override - { - for (const auto &[_, session] : sessions) - delete session; - } - - void ProcessMessage(const SASL::Message &m) override - { - if (m.target != "*") - { - Server *s = Server::Find(m.target); - if (s != Me) - { - User *u = User::Find(m.target); - if (!u || u->server != Me) - return; - } - } - - Session* session = GetSession(m.source); - - if (m.type == "S") - { - ServiceReference mech("SASL::Mechanism", m.data); - if (!mech) - { - Session tmp(NULL, m.source); - - sasl->SendMechs(&tmp); - sasl->Fail(&tmp); - return; - } - - Anope::string hostname, ip; - if (session) - { - // Copy over host/ip to mech-specific session - hostname = session->hostname; - ip = session->ip; - delete session; - } - - session = mech->CreateSession(m.source); - if (session) - { - session->hostname = hostname; - session->ip = ip; - - sessions[m.source] = session; - } - } - else if (m.type == "D") - { - delete session; - return; - } - else if (m.type == "H") - { - if (!session) - { - session = new Session(NULL, m.source); - sessions[m.source] = session; - } - session->hostname = m.data; - session->ip = m.ext; - } - - if (session && session->mech) - session->mech->ProcessMessage(session, m); - } - - Anope::string GetAgent() override - { - Anope::string agent = Config->GetModule(Service::owner)->Get("agent", "NickServ"); - BotInfo *bi = Config->GetClient(agent); - if (bi) - agent = bi->GetUID(); - return agent; - } - - Session* GetSession(const Anope::string &uid) override - { - std::map::iterator it = sessions.find(uid); - if (it != sessions.end()) - return it->second; - return NULL; - } - - void RemoveSession(Session *sess) override - { - sessions.erase(sess->uid); - } - - void DeleteSessions(Mechanism *mech, bool da) override - { - for (std::map::iterator it = sessions.begin(); it != sessions.end();) - { - std::map::iterator del = it++; - if (*del->second->mech == mech) - { - if (da) - this->SendMessage(del->second, "D", "A"); - delete del->second; - } - } - } - - void SendMessage(Session *session, const Anope::string &mtype, const Anope::string &data) override - { - SASL::Message msg; - msg.source = this->GetAgent(); - msg.target = session->uid; - msg.type = mtype; - msg.data = data; - - IRCD->SendSASLMessage(msg); - } - - void Succeed(Session *session, NickCore *nc) override - { - // If the user is already introduced then we log them in now. - // Otherwise, we send an SVSLOGIN to log them in later. - User *user = User::Find(session->uid); - NickAlias *na = NickAlias::Find(nc->display); - if (user) - { - if (na) - user->Identify(na); - else - user->Logout(); - } - else - { - IRCD->SendSVSLogin(session->uid, na); - } - this->SendMessage(session, "D", "S"); - } - - void Fail(Session *session) override - { - this->SendMessage(session, "D", "F"); - } - - void SendMechs(Session *session) override - { - std::vector mechs = Service::GetServiceKeys("SASL::Mechanism"); - Anope::string buf; - for (const auto &mech : mechs) - buf += "," + mech; - - this->SendMessage(session, "M", buf.empty() ? "" : buf.substr(1)); - } - - void Tick(time_t) override - { - for (std::map::iterator it = sessions.begin(); it != sessions.end();) - { - Anope::string key = it->first; - Session *s = it->second; - ++it; - - if (!s || s->created + 60 < Anope::CurTime) - { - delete s; - sessions.erase(key); - } - } - } -}; - -class ModuleSASL final - : public Module -{ - SASLService sasl; - - Anonymous anonymous; - Plain plain; - External *external = nullptr; - - std::vector mechs; - - void CheckMechs() - { - std::vector newmechs = ::Service::GetServiceKeys("SASL::Mechanism"); - if (newmechs == mechs) - return; - - mechs = newmechs; - - // If we are connected to the network then broadcast the mechlist. - if (Me && Me->IsSynced()) - IRCD->SendSASLMechanisms(mechs); - } - -public: - ModuleSASL(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), - sasl(this), anonymous(this), plain(this) - { - try - { - external = new External(this); - CheckMechs(); - } - catch (ModuleException &) { } - } - - ~ModuleSASL() override - { - delete external; - } - - void OnModuleLoad(User *, Module *) override - { - CheckMechs(); - } - - void OnModuleUnload(User *, Module *) override - { - CheckMechs(); - } - - void OnPreUplinkSync(Server *) override - { - // We have not yet sent a mechanism list so always do it here. - IRCD->SendSASLMechanisms(mechs); - } -}; - -MODULE_INIT(ModuleSASL) diff --git a/modules/m_xmlrpc.cpp b/modules/m_xmlrpc.cpp deleted file mode 100644 index 854fcc732..000000000 --- a/modules/m_xmlrpc.cpp +++ /dev/null @@ -1,229 +0,0 @@ -/* - * - * (C) 2010-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" -#include "modules/xmlrpc.h" -#include "modules/httpd.h" - -static struct special_chars final -{ - Anope::string character; - Anope::string replace; - - special_chars(const Anope::string &c, const Anope::string &r) : character(c), replace(r) { } -} -special[] = { - special_chars("&", "&"), - special_chars("\"", """), - special_chars("<", "<"), - special_chars(">", "&qt;"), - special_chars("'", "'"), - special_chars("\n", " "), - special_chars("\002", ""), // bold - special_chars("\003", ""), // color - special_chars("\035", ""), // italics - special_chars("\037", ""), // underline - special_chars("\026", ""), // reverses - special_chars("", "") -}; - -class MyXMLRPCServiceInterface final - : public XMLRPCServiceInterface - , public HTTPPage -{ - std::deque events; - -public: - MyXMLRPCServiceInterface(Module *creator, const Anope::string &sname) : XMLRPCServiceInterface(creator, sname), HTTPPage("/xmlrpc", "text/xml") { } - - void Register(XMLRPCEvent *event) override - { - this->events.push_back(event); - } - - void Unregister(XMLRPCEvent *event) override - { - std::deque::iterator it = std::find(this->events.begin(), this->events.end(), event); - - if (it != this->events.end()) - this->events.erase(it); - } - - Anope::string Sanitize(const Anope::string &string) override - { - Anope::string ret = string; - for (int i = 0; special[i].character.empty() == false; ++i) - ret = ret.replace_all_cs(special[i].character, special[i].replace); - return ret; - } - - static Anope::string Unescape(const Anope::string &string) - { - Anope::string ret = string; - for (int i = 0; special[i].character.empty() == false; ++i) - if (!special[i].replace.empty()) - ret = ret.replace_all_cs(special[i].replace, special[i].character); - - for (size_t i, last = 0; (i = string.find("&#", last)) != Anope::string::npos;) - { - last = i + 1; - - size_t end = string.find(';', i); - if (end == Anope::string::npos) - break; - - Anope::string ch = string.substr(i + 2, end - (i + 2)); - - if (ch.empty()) - continue; - - long l; - if (!ch.empty() && ch[0] == 'x') - l = strtol(ch.substr(1).c_str(), NULL, 16); - else - l = strtol(ch.c_str(), NULL, 10); - - if (l > 0 && l < 256) - ret = ret.replace_all_cs("&#" + ch + ";", Anope::string(l)); - } - - return ret; - } - -private: - static bool GetData(Anope::string &content, Anope::string &tag, Anope::string &data) - { - if (content.empty()) - return false; - - Anope::string prev, cur; - bool istag; - - do - { - prev = cur; - cur.clear(); - - size_t len = 0; - istag = false; - - if (content[0] == '<') - { - len = content.find_first_of('>'); - istag = true; - } - else if (content[0] != '>') - { - len = content.find_first_of('<'); - } - - // len must advance - if (len == Anope::string::npos || len == 0) - break; - - if (istag) - { - cur = content.substr(1, len - 1); - content.erase(0, len + 1); - while (!content.empty() && content[0] == ' ') - content.erase(content.begin()); - } - else - { - cur = content.substr(0, len); - content.erase(0, len); - } - } - while (istag && !content.empty()); - - tag = Unescape(prev); - data = Unescape(cur); - return !istag && !data.empty(); - } - -public: - bool OnRequest(HTTPProvider *provider, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) override - { - Anope::string content = message.content, tname, data; - XMLRPCRequest request(reply); - - while (GetData(content, tname, data)) - { - Log(LOG_DEBUG) << "m_xmlrpc: Tag name: " << tname << ", data: " << data; - if (tname == "methodName") - request.name = data; - else if (tname == "name" && data == "id") - { - GetData(content, tname, data); - request.id = data; - } - else if (tname == "string") - request.data.push_back(data); - } - - for (auto *e : this->events) - { - if (!e->Run(this, client, request)) - return false; - else if (!request.get_replies().empty()) - { - this->Reply(request); - return true; - } - } - - reply.error = HTTP_PAGE_NOT_FOUND; - reply.Write("Unrecognized query"); - return true; - } - - void Reply(XMLRPCRequest &request) override - { - if (!request.id.empty()) - request.reply("id", request.id); - - Anope::string r = "\n\n\n\n\n\n"; - for (const auto &[name, value] : request.get_replies()) - r += "\n" + name + "\n\n" + this->Sanitize(value) + "\n\n\n"; - r += "\n\n\n\n"; - - request.r.Write(r); - } -}; - -class ModuleXMLRPC final - : public Module -{ - ServiceReference httpref; -public: - MyXMLRPCServiceInterface xmlrpcinterface; - - ModuleXMLRPC(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), - xmlrpcinterface(this, "xmlrpc") - { - - } - - ~ModuleXMLRPC() override - { - if (httpref) - httpref->UnregisterPage(&xmlrpcinterface); - } - - void OnReload(Configuration::Conf *conf) override - { - if (httpref) - httpref->UnregisterPage(&xmlrpcinterface); - this->httpref = ServiceReference("HTTPProvider", conf->GetModule(this)->Get("server", "httpd/main")); - if (!httpref) - throw ConfigException("Unable to find http reference, is m_httpd loaded?"); - httpref->RegisterPage(&xmlrpcinterface); - } -}; - -MODULE_INIT(ModuleXMLRPC) diff --git a/modules/m_xmlrpc_main.cpp b/modules/m_xmlrpc_main.cpp deleted file mode 100644 index 63ad601e9..000000000 --- a/modules/m_xmlrpc_main.cpp +++ /dev/null @@ -1,306 +0,0 @@ -/* - * - * (C) 2010-2024 Anope Team - * Contact us at team@anope.org - * - * Please read COPYING and README for further details. - */ - -#include "module.h" -#include "modules/xmlrpc.h" - -static Module *me; - -class XMLRPCIdentifyRequest final - : public IdentifyRequest -{ - XMLRPCRequest request; - HTTPReply repl; /* Request holds a reference to the HTTPReply, because we might exist long enough to invalidate it - we'll copy it here then reset the reference before we use it */ - Reference client; - Reference xinterface; - -public: - XMLRPCIdentifyRequest(Module *m, XMLRPCRequest& req, HTTPClient *c, XMLRPCServiceInterface* iface, const Anope::string &acc, const Anope::string &pass) : IdentifyRequest(m, acc, pass), request(req), repl(request.r), client(c), xinterface(iface) { } - - void OnSuccess() override - { - if (!xinterface || !client) - return; - - request.r = this->repl; - - request.reply("result", "Success"); - request.reply("account", GetAccount()); - - xinterface->Reply(request); - client->SendReply(&request.r); - } - - void OnFail() override - { - if (!xinterface || !client) - return; - - request.r = this->repl; - - request.reply("error", "Invalid password"); - - xinterface->Reply(request); - client->SendReply(&request.r); - } -}; - -class MyXMLRPCEvent final - : public XMLRPCEvent -{ -public: - bool Run(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) override - { - if (request.name == "command") - this->DoCommand(iface, client, request); - else if (request.name == "checkAuthentication") - return this->DoCheckAuthentication(iface, client, request); - else if (request.name == "stats") - this->DoStats(iface, client, request); - else if (request.name == "channel") - this->DoChannel(iface, client, request); - else if (request.name == "user") - this->DoUser(iface, client, request); - else if (request.name == "opers") - this->DoOperType(iface, client, request); - else if (request.name == "notice") - this->DoNotice(iface, client, request); - - return true; - } - -private: - void DoCommand(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) - { - Anope::string service = request.data.size() > 0 ? request.data[0] : ""; - Anope::string user = request.data.size() > 1 ? request.data[1] : ""; - Anope::string command = request.data.size() > 2 ? request.data[2] : ""; - - if (service.empty() || user.empty() || command.empty()) - request.reply("error", "Invalid parameters"); - else - { - BotInfo *bi = BotInfo::Find(service, true); - if (!bi) - request.reply("error", "Invalid service"); - else - { - request.reply("result", "Success"); - - NickAlias *na = NickAlias::Find(user); - - Anope::string out; - - struct XMLRPCommandReply final - : CommandReply - { - Anope::string &str; - - XMLRPCommandReply(Anope::string &s) : str(s) { } - - void SendMessage(BotInfo *, const Anope::string &msg) override - { - str += msg + "\n"; - }; - } - reply(out); - - User *u = User::Find(user, true); - CommandSource source(user, u, na ? *na->nc : NULL, &reply, bi); - Command::Run(source, command); - - if (!out.empty()) - request.reply("return", iface->Sanitize(out)); - } - } - } - - bool DoCheckAuthentication(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) - { - Anope::string username = request.data.size() > 0 ? request.data[0] : ""; - Anope::string password = request.data.size() > 1 ? request.data[1] : ""; - - if (username.empty() || password.empty()) - request.reply("error", "Invalid parameters"); - else - { - auto *req = new XMLRPCIdentifyRequest(me, request, client, iface, username, password); - FOREACH_MOD(OnCheckAuthentication, (NULL, req)); - req->Dispatch(); - return false; - } - - return true; - } - - void DoStats(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) - { - request.reply("uptime", stringify(Anope::CurTime - Anope::StartTime)); - request.reply("uplinkname", Me->GetLinks().front()->GetName()); - { - Anope::string buf; - for (const auto &capab : Servers::Capab) - buf += " " + capab; - if (!buf.empty()) - buf.erase(buf.begin()); - request.reply("uplinkcapab", buf); - } - request.reply("usercount", stringify(UserListByNick.size())); - request.reply("maxusercount", stringify(MaxUserCount)); - request.reply("channelcount", stringify(ChannelList.size())); - } - - void DoChannel(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) - { - if (request.data.empty()) - return; - - Channel *c = Channel::Find(request.data[0]); - - request.reply("name", iface->Sanitize(c ? c->name : request.data[0])); - - if (c) - { - request.reply("bancount", stringify(c->HasMode("BAN"))); - int count = 0; - for (auto &ban : c->GetModeList("BAN")) - request.reply("ban" + stringify(++count), iface->Sanitize(ban)); - - request.reply("exceptcount", stringify(c->HasMode("EXCEPT"))); - count = 0; - for (auto &except : c->GetModeList("EXCEPT")) - request.reply("except" + stringify(++count), iface->Sanitize(except)); - - request.reply("invitecount", stringify(c->HasMode("INVITEOVERRIDE"))); - count = 0; - for (auto &invite : c->GetModeList("INVITEOVERRIDE")) - request.reply("invite" + stringify(++count), iface->Sanitize(invite)); - - Anope::string users; - for (Channel::ChanUserList::const_iterator it = c->users.begin(); it != c->users.end(); ++it) - { - ChanUserContainer *uc = it->second; - users += uc->status.BuildModePrefixList() + uc->user->nick + " "; - } - if (!users.empty()) - { - users.erase(users.length() - 1); - request.reply("users", iface->Sanitize(users)); - } - - if (!c->topic.empty()) - request.reply("topic", iface->Sanitize(c->topic)); - - if (!c->topic_setter.empty()) - request.reply("topicsetter", iface->Sanitize(c->topic_setter)); - - request.reply("topictime", stringify(c->topic_time)); - request.reply("topicts", stringify(c->topic_ts)); - } - } - - void DoUser(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) - { - if (request.data.empty()) - return; - - User *u = User::Find(request.data[0]); - - request.reply("nick", iface->Sanitize(u ? u->nick : request.data[0])); - - if (u) - { - request.reply("ident", iface->Sanitize(u->GetIdent())); - request.reply("vident", iface->Sanitize(u->GetVIdent())); - request.reply("host", iface->Sanitize(u->host)); - if (!u->vhost.empty()) - request.reply("vhost", iface->Sanitize(u->vhost)); - if (!u->chost.empty()) - request.reply("chost", iface->Sanitize(u->chost)); - request.reply("ip", u->ip.addr()); - request.reply("timestamp", stringify(u->timestamp)); - request.reply("signon", stringify(u->signon)); - if (u->Account()) - { - request.reply("account", iface->Sanitize(u->Account()->display)); - if (u->Account()->o) - request.reply("opertype", iface->Sanitize(u->Account()->o->ot->GetName())); - } - - Anope::string channels; - for (User::ChanUserList::const_iterator it = u->chans.begin(); it != u->chans.end(); ++it) - { - ChanUserContainer *cc = it->second; - channels += cc->status.BuildModePrefixList() + cc->chan->name + " "; - } - if (!channels.empty()) - { - channels.erase(channels.length() - 1); - request.reply("channels", channels); - } - } - } - - void DoOperType(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) - { - for (auto *ot : Config->MyOperTypes) - { - Anope::string perms; - for (const auto &priv : ot->GetPrivs()) - perms += " " + priv; - for (const auto &command : ot->GetCommands()) - perms += " " + command; - request.reply(ot->GetName(), perms); - } - } - - void DoNotice(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) - { - Anope::string from = request.data.size() > 0 ? request.data[0] : ""; - Anope::string to = request.data.size() > 1 ? request.data[1] : ""; - Anope::string message = request.data.size() > 2 ? request.data[2] : ""; - - BotInfo *bi = BotInfo::Find(from, true); - User *u = User::Find(to, true); - - if (!bi || !u || message.empty()) - return; - - u->SendMessage(bi, message); - - request.reply("result", "Success"); - } -}; - -class ModuleXMLRPCMain final - : public Module -{ - ServiceReference xmlrpc; - - MyXMLRPCEvent stats; - -public: - ModuleXMLRPCMain(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), xmlrpc("XMLRPCServiceInterface", "xmlrpc") - { - me = this; - - if (!xmlrpc) - throw ModuleException("Unable to find xmlrpc reference, is m_xmlrpc loaded?"); - - xmlrpc->Register(&stats); - } - - ~ModuleXMLRPCMain() override - { - if (xmlrpc) - xmlrpc->Unregister(&stats); - } -}; - -MODULE_INIT(ModuleXMLRPCMain) diff --git a/modules/proxyscan.cpp b/modules/proxyscan.cpp new file mode 100644 index 000000000..b92de0dea --- /dev/null +++ b/modules/proxyscan.cpp @@ -0,0 +1,388 @@ +/* + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" + +struct ProxyCheck final +{ + std::set types; + std::vector ports; + time_t duration; + Anope::string reason; +}; + +static Anope::string ProxyCheckString; +static Anope::string target_ip; +static unsigned short target_port; +static bool add_to_akill; + +class ProxyCallbackListener final + : public ListenSocket +{ + class ProxyCallbackClient final + : public ClientSocket + , public BufferedSocket + { + public: + ProxyCallbackClient(ListenSocket *l, int f, const sockaddrs &a) : Socket(f, l->GetFamily()), ClientSocket(l, a), BufferedSocket() + { + } + + void OnAccept() override + { + this->Write(ProxyCheckString); + } + + bool ProcessWrite() override + { + return !BufferedSocket::ProcessWrite() || this->write_buffer.empty() ? false : true; + } + }; + +public: + ProxyCallbackListener(const Anope::string &b, int p) : Socket(-1, b.find(':') == Anope::string::npos ? AF_INET : AF_INET6), ListenSocket(b, p, false) + { + } + + ClientSocket *OnAccept(int fd, const sockaddrs &addr) override + { + return new ProxyCallbackClient(this, fd, addr); + } +}; + +class ProxyConnect + : public ConnectionSocket +{ + static ServiceReference akills; + +public: + static std::set proxies; + + ProxyCheck proxy; + unsigned short port; + time_t created; + + ProxyConnect(ProxyCheck &p, unsigned short po) : Socket(-1), ConnectionSocket(), proxy(p), + port(po), created(Anope::CurTime) + { + proxies.insert(this); + } + + ~ProxyConnect() override + { + proxies.erase(this); + } + + void OnConnect() override = 0; + virtual const Anope::string GetType() const = 0; + +protected: + void Ban() + { + Anope::string reason = this->proxy.reason; + + reason = reason.replace_all_cs("%t", this->GetType()); + reason = reason.replace_all_cs("%i", this->conaddr.addr()); + reason = reason.replace_all_cs("%p", stringify(this->conaddr.port())); + + BotInfo *OperServ = Config->GetClient("OperServ"); + Log(OperServ) << "PROXYSCAN: Open " << this->GetType() << " proxy found on " << this->conaddr.str() << " (" << reason << ")"; + auto *x = new XLine("*@" + this->conaddr.addr(), OperServ ? OperServ->nick : "", Anope::CurTime + this->proxy.duration, reason, XLineManager::GenerateUID()); + if (add_to_akill && akills) + { + akills->AddXLine(x); + akills->Send(NULL, x); + } + else + { + if (IRCD->CanSZLine) + IRCD->SendSZLine(NULL, x); + else + IRCD->SendAkill(NULL, x); + delete x; + } + } +}; +ServiceReference ProxyConnect::akills("XLineManager", "xlinemanager/sgline"); +std::set ProxyConnect::proxies; + +class HTTPProxyConnect final + : public ProxyConnect + , public BufferedSocket +{ +public: + HTTPProxyConnect(ProxyCheck &p, unsigned short po) : Socket(-1), ProxyConnect(p, po), BufferedSocket() + { + } + + void OnConnect() override + { + this->Write("CONNECT %s:%d HTTP/1.0", target_ip.c_str(), target_port); + this->Write("Content-Length: 0"); + this->Write("Connection: close"); + this->Write(""); + } + + const Anope::string GetType() const override + { + return "HTTP"; + } + + bool ProcessRead() override + { + bool b = BufferedSocket::ProcessRead(); + if (this->GetLine() == ProxyCheckString) + { + this->Ban(); + return false; + } + return b; + } +}; + +class SOCKS5ProxyConnect final + : public ProxyConnect + , public BinarySocket +{ +public: + SOCKS5ProxyConnect(ProxyCheck &p, unsigned short po) : Socket(-1), ProxyConnect(p, po), BinarySocket() + { + } + + void OnConnect() override + { + sockaddrs target_addr; + char buf[4 + sizeof(target_addr.sa4.sin_addr.s_addr) + sizeof(target_addr.sa4.sin_port)]; + int ptr = 0; + target_addr.pton(AF_INET, target_ip, target_port); + if (!target_addr.valid()) + return; + + buf[ptr++] = 5; // Version + buf[ptr++] = 1; // # of methods + buf[ptr++] = 0; // No authentication + + this->Write(buf, ptr); + + ptr = 1; + buf[ptr++] = 1; // Connect + buf[ptr++] = 0; // Reserved + buf[ptr++] = 1; // IPv4 + memcpy(buf + ptr, &target_addr.sa4.sin_addr.s_addr, sizeof(target_addr.sa4.sin_addr.s_addr)); + ptr += sizeof(target_addr.sa4.sin_addr.s_addr); + memcpy(buf + ptr, &target_addr.sa4.sin_port, sizeof(target_addr.sa4.sin_port)); + ptr += sizeof(target_addr.sa4.sin_port); + + this->Write(buf, ptr); + } + + const Anope::string GetType() const override + { + return "SOCKS5"; + } + + bool Read(const char *buffer, size_t l) override + { + if (l >= ProxyCheckString.length() && !strncmp(buffer, ProxyCheckString.c_str(), ProxyCheckString.length())) + { + this->Ban(); + return false; + } + return true; + } +}; + +class ModuleProxyScan final + : public Module +{ + Anope::string listen_ip; + unsigned short listen_port; + Anope::string con_notice, con_source; + std::vector proxyscans; + + ProxyCallbackListener *listener; + + class ConnectionTimeout final + : public Timer + { + public: + ConnectionTimeout(Module *c, long timeout) : Timer(c, timeout, Anope::CurTime, true) + { + } + + void Tick(time_t) override + { + for (std::set::iterator it = ProxyConnect::proxies.begin(), it_end = ProxyConnect::proxies.end(); it != it_end;) + { + ProxyConnect *p = *it; + ++it; + + if (p->created + this->GetSecs() < Anope::CurTime) + delete p; + } + } + } connectionTimeout; + +public: + ModuleProxyScan(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), + connectionTimeout(this, 5) + { + + + this->listener = NULL; + } + + ~ModuleProxyScan() override + { + for (std::set::iterator it = ProxyConnect::proxies.begin(), it_end = ProxyConnect::proxies.end(); it != it_end;) + { + ProxyConnect *p = *it; + ++it; + delete p; + } + + for (std::map::const_iterator it = SocketEngine::Sockets.begin(), it_end = SocketEngine::Sockets.end(); it != it_end;) + { + Socket *s = it->second; + ++it; + + ClientSocket *cs = dynamic_cast(s); + if (cs != NULL && cs->ls == this->listener) + delete s; + } + + delete this->listener; + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *config = Config->GetModule(this); + + Anope::string s_target_ip = config->Get("target_ip"); + if (s_target_ip.empty()) + throw ConfigException(this->name + " target_ip may not be empty"); + + int s_target_port = config->Get("target_port", "-1"); + if (s_target_port <= 0) + throw ConfigException(this->name + " target_port may not be empty and must be a positive number"); + + Anope::string s_listen_ip = config->Get("listen_ip"); + if (s_listen_ip.empty()) + throw ConfigException(this->name + " listen_ip may not be empty"); + + int s_listen_port = config->Get("listen_port", "-1"); + if (s_listen_port <= 0) + throw ConfigException(this->name + " listen_port may not be empty and must be a positive number"); + + target_ip = s_target_ip; + target_port = s_target_port; + this->listen_ip = s_listen_ip; + this->listen_port = s_listen_port; + this->con_notice = config->Get("connect_notice"); + this->con_source = config->Get("connect_source"); + add_to_akill = config->Get("add_to_akill", "true"); + this->connectionTimeout.SetSecs(config->Get("timeout", "5s")); + + ProxyCheckString = Config->GetBlock("networkinfo")->Get("networkname") + " proxy check"; + delete this->listener; + this->listener = NULL; + try + { + this->listener = new ProxyCallbackListener(this->listen_ip, this->listen_port); + } + catch (const SocketException &ex) + { + throw ConfigException("proxyscan: " + ex.GetReason()); + } + + this->proxyscans.clear(); + for (int i = 0; i < config->CountBlock("proxyscan"); ++i) + { + Configuration::Block *block = config->GetBlock("proxyscan", i); + ProxyCheck p; + Anope::string token; + + commasepstream sep(block->Get("type")); + while (sep.GetToken(token)) + { + if (!token.equals_ci("HTTP") && !token.equals_ci("SOCKS5")) + continue; + p.types.insert(token); + } + if (p.types.empty()) + continue; + + commasepstream sep2(block->Get("port")); + while (sep2.GetToken(token)) + { + try + { + unsigned short port = convertTo(token); + p.ports.push_back(port); + } + catch (const ConvertException &) { } + } + if (p.ports.empty()) + continue; + + p.duration = block->Get("time", "4h"); + p.reason = block->Get("reason"); + if (p.reason.empty()) + continue; + + this->proxyscans.push_back(p); + } + } + + void OnUserConnect(User *user, bool &exempt) override + { + if (exempt || user->Quitting() || !Me->IsSynced() || !user->server->IsSynced()) + return; + + /* At this time we only support IPv4 */ + if (!user->ip.valid() || user->ip.sa.sa_family != AF_INET) + /* User doesn't have a valid IPv4 IP (ipv6/spoof/etc) */ + return; + + if (!this->con_notice.empty() && !this->con_source.empty()) + { + BotInfo *bi = BotInfo::Find(this->con_source, true); + if (bi) + user->SendMessage(bi, this->con_notice); + } + + for (unsigned i = this->proxyscans.size(); i > 0; --i) + { + ProxyCheck &p = this->proxyscans[i - 1]; + + for (const auto &type : p.types) + { + for (const auto port : p.ports) + { + try + { + ProxyConnect *con = NULL; + if (type.equals_ci("HTTP")) + con = new HTTPProxyConnect(p, port); + else if (type.equals_ci("SOCKS5")) + con = new SOCKS5ProxyConnect(p, port); + else + continue; + con->Connect(user->ip.addr(), port); + } + catch (const SocketException &ex) + { + Log(LOG_DEBUG) << "proxyscan: " << ex.GetReason(); + } + } + } + } + } +}; + +MODULE_INIT(ModuleProxyScan) diff --git a/modules/redis.cpp b/modules/redis.cpp new file mode 100644 index 000000000..6aa62bb29 --- /dev/null +++ b/modules/redis.cpp @@ -0,0 +1,618 @@ +/* + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/redis.h" + +using namespace Redis; + +class MyRedisService; + +class RedisSocket final + : public BinarySocket + , public ConnectionSocket +{ + size_t ParseReply(Reply &r, const char *buf, size_t l); +public: + MyRedisService *provider; + std::deque interfaces; + std::map subinterfaces; + + RedisSocket(MyRedisService *pro, bool v6) : Socket(-1, v6 ? AF_INET6 : AF_INET), provider(pro) { } + + ~RedisSocket() override; + + void OnConnect() override; + void OnError(const Anope::string &error) override; + + bool Read(const char *buffer, size_t l) override; +}; + +class Transaction final + : public Interface +{ +public: + std::deque interfaces; + + Transaction(Module *creator) : Interface(creator) { } + + ~Transaction() override + { + for (auto *iface : interfaces) + { + if (!iface) + continue; + + iface->OnError("Interface going away"); + } + } + + void OnResult(const Reply &r) override + { + /* This is a multi bulk reply of the results of the queued commands + * in this transaction + */ + + Log(LOG_DEBUG_2) << "redis: transaction complete with " << r.multi_bulk.size() << " results"; + + for (auto *result : r.multi_bulk) + { + if (interfaces.empty()) + break; + + Interface *inter = interfaces.front(); + interfaces.pop_front(); + + if (inter) + inter->OnResult(*result); + } + } +}; + +class MyRedisService final + : public Provider +{ +public: + Anope::string host; + int port; + unsigned db; + + RedisSocket *sock = nullptr, *sub = nullptr; + + Transaction ti; + bool in_transaction = false; + + MyRedisService(Module *c, const Anope::string &n, const Anope::string &h, int p, unsigned d) : Provider(c, n), host(h), port(p), db(d), ti(c) + { + sock = new RedisSocket(this, host.find(':') != Anope::string::npos); + sock->Connect(host, port); + + sub = new RedisSocket(this, host.find(':') != Anope::string::npos); + sub->Connect(host, port); + } + + ~MyRedisService() override + { + if (sock) + { + sock->flags[SF_DEAD] = true; + sock->provider = NULL; + } + + if (sub) + { + sub->flags[SF_DEAD] = true; + sub->provider = NULL; + } + } + +private: + inline void Pack(std::vector &buffer, const char *buf, size_t sz = 0) + { + if (!sz) + sz = strlen(buf); + + size_t old_size = buffer.size(); + buffer.resize(old_size + sz); + std::copy(buf, buf + sz, buffer.begin() + old_size); + } + + void Send(RedisSocket *s, Interface *i, const std::vector > &args) + { + std::vector buffer; + + Pack(buffer, "*"); + Pack(buffer, stringify(args.size()).c_str()); + Pack(buffer, "\r\n"); + + for (const auto &[key, value] : args) + { + Pack(buffer, "$"); + Pack(buffer, stringify(value).c_str()); + Pack(buffer, "\r\n"); + + Pack(buffer, key, value); + Pack(buffer, "\r\n"); + } + + if (buffer.empty()) + return; + + s->Write(&buffer[0], buffer.size()); + if (in_transaction) + { + ti.interfaces.push_back(i); + s->interfaces.push_back(NULL); // For the +Queued response + } + else + s->interfaces.push_back(i); + } + +public: + bool IsSocketDead() override + { + return this->sock && this->sock->flags[SF_DEAD]; + } + + void SendCommand(RedisSocket *s, Interface *i, const std::vector &cmds) + { + std::vector > args; + for (const auto &cmd : cmds) + args.emplace_back(cmd.c_str(), cmd.length()); + this->Send(s, i, args); + } + + void SendCommand(RedisSocket *s, Interface *i, const Anope::string &str) + { + std::vector args; + spacesepstream(str).GetTokens(args); + this->SendCommand(s, i, args); + } + + void Send(Interface *i, const std::vector > &args) + { + if (!sock) + { + sock = new RedisSocket(this, host.find(':') != Anope::string::npos); + sock->Connect(host, port); + } + + this->Send(sock, i, args); + } + + void SendCommand(Interface *i, const std::vector &cmds) override + { + std::vector > args; + for (const auto &cmd : cmds) + args.emplace_back(cmd.c_str(), cmd.length()); + this->Send(i, args); + } + + void SendCommand(Interface *i, const Anope::string &str) override + { + std::vector args; + spacesepstream(str).GetTokens(args); + this->SendCommand(i, args); + } + +public: + bool BlockAndProcess() override + { + if (!this->sock->ProcessWrite()) + this->sock->flags[SF_DEAD] = true; + this->sock->SetBlocking(true); + if (!this->sock->ProcessRead()) + this->sock->flags[SF_DEAD] = true; + this->sock->SetBlocking(false); + return !this->sock->interfaces.empty(); + } + + void Subscribe(Interface *i, const Anope::string &pattern) override + { + if (sub == NULL) + { + sub = new RedisSocket(this, host.find(':') != Anope::string::npos); + sub->Connect(host, port); + } + + std::vector args; + args.emplace_back("PSUBSCRIBE"); + args.push_back(pattern); + this->SendCommand(sub, NULL, args); + + sub->subinterfaces[pattern] = i; + } + + void Unsubscribe(const Anope::string &pattern) override + { + if (sub) + sub->subinterfaces.erase(pattern); + } + + void StartTransaction() override + { + if (in_transaction) + throw CoreException(); + + this->SendCommand(NULL, "MULTI"); + in_transaction = true; + } + + void CommitTransaction() override + { + /* The result of the transaction comes back to the reply of EXEC as a multi bulk. + * The reply to the individual commands that make up the transaction when executed + * is a simple +QUEUED + */ + in_transaction = false; + this->SendCommand(&this->ti, "EXEC"); + } +}; + +RedisSocket::~RedisSocket() +{ + if (provider) + { + if (provider->sock == this) + provider->sock = NULL; + else if (provider->sub == this) + provider->sub = NULL; + } + + for (auto *iface : interfaces) + { + if (!iface) + continue; + + iface->OnError("Interface going away"); + } +} + +void RedisSocket::OnConnect() +{ + Log() << "redis: Successfully connected to " << provider->name << (this == this->provider->sub ? " (sub)" : ""); + + this->provider->SendCommand(NULL, "CLIENT SETNAME Anope"); + this->provider->SendCommand(NULL, "SELECT " + stringify(provider->db)); + + if (this != this->provider->sub) + { + this->provider->SendCommand(this, NULL, "CONFIG SET notify-keyspace-events KA"); + } +} + +void RedisSocket::OnError(const Anope::string &error) +{ + Log() << "redis: Error on " << provider->name << (this == this->provider->sub ? " (sub)" : "") << ": " << error; +} + +size_t RedisSocket::ParseReply(Reply &r, const char *buffer, size_t l) +{ + size_t used = 0; + + if (!l) + return used; + + if (r.type == Reply::MULTI_BULK) + goto multi_bulk_cont; + + switch (*buffer) + { + case '+': + { + Anope::string reason(buffer, 1, l - 1); + size_t nl = reason.find("\r\n"); + Log(LOG_DEBUG_2) << "redis: status ok: " << reason.substr(0, nl); + if (nl != Anope::string::npos) + { + r.type = Reply::OK; + used = 1 + nl + 2; + } + break; + } + case '-': + { + Anope::string reason(buffer, 1, l - 1); + size_t nl = reason.find("\r\n"); + Log(LOG_DEBUG) << "redis: status error: " << reason.substr(0, nl); + if (nl != Anope::string::npos) + { + r.type = Reply::NOT_OK; + used = 1 + nl + 2; + } + break; + } + case ':': + { + Anope::string ibuf(buffer, 1, l - 1); + size_t nl = ibuf.find("\r\n"); + if (nl != Anope::string::npos) + { + try + { + r.i = convertTo(ibuf.substr(0, nl)); + } + catch (const ConvertException &) { } + + r.type = Reply::INT; + used = 1 + nl + 2; + } + break; + } + case '$': + { + Anope::string reply(buffer + 1, l - 1); + /* This assumes one bulk can always fit in our recv buffer */ + size_t nl = reply.find("\r\n"); + if (nl != Anope::string::npos) + { + int len; + try + { + len = convertTo(reply.substr(0, nl)); + if (len >= 0) + { + if (1 + nl + 2 + len + 2 <= l) + { + used = 1 + nl + 2 + len + 2; + r.bulk = reply.substr(nl + 2, len); + r.type = Reply::BULK; + } + } + else + { + used = 1 + nl + 2 + 2; + r.type = Reply::BULK; + } + } + catch (const ConvertException &) { } + } + break; + } + multi_bulk_cont: + case '*': + { + if (r.type != Reply::MULTI_BULK) + { + Anope::string reply(buffer + 1, l - 1); + size_t nl = reply.find("\r\n"); + if (nl != Anope::string::npos) + { + r.type = Reply::MULTI_BULK; + try + { + r.multi_bulk_size = convertTo(reply.substr(0, nl)); + } + catch (const ConvertException &) { } + + used = 1 + nl + 2; + } + else + break; + } + else if (r.multi_bulk_size >= 0 && r.multi_bulk.size() == static_cast(r.multi_bulk_size)) + { + /* This multi bulk is already complete, so check the sub bulks */ + for (auto &bulk : r.multi_bulk) + if (bulk->type == Reply::MULTI_BULK) + ParseReply(*bulk, buffer + used, l - used); + break; + } + + for (int i = r.multi_bulk.size(); i < r.multi_bulk_size; ++i) + { + auto *reply = new Reply(); + size_t u = ParseReply(*reply, buffer + used, l - used); + if (!u) + { + Log(LOG_DEBUG) << "redis: ran out of data to parse"; + delete reply; + break; + } + r.multi_bulk.push_back(reply); + used += u; + } + break; + } + default: + Log(LOG_DEBUG) << "redis: unknown reply " << *buffer; + } + + return used; +} + +bool RedisSocket::Read(const char *buffer, size_t l) +{ + static std::vector save; + std::vector copy; + + if (!save.empty()) + { + std::copy(buffer, buffer + l, std::back_inserter(save)); + + copy = save; + + buffer = ©[0]; + l = copy.size(); + } + + while (l) + { + static Reply r; + + size_t used = this->ParseReply(r, buffer, l); + if (!used) + { + Log(LOG_DEBUG) << "redis: used == 0 ?"; + r.Clear(); + break; + } + else if (used > l) + { + Log(LOG_DEBUG) << "redis: used > l ?"; + r.Clear(); + break; + } + + /* Full result is not here yet */ + if (r.type == Reply::MULTI_BULK && static_cast(r.multi_bulk_size) != r.multi_bulk.size()) + { + buffer += used; + l -= used; + break; + } + + if (this == provider->sub) + { + if (r.multi_bulk.size() == 4) + { + /* pmessage + * pattern subscribed to + * __keyevent@0__:set + * key + */ + std::map::iterator it = this->subinterfaces.find(r.multi_bulk[1]->bulk); + if (it != this->subinterfaces.end()) + it->second->OnResult(r); + } + } + else + { + if (this->interfaces.empty()) + { + Log(LOG_DEBUG) << "redis: no interfaces?"; + } + else + { + Interface *i = this->interfaces.front(); + this->interfaces.pop_front(); + + if (i) + { + if (r.type != Reply::NOT_OK) + i->OnResult(r); + else + i->OnError(r.bulk); + } + } + } + + buffer += used; + l -= used; + + r.Clear(); + } + + if (l) + { + save.resize(l); + std::copy(buffer, buffer + l, save.begin()); + } + else + std::vector().swap(save); + + return true; +} + + +class ModuleRedis final + : public Module +{ + std::map services; + +public: + ModuleRedis(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) + { + } + + ~ModuleRedis() override + { + for (auto &[_, p] : services) + { + delete p->sock; + p->sock = NULL; + + delete p->sub; + p->sub = NULL; + + delete p; + } + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *block = conf->GetModule(this); + std::vector new_services; + + for (int i = 0; i < block->CountBlock("redis"); ++i) + { + Configuration::Block *redis = block->GetBlock("redis", i); + + const Anope::string &n = redis->Get("name"), + &ip = redis->Get("ip"); + int port = redis->Get("port"); + unsigned db = redis->Get("db"); + + delete services[n]; + services[n] = new MyRedisService(this, n, ip, port, db); + new_services.push_back(n); + } + + for (std::map::iterator it = services.begin(); it != services.end();) + { + Provider *p = it->second; + ++it; + + if (std::find(new_services.begin(), new_services.end(), p->name) == new_services.end()) + delete it->second; + } + } + + void OnModuleUnload(User *, Module *m) override + { + for (auto &[_, p] : services) + { + if (p->sock) + for (unsigned i = p->sock->interfaces.size(); i > 0; --i) + { + Interface *inter = p->sock->interfaces[i - 1]; + + if (inter && inter->owner == m) + { + inter->OnError(m->name + " being unloaded"); + p->sock->interfaces.erase(p->sock->interfaces.begin() + i - 1); + } + } + + if (p->sub) + for (unsigned i = p->sub->interfaces.size(); i > 0; --i) + { + Interface *inter = p->sub->interfaces[i - 1]; + + if (inter && inter->owner == m) + { + inter->OnError(m->name + " being unloaded"); + p->sub->interfaces.erase(p->sub->interfaces.begin() + i - 1); + } + } + + for (unsigned i = p->ti.interfaces.size(); i > 0; --i) + { + Interface *inter = p->ti.interfaces[i - 1]; + + if (inter && inter->owner == m) + { + inter->OnError(m->name + " being unloaded"); + p->ti.interfaces.erase(p->ti.interfaces.begin() + i - 1); + } + } + } + } +}; + +MODULE_INIT(ModuleRedis) diff --git a/modules/regex_stdlib.cpp b/modules/regex_stdlib.cpp new file mode 100644 index 000000000..d759cc8fd --- /dev/null +++ b/modules/regex_stdlib.cpp @@ -0,0 +1,107 @@ +/* + * + * (C) 2012-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" + +#include + +class StdLibRegex final + : public Regex +{ +private: + std::regex regex; + +public: + StdLibRegex(const Anope::string &expr, std::regex::flag_type type) + : Regex(expr) + { + try + { + regex.assign(expr.str(), type | std::regex::optimize); + } + catch (const std::regex_error& error) + { + throw RegexException("Error in regex " + expr + ": " + error.what()); + } + } + + bool Matches(const Anope::string &str) + { + return std::regex_search(str.str(), regex); + } +}; + +class StdLibRegexProvider final + : public RegexProvider +{ +public: + std::regex_constants::syntax_option_type type; + + StdLibRegexProvider(Module *creator) + : RegexProvider(creator, "regex/stdlib") + { + } + + Regex *Compile(const Anope::string &expression) override + { + return new StdLibRegex(expression, type); + } +}; + +class ModuleRegexStdLib final + : public Module +{ +private: + StdLibRegexProvider stdlib_regex_provider; + +public: + ModuleRegexStdLib(const Anope::string &modname, const Anope::string &creator) + : Module(modname, creator, EXTRA | VENDOR) + , stdlib_regex_provider(this) + { + this->SetPermanent(true); + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *block = conf->GetModule(this); + + const Anope::string syntax = block->Get("syntax", "ecmascript"); + if (syntax == "awk") + stdlib_regex_provider.type = std::regex::awk; + else if (syntax == "basic") + stdlib_regex_provider.type = std::regex::basic; + else if (syntax == "ecmascript") + stdlib_regex_provider.type = std::regex::ECMAScript; + else if (syntax == "egrep") + stdlib_regex_provider.type = std::regex::egrep; + else if (syntax == "extended") + stdlib_regex_provider.type = std::regex::extended; + else if (syntax == "grep") + stdlib_regex_provider.type = std::regex::grep; + else + throw ConfigException(this->name + ": syntax must be set to awk, basic, ecmascript, egrep, extended, or grep."); + } + + ~ModuleRegexStdLib() + { + 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(ModuleRegexStdLib) diff --git a/modules/rewrite.cpp b/modules/rewrite.cpp new file mode 100644 index 000000000..3911b5ddf --- /dev/null +++ b/modules/rewrite.cpp @@ -0,0 +1,185 @@ +/* + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" + +struct Rewrite final +{ + Anope::string client, source_message, target_message, desc; + + bool Matches(const std::vector &message) + { + std::vector sm; + spacesepstream(this->source_message).GetTokens(sm); + + for (unsigned i = 0; i < sm.size(); ++i) + if (i >= message.size() || (sm[i] != "$" && !sm[i].equals_ci(message[i]))) + return false; + + return true; + } + + Anope::string Process(CommandSource &source, const std::vector ¶ms) + { + spacesepstream sep(this->target_message); + Anope::string token, message; + + while (sep.GetToken(token)) + { + if (token[0] != '$') + message += " " + token; + else if (token == "$me") + message += " " + source.GetNick(); + else + { + int num = -1, end = -1; + try + { + Anope::string num_str = token.substr(1); + size_t hy = num_str.find('-'); + if (hy == Anope::string::npos) + { + num = convertTo(num_str); + end = num + 1; + } + else + { + num = convertTo(num_str.substr(0, hy)); + if (hy == num_str.length() - 1) + end = params.size(); + else + end = convertTo(num_str.substr(hy + 1)) + 1; + } + } + catch (const ConvertException &) + { + continue; + } + + for (int i = num; i < end && static_cast(i) < params.size(); ++i) + message += " " + params[i]; + } + } + + message.trim(); + return message; + } + + static std::vector rewrites; + + static Rewrite *Find(const Anope::string &client, const Anope::string &cmd) + { + for (auto &r : rewrites) + { + if ((client.empty() || r.client.equals_ci(client)) && (r.source_message.equals_ci(cmd) || r.source_message.find_ci(cmd + " ") == 0)) + return &r; + } + + return NULL; + } + + static Rewrite *Match(const Anope::string &client, const std::vector ¶ms) + { + for (auto &r : rewrites) + { + if ((client.empty() || r.client.equals_ci(client)) && r.Matches(params)) + return &r; + } + + return NULL; + } +}; + +std::vector Rewrite::rewrites; + +class RewriteCommand final + : public Command +{ +public: + RewriteCommand(Module *creator) : Command(creator, "rewrite", 0, 0) { } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + std::vector full_params = params; + full_params.insert(full_params.begin(), source.command); + + Rewrite *r = Rewrite::Match(!source.c ? source.service->nick : "", full_params); + if (r != NULL) + { + Anope::string new_message = r->Process(source, full_params); + Log(LOG_DEBUG) << "rewrite: Rewrote '" << source.command << (!params.empty() ? " " + params[0] : "") << "' to '" << new_message << "' using '" << r->source_message << "'"; + source.service = BotInfo::Find(r->client, true); + if (!source.service) + return; + Command::Run(source, new_message); + } + else + Log() << "rewrite: Unable to rewrite '" << source.command << (!params.empty() ? " " + params[0] : "") << "'"; + } + + void OnServHelp(CommandSource &source) override + { + Rewrite *r = Rewrite::Find(!source.c ? source.service->nick : "", source.command); + if (r != NULL && !r->desc.empty()) + { + this->SetDesc(r->desc); + Command::OnServHelp(source); + } + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + Rewrite *r = Rewrite::Find(!source.c ? source.service->nick : "", source.command); + if (r != NULL && !r->desc.empty()) + { + source.Reply(r->desc); + size_t sz = r->target_message.find(' '); + source.Reply(_("This command is an alias to the command %s."), sz != Anope::string::npos ? r->target_message.substr(0, sz).c_str() : r->target_message.c_str()); + return true; + } + return false; + } +}; + +class ModuleRewrite final + : public Module +{ + RewriteCommand cmdrewrite; + +public: + ModuleRewrite(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR | EXTRA), cmdrewrite(this) + { + } + + void OnReload(Configuration::Conf *conf) override + { + Rewrite::rewrites.clear(); + + for (int i = 0; i < conf->CountBlock("command"); ++i) + { + Configuration::Block *block = conf->GetBlock("command", i); + + if (!block->Get("rewrite")) + continue; + + Rewrite rw; + + rw.client = block->Get("service"); + rw.source_message = block->Get("rewrite_source"); + rw.target_message = block->Get("rewrite_target"); + rw.desc = block->Get("rewrite_description"); + + if (rw.client.empty() || rw.source_message.empty() || rw.target_message.empty()) + continue; + + Rewrite::rewrites.push_back(rw); + } + } +}; + +MODULE_INIT(ModuleRewrite) diff --git a/modules/sasl.cpp b/modules/sasl.cpp new file mode 100644 index 000000000..298ad0e75 --- /dev/null +++ b/modules/sasl.cpp @@ -0,0 +1,414 @@ +/* + * + * (C) 2014-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/sasl.h" +#include "modules/ns_cert.h" + +using namespace SASL; + +class Plain final + : public Mechanism +{ +public: + Plain(Module *o) : Mechanism(o, "PLAIN") { } + + void ProcessMessage(Session *sess, const SASL::Message &m) override + { + if (m.type == "S") + { + sasl->SendMessage(sess, "C", "+"); + } + else if (m.type == "C") + { + // message = [authzid] UTF8NUL authcid UTF8NUL passwd + Anope::string message; + Anope::B64Decode(m.data, message); + + size_t zcsep = message.find('\0'); + if (zcsep == Anope::string::npos) + { + sasl->Fail(sess); + delete sess; + return; + } + + size_t cpsep = message.find('\0', zcsep + 1); + if (cpsep == Anope::string::npos) + { + sasl->Fail(sess); + delete sess; + return; + } + + Anope::string authzid = message.substr(0, zcsep); + Anope::string authcid = message.substr(zcsep + 1, cpsep - zcsep - 1); + + // We don't support having an authcid that is different to the authzid. + if (!authzid.empty() && authzid != authcid) + { + sasl->Fail(sess); + delete sess; + return; + } + + Anope::string passwd = message.substr(cpsep + 1); + + if (authcid.empty() || passwd.empty() || !IRCD->IsNickValid(authcid) || passwd.find_first_of("\r\n\0") != Anope::string::npos) + { + sasl->Fail(sess); + delete sess; + return; + } + + SASL::IdentifyRequest *req = new SASL::IdentifyRequest(this->owner, m.source, authcid, passwd, sess->hostname, sess->ip); + FOREACH_MOD(OnCheckAuthentication, (NULL, req)); + req->Dispatch(); + } + } +}; + +class External final + : public Mechanism +{ + ServiceReference certs; + + struct Session final + : SASL::Session + { + Anope::string cert; + + Session(Mechanism *m, const Anope::string &u) : SASL::Session(m, u) { } + }; + +public: + External(Module *o) : Mechanism(o, "EXTERNAL"), certs("CertService", "certs") + { + if (!IRCD || !IRCD->CanCertFP) + throw ModuleException("No CertFP"); + } + + Session* CreateSession(const Anope::string &uid) override + { + return new Session(this, uid); + } + + void ProcessMessage(SASL::Session *sess, const SASL::Message &m) override + { + Session *mysess = anope_dynamic_static_cast(sess); + + if (m.type == "S") + { + mysess->cert = m.ext; + + sasl->SendMessage(sess, "C", "+"); + } + else if (m.type == "C") + { + if (!certs || mysess->cert.empty()) + { + sasl->Fail(sess); + delete sess; + return; + } + + Anope::string user = "A user"; + if (!mysess->hostname.empty() && !mysess->ip.empty()) + user = mysess->hostname + " (" + mysess->ip + ")"; + + NickCore *nc = certs->FindAccountFromCert(mysess->cert); + if (!nc || nc->HasExt("NS_SUSPENDED") || nc->HasExt("UNCONFIRMED")) + { + Log(this->owner, "sasl", Config->GetClient("NickServ")) << user << " failed to identify using certificate " << mysess->cert << " using SASL EXTERNAL"; + sasl->Fail(sess); + delete sess; + return; + } + + Log(this->owner, "sasl", Config->GetClient("NickServ")) << user << " identified to account " << nc->display << " using SASL EXTERNAL"; + sasl->Succeed(sess, nc); + delete sess; + } + } +}; + +class Anonymous final + : public Mechanism +{ +public: + Anonymous(Module *o) : Mechanism(o, "ANONYMOUS") { } + + void ProcessMessage(Session *sess, const SASL::Message &m) override + { + if (!IRCD->CanSVSLogout && !User::Find(sess->uid)) + { + // This IRCd can't log users out yet. + sasl->Fail(sess); + delete sess; + return; + } + + if (m.type == "S") + { + sasl->SendMessage(sess, "C", "+"); + } + else if (m.type == "C") + { + Anope::string decoded; + Anope::B64Decode(m.data, decoded); + + Anope::string user = "A user"; + if (!sess->hostname.empty() && !sess->ip.empty()) + user = sess->hostname + " (" + sess->ip + ")"; + if (!decoded.empty()) + user += " [" + decoded + "]"; + + Log(this->owner, "sasl", Config->GetClient("NickServ")) << user << " unidentified using SASL ANONYMOUS"; + sasl->Succeed(sess, nullptr); + } + } +}; + +class SASLService final + : public SASL::Service + , public Timer +{ + std::map sessions; + +public: + SASLService(Module *o) : SASL::Service(o), Timer(o, 60, Anope::CurTime, true) { } + + ~SASLService() override + { + for (const auto &[_, session] : sessions) + delete session; + } + + void ProcessMessage(const SASL::Message &m) override + { + if (m.target != "*") + { + Server *s = Server::Find(m.target); + if (s != Me) + { + User *u = User::Find(m.target); + if (!u || u->server != Me) + return; + } + } + + Session* session = GetSession(m.source); + + if (m.type == "S") + { + ServiceReference mech("SASL::Mechanism", m.data); + if (!mech) + { + Session tmp(NULL, m.source); + + sasl->SendMechs(&tmp); + sasl->Fail(&tmp); + return; + } + + Anope::string hostname, ip; + if (session) + { + // Copy over host/ip to mech-specific session + hostname = session->hostname; + ip = session->ip; + delete session; + } + + session = mech->CreateSession(m.source); + if (session) + { + session->hostname = hostname; + session->ip = ip; + + sessions[m.source] = session; + } + } + else if (m.type == "D") + { + delete session; + return; + } + else if (m.type == "H") + { + if (!session) + { + session = new Session(NULL, m.source); + sessions[m.source] = session; + } + session->hostname = m.data; + session->ip = m.ext; + } + + if (session && session->mech) + session->mech->ProcessMessage(session, m); + } + + Anope::string GetAgent() override + { + Anope::string agent = Config->GetModule(Service::owner)->Get("agent", "NickServ"); + BotInfo *bi = Config->GetClient(agent); + if (bi) + agent = bi->GetUID(); + return agent; + } + + Session* GetSession(const Anope::string &uid) override + { + std::map::iterator it = sessions.find(uid); + if (it != sessions.end()) + return it->second; + return NULL; + } + + void RemoveSession(Session *sess) override + { + sessions.erase(sess->uid); + } + + void DeleteSessions(Mechanism *mech, bool da) override + { + for (std::map::iterator it = sessions.begin(); it != sessions.end();) + { + std::map::iterator del = it++; + if (*del->second->mech == mech) + { + if (da) + this->SendMessage(del->second, "D", "A"); + delete del->second; + } + } + } + + void SendMessage(Session *session, const Anope::string &mtype, const Anope::string &data) override + { + SASL::Message msg; + msg.source = this->GetAgent(); + msg.target = session->uid; + msg.type = mtype; + msg.data = data; + + IRCD->SendSASLMessage(msg); + } + + void Succeed(Session *session, NickCore *nc) override + { + // If the user is already introduced then we log them in now. + // Otherwise, we send an SVSLOGIN to log them in later. + User *user = User::Find(session->uid); + NickAlias *na = NickAlias::Find(nc->display); + if (user) + { + if (na) + user->Identify(na); + else + user->Logout(); + } + else + { + IRCD->SendSVSLogin(session->uid, na); + } + this->SendMessage(session, "D", "S"); + } + + void Fail(Session *session) override + { + this->SendMessage(session, "D", "F"); + } + + void SendMechs(Session *session) override + { + std::vector mechs = Service::GetServiceKeys("SASL::Mechanism"); + Anope::string buf; + for (const auto &mech : mechs) + buf += "," + mech; + + this->SendMessage(session, "M", buf.empty() ? "" : buf.substr(1)); + } + + void Tick(time_t) override + { + for (std::map::iterator it = sessions.begin(); it != sessions.end();) + { + Anope::string key = it->first; + Session *s = it->second; + ++it; + + if (!s || s->created + 60 < Anope::CurTime) + { + delete s; + sessions.erase(key); + } + } + } +}; + +class ModuleSASL final + : public Module +{ + SASLService sasl; + + Anonymous anonymous; + Plain plain; + External *external = nullptr; + + std::vector mechs; + + void CheckMechs() + { + std::vector newmechs = ::Service::GetServiceKeys("SASL::Mechanism"); + if (newmechs == mechs) + return; + + mechs = newmechs; + + // If we are connected to the network then broadcast the mechlist. + if (Me && Me->IsSynced()) + IRCD->SendSASLMechanisms(mechs); + } + +public: + ModuleSASL(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + sasl(this), anonymous(this), plain(this) + { + try + { + external = new External(this); + CheckMechs(); + } + catch (ModuleException &) { } + } + + ~ModuleSASL() override + { + delete external; + } + + void OnModuleLoad(User *, Module *) override + { + CheckMechs(); + } + + void OnModuleUnload(User *, Module *) override + { + CheckMechs(); + } + + void OnPreUplinkSync(Server *) override + { + // We have not yet sent a mechanism list so always do it here. + IRCD->SendSASLMechanisms(mechs); + } +}; + +MODULE_INIT(ModuleSASL) diff --git a/modules/stats/chanstats.cpp b/modules/stats/chanstats.cpp new file mode 100644 index 000000000..db92689d7 --- /dev/null +++ b/modules/stats/chanstats.cpp @@ -0,0 +1,660 @@ +/* + * + * (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" + +class CommandCSSetChanstats final + : public Command +{ +public: + CommandCSSetChanstats(Module *creator) : Command(creator, "chanserv/set/chanstats", 2, 2) + { + this->SetDesc(_("Turn chanstats statistics on or off")); + this->SetSyntax(_("\037channel\037 {ON | OFF}")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + ChannelInfo *ci = ChannelInfo::Find(params[0]); + if (!ci) + { + source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); + return; + } + + EventReturn MOD_RESULT; + FOREACH_RESULT(OnSetChannelOption, MOD_RESULT, (source, this, ci, params[1])); + if (MOD_RESULT == EVENT_STOP) + return; + + if (MOD_RESULT != EVENT_ALLOW && !source.AccessFor(ci).HasPriv("SET") && source.permission.empty() && !source.HasPriv("chanserv/administration")) + { + source.Reply(ACCESS_DENIED); + return; + } + + if (params[1].equals_ci("ON")) + { + ci->Extend("CS_STATS"); + source.Reply(_("Chanstats statistics are now enabled for this channel.")); + Log(source.AccessFor(ci).HasPriv("SET") ? LOG_COMMAND : LOG_OVERRIDE, source, this, ci) << "to enable chanstats"; + } + else if (params[1].equals_ci("OFF")) + { + Log(source.AccessFor(ci).HasPriv("SET") ? LOG_COMMAND : LOG_OVERRIDE, source, this, ci) << "to disable chanstats"; + ci->Shrink("CS_STATS"); + source.Reply(_("Chanstats statistics are now disabled for this channel.")); + } + else + this->OnSyntaxError(source, ""); + } + + bool OnHelp(CommandSource &source, const Anope::string &) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Turns chanstats statistics ON or OFF.")); + return true; + } +}; + +class CommandNSSetChanstats + : public Command +{ +public: + CommandNSSetChanstats(Module *creator, const Anope::string &sname = "nickserv/set/chanstats", size_t min = 1 ) : Command(creator, sname, min, min + 1) + { + this->SetDesc(_("Turn chanstats statistics on or off")); + this->SetSyntax("{ON | OFF}"); + } + void Run(CommandSource &source, const Anope::string &user, const Anope::string ¶m, bool saset = false) + { + NickAlias *na = NickAlias::Find(user); + if (!na) + { + source.Reply(NICK_X_NOT_REGISTERED, user.c_str()); + return; + } + + EventReturn MOD_RESULT; + FOREACH_RESULT(OnSetNickOption, MOD_RESULT, (source, this, na->nc, param)); + if (MOD_RESULT == EVENT_STOP) + return; + + if (param.equals_ci("ON")) + { + Log(na->nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to enable chanstats for " << na->nc->display; + na->nc->Extend("NS_STATS"); + if (saset) + source.Reply(_("Chanstats statistics are now enabled for %s"), na->nc->display.c_str()); + else + source.Reply(_("Chanstats statistics are now enabled for your nick.")); + } + else if (param.equals_ci("OFF")) + { + Log(na->nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to disable chanstats for " << na->nc->display; + na->nc->Shrink("NS_STATS"); + if (saset) + source.Reply(_("Chanstats statistics are now disabled for %s"), na->nc->display.c_str()); + else + source.Reply(_("Chanstats statistics are now disabled for your nick.")); + } + else + this->OnSyntaxError(source, "CHANSTATS"); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + this->Run(source, source.nc->display, params[0]); + } + + bool OnHelp(CommandSource &source, const Anope::string &) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Turns chanstats statistics ON or OFF.")); + return true; + } +}; + +class CommandNSSASetChanstats final + : public CommandNSSetChanstats +{ +public: + CommandNSSASetChanstats(Module *creator) : CommandNSSetChanstats(creator, "nickserv/saset/chanstats", 2) + { + this->ClearSyntax(); + this->SetSyntax(_("\037nickname\037 {ON | OFF}")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + this->Run(source, params[0], params[1], true); + } + + bool OnHelp(CommandSource &source, const Anope::string &) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Turns chanstats channel statistics ON or OFF for this user.")); + return true; + } +}; + +class MySQLInterface final + : public SQL::Interface +{ +public: + MySQLInterface(Module *o) : SQL::Interface(o) { } + + void OnResult(const SQL::Result &r) override + { + } + + void OnError(const SQL::Result &r) override + { + if (!r.GetQuery().query.empty()) + Log(LOG_DEBUG) << "Chanstats: Error executing query " << r.finished_query << ": " << r.GetError(); + else + Log(LOG_DEBUG) << "Chanstats: Error executing query: " << r.GetError(); + } +}; + +class MChanstats final + : public Module +{ + SerializableExtensibleItem cs_stats, ns_stats; + + CommandCSSetChanstats commandcssetchanstats; + + CommandNSSetChanstats commandnssetchanstats; + CommandNSSASetChanstats commandnssasetchanstats; + + ServiceReference sql; + MySQLInterface sqlinterface; + SQL::Query query; + Anope::string SmileysHappy, SmileysSad, SmileysOther, prefix; + std::vector TableList, ProcedureList, EventList; + bool NSDefChanstats, CSDefChanstats; + + void RunQuery(const SQL::Query &q) + { + if (sql) + sql->Run(&sqlinterface, q); + } + + size_t CountWords(const Anope::string &msg) + { + size_t words = 0; + for (size_t pos = 0; pos != Anope::string::npos; pos = msg.find(" ", pos+1)) + words++; + return words; + } + size_t CountSmileys(const Anope::string &msg, const Anope::string &smileylist) + { + size_t smileys = 0; + spacesepstream sep(smileylist); + Anope::string buf; + + while (sep.GetToken(buf) && !buf.empty()) + { + for (size_t pos = msg.find(buf, 0); pos != Anope::string::npos; pos = msg.find(buf, pos+1)) + smileys++; + } + return smileys; + } + + const Anope::string GetDisplay(User *u) + { + if (u && u->Account() && ns_stats.HasExt(u->Account())) + return u->Account()->display; + else + return ""; + } + + void GetTables() + { + TableList.clear(); + ProcedureList.clear(); + EventList.clear(); + if (!sql) + return; + + SQL::Result r = this->sql->RunQuery(this->sql->GetTables(prefix)); + for (int i = 0; i < r.Rows(); ++i) + { + const std::map &map = r.Row(i); + for (std::map::const_iterator it = map.begin(); it != map.end(); ++it) + TableList.push_back(it->second); + } + query = "SHOW PROCEDURE STATUS WHERE `Db` = Database();"; + r = this->sql->RunQuery(query); + for (int i = 0; i < r.Rows(); ++i) + { + ProcedureList.push_back(r.Get(i, "Name")); + } + query = "SHOW EVENTS WHERE `Db` = Database();"; + r = this->sql->RunQuery(query); + for (int i = 0; i < r.Rows(); ++i) + { + EventList.push_back(r.Get(i, "Name")); + } + } + + bool HasTable(const Anope::string &table) + { + for (std::vector::const_iterator it = TableList.begin(); it != TableList.end(); ++it) + if (*it == table) + return true; + return false; + } + + bool HasProcedure(const Anope::string &table) + { + for (std::vector::const_iterator it = ProcedureList.begin(); it != ProcedureList.end(); ++it) + if (*it == table) + return true; + return false; + } + + bool HasEvent(const Anope::string &table) + { + for (std::vector::const_iterator it = EventList.begin(); it != EventList.end(); ++it) + if (*it == table) + return true; + return false; + } + + + void CheckTables() + { + this->GetTables(); + if (!this->HasTable(prefix +"chanstats")) + { + query = "CREATE TABLE `" + prefix + "chanstats` (" + "`id` int(11) NOT NULL AUTO_INCREMENT," + "`chan` varchar(64) NOT NULL DEFAULT ''," + "`nick` varchar(64) NOT NULL DEFAULT ''," + "`type` ENUM('total', 'monthly', 'weekly', 'daily') NOT NULL," + "`letters` int(10) unsigned NOT NULL DEFAULT '0'," + "`words` int(10) unsigned NOT NULL DEFAULT '0'," + "`line` int(10) unsigned NOT NULL DEFAULT '0'," + "`actions` int(10) unsigned NOT NULL DEFAULT '0'," + "`smileys_happy` int(10) unsigned NOT NULL DEFAULT '0'," + "`smileys_sad` int(10) unsigned NOT NULL DEFAULT '0'," + "`smileys_other` int(10) unsigned NOT NULL DEFAULT '0'," + "`kicks` int(10) unsigned NOT NULL DEFAULT '0'," + "`kicked` int(10) unsigned NOT NULL DEFAULT '0'," + "`modes` int(10) unsigned NOT NULL DEFAULT '0'," + "`topics` int(10) unsigned NOT NULL DEFAULT '0'," + "`time0` int(10) unsigned NOT NULL default '0'," + "`time1` int(10) unsigned NOT NULL default '0'," + "`time2` int(10) unsigned NOT NULL default '0'," + "`time3` int(10) unsigned NOT NULL default '0'," + "`time4` int(10) unsigned NOT NULL default '0'," + "`time5` int(10) unsigned NOT NULL default '0'," + "`time6` int(10) unsigned NOT NULL default '0'," + "`time7` int(10) unsigned NOT NULL default '0'," + "`time8` int(10) unsigned NOT NULL default '0'," + "`time9` int(10) unsigned NOT NULL default '0'," + "`time10` int(10) unsigned NOT NULL default '0'," + "`time11` int(10) unsigned NOT NULL default '0'," + "`time12` int(10) unsigned NOT NULL default '0'," + "`time13` int(10) unsigned NOT NULL default '0'," + "`time14` int(10) unsigned NOT NULL default '0'," + "`time15` int(10) unsigned NOT NULL default '0'," + "`time16` int(10) unsigned NOT NULL default '0'," + "`time17` int(10) unsigned NOT NULL default '0'," + "`time18` int(10) unsigned NOT NULL default '0'," + "`time19` int(10) unsigned NOT NULL default '0'," + "`time20` int(10) unsigned NOT NULL default '0'," + "`time21` int(10) unsigned NOT NULL default '0'," + "`time22` int(10) unsigned NOT NULL default '0'," + "`time23` int(10) unsigned NOT NULL default '0'," + "PRIMARY KEY (`id`)," + "UNIQUE KEY `chan` (`chan`,`nick`,`type`)," + "KEY `nick` (`nick`)," + "KEY `chan_` (`chan`)," + "KEY `type` (`type`)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; + this->RunQuery(query); + } + /* There is no CREATE OR REPLACE PROCEDURE in MySQL */ + if (this->HasProcedure(prefix + "chanstats_proc_update")) + { + query = "DROP PROCEDURE " + prefix + "chanstats_proc_update"; + this->RunQuery(query); + } + query = "CREATE PROCEDURE `" + prefix + "chanstats_proc_update`" + "(chan_ VARCHAR(255), nick_ VARCHAR(255), line_ INT(10), letters_ INT(10)," + "words_ INT(10), actions_ INT(10), sm_h_ INT(10), sm_s_ INT(10), sm_o_ INT(10)," + "kicks_ INT(10), kicked_ INT(10), modes_ INT(10), topics_ INT(10))" + "BEGIN " + "DECLARE time_ VARCHAR(20);" + "SET time_ = CONCAT('time', hour(now()));" + "INSERT IGNORE INTO `" + prefix + "chanstats` (`nick`,`chan`, `type`) VALUES " + "('', chan_, 'total'), ('', chan_, 'monthly')," + "('', chan_, 'weekly'), ('', chan_, 'daily');" + "IF nick_ != '' THEN " + "INSERT IGNORE INTO `" + prefix + "chanstats` (`nick`,`chan`, `type`) VALUES " + "(nick_, chan_, 'total'), (nick_, chan_, 'monthly')," + "(nick_, chan_, 'weekly'),(nick_, chan_, 'daily')," + "(nick_, '', 'total'), (nick_, '', 'monthly')," + "(nick_, '', 'weekly'), (nick_, '', 'daily');" + "END IF;" + "SET @update_query = CONCAT('UPDATE `" + prefix + "chanstats` SET line=line+', line_, '," + "letters=letters+', letters_, ' , words=words+', words_, ', actions=actions+', actions_, ', " + "smileys_happy=smileys_happy+', sm_h_, ', smileys_sad=smileys_sad+', sm_s_, ', " + "smileys_other=smileys_other+', sm_o_, ', kicks=kicks+', kicks_, ', kicked=kicked+', kicked_, ', " + "modes=modes+', modes_, ', topics=topics+', topics_, ', ', time_ , '=', time_, '+', line_ ,' " + "WHERE (nick='''' OR nick=''', nick_, ''') AND (chan='''' OR chan=''', chan_, ''')');" + "PREPARE update_query FROM @update_query;" + "EXECUTE update_query;" + "DEALLOCATE PREPARE update_query;" + "END"; + this->RunQuery(query); + + if (this->HasProcedure(prefix + "chanstats_proc_chgdisplay")) + { + query = "DROP PROCEDURE " + prefix + "chanstats_proc_chgdisplay;"; + this->RunQuery(query); + } + query = "CREATE PROCEDURE `" + prefix + "chanstats_proc_chgdisplay`" + "(old_nick varchar(255), new_nick varchar(255))" + "BEGIN " + "DECLARE res_count int(10) unsigned;" + "SELECT COUNT(nick) INTO res_count FROM `" + prefix + "chanstats` WHERE nick = new_nick;" + "IF res_count = 0 THEN " + "UPDATE `" + prefix + "chanstats` SET `nick` = new_nick WHERE `nick` = old_nick;" + "ELSE " + "my_cursor: BEGIN " + "DECLARE no_more_rows BOOLEAN DEFAULT FALSE;" + "DECLARE chan_ VARCHAR(255);" + "DECLARE type_ ENUM('total', 'monthly', 'weekly', 'daily');" + "DECLARE letters_, words_, line_, actions_, smileys_happy_," + "smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_," + "time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_, time9_," + "time10_, time11_, time12_, time13_, time14_, time15_, time16_, time17_, time18_," + "time19_, time20_, time21_, time22_, time23_ INT(10) unsigned;" + "DECLARE stats_cursor CURSOR FOR " + "SELECT chan, type, letters, words, line, actions, smileys_happy," + "smileys_sad, smileys_other, kicks, kicked, modes, topics, time0, time1," + "time2, time3, time4, time5, time6, time7, time8, time9, time10, time11," + "time12, time13, time14, time15, time16, time17, time18, time19, time20," + "time21, time22, time23 " + "FROM `" + prefix + "chanstats` " + "WHERE `nick` = old_nick;" + "DECLARE CONTINUE HANDLER FOR NOT FOUND " + "SET no_more_rows = TRUE;" + "OPEN stats_cursor;" + "the_loop: LOOP " + "FETCH stats_cursor " + "INTO chan_, type_, letters_, words_, line_, actions_, smileys_happy_," + "smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_," + "time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_," + "time9_, time10_, time11_, time12_, time13_, time14_, time15_, time16_," + "time17_, time18_, time19_, time20_, time21_, time22_, time23_;" + "IF no_more_rows THEN " + "CLOSE stats_cursor;" + "LEAVE the_loop;" + "END IF;" + "INSERT INTO `" + prefix + "chanstats` " + "(chan, nick, type, letters, words, line, actions, smileys_happy, " + "smileys_sad, smileys_other, kicks, kicked, modes, topics, time0, time1, " + "time2, time3, time4, time5, time6, time7, time8, time9, time10, time11," + "time12, time13, time14, time15, time16, time17, time18, time19, time20," + "time21, time22, time23)" + "VALUES (chan_, new_nick, type_, letters_, words_, line_, actions_, smileys_happy_," + "smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_," + "time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_, " + "time9_, time10_, time11_, time12_, time13_, time14_, time15_, time16_, " + "time17_, time18_, time19_, time20_, time21_, time22_, time23_)" + "ON DUPLICATE KEY UPDATE letters=letters+VALUES(letters), words=words+VALUES(words)," + "line=line+VALUES(line), actions=actions+VALUES(actions)," + "smileys_happy=smileys_happy+VALUES(smileys_happy)," + "smileys_sad=smileys_sad+VALUES(smileys_sad)," + "smileys_other=smileys_other+VALUES(smileys_other)," + "kicks=kicks+VALUES(kicks), kicked=kicked+VALUES(kicked)," + "modes=modes+VALUES(modes), topics=topics+VALUES(topics)," + "time1=time1+VALUES(time1), time2=time2+VALUES(time2), time3=time3+VALUES(time3)," + "time4=time4+VALUES(time4), time5=time5+VALUES(time5), time6=time6+VALUES(time6)," + "time7=time7+VALUES(time7), time8=time8+VALUES(time8), time9=time9+VALUES(time9)," + "time10=time10+VALUES(time10), time11=time11+VALUES(time11), time12=time12+VALUES(time12)," + "time13=time13+VALUES(time13), time14=time14+VALUES(time14), time15=time15+VALUES(time15)," + "time16=time16+VALUES(time16), time17=time17+VALUES(time17), time18=time18+VALUES(time18)," + "time19=time19+VALUES(time19), time20=time20+VALUES(time20), time21=time21+VALUES(time21)," + "time22=time22+VALUES(time22), time23=time23+VALUES(time23);" + "END LOOP;" + "DELETE FROM `" + prefix + "chanstats` WHERE `nick` = old_nick;" + "END my_cursor;" + "END IF;" + "END;"; + this->RunQuery(query); + + /* don't prepend any database prefix to events so we can always delete/change old events */ + if (this->HasEvent("chanstats_event_cleanup_daily")) + { + query = "DROP EVENT chanstats_event_cleanup_daily"; + this->RunQuery(query); + } + query = "CREATE EVENT `chanstats_event_cleanup_daily` " + "ON SCHEDULE EVERY 1 DAY STARTS CURRENT_DATE " + "DO UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0," + "smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0," + "time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0," + "time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0," + "time20=0, time21=0, time22=0, time23=0 " + "WHERE type='daily';"; + this->RunQuery(query); + + if (this->HasEvent("chanstats_event_cleanup_weekly")) + { + query = "DROP EVENT `chanstats_event_cleanup_weekly`"; + this->RunQuery(query); + } + query = "CREATE EVENT `chanstats_event_cleanup_weekly` " + "ON SCHEDULE EVERY 1 WEEK STARTS ADDDATE(CURDATE(), INTERVAL 1-DAYOFWEEK(CURDATE()) DAY) " + "DO UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0," + "smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0," + "time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0," + "time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0," + "time20=0, time21=0, time22=0, time23=0 " + "WHERE type='weekly';"; + this->RunQuery(query); + + if (this->HasEvent("chanstats_event_cleanup_monthly")) + { + query = "DROP EVENT `chanstats_event_cleanup_monthly`;"; + this->RunQuery(query); + } + query = "CREATE EVENT `chanstats_event_cleanup_monthly` " + "ON SCHEDULE EVERY 1 MONTH STARTS LAST_DAY(CURRENT_TIMESTAMP) + INTERVAL 1 DAY " + "DO BEGIN " + "UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0," + "smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0," + "time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0," + "time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0, " + "time20=0, time21=0, time22=0, time23=0 " + "WHERE type='monthly';" + "OPTIMIZE TABLE `" + prefix + "chanstats`;" + "END;"; + this->RunQuery(query); + } + + +public: + MChanstats(const Anope::string &modname, const Anope::string &creator) : + Module(modname, creator, EXTRA | VENDOR), + cs_stats(this, "CS_STATS"), ns_stats(this, "NS_STATS"), + commandcssetchanstats(this), commandnssetchanstats(this), commandnssasetchanstats(this), + sqlinterface(this) + { + } + + void OnReload(Configuration::Conf *conf) override + { + Configuration::Block *block = conf->GetModule(this); + prefix = block->Get("prefix", "anope_"); + SmileysHappy = block->Get("SmileysHappy"); + SmileysSad = block->Get("SmileysSad"); + SmileysOther = block->Get("SmileysOther"); + NSDefChanstats = block->Get("ns_def_chanstats"); + CSDefChanstats = block->Get("cs_def_chanstats"); + Anope::string engine = block->Get("engine"); + this->sql = ServiceReference("SQL::Provider", engine); + if (sql) + this->CheckTables(); + else + Log(this) << "no database connection to " << engine; + } + + void OnChanInfo(CommandSource &source, ChannelInfo *ci, InfoFormatter &info, bool show_all) override + { + if (!show_all) + return; + if (cs_stats.HasExt(ci)) + info.AddOption(_("Chanstats")); + } + + void OnNickInfo(CommandSource &source, NickAlias *na, InfoFormatter &info, bool show_hidden) override + { + if (!show_hidden) + return; + if (ns_stats.HasExt(na->nc)) + info.AddOption(_("Chanstats")); + } + + void OnTopicUpdated(User *source, Channel *c, const Anope::string &user, const Anope::string &topic) override + { + if (!source || !source->Account() || !c->ci || !cs_stats.HasExt(c->ci)) + return; + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);"; + query.SetValue("channel", c->name); + query.SetValue("nick", GetDisplay(source)); + this->RunQuery(query); + } + + EventReturn OnChannelModeSet(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string ¶m) override + { + this->OnModeChange(c, setter.GetUser()); + return EVENT_CONTINUE; + } + + EventReturn OnChannelModeUnset(Channel *c, MessageSource &setter, ChannelMode *, const Anope::string ¶m) override + { + this->OnModeChange(c, setter.GetUser()); + return EVENT_CONTINUE; + } + +private: + void OnModeChange(Channel *c, User *u) + { + if (!u || !u->Account() || !c->ci || !cs_stats.HasExt(c->ci)) + return; + + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);"; + query.SetValue("channel", c->name); + query.SetValue("nick", GetDisplay(u)); + this->RunQuery(query); + } + +public: + void OnPreUserKicked(const MessageSource &source, ChanUserContainer *cu, const Anope::string &kickmsg) override + { + if (!cu->chan->ci || !cs_stats.HasExt(cu->chan->ci)) + return; + + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0);"; + query.SetValue("channel", cu->chan->name); + query.SetValue("nick", GetDisplay(cu->user)); + this->RunQuery(query); + + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0);"; + query.SetValue("channel", cu->chan->name); + query.SetValue("nick", GetDisplay(source.GetUser())); + this->RunQuery(query); + } + + void OnPrivmsg(User *u, Channel *c, Anope::string &msg) override + { + if (!c->ci || !cs_stats.HasExt(c->ci)) + return; + + size_t letters = msg.length(); + size_t words = this->CountWords(msg); + + size_t action = 0; + if (msg.find("\01ACTION")!=Anope::string::npos) + { + action = 1; + letters = letters - 7; + words--; + } + + // count smileys + size_t smileys_happy = CountSmileys(msg, SmileysHappy); + size_t smileys_sad = CountSmileys(msg, SmileysSad); + size_t smileys_other = CountSmileys(msg, SmileysOther); + + // do not count smileys as words + size_t smileys = smileys_happy + smileys_sad + smileys_other; + if (smileys > words) + words = 0; + else + words = words - smileys; + + query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 1, @letters@, @words@, @action@, " + "@smileys_happy@, @smileys_sad@, @smileys_other@, '0', '0', '0', '0');"; + query.SetValue("channel", c->name); + query.SetValue("nick", GetDisplay(u)); + query.SetValue("letters", letters); + query.SetValue("words", words); + query.SetValue("action", action); + query.SetValue("smileys_happy", smileys_happy); + query.SetValue("smileys_sad", smileys_sad); + query.SetValue("smileys_other", smileys_other); + this->RunQuery(query); + } + + void OnDelCore(NickCore *nc) override + { + query = "DELETE FROM `" + prefix + "chanstats` WHERE `nick` = @nick@;"; + query.SetValue("nick", nc->display); + this->RunQuery(query); + } + + void OnChangeCoreDisplay(NickCore *nc, const Anope::string &newdisplay) override + { + query = "CALL " + prefix + "chanstats_proc_chgdisplay(@old_display@, @new_display@);"; + query.SetValue("old_display", nc->display); + query.SetValue("new_display", newdisplay); + this->RunQuery(query); + } + + void OnDelChan(ChannelInfo *ci) override + { + query = "DELETE FROM `" + prefix + "chanstats` WHERE `chan` = @channel@;"; + query.SetValue("channel", ci->name); + this->RunQuery(query); + } + + void OnChanRegistered(ChannelInfo *ci) override + { + if (CSDefChanstats) + ci->Extend("CS_STATS"); + } + + void OnNickRegister(User *user, NickAlias *na, const Anope::string &) override + { + if (NSDefChanstats) + na->nc->Extend("NS_STATS"); + } +}; + +MODULE_INIT(MChanstats) diff --git a/modules/stats/cs_fantasy_stats.cpp b/modules/stats/cs_fantasy_stats.cpp index fedbd49b0..516dd02b9 100644 --- a/modules/stats/cs_fantasy_stats.cpp +++ b/modules/stats/cs_fantasy_stats.cpp @@ -79,14 +79,14 @@ public: void OnReload(Configuration::Conf *conf) override { - prefix = conf->GetModule("m_chanstats")->Get("prefix", "anope_"); - this->sql = ServiceReference("SQL::Provider", conf->GetModule("m_chanstats")->Get("engine")); + prefix = conf->GetModule("chanstats")->Get("prefix", "anope_"); + this->sql = ServiceReference("SQL::Provider", conf->GetModule("chanstats")->Get("engine")); } SQL::Result RunQuery(const SQL::Query &query) { if (!this->sql) - throw SQL::Exception("Unable to locate SQL reference, is m_mysql loaded and configured correctly?"); + throw SQL::Exception("Unable to locate SQL reference, is mysql loaded and configured correctly?"); SQL::Result res = this->sql->RunQuery(query); if (!res.GetError().empty()) diff --git a/modules/stats/cs_fantasy_top.cpp b/modules/stats/cs_fantasy_top.cpp index 6b3b344c6..3bc1fa261 100644 --- a/modules/stats/cs_fantasy_top.cpp +++ b/modules/stats/cs_fantasy_top.cpp @@ -106,14 +106,14 @@ public: void OnReload(Configuration::Conf *conf) override { - prefix = conf->GetModule("m_chanstats")->Get("prefix", "anope_"); - this->sql = ServiceReference("SQL::Provider", conf->GetModule("m_chanstats")->Get("engine")); + prefix = conf->GetModule("chanstats")->Get("prefix", "anope_"); + this->sql = ServiceReference("SQL::Provider", conf->GetModule("chanstats")->Get("engine")); } SQL::Result RunQuery(const SQL::Query &query) { if (!this->sql) - throw SQL::Exception("Unable to locate SQL reference, is m_mysql loaded and configured correctly?"); + throw SQL::Exception("Unable to locate SQL reference, is mysql loaded and configured correctly?"); SQL::Result res = sql->RunQuery(query); if (!res.GetError().empty()) diff --git a/modules/stats/irc2sql/irc2sql.cpp b/modules/stats/irc2sql/irc2sql.cpp index 2753dc00e..ed83001c3 100644 --- a/modules/stats/irc2sql/irc2sql.cpp +++ b/modules/stats/irc2sql/irc2sql.cpp @@ -11,7 +11,7 @@ void IRC2SQL::OnShutdown() { // TODO: test if we really have to use blocking query here - // (sometimes m_mysql get unloaded before the other thread executed all queries) + // (sometimes mysql get unloaded before the other thread executed all queries) if (this->sql) SQL::Result r = this->sql->RunQuery(SQL::Query("CALL " + prefix + "OnShutdown()")); quitting = true; diff --git a/modules/stats/irc2sql/irc2sql.h b/modules/stats/irc2sql/irc2sql.h index ea537a424..e08f39a34 100644 --- a/modules/stats/irc2sql/irc2sql.h +++ b/modules/stats/irc2sql/irc2sql.h @@ -24,9 +24,9 @@ public: void OnError(const SQL::Result &r) override { if (!r.GetQuery().query.empty()) - Log(LOG_DEBUG) << "m_irc2sql: Error executing query " << r.finished_query << ": " << r.GetError(); + Log(LOG_DEBUG) << "irc2sql: Error executing query " << r.finished_query << ": " << r.GetError(); else - Log(LOG_DEBUG) << "m_irc2sql: Error executing query: " << r.GetError(); + Log(LOG_DEBUG) << "irc2sql: Error executing query: " << r.GetError(); } }; diff --git a/modules/stats/m_chanstats.cpp b/modules/stats/m_chanstats.cpp deleted file mode 100644 index db92689d7..000000000 --- a/modules/stats/m_chanstats.cpp +++ /dev/null @@ -1,660 +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" - -class CommandCSSetChanstats final - : public Command -{ -public: - CommandCSSetChanstats(Module *creator) : Command(creator, "chanserv/set/chanstats", 2, 2) - { - this->SetDesc(_("Turn chanstats statistics on or off")); - this->SetSyntax(_("\037channel\037 {ON | OFF}")); - } - - void Execute(CommandSource &source, const std::vector ¶ms) override - { - ChannelInfo *ci = ChannelInfo::Find(params[0]); - if (!ci) - { - source.Reply(CHAN_X_NOT_REGISTERED, params[0].c_str()); - return; - } - - EventReturn MOD_RESULT; - FOREACH_RESULT(OnSetChannelOption, MOD_RESULT, (source, this, ci, params[1])); - if (MOD_RESULT == EVENT_STOP) - return; - - if (MOD_RESULT != EVENT_ALLOW && !source.AccessFor(ci).HasPriv("SET") && source.permission.empty() && !source.HasPriv("chanserv/administration")) - { - source.Reply(ACCESS_DENIED); - return; - } - - if (params[1].equals_ci("ON")) - { - ci->Extend("CS_STATS"); - source.Reply(_("Chanstats statistics are now enabled for this channel.")); - Log(source.AccessFor(ci).HasPriv("SET") ? LOG_COMMAND : LOG_OVERRIDE, source, this, ci) << "to enable chanstats"; - } - else if (params[1].equals_ci("OFF")) - { - Log(source.AccessFor(ci).HasPriv("SET") ? LOG_COMMAND : LOG_OVERRIDE, source, this, ci) << "to disable chanstats"; - ci->Shrink("CS_STATS"); - source.Reply(_("Chanstats statistics are now disabled for this channel.")); - } - else - this->OnSyntaxError(source, ""); - } - - bool OnHelp(CommandSource &source, const Anope::string &) override - { - this->SendSyntax(source); - source.Reply(" "); - source.Reply(_("Turns chanstats statistics ON or OFF.")); - return true; - } -}; - -class CommandNSSetChanstats - : public Command -{ -public: - CommandNSSetChanstats(Module *creator, const Anope::string &sname = "nickserv/set/chanstats", size_t min = 1 ) : Command(creator, sname, min, min + 1) - { - this->SetDesc(_("Turn chanstats statistics on or off")); - this->SetSyntax("{ON | OFF}"); - } - void Run(CommandSource &source, const Anope::string &user, const Anope::string ¶m, bool saset = false) - { - NickAlias *na = NickAlias::Find(user); - if (!na) - { - source.Reply(NICK_X_NOT_REGISTERED, user.c_str()); - return; - } - - EventReturn MOD_RESULT; - FOREACH_RESULT(OnSetNickOption, MOD_RESULT, (source, this, na->nc, param)); - if (MOD_RESULT == EVENT_STOP) - return; - - if (param.equals_ci("ON")) - { - Log(na->nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to enable chanstats for " << na->nc->display; - na->nc->Extend("NS_STATS"); - if (saset) - source.Reply(_("Chanstats statistics are now enabled for %s"), na->nc->display.c_str()); - else - source.Reply(_("Chanstats statistics are now enabled for your nick.")); - } - else if (param.equals_ci("OFF")) - { - Log(na->nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to disable chanstats for " << na->nc->display; - na->nc->Shrink("NS_STATS"); - if (saset) - source.Reply(_("Chanstats statistics are now disabled for %s"), na->nc->display.c_str()); - else - source.Reply(_("Chanstats statistics are now disabled for your nick.")); - } - else - this->OnSyntaxError(source, "CHANSTATS"); - } - - void Execute(CommandSource &source, const std::vector ¶ms) override - { - this->Run(source, source.nc->display, params[0]); - } - - bool OnHelp(CommandSource &source, const Anope::string &) override - { - this->SendSyntax(source); - source.Reply(" "); - source.Reply(_("Turns chanstats statistics ON or OFF.")); - return true; - } -}; - -class CommandNSSASetChanstats final - : public CommandNSSetChanstats -{ -public: - CommandNSSASetChanstats(Module *creator) : CommandNSSetChanstats(creator, "nickserv/saset/chanstats", 2) - { - this->ClearSyntax(); - this->SetSyntax(_("\037nickname\037 {ON | OFF}")); - } - - void Execute(CommandSource &source, const std::vector ¶ms) override - { - this->Run(source, params[0], params[1], true); - } - - bool OnHelp(CommandSource &source, const Anope::string &) override - { - this->SendSyntax(source); - source.Reply(" "); - source.Reply(_("Turns chanstats channel statistics ON or OFF for this user.")); - return true; - } -}; - -class MySQLInterface final - : public SQL::Interface -{ -public: - MySQLInterface(Module *o) : SQL::Interface(o) { } - - void OnResult(const SQL::Result &r) override - { - } - - void OnError(const SQL::Result &r) override - { - if (!r.GetQuery().query.empty()) - Log(LOG_DEBUG) << "Chanstats: Error executing query " << r.finished_query << ": " << r.GetError(); - else - Log(LOG_DEBUG) << "Chanstats: Error executing query: " << r.GetError(); - } -}; - -class MChanstats final - : public Module -{ - SerializableExtensibleItem cs_stats, ns_stats; - - CommandCSSetChanstats commandcssetchanstats; - - CommandNSSetChanstats commandnssetchanstats; - CommandNSSASetChanstats commandnssasetchanstats; - - ServiceReference sql; - MySQLInterface sqlinterface; - SQL::Query query; - Anope::string SmileysHappy, SmileysSad, SmileysOther, prefix; - std::vector TableList, ProcedureList, EventList; - bool NSDefChanstats, CSDefChanstats; - - void RunQuery(const SQL::Query &q) - { - if (sql) - sql->Run(&sqlinterface, q); - } - - size_t CountWords(const Anope::string &msg) - { - size_t words = 0; - for (size_t pos = 0; pos != Anope::string::npos; pos = msg.find(" ", pos+1)) - words++; - return words; - } - size_t CountSmileys(const Anope::string &msg, const Anope::string &smileylist) - { - size_t smileys = 0; - spacesepstream sep(smileylist); - Anope::string buf; - - while (sep.GetToken(buf) && !buf.empty()) - { - for (size_t pos = msg.find(buf, 0); pos != Anope::string::npos; pos = msg.find(buf, pos+1)) - smileys++; - } - return smileys; - } - - const Anope::string GetDisplay(User *u) - { - if (u && u->Account() && ns_stats.HasExt(u->Account())) - return u->Account()->display; - else - return ""; - } - - void GetTables() - { - TableList.clear(); - ProcedureList.clear(); - EventList.clear(); - if (!sql) - return; - - SQL::Result r = this->sql->RunQuery(this->sql->GetTables(prefix)); - for (int i = 0; i < r.Rows(); ++i) - { - const std::map &map = r.Row(i); - for (std::map::const_iterator it = map.begin(); it != map.end(); ++it) - TableList.push_back(it->second); - } - query = "SHOW PROCEDURE STATUS WHERE `Db` = Database();"; - r = this->sql->RunQuery(query); - for (int i = 0; i < r.Rows(); ++i) - { - ProcedureList.push_back(r.Get(i, "Name")); - } - query = "SHOW EVENTS WHERE `Db` = Database();"; - r = this->sql->RunQuery(query); - for (int i = 0; i < r.Rows(); ++i) - { - EventList.push_back(r.Get(i, "Name")); - } - } - - bool HasTable(const Anope::string &table) - { - for (std::vector::const_iterator it = TableList.begin(); it != TableList.end(); ++it) - if (*it == table) - return true; - return false; - } - - bool HasProcedure(const Anope::string &table) - { - for (std::vector::const_iterator it = ProcedureList.begin(); it != ProcedureList.end(); ++it) - if (*it == table) - return true; - return false; - } - - bool HasEvent(const Anope::string &table) - { - for (std::vector::const_iterator it = EventList.begin(); it != EventList.end(); ++it) - if (*it == table) - return true; - return false; - } - - - void CheckTables() - { - this->GetTables(); - if (!this->HasTable(prefix +"chanstats")) - { - query = "CREATE TABLE `" + prefix + "chanstats` (" - "`id` int(11) NOT NULL AUTO_INCREMENT," - "`chan` varchar(64) NOT NULL DEFAULT ''," - "`nick` varchar(64) NOT NULL DEFAULT ''," - "`type` ENUM('total', 'monthly', 'weekly', 'daily') NOT NULL," - "`letters` int(10) unsigned NOT NULL DEFAULT '0'," - "`words` int(10) unsigned NOT NULL DEFAULT '0'," - "`line` int(10) unsigned NOT NULL DEFAULT '0'," - "`actions` int(10) unsigned NOT NULL DEFAULT '0'," - "`smileys_happy` int(10) unsigned NOT NULL DEFAULT '0'," - "`smileys_sad` int(10) unsigned NOT NULL DEFAULT '0'," - "`smileys_other` int(10) unsigned NOT NULL DEFAULT '0'," - "`kicks` int(10) unsigned NOT NULL DEFAULT '0'," - "`kicked` int(10) unsigned NOT NULL DEFAULT '0'," - "`modes` int(10) unsigned NOT NULL DEFAULT '0'," - "`topics` int(10) unsigned NOT NULL DEFAULT '0'," - "`time0` int(10) unsigned NOT NULL default '0'," - "`time1` int(10) unsigned NOT NULL default '0'," - "`time2` int(10) unsigned NOT NULL default '0'," - "`time3` int(10) unsigned NOT NULL default '0'," - "`time4` int(10) unsigned NOT NULL default '0'," - "`time5` int(10) unsigned NOT NULL default '0'," - "`time6` int(10) unsigned NOT NULL default '0'," - "`time7` int(10) unsigned NOT NULL default '0'," - "`time8` int(10) unsigned NOT NULL default '0'," - "`time9` int(10) unsigned NOT NULL default '0'," - "`time10` int(10) unsigned NOT NULL default '0'," - "`time11` int(10) unsigned NOT NULL default '0'," - "`time12` int(10) unsigned NOT NULL default '0'," - "`time13` int(10) unsigned NOT NULL default '0'," - "`time14` int(10) unsigned NOT NULL default '0'," - "`time15` int(10) unsigned NOT NULL default '0'," - "`time16` int(10) unsigned NOT NULL default '0'," - "`time17` int(10) unsigned NOT NULL default '0'," - "`time18` int(10) unsigned NOT NULL default '0'," - "`time19` int(10) unsigned NOT NULL default '0'," - "`time20` int(10) unsigned NOT NULL default '0'," - "`time21` int(10) unsigned NOT NULL default '0'," - "`time22` int(10) unsigned NOT NULL default '0'," - "`time23` int(10) unsigned NOT NULL default '0'," - "PRIMARY KEY (`id`)," - "UNIQUE KEY `chan` (`chan`,`nick`,`type`)," - "KEY `nick` (`nick`)," - "KEY `chan_` (`chan`)," - "KEY `type` (`type`)" - ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; - this->RunQuery(query); - } - /* There is no CREATE OR REPLACE PROCEDURE in MySQL */ - if (this->HasProcedure(prefix + "chanstats_proc_update")) - { - query = "DROP PROCEDURE " + prefix + "chanstats_proc_update"; - this->RunQuery(query); - } - query = "CREATE PROCEDURE `" + prefix + "chanstats_proc_update`" - "(chan_ VARCHAR(255), nick_ VARCHAR(255), line_ INT(10), letters_ INT(10)," - "words_ INT(10), actions_ INT(10), sm_h_ INT(10), sm_s_ INT(10), sm_o_ INT(10)," - "kicks_ INT(10), kicked_ INT(10), modes_ INT(10), topics_ INT(10))" - "BEGIN " - "DECLARE time_ VARCHAR(20);" - "SET time_ = CONCAT('time', hour(now()));" - "INSERT IGNORE INTO `" + prefix + "chanstats` (`nick`,`chan`, `type`) VALUES " - "('', chan_, 'total'), ('', chan_, 'monthly')," - "('', chan_, 'weekly'), ('', chan_, 'daily');" - "IF nick_ != '' THEN " - "INSERT IGNORE INTO `" + prefix + "chanstats` (`nick`,`chan`, `type`) VALUES " - "(nick_, chan_, 'total'), (nick_, chan_, 'monthly')," - "(nick_, chan_, 'weekly'),(nick_, chan_, 'daily')," - "(nick_, '', 'total'), (nick_, '', 'monthly')," - "(nick_, '', 'weekly'), (nick_, '', 'daily');" - "END IF;" - "SET @update_query = CONCAT('UPDATE `" + prefix + "chanstats` SET line=line+', line_, '," - "letters=letters+', letters_, ' , words=words+', words_, ', actions=actions+', actions_, ', " - "smileys_happy=smileys_happy+', sm_h_, ', smileys_sad=smileys_sad+', sm_s_, ', " - "smileys_other=smileys_other+', sm_o_, ', kicks=kicks+', kicks_, ', kicked=kicked+', kicked_, ', " - "modes=modes+', modes_, ', topics=topics+', topics_, ', ', time_ , '=', time_, '+', line_ ,' " - "WHERE (nick='''' OR nick=''', nick_, ''') AND (chan='''' OR chan=''', chan_, ''')');" - "PREPARE update_query FROM @update_query;" - "EXECUTE update_query;" - "DEALLOCATE PREPARE update_query;" - "END"; - this->RunQuery(query); - - if (this->HasProcedure(prefix + "chanstats_proc_chgdisplay")) - { - query = "DROP PROCEDURE " + prefix + "chanstats_proc_chgdisplay;"; - this->RunQuery(query); - } - query = "CREATE PROCEDURE `" + prefix + "chanstats_proc_chgdisplay`" - "(old_nick varchar(255), new_nick varchar(255))" - "BEGIN " - "DECLARE res_count int(10) unsigned;" - "SELECT COUNT(nick) INTO res_count FROM `" + prefix + "chanstats` WHERE nick = new_nick;" - "IF res_count = 0 THEN " - "UPDATE `" + prefix + "chanstats` SET `nick` = new_nick WHERE `nick` = old_nick;" - "ELSE " - "my_cursor: BEGIN " - "DECLARE no_more_rows BOOLEAN DEFAULT FALSE;" - "DECLARE chan_ VARCHAR(255);" - "DECLARE type_ ENUM('total', 'monthly', 'weekly', 'daily');" - "DECLARE letters_, words_, line_, actions_, smileys_happy_," - "smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_," - "time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_, time9_," - "time10_, time11_, time12_, time13_, time14_, time15_, time16_, time17_, time18_," - "time19_, time20_, time21_, time22_, time23_ INT(10) unsigned;" - "DECLARE stats_cursor CURSOR FOR " - "SELECT chan, type, letters, words, line, actions, smileys_happy," - "smileys_sad, smileys_other, kicks, kicked, modes, topics, time0, time1," - "time2, time3, time4, time5, time6, time7, time8, time9, time10, time11," - "time12, time13, time14, time15, time16, time17, time18, time19, time20," - "time21, time22, time23 " - "FROM `" + prefix + "chanstats` " - "WHERE `nick` = old_nick;" - "DECLARE CONTINUE HANDLER FOR NOT FOUND " - "SET no_more_rows = TRUE;" - "OPEN stats_cursor;" - "the_loop: LOOP " - "FETCH stats_cursor " - "INTO chan_, type_, letters_, words_, line_, actions_, smileys_happy_," - "smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_," - "time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_," - "time9_, time10_, time11_, time12_, time13_, time14_, time15_, time16_," - "time17_, time18_, time19_, time20_, time21_, time22_, time23_;" - "IF no_more_rows THEN " - "CLOSE stats_cursor;" - "LEAVE the_loop;" - "END IF;" - "INSERT INTO `" + prefix + "chanstats` " - "(chan, nick, type, letters, words, line, actions, smileys_happy, " - "smileys_sad, smileys_other, kicks, kicked, modes, topics, time0, time1, " - "time2, time3, time4, time5, time6, time7, time8, time9, time10, time11," - "time12, time13, time14, time15, time16, time17, time18, time19, time20," - "time21, time22, time23)" - "VALUES (chan_, new_nick, type_, letters_, words_, line_, actions_, smileys_happy_," - "smileys_sad_, smileys_other_, kicks_, kicked_, modes_, topics_," - "time0_, time1_, time2_, time3_, time4_, time5_, time6_, time7_, time8_, " - "time9_, time10_, time11_, time12_, time13_, time14_, time15_, time16_, " - "time17_, time18_, time19_, time20_, time21_, time22_, time23_)" - "ON DUPLICATE KEY UPDATE letters=letters+VALUES(letters), words=words+VALUES(words)," - "line=line+VALUES(line), actions=actions+VALUES(actions)," - "smileys_happy=smileys_happy+VALUES(smileys_happy)," - "smileys_sad=smileys_sad+VALUES(smileys_sad)," - "smileys_other=smileys_other+VALUES(smileys_other)," - "kicks=kicks+VALUES(kicks), kicked=kicked+VALUES(kicked)," - "modes=modes+VALUES(modes), topics=topics+VALUES(topics)," - "time1=time1+VALUES(time1), time2=time2+VALUES(time2), time3=time3+VALUES(time3)," - "time4=time4+VALUES(time4), time5=time5+VALUES(time5), time6=time6+VALUES(time6)," - "time7=time7+VALUES(time7), time8=time8+VALUES(time8), time9=time9+VALUES(time9)," - "time10=time10+VALUES(time10), time11=time11+VALUES(time11), time12=time12+VALUES(time12)," - "time13=time13+VALUES(time13), time14=time14+VALUES(time14), time15=time15+VALUES(time15)," - "time16=time16+VALUES(time16), time17=time17+VALUES(time17), time18=time18+VALUES(time18)," - "time19=time19+VALUES(time19), time20=time20+VALUES(time20), time21=time21+VALUES(time21)," - "time22=time22+VALUES(time22), time23=time23+VALUES(time23);" - "END LOOP;" - "DELETE FROM `" + prefix + "chanstats` WHERE `nick` = old_nick;" - "END my_cursor;" - "END IF;" - "END;"; - this->RunQuery(query); - - /* don't prepend any database prefix to events so we can always delete/change old events */ - if (this->HasEvent("chanstats_event_cleanup_daily")) - { - query = "DROP EVENT chanstats_event_cleanup_daily"; - this->RunQuery(query); - } - query = "CREATE EVENT `chanstats_event_cleanup_daily` " - "ON SCHEDULE EVERY 1 DAY STARTS CURRENT_DATE " - "DO UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0," - "smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0," - "time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0," - "time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0," - "time20=0, time21=0, time22=0, time23=0 " - "WHERE type='daily';"; - this->RunQuery(query); - - if (this->HasEvent("chanstats_event_cleanup_weekly")) - { - query = "DROP EVENT `chanstats_event_cleanup_weekly`"; - this->RunQuery(query); - } - query = "CREATE EVENT `chanstats_event_cleanup_weekly` " - "ON SCHEDULE EVERY 1 WEEK STARTS ADDDATE(CURDATE(), INTERVAL 1-DAYOFWEEK(CURDATE()) DAY) " - "DO UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0," - "smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0," - "time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0," - "time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0," - "time20=0, time21=0, time22=0, time23=0 " - "WHERE type='weekly';"; - this->RunQuery(query); - - if (this->HasEvent("chanstats_event_cleanup_monthly")) - { - query = "DROP EVENT `chanstats_event_cleanup_monthly`;"; - this->RunQuery(query); - } - query = "CREATE EVENT `chanstats_event_cleanup_monthly` " - "ON SCHEDULE EVERY 1 MONTH STARTS LAST_DAY(CURRENT_TIMESTAMP) + INTERVAL 1 DAY " - "DO BEGIN " - "UPDATE `" + prefix + "chanstats` SET letters=0, words=0, line=0, actions=0, smileys_happy=0," - "smileys_sad=0, smileys_other=0, kicks=0, modes=0, topics=0, time0=0, time1=0, time2=0," - "time3=0, time4=0, time5=0, time6=0, time7=0, time8=0, time9=0, time10=0, time11=0," - "time12=0, time13=0, time14=0, time15=0, time16=0, time17=0, time18=0, time19=0, " - "time20=0, time21=0, time22=0, time23=0 " - "WHERE type='monthly';" - "OPTIMIZE TABLE `" + prefix + "chanstats`;" - "END;"; - this->RunQuery(query); - } - - -public: - MChanstats(const Anope::string &modname, const Anope::string &creator) : - Module(modname, creator, EXTRA | VENDOR), - cs_stats(this, "CS_STATS"), ns_stats(this, "NS_STATS"), - commandcssetchanstats(this), commandnssetchanstats(this), commandnssasetchanstats(this), - sqlinterface(this) - { - } - - void OnReload(Configuration::Conf *conf) override - { - Configuration::Block *block = conf->GetModule(this); - prefix = block->Get("prefix", "anope_"); - SmileysHappy = block->Get("SmileysHappy"); - SmileysSad = block->Get("SmileysSad"); - SmileysOther = block->Get("SmileysOther"); - NSDefChanstats = block->Get("ns_def_chanstats"); - CSDefChanstats = block->Get("cs_def_chanstats"); - Anope::string engine = block->Get("engine"); - this->sql = ServiceReference("SQL::Provider", engine); - if (sql) - this->CheckTables(); - else - Log(this) << "no database connection to " << engine; - } - - void OnChanInfo(CommandSource &source, ChannelInfo *ci, InfoFormatter &info, bool show_all) override - { - if (!show_all) - return; - if (cs_stats.HasExt(ci)) - info.AddOption(_("Chanstats")); - } - - void OnNickInfo(CommandSource &source, NickAlias *na, InfoFormatter &info, bool show_hidden) override - { - if (!show_hidden) - return; - if (ns_stats.HasExt(na->nc)) - info.AddOption(_("Chanstats")); - } - - void OnTopicUpdated(User *source, Channel *c, const Anope::string &user, const Anope::string &topic) override - { - if (!source || !source->Account() || !c->ci || !cs_stats.HasExt(c->ci)) - return; - query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);"; - query.SetValue("channel", c->name); - query.SetValue("nick", GetDisplay(source)); - this->RunQuery(query); - } - - EventReturn OnChannelModeSet(Channel *c, MessageSource &setter, ChannelMode *mode, const Anope::string ¶m) override - { - this->OnModeChange(c, setter.GetUser()); - return EVENT_CONTINUE; - } - - EventReturn OnChannelModeUnset(Channel *c, MessageSource &setter, ChannelMode *, const Anope::string ¶m) override - { - this->OnModeChange(c, setter.GetUser()); - return EVENT_CONTINUE; - } - -private: - void OnModeChange(Channel *c, User *u) - { - if (!u || !u->Account() || !c->ci || !cs_stats.HasExt(c->ci)) - return; - - query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);"; - query.SetValue("channel", c->name); - query.SetValue("nick", GetDisplay(u)); - this->RunQuery(query); - } - -public: - void OnPreUserKicked(const MessageSource &source, ChanUserContainer *cu, const Anope::string &kickmsg) override - { - if (!cu->chan->ci || !cs_stats.HasExt(cu->chan->ci)) - return; - - query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0);"; - query.SetValue("channel", cu->chan->name); - query.SetValue("nick", GetDisplay(cu->user)); - this->RunQuery(query); - - query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0);"; - query.SetValue("channel", cu->chan->name); - query.SetValue("nick", GetDisplay(source.GetUser())); - this->RunQuery(query); - } - - void OnPrivmsg(User *u, Channel *c, Anope::string &msg) override - { - if (!c->ci || !cs_stats.HasExt(c->ci)) - return; - - size_t letters = msg.length(); - size_t words = this->CountWords(msg); - - size_t action = 0; - if (msg.find("\01ACTION")!=Anope::string::npos) - { - action = 1; - letters = letters - 7; - words--; - } - - // count smileys - size_t smileys_happy = CountSmileys(msg, SmileysHappy); - size_t smileys_sad = CountSmileys(msg, SmileysSad); - size_t smileys_other = CountSmileys(msg, SmileysOther); - - // do not count smileys as words - size_t smileys = smileys_happy + smileys_sad + smileys_other; - if (smileys > words) - words = 0; - else - words = words - smileys; - - query = "CALL " + prefix + "chanstats_proc_update(@channel@, @nick@, 1, @letters@, @words@, @action@, " - "@smileys_happy@, @smileys_sad@, @smileys_other@, '0', '0', '0', '0');"; - query.SetValue("channel", c->name); - query.SetValue("nick", GetDisplay(u)); - query.SetValue("letters", letters); - query.SetValue("words", words); - query.SetValue("action", action); - query.SetValue("smileys_happy", smileys_happy); - query.SetValue("smileys_sad", smileys_sad); - query.SetValue("smileys_other", smileys_other); - this->RunQuery(query); - } - - void OnDelCore(NickCore *nc) override - { - query = "DELETE FROM `" + prefix + "chanstats` WHERE `nick` = @nick@;"; - query.SetValue("nick", nc->display); - this->RunQuery(query); - } - - void OnChangeCoreDisplay(NickCore *nc, const Anope::string &newdisplay) override - { - query = "CALL " + prefix + "chanstats_proc_chgdisplay(@old_display@, @new_display@);"; - query.SetValue("old_display", nc->display); - query.SetValue("new_display", newdisplay); - this->RunQuery(query); - } - - void OnDelChan(ChannelInfo *ci) override - { - query = "DELETE FROM `" + prefix + "chanstats` WHERE `chan` = @channel@;"; - query.SetValue("channel", ci->name); - this->RunQuery(query); - } - - void OnChanRegistered(ChannelInfo *ci) override - { - if (CSDefChanstats) - ci->Extend("CS_STATS"); - } - - void OnNickRegister(User *user, NickAlias *na, const Anope::string &) override - { - if (NSDefChanstats) - na->nc->Extend("NS_STATS"); - } -}; - -MODULE_INIT(MChanstats) diff --git a/modules/webcpanel/webcpanel.cpp b/modules/webcpanel/webcpanel.cpp index f0cb06771..53243103c 100644 --- a/modules/webcpanel/webcpanel.cpp +++ b/modules/webcpanel/webcpanel.cpp @@ -67,7 +67,7 @@ public: provider = ServiceReference("HTTPProvider", provider_name); if (!provider) - throw ModuleException("Unable to find HTTPD provider. Is m_httpd loaded?"); + throw ModuleException("Unable to find HTTPD provider. Is httpd loaded?"); provider->RegisterPage(&this->style_css); provider->RegisterPage(&this->logo_png); diff --git a/modules/xmlrpc.cpp b/modules/xmlrpc.cpp new file mode 100644 index 000000000..319730be0 --- /dev/null +++ b/modules/xmlrpc.cpp @@ -0,0 +1,229 @@ +/* + * + * (C) 2010-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/xmlrpc.h" +#include "modules/httpd.h" + +static struct special_chars final +{ + Anope::string character; + Anope::string replace; + + special_chars(const Anope::string &c, const Anope::string &r) : character(c), replace(r) { } +} +special[] = { + special_chars("&", "&"), + special_chars("\"", """), + special_chars("<", "<"), + special_chars(">", "&qt;"), + special_chars("'", "'"), + special_chars("\n", " "), + special_chars("\002", ""), // bold + special_chars("\003", ""), // color + special_chars("\035", ""), // italics + special_chars("\037", ""), // underline + special_chars("\026", ""), // reverses + special_chars("", "") +}; + +class MyXMLRPCServiceInterface final + : public XMLRPCServiceInterface + , public HTTPPage +{ + std::deque events; + +public: + MyXMLRPCServiceInterface(Module *creator, const Anope::string &sname) : XMLRPCServiceInterface(creator, sname), HTTPPage("/xmlrpc", "text/xml") { } + + void Register(XMLRPCEvent *event) override + { + this->events.push_back(event); + } + + void Unregister(XMLRPCEvent *event) override + { + std::deque::iterator it = std::find(this->events.begin(), this->events.end(), event); + + if (it != this->events.end()) + this->events.erase(it); + } + + Anope::string Sanitize(const Anope::string &string) override + { + Anope::string ret = string; + for (int i = 0; special[i].character.empty() == false; ++i) + ret = ret.replace_all_cs(special[i].character, special[i].replace); + return ret; + } + + static Anope::string Unescape(const Anope::string &string) + { + Anope::string ret = string; + for (int i = 0; special[i].character.empty() == false; ++i) + if (!special[i].replace.empty()) + ret = ret.replace_all_cs(special[i].replace, special[i].character); + + for (size_t i, last = 0; (i = string.find("&#", last)) != Anope::string::npos;) + { + last = i + 1; + + size_t end = string.find(';', i); + if (end == Anope::string::npos) + break; + + Anope::string ch = string.substr(i + 2, end - (i + 2)); + + if (ch.empty()) + continue; + + long l; + if (!ch.empty() && ch[0] == 'x') + l = strtol(ch.substr(1).c_str(), NULL, 16); + else + l = strtol(ch.c_str(), NULL, 10); + + if (l > 0 && l < 256) + ret = ret.replace_all_cs("&#" + ch + ";", Anope::string(l)); + } + + return ret; + } + +private: + static bool GetData(Anope::string &content, Anope::string &tag, Anope::string &data) + { + if (content.empty()) + return false; + + Anope::string prev, cur; + bool istag; + + do + { + prev = cur; + cur.clear(); + + size_t len = 0; + istag = false; + + if (content[0] == '<') + { + len = content.find_first_of('>'); + istag = true; + } + else if (content[0] != '>') + { + len = content.find_first_of('<'); + } + + // len must advance + if (len == Anope::string::npos || len == 0) + break; + + if (istag) + { + cur = content.substr(1, len - 1); + content.erase(0, len + 1); + while (!content.empty() && content[0] == ' ') + content.erase(content.begin()); + } + else + { + cur = content.substr(0, len); + content.erase(0, len); + } + } + while (istag && !content.empty()); + + tag = Unescape(prev); + data = Unescape(cur); + return !istag && !data.empty(); + } + +public: + bool OnRequest(HTTPProvider *provider, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) override + { + Anope::string content = message.content, tname, data; + XMLRPCRequest request(reply); + + while (GetData(content, tname, data)) + { + Log(LOG_DEBUG) << "xmlrpc: Tag name: " << tname << ", data: " << data; + if (tname == "methodName") + request.name = data; + else if (tname == "name" && data == "id") + { + GetData(content, tname, data); + request.id = data; + } + else if (tname == "string") + request.data.push_back(data); + } + + for (auto *e : this->events) + { + if (!e->Run(this, client, request)) + return false; + else if (!request.get_replies().empty()) + { + this->Reply(request); + return true; + } + } + + reply.error = HTTP_PAGE_NOT_FOUND; + reply.Write("Unrecognized query"); + return true; + } + + void Reply(XMLRPCRequest &request) override + { + if (!request.id.empty()) + request.reply("id", request.id); + + Anope::string r = "\n\n\n\n\n\n"; + for (const auto &[name, value] : request.get_replies()) + r += "\n" + name + "\n\n" + this->Sanitize(value) + "\n\n\n"; + r += "\n\n\n\n"; + + request.r.Write(r); + } +}; + +class ModuleXMLRPC final + : public Module +{ + ServiceReference httpref; +public: + MyXMLRPCServiceInterface xmlrpcinterface; + + ModuleXMLRPC(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), + xmlrpcinterface(this, "xmlrpc") + { + + } + + ~ModuleXMLRPC() override + { + if (httpref) + httpref->UnregisterPage(&xmlrpcinterface); + } + + void OnReload(Configuration::Conf *conf) override + { + if (httpref) + httpref->UnregisterPage(&xmlrpcinterface); + this->httpref = ServiceReference("HTTPProvider", conf->GetModule(this)->Get("server", "httpd/main")); + if (!httpref) + throw ConfigException("Unable to find http reference, is httpd loaded?"); + httpref->RegisterPage(&xmlrpcinterface); + } +}; + +MODULE_INIT(ModuleXMLRPC) diff --git a/modules/xmlrpc_main.cpp b/modules/xmlrpc_main.cpp new file mode 100644 index 000000000..23ea1fb0b --- /dev/null +++ b/modules/xmlrpc_main.cpp @@ -0,0 +1,306 @@ +/* + * + * (C) 2010-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +#include "module.h" +#include "modules/xmlrpc.h" + +static Module *me; + +class XMLRPCIdentifyRequest final + : public IdentifyRequest +{ + XMLRPCRequest request; + HTTPReply repl; /* Request holds a reference to the HTTPReply, because we might exist long enough to invalidate it + we'll copy it here then reset the reference before we use it */ + Reference client; + Reference xinterface; + +public: + XMLRPCIdentifyRequest(Module *m, XMLRPCRequest& req, HTTPClient *c, XMLRPCServiceInterface* iface, const Anope::string &acc, const Anope::string &pass) : IdentifyRequest(m, acc, pass), request(req), repl(request.r), client(c), xinterface(iface) { } + + void OnSuccess() override + { + if (!xinterface || !client) + return; + + request.r = this->repl; + + request.reply("result", "Success"); + request.reply("account", GetAccount()); + + xinterface->Reply(request); + client->SendReply(&request.r); + } + + void OnFail() override + { + if (!xinterface || !client) + return; + + request.r = this->repl; + + request.reply("error", "Invalid password"); + + xinterface->Reply(request); + client->SendReply(&request.r); + } +}; + +class MyXMLRPCEvent final + : public XMLRPCEvent +{ +public: + bool Run(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) override + { + if (request.name == "command") + this->DoCommand(iface, client, request); + else if (request.name == "checkAuthentication") + return this->DoCheckAuthentication(iface, client, request); + else if (request.name == "stats") + this->DoStats(iface, client, request); + else if (request.name == "channel") + this->DoChannel(iface, client, request); + else if (request.name == "user") + this->DoUser(iface, client, request); + else if (request.name == "opers") + this->DoOperType(iface, client, request); + else if (request.name == "notice") + this->DoNotice(iface, client, request); + + return true; + } + +private: + void DoCommand(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) + { + Anope::string service = request.data.size() > 0 ? request.data[0] : ""; + Anope::string user = request.data.size() > 1 ? request.data[1] : ""; + Anope::string command = request.data.size() > 2 ? request.data[2] : ""; + + if (service.empty() || user.empty() || command.empty()) + request.reply("error", "Invalid parameters"); + else + { + BotInfo *bi = BotInfo::Find(service, true); + if (!bi) + request.reply("error", "Invalid service"); + else + { + request.reply("result", "Success"); + + NickAlias *na = NickAlias::Find(user); + + Anope::string out; + + struct XMLRPCommandReply final + : CommandReply + { + Anope::string &str; + + XMLRPCommandReply(Anope::string &s) : str(s) { } + + void SendMessage(BotInfo *, const Anope::string &msg) override + { + str += msg + "\n"; + }; + } + reply(out); + + User *u = User::Find(user, true); + CommandSource source(user, u, na ? *na->nc : NULL, &reply, bi); + Command::Run(source, command); + + if (!out.empty()) + request.reply("return", iface->Sanitize(out)); + } + } + } + + bool DoCheckAuthentication(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) + { + Anope::string username = request.data.size() > 0 ? request.data[0] : ""; + Anope::string password = request.data.size() > 1 ? request.data[1] : ""; + + if (username.empty() || password.empty()) + request.reply("error", "Invalid parameters"); + else + { + auto *req = new XMLRPCIdentifyRequest(me, request, client, iface, username, password); + FOREACH_MOD(OnCheckAuthentication, (NULL, req)); + req->Dispatch(); + return false; + } + + return true; + } + + void DoStats(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) + { + request.reply("uptime", stringify(Anope::CurTime - Anope::StartTime)); + request.reply("uplinkname", Me->GetLinks().front()->GetName()); + { + Anope::string buf; + for (const auto &capab : Servers::Capab) + buf += " " + capab; + if (!buf.empty()) + buf.erase(buf.begin()); + request.reply("uplinkcapab", buf); + } + request.reply("usercount", stringify(UserListByNick.size())); + request.reply("maxusercount", stringify(MaxUserCount)); + request.reply("channelcount", stringify(ChannelList.size())); + } + + void DoChannel(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) + { + if (request.data.empty()) + return; + + Channel *c = Channel::Find(request.data[0]); + + request.reply("name", iface->Sanitize(c ? c->name : request.data[0])); + + if (c) + { + request.reply("bancount", stringify(c->HasMode("BAN"))); + int count = 0; + for (auto &ban : c->GetModeList("BAN")) + request.reply("ban" + stringify(++count), iface->Sanitize(ban)); + + request.reply("exceptcount", stringify(c->HasMode("EXCEPT"))); + count = 0; + for (auto &except : c->GetModeList("EXCEPT")) + request.reply("except" + stringify(++count), iface->Sanitize(except)); + + request.reply("invitecount", stringify(c->HasMode("INVITEOVERRIDE"))); + count = 0; + for (auto &invite : c->GetModeList("INVITEOVERRIDE")) + request.reply("invite" + stringify(++count), iface->Sanitize(invite)); + + Anope::string users; + for (Channel::ChanUserList::const_iterator it = c->users.begin(); it != c->users.end(); ++it) + { + ChanUserContainer *uc = it->second; + users += uc->status.BuildModePrefixList() + uc->user->nick + " "; + } + if (!users.empty()) + { + users.erase(users.length() - 1); + request.reply("users", iface->Sanitize(users)); + } + + if (!c->topic.empty()) + request.reply("topic", iface->Sanitize(c->topic)); + + if (!c->topic_setter.empty()) + request.reply("topicsetter", iface->Sanitize(c->topic_setter)); + + request.reply("topictime", stringify(c->topic_time)); + request.reply("topicts", stringify(c->topic_ts)); + } + } + + void DoUser(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) + { + if (request.data.empty()) + return; + + User *u = User::Find(request.data[0]); + + request.reply("nick", iface->Sanitize(u ? u->nick : request.data[0])); + + if (u) + { + request.reply("ident", iface->Sanitize(u->GetIdent())); + request.reply("vident", iface->Sanitize(u->GetVIdent())); + request.reply("host", iface->Sanitize(u->host)); + if (!u->vhost.empty()) + request.reply("vhost", iface->Sanitize(u->vhost)); + if (!u->chost.empty()) + request.reply("chost", iface->Sanitize(u->chost)); + request.reply("ip", u->ip.addr()); + request.reply("timestamp", stringify(u->timestamp)); + request.reply("signon", stringify(u->signon)); + if (u->Account()) + { + request.reply("account", iface->Sanitize(u->Account()->display)); + if (u->Account()->o) + request.reply("opertype", iface->Sanitize(u->Account()->o->ot->GetName())); + } + + Anope::string channels; + for (User::ChanUserList::const_iterator it = u->chans.begin(); it != u->chans.end(); ++it) + { + ChanUserContainer *cc = it->second; + channels += cc->status.BuildModePrefixList() + cc->chan->name + " "; + } + if (!channels.empty()) + { + channels.erase(channels.length() - 1); + request.reply("channels", channels); + } + } + } + + void DoOperType(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) + { + for (auto *ot : Config->MyOperTypes) + { + Anope::string perms; + for (const auto &priv : ot->GetPrivs()) + perms += " " + priv; + for (const auto &command : ot->GetCommands()) + perms += " " + command; + request.reply(ot->GetName(), perms); + } + } + + void DoNotice(XMLRPCServiceInterface *iface, HTTPClient *client, XMLRPCRequest &request) + { + Anope::string from = request.data.size() > 0 ? request.data[0] : ""; + Anope::string to = request.data.size() > 1 ? request.data[1] : ""; + Anope::string message = request.data.size() > 2 ? request.data[2] : ""; + + BotInfo *bi = BotInfo::Find(from, true); + User *u = User::Find(to, true); + + if (!bi || !u || message.empty()) + return; + + u->SendMessage(bi, message); + + request.reply("result", "Success"); + } +}; + +class ModuleXMLRPCMain final + : public Module +{ + ServiceReference xmlrpc; + + MyXMLRPCEvent stats; + +public: + ModuleXMLRPCMain(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), xmlrpc("XMLRPCServiceInterface", "xmlrpc") + { + me = this; + + if (!xmlrpc) + throw ModuleException("Unable to find xmlrpc reference, is xmlrpc loaded?"); + + xmlrpc->Register(&stats); + } + + ~ModuleXMLRPCMain() override + { + if (xmlrpc) + xmlrpc->Unregister(&stats); + } +}; + +MODULE_INIT(ModuleXMLRPCMain) -- cgit