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