diff options
Diffstat (limited to '')
-rw-r--r-- | src/global/dict_pgsql.c | 924 |
1 files changed, 924 insertions, 0 deletions
diff --git a/src/global/dict_pgsql.c b/src/global/dict_pgsql.c new file mode 100644 index 0000000..8eac256 --- /dev/null +++ b/src/global/dict_pgsql.c @@ -0,0 +1,924 @@ +/*++ +/* NAME +/* dict_pgsql 3 +/* SUMMARY +/* dictionary manager interface to PostgreSQL databases +/* SYNOPSIS +/* #include <dict_pgsql.h> +/* +/* DICT *dict_pgsql_open(name, open_flags, dict_flags) +/* const char *name; +/* int open_flags; +/* int dict_flags; +/* DESCRIPTION +/* dict_pgsql_open() creates a dictionary of type 'pgsql'. This +/* dictionary is an interface for the postfix key->value mappings +/* to pgsql. The result is a pointer to the installed dictionary, +/* or a null pointer in case of problems. +/* +/* The pgsql dictionary can manage multiple connections to +/* different sql servers for the same database. It assumes that +/* the underlying data on each server 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 +/* pgsql server. +/* .PP +/* Arguments: +/* .IP name +/* Either the path to the PostgreSQL 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 \fIpgsqlsource\fR, the parameters would look like +/* \fIpgsqlsource_user\fR, \fIpgsqlsource_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 query +/* Query template. If not defined a default query template is constructed +/* from the legacy \fIselect_function\fR or failing that the \fItable\fR, +/* \fIselect_field\fR, \fIwhere_field\fR, and \fIadditional_conditions\fR +/* parameters. Before the query is issues, variable substitutions are +/* performed. See pgsql_table(5). +/* .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 result_format +/* The format used to expand results from queries. Substitutions +/* are performed as described in pgsql_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 select_function +/* When \fIquery\fR is not defined, the function to be used instead of +/* the default query based on the legacy \fItable\fR, \fIselect_field\fR, +/* \fIwhere_field\fR, and \fIadditional_conditions\fR parameters. +/* .IP table +/* When \fIquery\fR and \fIselect_function\fR are not defined, the name of the +/* FROM table used to construct the default query template, see pgsql_table(5). +/* .IP select_field +/* When \fIquery\fR and \fIselect_function\fR are not defined, the name of the +/* SELECT field used to construct the default query template, see pgsql_table(5). +/* .IP where_field +/* When \fIquery\fR and \fIselect_function\fR are not defined, the name of the +/* WHERE field used to construct the default query template, see pgsql_table(5). +/* .IP additional_conditions +/* When \fIquery\fR and \fIselect_function\fR are not defined, the name of the +/* additional text to add to the WHERE field in the default query template (this +/* usually begins with "and") see pgsql_table(5). +/* .IP hosts +/* List of hosts to connect to. +/* .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 "postfix_info" located on hosts +/* host1.some.domain and host2.some.domain, logging in as user +/* "postfix" and password "passwd" then the configuration file +/* should read: +/* .PP +/* user = postfix +/* .br +/* password = passwd +/* .br +/* dbname = postfix_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) +/* Aaron Sethman +/* androsyn@ratbox.org +/* +/* Based upon dict_mysql.c by +/* +/* Scott Cotton +/* IC Group, Inc. +/* scott@icgroup.com +/* +/* Joshua Marcus +/* IC Group, Inc. +/* josh@icgroup.com +/*--*/ + +/* System library. */ + +#include "sys_defs.h" + +#ifdef HAS_PGSQL +#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 <postgres_ext.h> +#include <libpq-fe.h> + +/* Utility library. */ + +#include "dict.h" +#include "msg.h" +#include "mymalloc.h" +#include "argv.h" +#include "vstring.h" +#include "split_at.h" +#include "myrand.h" +#include "events.h" +#include "stringops.h" + +/* Global library. */ + +#include "cfg_parser.h" +#include "db_common.h" + +/* Application-specific. */ + +#include "dict_pgsql.h" + +#define STATACTIVE (1<<0) +#define STATFAIL (1<<1) +#define STATUNTRIED (1<<2) + +#define TYPEUNIX (1<<0) +#define TYPEINET (1<<1) +#define TYPECONNSTRING (1<<2) + +#define RETRY_CONN_MAX 100 +#define RETRY_CONN_INTV 60 /* 1 minute */ +#define IDLE_CONN_INTV 60 /* 1 minute */ + +typedef struct { + PGconn *db; + char *hostname; + char *name; + char *port; + unsigned type; /* TYPEUNIX | TYPEINET | TYPECONNSTRING */ + unsigned stat; /* STATUNTRIED | STATFAIL | STATCUR */ + time_t ts; /* used for attempting reconnection */ +} HOST; + +typedef struct { + int len_hosts; /* number of hosts */ + HOST **db_hosts; /* hosts on which databases reside */ +} PLPGSQL; + +typedef struct { + DICT dict; + CFG_PARSER *parser; + char *query; + char *result_format; + void *ctx; + int expansion_limit; + char *username; + char *password; + char *dbname; + char *table; + ARGV *hosts; + PLPGSQL *pldb; + HOST *active_host; +} DICT_PGSQL; + + +/* Just makes things a little easier for me.. */ +#define PGSQL_RES PGresult + +/* internal function declarations */ +static PLPGSQL *plpgsql_init(ARGV *); +static PGSQL_RES *plpgsql_query(DICT_PGSQL *, const char *, VSTRING *, char *, + char *, char *); +static void plpgsql_dealloc(PLPGSQL *); +static void plpgsql_close_host(HOST *); +static void plpgsql_down_host(HOST *); +static void plpgsql_connect_single(HOST *, char *, char *, char *); +static const char *dict_pgsql_lookup(DICT *, const char *); +DICT *dict_pgsql_open(const char *, int, int); +static void dict_pgsql_close(DICT *); +static HOST *host_init(const char *); + +/* dict_pgsql_quote - escape SQL metacharacters in input string */ + +static void dict_pgsql_quote(DICT *dict, const char *name, VSTRING *result) +{ + DICT_PGSQL *dict_pgsql = (DICT_PGSQL *) dict; + HOST *active_host = dict_pgsql->active_host; + char *myname = "dict_pgsql_quote"; + size_t len = strlen(name); + size_t buflen; + int err = 1; + + if (active_host == 0) + msg_panic("%s: bogus dict_pgsql->active_host", myname); + + /* + * We won't get arithmetic overflows in 2*len + 1, because Postfix input + * keys have reasonable size limits, better safe than sorry. + */ + if (len > (SSIZE_T_MAX - VSTRING_LEN(result) - 1) / 2) + msg_panic("%s: arithmetic overflow in %lu+2*%lu+1", + myname, (unsigned long) VSTRING_LEN(result), + (unsigned long) len); + buflen = 2 * len + 1; + + /* + * XXX Workaround: stop further processing when PQescapeStringConn() + * (below) fails. A more proper fix requires invasive changes, not + * suitable for a stable release. + */ + if (active_host->stat == STATFAIL) + return; + + /* + * Escape the input string, using PQescapeStringConn(), because the older + * PQescapeString() is not safe anymore, as stated by the documentation. + * + * From current libpq (8.1.4) documentation: + * + * PQescapeStringConn writes an escaped version of the from string to the to + * buffer, escaping special characters so that they cannot cause any + * harm, and adding a terminating zero byte. + * + * ... + * + * The parameter from points to the first character of the string that is to + * be escaped, and the length parameter gives the number of bytes in this + * string. A terminating zero byte is not required, and should not be + * counted in length. + * + * ... + * + * (The parameter) to shall point to a buffer that is able to hold at least + * one more byte than twice the value of length, otherwise the behavior + * is undefined. + * + * ... + * + * If the error parameter is not NULL, then *error is set to zero on + * success, nonzero on error ... The output string is still generated on + * error, but it can be expected that the server will reject it as + * malformed. On error, a suitable message is stored in the conn object, + * whether or not error is NULL. + */ + VSTRING_SPACE(result, buflen); + PQescapeStringConn(active_host->db, vstring_end(result), name, len, &err); + if (err == 0) { + VSTRING_SKIP(result); + } else { + + /* + * PQescapeStringConn() failed. According to the docs, we still have + * a valid, null-terminated output string, but we need not rely on + * this behavior. + */ + msg_warn("dict pgsql: (host %s) cannot escape input string: %s", + active_host->hostname, PQerrorMessage(active_host->db)); + active_host->stat = STATFAIL; + VSTRING_TERMINATE(result); + } +} + +/* dict_pgsql_lookup - find database entry */ + +static const char *dict_pgsql_lookup(DICT *dict, const char *name) +{ + const char *myname = "dict_pgsql_lookup"; + PGSQL_RES *query_res; + DICT_PGSQL *dict_pgsql; + static VSTRING *query; + static VSTRING *result; + int i; + int j; + int numrows; + int numcols; + int expansion; + const char *r; + int domain_rc; + + dict_pgsql = (DICT_PGSQL *) dict; + +#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); + INIT_VSTR(result, 10); + + 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_pgsql->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_pgsql->ctx, name)) == 0) { + if (msg_verbose) + msg_info("%s: Skipping lookup of '%s'", myname, name); + return (0); + } + if (domain_rc < 0) + DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0); + + /* + * Suppress the actual lookup if the 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 (!db_common_expand(dict_pgsql->ctx, dict_pgsql->query, + name, 0, query, 0)) + return (0); + + /* do the query - set dict->error & cleanup if there's an error */ + if ((query_res = plpgsql_query(dict_pgsql, name, query, + dict_pgsql->dbname, + dict_pgsql->username, + dict_pgsql->password)) == 0) { + dict->error = DICT_ERR_RETRY; + return 0; + } + numrows = PQntuples(query_res); + if (msg_verbose) + msg_info("%s: retrieved %d rows", myname, numrows); + if (numrows == 0) { + PQclear(query_res); + return 0; + } + numcols = PQnfields(query_res); + + for (expansion = i = 0; i < numrows && dict->error == 0; i++) { + for (j = 0; j < numcols; j++) { + r = PQgetvalue(query_res, i, j); + if (db_common_expand(dict_pgsql->ctx, dict_pgsql->result_format, + r, name, result, 0) + && dict_pgsql->expansion_limit > 0 + && ++expansion > dict_pgsql->expansion_limit) { + msg_warn("%s: %s: Expansion limit exceeded for key: '%s'", + myname, dict_pgsql->parser->name, name); + dict->error = DICT_ERR_RETRY; + break; + } + } + } + PQclear(query_res); + r = vstring_str(result); + return ((dict->error == 0 && *r) ? r : 0); +} + +/* dict_pgsql_check_stat - check the status of a host */ + +static int dict_pgsql_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_pgsql_find_host - find a host with the given status */ + +static HOST *dict_pgsql_find_host(PLPGSQL *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_pgsql_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_pgsql_check_stat(PLDB->db_hosts[i], stat, type, t) && + --idx == 0) + return PLDB->db_hosts[i]; + } + } + return 0; +} + +/* dict_pgsql_get_active - get an active connection */ + +static HOST *dict_pgsql_get_active(PLPGSQL *PLDB, char *dbname, + char *username, char *password) +{ + const char *myname = "dict_pgsql_get_active"; + HOST *host; + int count = RETRY_CONN_MAX; + + /* try the active connections first; prefer the ones to UNIX sockets */ + if ((host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPEUNIX)) != NULL || + (host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPEINET)) != NULL || + (host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPECONNSTRING)) != 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_pgsql_find_host(PLDB, STATUNTRIED | STATFAIL, + TYPEUNIX)) != NULL || + (host = dict_pgsql_find_host(PLDB, STATUNTRIED | STATFAIL, + TYPEINET)) != NULL || + (host = dict_pgsql_find_host(PLDB, STATUNTRIED | STATFAIL, + TYPECONNSTRING)) != NULL)) { + if (msg_verbose) + msg_info("%s: attempting to connect to host %s", myname, + host->hostname); + plpgsql_connect_single(host, dbname, username, password); + if (host->stat == STATACTIVE) + return host; + } + + /* bad news... */ + return 0; +} + +/* dict_pgsql_event - callback: close idle connections */ + +static void dict_pgsql_event(int unused_event, void *context) +{ + HOST *host = (HOST *) context; + + if (host->db) + plpgsql_close_host(host); +} + +/* + * plpgsql_query - process a PostgreSQL query. Return PGSQL_RES* on success. + * On failure, log failure and try other db instances. + * on failure of all db instances, return 0; + * close unnecessary active connections + */ + +static PGSQL_RES *plpgsql_query(DICT_PGSQL *dict_pgsql, + const char *name, + VSTRING *query, + char *dbname, + char *username, + char *password) +{ + PLPGSQL *PLDB = dict_pgsql->pldb; + HOST *host; + PGSQL_RES *res = 0; + ExecStatusType status; + + while ((host = dict_pgsql_get_active(PLDB, dbname, username, password)) != NULL) { + + /* + * The active host is used to escape strings in the context of the + * active connection's character encoding. + */ + dict_pgsql->active_host = host; + VSTRING_RESET(query); + VSTRING_TERMINATE(query); + db_common_expand(dict_pgsql->ctx, dict_pgsql->query, + name, 0, query, dict_pgsql_quote); + dict_pgsql->active_host = 0; + + /* Check for potential dict_pgsql_quote() failure. */ + if (host->stat == STATFAIL) { + plpgsql_down_host(host); + continue; + } + + /* + * Submit a command to the server. Be paranoid when processing the + * result set: try to enumerate every successful case, and reject + * everything else. + * + * From PostgreSQL 8.1.4 docs: (PQexec) returns a PGresult pointer or + * possibly a null pointer. A non-null pointer will generally be + * returned except in out-of-memory conditions or serious errors such + * as inability to send the command to the server. + */ + if ((res = PQexec(host->db, vstring_str(query))) != 0) { + + /* + * XXX Because non-null result pointer does not imply success, we + * need to check the command's result status. + * + * Section 28.3.1: A result of status PGRES_NONFATAL_ERROR will + * never be returned directly by PQexec or other query execution + * functions; results of this kind are instead passed to the + * notice processor. + * + * PGRES_EMPTY_QUERY is being sent by the server when the query + * string is empty. The sanity-checking done by the Postfix + * infrastructure makes this case impossible, so we need not + * handle this situation explicitly. + */ + switch ((status = PQresultStatus(res))) { + case PGRES_TUPLES_OK: + case PGRES_COMMAND_OK: + /* Success. */ + if (msg_verbose) + msg_info("dict_pgsql: successful query from host %s", + host->hostname); + event_request_timer(dict_pgsql_event, (void *) host, + IDLE_CONN_INTV); + return (res); + case PGRES_FATAL_ERROR: + msg_warn("pgsql query failed: fatal error from host %s: %s", + host->hostname, PQresultErrorMessage(res)); + break; + case PGRES_BAD_RESPONSE: + msg_warn("pgsql query failed: protocol error, host %s", + host->hostname); + break; + default: + msg_warn("pgsql query failed: unknown code 0x%lx from host %s", + (unsigned long) status, host->hostname); + break; + } + } else { + + /* + * This driver treats null pointers like fatal, non-null result + * pointer errors, as suggested by the PostgreSQL 8.1.4 + * documentation. + */ + msg_warn("pgsql query failed: fatal error from host %s: %s", + host->hostname, PQerrorMessage(host->db)); + } + + /* + * XXX An error occurred. Clean up memory and skip this connection. + */ + if (res != 0) + PQclear(res); + plpgsql_down_host(host); + } + + return (0); +} + +/* + * plpgsql_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 plpgsql_connect_single(HOST *host, char *dbname, char *username, char *password) +{ + if (host->type == TYPECONNSTRING) { + host->db = PQconnectdb(host->name); + } else { + host->db = PQsetdbLogin(host->name, host->port, NULL, NULL, + dbname, username, password); + } + if (host->db == NULL || PQstatus(host->db) != CONNECTION_OK) { + msg_warn("connect to pgsql server %s: %s", + host->hostname, PQerrorMessage(host->db)); + plpgsql_down_host(host); + return; + } + if (msg_verbose) + msg_info("dict_pgsql: successful connection to host %s", + host->hostname); + + /* + * The only legitimate encodings for Internet mail are ASCII and UTF-8. + */ +#ifdef SNAPSHOT + if (PQsetClientEncoding(host->db, "UTF8") != 0) { + msg_warn("dict_pgsql: cannot set the encoding to UTF8, skipping %s", + host->hostname); + plpgsql_down_host(host); + return; + } +#else + + /* + * XXX Postfix does not send multi-byte characters. The following piece + * of code is an explicit statement of this fact, and the database server + * should not accept multi-byte information after this point. + */ + if (PQsetClientEncoding(host->db, "LATIN1") != 0) { + msg_warn("dict_pgsql: cannot set the encoding to LATIN1, skipping %s", + host->hostname); + plpgsql_down_host(host); + return; + } +#endif + /* Success. */ + host->stat = STATACTIVE; +} + +/* plpgsql_close_host - close an established PostgreSQL connection */ + +static void plpgsql_close_host(HOST *host) +{ + if (host->db) + PQfinish(host->db); + host->db = 0; + host->stat = STATUNTRIED; +} + +/* + * plpgsql_down_host - close a failed connection AND set a "stay away from + * this host" timer. + */ +static void plpgsql_down_host(HOST *host) +{ + if (host->db) + PQfinish(host->db); + host->db = 0; + host->ts = time((time_t *) 0) + RETRY_CONN_INTV; + host->stat = STATFAIL; + event_cancel_timer(dict_pgsql_event, (void *) host); +} + +/* pgsql_parse_config - parse pgsql configuration file */ + +static void pgsql_parse_config(DICT_PGSQL *dict_pgsql, const char *pgsqlcf) +{ + const char *myname = "pgsql_parse_config"; + CFG_PARSER *p = dict_pgsql->parser; + char *hosts; + VSTRING *query; + char *select_function; + + dict_pgsql->username = cfg_get_str(p, "user", "", 0, 0); + dict_pgsql->password = cfg_get_str(p, "password", "", 0, 0); + dict_pgsql->dbname = cfg_get_str(p, "dbname", "", 1, 0); + dict_pgsql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0); + + /* + * XXX: The default should be non-zero for safety, but that is not + * backwards compatible. + */ + dict_pgsql->expansion_limit = cfg_get_int(dict_pgsql->parser, + "expansion_limit", 0, 0, 0); + + if ((dict_pgsql->query = cfg_get_str(p, "query", 0, 0, 0)) == 0) { + + /* + * No query specified -- fallback to building it from components ( + * old style "select %s from %s where %s" ) + */ + query = vstring_alloc(64); + select_function = cfg_get_str(p, "select_function", 0, 0, 0); + if (select_function != 0) { + vstring_sprintf(query, "SELECT %s('%%s')", select_function); + myfree(select_function); + } else + db_common_sql_build_query(query, p); + dict_pgsql->query = vstring_export(query); + } + + /* + * Must parse all templates before we can use db_common_expand() + */ + dict_pgsql->ctx = 0; + (void) db_common_parse(&dict_pgsql->dict, &dict_pgsql->ctx, + dict_pgsql->query, 1); + (void) db_common_parse(0, &dict_pgsql->ctx, dict_pgsql->result_format, 0); + db_common_parse_domain(p, dict_pgsql->ctx); + + /* + * Maps that use substring keys should only be used with the full input + * key. + */ + if (db_common_dict_partial(dict_pgsql->ctx)) + dict_pgsql->dict.flags |= DICT_FLAG_PATTERN; + else + dict_pgsql->dict.flags |= DICT_FLAG_FIXED; + if (dict_pgsql->dict.flags & DICT_FLAG_FOLD_FIX) + dict_pgsql->dict.fold_buf = vstring_alloc(10); + + hosts = cfg_get_str(p, "hosts", "", 0, 0); + + dict_pgsql->hosts = argv_split(hosts, CHARS_COMMA_SP); + if (dict_pgsql->hosts->argc == 0) { + argv_add(dict_pgsql->hosts, "localhost", ARGV_END); + argv_terminate(dict_pgsql->hosts); + if (msg_verbose) + msg_info("%s: %s: no hostnames specified, defaulting to '%s'", + myname, pgsqlcf, dict_pgsql->hosts->argv[0]); + } + myfree(hosts); +} + +/* dict_pgsql_open - open PGSQL data base */ + +DICT *dict_pgsql_open(const char *name, int open_flags, int dict_flags) +{ + DICT_PGSQL *dict_pgsql; + CFG_PARSER *parser; + + /* + * Sanity check. + */ + if (open_flags != O_RDONLY) + return (dict_surrogate(DICT_TYPE_PGSQL, name, open_flags, dict_flags, + "%s:%s map requires O_RDONLY access mode", + DICT_TYPE_PGSQL, name)); + + /* + * Open the configuration file. + */ + if ((parser = cfg_parser_alloc(name)) == 0) + return (dict_surrogate(DICT_TYPE_PGSQL, name, open_flags, dict_flags, + "open %s: %m", name)); + + dict_pgsql = (DICT_PGSQL *) dict_alloc(DICT_TYPE_PGSQL, name, + sizeof(DICT_PGSQL)); + dict_pgsql->dict.lookup = dict_pgsql_lookup; + dict_pgsql->dict.close = dict_pgsql_close; + dict_pgsql->dict.flags = dict_flags; + dict_pgsql->parser = parser; + pgsql_parse_config(dict_pgsql, name); + dict_pgsql->active_host = 0; + dict_pgsql->pldb = plpgsql_init(dict_pgsql->hosts); + if (dict_pgsql->pldb == NULL) + msg_fatal("couldn't initialize pldb!\n"); + dict_pgsql->dict.owner = cfg_get_owner(dict_pgsql->parser); + return (DICT_DEBUG (&dict_pgsql->dict)); +} + +/* plpgsql_init - initialize a PGSQL database */ + +static PLPGSQL *plpgsql_init(ARGV *hosts) +{ + PLPGSQL *PLDB; + int i; + + PLDB = (PLPGSQL *) mymalloc(sizeof(PLPGSQL)); + PLDB->len_hosts = hosts->argc; + PLDB->db_hosts = (HOST **) mymalloc(sizeof(HOST *) * hosts->argc); + 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 = "pgsql host_init"; + HOST *host = (HOST *) mymalloc(sizeof(HOST)); + const char *d = hostname; + + host->db = 0; + host->hostname = mystrdup(hostname); + host->stat = STATUNTRIED; + host->ts = 0; + + /* + * Modern syntax: "postgresql://connection-info". + */ + if (strncmp(d, "postgresql:", 11) == 0) { + host->type = TYPECONNSTRING; + host->name = mystrdup(d); + host->port = 0; + } + + /* + * Historical syntax: "unix:/pathname" and "inet:host:port". Strip the + * "unix:" and "inet:" prefixes. Look at the first character, which is + * how PgSQL historically distinguishes between UNIX and INET. + */ + else { + if (strncmp(d, "unix:", 5) == 0 || strncmp(d, "inet:", 5) == 0) + d += 5; + host->name = mystrdup(d); + if (host->name[0] && host->name[0] != '/') { + host->type = TYPEINET; + host->port = split_at_right(host->name, ':'); + } else { + host->type = TYPEUNIX; + host->port = 0; + } + } + if (msg_verbose > 1) + msg_info("%s: host=%s, port=%s, type=%s", myname, host->name, + host->port ? host->port : "", + host->type == TYPEUNIX ? "unix" : + host->type == TYPEINET ? "inet" : + "uri"); + return host; +} + +/* dict_pgsql_close - close PGSQL data base */ + +static void dict_pgsql_close(DICT *dict) +{ + DICT_PGSQL *dict_pgsql = (DICT_PGSQL *) dict; + + plpgsql_dealloc(dict_pgsql->pldb); + cfg_parser_free(dict_pgsql->parser); + myfree(dict_pgsql->username); + myfree(dict_pgsql->password); + myfree(dict_pgsql->dbname); + myfree(dict_pgsql->query); + myfree(dict_pgsql->result_format); + if (dict_pgsql->hosts) + argv_free(dict_pgsql->hosts); + if (dict_pgsql->ctx) + db_common_free_ctx(dict_pgsql->ctx); + if (dict->fold_buf) + vstring_free(dict->fold_buf); + dict_free(dict); +} + +/* plpgsql_dealloc - free memory associated with PLPGSQL close databases */ + +static void plpgsql_dealloc(PLPGSQL *PLDB) +{ + int i; + + for (i = 0; i < PLDB->len_hosts; i++) { + event_cancel_timer(dict_pgsql_event, (void *) (PLDB->db_hosts[i])); + if (PLDB->db_hosts[i]->db) + PQfinish(PLDB->db_hosts[i]->db); + myfree(PLDB->db_hosts[i]->hostname); + myfree(PLDB->db_hosts[i]->name); + myfree((void *) PLDB->db_hosts[i]); + } + myfree((void *) PLDB->db_hosts); + myfree((void *) (PLDB)); +} + +#endif |