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 */
123typedef 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
134typedef struct {
135    int     len_hosts;			/* number of hosts */
136    HOST  **db_hosts;			/* the hosts on which the databases
137					 * reside */
138} PLMYSQL;
139
140typedef 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 */
180static PLMYSQL *plmysql_init(ARGV *);
181static int plmysql_query(DICT_MYSQL *, const char *, VSTRING *, MYSQL_RES **);
182static void plmysql_dealloc(PLMYSQL *);
183static void plmysql_close_host(HOST *);
184static void plmysql_down_host(HOST *);
185static void plmysql_connect_single(DICT_MYSQL *, HOST *);
186static const char *dict_mysql_lookup(DICT *, const char *);
187DICT   *dict_mysql_open(const char *, int, int);
188static void dict_mysql_close(DICT *);
189static void mysql_parse_config(DICT_MYSQL *, const char *);
190static HOST *host_init(const char *);
191
192/* dict_mysql_quote - escape SQL metacharacters in input string */
193
194static 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
223static 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
340static 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
354static 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
382static 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
422static 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
437static 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 */
578static 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 */
619static 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 */
630static 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
641static 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
719DICT   *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 */
760static 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 */
779static 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
822static 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 */
859static 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