diff options
Diffstat (limited to 'src/global/dict_mysql.c')
-rw-r--r-- | src/global/dict_mysql.c | 963 |
1 files changed, 963 insertions, 0 deletions
diff --git a/src/global/dict_mysql.c b/src/global/dict_mysql.c new file mode 100644 index 0000000..735e195 --- /dev/null +++ b/src/global/dict_mysql.c @@ -0,0 +1,963 @@ +/*++ +/* NAME +/* dict_mysql 3 +/* SUMMARY +/* dictionary manager interface to MySQL databases +/* SYNOPSIS +/* #include <dict_mysql.h> +/* +/* DICT *dict_mysql_open(name, open_flags, dict_flags) +/* const char *name; +/* int open_flags; +/* int dict_flags; +/* DESCRIPTION +/* dict_mysql_open() creates a dictionary of type 'mysql'. This +/* dictionary is an interface for the postfix key->value mappings +/* to mysql. The result is a pointer to the installed dictionary, +/* or a null pointer in case of problems. +/* +/* The mysql dictionary can manage multiple connections to different +/* sql servers on different hosts. It assumes that the underlying data +/* on each host is identical (mirrored) and maintains one connection +/* at any given time. If any connection fails, any other available +/* ones will be opened and used. The intent of this feature is to eliminate +/* a single point of failure for mail systems that would otherwise rely +/* on a single mysql server. +/* .PP +/* Arguments: +/* .IP name +/* Either the path to the MySQL configuration file (if it starts +/* with '/' or '.'), or the prefix which will be used to obtain +/* main.cf configuration parameters for this search. +/* +/* In the first case, the configuration parameters below are +/* specified in the file as \fIname\fR=\fIvalue\fR pairs. +/* +/* In the second case, the configuration parameters are +/* prefixed with the value of \fIname\fR and an underscore, +/* and they are specified in main.cf. For example, if this +/* value is \fImysqlsource\fR, the parameters would look like +/* \fImysqlsource_user\fR, \fImysqlsource_table\fR, and so on. +/* +/* .IP other_name +/* reference for outside use. +/* .IP open_flags +/* Must be O_RDONLY. +/* .IP dict_flags +/* See dict_open(3). +/* .PP +/* Configuration parameters: +/* .IP user +/* Username for connecting to the database. +/* .IP password +/* Password for the above. +/* .IP dbname +/* Name of the database. +/* .IP domain +/* List of domains the queries should be restricted to. If +/* specified, only FQDN addresses whose domain parts matching this +/* list will be queried against the SQL database. Lookups for +/* partial addresses are also suppressed. This can significantly +/* reduce the query load on the server. +/* .IP query +/* Query template, before the query is actually issued, variable +/* substitutions are performed. See mysql_table(5) for details. If +/* No query is specified, the legacy variables \fItable\fR, +/* \fIselect_field\fR, \fIwhere_field\fR and \fIadditional_conditions\fR +/* are used to construct the query template. +/* .IP result_format +/* The format used to expand results from queries. Substitutions +/* are performed as described in mysql_table(5). Defaults to returning +/* the lookup result unchanged. +/* .IP expansion_limit +/* Limit (if any) on the total number of lookup result values. Lookups which +/* exceed the limit fail with dict->error=DICT_ERR_RETRY. Note that each +/* non-empty (and non-NULL) column of a multi-column result row counts as +/* one result. +/* .IP table +/* When \fIquery\fR is not set, name of the table used to construct the +/* query string. This provides compatibility with older releases. +/* .IP select_field +/* When \fIquery\fR is not set, name of the result field used to +/* construct the query string. This provides compatibility with older +/* releases. +/* .IP where_field +/* When \fIquery\fR is not set, name of the where clause field used to +/* construct the query string. This provides compatibility with older +/* releases. +/* .IP additional_conditions +/* When \fIquery\fR is not set, additional where clause conditions used +/* to construct the query string. This provides compatibility with older +/* releases. +/* .IP hosts +/* List of hosts to connect to. +/* .IP option_file +/* Read options from the given file instead of the default my.cnf +/* location. +/* .IP option_group +/* Read options from the given group. +/* .IP require_result_set +/* Require that every query produces a result set. +/* .IP tls_cert_file +/* File containing client's X509 certificate. +/* .IP tls_key_file +/* File containing the private key corresponding to \fItls_cert_file\fR. +/* .IP tls_CAfile +/* File containing certificates for all of the X509 Certification +/* Authorities the client will recognize. Takes precedence over +/* \fItls_CApath\fR. +/* .IP tls_CApath +/* Directory containing X509 Certification Authority certificates +/* in separate individual files. +/* .IP tls_verify_cert +/* Verify that the server's name matches the common name of the +/* certificate. +/* .PP +/* For example, if you want the map to reference databases of +/* the name "your_db" and execute a query like this: select +/* forw_addr from aliases where alias like '<some username>' +/* against any database called "vmailer_info" located on hosts +/* host1.some.domain and host2.some.domain, logging in as user +/* "vmailer" and password "passwd" then the configuration file +/* should read: +/* .PP +/* user = vmailer +/* .br +/* password = passwd +/* .br +/* dbname = vmailer_info +/* .br +/* table = aliases +/* .br +/* select_field = forw_addr +/* .br +/* where_field = alias +/* .br +/* hosts = host1.some.domain host2.some.domain +/* .PP +/* SEE ALSO +/* dict(3) generic dictionary manager +/* AUTHOR(S) +/* Scott Cotton, Joshua Marcus +/* IC Group, Inc. +/* scott@icgroup.com +/* +/* Liviu Daia +/* Institute of Mathematics of the Romanian Academy +/* P.O. BOX 1-764 +/* RO-014700 Bucharest, ROMANIA +/* +/* John Fawcett +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ +#include "sys_defs.h" + +#ifdef HAS_MYSQL +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <netdb.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <syslog.h> +#include <time.h> +#include <mysql.h> +#include <limits.h> +#include <errno.h> + +#ifdef STRCASECMP_IN_STRINGS_H +#include <strings.h> +#endif + +/* Utility library. */ + +#include "dict.h" +#include "msg.h" +#include "mymalloc.h" +#include "argv.h" +#include "vstring.h" +#include "split_at.h" +#include "find_inet.h" +#include "myrand.h" +#include "events.h" +#include "stringops.h" + +/* Global library. */ + +#include "cfg_parser.h" +#include "db_common.h" + +/* Application-specific. */ + +#include "dict_mysql.h" + +/* MySQL 8.x API change */ + +#if defined(MARIADB_BASE_VERSION) && MYSQL_VERSION_ID >= 50023 +#define DICT_MYSQL_SSL_VERIFY_SERVER_CERT MYSQL_OPT_SSL_VERIFY_SERVER_CERT +#elif MYSQL_VERSION_ID >= 80000 +#define DICT_MYSQL_SSL_VERIFY_SERVER_CERT MYSQL_OPT_SSL_MODE +#endif + +/* need some structs to help organize things */ +typedef struct { + MYSQL *db; + char *hostname; + char *name; + unsigned port; + unsigned type; /* TYPEUNIX | TYPEINET */ + unsigned stat; /* STATUNTRIED | STATFAIL | STATCUR */ + time_t ts; /* used for attempting reconnection + * every so often if a host is down */ +} HOST; + +typedef struct { + int len_hosts; /* number of hosts */ + HOST **db_hosts; /* the hosts on which the databases + * reside */ +} PLMYSQL; + +typedef struct { + DICT dict; + CFG_PARSER *parser; + char *query; + char *result_format; + char *option_file; + char *option_group; + void *ctx; + int expansion_limit; + char *username; + char *password; + char *dbname; + ARGV *hosts; + PLMYSQL *pldb; +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 + HOST *active_host; + char *tls_cert_file; + char *tls_key_file; + char *tls_CAfile; + char *tls_CApath; + char *tls_ciphers; +#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT) + int tls_verify_cert; +#endif +#endif + int require_result_set; +} DICT_MYSQL; + +#define STATACTIVE (1<<0) +#define STATFAIL (1<<1) +#define STATUNTRIED (1<<2) + +#define TYPEUNIX (1<<0) +#define TYPEINET (1<<1) + +#define RETRY_CONN_MAX 100 +#define RETRY_CONN_INTV 60 /* 1 minute */ +#define IDLE_CONN_INTV 60 /* 1 minute */ + +/* internal function declarations */ +static PLMYSQL *plmysql_init(ARGV *); +static int plmysql_query(DICT_MYSQL *, const char *, VSTRING *, MYSQL_RES **); +static void plmysql_dealloc(PLMYSQL *); +static void plmysql_close_host(HOST *); +static void plmysql_down_host(HOST *); +static void plmysql_connect_single(DICT_MYSQL *, HOST *); +static const char *dict_mysql_lookup(DICT *, const char *); +DICT *dict_mysql_open(const char *, int, int); +static void dict_mysql_close(DICT *); +static void mysql_parse_config(DICT_MYSQL *, const char *); +static HOST *host_init(const char *); + +/* dict_mysql_quote - escape SQL metacharacters in input string */ + +static void dict_mysql_quote(DICT *dict, const char *name, VSTRING *result) +{ + DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict; + int len = strlen(name); + int buflen; + + /* + * We won't get integer overflows in 2*len + 1, because Postfix input + * keys have reasonable size limits, better safe than sorry. + */ + if (len > (INT_MAX - VSTRING_LEN(result) - 1) / 2) + msg_panic("dict_mysql_quote: integer overflow in %lu+2*%d+1", + (unsigned long) VSTRING_LEN(result), len); + buflen = 2 * len + 1; + VSTRING_SPACE(result, buflen); + +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 + if (dict_mysql->active_host) + mysql_real_escape_string(dict_mysql->active_host->db, + vstring_end(result), name, len); + else +#endif + mysql_escape_string(vstring_end(result), name, len); + + VSTRING_SKIP(result); +} + +/* dict_mysql_lookup - find database entry */ + +static const char *dict_mysql_lookup(DICT *dict, const char *name) +{ + const char *myname = "dict_mysql_lookup"; + DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict; + MYSQL_RES *query_res; + MYSQL_ROW row; + static VSTRING *result; + static VSTRING *query; + int i; + int j; + int numrows; + int expansion; + const char *r; + db_quote_callback_t quote_func = dict_mysql_quote; + int domain_rc; + + dict->error = 0; + + /* + * Don't frustrate future attempts to make Postfix UTF-8 transparent. + */ +#ifdef SNAPSHOT + if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0 + && !valid_utf8_string(name, strlen(name))) { + if (msg_verbose) + msg_info("%s: %s: Skipping lookup of non-UTF-8 key '%s'", + myname, dict_mysql->parser->name, name); + return (0); + } +#endif + + /* + * Optionally fold the key. + */ + if (dict->flags & DICT_FLAG_FOLD_FIX) { + if (dict->fold_buf == 0) + dict->fold_buf = vstring_alloc(10); + vstring_strcpy(dict->fold_buf, name); + name = lowercase(vstring_str(dict->fold_buf)); + } + + /* + * If there is a domain list for this map, then only search for addresses + * in domains on the list. This can significantly reduce the load on the + * server. + */ + if ((domain_rc = db_common_check_domain(dict_mysql->ctx, name)) == 0) { + if (msg_verbose) + msg_info("%s: Skipping lookup of '%s'", myname, name); + return (0); + } + if (domain_rc < 0) { + msg_warn("%s:%s 'domain' pattern match failed for '%s'", + dict->type, dict->name, name); + DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0); + } +#define INIT_VSTR(buf, len) do { \ + if (buf == 0) \ + buf = vstring_alloc(len); \ + VSTRING_RESET(buf); \ + VSTRING_TERMINATE(buf); \ + } while (0) + + INIT_VSTR(query, 10); + + /* + * Suppress the lookup if the query expansion is empty + * + * This initial expansion is outside the context of any specific host + * connection, we just want to check the key pre-requisites, so when + * quoting happens separately for each connection, we don't bother with + * quoting... + */ +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 + quote_func = 0; +#endif + if (!db_common_expand(dict_mysql->ctx, dict_mysql->query, + name, 0, query, quote_func)) + return (0); + + /* do the query - set dict->error & cleanup if there's an error */ + if (plmysql_query(dict_mysql, name, query, &query_res) == 0) { + dict->error = DICT_ERR_RETRY; + return (0); + } + if (query_res == 0) + return (0); + numrows = mysql_num_rows(query_res); + if (msg_verbose) + msg_info("%s: retrieved %d rows", myname, numrows); + if (numrows == 0) { + mysql_free_result(query_res); + return 0; + } + INIT_VSTR(result, 10); + + for (expansion = i = 0; i < numrows && dict->error == 0; i++) { + row = mysql_fetch_row(query_res); + for (j = 0; j < mysql_num_fields(query_res); j++) { + if (db_common_expand(dict_mysql->ctx, dict_mysql->result_format, + row[j], name, result, 0) + && dict_mysql->expansion_limit > 0 + && ++expansion > dict_mysql->expansion_limit) { + msg_warn("%s: %s: Expansion limit exceeded for key: '%s'", + myname, dict_mysql->parser->name, name); + dict->error = DICT_ERR_RETRY; + break; + } + } + } + mysql_free_result(query_res); + r = vstring_str(result); + return ((dict->error == 0 && *r) ? r : 0); +} + +/* dict_mysql_check_stat - check the status of a host */ + +static int dict_mysql_check_stat(HOST *host, unsigned stat, unsigned type, + time_t t) +{ + if ((host->stat & stat) && (!type || host->type & type)) { + /* try not to hammer the dead hosts too often */ + if (host->stat == STATFAIL && host->ts > 0 && host->ts >= t) + return 0; + return 1; + } + return 0; +} + +/* dict_mysql_find_host - find a host with the given status */ + +static HOST *dict_mysql_find_host(PLMYSQL *PLDB, unsigned stat, unsigned type) +{ + time_t t; + int count = 0; + int idx; + int i; + + t = time((time_t *) 0); + for (i = 0; i < PLDB->len_hosts; i++) { + if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t)) + count++; + } + + if (count) { + idx = (count > 1) ? + 1 + count * (double) myrand() / (1.0 + RAND_MAX) : 1; + + for (i = 0; i < PLDB->len_hosts; i++) { + if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t) && + --idx == 0) + return PLDB->db_hosts[i]; + } + } + return 0; +} + +/* dict_mysql_get_active - get an active connection */ + +static HOST *dict_mysql_get_active(DICT_MYSQL *dict_mysql) +{ + const char *myname = "dict_mysql_get_active"; + PLMYSQL *PLDB = dict_mysql->pldb; + HOST *host; + int count = RETRY_CONN_MAX; + + /* Try the active connections first; prefer the ones to UNIX sockets. */ + if ((host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEUNIX)) != NULL || + (host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEINET)) != NULL) { + if (msg_verbose) + msg_info("%s: found active connection to host %s", myname, + host->hostname); + return host; + } + + /* + * Try the remaining hosts. "count" is a safety net, in case the loop + * takes more than RETRY_CONN_INTV and the dead hosts are no longer + * skipped. + */ + while (--count > 0 && + ((host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL, + TYPEUNIX)) != NULL || + (host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL, + TYPEINET)) != NULL)) { + if (msg_verbose) + msg_info("%s: attempting to connect to host %s", myname, + host->hostname); + plmysql_connect_single(dict_mysql, host); + if (host->stat == STATACTIVE) + return host; + } + + /* bad news... */ + return 0; +} + +/* dict_mysql_event - callback: close idle connections */ + +static void dict_mysql_event(int unused_event, void *context) +{ + HOST *host = (HOST *) context; + + if (host->db) + plmysql_close_host(host); +} + +/* + * plmysql_query - process a MySQL query. Return 'true' on success. + * On failure, log failure and try other db instances. + * on failure of all db instances, return 'false'; + * close unnecessary active connections + */ + +static int plmysql_query(DICT_MYSQL *dict_mysql, + const char *name, + VSTRING *query, + MYSQL_RES **result) +{ + HOST *host; + MYSQL_RES *first_result = 0; + int query_error = 1; + + /* + * Helper to avoid spamming the log with warnings. + */ +#define SET_ERROR_AND_WARN_ONCE(err, ...) \ + do { \ + if (err == 0) { \ + err = 1; \ + msg_warn(__VA_ARGS__); \ + } \ + } while (0) + + while ((host = dict_mysql_get_active(dict_mysql)) != NULL) { + +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 + + /* + * The active host is used to escape strings in the context of the + * active connection's character encoding. + */ + dict_mysql->active_host = host; + VSTRING_RESET(query); + VSTRING_TERMINATE(query); + db_common_expand(dict_mysql->ctx, dict_mysql->query, + name, 0, query, dict_mysql_quote); + dict_mysql->active_host = 0; +#endif + + query_error = 0; + errno = 0; + + /* + * The query must complete. + */ + if (mysql_query(host->db, vstring_str(query)) != 0) { + query_error = 1; + msg_warn("%s:%s: query failed: %s", + dict_mysql->dict.type, dict_mysql->dict.name, + mysql_error(host->db)); + } + + /* + * Collect all result sets to avoid synchronization errors. + */ + else { + int next_res_status; + + do { + MYSQL_RES *temp_result; + + /* + * Keep the first result set. Reject multiple result sets. + */ + if ((temp_result = mysql_store_result(host->db)) != 0) { + if (first_result == 0) { + first_result = temp_result; + } else { + SET_ERROR_AND_WARN_ONCE(query_error, + "%s:%s: query failed: multiple result sets " + "returning data are not supported", + dict_mysql->dict.type, + dict_mysql->dict.name); + mysql_free_result(temp_result); + } + } + + /* + * No result: the mysql_field_count() function must return 0 + * to indicate that mysql_store_result() completed normally. + */ + else if (mysql_field_count(host->db) != 0) { + SET_ERROR_AND_WARN_ONCE(query_error, + "%s:%s: query failed (mysql_store_result): %s", + dict_mysql->dict.type, + dict_mysql->dict.name, + mysql_error(host->db)); + } + + /* + * Are there more results? -1 = no, 0 = yes, > 0 = error. + */ + if ((next_res_status = mysql_next_result(host->db)) > 0) { + SET_ERROR_AND_WARN_ONCE(query_error, + "%s:%s: query failed (mysql_next_result): %s", + dict_mysql->dict.type, + dict_mysql->dict.name, + mysql_error(host->db)); + } + } while (next_res_status == 0); + + /* + * Enforce the require_result_set setting. + */ + if (first_result == 0 && dict_mysql->require_result_set) { + SET_ERROR_AND_WARN_ONCE(query_error, + "%s:%s: query failed: query returned no result set" + "(require_result_set = yes)", + dict_mysql->dict.type, + dict_mysql->dict.name); + } + } + + /* + * See what we got. + */ + if (query_error) { + plmysql_down_host(host); + if (errno == 0) + errno = ENOTSUP; + if (first_result) { + mysql_free_result(first_result); + first_result = 0; + } + } else { + if (msg_verbose) + msg_info("%s:%s: successful query result from host %s", + dict_mysql->dict.type, dict_mysql->dict.name, + host->hostname); + event_request_timer(dict_mysql_event, (void *) host, + IDLE_CONN_INTV); + break; + } + } + + *result = first_result; + return (query_error == 0); +} + +/* + * plmysql_connect_single - + * used to reconnect to a single database when one is down or none is + * connected yet. Log all errors and set the stat field of host accordingly + */ +static void plmysql_connect_single(DICT_MYSQL *dict_mysql, HOST *host) +{ + if ((host->db = mysql_init(NULL)) == NULL) + msg_fatal("dict_mysql: insufficient memory"); + if (dict_mysql->option_file) + mysql_options(host->db, MYSQL_READ_DEFAULT_FILE, dict_mysql->option_file); + if (dict_mysql->option_group && dict_mysql->option_group[0]) + mysql_options(host->db, MYSQL_READ_DEFAULT_GROUP, dict_mysql->option_group); +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 + if (dict_mysql->tls_key_file || dict_mysql->tls_cert_file || + dict_mysql->tls_CAfile || dict_mysql->tls_CApath || dict_mysql->tls_ciphers) + mysql_ssl_set(host->db, + dict_mysql->tls_key_file, dict_mysql->tls_cert_file, + dict_mysql->tls_CAfile, dict_mysql->tls_CApath, + dict_mysql->tls_ciphers); +#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT) + if (dict_mysql->tls_verify_cert != -1) + mysql_options(host->db, DICT_MYSQL_SSL_VERIFY_SERVER_CERT, + &dict_mysql->tls_verify_cert); +#endif +#endif + if (mysql_real_connect(host->db, + (host->type == TYPEINET ? host->name : 0), + dict_mysql->username, + dict_mysql->password, + dict_mysql->dbname, + host->port, + (host->type == TYPEUNIX ? host->name : 0), + CLIENT_MULTI_RESULTS)) { + if (msg_verbose) + msg_info("dict_mysql: successful connection to host %s", + host->hostname); + host->stat = STATACTIVE; + } else { + msg_warn("connect to mysql server %s: %s", + host->hostname, mysql_error(host->db)); + plmysql_down_host(host); + } +} + +/* plmysql_close_host - close an established MySQL connection */ +static void plmysql_close_host(HOST *host) +{ + mysql_close(host->db); + host->db = 0; + host->stat = STATUNTRIED; +} + +/* + * plmysql_down_host - close a failed connection AND set a "stay away from + * this host" timer + */ +static void plmysql_down_host(HOST *host) +{ + mysql_close(host->db); + host->db = 0; + host->ts = time((time_t *) 0) + RETRY_CONN_INTV; + host->stat = STATFAIL; + event_cancel_timer(dict_mysql_event, (void *) host); +} + +/* mysql_parse_config - parse mysql configuration file */ + +static void mysql_parse_config(DICT_MYSQL *dict_mysql, const char *mysqlcf) +{ + const char *myname = "mysql_parse_config"; + CFG_PARSER *p = dict_mysql->parser; + VSTRING *buf; + char *hosts; + + dict_mysql->username = cfg_get_str(p, "user", "", 0, 0); + dict_mysql->password = cfg_get_str(p, "password", "", 0, 0); + dict_mysql->dbname = cfg_get_str(p, "dbname", "", 1, 0); + dict_mysql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0); + dict_mysql->option_file = cfg_get_str(p, "option_file", NULL, 0, 0); + dict_mysql->option_group = cfg_get_str(p, "option_group", "client", 0, 0); +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 + dict_mysql->tls_key_file = cfg_get_str(p, "tls_key_file", NULL, 0, 0); + dict_mysql->tls_cert_file = cfg_get_str(p, "tls_cert_file", NULL, 0, 0); + dict_mysql->tls_CAfile = cfg_get_str(p, "tls_CAfile", NULL, 0, 0); + dict_mysql->tls_CApath = cfg_get_str(p, "tls_CApath", NULL, 0, 0); + dict_mysql->tls_ciphers = cfg_get_str(p, "tls_ciphers", NULL, 0, 0); +#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT) + dict_mysql->tls_verify_cert = cfg_get_bool(p, "tls_verify_cert", -1); +#endif +#endif + dict_mysql->require_result_set = cfg_get_bool(p, "require_result_set", 1); + + /* + * XXX: The default should be non-zero for safety, but that is not + * backwards compatible. + */ + dict_mysql->expansion_limit = cfg_get_int(dict_mysql->parser, + "expansion_limit", 0, 0, 0); + + if ((dict_mysql->query = cfg_get_str(p, "query", NULL, 0, 0)) == 0) { + + /* + * No query specified -- fallback to building it from components (old + * style "select %s from %s where %s") + */ + buf = vstring_alloc(64); + db_common_sql_build_query(buf, p); + dict_mysql->query = vstring_export(buf); + } + + /* + * Must parse all templates before we can use db_common_expand() + */ + dict_mysql->ctx = 0; + (void) db_common_parse(&dict_mysql->dict, &dict_mysql->ctx, + dict_mysql->query, 1); + (void) db_common_parse(0, &dict_mysql->ctx, dict_mysql->result_format, 0); + db_common_parse_domain(p, dict_mysql->ctx); + + /* + * Maps that use substring keys should only be used with the full input + * key. + */ + if (db_common_dict_partial(dict_mysql->ctx)) + dict_mysql->dict.flags |= DICT_FLAG_PATTERN; + else + dict_mysql->dict.flags |= DICT_FLAG_FIXED; + if (dict_mysql->dict.flags & DICT_FLAG_FOLD_FIX) + dict_mysql->dict.fold_buf = vstring_alloc(10); + + hosts = cfg_get_str(p, "hosts", "", 0, 0); + + dict_mysql->hosts = argv_split(hosts, CHARS_COMMA_SP); + if (dict_mysql->hosts->argc == 0) { + argv_add(dict_mysql->hosts, "localhost", ARGV_END); + argv_terminate(dict_mysql->hosts); + if (msg_verbose) + msg_info("%s: %s: no hostnames specified, defaulting to '%s'", + myname, mysqlcf, dict_mysql->hosts->argv[0]); + } + myfree(hosts); +} + +/* dict_mysql_open - open MYSQL data base */ + +DICT *dict_mysql_open(const char *name, int open_flags, int dict_flags) +{ + DICT_MYSQL *dict_mysql; + CFG_PARSER *parser; + + /* + * Sanity checks. + */ + if (open_flags != O_RDONLY) + return (dict_surrogate(DICT_TYPE_MYSQL, name, open_flags, dict_flags, + "%s:%s map requires O_RDONLY access mode", + DICT_TYPE_MYSQL, name)); + + /* + * Open the configuration file. + */ + if ((parser = cfg_parser_alloc(name)) == 0) + return (dict_surrogate(DICT_TYPE_MYSQL, name, open_flags, dict_flags, + "open %s: %m", name)); + + dict_mysql = (DICT_MYSQL *) dict_alloc(DICT_TYPE_MYSQL, name, + sizeof(DICT_MYSQL)); + dict_mysql->dict.lookup = dict_mysql_lookup; + dict_mysql->dict.close = dict_mysql_close; + dict_mysql->dict.flags = dict_flags; + dict_mysql->parser = parser; + mysql_parse_config(dict_mysql, name); +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 + dict_mysql->active_host = 0; +#endif + dict_mysql->pldb = plmysql_init(dict_mysql->hosts); + if (dict_mysql->pldb == NULL) + msg_fatal("couldn't initialize pldb!\n"); + dict_mysql->dict.owner = cfg_get_owner(dict_mysql->parser); + return (DICT_DEBUG (&dict_mysql->dict)); +} + +/* + * plmysql_init - initialize a MYSQL database. + * Return NULL on failure, or a PLMYSQL * on success. + */ +static PLMYSQL *plmysql_init(ARGV *hosts) +{ + PLMYSQL *PLDB; + int i; + + if ((PLDB = (PLMYSQL *) mymalloc(sizeof(PLMYSQL))) == 0) + msg_fatal("mymalloc of pldb failed"); + + PLDB->len_hosts = hosts->argc; + if ((PLDB->db_hosts = (HOST **) mymalloc(sizeof(HOST *) * hosts->argc)) == 0) + return (0); + for (i = 0; i < hosts->argc; i++) + PLDB->db_hosts[i] = host_init(hosts->argv[i]); + + return PLDB; +} + + +/* host_init - initialize HOST structure */ +static HOST *host_init(const char *hostname) +{ + const char *myname = "mysql host_init"; + HOST *host = (HOST *) mymalloc(sizeof(HOST)); + const char *d = hostname; + char *s; + + host->db = 0; + host->hostname = mystrdup(hostname); + host->port = 0; + host->stat = STATUNTRIED; + host->ts = 0; + + /* + * Ad-hoc parsing code. Expect "unix:pathname" or "inet:host:port", where + * both "inet:" and ":port" are optional. + */ + if (strncmp(d, "unix:", 5) == 0) { + d += 5; + host->type = TYPEUNIX; + } else { + if (strncmp(d, "inet:", 5) == 0) + d += 5; + host->type = TYPEINET; + } + host->name = mystrdup(d); + if ((s = split_at_right(host->name, ':')) != 0) + host->port = ntohs(find_inet_port(s, "tcp")); + if (strcasecmp(host->name, "localhost") == 0) { + /* The MySQL way: this will actually connect over the UNIX socket */ + myfree(host->name); + host->name = 0; + host->type = TYPEUNIX; + } + if (msg_verbose > 1) + msg_info("%s: host=%s, port=%d, type=%s", myname, + host->name ? host->name : "localhost", + host->port, host->type == TYPEUNIX ? "unix" : "inet"); + return host; +} + +/* dict_mysql_close - close MYSQL database */ + +static void dict_mysql_close(DICT *dict) +{ + DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict; + + plmysql_dealloc(dict_mysql->pldb); + cfg_parser_free(dict_mysql->parser); + myfree(dict_mysql->username); + myfree(dict_mysql->password); + myfree(dict_mysql->dbname); + myfree(dict_mysql->query); + myfree(dict_mysql->result_format); + if (dict_mysql->option_file) + myfree(dict_mysql->option_file); + if (dict_mysql->option_group) + myfree(dict_mysql->option_group); +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000 + if (dict_mysql->tls_key_file) + myfree(dict_mysql->tls_key_file); + if (dict_mysql->tls_cert_file) + myfree(dict_mysql->tls_cert_file); + if (dict_mysql->tls_CAfile) + myfree(dict_mysql->tls_CAfile); + if (dict_mysql->tls_CApath) + myfree(dict_mysql->tls_CApath); + if (dict_mysql->tls_ciphers) + myfree(dict_mysql->tls_ciphers); +#endif + if (dict_mysql->hosts) + argv_free(dict_mysql->hosts); + if (dict_mysql->ctx) + db_common_free_ctx(dict_mysql->ctx); + if (dict->fold_buf) + vstring_free(dict->fold_buf); + dict_free(dict); +} + +/* plmysql_dealloc - free memory associated with PLMYSQL close databases */ +static void plmysql_dealloc(PLMYSQL *PLDB) +{ + int i; + + for (i = 0; i < PLDB->len_hosts; i++) { + event_cancel_timer(dict_mysql_event, (void *) (PLDB->db_hosts[i])); + if (PLDB->db_hosts[i]->db) + mysql_close(PLDB->db_hosts[i]->db); + myfree(PLDB->db_hosts[i]->hostname); + if (PLDB->db_hosts[i]->name) + myfree(PLDB->db_hosts[i]->name); + myfree((void *) PLDB->db_hosts[i]); + } + myfree((void *) PLDB->db_hosts); + myfree((void *) (PLDB)); +} + +#endif |