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