1 /* +------------------------------------+
2 * | Inspire Internet Relay Chat Daemon |
3 * +------------------------------------+
5 * InspIRCd: (C) 2002-2008 InspIRCd Development Team
6 * See: http://www.inspircd.org/wiki/index.php/Credits
8 * This program is free but copyrighted software; see
9 * the file COPYING for details.
11 * ---------------------------------------------------
21 #include "configreader.h"
24 /* $ModDesc: PostgreSQL Service Provider module for all other m_sql* modules, uses v2 of the SQL API */
25 /* $CompileFlags: -Iexec("pg_config --includedir") eval("my $s = `pg_config --version`;$s =~ /^.*?(\d+)\.(\d+)\.(\d+).*?$/;my $v = hex(sprintf("0x%02x%02x%02x", $1, $2, $3));print "-DPGSQL_HAS_ESCAPECONN" if(($v >= 0x080104) || ($v >= 0x07030F && $v < 0x070400) || ($v >= 0x07040D && $v < 0x080000) || ($v >= 0x080008 && $v < 0x080100));") */
26 /* $LinkerFlags: -Lexec("pg_config --libdir") -lpq */
27 /* $ModDep: m_sqlv2.h */
30 /* SQLConn rewritten by peavey to
31 * use EventHandler instead of
32 * BufferedSocket. This is much neater
33 * and gives total control of destroy
34 * and delete of resources.
37 /* Forward declare, so we can have the typedef neatly at the top */
40 typedef std::map<std::string, SQLConn*> ConnMap;
42 /* CREAD, Connecting and wants read event
43 * CWRITE, Connecting and wants write event
44 * WREAD, Connected/Working and wants read event
45 * WWRITE, Connected/Working and wants write event
46 * RREAD, Resetting and wants read event
47 * RWRITE, Resetting and wants write event
49 enum SQLstatus { CREAD, CWRITE, WREAD, WWRITE, RREAD, RWRITE };
51 /** SQLhost::GetDSN() - Overload to return correct DSN for PostgreSQL
53 std::string SQLhost::GetDSN()
55 std::ostringstream conninfo("connect_timeout = '2'");
58 conninfo << " hostaddr = '" << ip << "'";
61 conninfo << " port = '" << port << "'";
64 conninfo << " dbname = '" << name << "'";
67 conninfo << " user = '" << user << "'";
70 conninfo << " password = '" << pass << "'";
74 conninfo << " sslmode = 'require'";
78 conninfo << " sslmode = 'disable'";
81 return conninfo.str();
84 class ReconnectTimer : public Timer
89 ReconnectTimer(InspIRCd* SI, Module* m)
90 : Timer(5, SI->Time(), false), mod(m)
93 virtual void Tick(time_t TIME);
97 /** Used to resolve sql server hostnames
99 class SQLresolver : public Resolver
105 SQLresolver(Module* m, InspIRCd* Instance, const SQLhost& hi, bool &cached)
106 : Resolver(Instance, hi.host, DNS_QUERY_FORWARD, cached, (Module*)m), host(hi), mod(m)
110 virtual void OnLookupComplete(const std::string &result, unsigned int ttl, bool cached, int resultnum = 0);
112 virtual void OnError(ResolverError e, const std::string &errormessage)
114 ServerInstance->Logs->Log("m_pgsql",DEBUG, "PgSQL: DNS lookup failed (%s), dying horribly", errormessage.c_str());
118 /** PgSQLresult is a subclass of the mostly-pure-virtual class SQLresult.
119 * All SQL providers must create their own subclass and define it's methods using that
120 * database library's data retriveal functions. The aim is to avoid a slow and inefficient process
121 * of converting all data to a common format before it reaches the result structure. This way
122 * data is passes to the module nearly as directly as if it was using the API directly itself.
125 class PgSQLresult : public SQLresult
132 SQLfieldList* fieldlist;
133 SQLfieldMap* fieldmap;
135 PgSQLresult(Module* self, Module* to, unsigned long rid, PGresult* result)
136 : SQLresult(self, to, rid), res(result), currentrow(0), fieldlist(NULL), fieldmap(NULL)
138 rows = PQntuples(res);
139 cols = PQnfields(res);
144 /* If we allocated these, free them... */
158 return atoi(PQcmdTuples(res));
168 return PQnfields(res);
171 virtual std::string ColName(int column)
173 char* name = PQfname(res, column);
175 return (name) ? name : "";
178 virtual int ColNum(const std::string &column)
180 int n = PQfnumber(res, column.c_str());
184 throw SQLbadColName();
192 virtual SQLfield GetValue(int row, int column)
194 char* v = PQgetvalue(res, row, column);
198 return SQLfield(std::string(v, PQgetlength(res, row, column)), PQgetisnull(res, row, column));
202 throw SQLbadColName();
206 virtual SQLfieldList& GetRow()
208 /* In an effort to reduce overhead we don't actually allocate the list
209 * until the first time it's needed...so...
217 fieldlist = new SQLfieldList;
220 if(currentrow < PQntuples(res))
222 int ncols = PQnfields(res);
224 for(int i = 0; i < ncols; i++)
226 fieldlist->push_back(GetValue(currentrow, i));
235 virtual SQLfieldMap& GetRowMap()
237 /* In an effort to reduce overhead we don't actually allocate the map
238 * until the first time it's needed...so...
246 fieldmap = new SQLfieldMap;
249 if(currentrow < PQntuples(res))
251 int ncols = PQnfields(res);
253 for(int i = 0; i < ncols; i++)
255 fieldmap->insert(std::make_pair(ColName(i), GetValue(currentrow, i)));
264 virtual SQLfieldList* GetRowPtr()
266 SQLfieldList* fl = new SQLfieldList;
268 if(currentrow < PQntuples(res))
270 int ncols = PQnfields(res);
272 for(int i = 0; i < ncols; i++)
274 fl->push_back(GetValue(currentrow, i));
283 virtual SQLfieldMap* GetRowMapPtr()
285 SQLfieldMap* fm = new SQLfieldMap;
287 if(currentrow < PQntuples(res))
289 int ncols = PQnfields(res);
291 for(int i = 0; i < ncols; i++)
293 fm->insert(std::make_pair(ColName(i), GetValue(currentrow, i)));
302 virtual void Free(SQLfieldMap* fm)
307 virtual void Free(SQLfieldList* fl)
313 /** SQLConn represents one SQL session.
315 class SQLConn : public EventHandler
319 SQLhost confhost; /* The <database> entry */
320 Module* us; /* Pointer to the SQL provider itself */
321 PGconn* sql; /* PgSQL database connection handle */
322 SQLstatus status; /* PgSQL database connection status */
323 bool qinprog; /* If there is currently a query in progress */
324 QueryQueue queue; /* Queue of queries waiting to be executed on this connection */
325 time_t idle; /* Time we last heard from the database */
328 SQLConn(InspIRCd* SI, Module* self, const SQLhost& hi)
329 : EventHandler(), Instance(SI), confhost(hi), us(self), sql(NULL), status(CWRITE), qinprog(false)
331 idle = this->Instance->Time();
334 Instance->Logs->Log("m_pgsql",DEFAULT, "WARNING: Could not connect to database with id: " + ConvToStr(hi.id));
344 virtual void HandleEvent(EventType et, int errornum)
367 if(!(sql = PQconnectStart(confhost.GetDSN().c_str())))
370 if(PQstatus(sql) == CONNECTION_BAD)
373 if(PQsetnonblocking(sql, 1) == -1)
376 /* OK, we've initalised the connection, now to get it hooked into the socket engine
377 * and then start polling it.
379 this->fd = PQsocket(sql);
384 if (!this->Instance->SE->AddFd(this))
386 Instance->Logs->Log("m_pgsql",DEBUG, "BUG: Couldn't add pgsql socket to socket engine");
390 /* Socket all hooked into the engine, now to tell PgSQL to start connecting */
396 switch(PQconnectPoll(sql))
398 case PGRES_POLLING_WRITING:
399 Instance->SE->WantWrite(this);
402 case PGRES_POLLING_READING:
405 case PGRES_POLLING_FAILED:
407 case PGRES_POLLING_OK:
409 return DoConnectedPoll();
415 bool DoConnectedPoll()
417 if(!qinprog && queue.totalsize())
419 /* There's no query currently in progress, and there's queries in the queue. */
420 SQLrequest& query = queue.front();
424 if(PQconsumeInput(sql))
426 /* We just read stuff from the server, that counts as it being alive
427 * so update the idle-since time :p
429 idle = this->Instance->Time();
433 /* Nothing happens here */
437 /* Grab the request we're processing */
438 SQLrequest& query = queue.front();
440 /* Get a pointer to the module we're about to return the result to */
441 Module* to = query.GetSource();
443 /* Fetch the result.. */
444 PGresult* result = PQgetResult(sql);
446 /* PgSQL would allow a query string to be sent which has multiple
447 * queries in it, this isn't portable across database backends and
448 * we don't want modules doing it. But just in case we make sure we
449 * drain any results there are and just use the last one.
450 * If the module devs are behaving there will only be one result.
452 while (PGresult* temp = PQgetResult(sql))
460 /* ..and the result */
461 PgSQLresult reply(us, to, query.id, result);
463 /* Fix by brain, make sure the original query gets sent back in the reply */
464 reply.query = query.query.q;
466 switch(PQresultStatus(result))
468 case PGRES_EMPTY_QUERY:
469 case PGRES_BAD_RESPONSE:
470 case PGRES_FATAL_ERROR:
471 reply.error.Id(QREPLY_FAIL);
472 reply.error.Str(PQresultErrorMessage(result));
474 /* No action, other values are not errors */
479 /* PgSQLresult's destructor will free the PGresult */
483 /* If the client module is unloaded partway through a query then the provider will set
484 * the pointer to NULL. We cannot just cancel the query as the result will still come
485 * through at some point...and it could get messy if we play with invalid pointers...
497 /* I think we'll assume this means the server died...it might not,
498 * but I think that any error serious enough we actually get here
499 * deserves to reconnect [/excuse]
500 * Returning true so the core doesn't try and close the connection.
509 switch(PQresetPoll(sql))
511 case PGRES_POLLING_WRITING:
512 Instance->SE->WantWrite(this);
515 case PGRES_POLLING_READING:
518 case PGRES_POLLING_FAILED:
520 case PGRES_POLLING_OK:
522 return DoConnectedPoll();
530 /* Always return true here, false would close the socket - we need to do that ourselves with the pgsql API */
536 /* Always return true here, false would close the socket - we need to do that ourselves with the pgsql API */
545 void DelayReconnect();
551 if((status == CREAD) || (status == CWRITE))
555 else if((status == RREAD) || (status == RWRITE))
561 ret = DoConnectedPoll();
566 SQLerror DoQuery(SQLrequest &req)
568 if((status == WREAD) || (status == WWRITE))
572 /* Parse the command string and dispatch it */
574 /* Pointer to the buffer we screw around with substitution in */
576 /* Pointer to the current end of query, where we append new stuff */
578 /* Total length of the unescaped parameters */
579 unsigned int paramlen;
583 for(ParamL::iterator i = req.query.p.begin(); i != req.query.p.end(); i++)
585 paramlen += i->size();
588 /* To avoid a lot of allocations, allocate enough memory for the biggest the escaped query could possibly be.
589 * sizeofquery + (totalparamlength*2) + 1
591 * The +1 is for null-terminating the string for PQsendQuery()
594 query = new char[req.query.q.length() + (paramlen*2) + 1];
597 /* Okay, now we have a buffer large enough we need to start copying the query into it and escaping and substituting
598 * the parameters into it...
601 for(unsigned int i = 0; i < req.query.q.length(); i++)
603 if(req.query.q[i] == '?')
605 /* We found a place to substitute..what fun.
606 * Use the PgSQL calls to escape and write the
607 * escaped string onto the end of our query buffer,
608 * then we "just" need to make sure queryend is
609 * pointing at the right place.
612 if(req.query.p.size())
617 #ifdef PGSQL_HAS_ESCAPECONN
618 len = PQescapeStringConn(sql, queryend, req.query.p.front().c_str(), req.query.p.front().length(), &error);
620 len = PQescapeString (queryend, req.query.p.front().c_str(), req.query.p.front().length());
624 Instance->Logs->Log("m_pgsql",DEBUG, "BUG: Apparently PQescapeStringConn() failed somehow...don't know how or what to do...");
627 /* Incremenet queryend to the end of the newly escaped parameter */
630 /* Remove the parameter we just substituted in */
631 req.query.p.pop_front();
635 Instance->Logs->Log("m_pgsql",DEBUG, "BUG: Found a substitution location but no parameter to substitute :|");
641 *queryend = req.query.q[i];
646 /* Null-terminate the query */
650 if(PQsendQuery(sql, query))
659 return SQLerror(QSEND_FAIL, PQerrorMessage(sql));
663 return SQLerror(BAD_CONN, "Can't query until connection is complete");
666 SQLerror Query(const SQLrequest &req)
670 if(!qinprog && queue.totalsize())
672 /* There's no query currently in progress, and there's queries in the queue. */
673 SQLrequest& query = queue.front();
674 return DoQuery(query);
682 void OnUnloadModule(Module* mod)
684 queue.PurgeModule(mod);
687 const SQLhost GetConfHost()
693 if (!this->Instance->SE->DelFd(this))
695 if (sql && PQstatus(sql) == CONNECTION_BAD)
697 this->Instance->SE->DelFd(this, true);
701 Instance->Logs->Log("m_pgsql",DEBUG, "BUG: PQsocket cant be removed from socket engine!");
714 class ModulePgSQL : public Module
718 unsigned long currid;
720 ReconnectTimer* retimer;
723 ModulePgSQL(InspIRCd* Me)
724 : Module::Module(Me), currid(0)
726 ServerInstance->Modules->UseInterface("SQLutils");
728 sqlsuccess = new char[strlen(SQLSUCCESS)+1];
730 strlcpy(sqlsuccess, SQLSUCCESS, strlen(SQLSUCCESS));
732 if (!ServerInstance->Modules->PublishFeature("SQL", this))
734 throw ModuleException("BUG: PgSQL Unable to publish feature 'SQL'");
739 ServerInstance->Modules->PublishInterface("SQL", this);
740 Implementation eventlist[] = { I_OnUnloadModule, I_OnRequest, I_OnRehash, I_OnUserRegister, I_OnCheckReady, I_OnUserDisconnect };
741 ServerInstance->Modules->Attach(eventlist, this, 6);
744 virtual ~ModulePgSQL()
747 ServerInstance->Timers->DelTimer(retimer);
748 ClearAllConnections();
750 ServerInstance->Modules->UnpublishInterface("SQL", this);
751 ServerInstance->Modules->UnpublishFeature("SQL");
752 ServerInstance->Modules->DoneWithInterface("SQLutils");
756 virtual void OnRehash(User* user, const std::string ¶meter)
761 bool HasHost(const SQLhost &host)
763 for (ConnMap::iterator iter = connections.begin(); iter != connections.end(); iter++)
765 if (host == iter->second->GetConfHost())
771 bool HostInConf(const SQLhost &h)
773 ConfigReader conf(ServerInstance);
774 for(int i = 0; i < conf.Enumerate("database"); i++)
777 host.id = conf.ReadValue("database", "id", i);
778 host.host = conf.ReadValue("database", "hostname", i);
779 host.port = conf.ReadInteger("database", "port", i, true);
780 host.name = conf.ReadValue("database", "name", i);
781 host.user = conf.ReadValue("database", "username", i);
782 host.pass = conf.ReadValue("database", "password", i);
783 host.ssl = conf.ReadFlag("database", "ssl", "0", i);
792 ClearOldConnections();
794 ConfigReader conf(ServerInstance);
795 for(int i = 0; i < conf.Enumerate("database"); i++)
800 host.id = conf.ReadValue("database", "id", i);
801 host.host = conf.ReadValue("database", "hostname", i);
802 host.port = conf.ReadInteger("database", "port", i, true);
803 host.name = conf.ReadValue("database", "name", i);
804 host.user = conf.ReadValue("database", "username", i);
805 host.pass = conf.ReadValue("database", "password", i);
806 host.ssl = conf.ReadFlag("database", "ssl", "0", i);
812 if (strchr(host.host.c_str(),':'))
815 ipvalid = inet_pton(AF_INET6, host.host.c_str(), &blargle);
821 ipvalid = inet_aton(host.host.c_str(), &blargle);
826 /* The conversion succeeded, we were given an IP and we can give it straight to SQLConn */
830 else if(ipvalid == 0)
832 /* Conversion failed, assume it's a host */
833 SQLresolver* resolver;
838 resolver = new SQLresolver(this, ServerInstance, host, cached);
839 ServerInstance->AddResolver(resolver, cached);
843 /* THE WORLD IS COMING TO AN END! */
848 /* Invalid address family, die horribly. */
849 ServerInstance->Logs->Log("m_pgsql",DEBUG, "BUG: insp_aton failed returning -1, oh noes.");
854 void ClearOldConnections()
856 ConnMap::iterator iter,safei;
857 for (iter = connections.begin(); iter != connections.end(); iter++)
859 if (!HostInConf(iter->second->GetConfHost()))
864 connections.erase(safei);
869 void ClearAllConnections()
872 while ((i = connections.begin()) != connections.end())
874 connections.erase(i);
879 void AddConn(const SQLhost& hi)
883 ServerInstance->Logs->Log("m_pgsql",DEFAULT, "WARNING: A pgsql connection with id: %s already exists, possibly due to DNS delay. Aborting connection attempt.", hi.id.c_str());
889 /* The conversion succeeded, we were given an IP and we can give it straight to SQLConn */
890 newconn = new SQLConn(ServerInstance, this, hi);
892 connections.insert(std::make_pair(hi.id, newconn));
895 void ReconnectConn(SQLConn* conn)
897 for (ConnMap::iterator iter = connections.begin(); iter != connections.end(); iter++)
899 if (conn == iter->second)
902 connections.erase(iter);
906 retimer = new ReconnectTimer(ServerInstance, this);
907 ServerInstance->Timers->AddTimer(retimer);
910 virtual const char* OnRequest(Request* request)
912 if(strcmp(SQLREQID, request->GetId()) == 0)
914 SQLrequest* req = (SQLrequest*)request;
915 ConnMap::iterator iter;
916 if((iter = connections.find(req->dbid)) != connections.end())
920 req->error = iter->second->Query(*req);
922 return (req->error.Id() == NO_ERROR) ? sqlsuccess : NULL;
926 req->error.Id(BAD_DBID);
933 virtual void OnUnloadModule(Module* mod, const std::string& name)
935 /* When a module unloads we have to check all the pending queries for all our connections
936 * and set the Module* specifying where the query came from to NULL. If the query has already
937 * been dispatched then when it is processed it will be dropped if the pointer is NULL.
939 * If the queries we find are not already being executed then we can simply remove them immediately.
941 for(ConnMap::iterator iter = connections.begin(); iter != connections.end(); iter++)
943 iter->second->OnUnloadModule(mod);
947 unsigned long NewID()
955 virtual Version GetVersion()
957 return Version(1, 2, 0, 0, VF_VENDOR|VF_SERVICEPROVIDER, API_VERSION);
961 /* move this here to use AddConn, rather that than having the whole
962 * module above SQLConn, since this is buggin me right now :/
964 void SQLresolver::OnLookupComplete(const std::string &result, unsigned int ttl, bool cached, int resultnum)
969 ((ModulePgSQL*)mod)->AddConn(host);
970 ((ModulePgSQL*)mod)->ClearOldConnections();
974 void ReconnectTimer::Tick(time_t time)
976 ((ModulePgSQL*)mod)->ReadConf();
979 void SQLConn::DelayReconnect()
981 ((ModulePgSQL*)us)->ReconnectConn(this);
984 MODULE_INIT(ModulePgSQL)