aboutsummaryrefslogtreecommitdiffstats
path: root/src/modules/extra/m_pgsql.cpp
diff options
context:
space:
mode:
authorGravatar Sadie Powell2024-07-17 00:06:50 +0100
committerGravatar Sadie Powell2024-07-17 00:06:50 +0100
commit889d521e05f2e922683d48c74eb00290e7a2f548 (patch)
treeb4c56bd4cdf0881601c7e4d6ff571e491af10bf4 /src/modules/extra/m_pgsql.cpp
parentUse std::endian from C++20 in the sha1 module. (diff)
downloadinspircd++-889d521e05f2e922683d48c74eb00290e7a2f548.tar.gz
inspircd++-889d521e05f2e922683d48c74eb00290e7a2f548.tar.bz2
inspircd++-889d521e05f2e922683d48c74eb00290e7a2f548.zip
Shuffle the modules about a bit.
Diffstat (limited to 'src/modules/extra/m_pgsql.cpp')
-rw-r--r--src/modules/extra/m_pgsql.cpp694
1 files changed, 0 insertions, 694 deletions
diff --git a/src/modules/extra/m_pgsql.cpp b/src/modules/extra/m_pgsql.cpp
deleted file mode 100644
index 5ff50c765..000000000
--- a/src/modules/extra/m_pgsql.cpp
+++ /dev/null
@@ -1,694 +0,0 @@
-/*
- * InspIRCd -- Internet Relay Chat Daemon
- *
- * Copyright (C) 2015 Daniel Vassdal <shutter@canternet.org>
- * Copyright (C) 2013, 2016-2017, 2019-2024 Sadie Powell <sadie@witchery.services>
- * Copyright (C) 2012-2015 Attila Molnar <attilamolnar@hush.com>
- * Copyright (C) 2012 Robby <robby@chatbelgie.be>
- * Copyright (C) 2009-2010 Daniel De Graaf <danieldg@inspircd.org>
- * Copyright (C) 2008 Thomas Stagner <aquanight@gmail.com>
- * Copyright (C) 2007, 2009 Dennis Friis <peavey@inspircd.org>
- * Copyright (C) 2007, 2009 Craig Edwards <brain@inspircd.org>
- * Copyright (C) 2007 Robin Burchell <robin+git@viroteck.net>
- * Copyright (C) 2006 Oliver Lupton <om@inspircd.org>
- *
- * This file is part of InspIRCd. InspIRCd is free software: you can
- * redistribute it and/or modify it under the terms of the GNU General Public
- * License as published by the Free Software Foundation, version 2.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
- * details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-/// $CompilerFlags: find_compiler_flags("libpq")
-/// $LinkerFlags: find_linker_flags("libpq")
-
-/// $PackageInfo: require_system("alpine") pcre2-dev pkgconf
-/// $PackageInfo: require_system("arch") pkgconf postgresql-libs
-/// $PackageInfo: require_system("darwin") libpq pkg-config
-/// $PackageInfo: require_system("debian~") libpq-dev pkg-config
-/// $PackageInfo: require_system("rhel~") pkg-config postgresql-devel
-/// $PackageInfo: require_system("ubuntu") libpq-dev pkg-config
-
-
-#include <libpq-fe.h>
-#include <pg_config.h>
-
-#include "inspircd.h"
-#include "modules/sql.h"
-#include "utility/string.h"
-
-/* SQLConn rewritten by peavey to
- * use EventHandler instead of
- * BufferedSocket. This is much neater
- * and gives total control of destroy
- * and delete of resources.
- */
-
-/* Forward declare, so we can have the typedef neatly at the top */
-class SQLConn;
-class ModulePgSQL;
-
-typedef insp::flat_map<std::string, SQLConn*> ConnMap;
-
-enum SQLstatus
-{
- // The connection has died.
- DEAD,
-
- // Connecting and wants read event.
- CREAD,
-
- // Connecting and wants write event.
- CWRITE,
-
- // Connected/working and wants read event.
- WREAD,
-
- // Connected/working and wants write event.
- WWRITE
-};
-
-class ReconnectTimer final
- : public Timer
-{
-private:
- ModulePgSQL* mod;
-public:
- ReconnectTimer(ModulePgSQL* m)
- : Timer(5, false)
- , mod(m)
- {
- }
- bool Tick() override;
-};
-
-struct QueueItem final
-{
- SQL::Query* c;
- std::string q;
- QueueItem(SQL::Query* C, const std::string& Q)
- : c(C)
- , q(Q)
- {
- }
-};
-
-/** PgSQLresult is a subclass of the mostly-pure-virtual class SQLresult.
- * All SQL providers must create their own subclass and define it's methods using that
- * database library's data retrieval functions. The aim is to avoid a slow and inefficient process
- * of converting all data to a common format before it reaches the result structure. This way
- * data is passes to the module nearly as directly as if it was using the API directly itself.
- */
-
-class PgSQLresult final
- : public SQL::Result
-{
- PGresult* res;
- int currentrow = 0;
- int rows = 0;
- std::vector<std::string> colnames;
-
- void getColNames()
- {
- colnames.resize(PQnfields(res));
- for(unsigned int i=0; i < colnames.size(); i++)
- {
- colnames[i] = PQfname(res, i);
- }
- }
-public:
- PgSQLresult(PGresult* result)
- : res(result)
- {
- rows = PQntuples(res);
- if (!rows)
- rows = ConvToNum<int>(PQcmdTuples(res));
- }
-
- ~PgSQLresult() override
- {
- PQclear(res);
- }
-
- int Rows() override
- {
- return rows;
- }
-
- void GetCols(std::vector<std::string>& result) override
- {
- if (colnames.empty())
- getColNames();
- result = colnames;
- }
-
- bool HasColumn(const std::string& column, size_t& index) override
- {
- if (colnames.empty())
- getColNames();
-
- for (size_t i = 0; i < colnames.size(); ++i)
- {
- if (colnames[i] == column)
- {
- index = i;
- return true;
- }
- }
- return false;
- }
-
- SQL::Field GetValue(int row, int column)
- {
- char* v = PQgetvalue(res, row, column);
- if (!v || PQgetisnull(res, row, column))
- return std::nullopt;
-
- return std::string(v, PQgetlength(res, row, column));
- }
-
- bool GetRow(SQL::Row& result) override
- {
- if (currentrow >= PQntuples(res))
- return false;
- int ncols = PQnfields(res);
-
- for(int i = 0; i < ncols; i++)
- {
- result.push_back(GetValue(currentrow, i));
- }
- currentrow++;
-
- return true;
- }
-};
-
-/** SQLConn represents one SQL session.
- */
-class SQLConn final
- : public SQL::Provider
- , public EventHandler
-{
-public:
- std::shared_ptr<ConfigTag> conf; /* The <database> entry */
- std::deque<QueueItem> queue;
- PGconn* sql = nullptr; /* PgSQL database connection handle */
- SQLstatus status = CWRITE; /* PgSQL database connection status */
- QueueItem qinprog; /* If there is currently a query in progress */
-
- SQLConn(Module* Creator, const std::shared_ptr<ConfigTag>& tag)
- : SQL::Provider(Creator, tag->getString("id"))
- , conf(tag)
- , qinprog(nullptr, "")
- {
- if (!DoConnect())
- DelayReconnect();
- }
-
- Cullable::Result Cull() override
- {
- this->SQL::Provider::Cull();
- ServerInstance->Modules.DelService(*this);
- return this->EventHandler::Cull();
- }
-
- ~SQLConn() override
- {
- SQL::Error err(SQL::BAD_DBID);
- if (qinprog.c)
- {
- qinprog.c->OnError(err);
- delete qinprog.c;
- }
- for (const auto& item : queue)
- {
- SQL::Query* q = item.c;
- q->OnError(err);
- delete q;
- }
- Close();
- }
-
- void OnEventHandlerRead() override
- {
- DoEvent();
- }
-
- void OnEventHandlerWrite() override
- {
- DoEvent();
- }
-
- void OnEventHandlerError(int errornum) override
- {
- DelayReconnect();
- }
-
- std::string EscapeDSN(const std::string& str)
- {
- std::string out;
- out.reserve(str.size());
-
- for (std::string::const_iterator it = str.begin(); it != str.end(); ++it)
- {
- char chr = *it;
- switch (chr)
- {
- case '\\':
- out.append("\\\\");
- break;
-
- case '\'':
- out.append("\\'");
- break;
-
- default:
- out.push_back(chr);
- break;
- }
- }
-
- return out;
- }
-
- std::string GetDSN()
- {
- std::ostringstream conninfo("connect_timeout = '5'");
- std::string item;
-
- if (conf->readString("host", item))
- conninfo << " host = '" << EscapeDSN(item) << "'";
-
- if (conf->readString("port", item))
- conninfo << " port = '" << EscapeDSN(item) << "'";
-
- if (conf->readString("name", item))
- conninfo << " dbname = '" << EscapeDSN(item) << "'";
-
- if (conf->readString("user", item))
- conninfo << " user = '" << EscapeDSN(item) << "'";
-
- if (conf->readString("pass", item))
- conninfo << " password = '" << EscapeDSN(item) << "'";
-
- if (conf->getBool("tls", conf->getBool("ssl", true)))
- conninfo << " sslmode = 'require'";
- else
- conninfo << " sslmode = 'disable'";
-
- return conninfo.str();
- }
-
- bool HandleConnectError(const char* reason)
- {
- ServerInstance->Logs.Critical(MODNAME, "Could not connect to the \"{}\" database: {}",
- GetId(), reason);
- return false;
- }
-
- bool DoConnect()
- {
- sql = PQconnectStart(GetDSN().c_str());
- if (!sql)
- return HandleConnectError("PQconnectStart returned NULL");
-
- if(PQstatus(sql) == CONNECTION_BAD)
- return HandleConnectError("connection status is bad");
-
- if(PQsetnonblocking(sql, 1) == -1)
- return HandleConnectError("unable to mark fd as non-blocking");
-
- /* OK, we've initialised the connection, now to get it hooked into the socket engine
- * and then start polling it.
- */
- SetFd(PQsocket(sql));
- if(!HasFd())
- return HandleConnectError("PQsocket returned an invalid fd");
-
- if (!SocketEngine::AddFd(this, FD_WANT_NO_WRITE | FD_WANT_NO_READ))
- return HandleConnectError("could not add the pgsql socket to the socket engine");
-
- /* Socket all hooked into the engine, now to tell PgSQL to start connecting */
- if (!DoPoll())
- return HandleConnectError("could not poll the connection state");
-
- return true;
- }
-
- bool DoPoll()
- {
- switch(PQconnectPoll(sql))
- {
- case PGRES_POLLING_WRITING:
- SocketEngine::ChangeEventMask(this, FD_WANT_POLL_WRITE | FD_WANT_NO_READ);
- status = CWRITE;
- return true;
- case PGRES_POLLING_READING:
- SocketEngine::ChangeEventMask(this, FD_WANT_POLL_READ | FD_WANT_NO_WRITE);
- status = CREAD;
- return true;
- case PGRES_POLLING_FAILED:
- SocketEngine::ChangeEventMask(this, FD_WANT_NO_READ | FD_WANT_NO_WRITE);
- status = DEAD;
- return false;
- case PGRES_POLLING_OK:
- SocketEngine::ChangeEventMask(this, FD_WANT_POLL_READ | FD_WANT_NO_WRITE);
- status = WWRITE;
- DoConnectedPoll();
- return true;
- default:
- return true;
- }
- }
-
- void DoConnectedPoll()
- {
-restart:
- while (qinprog.q.empty() && !queue.empty())
- {
- /* There's no query currently in progress, and there's queries in the queue. */
- DoQuery(queue.front());
- queue.pop_front();
- }
-
- if (PQconsumeInput(sql))
- {
- if (PQisBusy(sql))
- {
- /* Nothing happens here */
- }
- else if (qinprog.c)
- {
- /* Fetch the result.. */
- PGresult* result = PQgetResult(sql);
-
- /* PgSQL would allow a query string to be sent which has multiple
- * queries in it, this isn't portable across database backends and
- * we don't want modules doing it. But just in case we make sure we
- * drain any results there are and just use the last one.
- * If the module devs are behaving there will only be one result.
- */
- while (PGresult* temp = PQgetResult(sql))
- {
- PQclear(result);
- result = temp;
- }
-
- /* ..and the result */
- PgSQLresult reply(result);
- switch(PQresultStatus(result))
- {
- case PGRES_EMPTY_QUERY:
- case PGRES_BAD_RESPONSE:
- case PGRES_FATAL_ERROR:
- {
- SQL::Error err(SQL::QREPLY_FAIL, PQresultErrorMessage(result));
- qinprog.c->OnError(err);
- break;
- }
- default:
- /* Other values are not errors */
- qinprog.c->OnResult(reply);
- }
-
- delete qinprog.c;
- qinprog = QueueItem(nullptr, "");
- goto restart;
- }
- else
- {
- qinprog.q.clear();
- }
- }
- else
- {
- /* I think we'll assume this means the server died...it might not,
- * but I think that any error serious enough we actually get here
- * deserves to reconnect [/excuse]
- * Returning true so the core doesn't try and close the connection.
- */
- DelayReconnect();
- }
- }
-
- void DelayReconnect();
-
- void DoEvent()
- {
- if((status == CREAD) || (status == CWRITE))
- {
- DoPoll();
- }
- else if (status == WREAD || status == WWRITE)
- {
- DoConnectedPoll();
- }
- }
-
- void Submit(SQL::Query* req, const std::string& q) override
- {
- ServerInstance->Logs.Debug(MODNAME, "Executing PostgreSQL query: " + q);
- if (qinprog.q.empty())
- {
- DoQuery(QueueItem(req, q));
- }
- else
- {
- // wait your turn.
- queue.emplace_back(req, q);
- }
- }
-
- void Submit(SQL::Query* req, const std::string& q, const SQL::ParamList& p) override
- {
- std::string res;
- unsigned int param = 0;
- for (const auto chr : q)
- {
- if (chr != '?')
- res.push_back(chr);
- else
- {
- if (param < p.size())
- {
- std::string parm = p[param++];
- std::vector<char> buffer(parm.length() * 2 + 1);
- int error;
- size_t escapedsize = PQescapeStringConn(sql, buffer.data(), parm.data(), parm.length(), &error);
- if (error)
- ServerInstance->Logs.Debug(MODNAME, "BUG: Apparently PQescapeStringConn() failed");
- res.append(buffer.data(), escapedsize);
- }
- }
- }
- Submit(req, res);
- }
-
- void Submit(SQL::Query* req, const std::string& q, const SQL::ParamMap& p) override
- {
- std::string res;
- for(std::string::size_type i = 0; i < q.length(); i++)
- {
- if (q[i] != '$')
- res.push_back(q[i]);
- else
- {
- std::string field;
- i++;
- while (i < q.length() && isalnum(q[i]))
- field.push_back(q[i++]);
- i--;
-
- SQL::ParamMap::const_iterator it = p.find(field);
- if (it != p.end())
- {
- std::string parm = it->second;
- std::vector<char> buffer(parm.length() * 2 + 1);
- int error;
- size_t escapedsize = PQescapeStringConn(sql, buffer.data(), parm.data(), parm.length(), &error);
- if (error)
- ServerInstance->Logs.Debug(MODNAME, "BUG: Apparently PQescapeStringConn() failed");
- res.append(buffer.data(), escapedsize);
- }
- }
- }
- Submit(req, res);
- }
-
- void DoQuery(const QueueItem& req)
- {
- if (status != WREAD && status != WWRITE)
- {
- // whoops, not connected...
- SQL::Error err(SQL::BAD_CONN);
- req.c->OnError(err);
- delete req.c;
- return;
- }
-
- if(PQsendQuery(sql, req.q.c_str()))
- {
- qinprog = req;
- }
- else
- {
- SQL::Error err(SQL::QSEND_FAIL, PQerrorMessage(sql));
- req.c->OnError(err);
- delete req.c;
- }
- }
-
- void Close()
- {
- status = DEAD;
-
- if (HasFd() && SocketEngine::HasFd(GetFd()))
- SocketEngine::DelFd(this);
-
- if(sql)
- {
- PQfinish(sql);
- sql = nullptr;
- }
- }
-};
-
-class ModulePgSQL final
- : public Module
-{
-public:
- ConnMap connections;
- ReconnectTimer* retimer = nullptr;
-
- ModulePgSQL()
- : Module(VF_VENDOR, "Provides the ability for SQL modules to query a PostgreSQL database.")
- {
- }
-
- ~ModulePgSQL() override
- {
- delete retimer;
- ClearAllConnections();
- }
-
- void init() override
- {
- int version = PQlibVersion();
- int minor = version / 100 % 100;
- int revision = version % 100;
- if (version >= 10'00'00)
- {
- // Ref: https://www.postgresql.org/docs/current/libpq-misc.html#LIBPQ-PQLIBVERSION
- minor = revision;
- revision = 0;
- }
- ServerInstance->Logs.Normal(MODNAME, "Module was compiled against libpq version {} and is running against version {}.{}.{}",
- PG_VERSION, version / 10000, minor, revision);
- }
-
- void ReadConfig(ConfigStatus& status) override
- {
- ReadConf();
- }
-
- void ReadConf()
- {
- ConnMap conns;
-
- for (const auto& [_, tag] : ServerInstance->Config->ConfTags("database"))
- {
- if (!insp::equalsci(tag->getString("module"), "pgsql"))
- continue;
-
- std::string id = tag->getString("id");
- ConnMap::iterator curr = connections.find(id);
- if (curr == connections.end())
- {
- auto* conn = new SQLConn(this, tag);
- if (conn->status != DEAD)
- {
- conns.emplace(id, conn);
- ServerInstance->Modules.AddService(*conn);
- }
- // If the connection is dead it has already been queued for culling
- // at the end of the main loop so we don't need to delete it here.
- }
- else
- {
- conns.insert(*curr);
- connections.erase(curr);
- }
- }
- ClearAllConnections();
- conns.swap(connections);
- }
-
- void ClearAllConnections()
- {
- for (const auto& [_, conn] : connections)
- {
- conn->Cull();
- delete conn;
- }
- connections.clear();
- }
-
- void OnUnloadModule(Module* mod) override
- {
- SQL::Error err(SQL::BAD_DBID);
- for (const auto& [_, conn] : connections)
- {
- if (conn->qinprog.c && conn->qinprog.c->creator == mod)
- {
- conn->qinprog.c->OnError(err);
- delete conn->qinprog.c;
- conn->qinprog.c = nullptr;
- }
- std::deque<QueueItem>::iterator j = conn->queue.begin();
- while (j != conn->queue.end())
- {
- SQL::Query* q = j->c;
- if (q->creator == mod)
- {
- q->OnError(err);
- delete q;
- j = conn->queue.erase(j);
- }
- else
- j++;
- }
- }
- }
-};
-
-bool ReconnectTimer::Tick()
-{
- mod->retimer = nullptr;
- mod->ReadConf();
- delete this;
- return false;
-}
-
-void SQLConn::DelayReconnect()
-{
- status = DEAD;
- ModulePgSQL* mod = (ModulePgSQL*)(Module*)creator;
-
- ConnMap::iterator it = mod->connections.find(conf->getString("id"));
- if (it != mod->connections.end())
- mod->connections.erase(it);
- ServerInstance->GlobalCulls.AddItem((EventHandler*)this);
- if (!mod->retimer)
- {
- mod->retimer = new ReconnectTimer(mod);
- ServerInstance->Timers.AddTimer(mod->retimer);
- }
-}
-
-MODULE_INIT(ModulePgSQL)