xref: /netbsd-src/external/ibm-public/postfix/dist/src/global/dict_mysql.c (revision c48c605c14fd8622b523d1d6a3f0c0bad133ea89)
1 /*	$NetBSD: dict_mysql.c,v 1.4 2023/12/23 20:30:43 christos Exp $	*/
2 
3 /*++
4 /* NAME
5 /*	dict_mysql 3
6 /* SUMMARY
7 /*	dictionary manager interface to MySQL databases
8 /* SYNOPSIS
9 /*	#include <dict_mysql.h>
10 /*
11 /*	DICT	*dict_mysql_open(name, open_flags, dict_flags)
12 /*	const char *name;
13 /*	int	open_flags;
14 /*	int	dict_flags;
15 /* DESCRIPTION
16 /*	dict_mysql_open() creates a dictionary of type 'mysql'.  This
17 /*	dictionary is an interface for the postfix key->value mappings
18 /*	to mysql.  The result is a pointer to the installed dictionary,
19 /*	or a null pointer in case of problems.
20 /*
21 /*	The mysql dictionary can manage multiple connections to different
22 /*	sql servers on different hosts.  It assumes that the underlying data
23 /*	on each host is identical (mirrored) and maintains one connection
24 /*	at any given time.  If any connection fails,  any other available
25 /*	ones will be opened and used.  The intent of this feature is to eliminate
26 /*	a single point of failure for mail systems that would otherwise rely
27 /*	on a single mysql server.
28 /* .PP
29 /*	Arguments:
30 /* .IP name
31 /*	Either the path to the MySQL configuration file (if it starts
32 /*	with '/' or '.'), or the prefix which will be used to obtain
33 /*	main.cf configuration parameters for this search.
34 /*
35 /*	In the first case, the configuration parameters below are
36 /*	specified in the file as \fIname\fR=\fIvalue\fR pairs.
37 /*
38 /*	In the second case, the configuration parameters are
39 /*	prefixed with the value of \fIname\fR and an underscore,
40 /*	and they are specified in main.cf.  For example, if this
41 /*	value is \fImysqlsource\fR, the parameters would look like
42 /*	\fImysqlsource_user\fR, \fImysqlsource_table\fR, and so on.
43 /*
44 /* .IP other_name
45 /*	reference for outside use.
46 /* .IP open_flags
47 /*	Must be O_RDONLY.
48 /* .IP dict_flags
49 /*	See dict_open(3).
50 /* SEE ALSO
51 /*	dict(3) generic dictionary manager
52 /*	mysql_table(5) MySQL client configuration
53 /* AUTHOR(S)
54 /*	Scott Cotton, Joshua Marcus
55 /*	IC Group, Inc.
56 /*	scott@icgroup.com
57 /*
58 /*	Liviu Daia
59 /*	Institute of Mathematics of the Romanian Academy
60 /*	P.O. BOX 1-764
61 /*	RO-014700 Bucharest, ROMANIA
62 /*
63 /*	John Fawcett
64 /*
65 /*	Wietse Venema
66 /*	Google, Inc.
67 /*	111 8th Avenue
68 /*	New York, NY 10011, USA
69 /*--*/
70 
71 /* System library. */
72 #include "sys_defs.h"
73 
74 #ifdef HAS_MYSQL
75 #include <sys/socket.h>
76 #include <netinet/in.h>
77 #include <arpa/inet.h>
78 #include <netdb.h>
79 #include <stdio.h>
80 #include <string.h>
81 #include <stdlib.h>
82 #include <syslog.h>
83 #include <time.h>
84 #include <mysql.h>
85 #include <limits.h>
86 #include <errno.h>
87 
88 #ifdef STRCASECMP_IN_STRINGS_H
89 #include <strings.h>
90 #endif
91 
92 /* Utility library. */
93 
94 #include "dict.h"
95 #include "msg.h"
96 #include "mymalloc.h"
97 #include "argv.h"
98 #include "vstring.h"
99 #include "split_at.h"
100 #include "find_inet.h"
101 #include "myrand.h"
102 #include "events.h"
103 #include "stringops.h"
104 
105 /* Global library. */
106 
107 #include "cfg_parser.h"
108 #include "db_common.h"
109 
110 /* Application-specific. */
111 
112 #include "dict_mysql.h"
113 
114 /* MySQL 8.x API change */
115 
116 #if defined(MARIADB_BASE_VERSION) && MYSQL_VERSION_ID >= 50023
117 #define DICT_MYSQL_SSL_VERIFY_SERVER_CERT MYSQL_OPT_SSL_VERIFY_SERVER_CERT
118 #elif MYSQL_VERSION_ID >= 80000
119 #define DICT_MYSQL_SSL_VERIFY_SERVER_CERT MYSQL_OPT_SSL_MODE
120 #endif
121 
122 /* need some structs to help organize things */
123 typedef struct {
124     MYSQL  *db;
125     char   *hostname;
126     char   *name;
127     unsigned port;
128     unsigned type;			/* TYPEUNIX | TYPEINET */
129     unsigned stat;			/* STATUNTRIED | STATFAIL | STATCUR */
130     time_t  ts;				/* used for attempting reconnection
131 					 * every so often if a host is down */
132 } HOST;
133 
134 typedef struct {
135     int     len_hosts;			/* number of hosts */
136     HOST  **db_hosts;			/* the hosts on which the databases
137 					 * reside */
138 } PLMYSQL;
139 
140 typedef struct {
141     DICT    dict;
142     CFG_PARSER *parser;
143     char   *query;
144     char   *result_format;
145     char   *option_file;
146     char   *option_group;
147     void   *ctx;
148     int     expansion_limit;
149     char   *username;
150     char   *password;
151     char   *dbname;
152     ARGV   *hosts;
153     PLMYSQL *pldb;
154 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
155     HOST   *active_host;
156     char   *tls_cert_file;
157     char   *tls_key_file;
158     char   *tls_CAfile;
159     char   *tls_CApath;
160     char   *tls_ciphers;
161 #if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
162     int     tls_verify_cert;
163 #endif
164 #endif
165     int     require_result_set;
166 } DICT_MYSQL;
167 
168 #define STATACTIVE			(1<<0)
169 #define STATFAIL			(1<<1)
170 #define STATUNTRIED			(1<<2)
171 
172 #define TYPEUNIX			(1<<0)
173 #define TYPEINET			(1<<1)
174 
175 #define RETRY_CONN_MAX			100
176 #define RETRY_CONN_INTV			60	/* 1 minute */
177 #define IDLE_CONN_INTV			60	/* 1 minute */
178 
179 /* internal function declarations */
180 static PLMYSQL *plmysql_init(ARGV *);
181 static int plmysql_query(DICT_MYSQL *, const char *, VSTRING *, MYSQL_RES **);
182 static void plmysql_dealloc(PLMYSQL *);
183 static void plmysql_close_host(HOST *);
184 static void plmysql_down_host(HOST *);
185 static void plmysql_connect_single(DICT_MYSQL *, HOST *);
186 static const char *dict_mysql_lookup(DICT *, const char *);
187 DICT   *dict_mysql_open(const char *, int, int);
188 static void dict_mysql_close(DICT *);
189 static void mysql_parse_config(DICT_MYSQL *, const char *);
190 static HOST *host_init(const char *);
191 
192 /* dict_mysql_quote - escape SQL metacharacters in input string */
193 
dict_mysql_quote(DICT * dict,const char * name,VSTRING * result)194 static void dict_mysql_quote(DICT *dict, const char *name, VSTRING *result)
195 {
196     DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;
197     int     len = strlen(name);
198     int     buflen;
199 
200     /*
201      * We won't get integer overflows in 2*len + 1, because Postfix input
202      * keys have reasonable size limits, better safe than sorry.
203      */
204     if (len > (INT_MAX - VSTRING_LEN(result) - 1) / 2)
205 	msg_panic("dict_mysql_quote: integer overflow in %lu+2*%d+1",
206 		  (unsigned long) VSTRING_LEN(result), len);
207     buflen = 2 * len + 1;
208     VSTRING_SPACE(result, buflen);
209 
210 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
211     if (dict_mysql->active_host)
212 	mysql_real_escape_string(dict_mysql->active_host->db,
213 				 vstring_end(result), name, len);
214     else
215 #endif
216 	mysql_escape_string(vstring_end(result), name, len);
217 
218     VSTRING_SKIP(result);
219 }
220 
221 /* dict_mysql_lookup - find database entry */
222 
dict_mysql_lookup(DICT * dict,const char * name)223 static const char *dict_mysql_lookup(DICT *dict, const char *name)
224 {
225     const char *myname = "dict_mysql_lookup";
226     DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;
227     MYSQL_RES *query_res;
228     MYSQL_ROW row;
229     static VSTRING *result;
230     static VSTRING *query;
231     int     i;
232     int     j;
233     int     numrows;
234     int     expansion;
235     const char *r;
236     db_quote_callback_t quote_func = dict_mysql_quote;
237     int     domain_rc;
238 
239     dict->error = 0;
240 
241     /*
242      * Don't frustrate future attempts to make Postfix UTF-8 transparent.
243      */
244 #ifdef SNAPSHOT
245     if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0
246 	&& !valid_utf8_string(name, strlen(name))) {
247 	if (msg_verbose)
248 	    msg_info("%s: %s: Skipping lookup of non-UTF-8 key '%s'",
249 		     myname, dict_mysql->parser->name, name);
250 	return (0);
251     }
252 #endif
253 
254     /*
255      * Optionally fold the key.
256      */
257     if (dict->flags & DICT_FLAG_FOLD_FIX) {
258 	if (dict->fold_buf == 0)
259 	    dict->fold_buf = vstring_alloc(10);
260 	vstring_strcpy(dict->fold_buf, name);
261 	name = lowercase(vstring_str(dict->fold_buf));
262     }
263 
264     /*
265      * If there is a domain list for this map, then only search for addresses
266      * in domains on the list. This can significantly reduce the load on the
267      * server.
268      */
269     if ((domain_rc = db_common_check_domain(dict_mysql->ctx, name)) == 0) {
270 	if (msg_verbose)
271 	    msg_info("%s: Skipping lookup of '%s'", myname, name);
272 	return (0);
273     }
274     if (domain_rc < 0) {
275 	msg_warn("%s:%s 'domain' pattern match failed for '%s'",
276 		 dict->type, dict->name, name);
277 	DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0);
278     }
279 #define INIT_VSTR(buf, len) do { \
280 	if (buf == 0) \
281 	    buf = vstring_alloc(len); \
282 	VSTRING_RESET(buf); \
283 	VSTRING_TERMINATE(buf); \
284     } while (0)
285 
286     INIT_VSTR(query, 10);
287 
288     /*
289      * Suppress the lookup if the query expansion is empty
290      *
291      * This initial expansion is outside the context of any specific host
292      * connection, we just want to check the key pre-requisites, so when
293      * quoting happens separately for each connection, we don't bother with
294      * quoting...
295      */
296 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
297     quote_func = 0;
298 #endif
299     if (!db_common_expand(dict_mysql->ctx, dict_mysql->query,
300 			  name, 0, query, quote_func))
301 	return (0);
302 
303     /* do the query - set dict->error & cleanup if there's an error */
304     if (plmysql_query(dict_mysql, name, query, &query_res) == 0) {
305 	dict->error = DICT_ERR_RETRY;
306 	return (0);
307     }
308     if (query_res == 0)
309 	return (0);
310     numrows = mysql_num_rows(query_res);
311     if (msg_verbose)
312 	msg_info("%s: retrieved %d rows", myname, numrows);
313     if (numrows == 0) {
314 	mysql_free_result(query_res);
315 	return 0;
316     }
317     INIT_VSTR(result, 10);
318 
319     for (expansion = i = 0; i < numrows && dict->error == 0; i++) {
320 	row = mysql_fetch_row(query_res);
321 	for (j = 0; j < mysql_num_fields(query_res); j++) {
322 	    if (db_common_expand(dict_mysql->ctx, dict_mysql->result_format,
323 				 row[j], name, result, 0)
324 		&& dict_mysql->expansion_limit > 0
325 		&& ++expansion > dict_mysql->expansion_limit) {
326 		msg_warn("%s: %s: Expansion limit exceeded for key: '%s'",
327 			 myname, dict_mysql->parser->name, name);
328 		dict->error = DICT_ERR_RETRY;
329 		break;
330 	    }
331 	}
332     }
333     mysql_free_result(query_res);
334     r = vstring_str(result);
335     return ((dict->error == 0 && *r) ? r : 0);
336 }
337 
338 /* dict_mysql_check_stat - check the status of a host */
339 
dict_mysql_check_stat(HOST * host,unsigned stat,unsigned type,time_t t)340 static int dict_mysql_check_stat(HOST *host, unsigned stat, unsigned type,
341 				         time_t t)
342 {
343     if ((host->stat & stat) && (!type || host->type & type)) {
344 	/* try not to hammer the dead hosts too often */
345 	if (host->stat == STATFAIL && host->ts > 0 && host->ts >= t)
346 	    return 0;
347 	return 1;
348     }
349     return 0;
350 }
351 
352 /* dict_mysql_find_host - find a host with the given status */
353 
dict_mysql_find_host(PLMYSQL * PLDB,unsigned stat,unsigned type)354 static HOST *dict_mysql_find_host(PLMYSQL *PLDB, unsigned stat, unsigned type)
355 {
356     time_t  t;
357     int     count = 0;
358     int     idx;
359     int     i;
360 
361     t = time((time_t *) 0);
362     for (i = 0; i < PLDB->len_hosts; i++) {
363 	if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t))
364 	    count++;
365     }
366 
367     if (count) {
368 	idx = (count > 1) ?
369 	    1 + count * (double) myrand() / (1.0 + RAND_MAX) : 1;
370 
371 	for (i = 0; i < PLDB->len_hosts; i++) {
372 	    if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t) &&
373 		--idx == 0)
374 		return PLDB->db_hosts[i];
375 	}
376     }
377     return 0;
378 }
379 
380 /* dict_mysql_get_active - get an active connection */
381 
dict_mysql_get_active(DICT_MYSQL * dict_mysql)382 static HOST *dict_mysql_get_active(DICT_MYSQL *dict_mysql)
383 {
384     const char *myname = "dict_mysql_get_active";
385     PLMYSQL *PLDB = dict_mysql->pldb;
386     HOST   *host;
387     int     count = RETRY_CONN_MAX;
388 
389     /* Try the active connections first; prefer the ones to UNIX sockets. */
390     if ((host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEUNIX)) != NULL ||
391 	(host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEINET)) != NULL) {
392 	if (msg_verbose)
393 	    msg_info("%s: found active connection to host %s", myname,
394 		     host->hostname);
395 	return host;
396     }
397 
398     /*
399      * Try the remaining hosts. "count" is a safety net, in case the loop
400      * takes more than RETRY_CONN_INTV and the dead hosts are no longer
401      * skipped.
402      */
403     while (--count > 0 &&
404 	   ((host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL,
405 					 TYPEUNIX)) != NULL ||
406 	    (host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL,
407 					 TYPEINET)) != NULL)) {
408 	if (msg_verbose)
409 	    msg_info("%s: attempting to connect to host %s", myname,
410 		     host->hostname);
411 	plmysql_connect_single(dict_mysql, host);
412 	if (host->stat == STATACTIVE)
413 	    return host;
414     }
415 
416     /* bad news... */
417     return 0;
418 }
419 
420 /* dict_mysql_event - callback: close idle connections */
421 
dict_mysql_event(int unused_event,void * context)422 static void dict_mysql_event(int unused_event, void *context)
423 {
424     HOST   *host = (HOST *) context;
425 
426     if (host->db)
427 	plmysql_close_host(host);
428 }
429 
430 /*
431  * plmysql_query - process a MySQL query.  Return 'true' on success.
432  *			On failure, log failure and try other db instances.
433  *			on failure of all db instances, return 'false';
434  *			close unnecessary active connections
435  */
436 
plmysql_query(DICT_MYSQL * dict_mysql,const char * name,VSTRING * query,MYSQL_RES ** result)437 static int plmysql_query(DICT_MYSQL *dict_mysql,
438 			         const char *name,
439 			         VSTRING *query,
440 			         MYSQL_RES **result)
441 {
442     HOST   *host;
443     MYSQL_RES *first_result = 0;
444     int     query_error = 1;
445 
446     /*
447      * Helper to avoid spamming the log with warnings.
448      */
449 #define SET_ERROR_AND_WARN_ONCE(err, ...) \
450     do { \
451 	if (err == 0) { \
452 	    err = 1; \
453 	    msg_warn(__VA_ARGS__); \
454 	} \
455     } while (0)
456 
457     while ((host = dict_mysql_get_active(dict_mysql)) != NULL) {
458 
459 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
460 
461 	/*
462 	 * The active host is used to escape strings in the context of the
463 	 * active connection's character encoding.
464 	 */
465 	dict_mysql->active_host = host;
466 	VSTRING_RESET(query);
467 	VSTRING_TERMINATE(query);
468 	db_common_expand(dict_mysql->ctx, dict_mysql->query,
469 			 name, 0, query, dict_mysql_quote);
470 	dict_mysql->active_host = 0;
471 #endif
472 
473 	query_error = 0;
474 	errno = 0;
475 
476 	/*
477 	 * The query must complete.
478 	 */
479 	if (mysql_query(host->db, vstring_str(query)) != 0) {
480 	    query_error = 1;
481 	    msg_warn("%s:%s: query failed: %s",
482 		     dict_mysql->dict.type, dict_mysql->dict.name,
483 		     mysql_error(host->db));
484 	}
485 
486 	/*
487 	 * Collect all result sets to avoid synchronization errors.
488 	 */
489 	else {
490 	    int     next_res_status;
491 
492 	    do {
493 		MYSQL_RES *temp_result;
494 
495 		/*
496 		 * Keep the first result set. Reject multiple result sets.
497 		 */
498 		if ((temp_result = mysql_store_result(host->db)) != 0) {
499 		    if (first_result == 0) {
500 			first_result = temp_result;
501 		    } else {
502 			SET_ERROR_AND_WARN_ONCE(query_error,
503 				"%s:%s: query failed: multiple result sets "
504 					 "returning data are not supported",
505 						dict_mysql->dict.type,
506 						dict_mysql->dict.name);
507 			mysql_free_result(temp_result);
508 		    }
509 		}
510 
511 		/*
512 		 * No result: the mysql_field_count() function must return 0
513 		 * to indicate that mysql_store_result() completed normally.
514 		 */
515 		else if (mysql_field_count(host->db) != 0) {
516 		    SET_ERROR_AND_WARN_ONCE(query_error,
517 			     "%s:%s: query failed (mysql_store_result): %s",
518 					    dict_mysql->dict.type,
519 					    dict_mysql->dict.name,
520 					    mysql_error(host->db));
521 		}
522 
523 		/*
524 		 * Are there more results? -1 = no, 0 = yes, > 0 = error.
525 		 */
526 		if ((next_res_status = mysql_next_result(host->db)) > 0) {
527 		    SET_ERROR_AND_WARN_ONCE(query_error,
528 			      "%s:%s: query failed (mysql_next_result): %s",
529 					    dict_mysql->dict.type,
530 					    dict_mysql->dict.name,
531 					    mysql_error(host->db));
532 		}
533 	    } while (next_res_status == 0);
534 
535 	    /*
536 	     * Enforce the require_result_set setting.
537 	     */
538 	    if (first_result == 0 && dict_mysql->require_result_set) {
539 		SET_ERROR_AND_WARN_ONCE(query_error,
540 			 "%s:%s: query failed: query returned no result set"
541 					"(require_result_set = yes)",
542 					dict_mysql->dict.type,
543 					dict_mysql->dict.name);
544 	    }
545 	}
546 
547 	/*
548 	 * See what we got.
549 	 */
550 	if (query_error) {
551 	    plmysql_down_host(host);
552 	    if (errno == 0)
553 		errno = ENOTSUP;
554 	    if (first_result) {
555 		mysql_free_result(first_result);
556 		first_result = 0;
557 	    }
558 	} else {
559 	    if (msg_verbose)
560 		msg_info("%s:%s: successful query result from host %s",
561 			 dict_mysql->dict.type, dict_mysql->dict.name,
562 			 host->hostname);
563 	    event_request_timer(dict_mysql_event, (void *) host,
564 				IDLE_CONN_INTV);
565 	    break;
566 	}
567     }
568 
569     *result = first_result;
570     return (query_error == 0);
571 }
572 
573 /*
574  * plmysql_connect_single -
575  * used to reconnect to a single database when one is down or none is
576  * connected yet. Log all errors and set the stat field of host accordingly
577  */
plmysql_connect_single(DICT_MYSQL * dict_mysql,HOST * host)578 static void plmysql_connect_single(DICT_MYSQL *dict_mysql, HOST *host)
579 {
580     if ((host->db = mysql_init(NULL)) == NULL)
581 	msg_fatal("dict_mysql: insufficient memory");
582     if (dict_mysql->option_file)
583 	mysql_options(host->db, MYSQL_READ_DEFAULT_FILE, dict_mysql->option_file);
584     if (dict_mysql->option_group && dict_mysql->option_group[0])
585 	mysql_options(host->db, MYSQL_READ_DEFAULT_GROUP, dict_mysql->option_group);
586 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
587     if (dict_mysql->tls_key_file || dict_mysql->tls_cert_file ||
588 	dict_mysql->tls_CAfile || dict_mysql->tls_CApath || dict_mysql->tls_ciphers)
589 	mysql_ssl_set(host->db,
590 		      dict_mysql->tls_key_file, dict_mysql->tls_cert_file,
591 		      dict_mysql->tls_CAfile, dict_mysql->tls_CApath,
592 		      dict_mysql->tls_ciphers);
593 #if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
594     if (dict_mysql->tls_verify_cert != -1)
595 	mysql_options(host->db, DICT_MYSQL_SSL_VERIFY_SERVER_CERT,
596 		      &dict_mysql->tls_verify_cert);
597 #endif
598 #endif
599     if (mysql_real_connect(host->db,
600 			   (host->type == TYPEINET ? host->name : 0),
601 			   dict_mysql->username,
602 			   dict_mysql->password,
603 			   dict_mysql->dbname,
604 			   host->port,
605 			   (host->type == TYPEUNIX ? host->name : 0),
606 			   CLIENT_MULTI_RESULTS)) {
607 	if (msg_verbose)
608 	    msg_info("dict_mysql: successful connection to host %s",
609 		     host->hostname);
610 	host->stat = STATACTIVE;
611     } else {
612 	msg_warn("connect to mysql server %s: %s",
613 		 host->hostname, mysql_error(host->db));
614 	plmysql_down_host(host);
615     }
616 }
617 
618 /* plmysql_close_host - close an established MySQL connection */
plmysql_close_host(HOST * host)619 static void plmysql_close_host(HOST *host)
620 {
621     mysql_close(host->db);
622     host->db = 0;
623     host->stat = STATUNTRIED;
624 }
625 
626 /*
627  * plmysql_down_host - close a failed connection AND set a "stay away from
628  * this host" timer
629  */
plmysql_down_host(HOST * host)630 static void plmysql_down_host(HOST *host)
631 {
632     mysql_close(host->db);
633     host->db = 0;
634     host->ts = time((time_t *) 0) + RETRY_CONN_INTV;
635     host->stat = STATFAIL;
636     event_cancel_timer(dict_mysql_event, (void *) host);
637 }
638 
639 /* mysql_parse_config - parse mysql configuration file */
640 
mysql_parse_config(DICT_MYSQL * dict_mysql,const char * mysqlcf)641 static void mysql_parse_config(DICT_MYSQL *dict_mysql, const char *mysqlcf)
642 {
643     const char *myname = "mysql_parse_config";
644     CFG_PARSER *p = dict_mysql->parser;
645     VSTRING *buf;
646     char   *hosts;
647 
648     dict_mysql->username = cfg_get_str(p, "user", "", 0, 0);
649     dict_mysql->password = cfg_get_str(p, "password", "", 0, 0);
650     dict_mysql->dbname = cfg_get_str(p, "dbname", "", 1, 0);
651     dict_mysql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0);
652     dict_mysql->option_file = cfg_get_str(p, "option_file", NULL, 0, 0);
653     dict_mysql->option_group = cfg_get_str(p, "option_group", "client", 0, 0);
654 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
655     dict_mysql->tls_key_file = cfg_get_str(p, "tls_key_file", NULL, 0, 0);
656     dict_mysql->tls_cert_file = cfg_get_str(p, "tls_cert_file", NULL, 0, 0);
657     dict_mysql->tls_CAfile = cfg_get_str(p, "tls_CAfile", NULL, 0, 0);
658     dict_mysql->tls_CApath = cfg_get_str(p, "tls_CApath", NULL, 0, 0);
659     dict_mysql->tls_ciphers = cfg_get_str(p, "tls_ciphers", NULL, 0, 0);
660 #if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
661     dict_mysql->tls_verify_cert = cfg_get_bool(p, "tls_verify_cert", -1);
662 #endif
663 #endif
664     dict_mysql->require_result_set = cfg_get_bool(p, "require_result_set", 1);
665 
666     /*
667      * XXX: The default should be non-zero for safety, but that is not
668      * backwards compatible.
669      */
670     dict_mysql->expansion_limit = cfg_get_int(dict_mysql->parser,
671 					      "expansion_limit", 0, 0, 0);
672 
673     if ((dict_mysql->query = cfg_get_str(p, "query", NULL, 0, 0)) == 0) {
674 
675 	/*
676 	 * No query specified -- fallback to building it from components (old
677 	 * style "select %s from %s where %s")
678 	 */
679 	buf = vstring_alloc(64);
680 	db_common_sql_build_query(buf, p);
681 	dict_mysql->query = vstring_export(buf);
682     }
683 
684     /*
685      * Must parse all templates before we can use db_common_expand()
686      */
687     dict_mysql->ctx = 0;
688     (void) db_common_parse(&dict_mysql->dict, &dict_mysql->ctx,
689 			   dict_mysql->query, 1);
690     (void) db_common_parse(0, &dict_mysql->ctx, dict_mysql->result_format, 0);
691     db_common_parse_domain(p, dict_mysql->ctx);
692 
693     /*
694      * Maps that use substring keys should only be used with the full input
695      * key.
696      */
697     if (db_common_dict_partial(dict_mysql->ctx))
698 	dict_mysql->dict.flags |= DICT_FLAG_PATTERN;
699     else
700 	dict_mysql->dict.flags |= DICT_FLAG_FIXED;
701     if (dict_mysql->dict.flags & DICT_FLAG_FOLD_FIX)
702 	dict_mysql->dict.fold_buf = vstring_alloc(10);
703 
704     hosts = cfg_get_str(p, "hosts", "", 0, 0);
705 
706     dict_mysql->hosts = argv_split(hosts, CHARS_COMMA_SP);
707     if (dict_mysql->hosts->argc == 0) {
708 	argv_add(dict_mysql->hosts, "localhost", ARGV_END);
709 	argv_terminate(dict_mysql->hosts);
710 	if (msg_verbose)
711 	    msg_info("%s: %s: no hostnames specified, defaulting to '%s'",
712 		     myname, mysqlcf, dict_mysql->hosts->argv[0]);
713     }
714     myfree(hosts);
715 }
716 
717 /* dict_mysql_open - open MYSQL data base */
718 
dict_mysql_open(const char * name,int open_flags,int dict_flags)719 DICT   *dict_mysql_open(const char *name, int open_flags, int dict_flags)
720 {
721     DICT_MYSQL *dict_mysql;
722     CFG_PARSER *parser;
723 
724     /*
725      * Sanity checks.
726      */
727     if (open_flags != O_RDONLY)
728 	return (dict_surrogate(DICT_TYPE_MYSQL, name, open_flags, dict_flags,
729 			       "%s:%s map requires O_RDONLY access mode",
730 			       DICT_TYPE_MYSQL, name));
731 
732     /*
733      * Open the configuration file.
734      */
735     if ((parser = cfg_parser_alloc(name)) == 0)
736 	return (dict_surrogate(DICT_TYPE_MYSQL, name, open_flags, dict_flags,
737 			       "open %s: %m", name));
738 
739     dict_mysql = (DICT_MYSQL *) dict_alloc(DICT_TYPE_MYSQL, name,
740 					   sizeof(DICT_MYSQL));
741     dict_mysql->dict.lookup = dict_mysql_lookup;
742     dict_mysql->dict.close = dict_mysql_close;
743     dict_mysql->dict.flags = dict_flags;
744     dict_mysql->parser = parser;
745     mysql_parse_config(dict_mysql, name);
746 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
747     dict_mysql->active_host = 0;
748 #endif
749     dict_mysql->pldb = plmysql_init(dict_mysql->hosts);
750     if (dict_mysql->pldb == NULL)
751 	msg_fatal("couldn't initialize pldb!\n");
752     dict_mysql->dict.owner = cfg_get_owner(dict_mysql->parser);
753     return (DICT_DEBUG (&dict_mysql->dict));
754 }
755 
756 /*
757  * plmysql_init - initialize a MYSQL database.
758  *		    Return NULL on failure, or a PLMYSQL * on success.
759  */
plmysql_init(ARGV * hosts)760 static PLMYSQL *plmysql_init(ARGV *hosts)
761 {
762     PLMYSQL *PLDB;
763     int     i;
764 
765     if ((PLDB = (PLMYSQL *) mymalloc(sizeof(PLMYSQL))) == 0)
766 	msg_fatal("mymalloc of pldb failed");
767 
768     PLDB->len_hosts = hosts->argc;
769     if ((PLDB->db_hosts = (HOST **) mymalloc(sizeof(HOST *) * hosts->argc)) == 0)
770 	return (0);
771     for (i = 0; i < hosts->argc; i++)
772 	PLDB->db_hosts[i] = host_init(hosts->argv[i]);
773 
774     return PLDB;
775 }
776 
777 
778 /* host_init - initialize HOST structure */
host_init(const char * hostname)779 static HOST *host_init(const char *hostname)
780 {
781     const char *myname = "mysql host_init";
782     HOST   *host = (HOST *) mymalloc(sizeof(HOST));
783     const char *d = hostname;
784     char   *s;
785 
786     host->db = 0;
787     host->hostname = mystrdup(hostname);
788     host->port = 0;
789     host->stat = STATUNTRIED;
790     host->ts = 0;
791 
792     /*
793      * Ad-hoc parsing code. Expect "unix:pathname" or "inet:host:port", where
794      * both "inet:" and ":port" are optional.
795      */
796     if (strncmp(d, "unix:", 5) == 0) {
797 	d += 5;
798 	host->type = TYPEUNIX;
799     } else {
800 	if (strncmp(d, "inet:", 5) == 0)
801 	    d += 5;
802 	host->type = TYPEINET;
803     }
804     host->name = mystrdup(d);
805     if ((s = split_at_right(host->name, ':')) != 0)
806 	host->port = ntohs(find_inet_port(s, "tcp"));
807     if (strcasecmp(host->name, "localhost") == 0) {
808 	/* The MySQL way: this will actually connect over the UNIX socket */
809 	myfree(host->name);
810 	host->name = 0;
811 	host->type = TYPEUNIX;
812     }
813     if (msg_verbose > 1)
814 	msg_info("%s: host=%s, port=%d, type=%s", myname,
815 		 host->name ? host->name : "localhost",
816 		 host->port, host->type == TYPEUNIX ? "unix" : "inet");
817     return host;
818 }
819 
820 /* dict_mysql_close - close MYSQL database */
821 
dict_mysql_close(DICT * dict)822 static void dict_mysql_close(DICT *dict)
823 {
824     DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;
825 
826     plmysql_dealloc(dict_mysql->pldb);
827     cfg_parser_free(dict_mysql->parser);
828     myfree(dict_mysql->username);
829     myfree(dict_mysql->password);
830     myfree(dict_mysql->dbname);
831     myfree(dict_mysql->query);
832     myfree(dict_mysql->result_format);
833     if (dict_mysql->option_file)
834 	myfree(dict_mysql->option_file);
835     if (dict_mysql->option_group)
836 	myfree(dict_mysql->option_group);
837 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
838     if (dict_mysql->tls_key_file)
839 	myfree(dict_mysql->tls_key_file);
840     if (dict_mysql->tls_cert_file)
841 	myfree(dict_mysql->tls_cert_file);
842     if (dict_mysql->tls_CAfile)
843 	myfree(dict_mysql->tls_CAfile);
844     if (dict_mysql->tls_CApath)
845 	myfree(dict_mysql->tls_CApath);
846     if (dict_mysql->tls_ciphers)
847 	myfree(dict_mysql->tls_ciphers);
848 #endif
849     if (dict_mysql->hosts)
850 	argv_free(dict_mysql->hosts);
851     if (dict_mysql->ctx)
852 	db_common_free_ctx(dict_mysql->ctx);
853     if (dict->fold_buf)
854 	vstring_free(dict->fold_buf);
855     dict_free(dict);
856 }
857 
858 /* plmysql_dealloc - free memory associated with PLMYSQL close databases */
plmysql_dealloc(PLMYSQL * PLDB)859 static void plmysql_dealloc(PLMYSQL *PLDB)
860 {
861     int     i;
862 
863     for (i = 0; i < PLDB->len_hosts; i++) {
864 	event_cancel_timer(dict_mysql_event, (void *) (PLDB->db_hosts[i]));
865 	if (PLDB->db_hosts[i]->db)
866 	    mysql_close(PLDB->db_hosts[i]->db);
867 	myfree(PLDB->db_hosts[i]->hostname);
868 	if (PLDB->db_hosts[i]->name)
869 	    myfree(PLDB->db_hosts[i]->name);
870 	myfree((void *) PLDB->db_hosts[i]);
871     }
872     myfree((void *) PLDB->db_hosts);
873     myfree((void *) (PLDB));
874 }
875 
876 #endif
877