1/*	$NetBSD$	*/
2
3/*++
4/* NAME
5/*	dict_proxy 3
6/* SUMMARY
7/*	generic dictionary proxy client
8/* SYNOPSIS
9/*	#include <dict_proxy.h>
10/*
11/*	DICT	*dict_proxy_open(map, open_flags, dict_flags)
12/*	const char *map;
13/*	int	open_flags;
14/*	int	dict_flags;
15/* DESCRIPTION
16/*	dict_proxy_open() relays read-only operations through
17/*	the Postfix proxymap server.
18/*
19/*	The \fIopen_flags\fR argument must specify O_RDONLY
20/*	or O_RDWR|O_CREAT. Depending on this, the client
21/*	connects to the proxymap multiserver or to the
22/*	proxywrite single updater.
23/*
24/*	The connection to the Postfix proxymap server is automatically
25/*	closed after $ipc_idle seconds of idle time, or after $ipc_ttl
26/*	seconds of activity.
27/* SECURITY
28/*      The proxy map server is not meant to be a trusted process. Proxy
29/*	maps must not be used to look up security sensitive information
30/*	such as user/group IDs, output files, or external commands.
31/* SEE ALSO
32/*	dict(3) generic dictionary manager
33/*	clnt_stream(3) client endpoint connection management
34/* DIAGNOSTICS
35/*	Fatal errors: out of memory, unimplemented operation,
36/*	bad request parameter, map not approved for proxy access.
37/* LICENSE
38/* .ad
39/* .fi
40/*	The Secure Mailer license must be distributed with this software.
41/* AUTHOR(S)
42/*	Wietse Venema
43/*	IBM T.J. Watson Research
44/*	P.O. Box 704
45/*	Yorktown Heights, NY 10598, USA
46/*--*/
47
48/* System library. */
49
50#include <sys_defs.h>
51#include <errno.h>
52#include <unistd.h>
53
54/* Utility library. */
55
56#include <msg.h>
57#include <mymalloc.h>
58#include <stringops.h>
59#include <vstring.h>
60#include <vstream.h>
61#include <attr.h>
62#include <dict.h>
63
64/* Global library. */
65
66#include <mail_proto.h>
67#include <mail_params.h>
68#include <clnt_stream.h>
69#include <dict_proxy.h>
70
71/* Application-specific. */
72
73typedef struct {
74    DICT    dict;			/* generic members */
75    CLNT_STREAM *clnt;			/* client handle (shared) */
76    const char *service;		/* service name */
77    int     in_flags;			/* caller-specified flags */
78    VSTRING *result;			/* storage */
79} DICT_PROXY;
80
81 /*
82  * SLMs.
83  */
84#define STR(x)		vstring_str(x)
85#define VSTREQ(v,s)	(strcmp(STR(v),s) == 0)
86
87 /*
88  * All proxied maps of the same type share the same query/reply socket.
89  */
90static CLNT_STREAM *proxymap_stream;	/* read-only maps */
91static CLNT_STREAM *proxywrite_stream;	/* read-write maps */
92
93/* dict_proxy_lookup - find table entry */
94
95static const char *dict_proxy_lookup(DICT *dict, const char *key)
96{
97    const char *myname = "dict_proxy_lookup";
98    DICT_PROXY *dict_proxy = (DICT_PROXY *) dict;
99    VSTREAM *stream;
100    int     status;
101    int     count = 0;
102    int     request_flags;
103
104    /*
105     * The client and server live in separate processes that may start and
106     * terminate independently. We cannot rely on a persistent connection,
107     * let alone on persistent state (such as a specific open table) that is
108     * associated with a specific connection. Each lookup needs to specify
109     * the table and the flags that were specified to dict_proxy_open().
110     */
111    VSTRING_RESET(dict_proxy->result);
112    VSTRING_TERMINATE(dict_proxy->result);
113    request_flags = (dict_proxy->in_flags & DICT_FLAG_RQST_MASK)
114	| (dict->flags & DICT_FLAG_RQST_MASK);
115    for (;;) {
116	stream = clnt_stream_access(dict_proxy->clnt);
117	errno = 0;
118	count += 1;
119	if (attr_print(stream, ATTR_FLAG_NONE,
120		       ATTR_TYPE_STR, MAIL_ATTR_REQ, PROXY_REQ_LOOKUP,
121		       ATTR_TYPE_STR, MAIL_ATTR_TABLE, dict->name,
122		       ATTR_TYPE_INT, MAIL_ATTR_FLAGS, request_flags,
123		       ATTR_TYPE_STR, MAIL_ATTR_KEY, key,
124		       ATTR_TYPE_END) != 0
125	    || vstream_fflush(stream)
126	    || attr_scan(stream, ATTR_FLAG_STRICT,
127			 ATTR_TYPE_INT, MAIL_ATTR_STATUS, &status,
128			 ATTR_TYPE_STR, MAIL_ATTR_VALUE, dict_proxy->result,
129			 ATTR_TYPE_END) != 2) {
130	    if (msg_verbose || count > 1 || (errno && errno != EPIPE && errno != ENOENT))
131		msg_warn("%s: service %s: %m", myname, VSTREAM_PATH(stream));
132	} else {
133	    if (msg_verbose)
134		msg_info("%s: table=%s flags=%s key=%s -> status=%d result=%s",
135			 myname, dict->name,
136			 dict_flags_str(request_flags), key,
137			 status, STR(dict_proxy->result));
138	    switch (status) {
139	    case PROXY_STAT_BAD:
140		msg_fatal("%s lookup failed for table \"%s\" key \"%s\": "
141			  "invalid request",
142			  dict_proxy->service, dict->name, key);
143	    case PROXY_STAT_DENY:
144		msg_fatal("%s service is not configured for table \"%s\"",
145			  dict_proxy->service, dict->name);
146	    case PROXY_STAT_OK:
147		return (STR(dict_proxy->result));
148	    case PROXY_STAT_NOKEY:
149		dict_errno = 0;
150		return (0);
151	    case PROXY_STAT_RETRY:
152		dict_errno = DICT_ERR_RETRY;
153		return (0);
154	    default:
155		msg_warn("%s lookup failed for table \"%s\" key \"%s\": "
156			 "unexpected reply status %d",
157			 dict_proxy->service, dict->name, key, status);
158	    }
159	}
160	clnt_stream_recover(dict_proxy->clnt);
161	sleep(1);				/* XXX make configurable */
162    }
163}
164
165/* dict_proxy_update - update table entry */
166
167static void dict_proxy_update(DICT *dict, const char *key, const char *value)
168{
169    const char *myname = "dict_proxy_update";
170    DICT_PROXY *dict_proxy = (DICT_PROXY *) dict;
171    VSTREAM *stream;
172    int     status;
173    int     count = 0;
174    int     request_flags;
175
176    /*
177     * The client and server live in separate processes that may start and
178     * terminate independently. We cannot rely on a persistent connection,
179     * let alone on persistent state (such as a specific open table) that is
180     * associated with a specific connection. Each lookup needs to specify
181     * the table and the flags that were specified to dict_proxy_open().
182     */
183    request_flags = (dict_proxy->in_flags & DICT_FLAG_RQST_MASK)
184	| (dict->flags & DICT_FLAG_RQST_MASK);
185    for (;;) {
186	stream = clnt_stream_access(dict_proxy->clnt);
187	errno = 0;
188	count += 1;
189	if (attr_print(stream, ATTR_FLAG_NONE,
190		       ATTR_TYPE_STR, MAIL_ATTR_REQ, PROXY_REQ_UPDATE,
191		       ATTR_TYPE_STR, MAIL_ATTR_TABLE, dict->name,
192		       ATTR_TYPE_INT, MAIL_ATTR_FLAGS, request_flags,
193		       ATTR_TYPE_STR, MAIL_ATTR_KEY, key,
194		       ATTR_TYPE_STR, MAIL_ATTR_VALUE, value,
195		       ATTR_TYPE_END) != 0
196	    || vstream_fflush(stream)
197	    || attr_scan(stream, ATTR_FLAG_STRICT,
198			 ATTR_TYPE_INT, MAIL_ATTR_STATUS, &status,
199			 ATTR_TYPE_END) != 1) {
200	    if (msg_verbose || count > 1 || (errno && errno != EPIPE && errno != ENOENT))
201		msg_warn("%s: service %s: %m", myname, VSTREAM_PATH(stream));
202	} else {
203	    if (msg_verbose)
204		msg_info("%s: table=%s flags=%s key=%s value=%s -> status=%d",
205			 myname, dict->name, dict_flags_str(request_flags),
206			 key, value, status);
207	    switch (status) {
208	    case PROXY_STAT_BAD:
209		msg_fatal("%s update failed for table \"%s\" key \"%s\": "
210			  "invalid request",
211			  dict_proxy->service, dict->name, key);
212	    case PROXY_STAT_DENY:
213		msg_fatal("%s update access is not configured for table \"%s\"",
214			  dict_proxy->service, dict->name);
215	    case PROXY_STAT_OK:
216		return;
217	    default:
218		msg_warn("%s update failed for table \"%s\" key \"%s\": "
219			 "unexpected reply status %d",
220			 dict_proxy->service, dict->name, key, status);
221	    }
222	}
223	clnt_stream_recover(dict_proxy->clnt);
224	sleep(1);				/* XXX make configurable */
225    }
226}
227
228/* dict_proxy_delete - delete table entry */
229
230static int dict_proxy_delete(DICT *dict, const char *key)
231{
232    const char *myname = "dict_proxy_delete";
233    DICT_PROXY *dict_proxy = (DICT_PROXY *) dict;
234    VSTREAM *stream;
235    int     status;
236    int     count = 0;
237    int     request_flags;
238
239    /*
240     * The client and server live in separate processes that may start and
241     * terminate independently. We cannot rely on a persistent connection,
242     * let alone on persistent state (such as a specific open table) that is
243     * associated with a specific connection. Each lookup needs to specify
244     * the table and the flags that were specified to dict_proxy_open().
245     */
246    request_flags = (dict_proxy->in_flags & DICT_FLAG_RQST_MASK)
247	| (dict->flags & DICT_FLAG_RQST_MASK);
248    for (;;) {
249	stream = clnt_stream_access(dict_proxy->clnt);
250	errno = 0;
251	count += 1;
252	if (attr_print(stream, ATTR_FLAG_NONE,
253		       ATTR_TYPE_STR, MAIL_ATTR_REQ, PROXY_REQ_DELETE,
254		       ATTR_TYPE_STR, MAIL_ATTR_TABLE, dict->name,
255		       ATTR_TYPE_INT, MAIL_ATTR_FLAGS, request_flags,
256		       ATTR_TYPE_STR, MAIL_ATTR_KEY, key,
257		       ATTR_TYPE_END) != 0
258	    || vstream_fflush(stream)
259	    || attr_scan(stream, ATTR_FLAG_STRICT,
260			 ATTR_TYPE_INT, MAIL_ATTR_STATUS, &status,
261			 ATTR_TYPE_END) != 1) {
262	    if (msg_verbose || count > 1 || (errno && errno != EPIPE && errno !=
263					     ENOENT))
264		msg_warn("%s: service %s: %m", myname, VSTREAM_PATH(stream));
265	} else {
266	    if (msg_verbose)
267		msg_info("%s: table=%s flags=%s key=%s -> status=%d",
268			 myname, dict->name, dict_flags_str(request_flags),
269			 key, status);
270	    switch (status) {
271	    case PROXY_STAT_BAD:
272		msg_fatal("%s delete failed for table \"%s\" key \"%s\": "
273			  "invalid request",
274			  dict_proxy->service, dict->name, key);
275	    case PROXY_STAT_DENY:
276		msg_fatal("%s update access is not configured for table \"%s\"",
277			  dict_proxy->service, dict->name);
278	    case PROXY_STAT_OK:
279		return 0;
280	    case PROXY_STAT_NOKEY:
281		return 1;
282	    default:
283		msg_warn("%s delete failed for table \"%s\" key \"%s\": "
284			 "unexpected reply status %d",
285			 dict_proxy->service, dict->name, key, status);
286	    }
287	}
288	clnt_stream_recover(dict_proxy->clnt);
289	sleep(1);				/* XXX make configurable */
290    }
291}
292
293/* dict_proxy_close - disconnect */
294
295static void dict_proxy_close(DICT *dict)
296{
297    DICT_PROXY *dict_proxy = (DICT_PROXY *) dict;
298
299    vstring_free(dict_proxy->result);
300    dict_free(dict);
301}
302
303/* dict_proxy_open - open remote map */
304
305DICT   *dict_proxy_open(const char *map, int open_flags, int dict_flags)
306{
307    const char *myname = "dict_proxy_open";
308    DICT_PROXY *dict_proxy;
309    VSTREAM *stream;
310    int     server_flags;
311    int     status;
312    const char *service;
313    char   *relative_path;
314    char   *kludge = 0;
315    char   *prefix;
316    CLNT_STREAM **pstream;
317
318    /*
319     * If this map can't be proxied then we silently do a direct open. This
320     * allows sites to benefit from proxying the virtual mailbox maps without
321     * unnecessary pain.
322     */
323    if (dict_flags & DICT_FLAG_NO_PROXY)
324	return (dict_open(map, open_flags, dict_flags));
325
326    /*
327     * Use a shared stream for proxied table lookups of the same type.
328     *
329     * XXX A complete implementation would also allow O_RDWR without O_CREAT.
330     * But we must not pass on every possible set of flags to the proxy
331     * server; only sets that make sense. For now, the flags are passed
332     * implicitly by choosing between the proxymap or proxywrite service.
333     *
334     * XXX Use absolute pathname to make this work from non-daemon processes.
335     */
336    if (open_flags == O_RDONLY) {
337	pstream = &proxymap_stream;
338	service = var_proxymap_service;
339    } else if (open_flags == (O_RDWR | O_CREAT)) {
340	pstream = &proxywrite_stream;
341	service = var_proxywrite_service;
342    } else
343	msg_fatal("%s: %s map open requires O_RDONLY or O_RDWR|O_CREAT mode",
344		  map, DICT_TYPE_PROXY);
345
346    if (*pstream == 0) {
347	relative_path = concatenate(MAIL_CLASS_PRIVATE "/",
348				    service, (char *) 0);
349	if (access(relative_path, F_OK) == 0)
350	    prefix = MAIL_CLASS_PRIVATE;
351	else
352	    prefix = kludge = concatenate(var_queue_dir, "/",
353					  MAIL_CLASS_PRIVATE, (char *) 0);
354	*pstream = clnt_stream_create(prefix, service, var_ipc_idle_limit,
355				      var_ipc_ttl_limit);
356	if (kludge)
357	    myfree(kludge);
358	myfree(relative_path);
359    }
360
361    /*
362     * Local initialization.
363     */
364    dict_proxy = (DICT_PROXY *)
365	dict_alloc(DICT_TYPE_PROXY, map, sizeof(*dict_proxy));
366    dict_proxy->dict.lookup = dict_proxy_lookup;
367    dict_proxy->dict.update = dict_proxy_update;
368    dict_proxy->dict.delete = dict_proxy_delete;
369    dict_proxy->dict.close = dict_proxy_close;
370    dict_proxy->in_flags = dict_flags;
371    dict_proxy->result = vstring_alloc(10);
372    dict_proxy->clnt = *pstream;
373    dict_proxy->service = service;
374
375    /*
376     * Establish initial contact and get the map type specific flags.
377     *
378     * XXX Should retrieve flags from local instance.
379     */
380    for (;;) {
381	stream = clnt_stream_access(dict_proxy->clnt);
382	errno = 0;
383	if (attr_print(stream, ATTR_FLAG_NONE,
384		       ATTR_TYPE_STR, MAIL_ATTR_REQ, PROXY_REQ_OPEN,
385		       ATTR_TYPE_STR, MAIL_ATTR_TABLE, dict_proxy->dict.name,
386		       ATTR_TYPE_INT, MAIL_ATTR_FLAGS, dict_proxy->in_flags,
387		       ATTR_TYPE_END) != 0
388	    || vstream_fflush(stream)
389	    || attr_scan(stream, ATTR_FLAG_STRICT,
390			 ATTR_TYPE_INT, MAIL_ATTR_STATUS, &status,
391			 ATTR_TYPE_INT, MAIL_ATTR_FLAGS, &server_flags,
392			 ATTR_TYPE_END) != 2) {
393	    if (msg_verbose || (errno != EPIPE && errno != ENOENT))
394		msg_warn("%s: service %s: %m", VSTREAM_PATH(stream), myname);
395	} else {
396	    if (msg_verbose)
397		msg_info("%s: connect to map=%s status=%d server_flags=%s",
398			 myname, dict_proxy->dict.name, status,
399			 dict_flags_str(server_flags));
400	    switch (status) {
401	    case PROXY_STAT_BAD:
402		msg_fatal("%s open failed for table \"%s\": invalid request",
403			  dict_proxy->service, dict_proxy->dict.name);
404	    case PROXY_STAT_DENY:
405		msg_fatal("%s service is not configured for table \"%s\"",
406			  dict_proxy->service, dict_proxy->dict.name);
407	    case PROXY_STAT_OK:
408		dict_proxy->dict.flags = dict_proxy->in_flags
409		    | (server_flags & DICT_FLAG_IMPL_MASK);
410		return (DICT_DEBUG (&dict_proxy->dict));
411	    default:
412		msg_warn("%s open failed for table \"%s\": unexpected status %d",
413			 dict_proxy->service, dict_proxy->dict.name, status);
414	    }
415	}
416	clnt_stream_recover(dict_proxy->clnt);
417	sleep(1);				/* XXX make configurable */
418    }
419}
420