1 /*
2 * InspIRCd -- Internet Relay Chat Daemon
3 *
4 * Copyright (C) 2019 linuxdaemon <linuxdaemon.irc@gmail.com>
5 * Copyright (C) 2016 Adam <Adam@anope.org>
6 * Copyright (C) 2015 Daniel Vassdal <shutter@canternet.org>
7 * Copyright (C) 2013-2014 Attila Molnar <attilamolnar@hush.com>
8 * Copyright (C) 2013, 2016-2021 Sadie Powell <sadie@witchery.services>
9 * Copyright (C) 2012 Robby <robby@chatbelgie.be>
10 * Copyright (C) 2012 ChrisTX <xpipe@hotmail.de>
11 * Copyright (C) 2009-2010 Daniel De Graaf <danieldg@inspircd.org>
12 * Copyright (C) 2009 Uli Schlachter <psychon@inspircd.org>
13 * Copyright (C) 2007, 2009 Dennis Friis <peavey@inspircd.org>
14 * Copyright (C) 2005, 2008-2010 Craig Edwards <brain@inspircd.org>
15 *
16 * This file is part of InspIRCd. InspIRCd is free software: you can
17 * redistribute it and/or modify it under the terms of the GNU General Public
18 * License as published by the Free Software Foundation, version 2.
19 *
20 * This program is distributed in the hope that it will be useful, but WITHOUT
21 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
22 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
23 * details.
24 *
25 * You should have received a copy of the GNU General Public License
26 * along with this program. If not, see <http://www.gnu.org/licenses/>.
27 */
28
29 /// $CompilerFlags: execute("mysql_config --include" "MYSQL_CXXFLAGS")
30 /// $LinkerFlags: execute("mysql_config --libs_r" "MYSQL_LDFLAGS" "-lmysqlclient")
31
32 /// $PackageInfo: require_system("arch") mariadb-libs
33 /// $PackageInfo: require_system("centos" "6.0" "6.99") mysql-devel
34 /// $PackageInfo: require_system("centos" "7.0") mariadb-devel
35 /// $PackageInfo: require_system("darwin") mysql-connector-c
36 /// $PackageInfo: require_system("debian") libmysqlclient-dev
37 /// $PackageInfo: require_system("ubuntu") libmysqlclient-dev
38
39 #ifdef __GNUC__
40 # pragma GCC diagnostic push
41 #endif
42
43 // Fix warnings about the use of `long long` on C++03.
44 #if defined __clang__
45 # pragma clang diagnostic ignored "-Wc++11-long-long"
46 #elif defined __GNUC__
47 # pragma GCC diagnostic ignored "-Wlong-long"
48 #endif
49
50 #include "inspircd.h"
51 #include <mysql.h>
52 #include "modules/sql.h"
53
54 #ifdef __GNUC__
55 # pragma GCC diagnostic pop
56 #endif
57
58 #ifdef _WIN32
59 # pragma comment(lib, "mysqlclient.lib")
60 #endif
61
62 /* VERSION 3 API: With nonblocking (threaded) requests */
63
64 /* THE NONBLOCKING MYSQL API!
65 *
66 * MySQL provides no nonblocking (asynchronous) API of its own, and its developers recommend
67 * that instead, you should thread your program. This is what i've done here to allow for
68 * asynchronous SQL requests via mysql. The way this works is as follows:
69 *
70 * The module spawns a thread via class Thread, and performs its mysql queries in this thread,
71 * using a queue with priorities. There is a mutex on either end which prevents two threads
72 * adjusting the queue at the same time, and crashing the ircd. Every 50 milliseconds, the
73 * worker thread wakes up, and checks if there is a request at the head of its queue.
74 * If there is, it processes this request, blocking the worker thread but leaving the ircd
75 * thread to go about its business as usual. During this period, the ircd thread is able
76 * to insert further pending requests into the queue.
77 *
78 * Once the processing of a request is complete, it is removed from the incoming queue to
79 * an outgoing queue, and initialized as a 'response'. The worker thread then signals the
80 * ircd thread (via a loopback socket) of the fact a result is available, by sending the
81 * connection ID through the connection.
82 *
83 * The ircd thread then mutexes the queue once more, reads the outbound response off the head
84 * of the queue, and sends it on its way to the original calling module.
85 *
86 * XXX: You might be asking "why doesnt it just send the response from within the worker thread?"
87 * The answer to this is simple. The majority of InspIRCd, and in fact most ircd's are not
88 * threadsafe. This module is designed to be threadsafe and is careful with its use of threads,
89 * however, if we were to call a module's OnRequest even from within a thread which was not the
90 * one the module was originally instantiated upon, there is a chance of all hell breaking loose
91 * if a module is ever put in a reentrant state (stack corruption could occur, crashes, data
92 * corruption, and worse, so DONT think about it until the day comes when InspIRCd is 100%
93 * guaranteed threadsafe!)
94 */
95
96 class SQLConnection;
97 class MySQLresult;
98 class DispatcherThread;
99
100 struct QueryQueueItem
101 {
102 // An SQL database which this query is executed on.
103 SQLConnection* connection;
104
105 // An object which handles the result of the query.
106 SQL::Query* query;
107
108 // The SQL query which is to be executed.
109 std::string querystr;
110
QueryQueueItemQueryQueueItem111 QueryQueueItem(SQL::Query* q, const std::string& s, SQLConnection* c)
112 : connection(c)
113 , query(q)
114 , querystr(s)
115 {
116 }
117 };
118
119 struct ResultQueueItem
120 {
121 // An object which handles the result of the query.
122 SQL::Query* query;
123
124 // The result returned from executing the MySQL query.
125 MySQLresult* result;
126
ResultQueueItemResultQueueItem127 ResultQueueItem(SQL::Query* q, MySQLresult* r)
128 : query(q)
129 , result(r)
130 {
131 }
132 };
133
134 typedef insp::flat_map<std::string, SQLConnection*> ConnMap;
135 typedef std::deque<QueryQueueItem> QueryQueue;
136 typedef std::deque<ResultQueueItem> ResultQueue;
137
138 /** MySQL module
139 * */
140 class ModuleSQL : public Module
141 {
142 public:
143 DispatcherThread* Dispatcher;
144 QueryQueue qq; // MUST HOLD MUTEX
145 ResultQueue rq; // MUST HOLD MUTEX
146 ConnMap connections; // main thread only
147
148 ModuleSQL();
149 void init() CXX11_OVERRIDE;
150 ~ModuleSQL();
151 void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE;
152 void OnUnloadModule(Module* mod) CXX11_OVERRIDE;
153 Version GetVersion() CXX11_OVERRIDE;
154 };
155
156 class DispatcherThread : public SocketThread
157 {
158 private:
159 ModuleSQL* const Parent;
160 public:
DispatcherThread(ModuleSQL * CreatorModule)161 DispatcherThread(ModuleSQL* CreatorModule) : Parent(CreatorModule) { }
~DispatcherThread()162 ~DispatcherThread() { }
163 void Run() CXX11_OVERRIDE;
164 void OnNotify() CXX11_OVERRIDE;
165 };
166
167 /** Represents a mysql result set
168 */
169 class MySQLresult : public SQL::Result
170 {
171 public:
172 SQL::Error err;
173 int currentrow;
174 int rows;
175 std::vector<std::string> colnames;
176 std::vector<SQL::Row> fieldlists;
177
MySQLresult(MYSQL_RES * res,int affected_rows)178 MySQLresult(MYSQL_RES* res, int affected_rows)
179 : err(SQL::SUCCESS)
180 , currentrow(0)
181 , rows(0)
182 {
183 if (affected_rows >= 1)
184 {
185 rows = affected_rows;
186 fieldlists.resize(rows);
187 }
188 unsigned int field_count = 0;
189 if (res)
190 {
191 MYSQL_ROW row;
192 int n = 0;
193 while ((row = mysql_fetch_row(res)))
194 {
195 if (fieldlists.size() < (unsigned int)rows+1)
196 {
197 fieldlists.resize(fieldlists.size()+1);
198 }
199 field_count = 0;
200 MYSQL_FIELD *fields = mysql_fetch_fields(res);
201 if(mysql_num_fields(res) == 0)
202 break;
203 if (fields && mysql_num_fields(res))
204 {
205 colnames.clear();
206 while (field_count < mysql_num_fields(res))
207 {
208 std::string a = (fields[field_count].name ? fields[field_count].name : "");
209 if (row[field_count])
210 fieldlists[n].push_back(SQL::Field(row[field_count]));
211 else
212 fieldlists[n].push_back(SQL::Field());
213 colnames.push_back(a);
214 field_count++;
215 }
216 n++;
217 }
218 rows++;
219 }
220 mysql_free_result(res);
221 }
222 }
223
MySQLresult(SQL::Error & e)224 MySQLresult(SQL::Error& e)
225 : err(e)
226 , currentrow(0)
227 , rows(0)
228 {
229
230 }
231
Rows()232 int Rows() CXX11_OVERRIDE
233 {
234 return rows;
235 }
236
GetCols(std::vector<std::string> & result)237 void GetCols(std::vector<std::string>& result) CXX11_OVERRIDE
238 {
239 result.assign(colnames.begin(), colnames.end());
240 }
241
HasColumn(const std::string & column,size_t & index)242 bool HasColumn(const std::string& column, size_t& index) CXX11_OVERRIDE
243 {
244 for (size_t i = 0; i < colnames.size(); ++i)
245 {
246 if (colnames[i] == column)
247 {
248 index = i;
249 return true;
250 }
251 }
252 return false;
253 }
254
GetValue(int row,int column)255 SQL::Field GetValue(int row, int column)
256 {
257 if ((row >= 0) && (row < rows) && (column >= 0) && (column < (int)fieldlists[row].size()))
258 {
259 return fieldlists[row][column];
260 }
261 return SQL::Field();
262 }
263
GetRow(SQL::Row & result)264 bool GetRow(SQL::Row& result) CXX11_OVERRIDE
265 {
266 if (currentrow < rows)
267 {
268 result.assign(fieldlists[currentrow].begin(), fieldlists[currentrow].end());
269 currentrow++;
270 return true;
271 }
272 else
273 {
274 result.clear();
275 return false;
276 }
277 }
278 };
279
280 /** Represents a connection to a mysql database
281 */
282 class SQLConnection : public SQL::Provider
283 {
284 private:
EscapeString(SQL::Query * query,const std::string & in,std::string & out)285 bool EscapeString(SQL::Query* query, const std::string& in, std::string& out)
286 {
287 // In the worst case each character may need to be encoded as using two bytes and one
288 // byte is the NUL terminator.
289 std::vector<char> buffer(in.length() * 2 + 1);
290
291 // The return value of mysql_escape_string() is either an error or the length of the
292 // encoded string not including the NUL terminator.
293 //
294 // Unfortunately, someone genius decided that mysql_escape_string should return an
295 // unsigned type even though -1 is returned on error so checking whether an error
296 // happened is a bit cursed.
297 unsigned long escapedsize = mysql_escape_string(&buffer[0], in.c_str(), in.length());
298 if (escapedsize == static_cast<unsigned long>(-1))
299 {
300 SQL::Error err(SQL::QSEND_FAIL, InspIRCd::Format("%u: %s", mysql_errno(connection), mysql_error(connection)));
301 query->OnError(err);
302 return false;
303 }
304
305 out.append(&buffer[0], escapedsize);
306 return true;
307 }
308
309 public:
310 reference<ConfigTag> config;
311 MYSQL *connection;
312 Mutex lock;
313
314 // This constructor creates an SQLConnection object with the given credentials, but does not connect yet.
SQLConnection(Module * p,ConfigTag * tag)315 SQLConnection(Module* p, ConfigTag* tag)
316 : SQL::Provider(p, tag->getString("id"))
317 , config(tag)
318 , connection(NULL)
319 {
320 }
321
~SQLConnection()322 ~SQLConnection()
323 {
324 mysql_close(connection);
325 }
326
327 // This method connects to the database using the credentials supplied to the constructor, and returns
328 // true upon success.
Connect()329 bool Connect()
330 {
331 connection = mysql_init(connection);
332
333 // Set the connection timeout.
334 unsigned int timeout = config->getDuration("timeout", 5, 1, 30);
335 mysql_options(connection, MYSQL_OPT_CONNECT_TIMEOUT, &timeout);
336
337 // Attempt to connect to the database.
338 const std::string host = config->getString("host");
339 const std::string user = config->getString("user");
340 const std::string pass = config->getString("pass");
341 const std::string dbname = config->getString("name");
342 unsigned int port = config->getUInt("port", 3306, 1, 65535);
343 if (!mysql_real_connect(connection, host.c_str(), user.c_str(), pass.c_str(), dbname.c_str(), port, NULL, CLIENT_IGNORE_SIGPIPE))
344 {
345 ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Unable to connect to the %s MySQL server: %s",
346 GetId().c_str(), mysql_error(connection));
347 return false;
348 }
349
350 // Set the default character set.
351 const std::string charset = config->getString("charset");
352 if (!charset.empty() && mysql_set_character_set(connection, charset.c_str()))
353 {
354 ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Could not set character set for %s to \"%s\": %s",
355 GetId().c_str(), charset.c_str(), mysql_error(connection));
356 return false;
357 }
358
359 // Execute the initial SQL query.
360 const std::string initialquery = config->getString("initialquery");
361 if (!initialquery.empty() && mysql_real_query(connection, initialquery.data(), initialquery.length()))
362 {
363 ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Could not execute initial query \"%s\" for %s: %s",
364 initialquery.c_str(), name.c_str(), mysql_error(connection));
365 return false;
366 }
367
368 return true;
369 }
370
Parent()371 ModuleSQL* Parent()
372 {
373 return (ModuleSQL*)(Module*)creator;
374 }
375
DoBlockingQuery(const std::string & query)376 MySQLresult* DoBlockingQuery(const std::string& query)
377 {
378
379 /* Parse the command string and dispatch it to mysql */
380 if (CheckConnection() && !mysql_real_query(connection, query.data(), query.length()))
381 {
382 /* Successful query */
383 MYSQL_RES* res = mysql_use_result(connection);
384 unsigned long rows = mysql_affected_rows(connection);
385 return new MySQLresult(res, rows);
386 }
387 else
388 {
389 /* XXX: See /usr/include/mysql/mysqld_error.h for a list of
390 * possible error numbers and error messages */
391 SQL::Error e(SQL::QREPLY_FAIL, InspIRCd::Format("%u: %s", mysql_errno(connection), mysql_error(connection)));
392 return new MySQLresult(e);
393 }
394 }
395
CheckConnection()396 bool CheckConnection()
397 {
398 if (!connection || mysql_ping(connection) != 0)
399 return Connect();
400 return true;
401 }
402
Submit(SQL::Query * q,const std::string & qs)403 void Submit(SQL::Query* q, const std::string& qs) CXX11_OVERRIDE
404 {
405 ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Executing MySQL query: " + qs);
406 Parent()->Dispatcher->LockQueue();
407 Parent()->qq.push_back(QueryQueueItem(q, qs, this));
408 Parent()->Dispatcher->UnlockQueueWakeup();
409 }
410
Submit(SQL::Query * call,const std::string & q,const SQL::ParamList & p)411 void Submit(SQL::Query* call, const std::string& q, const SQL::ParamList& p) CXX11_OVERRIDE
412 {
413 std::string res;
414 unsigned int param = 0;
415 for(std::string::size_type i = 0; i < q.length(); i++)
416 {
417 if (q[i] != '?')
418 res.push_back(q[i]);
419 else if (param < p.size() && !EscapeString(call, p[param++], res))
420 return;
421 }
422 Submit(call, res);
423 }
424
Submit(SQL::Query * call,const std::string & q,const SQL::ParamMap & p)425 void Submit(SQL::Query* call, const std::string& q, const SQL::ParamMap& p) CXX11_OVERRIDE
426 {
427 std::string res;
428 for(std::string::size_type i = 0; i < q.length(); i++)
429 {
430 if (q[i] != '$')
431 res.push_back(q[i]);
432 else
433 {
434 std::string field;
435 i++;
436 while (i < q.length() && isalnum(q[i]))
437 field.push_back(q[i++]);
438 i--;
439
440 SQL::ParamMap::const_iterator it = p.find(field);
441 if (it != p.end() && !EscapeString(call, it->second, res))
442 return;
443 }
444 }
445 Submit(call, res);
446 }
447 };
448
ModuleSQL()449 ModuleSQL::ModuleSQL()
450 : Dispatcher(NULL)
451 {
452 }
453
init()454 void ModuleSQL::init()
455 {
456 if (mysql_library_init(0, NULL, NULL))
457 throw ModuleException("Unable to initialise the MySQL library!");
458
459 Dispatcher = new DispatcherThread(this);
460 ServerInstance->Threads.Start(Dispatcher);
461 }
462
~ModuleSQL()463 ModuleSQL::~ModuleSQL()
464 {
465 if (Dispatcher)
466 {
467 Dispatcher->join();
468 Dispatcher->OnNotify();
469 delete Dispatcher;
470 }
471
472 for(ConnMap::iterator i = connections.begin(); i != connections.end(); i++)
473 {
474 delete i->second;
475 }
476
477 mysql_library_end();
478 }
479
ReadConfig(ConfigStatus & status)480 void ModuleSQL::ReadConfig(ConfigStatus& status)
481 {
482 ConnMap conns;
483 ConfigTagList tags = ServerInstance->Config->ConfTags("database");
484 for(ConfigIter i = tags.first; i != tags.second; i++)
485 {
486 if (!stdalgo::string::equalsci(i->second->getString("module"), "mysql"))
487 continue;
488 std::string id = i->second->getString("id");
489 ConnMap::iterator curr = connections.find(id);
490 if (curr == connections.end())
491 {
492 SQLConnection* conn = new SQLConnection(this, i->second);
493 conns.insert(std::make_pair(id, conn));
494 ServerInstance->Modules->AddService(*conn);
495 }
496 else
497 {
498 conns.insert(*curr);
499 connections.erase(curr);
500 }
501 }
502
503 // now clean up the deleted databases
504 Dispatcher->LockQueue();
505 SQL::Error err(SQL::BAD_DBID);
506 for(ConnMap::iterator i = connections.begin(); i != connections.end(); i++)
507 {
508 ServerInstance->Modules->DelService(*i->second);
509 // it might be running a query on this database. Wait for that to complete
510 i->second->lock.Lock();
511 i->second->lock.Unlock();
512 // now remove all active queries to this DB
513 for (size_t j = qq.size(); j > 0; j--)
514 {
515 size_t k = j - 1;
516 if (qq[k].connection == i->second)
517 {
518 qq[k].query->OnError(err);
519 delete qq[k].query;
520 qq.erase(qq.begin() + k);
521 }
522 }
523 // finally, nuke the connection
524 delete i->second;
525 }
526 Dispatcher->UnlockQueue();
527 connections.swap(conns);
528 }
529
OnUnloadModule(Module * mod)530 void ModuleSQL::OnUnloadModule(Module* mod)
531 {
532 SQL::Error err(SQL::BAD_DBID);
533 Dispatcher->LockQueue();
534 unsigned int i = qq.size();
535 while (i > 0)
536 {
537 i--;
538 if (qq[i].query->creator == mod)
539 {
540 if (i == 0)
541 {
542 // need to wait until the query is done
543 // (the result will be discarded)
544 qq[i].connection->lock.Lock();
545 qq[i].connection->lock.Unlock();
546 }
547 qq[i].query->OnError(err);
548 delete qq[i].query;
549 qq.erase(qq.begin() + i);
550 }
551 }
552 Dispatcher->UnlockQueue();
553 // clean up any result queue entries
554 Dispatcher->OnNotify();
555 }
556
GetVersion()557 Version ModuleSQL::GetVersion()
558 {
559 return Version("Provides the ability for SQL modules to query a MySQL database.", VF_VENDOR);
560 }
561
Run()562 void DispatcherThread::Run()
563 {
564 this->LockQueue();
565 while (!this->GetExitFlag())
566 {
567 if (!Parent->qq.empty())
568 {
569 QueryQueueItem i = Parent->qq.front();
570 i.connection->lock.Lock();
571 this->UnlockQueue();
572 MySQLresult* res = i.connection->DoBlockingQuery(i.querystr);
573 i.connection->lock.Unlock();
574
575 /*
576 * At this point, the main thread could be working on:
577 * Rehash - delete i.connection out from under us. We don't care about that.
578 * UnloadModule - delete i.query and the qq item. Need to avoid reporting results.
579 */
580
581 this->LockQueue();
582 if (!Parent->qq.empty() && Parent->qq.front().query == i.query)
583 {
584 Parent->qq.pop_front();
585 Parent->rq.push_back(ResultQueueItem(i.query, res));
586 NotifyParent();
587 }
588 else
589 {
590 // UnloadModule ate the query
591 delete res;
592 }
593 }
594 else
595 {
596 /* We know the queue is empty, we can safely hang this thread until
597 * something happens
598 */
599 this->WaitForQueue();
600 }
601 }
602 this->UnlockQueue();
603 }
604
OnNotify()605 void DispatcherThread::OnNotify()
606 {
607 // this could unlock during the dispatch, but OnResult isn't expected to take that long
608 this->LockQueue();
609 for(ResultQueue::iterator i = Parent->rq.begin(); i != Parent->rq.end(); i++)
610 {
611 MySQLresult* res = i->result;
612 if (res->err.code == SQL::SUCCESS)
613 i->query->OnResult(*res);
614 else
615 i->query->OnError(res->err);
616 delete i->query;
617 delete i->result;
618 }
619 Parent->rq.clear();
620 this->UnlockQueue();
621 }
622
623 MODULE_INIT(ModuleSQL)
624