Home | History | Annotate | Line # | Download | only in util
      1 /*	$NetBSD: dict_sockmap.c,v 1.7 2026/05/09 18:49:22 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	dict_sockmap 3
      6 /* SUMMARY
      7 /*	dictionary manager interface to Sendmail-style socketmap server
      8 /* SYNOPSIS
      9 /*	#include <dict_sockmap.h>
     10 /*
     11 /*	DICT	*dict_sockmap_open(map, open_flags, dict_flags)
     12 /*	const char *map;
     13 /*	int	open_flags;
     14 /*	int	dict_flags;
     15 /* DESCRIPTION
     16 /*	dict_sockmap_open() makes a Sendmail-style socketmap server
     17 /*	accessible via the generic dictionary operations described
     18 /*	in dict_open(3).  The only implemented operation is dictionary
     19 /*	lookup. This map type can be useful for simulating a dynamic
     20 /*	lookup table.
     21 /*
     22 /*	Postfix socketmap names have the form inet:host:port:socketmap-name
     23 /*	or unix:pathname:socketmap-name, where socketmap-name
     24 /*	specifies the socketmap name that the socketmap server uses.
     25 /*
     26 /*	To test this module, build the netstring and dict_open test
     27 /*	programs. Run "./netstring nc -l portnumber" as the server,
     28 /*	and "./dict_open socketmap:127.0.0.1:portnumber:socketmapname"
     29 /*	as the client.
     30 /* PROTOCOL
     31 /* .ad
     32 /* .fi
     33 /*	The socketmap class implements a simple protocol: the client
     34 /*	sends one request, and the server sends one reply.
     35 /* ENCODING
     36 /* .ad
     37 /* .fi
     38 /*	Each request and reply are sent as one netstring object.
     39 /* REQUEST FORMAT
     40 /* .ad
     41 /* .fi
     42 /* .IP "<mapname> <space> <key>"
     43 /*	Search the specified socketmap under the specified key.
     44 /* REPLY FORMAT
     45 /* .ad
     46 /* .fi
     47 /*	Replies must be no longer than 100000 characters (not including
     48 /*	the netstring encapsulation), and must have the following
     49 /*	form:
     50 /* .IP "OK <space> <data>"
     51 /*	The requested data was found.
     52 /* .IP "NOTFOUND <space>"
     53 /*	The requested data was not found.
     54 /* .IP "TEMP <space> <reason>"
     55 /* .IP "TIMEOUT <space> <reason>"
     56 /* .IP "PERM <space> <reason>"
     57 /*	The request failed. The reason, if non-empty, is descriptive
     58 /*	text.
     59 /* SECURITY
     60 /*	This map cannot 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 /*	netstring(3) netstring stream I/O support
     65 /* DIAGNOSTICS
     66 /*	Fatal errors: out of memory, unknown host or service name,
     67 /*	attempt to update or iterate over map.
     68 /* BUGS
     69 /*	The protocol limits are not yet configurable.
     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  /*
     82   * System library.
     83   */
     84 #include <sys_defs.h>
     85 #include <errno.h>
     86 #include <string.h>
     87 #include <ctype.h>
     88 
     89  /*
     90   * Utility library.
     91   */
     92 #include <mymalloc.h>
     93 #include <msg.h>
     94 #include <vstream.h>
     95 #include <auto_clnt.h>
     96 #include <netstring.h>
     97 #include <split_at.h>
     98 #include <stringops.h>
     99 #include <htable.h>
    100 #include <dict_sockmap.h>
    101 
    102  /*
    103   * Socket map data structure.
    104   */
    105 typedef struct {
    106     DICT    dict;			/* parent class */
    107     char   *sockmap_name;		/* on-the-wire socketmap name */
    108     VSTRING *rdwr_buf;			/* read/write buffer */
    109     HTABLE_INFO *client_info;		/* shared endpoint name and handle */
    110 } DICT_SOCKMAP;
    111 
    112  /*
    113   * Default limits.
    114   */
    115 #define DICT_SOCKMAP_DEF_TIMEOUT	100	/* connect/read/write timeout */
    116 #define DICT_SOCKMAP_DEF_MAX_REPLY	100000	/* reply size limit */
    117 #define DICT_SOCKMAP_DEF_MAX_IDLE	10	/* close idle socket */
    118 #define DICT_SOCKMAP_DEF_MAX_TTL	100	/* close old socket */
    119 
    120  /*
    121   * Class variables.
    122   */
    123 static int dict_sockmap_timeout = DICT_SOCKMAP_DEF_TIMEOUT;
    124 int     dict_sockmap_max_reply = DICT_SOCKMAP_DEF_MAX_REPLY;
    125 static int dict_sockmap_max_idle = DICT_SOCKMAP_DEF_MAX_IDLE;
    126 static int dict_sockmap_max_ttl = DICT_SOCKMAP_DEF_MAX_TTL;
    127 
    128  /*
    129   * The client handle is shared between socketmap instances that have the
    130   * same inet:host:port or unix:pathame information. This could be factored
    131   * out as a general module for reference-counted handles of any kind.
    132   */
    133 static HTABLE *dict_sockmap_handles;	/* shared handles */
    134 
    135 typedef struct {
    136     AUTO_CLNT *client_handle;		/* the client handle */
    137     int     refcount;			/* the reference count */
    138 } DICT_SOCKMAP_REFC_HANDLE;
    139 
    140 #define DICT_SOCKMAP_RH_NAME(ht)	(ht)->key
    141 #define DICT_SOCKMAP_RH_HANDLE(ht) \
    142 	((DICT_SOCKMAP_REFC_HANDLE *) (ht)->value)->client_handle
    143 #define DICT_SOCKMAP_RH_REFCOUNT(ht) \
    144 	((DICT_SOCKMAP_REFC_HANDLE *) (ht)->value)->refcount
    145 
    146  /*
    147   * Socketmap protocol elements.
    148   */
    149 #define DICT_SOCKMAP_PROT_OK		"OK"
    150 #define DICT_SOCKMAP_PROT_NOTFOUND	"NOTFOUND"
    151 #define DICT_SOCKMAP_PROT_TEMP		"TEMP"
    152 #define DICT_SOCKMAP_PROT_TIMEOUT	"TIMEOUT"
    153 #define DICT_SOCKMAP_PROT_PERM		"PERM"
    154 
    155  /*
    156   * SLMs.
    157   */
    158 #define STR(x)	vstring_str(x)
    159 #define LEN(x)	VSTRING_LEN(x)
    160 
    161 /* dict_sockmap_lookup - socket map lookup */
    162 
    163 static const char *dict_sockmap_lookup(DICT *dict, const char *key)
    164 {
    165     const char *myname = "dict_sockmap_lookup";
    166     DICT_SOCKMAP *dp = (DICT_SOCKMAP *) dict;
    167     AUTO_CLNT *sockmap_clnt = DICT_SOCKMAP_RH_HANDLE(dp->client_info);
    168     VSTREAM *fp;
    169     int     netstring_err;
    170     char   *reply_payload;
    171     int     except_count;
    172     const char *error_class;
    173 
    174     if (msg_verbose)
    175 	msg_info("%s: key %s", myname, key);
    176 
    177     /*
    178      * Optionally fold the key.
    179      */
    180     if (dict->flags & DICT_FLAG_FOLD_MUL) {
    181 	if (dict->fold_buf == 0)
    182 	    dict->fold_buf = vstring_alloc(100);
    183 	vstring_strcpy(dict->fold_buf, key);
    184 	key = lowercase(STR(dict->fold_buf));
    185     }
    186 
    187     /*
    188      * We retry connection-level errors once, to make server restarts
    189      * transparent.
    190      */
    191     for (except_count = 0; /* see below */ ; except_count++) {
    192 
    193 	/*
    194 	 * Look up the stream.
    195 	 */
    196 	if ((fp = auto_clnt_access(sockmap_clnt)) == 0) {
    197 	    msg_warn("table %s:%s lookup error: %m", dict->type, dict->name);
    198 	    dict->error = DICT_ERR_RETRY;
    199 	    return (0);
    200 	}
    201 
    202 	/*
    203 	 * Set up an exception handler.
    204 	 */
    205 	netstring_setup(fp, dict_sockmap_timeout);
    206 	if ((netstring_err = vstream_setjmp(fp)) == 0) {
    207 
    208 	    /*
    209 	     * Send the query. This may raise an exception.
    210 	     */
    211 	    vstring_sprintf(dp->rdwr_buf, "%s %s", dp->sockmap_name, key);
    212 	    NETSTRING_PUT_BUF(fp, dp->rdwr_buf);
    213 
    214 	    /*
    215 	     * Receive the response. This may raise an exception.
    216 	     */
    217 	    netstring_get(fp, dp->rdwr_buf, dict_sockmap_max_reply);
    218 
    219 	    /*
    220 	     * If we got here, then no exception was raised.
    221 	     */
    222 	    break;
    223 	}
    224 
    225 	/*
    226 	 * Handle exceptions.
    227 	 */
    228 	else {
    229 
    230 	    /*
    231 	     * We retry a broken connection only once.
    232 	     */
    233 	    if (except_count == 0 && netstring_err == NETSTRING_ERR_EOF
    234 		&& errno != ETIMEDOUT) {
    235 		auto_clnt_recover(sockmap_clnt);
    236 		continue;
    237 	    }
    238 
    239 	    /*
    240 	     * We do not retry other errors.
    241 	     */
    242 	    else {
    243 		msg_warn("table %s:%s lookup error: %s",
    244 			 dict->type, dict->name,
    245 			 netstring_strerror(netstring_err));
    246 		dict->error = DICT_ERR_RETRY;
    247 		return (0);
    248 	    }
    249 	}
    250     }
    251 
    252     /*
    253      * Parse the reply.
    254      */
    255     VSTRING_TERMINATE(dp->rdwr_buf);
    256     reply_payload = split_at(STR(dp->rdwr_buf), ' ');
    257     if (strcmp(STR(dp->rdwr_buf), DICT_SOCKMAP_PROT_OK) == 0) {
    258 	dict->error = 0;
    259 	/* 202604 Claude: don't return NULL with dict->error==0. */
    260 	return (reply_payload ? reply_payload : "");
    261     } else if (strcmp(STR(dp->rdwr_buf), DICT_SOCKMAP_PROT_NOTFOUND) == 0) {
    262 	dict->error = 0;
    263 	return (0);
    264     }
    265     /* We got no definitive reply. */
    266     if (strcmp(STR(dp->rdwr_buf), DICT_SOCKMAP_PROT_TEMP) == 0) {
    267 	error_class = "temporary";
    268 	dict->error = DICT_ERR_RETRY;
    269     } else if (strcmp(STR(dp->rdwr_buf), DICT_SOCKMAP_PROT_TIMEOUT) == 0) {
    270 	error_class = "timeout";
    271 	dict->error = DICT_ERR_RETRY;
    272     } else if (strcmp(STR(dp->rdwr_buf), DICT_SOCKMAP_PROT_PERM) == 0) {
    273 	error_class = "permanent";
    274 	dict->error = DICT_ERR_CONFIG;
    275     } else {
    276 	error_class = "unknown";
    277 	dict->error = DICT_ERR_RETRY;
    278     }
    279     while (reply_payload && ISSPACE(*reply_payload))
    280 	reply_payload++;
    281     msg_warn("%s:%s socketmap server %s error%s%.200s",
    282 	     dict->type, dict->name, error_class,
    283 	     reply_payload && *reply_payload ? ": " : "",
    284 	     reply_payload && *reply_payload ?
    285 	     printable(reply_payload, '?') : "");
    286     return (0);
    287 }
    288 
    289 /* dict_sockmap_close - close socket map */
    290 
    291 static void dict_sockmap_close(DICT *dict)
    292 {
    293     const char *myname = "dict_sockmap_close";
    294     DICT_SOCKMAP *dp = (DICT_SOCKMAP *) dict;
    295 
    296     if (dict_sockmap_handles == 0 || dict_sockmap_handles->used == 0)
    297 	msg_panic("%s: attempt to close a non-existent map", myname);
    298     vstring_free(dp->rdwr_buf);
    299     myfree(dp->sockmap_name);
    300     if (--DICT_SOCKMAP_RH_REFCOUNT(dp->client_info) == 0) {
    301 	auto_clnt_free(DICT_SOCKMAP_RH_HANDLE(dp->client_info));
    302 	htable_delete(dict_sockmap_handles,
    303 		      DICT_SOCKMAP_RH_NAME(dp->client_info), myfree);
    304     }
    305     if (dict->fold_buf)
    306 	vstring_free(dict->fold_buf);
    307     dict_free(dict);
    308 }
    309 
    310 /* dict_sockmap_open - open socket map */
    311 
    312 DICT   *dict_sockmap_open(const char *mapname, int open_flags, int dict_flags)
    313 {
    314     DICT_SOCKMAP *dp;
    315     char   *saved_name = 0;
    316     char   *sockmap;
    317     DICT_SOCKMAP_REFC_HANDLE *ref_handle;
    318     HTABLE_INFO *client_info;
    319 
    320     /*
    321      * Let the optimizer worry about eliminating redundant code.
    322      */
    323 #define DICT_SOCKMAP_OPEN_RETURN(d) do { \
    324 	DICT *__d = (d); \
    325 	if (saved_name != 0) \
    326 	    myfree(saved_name); \
    327 	return (__d); \
    328     } while (0)
    329 
    330     /*
    331      * Sanity checks.
    332      */
    333     if (open_flags != O_RDONLY)
    334 	DICT_SOCKMAP_OPEN_RETURN(dict_surrogate(DICT_TYPE_SOCKMAP, mapname,
    335 						open_flags, dict_flags,
    336 				  "%s:%s map requires O_RDONLY access mode",
    337 						DICT_TYPE_SOCKMAP, mapname));
    338     if (dict_flags & DICT_FLAG_NO_UNAUTH)
    339 	DICT_SOCKMAP_OPEN_RETURN(dict_surrogate(DICT_TYPE_SOCKMAP, mapname,
    340 						open_flags, dict_flags,
    341 		     "%s:%s map is not allowed for security-sensitive data",
    342 						DICT_TYPE_SOCKMAP, mapname));
    343 
    344     /*
    345      * Separate the socketmap name from the socketmap server name.
    346      */
    347     saved_name = mystrdup(mapname);
    348     if ((sockmap = split_at_right(saved_name, ':')) == 0)
    349 	DICT_SOCKMAP_OPEN_RETURN(dict_surrogate(DICT_TYPE_SOCKMAP, mapname,
    350 						open_flags, dict_flags,
    351 				    "%s requires server:socketmap argument",
    352 						DICT_TYPE_SOCKMAP));
    353 
    354     /*
    355      * Use one reference-counted client handle for all socketmaps with the
    356      * same inet:host:port or unix:pathname information.
    357      *
    358      * XXX Todo: graceful degradation after endpoint syntax error.
    359      */
    360     if (dict_sockmap_handles == 0)
    361 	dict_sockmap_handles = htable_create(1);
    362     if ((client_info = htable_locate(dict_sockmap_handles, saved_name)) == 0) {
    363 	ref_handle = (DICT_SOCKMAP_REFC_HANDLE *) mymalloc(sizeof(*ref_handle));
    364 	client_info = htable_enter(dict_sockmap_handles,
    365 				   saved_name, (void *) ref_handle);
    366 	/* XXX Late initialization, so we can reuse macros for consistency. */
    367 	DICT_SOCKMAP_RH_REFCOUNT(client_info) = 1;
    368 	DICT_SOCKMAP_RH_HANDLE(client_info) =
    369 	    auto_clnt_create(saved_name, dict_sockmap_timeout,
    370 			     dict_sockmap_max_idle, dict_sockmap_max_ttl);
    371     } else
    372 	DICT_SOCKMAP_RH_REFCOUNT(client_info) += 1;
    373 
    374     /*
    375      * Instantiate a socket map handle.
    376      */
    377     dp = (DICT_SOCKMAP *) dict_alloc(DICT_TYPE_SOCKMAP, mapname, sizeof(*dp));
    378     dp->rdwr_buf = vstring_alloc(100);
    379     dp->sockmap_name = mystrdup(sockmap);
    380     dp->client_info = client_info;
    381     dp->dict.lookup = dict_sockmap_lookup;
    382     dp->dict.close = dict_sockmap_close;
    383     /* Don't look up parent domains or network superblocks. */
    384     dp->dict.flags = dict_flags | DICT_FLAG_PATTERN;
    385 
    386     DICT_SOCKMAP_OPEN_RETURN(&dp->dict);
    387 }
    388