xref: /netbsd-src/external/ibm-public/postfix/dist/src/global/dict_mysql.c (revision d909946ca08dceb44d7d0f22ec9488679695d976)
1 /*	$NetBSD: dict_mysql.c,v 1.1.1.4 2014/07/06 19:27:50 tron 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 Certificate
107 /*      Authorities the client will recognize.  Takes precedence over
108 /*      \fItls_CApath\fR.
109 /* .IP tls_CApath
110 /*      Directory containing X509 Certificate 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 /*      certficate.
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\fR \fBhost2.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\fR \fBhost2.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 	DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0);
356 
357 #define INIT_VSTR(buf, len) do { \
358 	if (buf == 0) \
359 	    buf = vstring_alloc(len); \
360 	VSTRING_RESET(buf); \
361 	VSTRING_TERMINATE(buf); \
362     } while (0)
363 
364     INIT_VSTR(query, 10);
365 
366     /*
367      * Suppress the lookup if the query expansion is empty
368      *
369      * This initial expansion is outside the context of any specific host
370      * connection, we just want to check the key pre-requisites, so when
371      * quoting happens separately for each connection, we don't bother with
372      * quoting...
373      */
374 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
375     quote_func = 0;
376 #endif
377     if (!db_common_expand(dict_mysql->ctx, dict_mysql->query,
378 			  name, 0, query, quote_func))
379 	return (0);
380 
381     /* do the query - set dict->error & cleanup if there's an error */
382     if ((query_res = plmysql_query(dict_mysql, name, query)) == 0) {
383 	dict->error = DICT_ERR_RETRY;
384 	return (0);
385     }
386     numrows = mysql_num_rows(query_res);
387     if (msg_verbose)
388 	msg_info("%s: retrieved %d rows", myname, numrows);
389     if (numrows == 0) {
390 	mysql_free_result(query_res);
391 	return 0;
392     }
393     INIT_VSTR(result, 10);
394 
395     for (expansion = i = 0; i < numrows && dict->error == 0; i++) {
396 	row = mysql_fetch_row(query_res);
397 	for (j = 0; j < mysql_num_fields(query_res); j++) {
398 	    if (db_common_expand(dict_mysql->ctx, dict_mysql->result_format,
399 				 row[j], name, result, 0)
400 		&& dict_mysql->expansion_limit > 0
401 		&& ++expansion > dict_mysql->expansion_limit) {
402 		msg_warn("%s: %s: Expansion limit exceeded for key: '%s'",
403 			 myname, dict_mysql->parser->name, name);
404 		dict->error = DICT_ERR_RETRY;
405 		break;
406 	    }
407 	}
408     }
409     mysql_free_result(query_res);
410     r = vstring_str(result);
411     return ((dict->error == 0 && *r) ? r : 0);
412 }
413 
414 /* dict_mysql_check_stat - check the status of a host */
415 
416 static int dict_mysql_check_stat(HOST *host, unsigned stat, unsigned type,
417 				         time_t t)
418 {
419     if ((host->stat & stat) && (!type || host->type & type)) {
420 	/* try not to hammer the dead hosts too often */
421 	if (host->stat == STATFAIL && host->ts > 0 && host->ts >= t)
422 	    return 0;
423 	return 1;
424     }
425     return 0;
426 }
427 
428 /* dict_mysql_find_host - find a host with the given status */
429 
430 static HOST *dict_mysql_find_host(PLMYSQL *PLDB, unsigned stat, unsigned type)
431 {
432     time_t  t;
433     int     count = 0;
434     int     idx;
435     int     i;
436 
437     t = time((time_t *) 0);
438     for (i = 0; i < PLDB->len_hosts; i++) {
439 	if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t))
440 	    count++;
441     }
442 
443     if (count) {
444 	idx = (count > 1) ?
445 	    1 + count * (double) myrand() / (1.0 + RAND_MAX) : 1;
446 
447 	for (i = 0; i < PLDB->len_hosts; i++) {
448 	    if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t) &&
449 		--idx == 0)
450 		return PLDB->db_hosts[i];
451 	}
452     }
453     return 0;
454 }
455 
456 /* dict_mysql_get_active - get an active connection */
457 
458 static HOST *dict_mysql_get_active(DICT_MYSQL *dict_mysql)
459 {
460     const char *myname = "dict_mysql_get_active";
461     PLMYSQL *PLDB = dict_mysql->pldb;
462     HOST   *host;
463     int     count = RETRY_CONN_MAX;
464 
465     /* Try the active connections first; prefer the ones to UNIX sockets. */
466     if ((host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEUNIX)) != NULL ||
467 	(host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEINET)) != NULL) {
468 	if (msg_verbose)
469 	    msg_info("%s: found active connection to host %s", myname,
470 		     host->hostname);
471 	return host;
472     }
473 
474     /*
475      * Try the remaining hosts. "count" is a safety net, in case the loop
476      * takes more than RETRY_CONN_INTV and the dead hosts are no longer
477      * skipped.
478      */
479     while (--count > 0 &&
480 	   ((host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL,
481 					 TYPEUNIX)) != NULL ||
482 	    (host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL,
483 					 TYPEINET)) != NULL)) {
484 	if (msg_verbose)
485 	    msg_info("%s: attempting to connect to host %s", myname,
486 		     host->hostname);
487 	plmysql_connect_single(dict_mysql, host);
488 	if (host->stat == STATACTIVE)
489 	    return host;
490     }
491 
492     /* bad news... */
493     return 0;
494 }
495 
496 /* dict_mysql_event - callback: close idle connections */
497 
498 static void dict_mysql_event(int unused_event, char *context)
499 {
500     HOST   *host = (HOST *) context;
501 
502     if (host->db)
503 	plmysql_close_host(host);
504 }
505 
506 /*
507  * plmysql_query - process a MySQL query.  Return MYSQL_RES* on success.
508  *			On failure, log failure and try other db instances.
509  *			on failure of all db instances, return 0;
510  *			close unnecessary active connections
511  */
512 
513 static MYSQL_RES *plmysql_query(DICT_MYSQL *dict_mysql,
514 				        const char *name,
515 				        VSTRING *query)
516 {
517     HOST   *host;
518     MYSQL_RES *res = 0;
519 
520     while ((host = dict_mysql_get_active(dict_mysql)) != NULL) {
521 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
522 
523 	/*
524 	 * The active host is used to escape strings in the context of the
525 	 * active connection's character encoding.
526 	 */
527 	dict_mysql->active_host = host;
528 	VSTRING_RESET(query);
529 	VSTRING_TERMINATE(query);
530 	db_common_expand(dict_mysql->ctx, dict_mysql->query,
531 			 name, 0, query, dict_mysql_quote);
532 	dict_mysql->active_host = 0;
533 #endif
534 
535 	if (!(mysql_query(host->db, vstring_str(query)))) {
536 	    if ((res = mysql_store_result(host->db)) == 0) {
537 		msg_warn("mysql query failed: %s", mysql_error(host->db));
538 		plmysql_down_host(host);
539 	    } else {
540 		if (msg_verbose)
541 		    msg_info("dict_mysql: successful query from host %s", host->hostname);
542 		event_request_timer(dict_mysql_event, (char *) host, IDLE_CONN_INTV);
543 		break;
544 	    }
545 	} else {
546 	    msg_warn("mysql query failed: %s", mysql_error(host->db));
547 	    plmysql_down_host(host);
548 	}
549     }
550 
551     return res;
552 }
553 
554 /*
555  * plmysql_connect_single -
556  * used to reconnect to a single database when one is down or none is
557  * connected yet. Log all errors and set the stat field of host accordingly
558  */
559 static void plmysql_connect_single(DICT_MYSQL *dict_mysql, HOST *host)
560 {
561     if ((host->db = mysql_init(NULL)) == NULL)
562 	msg_fatal("dict_mysql: insufficient memory");
563     if (dict_mysql->option_file)
564 	mysql_options(host->db, MYSQL_READ_DEFAULT_FILE, dict_mysql->option_file);
565     if (dict_mysql->option_group)
566 	mysql_options(host->db, MYSQL_READ_DEFAULT_GROUP, dict_mysql->option_group);
567 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
568     if (dict_mysql->tls_key_file || dict_mysql->tls_cert_file ||
569 	dict_mysql->tls_CAfile || dict_mysql->tls_CApath || dict_mysql->tls_ciphers)
570 	mysql_ssl_set(host->db,
571 		      dict_mysql->tls_key_file, dict_mysql->tls_cert_file,
572 		      dict_mysql->tls_CAfile, dict_mysql->tls_CApath,
573 		      dict_mysql->tls_ciphers);
574 #if MYSQL_VERSION_ID >= 50023
575     if (dict_mysql->tls_verify_cert != -1)
576 	mysql_options(host->db, MYSQL_OPT_SSL_VERIFY_SERVER_CERT,
577 		      &dict_mysql->tls_verify_cert);
578 #endif
579 #endif
580     if (mysql_real_connect(host->db,
581 			   (host->type == TYPEINET ? host->name : 0),
582 			   dict_mysql->username,
583 			   dict_mysql->password,
584 			   dict_mysql->dbname,
585 			   host->port,
586 			   (host->type == TYPEUNIX ? host->name : 0),
587 			   0)) {
588 	if (msg_verbose)
589 	    msg_info("dict_mysql: successful connection to host %s",
590 		     host->hostname);
591 	host->stat = STATACTIVE;
592     } else {
593 	msg_warn("connect to mysql server %s: %s",
594 		 host->hostname, mysql_error(host->db));
595 	plmysql_down_host(host);
596     }
597 }
598 
599 /* plmysql_close_host - close an established MySQL connection */
600 static void plmysql_close_host(HOST *host)
601 {
602     mysql_close(host->db);
603     host->db = 0;
604     host->stat = STATUNTRIED;
605 }
606 
607 /*
608  * plmysql_down_host - close a failed connection AND set a "stay away from
609  * this host" timer
610  */
611 static void plmysql_down_host(HOST *host)
612 {
613     mysql_close(host->db);
614     host->db = 0;
615     host->ts = time((time_t *) 0) + RETRY_CONN_INTV;
616     host->stat = STATFAIL;
617     event_cancel_timer(dict_mysql_event, (char *) host);
618 }
619 
620 /* mysql_parse_config - parse mysql configuration file */
621 
622 static void mysql_parse_config(DICT_MYSQL *dict_mysql, const char *mysqlcf)
623 {
624     const char *myname = "mysql_parse_config";
625     CFG_PARSER *p = dict_mysql->parser;
626     VSTRING *buf;
627     char   *hosts;
628 
629     dict_mysql->username = cfg_get_str(p, "user", "", 0, 0);
630     dict_mysql->password = cfg_get_str(p, "password", "", 0, 0);
631     dict_mysql->dbname = cfg_get_str(p, "dbname", "", 1, 0);
632     dict_mysql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0);
633     dict_mysql->option_file = cfg_get_str(p, "option_file", NULL, 0, 0);
634     dict_mysql->option_group = cfg_get_str(p, "option_group", NULL, 0, 0);
635 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
636     dict_mysql->tls_key_file = cfg_get_str(p, "tls_key_file", NULL, 0, 0);
637     dict_mysql->tls_cert_file = cfg_get_str(p, "tls_cert_file", NULL, 0, 0);
638     dict_mysql->tls_CAfile = cfg_get_str(p, "tls_CAfile", NULL, 0, 0);
639     dict_mysql->tls_CApath = cfg_get_str(p, "tls_CApath", NULL, 0, 0);
640     dict_mysql->tls_ciphers = cfg_get_str(p, "tls_ciphers", NULL, 0, 0);
641 #if MYSQL_VERSION_ID >= 50023
642     dict_mysql->tls_verify_cert = cfg_get_bool(p, "tls_verify_cert", -1);
643 #endif
644 #endif
645 
646     /*
647      * XXX: The default should be non-zero for safety, but that is not
648      * backwards compatible.
649      */
650     dict_mysql->expansion_limit = cfg_get_int(dict_mysql->parser,
651 					      "expansion_limit", 0, 0, 0);
652 
653     if ((dict_mysql->query = cfg_get_str(p, "query", NULL, 0, 0)) == 0) {
654 
655 	/*
656 	 * No query specified -- fallback to building it from components (old
657 	 * style "select %s from %s where %s")
658 	 */
659 	buf = vstring_alloc(64);
660 	db_common_sql_build_query(buf, p);
661 	dict_mysql->query = vstring_export(buf);
662     }
663 
664     /*
665      * Must parse all templates before we can use db_common_expand()
666      */
667     dict_mysql->ctx = 0;
668     (void) db_common_parse(&dict_mysql->dict, &dict_mysql->ctx,
669 			   dict_mysql->query, 1);
670     (void) db_common_parse(0, &dict_mysql->ctx, dict_mysql->result_format, 0);
671     db_common_parse_domain(p, dict_mysql->ctx);
672 
673     /*
674      * Maps that use substring keys should only be used with the full input
675      * key.
676      */
677     if (db_common_dict_partial(dict_mysql->ctx))
678 	dict_mysql->dict.flags |= DICT_FLAG_PATTERN;
679     else
680 	dict_mysql->dict.flags |= DICT_FLAG_FIXED;
681     if (dict_mysql->dict.flags & DICT_FLAG_FOLD_FIX)
682 	dict_mysql->dict.fold_buf = vstring_alloc(10);
683 
684     hosts = cfg_get_str(p, "hosts", "", 0, 0);
685 
686     dict_mysql->hosts = argv_split(hosts, " ,\t\r\n");
687     if (dict_mysql->hosts->argc == 0) {
688 	argv_add(dict_mysql->hosts, "localhost", ARGV_END);
689 	argv_terminate(dict_mysql->hosts);
690 	if (msg_verbose)
691 	    msg_info("%s: %s: no hostnames specified, defaulting to '%s'",
692 		     myname, mysqlcf, dict_mysql->hosts->argv[0]);
693     }
694     myfree(hosts);
695 }
696 
697 /* dict_mysql_open - open MYSQL data base */
698 
699 DICT   *dict_mysql_open(const char *name, int open_flags, int dict_flags)
700 {
701     DICT_MYSQL *dict_mysql;
702     CFG_PARSER *parser;
703 
704     /*
705      * Sanity checks.
706      */
707     if (open_flags != O_RDONLY)
708 	return (dict_surrogate(DICT_TYPE_MYSQL, name, open_flags, dict_flags,
709 			       "%s:%s map requires O_RDONLY access mode",
710 			       DICT_TYPE_MYSQL, name));
711 
712     /*
713      * Open the configuration file.
714      */
715     if ((parser = cfg_parser_alloc(name)) == 0)
716 	return (dict_surrogate(DICT_TYPE_MYSQL, name, open_flags, dict_flags,
717 			       "open %s: %m", name));
718 
719     dict_mysql = (DICT_MYSQL *) dict_alloc(DICT_TYPE_MYSQL, name,
720 					   sizeof(DICT_MYSQL));
721     dict_mysql->dict.lookup = dict_mysql_lookup;
722     dict_mysql->dict.close = dict_mysql_close;
723     dict_mysql->dict.flags = dict_flags;
724     dict_mysql->parser = parser;
725     mysql_parse_config(dict_mysql, name);
726 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
727     dict_mysql->active_host = 0;
728 #endif
729     dict_mysql->pldb = plmysql_init(dict_mysql->hosts);
730     if (dict_mysql->pldb == NULL)
731 	msg_fatal("couldn't intialize pldb!\n");
732     dict_mysql->dict.owner = cfg_get_owner(dict_mysql->parser);
733     return (DICT_DEBUG (&dict_mysql->dict));
734 }
735 
736 /*
737  * plmysql_init - initalize a MYSQL database.
738  *		    Return NULL on failure, or a PLMYSQL * on success.
739  */
740 static PLMYSQL *plmysql_init(ARGV *hosts)
741 {
742     PLMYSQL *PLDB;
743     int     i;
744 
745     if ((PLDB = (PLMYSQL *) mymalloc(sizeof(PLMYSQL))) == 0)
746 	msg_fatal("mymalloc of pldb failed");
747 
748     PLDB->len_hosts = hosts->argc;
749     if ((PLDB->db_hosts = (HOST **) mymalloc(sizeof(HOST *) * hosts->argc)) == 0)
750 	return (0);
751     for (i = 0; i < hosts->argc; i++)
752 	PLDB->db_hosts[i] = host_init(hosts->argv[i]);
753 
754     return PLDB;
755 }
756 
757 
758 /* host_init - initialize HOST structure */
759 static HOST *host_init(const char *hostname)
760 {
761     const char *myname = "mysql host_init";
762     HOST   *host = (HOST *) mymalloc(sizeof(HOST));
763     const char *d = hostname;
764     char   *s;
765 
766     host->db = 0;
767     host->hostname = mystrdup(hostname);
768     host->port = 0;
769     host->stat = STATUNTRIED;
770     host->ts = 0;
771 
772     /*
773      * Ad-hoc parsing code. Expect "unix:pathname" or "inet:host:port", where
774      * both "inet:" and ":port" are optional.
775      */
776     if (strncmp(d, "unix:", 5) == 0) {
777 	d += 5;
778 	host->type = TYPEUNIX;
779     } else {
780 	if (strncmp(d, "inet:", 5) == 0)
781 	    d += 5;
782 	host->type = TYPEINET;
783     }
784     host->name = mystrdup(d);
785     if ((s = split_at_right(host->name, ':')) != 0)
786 	host->port = ntohs(find_inet_port(s, "tcp"));
787     if (strcasecmp(host->name, "localhost") == 0) {
788 	/* The MySQL way: this will actually connect over the UNIX socket */
789 	myfree(host->name);
790 	host->name = 0;
791 	host->type = TYPEUNIX;
792     }
793     if (msg_verbose > 1)
794 	msg_info("%s: host=%s, port=%d, type=%s", myname,
795 		 host->name ? host->name : "localhost",
796 		 host->port, host->type == TYPEUNIX ? "unix" : "inet");
797     return host;
798 }
799 
800 /* dict_mysql_close - close MYSQL database */
801 
802 static void dict_mysql_close(DICT *dict)
803 {
804     DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;
805 
806     plmysql_dealloc(dict_mysql->pldb);
807     cfg_parser_free(dict_mysql->parser);
808     myfree(dict_mysql->username);
809     myfree(dict_mysql->password);
810     myfree(dict_mysql->dbname);
811     myfree(dict_mysql->query);
812     myfree(dict_mysql->result_format);
813     if (dict_mysql->option_file)
814 	myfree(dict_mysql->option_file);
815     if (dict_mysql->option_group)
816 	myfree(dict_mysql->option_group);
817 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
818     if (dict_mysql->tls_key_file)
819 	myfree(dict_mysql->tls_key_file);
820     if (dict_mysql->tls_cert_file)
821 	myfree(dict_mysql->tls_cert_file);
822     if (dict_mysql->tls_CAfile)
823 	myfree(dict_mysql->tls_CAfile);
824     if (dict_mysql->tls_CApath)
825 	myfree(dict_mysql->tls_CApath);
826     if (dict_mysql->tls_ciphers)
827 	myfree(dict_mysql->tls_ciphers);
828 #endif
829     if (dict_mysql->hosts)
830 	argv_free(dict_mysql->hosts);
831     if (dict_mysql->ctx)
832 	db_common_free_ctx(dict_mysql->ctx);
833     if (dict->fold_buf)
834 	vstring_free(dict->fold_buf);
835     dict_free(dict);
836 }
837 
838 /* plmysql_dealloc - free memory associated with PLMYSQL close databases */
839 static void plmysql_dealloc(PLMYSQL *PLDB)
840 {
841     int     i;
842 
843     for (i = 0; i < PLDB->len_hosts; i++) {
844 	event_cancel_timer(dict_mysql_event, (char *) (PLDB->db_hosts[i]));
845 	if (PLDB->db_hosts[i]->db)
846 	    mysql_close(PLDB->db_hosts[i]->db);
847 	myfree(PLDB->db_hosts[i]->hostname);
848 	if (PLDB->db_hosts[i]->name)
849 	    myfree(PLDB->db_hosts[i]->name);
850 	myfree((char *) PLDB->db_hosts[i]);
851     }
852     myfree((char *) PLDB->db_hosts);
853     myfree((char *) (PLDB));
854 }
855 
856 #endif
857