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