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