Home | History | Annotate | Line # | Download | only in util
      1 /*	$NetBSD: dict_tcp.c,v 1.3 2026/05/09 18:49:22 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 
    104 typedef 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 
    119 static 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 
    148 static 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 
    156 static 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 
    270 static 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 
    287 DICT   *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_tcp->dict);
    317 }
    318