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