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
52 ~MySQLThreadCloser() {
62 bool d_enabled
= false;
65 static thread_local MySQLThreadCloser threadcloser
;
68 pthread_mutex_t
SMySQL::s_myinitlock
= PTHREAD_MUTEX_INITIALIZER
;
70 class SMySQLStatement
: public SSqlStatement
73 SMySQLStatement(const string
& query
, bool dolog
, int nparams
, MYSQL
* db
) : d_prepared(false)
78 d_paridx
= d_fnum
= d_resnum
= d_residx
= 0;
80 d_req_bind
= d_res_bind
= NULL
;
88 SSqlStatement
* bind(const string
& name
, bool value
) {
90 if (d_paridx
>= d_parnum
) {
92 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
94 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_TINY
;
95 d_req_bind
[d_paridx
].buffer
= new char[1];
96 *((char*)d_req_bind
[d_paridx
].buffer
) = (value
?1:0);
100 SSqlStatement
* bind(const string
& name
, int value
) {
101 return bind(name
, (long)value
);
103 SSqlStatement
* bind(const string
& name
, uint32_t value
) {
104 return bind(name
, (unsigned long)value
);
106 SSqlStatement
* bind(const string
& name
, long value
) {
108 if (d_paridx
>= d_parnum
) {
110 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
112 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_LONG
;
113 d_req_bind
[d_paridx
].buffer
= new long[1];
114 *((long*)d_req_bind
[d_paridx
].buffer
) = value
;
118 SSqlStatement
* bind(const string
& name
, unsigned long value
) {
120 if (d_paridx
>= d_parnum
) {
122 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
124 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_LONG
;
125 d_req_bind
[d_paridx
].buffer
= new unsigned long[1];
126 d_req_bind
[d_paridx
].is_unsigned
= 1;
127 *((unsigned long*)d_req_bind
[d_paridx
].buffer
) = value
;
131 SSqlStatement
* bind(const string
& name
, long long value
) {
133 if (d_paridx
>= d_parnum
) {
135 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
137 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_LONGLONG
;
138 d_req_bind
[d_paridx
].buffer
= new long long[1];
139 *((long long*)d_req_bind
[d_paridx
].buffer
) = value
;
143 SSqlStatement
* bind(const string
& name
, unsigned long long value
) {
145 if (d_paridx
>= d_parnum
) {
147 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
149 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_LONGLONG
;
150 d_req_bind
[d_paridx
].buffer
= new unsigned long long[1];
151 d_req_bind
[d_paridx
].is_unsigned
= 1;
152 *((unsigned long long*)d_req_bind
[d_paridx
].buffer
) = value
;
156 SSqlStatement
* bind(const string
& name
, const std::string
& value
) {
158 if (d_paridx
>= d_parnum
) {
160 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
162 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_STRING
;
163 d_req_bind
[d_paridx
].buffer
= new char[value
.size()+1];
164 d_req_bind
[d_paridx
].length
= new unsigned long[1];
165 *d_req_bind
[d_paridx
].length
= value
.size();
166 d_req_bind
[d_paridx
].buffer_length
= *d_req_bind
[d_paridx
].length
+1;
167 memset(d_req_bind
[d_paridx
].buffer
, 0, value
.size()+1);
168 value
.copy((char*)d_req_bind
[d_paridx
].buffer
, value
.size());
172 SSqlStatement
* bindNull(const string
& name
) {
174 if (d_paridx
>= d_parnum
) {
176 throw SSqlException("Attempt to bind more parameters than query has: " + d_query
);
178 d_req_bind
[d_paridx
].buffer_type
= MYSQL_TYPE_NULL
;
183 SSqlStatement
* execute() {
188 if (!d_stmt
) return this;
191 g_log
<<Logger::Warning
<< "Query "<<((long)(void*)this)<<": " << d_query
<< endl
;
195 if ((err
= mysql_stmt_bind_param(d_stmt
, d_req_bind
))) {
196 string
error(mysql_stmt_error(d_stmt
));
198 throw SSqlException("Could not bind mysql statement: " + d_query
+ string(": ") + error
);
201 if ((err
= mysql_stmt_execute(d_stmt
))) {
202 string
error(mysql_stmt_error(d_stmt
));
204 throw SSqlException("Could not execute mysql statement: " + d_query
+ string(": ") + error
);
207 // MySQL documentation says you can call this safely for all queries
208 if ((err
= mysql_stmt_store_result(d_stmt
))) {
209 string
error(mysql_stmt_error(d_stmt
));
211 throw SSqlException("Could not store mysql statement: " + d_query
+ string(": ") + error
);
214 if ((d_fnum
= static_cast<int>(mysql_stmt_field_count(d_stmt
)))>0) {
215 // prepare for result
216 d_resnum
= mysql_stmt_num_rows(d_stmt
);
218 if (d_resnum
> 0 && d_res_bind
== nullptr) {
219 MYSQL_RES
* meta
= mysql_stmt_result_metadata(d_stmt
);
220 d_fnum
= static_cast<int>(mysql_num_fields(meta
)); // ensure correct number of fields
221 d_res_bind
= new MYSQL_BIND
[d_fnum
];
222 memset(d_res_bind
, 0, sizeof(MYSQL_BIND
)*d_fnum
);
223 MYSQL_FIELD
* fields
= mysql_fetch_fields(meta
);
225 for(int i
= 0; i
< d_fnum
; i
++) {
226 unsigned long len
= std::max(fields
[i
].max_length
, fields
[i
].length
)+1;
227 if (len
> 128 * 1024) len
= 128 * 1024; // LONGTEXT may tell us it needs 4GB!
228 d_res_bind
[i
].is_null
= new my_bool
[1];
229 d_res_bind
[i
].error
= new my_bool
[1];
230 d_res_bind
[i
].length
= new unsigned long[1];
231 d_res_bind
[i
].buffer
= new char[len
];
232 d_res_bind
[i
].buffer_length
= len
;
233 d_res_bind
[i
].buffer_type
= MYSQL_TYPE_STRING
;
236 mysql_free_result(meta
);
239 /* we need to bind the results array again because a call to mysql_stmt_next_result() followed
240 by a call to mysql_stmt_store_result() might have invalidated it (the first one sets
241 stmt->bind_result_done to false, causing the second to reset the existing binding),
242 and we can't bind it right after the call to mysql_stmt_store_result() if it returned
243 no rows, because then the statement 'contains no metadata' */
244 if (d_res_bind
!= nullptr && (err
= mysql_stmt_bind_result(d_stmt
, d_res_bind
))) {
245 string
error(mysql_stmt_error(d_stmt
));
247 throw SSqlException("Could not bind parameters to mysql statement: " + d_query
+ string(": ") + error
);
252 g_log
<<Logger::Warning
<< "Query "<<((long)(void*)this)<<": "<<d_dtime
.udiffNoReset()<<" usec to execute"<<endl
;
258 if(d_dolog
&& d_residx
== d_resnum
) {
259 g_log
<<Logger::Warning
<< "Query "<<((long)(void*)this)<<": "<<d_dtime
.udiffNoReset()<<" total usec to last row"<<endl
;
261 return d_residx
< d_resnum
;
264 SSqlStatement
* nextRow(row_t
& row
) {
271 if ((err
= mysql_stmt_fetch(d_stmt
))) {
272 if (err
!= MYSQL_DATA_TRUNCATED
) {
273 string
error(mysql_stmt_error(d_stmt
));
275 throw SSqlException("Could not fetch result: " + d_query
+ string(": ") + error
);
281 for(int i
=0;i
<d_fnum
;i
++) {
282 if (err
== MYSQL_DATA_TRUNCATED
&& *d_res_bind
[i
].error
) {
283 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
;
285 if (*d_res_bind
[i
].is_null
) {
286 row
.emplace_back("");
289 row
.emplace_back((char*)d_res_bind
[i
].buffer
, std::min(d_res_bind
[i
].buffer_length
, *d_res_bind
[i
].length
));
294 #if MYSQL_VERSION_ID >= 50500
295 if (d_residx
>= d_resnum
) {
296 mysql_stmt_free_result(d_stmt
);
297 while(!mysql_stmt_next_result(d_stmt
)) {
298 if ((err
= mysql_stmt_store_result(d_stmt
))) {
299 string
error(mysql_stmt_error(d_stmt
));
301 throw SSqlException("Could not store mysql statement while processing additional sets: " + d_query
+ string(": ") + error
);
303 d_resnum
= mysql_stmt_num_rows(d_stmt
);
304 // XXX: For some reason mysql_stmt_result_metadata returns NULL here, so we cannot
305 // ensure row field count matches first result set.
306 if (d_resnum
> 0) { // ignore empty result set
307 if (d_res_bind
!= nullptr && (err
= mysql_stmt_bind_result(d_stmt
, d_res_bind
))) {
308 string
error(mysql_stmt_error(d_stmt
));
310 throw SSqlException("Could not bind parameters to mysql statement: " + d_query
+ string(": ") + error
);
315 mysql_stmt_free_result(d_stmt
);
322 SSqlStatement
* getResult(result_t
& result
) {
324 result
.reserve(d_resnum
);
327 while(hasNextRow()) {
329 result
.push_back(std::move(row
));
335 SSqlStatement
* reset() {
336 if (!d_stmt
) return this;
338 mysql_stmt_free_result(d_stmt
);
339 #if MYSQL_VERSION_ID >= 50500
340 while((err
= mysql_stmt_next_result(d_stmt
)) == 0) {
341 mysql_stmt_free_result(d_stmt
);
345 string
error(mysql_stmt_error(d_stmt
));
347 throw SSqlException("Could not get next result from mysql statement: " + d_query
+ string(": ") + error
);
349 mysql_stmt_reset(d_stmt
);
351 for(int i
=0;i
<d_parnum
;i
++) {
352 if (d_req_bind
[i
].buffer
) delete [] (char*)d_req_bind
[i
].buffer
;
353 if (d_req_bind
[i
].length
) delete [] d_req_bind
[i
].length
;
355 memset(d_req_bind
, 0, sizeof(MYSQL_BIND
)*d_parnum
);
357 d_residx
= d_resnum
= 0;
362 const std::string
& getQuery() { return d_query
; }
369 void prepareStatement() {
372 if (d_prepared
) return;
373 if (d_query
.empty()) {
378 if ((d_stmt
= mysql_stmt_init(d_db
))==NULL
)
379 throw SSqlException("Could not initialize mysql statement, out of memory: " + d_query
);
381 if ((err
= mysql_stmt_prepare(d_stmt
, d_query
.c_str(), d_query
.size()))) {
382 string
error(mysql_stmt_error(d_stmt
));
384 throw SSqlException("Could not prepare statement: " + d_query
+ string(": ") + error
);
387 if (static_cast<int>(mysql_stmt_param_count(d_stmt
)) != d_parnum
) {
389 throw SSqlException("Provided parameter count does not match statement: " + d_query
);
393 d_req_bind
= new MYSQL_BIND
[d_parnum
];
394 memset(d_req_bind
, 0, sizeof(MYSQL_BIND
)*d_parnum
);
400 void releaseStatement() {
403 mysql_stmt_close(d_stmt
);
406 for(int i
=0;i
<d_parnum
;i
++) {
407 if (d_req_bind
[i
].buffer
) delete [] (char*)d_req_bind
[i
].buffer
;
408 if (d_req_bind
[i
].length
) delete [] d_req_bind
[i
].length
;
410 delete [] d_req_bind
;
414 for(int i
=0;i
<d_fnum
;i
++) {
415 if (d_res_bind
[i
].buffer
) delete [] (char*)d_res_bind
[i
].buffer
;
416 if (d_res_bind
[i
].length
) delete [] d_res_bind
[i
].length
;
417 if (d_res_bind
[i
].error
) delete [] d_res_bind
[i
].error
;
418 if (d_res_bind
[i
].is_null
) delete [] d_res_bind
[i
].is_null
;
420 delete [] d_res_bind
;
423 d_paridx
= d_fnum
= d_resnum
= d_residx
= 0;
428 MYSQL_BIND
* d_req_bind
;
429 MYSQL_BIND
* d_res_bind
;
435 DTime d_dtime
; // only used if d_dolog is set
443 void SMySQL::connect()
447 Lock
l(&s_myinitlock
);
448 if (d_threadCleanup
) {
449 threadcloser
.enable();
452 if (!mysql_init(&d_db
))
453 throw sPerrorException("Unable to initialize mysql driver");
457 #if MYSQL_VERSION_ID >= 50013
458 my_bool set_reconnect
= 0;
459 mysql_options(&d_db
, MYSQL_OPT_RECONNECT
, &set_reconnect
);
462 #if MYSQL_VERSION_ID >= 50100
464 mysql_options(&d_db
, MYSQL_OPT_READ_TIMEOUT
, &d_timeout
);
465 mysql_options(&d_db
, MYSQL_OPT_WRITE_TIMEOUT
, &d_timeout
);
469 #if MYSQL_VERSION_ID >= 50500
470 mysql_options(&d_db
, MYSQL_SET_CHARSET_NAME
, MYSQL_AUTODETECT_CHARSET_NAME
);
473 if (d_setIsolation
&& (retry
== 1))
474 mysql_options(&d_db
, MYSQL_INIT_COMMAND
,"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
476 mysql_options(&d_db
, MYSQL_READ_DEFAULT_GROUP
, d_group
.c_str());
478 if (!mysql_real_connect(&d_db
, d_host
.empty() ? NULL
: d_host
.c_str(),
479 d_user
.empty() ? NULL
: d_user
.c_str(),
480 d_password
.empty() ? NULL
: d_password
.c_str(),
481 d_database
.empty() ? NULL
: d_database
.c_str(),
483 d_msocket
.empty() ? NULL
: d_msocket
.c_str(),
484 (d_clientSSL
? CLIENT_SSL
: 0) | CLIENT_MULTI_RESULTS
)) {
487 throw sPerrorException("Unable to connect to database");
492 throw sPerrorException("Please add '(gmysql-)innodb-read-committed=no' to your PowerDNS configuration, and reconsider your storage engine if it does not support transactions.");
496 } while (retry
>= 0);
499 SMySQL::SMySQL(const string
&database
, const string
&host
, uint16_t port
, const string
&msocket
, const string
&user
,
500 const string
&password
, const string
&group
, bool setIsolation
, unsigned int timeout
, bool threadCleanup
, bool clientSSL
):
501 d_database(database
), d_host(host
), d_msocket(msocket
), d_user(user
), d_password(password
), d_group(group
), d_timeout(timeout
), d_port(port
), d_setIsolation(setIsolation
), d_threadCleanup(threadCleanup
), d_clientSSL(clientSSL
)
506 void SMySQL::setLog(bool state
)
516 SSqlException
SMySQL::sPerrorException(const string
&reason
)
518 return SSqlException(reason
+string(": ")+mysql_error(&d_db
));
521 std::unique_ptr
<SSqlStatement
> SMySQL::prepare(const string
& query
, int nparams
)
523 return std::unique_ptr
<SSqlStatement
>(new SMySQLStatement(query
, s_dolog
, nparams
, &d_db
));
526 void SMySQL::execute(const string
& query
)
529 g_log
<<Logger::Warning
<<"Query: "<<query
<<endl
;
532 if((err
=mysql_query(&d_db
,query
.c_str())))
533 throw sPerrorException("Failed to execute mysql_query '" + query
+ "' Err="+itoa(err
));
536 void SMySQL::startTransaction() {
540 void SMySQL::commit() {
544 void SMySQL::rollback() {
548 bool SMySQL::isConnectionUsable()
551 int sd
= d_db
.net
.fd
;
552 bool wasNonBlocking
= isNonBlocking(sd
);
554 if (!wasNonBlocking
) {
555 if (!setNonBlocking(sd
)) {
560 usable
= isTCPSocketUsable(sd
);
562 if (!wasNonBlocking
) {
563 if (!setBlocking(sd
)) {