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