1/*	$NetBSD: dict_pgsql.c,v 1.4 2023/12/23 20:30:43 christos Exp $	*/
2
3/*++
4/* NAME
5/*	dict_pgsql 3
6/* SUMMARY
7/*	dictionary manager interface to PostgreSQL databases
8/* SYNOPSIS
9/*	#include <dict_pgsql.h>
10/*
11/*	DICT	*dict_pgsql_open(name, open_flags, dict_flags)
12/*	const char *name;
13/*	int	open_flags;
14/*	int	dict_flags;
15/* DESCRIPTION
16/*	dict_pgsql_open() creates a dictionary of type 'pgsql'.  This
17/*	dictionary is an interface for the postfix key->value mappings
18/*	to pgsql.  The result is a pointer to the installed dictionary,
19/*	or a null pointer in case of problems.
20/*
21/*	The pgsql dictionary can manage multiple connections to
22/*	different sql servers for the same database.  It assumes that
23/*	the underlying data on each server is identical (mirrored) and
24/*	maintains one connection at any given time.  If any connection
25/*	fails,  any other available ones will be opened and used.
26/*	The intent of this feature is to eliminate a single point of
27/*	failure for mail systems that would otherwise rely on a single
28/*	pgsql server.
29/* .PP
30/*	Arguments:
31/* .IP name
32/*	Either the path to the PostgreSQL configuration file (if it
33/*	starts with '/' or '.'), or the prefix which will be used to
34/*	obtain main.cf configuration parameters for this search.
35/*
36/*	In the first case, the configuration parameters below are
37/*	specified in the file as \fIname\fR=\fIvalue\fR pairs.
38/*
39/*	In the second case, the configuration parameters are
40/*	prefixed with the value of \fIname\fR and an underscore,
41/*	and they are specified in main.cf.  For example, if this
42/*	value is \fIpgsqlsource\fR, the parameters would look like
43/*	\fIpgsqlsource_user\fR, \fIpgsqlsource_table\fR, and so on.
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/*	pgsql_table(5) PostgreSQL client configuration
53/* AUTHOR(S)
54/*	Aaron Sethman
55/*	androsyn@ratbox.org
56/*
57/*	Based upon dict_mysql.c by
58/*
59/*	Scott Cotton
60/*	IC Group, Inc.
61/*	scott@icgroup.com
62/*
63/*	Joshua Marcus
64/*	IC Group, Inc.
65/*	josh@icgroup.com
66/*--*/
67
68/* System library. */
69
70#include "sys_defs.h"
71
72#ifdef HAS_PGSQL
73#include <sys/socket.h>
74#include <netinet/in.h>
75#include <arpa/inet.h>
76#include <netdb.h>
77#include <stdio.h>
78#include <string.h>
79#include <stdlib.h>
80#include <syslog.h>
81#include <time.h>
82
83#include <postgres_ext.h>
84#include <libpq-fe.h>
85
86/* Utility library. */
87
88#include "dict.h"
89#include "msg.h"
90#include "mymalloc.h"
91#include "argv.h"
92#include "vstring.h"
93#include "split_at.h"
94#include "myrand.h"
95#include "events.h"
96#include "stringops.h"
97
98/* Global library. */
99
100#include "cfg_parser.h"
101#include "db_common.h"
102
103/* Application-specific. */
104
105#include "dict_pgsql.h"
106
107#define STATACTIVE			(1<<0)
108#define STATFAIL			(1<<1)
109#define STATUNTRIED			(1<<2)
110
111#define TYPEUNIX			(1<<0)
112#define TYPEINET			(1<<1)
113#define TYPECONNSTRING			(1<<2)
114
115#define RETRY_CONN_MAX			100
116#define RETRY_CONN_INTV			60	/* 1 minute */
117#define IDLE_CONN_INTV			60	/* 1 minute */
118
119typedef struct {
120    PGconn *db;
121    char   *hostname;
122    char   *name;
123    char   *port;
124    unsigned type;			/* TYPEUNIX | TYPEINET | TYPECONNSTRING */
125    unsigned stat;			/* STATUNTRIED | STATFAIL | STATCUR */
126    time_t  ts;				/* used for attempting reconnection */
127} HOST;
128
129typedef struct {
130    int     len_hosts;			/* number of hosts */
131    HOST  **db_hosts;			/* hosts on which databases reside */
132} PLPGSQL;
133
134typedef struct {
135    DICT    dict;
136    CFG_PARSER *parser;
137    char   *query;
138    char   *result_format;
139    void   *ctx;
140    int     expansion_limit;
141    char   *username;
142    char   *password;
143    char   *dbname;
144    char   *encoding;
145    char   *table;
146    ARGV   *hosts;
147    PLPGSQL *pldb;
148    HOST   *active_host;
149} DICT_PGSQL;
150
151
152/* Just makes things a little easier for me.. */
153#define PGSQL_RES PGresult
154
155/* internal function declarations */
156static PLPGSQL *plpgsql_init(ARGV *);
157static PGSQL_RES *plpgsql_query(DICT_PGSQL *, const char *, VSTRING *, char *,
158				        char *, char *, char *);
159static void plpgsql_dealloc(PLPGSQL *);
160static void plpgsql_close_host(HOST *);
161static void plpgsql_down_host(HOST *);
162static void plpgsql_connect_single(HOST *, char *, char *, char *, char *);
163static const char *dict_pgsql_lookup(DICT *, const char *);
164DICT   *dict_pgsql_open(const char *, int, int);
165static void dict_pgsql_close(DICT *);
166static HOST *host_init(const char *);
167
168/* dict_pgsql_quote - escape SQL metacharacters in input string */
169
170static void dict_pgsql_quote(DICT *dict, const char *name, VSTRING *result)
171{
172    DICT_PGSQL *dict_pgsql = (DICT_PGSQL *) dict;
173    HOST   *active_host = dict_pgsql->active_host;
174    char   *myname = "dict_pgsql_quote";
175    size_t  len = strlen(name);
176    size_t  buflen;
177    int     err = 1;
178
179    if (active_host == 0)
180	msg_panic("%s: bogus dict_pgsql->active_host", myname);
181
182    /*
183     * We won't get arithmetic overflows in 2*len + 1, because Postfix input
184     * keys have reasonable size limits, better safe than sorry.
185     */
186    if (len > (SSIZE_T_MAX - VSTRING_LEN(result) - 1) / 2)
187	msg_panic("%s: arithmetic overflow in %lu+2*%lu+1",
188		  myname, (unsigned long) VSTRING_LEN(result),
189		  (unsigned long) len);
190    buflen = 2 * len + 1;
191
192    /*
193     * XXX Workaround: stop further processing when PQescapeStringConn()
194     * (below) fails. A more proper fix requires invasive changes, not
195     * suitable for a stable release.
196     */
197    if (active_host->stat == STATFAIL)
198	return;
199
200    /*
201     * Escape the input string, using PQescapeStringConn(), because the older
202     * PQescapeString() is not safe anymore, as stated by the documentation.
203     *
204     * From current libpq (8.1.4) documentation:
205     *
206     * PQescapeStringConn writes an escaped version of the from string to the to
207     * buffer, escaping special characters so that they cannot cause any
208     * harm, and adding a terminating zero byte.
209     *
210     * ...
211     *
212     * The parameter from points to the first character of the string that is to
213     * be escaped, and the length parameter gives the number of bytes in this
214     * string. A terminating zero byte is not required, and should not be
215     * counted in length.
216     *
217     * ...
218     *
219     * (The parameter) to shall point to a buffer that is able to hold at least
220     * one more byte than twice the value of length, otherwise the behavior
221     * is undefined.
222     *
223     * ...
224     *
225     * If the error parameter is not NULL, then *error is set to zero on
226     * success, nonzero on error ... The output string is still generated on
227     * error, but it can be expected that the server will reject it as
228     * malformed. On error, a suitable message is stored in the conn object,
229     * whether or not error is NULL.
230     */
231    VSTRING_SPACE(result, buflen);
232    PQescapeStringConn(active_host->db, vstring_end(result), name, len, &err);
233    if (err == 0) {
234	VSTRING_SKIP(result);
235    } else {
236
237	/*
238	 * PQescapeStringConn() failed. According to the docs, we still have
239	 * a valid, null-terminated output string, but we need not rely on
240	 * this behavior.
241	 */
242	msg_warn("dict pgsql: (host %s) cannot escape input string: %s",
243		 active_host->hostname, PQerrorMessage(active_host->db));
244	active_host->stat = STATFAIL;
245	VSTRING_TERMINATE(result);
246    }
247}
248
249/* dict_pgsql_lookup - find database entry */
250
251static const char *dict_pgsql_lookup(DICT *dict, const char *name)
252{
253    const char *myname = "dict_pgsql_lookup";
254    PGSQL_RES *query_res;
255    DICT_PGSQL *dict_pgsql;
256    static VSTRING *query;
257    static VSTRING *result;
258    int     i;
259    int     j;
260    int     numrows;
261    int     numcols;
262    int     expansion;
263    const char *r;
264    int     domain_rc;
265
266    dict_pgsql = (DICT_PGSQL *) dict;
267
268#define INIT_VSTR(buf, len) do { \
269	if (buf == 0) \
270	    buf = vstring_alloc(len); \
271	VSTRING_RESET(buf); \
272	VSTRING_TERMINATE(buf); \
273    } while (0)
274
275    INIT_VSTR(query, 10);
276    INIT_VSTR(result, 10);
277
278    dict->error = 0;
279
280    /*
281     * Don't frustrate future attempts to make Postfix UTF-8 transparent.
282     */
283#ifdef SNAPSHOT
284    if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0
285	&& !valid_utf8_string(name, strlen(name))) {
286	if (msg_verbose)
287	    msg_info("%s: %s: Skipping lookup of non-UTF-8 key '%s'",
288		     myname, dict_pgsql->parser->name, name);
289	return (0);
290    }
291#endif
292
293    /*
294     * Optionally fold the key.
295     */
296    if (dict->flags & DICT_FLAG_FOLD_FIX) {
297	if (dict->fold_buf == 0)
298	    dict->fold_buf = vstring_alloc(10);
299	vstring_strcpy(dict->fold_buf, name);
300	name = lowercase(vstring_str(dict->fold_buf));
301    }
302
303    /*
304     * If there is a domain list for this map, then only search for addresses
305     * in domains on the list. This can significantly reduce the load on the
306     * server.
307     */
308    if ((domain_rc = db_common_check_domain(dict_pgsql->ctx, name)) == 0) {
309	if (msg_verbose)
310	    msg_info("%s: Skipping lookup of '%s'", myname, name);
311	return (0);
312    }
313    if (domain_rc < 0)
314	DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0);
315
316    /*
317     * Suppress the actual lookup if the expansion is empty.
318     *
319     * This initial expansion is outside the context of any specific host
320     * connection, we just want to check the key pre-requisites, so when
321     * quoting happens separately for each connection, we don't bother with
322     * quoting...
323     */
324    if (!db_common_expand(dict_pgsql->ctx, dict_pgsql->query,
325			  name, 0, query, 0))
326	return (0);
327
328    /* do the query - set dict->error & cleanup if there's an error */
329    if ((query_res = plpgsql_query(dict_pgsql, name, query,
330				   dict_pgsql->dbname,
331				   dict_pgsql->encoding,
332				   dict_pgsql->username,
333				   dict_pgsql->password)) == 0) {
334	dict->error = DICT_ERR_RETRY;
335	return 0;
336    }
337    numrows = PQntuples(query_res);
338    if (msg_verbose)
339	msg_info("%s: retrieved %d rows", myname, numrows);
340    if (numrows == 0) {
341	PQclear(query_res);
342	return 0;
343    }
344    numcols = PQnfields(query_res);
345
346    for (expansion = i = 0; i < numrows && dict->error == 0; i++) {
347	for (j = 0; j < numcols; j++) {
348	    r = PQgetvalue(query_res, i, j);
349	    if (db_common_expand(dict_pgsql->ctx, dict_pgsql->result_format,
350				 r, name, result, 0)
351		&& dict_pgsql->expansion_limit > 0
352		&& ++expansion > dict_pgsql->expansion_limit) {
353		msg_warn("%s: %s: Expansion limit exceeded for key: '%s'",
354			 myname, dict_pgsql->parser->name, name);
355		dict->error = DICT_ERR_RETRY;
356		break;
357	    }
358	}
359    }
360    PQclear(query_res);
361    r = vstring_str(result);
362    return ((dict->error == 0 && *r) ? r : 0);
363}
364
365/* dict_pgsql_check_stat - check the status of a host */
366
367static int dict_pgsql_check_stat(HOST *host, unsigned stat, unsigned type,
368				         time_t t)
369{
370    if ((host->stat & stat) && (!type || host->type & type)) {
371	/* try not to hammer the dead hosts too often */
372	if (host->stat == STATFAIL && host->ts > 0 && host->ts >= t)
373	    return 0;
374	return 1;
375    }
376    return 0;
377}
378
379/* dict_pgsql_find_host - find a host with the given status */
380
381static HOST *dict_pgsql_find_host(PLPGSQL *PLDB, unsigned stat, unsigned type)
382{
383    time_t  t;
384    int     count = 0;
385    int     idx;
386    int     i;
387
388    t = time((time_t *) 0);
389    for (i = 0; i < PLDB->len_hosts; i++) {
390	if (dict_pgsql_check_stat(PLDB->db_hosts[i], stat, type, t))
391	    count++;
392    }
393
394    if (count) {
395	idx = (count > 1) ?
396	    1 + count * (double) myrand() / (1.0 + RAND_MAX) : 1;
397
398	for (i = 0; i < PLDB->len_hosts; i++) {
399	    if (dict_pgsql_check_stat(PLDB->db_hosts[i], stat, type, t) &&
400		--idx == 0)
401		return PLDB->db_hosts[i];
402	}
403    }
404    return 0;
405}
406
407/* dict_pgsql_get_active - get an active connection */
408
409static HOST *dict_pgsql_get_active(PLPGSQL *PLDB, char *dbname, char *encoding,
410				           char *username, char *password)
411{
412    const char *myname = "dict_pgsql_get_active";
413    HOST   *host;
414    int     count = RETRY_CONN_MAX;
415
416    /* try the active connections first; prefer the ones to UNIX sockets */
417    if ((host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPEUNIX)) != NULL ||
418	(host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPEINET)) != NULL ||
419	(host = dict_pgsql_find_host(PLDB, STATACTIVE, TYPECONNSTRING)) != NULL) {
420	if (msg_verbose)
421	    msg_info("%s: found active connection to host %s", myname,
422		     host->hostname);
423	return host;
424    }
425
426    /*
427     * Try the remaining hosts. "count" is a safety net, in case the loop
428     * takes more than RETRY_CONN_INTV and the dead hosts are no longer
429     * skipped.
430     */
431    while (--count > 0 &&
432	   ((host = dict_pgsql_find_host(PLDB, STATUNTRIED | STATFAIL,
433					 TYPEUNIX)) != NULL ||
434	    (host = dict_pgsql_find_host(PLDB, STATUNTRIED | STATFAIL,
435					 TYPEINET)) != NULL ||
436	    (host = dict_pgsql_find_host(PLDB, STATUNTRIED | STATFAIL,
437					 TYPECONNSTRING)) != NULL)) {
438	if (msg_verbose)
439	    msg_info("%s: attempting to connect to host %s", myname,
440		     host->hostname);
441	plpgsql_connect_single(host, dbname, encoding, username, password);
442	if (host->stat == STATACTIVE)
443	    return host;
444    }
445
446    /* bad news... */
447    return 0;
448}
449
450/* dict_pgsql_event - callback: close idle connections */
451
452static void dict_pgsql_event(int unused_event, void *context)
453{
454    HOST   *host = (HOST *) context;
455
456    if (host->db)
457	plpgsql_close_host(host);
458}
459
460/*
461 * plpgsql_query - process a PostgreSQL query.  Return PGSQL_RES* on success.
462 *			On failure, log failure and try other db instances.
463 *			on failure of all db instances, return 0;
464 *			close unnecessary active connections
465 */
466
467static PGSQL_RES *plpgsql_query(DICT_PGSQL *dict_pgsql,
468				        const char *name,
469				        VSTRING *query,
470				        char *dbname,
471				        char *encoding,
472				        char *username,
473				        char *password)
474{
475    PLPGSQL *PLDB = dict_pgsql->pldb;
476    HOST   *host;
477    PGSQL_RES *res = 0;
478    ExecStatusType status;
479
480    while ((host = dict_pgsql_get_active(PLDB, dbname, encoding, username, password)) != NULL) {
481
482	/*
483	 * The active host is used to escape strings in the context of the
484	 * active connection's character encoding.
485	 */
486	dict_pgsql->active_host = host;
487	VSTRING_RESET(query);
488	VSTRING_TERMINATE(query);
489	db_common_expand(dict_pgsql->ctx, dict_pgsql->query,
490			 name, 0, query, dict_pgsql_quote);
491	dict_pgsql->active_host = 0;
492
493	/* Check for potential dict_pgsql_quote() failure. */
494	if (host->stat == STATFAIL) {
495	    plpgsql_down_host(host);
496	    continue;
497	}
498
499	/*
500	 * Submit a command to the server. Be paranoid when processing the
501	 * result set: try to enumerate every successful case, and reject
502	 * everything else.
503	 *
504	 * From PostgreSQL 8.1.4 docs: (PQexec) returns a PGresult pointer or
505	 * possibly a null pointer. A non-null pointer will generally be
506	 * returned except in out-of-memory conditions or serious errors such
507	 * as inability to send the command to the server.
508	 */
509	if ((res = PQexec(host->db, vstring_str(query))) != 0) {
510
511	    /*
512	     * XXX Because non-null result pointer does not imply success, we
513	     * need to check the command's result status.
514	     *
515	     * Section 28.3.1: A result of status PGRES_NONFATAL_ERROR will
516	     * never be returned directly by PQexec or other query execution
517	     * functions; results of this kind are instead passed to the
518	     * notice processor.
519	     *
520	     * PGRES_EMPTY_QUERY is being sent by the server when the query
521	     * string is empty. The sanity-checking done by the Postfix
522	     * infrastructure makes this case impossible, so we need not
523	     * handle this situation explicitly.
524	     */
525	    switch ((status = PQresultStatus(res))) {
526	    case PGRES_TUPLES_OK:
527	    case PGRES_COMMAND_OK:
528		/* Success. */
529		if (msg_verbose)
530		    msg_info("dict_pgsql: successful query from host %s",
531			     host->hostname);
532		event_request_timer(dict_pgsql_event, (void *) host,
533				    IDLE_CONN_INTV);
534		return (res);
535	    case PGRES_FATAL_ERROR:
536		msg_warn("pgsql query failed: fatal error from host %s: %s",
537			 host->hostname, PQresultErrorMessage(res));
538		break;
539	    case PGRES_BAD_RESPONSE:
540		msg_warn("pgsql query failed: protocol error, host %s",
541			 host->hostname);
542		break;
543	    default:
544		msg_warn("pgsql query failed: unknown code 0x%lx from host %s",
545			 (unsigned long) status, host->hostname);
546		break;
547	    }
548	} else {
549
550	    /*
551	     * This driver treats null pointers like fatal, non-null result
552	     * pointer errors, as suggested by the PostgreSQL 8.1.4
553	     * documentation.
554	     */
555	    msg_warn("pgsql query failed: fatal error from host %s: %s",
556		     host->hostname, PQerrorMessage(host->db));
557	}
558
559	/*
560	 * XXX An error occurred. Clean up memory and skip this connection.
561	 */
562	if (res != 0)
563	    PQclear(res);
564	plpgsql_down_host(host);
565    }
566
567    return (0);
568}
569
570/*
571 * plpgsql_connect_single -
572 * used to reconnect to a single database when one is down or none is
573 * connected yet. Log all errors and set the stat field of host accordingly
574 */
575static void plpgsql_connect_single(HOST *host, char *dbname, char *encoding, char *username, char *password)
576{
577    if (host->type == TYPECONNSTRING) {
578	host->db = PQconnectdb(host->name);
579    } else {
580	host->db = PQsetdbLogin(host->name, host->port, NULL, NULL,
581				dbname, username, password);
582    }
583    if (host->db == NULL || PQstatus(host->db) != CONNECTION_OK) {
584	msg_warn("connect to pgsql server %s: %s",
585		 host->hostname, PQerrorMessage(host->db));
586	plpgsql_down_host(host);
587	return;
588    }
589    if (PQsetClientEncoding(host->db, encoding) != 0) {
590	msg_warn("dict_pgsql: cannot set the encoding to %s, skipping %s",
591		 encoding, host->hostname);
592	plpgsql_down_host(host);
593	return;
594    }
595    if (msg_verbose)
596	msg_info("dict_pgsql: successful connection to host %s",
597		 host->hostname);
598    /* Success. */
599    host->stat = STATACTIVE;
600}
601
602/* plpgsql_close_host - close an established PostgreSQL connection */
603
604static void plpgsql_close_host(HOST *host)
605{
606    if (host->db)
607	PQfinish(host->db);
608    host->db = 0;
609    host->stat = STATUNTRIED;
610}
611
612/*
613 * plpgsql_down_host - close a failed connection AND set a "stay away from
614 * this host" timer.
615 */
616static void plpgsql_down_host(HOST *host)
617{
618    if (host->db)
619	PQfinish(host->db);
620    host->db = 0;
621    host->ts = time((time_t *) 0) + RETRY_CONN_INTV;
622    host->stat = STATFAIL;
623    event_cancel_timer(dict_pgsql_event, (void *) host);
624}
625
626/* pgsql_parse_config - parse pgsql configuration file */
627
628static void pgsql_parse_config(DICT_PGSQL *dict_pgsql, const char *pgsqlcf)
629{
630    const char *myname = "pgsql_parse_config";
631    CFG_PARSER *p = dict_pgsql->parser;
632    char   *hosts;
633    VSTRING *query;
634    char   *select_function;
635
636    dict_pgsql->username = cfg_get_str(p, "user", "", 0, 0);
637    dict_pgsql->password = cfg_get_str(p, "password", "", 0, 0);
638    dict_pgsql->dbname = cfg_get_str(p, "dbname", "", 1, 0);
639    dict_pgsql->encoding = cfg_get_str(p, "encoding", "UTF8", 1, 0);
640    dict_pgsql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0);
641
642    /*
643     * XXX: The default should be non-zero for safety, but that is not
644     * backwards compatible.
645     */
646    dict_pgsql->expansion_limit = cfg_get_int(dict_pgsql->parser,
647					      "expansion_limit", 0, 0, 0);
648
649    if ((dict_pgsql->query = cfg_get_str(p, "query", 0, 0, 0)) == 0) {
650
651	/*
652	 * No query specified -- fallback to building it from components (
653	 * old style "select %s from %s where %s" )
654	 */
655	query = vstring_alloc(64);
656	select_function = cfg_get_str(p, "select_function", 0, 0, 0);
657	if (select_function != 0) {
658	    vstring_sprintf(query, "SELECT %s('%%s')", select_function);
659	    myfree(select_function);
660	} else
661	    db_common_sql_build_query(query, p);
662	dict_pgsql->query = vstring_export(query);
663    }
664
665    /*
666     * Must parse all templates before we can use db_common_expand()
667     */
668    dict_pgsql->ctx = 0;
669    (void) db_common_parse(&dict_pgsql->dict, &dict_pgsql->ctx,
670			   dict_pgsql->query, 1);
671    (void) db_common_parse(0, &dict_pgsql->ctx, dict_pgsql->result_format, 0);
672    db_common_parse_domain(p, dict_pgsql->ctx);
673
674    /*
675     * Maps that use substring keys should only be used with the full input
676     * key.
677     */
678    if (db_common_dict_partial(dict_pgsql->ctx))
679	dict_pgsql->dict.flags |= DICT_FLAG_PATTERN;
680    else
681	dict_pgsql->dict.flags |= DICT_FLAG_FIXED;
682    if (dict_pgsql->dict.flags & DICT_FLAG_FOLD_FIX)
683	dict_pgsql->dict.fold_buf = vstring_alloc(10);
684
685    hosts = cfg_get_str(p, "hosts", "", 0, 0);
686
687    dict_pgsql->hosts = argv_split(hosts, CHARS_COMMA_SP);
688    if (dict_pgsql->hosts->argc == 0) {
689	argv_add(dict_pgsql->hosts, "localhost", ARGV_END);
690	argv_terminate(dict_pgsql->hosts);
691	if (msg_verbose)
692	    msg_info("%s: %s: no hostnames specified, defaulting to '%s'",
693		     myname, pgsqlcf, dict_pgsql->hosts->argv[0]);
694    }
695    myfree(hosts);
696}
697
698/* dict_pgsql_open - open PGSQL data base */
699
700DICT   *dict_pgsql_open(const char *name, int open_flags, int dict_flags)
701{
702    DICT_PGSQL *dict_pgsql;
703    CFG_PARSER *parser;
704
705    /*
706     * Sanity check.
707     */
708    if (open_flags != O_RDONLY)
709	return (dict_surrogate(DICT_TYPE_PGSQL, name, open_flags, dict_flags,
710			       "%s:%s map requires O_RDONLY access mode",
711			       DICT_TYPE_PGSQL, name));
712
713    /*
714     * Open the configuration file.
715     */
716    if ((parser = cfg_parser_alloc(name)) == 0)
717	return (dict_surrogate(DICT_TYPE_PGSQL, name, open_flags, dict_flags,
718			       "open %s: %m", name));
719
720    dict_pgsql = (DICT_PGSQL *) dict_alloc(DICT_TYPE_PGSQL, name,
721					   sizeof(DICT_PGSQL));
722    dict_pgsql->dict.lookup = dict_pgsql_lookup;
723    dict_pgsql->dict.close = dict_pgsql_close;
724    dict_pgsql->dict.flags = dict_flags;
725    dict_pgsql->parser = parser;
726    pgsql_parse_config(dict_pgsql, name);
727    dict_pgsql->active_host = 0;
728    dict_pgsql->pldb = plpgsql_init(dict_pgsql->hosts);
729    if (dict_pgsql->pldb == NULL)
730	msg_fatal("couldn't initialize pldb!\n");
731    dict_pgsql->dict.owner = cfg_get_owner(dict_pgsql->parser);
732    return (DICT_DEBUG (&dict_pgsql->dict));
733}
734
735/* plpgsql_init - initialize a PGSQL database */
736
737static PLPGSQL *plpgsql_init(ARGV *hosts)
738{
739    PLPGSQL *PLDB;
740    int     i;
741
742    PLDB = (PLPGSQL *) mymalloc(sizeof(PLPGSQL));
743    PLDB->len_hosts = hosts->argc;
744    PLDB->db_hosts = (HOST **) mymalloc(sizeof(HOST *) * hosts->argc);
745    for (i = 0; i < hosts->argc; i++)
746	PLDB->db_hosts[i] = host_init(hosts->argv[i]);
747
748    return PLDB;
749}
750
751
752/* host_init - initialize HOST structure */
753
754static HOST *host_init(const char *hostname)
755{
756    const char *myname = "pgsql host_init";
757    HOST   *host = (HOST *) mymalloc(sizeof(HOST));
758    const char *d = hostname;
759
760    host->db = 0;
761    host->hostname = mystrdup(hostname);
762    host->stat = STATUNTRIED;
763    host->ts = 0;
764
765    /*
766     * Modern syntax: "postgresql://connection-info".
767     */
768    if (strncmp(d, "postgresql:", 11) == 0) {
769	host->type = TYPECONNSTRING;
770	host->name = mystrdup(d);
771	host->port = 0;
772    }
773
774    /*
775     * Historical syntax: "unix:/pathname" and "inet:host:port". Strip the
776     * "unix:" and "inet:" prefixes. Look at the first character, which is
777     * how PgSQL historically distinguishes between UNIX and INET.
778     */
779    else {
780	if (strncmp(d, "unix:", 5) == 0 || strncmp(d, "inet:", 5) == 0)
781	    d += 5;
782	host->name = mystrdup(d);
783	if (host->name[0] && host->name[0] != '/') {
784	    host->type = TYPEINET;
785	    host->port = split_at_right(host->name, ':');
786	} else {
787	    host->type = TYPEUNIX;
788	    host->port = 0;
789	}
790    }
791    if (msg_verbose > 1)
792	msg_info("%s: host=%s, port=%s, type=%s", myname, host->name,
793		 host->port ? host->port : "",
794		 host->type == TYPEUNIX ? "unix" :
795		 host->type == TYPEINET ? "inet" :
796		 "uri");
797    return host;
798}
799
800/* dict_pgsql_close - close PGSQL data base */
801
802static void dict_pgsql_close(DICT *dict)
803{
804    DICT_PGSQL *dict_pgsql = (DICT_PGSQL *) dict;
805
806    plpgsql_dealloc(dict_pgsql->pldb);
807    cfg_parser_free(dict_pgsql->parser);
808    myfree(dict_pgsql->username);
809    myfree(dict_pgsql->password);
810    myfree(dict_pgsql->dbname);
811    myfree(dict_pgsql->encoding);
812    myfree(dict_pgsql->query);
813    myfree(dict_pgsql->result_format);
814    if (dict_pgsql->hosts)
815	argv_free(dict_pgsql->hosts);
816    if (dict_pgsql->ctx)
817	db_common_free_ctx(dict_pgsql->ctx);
818    if (dict->fold_buf)
819	vstring_free(dict->fold_buf);
820    dict_free(dict);
821}
822
823/* plpgsql_dealloc - free memory associated with PLPGSQL close databases */
824
825static void plpgsql_dealloc(PLPGSQL *PLDB)
826{
827    int     i;
828
829    for (i = 0; i < PLDB->len_hosts; i++) {
830	event_cancel_timer(dict_pgsql_event, (void *) (PLDB->db_hosts[i]));
831	if (PLDB->db_hosts[i]->db)
832	    PQfinish(PLDB->db_hosts[i]->db);
833	myfree(PLDB->db_hosts[i]->hostname);
834	myfree(PLDB->db_hosts[i]->name);
835	myfree((void *) PLDB->db_hosts[i]);
836    }
837    myfree((void *) PLDB->db_hosts);
838    myfree((void *) (PLDB));
839}
840
841#endif
842