2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
29 #include "pdns/misc.hh"
30 #include "pdns/logger.hh"
31 #include "pdns/dns.hh"
32 #include "pdns/namespaces.hh"
33 #include "pdns/lock.hh"
35 #if MYSQL_VERSION_ID >= 80000 && !defined(MARIADB_BASE_VERSION)
36 // Need to keep this for compatibility with MySQL < 8.0.0, which used typedef char my_bool;
37 // MariaDB up to 10.4 also always define it.
42 * Older versions of the MySQL and MariaDB client leak memory
43 * because they expect the application to call mysql_thread_end()
44 * when a thread ends. This thread_local static object provides
45 * that closure, but only when the user has asked for it
46 * by setting gmysql-thread-cleanup.
47 * For more discussion, see https://github.com/PowerDNS/pdns/issues/6231
49 class MySQLThreadCloser
64 bool d_enabled
= false;
67 static thread_local MySQLThreadCloser threadcloser
;
70 std::mutex
SMySQL::s_myinitlock
;
72 class SMySQLStatement
: public SSqlStatement
75 SMySQLStatement(const string
& query
, bool dolog
, int nparams
, MYSQL
* db
) :
81 d_paridx
= d_fnum
= d_resnum
= d_residx
= 0;
83 d_req_bind
= d_res_bind
= nullptr;
91 SSqlStatement
* bind(const string
& name
, bool value
)
94 if (d_paridx
>= d_parnum
) {
96 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
98 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_TINY
;
99 d_req_bind
[d_paridx
].buffer
= new char[1];
100 *((char*)d_req_bind
[d_paridx
].buffer
) = (value
? 1 : 0);
104 SSqlStatement
* bind(const string
& name
, int value
)
106 return bind(name
, (long)value
);
108 SSqlStatement
* bind(const string
& name
, uint32_t value
)
110 return bind(name
, (unsigned long)value
);
112 SSqlStatement
* bind(const string
& name
, long value
)
115 if (d_paridx
>= d_parnum
) {
117 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
119 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_LONG
;
120 d_req_bind
[d_paridx
].buffer
= new long[1];
121 *((long*)d_req_bind
[d_paridx
].buffer
) = value
;
125 SSqlStatement
* bind(const string
& name
, unsigned long value
)
128 if (d_paridx
>= d_parnum
) {
130 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
132 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_LONG
;
133 d_req_bind
[d_paridx
].buffer
= new unsigned long[1];
134 d_req_bind
[d_paridx
].is_unsigned
= 1;
135 *((unsigned long*)d_req_bind
[d_paridx
].buffer
) = value
;
139 SSqlStatement
* bind(const string
& name
, long long value
)
142 if (d_paridx
>= d_parnum
) {
144 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
146 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_LONGLONG
;
147 d_req_bind
[d_paridx
].buffer
= new long long[1];
148 *((long long*)d_req_bind
[d_paridx
].buffer
) = value
;
152 SSqlStatement
* bind(const string
& name
, unsigned long long value
)
155 if (d_paridx
>= d_parnum
) {
157 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
159 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_LONGLONG
;
160 d_req_bind
[d_paridx
].buffer
= new unsigned long long[1];
161 d_req_bind
[d_paridx
].is_unsigned
= 1;
162 *((unsigned long long*)d_req_bind
[d_paridx
].buffer
) = value
;
166 SSqlStatement
* bind(const string
& name
, const std::string
& value
)
169 if (d_paridx
>= d_parnum
) {
171 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
173 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_STRING
;
174 d_req_bind
[d_paridx
].buffer
= new char[value
.size() + 1];
175 d_req_bind
[d_paridx
].length
= new unsigned long[1];
176 *d_req_bind
[d_paridx
].length
= value
.size();
177 d_req_bind
[d_paridx
].buffer_length
= *d_req_bind
[d_paridx
].length
+ 1;
178 memset(d_req_bind
[d_paridx
].buffer
, 0, value
.size() + 1);
179 value
.copy((char*)d_req_bind
[d_paridx
].buffer
, value
.size());
183 SSqlStatement
* bindNull(const string
& name
)
186 if (d_paridx
>= d_parnum
) {
188 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
190 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_NULL
;
195 SSqlStatement
* execute()
203 g_log
<< Logger::Warning
<< "Query " << ((long)(void*)this) << ": " << d_query
<< endl
;
207 if (mysql_stmt_bind_param(d_stmt
, d_req_bind
) != 0) {
208 string
error(mysql_stmt_error(d_stmt
));
210 throw SSqlException("Could not bind mysql statement: " + d_query
+ string(": ") + error
);
213 if (mysql_stmt_execute(d_stmt
) != 0) {
214 string
error(mysql_stmt_error(d_stmt
));
216 throw SSqlException("Could not execute mysql statement: " + d_query
+ string(": ") + error
);
219 // MySQL documentation says you can call this safely for all queries
220 if (mysql_stmt_store_result(d_stmt
) != 0) {
221 string
error(mysql_stmt_error(d_stmt
));
223 throw SSqlException("Could not store mysql statement: " + d_query
+ string(": ") + error
);
226 if ((d_fnum
= static_cast<int>(mysql_stmt_field_count(d_stmt
))) > 0) {
227 // prepare for result
228 d_resnum
= mysql_stmt_num_rows(d_stmt
);
230 if (d_resnum
> 0 && d_res_bind
== nullptr) {
231 MYSQL_RES
* meta
= mysql_stmt_result_metadata(d_stmt
);
232 d_fnum
= static_cast<int>(mysql_num_fields(meta
)); // ensure correct number of fields
233 d_res_bind
= new MYSQL_BIND
[d_fnum
];
234 memset(d_res_bind
, 0, sizeof(MYSQL_BIND
) * d_fnum
);
235 MYSQL_FIELD
* fields
= mysql_fetch_fields(meta
);
237 for (int i
= 0; i
< d_fnum
; i
++) {
238 unsigned long len
= std::max(fields
[i
].max_length
, fields
[i
].length
) + 1;
239 if (len
> 128 * 1024)
240 len
= 128 * 1024; // LONGTEXT may tell us it needs 4GB!
241 d_res_bind
[i
].is_null
= new my_bool
[1];
242 d_res_bind
[i
].error
= new my_bool
[1];
243 d_res_bind
[i
].length
= new unsigned long[1];
244 d_res_bind
[i
].buffer
= new char[len
];
245 d_res_bind
[i
].buffer_length
= len
;
246 d_res_bind
[i
].buffer_type
= MYSQL_TYPE_STRING
;
249 mysql_free_result(meta
);
252 /* we need to bind the results array again because a call to mysql_stmt_next_result() followed
253 by a call to mysql_stmt_store_result() might have invalidated it (the first one sets
254 stmt->bind_result_done to false, causing the second to reset the existing binding),
255 and we can't bind it right after the call to mysql_stmt_store_result() if it returned
256 no rows, because then the statement 'contains no metadata' */
257 if (d_res_bind
!= nullptr && mysql_stmt_bind_result(d_stmt
, d_res_bind
) != 0) {
258 string
error(mysql_stmt_error(d_stmt
));
260 throw SSqlException("Could not bind parameters to mysql statement: " + d_query
+ string(": ") + error
);
265 g_log
<< Logger::Warning
<< "Query " << ((long)(void*)this) << ": " << d_dtime
.udiffNoReset() << " usec to execute" << endl
;
272 if (d_dolog
&& d_residx
== d_resnum
) {
273 g_log
<< Logger::Warning
<< "Query " << ((long)(void*)this) << ": " << d_dtime
.udiffNoReset() << " total usec to last row" << endl
;
275 return d_residx
< d_resnum
;
278 SSqlStatement
* nextRow(row_t
& row
)
286 if ((err
= mysql_stmt_fetch(d_stmt
))) {
287 if (err
!= MYSQL_DATA_TRUNCATED
) {
288 string
error(mysql_stmt_error(d_stmt
));
290 throw SSqlException("Could not fetch result: " + d_query
+ string(": ") + error
);
296 for (int i
= 0; i
< d_fnum
; i
++) {
297 if (err
== MYSQL_DATA_TRUNCATED
&& *d_res_bind
[i
].error
) {
298 g_log
<< Logger::Warning
<< "Result field at row " << d_residx
<< " column " << i
<< " has been truncated, we allocated " << d_res_bind
[i
].buffer_length
<< " bytes but at least " << *d_res_bind
[i
].length
<< " was needed" << endl
;
300 if (*d_res_bind
[i
].is_null
) {
301 row
.emplace_back("");
305 row
.emplace_back((char*)d_res_bind
[i
].buffer
, std::min(d_res_bind
[i
].buffer_length
, *d_res_bind
[i
].length
));
310 #if MYSQL_VERSION_ID >= 50500
311 if (d_residx
>= d_resnum
) {
312 mysql_stmt_free_result(d_stmt
);
313 while (!mysql_stmt_next_result(d_stmt
)) {
314 if (mysql_stmt_store_result(d_stmt
) != 0) {
315 string
error(mysql_stmt_error(d_stmt
));
317 throw SSqlException("Could not store mysql statement while processing additional sets: " + d_query
+ string(": ") + error
);
319 d_resnum
= mysql_stmt_num_rows(d_stmt
);
320 // XXX: For some reason mysql_stmt_result_metadata returns NULL here, so we cannot
321 // ensure row field count matches first result set.
322 // We need to check the field count as stored procedure return the final values of OUT and INOUT parameters
323 // as an extra single-row result set following any result sets produced by the procedure itself.
324 // mysql_stmt_field_count() will return 0 for those.
325 if (mysql_stmt_field_count(d_stmt
) > 0 && d_resnum
> 0) { // ignore empty result set
326 if (d_res_bind
!= nullptr && mysql_stmt_bind_result(d_stmt
, d_res_bind
) != 0) {
327 string
error(mysql_stmt_error(d_stmt
));
329 throw SSqlException("Could not bind parameters to mysql statement: " + d_query
+ string(": ") + error
);
334 mysql_stmt_free_result(d_stmt
);
341 SSqlStatement
* getResult(result_t
& result
)
344 result
.reserve(d_resnum
);
347 while (hasNextRow()) {
349 result
.push_back(std::move(row
));
355 SSqlStatement
* reset()
360 mysql_stmt_free_result(d_stmt
);
361 #if MYSQL_VERSION_ID >= 50500
362 while ((err
= mysql_stmt_next_result(d_stmt
)) == 0) {
363 mysql_stmt_free_result(d_stmt
);
367 string
error(mysql_stmt_error(d_stmt
));
369 throw SSqlException("Could not get next result from mysql statement: " + d_query
+ string(": ") + error
);
371 mysql_stmt_reset(d_stmt
);
373 for (int i
= 0; i
< d_parnum
; i
++) {
374 if (d_req_bind
[i
].buffer
)
375 delete[](char*) d_req_bind
[i
].buffer
;
376 if (d_req_bind
[i
].length
)
377 delete[] d_req_bind
[i
].length
;
379 memset(d_req_bind
, 0, sizeof(MYSQL_BIND
) * d_parnum
);
381 d_residx
= d_resnum
= 0;
386 const std::string
& getQuery() { return d_query
; }
394 void prepareStatement()
398 if (d_query
.empty()) {
403 if ((d_stmt
= mysql_stmt_init(d_db
)) == nullptr)
404 throw SSqlException("Could not initialize mysql statement, out of memory: " + d_query
);
406 if (mysql_stmt_prepare(d_stmt
, d_query
.c_str(), d_query
.size()) != 0) {
407 string
error(mysql_stmt_error(d_stmt
));
409 throw SSqlException("Could not prepare statement: " + d_query
+ string(": ") + error
);
412 if (static_cast<int>(mysql_stmt_param_count(d_stmt
)) != d_parnum
) {
414 throw SSqlException("Provided parameter count does not match statement: " + d_query
);
418 d_req_bind
= new MYSQL_BIND
[d_parnum
];
419 memset(d_req_bind
, 0, sizeof(MYSQL_BIND
) * d_parnum
);
425 void releaseStatement()
429 mysql_stmt_close(d_stmt
);
432 for (int i
= 0; i
< d_parnum
; i
++) {
433 if (d_req_bind
[i
].buffer
)
434 delete[](char*) d_req_bind
[i
].buffer
;
435 if (d_req_bind
[i
].length
)
436 delete[] d_req_bind
[i
].length
;
439 d_req_bind
= nullptr;
442 for (int i
= 0; i
< d_fnum
; i
++) {
443 if (d_res_bind
[i
].buffer
)
444 delete[](char*) d_res_bind
[i
].buffer
;
445 if (d_res_bind
[i
].length
)
446 delete[] d_res_bind
[i
].length
;
447 if (d_res_bind
[i
].error
)
448 delete[] d_res_bind
[i
].error
;
449 if (d_res_bind
[i
].is_null
)
450 delete[] d_res_bind
[i
].is_null
;
453 d_res_bind
= nullptr;
455 d_paridx
= d_fnum
= d_resnum
= d_residx
= 0;
460 MYSQL_BIND
* d_req_bind
;
461 MYSQL_BIND
* d_res_bind
;
467 DTime d_dtime
; // only used if d_dolog is set
475 void SMySQL::connect()
480 std::lock_guard
<std::mutex
> l(s_myinitlock
);
481 if (d_threadCleanup
) {
482 threadcloser
.enable();
485 if (!mysql_init(&d_db
)) {
486 throw sPerrorException("Unable to initialize mysql driver");
492 #if MYSQL_VERSION_ID >= 50013
493 my_bool set_reconnect
= 0;
494 mysql_options(&d_db
, MYSQL_OPT_RECONNECT
, &set_reconnect
);
497 #if MYSQL_VERSION_ID >= 50100
499 mysql_options(&d_db
, MYSQL_OPT_READ_TIMEOUT
, &d_timeout
);
500 mysql_options(&d_db
, MYSQL_OPT_WRITE_TIMEOUT
, &d_timeout
);
504 if (d_setIsolation
&& (retry
== 1))
505 mysql_options(&d_db
, MYSQL_INIT_COMMAND
, "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
507 mysql_options(&d_db
, MYSQL_READ_DEFAULT_GROUP
, d_group
.c_str());
509 if (!mysql_real_connect(&d_db
, d_host
.empty() ? nullptr : d_host
.c_str(),
510 d_user
.empty() ? nullptr : d_user
.c_str(),
511 d_password
.empty() ? nullptr : d_password
.c_str(),
512 d_database
.empty() ? nullptr : d_database
.c_str(),
514 d_msocket
.empty() ? nullptr : d_msocket
.c_str(),
515 (d_clientSSL
? CLIENT_SSL
: 0) | CLIENT_MULTI_RESULTS
)) {
518 throw sPerrorException("Unable to connect to database");
524 throw sPerrorException("Please add '(gmysql-)innodb-read-committed=no' to your PowerDNS configuration, and reconsider your storage engine if it does not support transactions.");
528 } while (retry
>= 0);
531 SMySQL::SMySQL(string database
, string host
, uint16_t port
, string msocket
, string user
,
532 string password
, string group
, bool setIsolation
, unsigned int timeout
, bool threadCleanup
, bool clientSSL
) :
533 d_database(std::move(database
)), d_host(std::move(host
)), d_msocket(std::move(msocket
)), d_user(std::move(user
)), d_password(std::move(password
)), d_group(std::move(group
)), d_timeout(timeout
), d_port(port
), d_setIsolation(setIsolation
), d_threadCleanup(threadCleanup
), d_clientSSL(clientSSL
)
538 void SMySQL::setLog(bool state
)
548 SSqlException
SMySQL::sPerrorException(const string
& reason
)
550 return SSqlException(reason
+ string(": ERROR ") + std::to_string(mysql_errno(&d_db
)) + " (" + string(mysql_sqlstate(&d_db
)) + "): " + mysql_error(&d_db
));
553 std::unique_ptr
<SSqlStatement
> SMySQL::prepare(const string
& query
, int nparams
)
555 return std::make_unique
<SMySQLStatement
>(query
, s_dolog
, nparams
, &d_db
);
558 void SMySQL::execute(const string
& query
)
561 g_log
<< Logger::Warning
<< "Query: " << query
<< endl
;
564 if ((err
= mysql_query(&d_db
, query
.c_str())))
565 throw sPerrorException("Failed to execute mysql_query '" + query
+ "' Err=" + itoa(err
));
568 void SMySQL::startTransaction()
573 void SMySQL::commit()
578 void SMySQL::rollback()
583 bool SMySQL::isConnectionUsable()
586 int sd
= d_db
.net
.fd
;
587 bool wasNonBlocking
= isNonBlocking(sd
);
589 if (!wasNonBlocking
) {
590 if (!setNonBlocking(sd
)) {
595 usable
= isTCPSocketUsable(sd
);
597 if (!wasNonBlocking
) {
598 if (!setBlocking(sd
)) {