1/*++
2/* NAME
3/*	dict_tcp 3
4/* SUMMARY
5/*	dictionary manager interface to tcp-based lookup tables
6/* SYNOPSIS
7/*	#include <dict_tcp.h>
8/*
9/*	DICT	*dict_tcp_open(map, open_flags, dict_flags)
10/*	const char *map;
11/*	int	open_flags;
12/*	int	dict_flags;
13/* DESCRIPTION
14/*	dict_tcp_open() makes a TCP server accessible via the generic
15/*	dictionary operations described in dict_open(3).
16/*	The only implemented operation is dictionary lookup. This map
17/*	type can be useful for simulating a dynamic lookup table.
18/*
19/*	Map names have the form host:port.
20/*
21/*	The TCP map class implements a very simple protocol: the client
22/*	sends a request, and the server sends one reply. Requests and
23/*	replies are sent as one line of ASCII text, terminated by the
24/*	ASCII newline character. Request and reply parameters (see below)
25/*	are separated by whitespace.
26/* ENCODING
27/* .ad
28/* .fi
29/*	In request and reply parameters, the character % and any non-printing
30/*	and whitespace characters must be replaced by %XX, XX being the
31/*	corresponding ASCII hexadecimal character value. The hexadecimal codes
32/*	can be specified in any case (upper, lower, mixed).
33/* REQUEST FORMAT
34/* .ad
35/* .fi
36/*	Requests are strings that serve as lookup key in the simulated
37/*	table.
38/* .IP "get SPACE key NEWLINE"
39/*	Look up data under the specified key.
40/* .IP "put SPACE key SPACE value NEWLINE"
41/*	This request is currently not implemented.
42/* REPLY FORMAT
43/* .ad
44/* .fi
45/*      Replies must be no longer than 4096 characters including the
46/*	newline terminator, and must have the following form:
47/* .IP "500 SPACE text NEWLINE"
48/*	In case of a lookup request, the requested data does not exist.
49/*	In case of an update request, the request was rejected.
50/*	The text gives the nature of the problem.
51/* .IP "400 SPACE text NEWLINE"
52/*	This indicates an error condition. The text gives the nature of
53/*	the problem. The client should retry the request later.
54/* .IP "200 SPACE text NEWLINE"
55/*	The request was successful. In the case of a lookup request,
56/*	the text contains an encoded version of the requested data.
57/* SECURITY
58/*	This map must not be used for security sensitive information,
59/*	because neither the connection nor the server are authenticated.
60/* SEE ALSO
61/*	dict(3) generic dictionary manager
62/*	hex_quote(3) http-style quoting
63/* DIAGNOSTICS
64/*	Fatal errors: out of memory, unknown host or service name,
65/*	attempt to update or iterate over map.
66/* BUGS
67/*	Only the lookup method is currently implemented.
68/* LICENSE
69/* .ad
70/* .fi
71/*	The Secure Mailer license must be distributed with this software.
72/* AUTHOR(S)
73/*	Wietse Venema
74/*	IBM T.J. Watson Research
75/*	P.O. Box 704
76/*	Yorktown Heights, NY 10598, USA
77/*--*/
78
79/* System library. */
80
81#include "sys_defs.h"
82#include <unistd.h>
83#include <string.h>
84#include <errno.h>
85#include <ctype.h>
86
87/* Utility library. */
88
89#include <msg.h>
90#include <mymalloc.h>
91#include <vstring.h>
92#include <vstream.h>
93#include <vstring_vstream.h>
94#include <connect.h>
95#include <hex_quote.h>
96#include <dict.h>
97#include <stringops.h>
98#include <dict_tcp.h>
99
100/* Application-specific. */
101
102typedef struct {
103    DICT    dict;			/* generic members */
104    VSTRING *raw_buf;			/* raw I/O buffer */
105    VSTRING *hex_buf;			/* quoted I/O buffer */
106    VSTREAM *fp;			/* I/O stream */
107} DICT_TCP;
108
109#define DICT_TCP_MAXTRY	10		/* attempts before giving up */
110#define DICT_TCP_TMOUT	100		/* connect/read/write timeout */
111#define DICT_TCP_MAXLEN	4096		/* server reply size limit */
112
113#define STR(x)		vstring_str(x)
114
115/* dict_tcp_connect - connect to TCP server */
116
117static int dict_tcp_connect(DICT_TCP *dict_tcp)
118{
119    int     fd;
120
121    /*
122     * Connect to the server. Enforce a time limit on all operations so that
123     * we do not get stuck.
124     */
125    if ((fd = inet_connect(dict_tcp->dict.name, NON_BLOCKING, DICT_TCP_TMOUT)) < 0) {
126	msg_warn("connect to TCP map %s: %m", dict_tcp->dict.name);
127	return (-1);
128    }
129    dict_tcp->fp = vstream_fdopen(fd, O_RDWR);
130    vstream_control(dict_tcp->fp,
131		    VSTREAM_CTL_TIMEOUT, DICT_TCP_TMOUT,
132		    VSTREAM_CTL_END);
133
134    /*
135     * Allocate per-map I/O buffers on the fly.
136     */
137    if (dict_tcp->raw_buf == 0) {
138	dict_tcp->raw_buf = vstring_alloc(10);
139	dict_tcp->hex_buf = vstring_alloc(10);
140    }
141    return (0);
142}
143
144/* dict_tcp_disconnect - disconnect from TCP server */
145
146static void dict_tcp_disconnect(DICT_TCP *dict_tcp)
147{
148    (void) vstream_fclose(dict_tcp->fp);
149    dict_tcp->fp = 0;
150}
151
152/* dict_tcp_lookup - request TCP server */
153
154static const char *dict_tcp_lookup(DICT *dict, const char *key)
155{
156    DICT_TCP *dict_tcp = (DICT_TCP *) dict;
157    const char *myname = "dict_tcp_lookup";
158    int     tries;
159    char   *start;
160    int     last_ch;
161
162#define RETURN(errval, result) { dict->error = errval; return (result); }
163
164    if (msg_verbose)
165	msg_info("%s: key %s", myname, key);
166
167    /*
168     * Optionally fold the key.
169     */
170    if (dict->flags & DICT_FLAG_FOLD_MUL) {
171	if (dict->fold_buf == 0)
172	    dict->fold_buf = vstring_alloc(10);
173	vstring_strcpy(dict->fold_buf, key);
174	key = lowercase(vstring_str(dict->fold_buf));
175    }
176    for (tries = 0; /* see below */ ; /* see below */ ) {
177
178	/*
179	 * Connect to the server, or use an existing connection.
180	 */
181	if (dict_tcp->fp != 0 || dict_tcp_connect(dict_tcp) == 0) {
182
183	    /*
184	     * Send request and receive response. Both are %XX quoted and
185	     * both are terminated by newline. This encoding is convenient
186	     * for data that is mostly text.
187	     */
188	    hex_quote(dict_tcp->hex_buf, key);
189	    vstream_fprintf(dict_tcp->fp, "get %s\n", STR(dict_tcp->hex_buf));
190	    if (msg_verbose)
191		msg_info("%s: send: get %s", myname, STR(dict_tcp->hex_buf));
192	    last_ch = vstring_get_nonl_bound(dict_tcp->hex_buf, dict_tcp->fp,
193					     DICT_TCP_MAXLEN);
194	    if (last_ch == '\n')
195		break;
196
197	    /*
198	     * Disconnect from the server if it can't talk to us.
199	     */
200	    if (last_ch < 0)
201		msg_warn("read TCP map reply from %s: unexpected EOF (%m)",
202			 dict_tcp->dict.name);
203	    else
204		msg_warn("read TCP map reply from %s: text longer than %d",
205			 dict_tcp->dict.name, DICT_TCP_MAXLEN);
206	    dict_tcp_disconnect(dict_tcp);
207	}
208
209	/*
210	 * Try to connect a limited number of times before giving up.
211	 */
212	if (++tries >= DICT_TCP_MAXTRY)
213	    RETURN(DICT_ERR_RETRY, 0);
214
215	/*
216	 * Sleep between attempts, instead of hammering the server.
217	 */
218	sleep(1);
219    }
220    if (msg_verbose)
221	msg_info("%s: recv: %s", myname, STR(dict_tcp->hex_buf));
222
223    /*
224     * Check the general reply syntax. If the reply is malformed, disconnect
225     * and try again later.
226     */
227    if (start = STR(dict_tcp->hex_buf),
228	!ISDIGIT(start[0]) || !ISDIGIT(start[1])
229	|| !ISDIGIT(start[2]) || !ISSPACE(start[3])
230	|| !hex_unquote(dict_tcp->raw_buf, start + 4)) {
231	msg_warn("read TCP map reply from %s: malformed reply: %.100s",
232	       dict_tcp->dict.name, printable(STR(dict_tcp->hex_buf), '_'));
233	dict_tcp_disconnect(dict_tcp);
234	RETURN(DICT_ERR_RETRY, 0);
235    }
236
237    /*
238     * Examine the reply status code. If the reply is malformed, disconnect
239     * and try again later.
240     */
241    switch (start[0]) {
242    default:
243	msg_warn("read TCP map reply from %s: bad status code: %.100s",
244	       dict_tcp->dict.name, printable(STR(dict_tcp->hex_buf), '_'));
245	dict_tcp_disconnect(dict_tcp);
246	RETURN(DICT_ERR_RETRY, 0);
247    case '4':
248	if (msg_verbose)
249	    msg_info("%s: soft error: %s",
250		     myname, printable(STR(dict_tcp->hex_buf), '_'));
251	dict_tcp_disconnect(dict_tcp);
252	RETURN(DICT_ERR_RETRY, 0);
253    case '5':
254	if (msg_verbose)
255	    msg_info("%s: not found: %s",
256		     myname, printable(STR(dict_tcp->hex_buf), '_'));
257	RETURN(DICT_ERR_NONE, 0);
258    case '2':
259	if (msg_verbose)
260	    msg_info("%s: found: %s",
261		     myname, printable(STR(dict_tcp->raw_buf), '_'));
262	RETURN(DICT_ERR_NONE, STR(dict_tcp->raw_buf));
263    }
264}
265
266/* dict_tcp_close - close TCP map */
267
268static void dict_tcp_close(DICT *dict)
269{
270    DICT_TCP *dict_tcp = (DICT_TCP *) dict;
271
272    if (dict_tcp->fp)
273	(void) vstream_fclose(dict_tcp->fp);
274    if (dict_tcp->raw_buf)
275	vstring_free(dict_tcp->raw_buf);
276    if (dict_tcp->hex_buf)
277	vstring_free(dict_tcp->hex_buf);
278    if (dict->fold_buf)
279	vstring_free(dict->fold_buf);
280    dict_free(dict);
281}
282
283/* dict_tcp_open - open TCP map */
284
285DICT   *dict_tcp_open(const char *map, int open_flags, int dict_flags)
286{
287    DICT_TCP *dict_tcp;
288
289    /*
290     * Sanity checks.
291     */
292    if (dict_flags & DICT_FLAG_NO_UNAUTH)
293	return (dict_surrogate(DICT_TYPE_TCP, map, open_flags, dict_flags,
294		     "%s:%s map is not allowed for security sensitive data",
295			       DICT_TYPE_TCP, map));
296    if (open_flags != O_RDONLY)
297	return (dict_surrogate(DICT_TYPE_TCP, map, open_flags, dict_flags,
298			       "%s:%s map requires O_RDONLY access mode",
299			       DICT_TYPE_TCP, map));
300
301    /*
302     * Create the dictionary handle. Do not open the connection until the
303     * first request is made.
304     */
305    dict_tcp = (DICT_TCP *) dict_alloc(DICT_TYPE_TCP, map, sizeof(*dict_tcp));
306    dict_tcp->fp = 0;
307    dict_tcp->raw_buf = dict_tcp->hex_buf = 0;
308    dict_tcp->dict.lookup = dict_tcp_lookup;
309    dict_tcp->dict.close = dict_tcp_close;
310    dict_tcp->dict.flags = dict_flags | DICT_FLAG_PATTERN;
311    if (dict_flags & DICT_FLAG_FOLD_MUL)
312	dict_tcp->dict.fold_buf = vstring_alloc(10);
313
314    return (DICT_DEBUG (&dict_tcp->dict));
315}
316