1 /* $NetBSD: dict_mongodb.c,v 1.3 2026/05/09 18:49:16 christos Exp $ */ 2 3 /*++ 4 /* NAME 5 /* dict_mongodb 3 6 /* SUMMARY 7 /* dictionary interface to mongodb, compatible with libmongoc-1.0 8 /* SYNOPSIS 9 /* #include <dict_mongodb.h> 10 /* 11 /* DICT *dict_mongodb_open(name, open_flags, dict_flags) 12 /* const char *name; 13 /* int open_flags; 14 /* int dict_flags; 15 /* DESCRIPTION 16 /* dict_mongodb_open() opens a MongoDB database, providing a 17 /* dictionary interface for Postfix mappings. The result is a 18 /* pointer to the installed dictionary. 19 /* 20 /* Configuration parameters are described in mongodb_table(5). 21 /* 22 /* Arguments: 23 /* .IP name 24 /* Either the path to the MongoDB configuration file (if it 25 /* starts with '/' or '.'), or the prefix which will be used 26 /* to obtain main.cf configuration parameters for this search. 27 /* 28 /* In the first case, configuration parameters are specified 29 /* in the file as \fIname\fR=\fIvalue\fR pairs. 30 /* 31 /* In the second case, the configuration parameters are prefixed 32 /* with the value of \fIname\fR and an underscore, and they 33 /* are specified in main.cf. For example, if this value is 34 /* \fImongodbconf\fR, the parameters would look like 35 /* \fImongodbconf_uri\fR, \fImongodbconf_collection\fR, and 36 /* so on. 37 /* .IP open_flags 38 /* Must be O_RDONLY 39 /* .IP dict_flags 40 /* See dict_open(3). 41 /* SEE ALSO 42 /* dict(3) generic dictionary manager 43 /* HISTORY 44 /* .ad 45 /* .fi 46 /* MongoDB support was added in Postfix 3.9. 47 /* AUTHOR(S) 48 /* Hamid Maadani (hamid (at) dexo.tech) 49 /* Dextrous Technologies, LLC 50 /* 51 /* Edited by: 52 /* Wietse Venema 53 /* porcupine.org 54 /* 55 /* Based on prior work by: 56 /* Stephan Ferraro 57 /* Aionda GmbH 58 /*--*/ 59 60 /* 61 * System library. 62 */ 63 #include <sys_defs.h> 64 #ifdef HAS_MONGODB 65 #include <stdio.h> 66 #include <string.h> 67 #include <stdlib.h> 68 #include <errno.h> 69 #include <ctype.h> 70 #include <inttypes.h> /* C99 PRId64 */ 71 72 #include <bson/bson.h> 73 #include <mongoc/mongoc.h> 74 75 /* 76 * Utility library. 77 */ 78 #include <dict.h> 79 #include <msg.h> 80 #include <mymalloc.h> 81 #include <vstring.h> 82 #include <stringops.h> 83 #include <auto_clnt.h> 84 #include <vstream.h> 85 86 /* 87 * Global library. 88 */ 89 #include <cfg_parser.h> 90 #include <db_common.h> 91 92 /* 93 * Application-specific. 94 */ 95 #include <dict_mongodb.h> 96 97 /* 98 * Initial size for dynamically-allocated buffers. 99 */ 100 #ifndef BUFFER_SIZE 101 #define BUFFER_SIZE 1024 102 #endif 103 104 #define INIT_VSTR(buf, len) do { \ 105 if (buf == 0) \ 106 buf = vstring_alloc(len); \ 107 VSTRING_RESET(buf); \ 108 VSTRING_TERMINATE(buf); \ 109 } while (0) 110 111 /* Structure of one mongodb dictionary handle. */ 112 typedef struct { 113 /* Initialized by dict_mongodb_open(). */ 114 DICT dict; /* Parent class */ 115 CFG_PARSER *parser; /* Configuration file parser */ 116 mongoc_client_t *client; /* Mongo C client handle */ 117 /* Initialized by mongodb_parse_config(). */ 118 char *uri; /* mongodb+srv:/*localhost:27017 */ 119 char *dbname; /* Database name */ 120 char *collection; /* Collection name */ 121 char *query_filter; /* db_common_expand() query template */ 122 char *projection; /* Advanced MongoDB projection */ 123 char *result_attribute; /* The key(s) to return the data for */ 124 char *result_format; /* db_common_expand() result_template */ 125 int expansion_limit; /* Result expansion limit */ 126 void *ctx; /* db_common handle */ 127 } DICT_MONGODB; 128 129 /* Per-process initialization. */ 130 static bool init_done = false; 131 132 /* itoa - int64_t to string */ 133 134 static char *itoa(int64_t val) 135 { 136 static char buf[21] = {0}; 137 int ret; 138 139 /* 140 * XXX(Wietse) replaced custom code with standard library calls that 141 * handle zero, and negative values. 142 */ 143 #define PRId64_FORMAT "%" PRId64 144 145 ret = snprintf(buf, sizeof(buf), PRId64_FORMAT, val); 146 if (ret < 0) 147 msg_panic("itoa: output error for '%s'", PRId64_FORMAT); 148 if (ret >= sizeof(buf)) 149 msg_panic("itoa: output for '%s' exceeds space %ld", 150 PRId64_FORMAT, sizeof(buf)); 151 return (buf); 152 } 153 154 /* mongodb_parse_config - parse mongodb configuration file */ 155 156 static void mongodb_parse_config(DICT_MONGODB *dict_mongodb, 157 const char *mongodbcf) 158 { 159 CFG_PARSER *p = dict_mongodb->parser; 160 161 /* 162 * Parse the configuration file. 163 */ 164 dict_mongodb->uri = cfg_get_str(p, "uri", NULL, 1, 0); 165 dict_mongodb->dbname = cfg_get_str(p, "dbname", NULL, 1, 0); 166 dict_mongodb->collection = cfg_get_str(p, "collection", NULL, 1, 0); 167 dict_mongodb->query_filter = cfg_get_str(p, "query_filter", NULL, 1, 0); 168 169 /* 170 * One of projection and result_attribute must be specified. That is 171 * enforced in the caller. 172 */ 173 dict_mongodb->projection = cfg_get_str(p, "projection", NULL, 0, 0); 174 dict_mongodb->result_attribute 175 = cfg_get_str(p, "result_attribute", NULL, 0, 0); 176 dict_mongodb->result_format 177 = cfg_get_str(dict_mongodb->parser, "result_format", "%s", 1, 0); 178 dict_mongodb->expansion_limit 179 = cfg_get_int(dict_mongodb->parser, "expansion_limit", 10, 0, 100); 180 181 /* 182 * db_common query parsing and domain pattern lookup. 183 */ 184 dict_mongodb->ctx = 0; 185 (void) db_common_parse(&dict_mongodb->dict, &dict_mongodb->ctx, 186 dict_mongodb->query_filter, 1); 187 db_common_parse_domain(dict_mongodb->parser, dict_mongodb->ctx); 188 } 189 190 /* expand_value - expand lookup result value */ 191 192 static bool expand_value(DICT_MONGODB *dict_mongodb, const char *p, 193 const char *lookup_name, 194 VSTRING *resultString, 195 int *expansion, const char *key) 196 { 197 198 /* 199 * If a lookup result cannot be processed due to an expansion limit 200 * error, return a DICT_ERR_RETRY error code and a 'false' result value. 201 * As documented for many dict_xxx() implementations, and expansion limit 202 * error is considered a temporary error. 203 */ 204 if (dict_mongodb->expansion_limit > 0 205 && ++(*expansion) > dict_mongodb->expansion_limit) { 206 msg_warn("%s:%s: expansion limit exceeded for key: '%s'", 207 dict_mongodb->dict.type, dict_mongodb->dict.name, key); 208 dict_mongodb->dict.error = DICT_ERR_RETRY; 209 return (false); 210 } 211 212 /* 213 * XXX(Wietse) Added the dict_mongodb_lookup() lookup_name argument, 214 * because it selects code paths inside db_common_expand() that are 215 * specifically for lookup results instead of lookup keys, including 216 * %[SUD] substitution. 217 */ 218 db_common_expand(dict_mongodb->ctx, dict_mongodb->result_format, p, 219 lookup_name, resultString, 0); 220 return (true); 221 } 222 223 /* get_result_string - convert lookup result to string, or set dict.error */ 224 225 static char *get_result_string(DICT_MONGODB *dict_mongodb, 226 VSTRING *resultString, 227 bson_iter_t *iter, 228 const char *lookup_name, 229 int *expansion, 230 const char *key) 231 { 232 char *p = NULL; 233 bool got_one_result = false; 234 235 /* 236 * If a lookup result cannot be processed due to an error, return a 237 * non-zero error code and a NULL result value. 238 */ 239 INIT_VSTR(resultString, BUFFER_SIZE); 240 while (dict_mongodb->dict.error == DICT_ERR_NONE && bson_iter_next(iter)) { 241 switch (bson_iter_type(iter)) { 242 case BSON_TYPE_UTF8: 243 p = (char *) bson_iter_utf8(iter, NULL); 244 if (!bson_utf8_validate(p, strlen(p), true)) { 245 msg_warn("%s:%s: invalid UTF-8 in lookup result '%s'", 246 dict_mongodb->dict.type, dict_mongodb->dict.name, p); 247 dict_mongodb->dict.error = DICT_ERR_RETRY; 248 break; 249 } 250 got_one_result |= expand_value(dict_mongodb, p, lookup_name, 251 resultString, expansion, key); 252 break; 253 case BSON_TYPE_INT64: 254 case BSON_TYPE_INT32: 255 p = itoa(bson_iter_as_int64(iter)); 256 got_one_result |= expand_value(dict_mongodb, p, lookup_name, 257 resultString, expansion, key); 258 break; 259 case BSON_TYPE_ARRAY: 260 ; /* For pre-C23 Clang. */ 261 const uint8_t *dataBuffer = NULL; 262 unsigned int len = 0; 263 bson_iter_t dataIter; 264 bson_t *data = NULL; 265 266 /* 267 * XXX(Wietse) are there any non-error cases, such as a valid but 268 * empty array, where bson_new_from_data() or bson_iter_init() 269 * would return null or false? If there are no such cases then we 270 * must handle null/false as an error. 271 */ 272 bson_iter_array(iter, &len, &dataBuffer); 273 if ((data = bson_new_from_data(dataBuffer, len)) != 0 274 && bson_iter_init(&dataIter, data)) { 275 VSTRING *iterResult = vstring_alloc(BUFFER_SIZE); 276 277 if ((p = get_result_string(dict_mongodb, iterResult, &dataIter, 278 lookup_name, expansion, key)) != 0) { 279 vstring_sprintf_append(resultString, (got_one_result) ? 280 ",%s" : "%s", p); 281 got_one_result |= true; 282 } 283 vstring_free(iterResult); 284 } 285 bson_destroy(data); 286 break; 287 default: 288 /* Unexpected field type. As documented, warn and ignore. */ 289 msg_warn("%s:%s: failed to retrieve value of '%s', " 290 "Unknown result type %d.", dict_mongodb->dict.type, 291 dict_mongodb->dict.name, bson_iter_key(iter), 292 bson_iter_type(iter)); 293 break; 294 } 295 } 296 if (dict_mongodb->dict.error != DICT_ERR_NONE || !got_one_result) 297 return (0); 298 return (vstring_str(resultString)); 299 } 300 301 /* dict_mongdb_quote - quote json string */ 302 303 static void dict_mongdb_quote(DICT *dict, const char *name, VSTRING *result) 304 { 305 /* quote_for_json_append() will resize the result buffer as needed. */ 306 (void) quote_for_json_append(result, name, -1); 307 } 308 309 /* dict_mongdb_append_result_attributes - projection builder */ 310 311 static int dict_mongdb_append_result_attribute(bson_t * projection, 312 const char *result_attribute) 313 { 314 char *ra = mystrdup(result_attribute); 315 char *pp = ra; 316 char *cp; 317 int ok = 1; 318 319 while (ok && (cp = mystrtok(&pp, CHARS_COMMA_SP)) != 0) 320 ok = BSON_APPEND_INT32(projection, cp, 1); 321 myfree(ra); 322 return (ok); 323 } 324 325 /* dict_mongodb_lookup - find database entry using mongo query language */ 326 327 static const char *dict_mongodb_lookup(DICT *dict, const char *name) 328 { 329 DICT_MONGODB *dict_mongodb = (DICT_MONGODB *) dict; 330 mongoc_collection_t *coll = NULL; 331 mongoc_cursor_t *cursor = NULL; 332 bson_iter_t iter; 333 const bson_t *doc = NULL; 334 bson_t *query = NULL; 335 bson_t *options = NULL; 336 bson_t *projection = NULL; 337 bson_error_t error; 338 char *result = NULL; 339 static VSTRING *queryString = NULL; 340 static VSTRING *resultString = NULL; 341 int domain_rc; 342 int expansion = 0; 343 344 dict_mongodb->dict.error = DICT_ERR_NONE; 345 346 /* 347 * If they specified a domain list for this map, then only search for 348 * addresses in domains on the list. This can significantly reduce the 349 * load on the database. 350 */ 351 if ((domain_rc = db_common_check_domain(dict_mongodb->ctx, name)) == 0) { 352 if (msg_verbose) 353 msg_info("%s:%s: skipping lookup of '%s': domain mismatch", 354 dict_mongodb->dict.type, dict_mongodb->dict.name, name); 355 return (0); 356 } else if (domain_rc < 0) { 357 DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0); 358 } 359 360 /* 361 * Ugly macros to make error and non-error handling code more readable. 362 * If code size is a concern, them an optimizing compiler can eliminate 363 * dead code or duplicated code. 364 */ 365 366 /* Set an error code, and return null. */ 367 #define DICT_MONGODB_LOOKUP_ERR_RETURN(err) do { \ 368 dict_mongodb->dict.error = (err); \ 369 DICT_MONGODB_LOOKUP_RETURN((char *) 0); \ 370 } while (0); 371 372 /* Pass through any error, and return the specified value. */ 373 #define DICT_MONGODB_LOOKUP_RETURN(val) do { \ 374 if (coll) mongoc_collection_destroy(coll); \ 375 if (cursor) mongoc_cursor_destroy(cursor); \ 376 if (query) bson_destroy(query); \ 377 if (options) bson_destroy(options); \ 378 if (projection) bson_destroy(projection); \ 379 return (val); \ 380 } while (0) 381 382 coll = mongoc_client_get_collection(dict_mongodb->client, 383 dict_mongodb->dbname, 384 dict_mongodb->collection); 385 if (!coll) { 386 msg_warn("%s:%s: failed to get collection [%s] from [%s]", 387 dict_mongodb->dict.type, dict_mongodb->dict.name, 388 dict_mongodb->collection, dict_mongodb->dbname); 389 DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); 390 } 391 392 /* 393 * Use the specified result projection, or craft one from the 394 * result_attribute. Exclude the _id field from the result. 395 */ 396 options = bson_new(); 397 if (dict_mongodb->projection) { 398 projection = bson_new_from_json((uint8_t *) dict_mongodb->projection, 399 -1, &error); 400 if (!projection) { 401 msg_warn("%s:%s: failed to create a projection from '%s': %s", 402 dict_mongodb->dict.type, dict_mongodb->dict.name, 403 dict_mongodb->projection, error.message); 404 DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); 405 } 406 if (!BSON_APPEND_INT32(projection, "_id", 0) 407 || !BSON_APPEND_DOCUMENT(options, "projection", projection)) { 408 msg_warn("%s:%s: failed to append a projection from '%s'", 409 dict_mongodb->dict.type, dict_mongodb->dict.name, 410 dict_mongodb->projection); 411 DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); 412 } 413 } else if (dict_mongodb->result_attribute) { 414 bson_t res_attr; 415 416 if (!BSON_APPEND_DOCUMENT_BEGIN(options, "projection", &res_attr) 417 || !BSON_APPEND_INT32(&res_attr, "_id", 0) 418 || !dict_mongdb_append_result_attribute(&res_attr, 419 dict_mongodb->result_attribute) 420 || !bson_append_document_end(options, &res_attr)) { 421 msg_warn("%s:%s: failed to append a projection from '%s'", 422 dict_mongodb->dict.type, dict_mongodb->dict.name, 423 dict_mongodb->result_attribute); 424 DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); 425 } 426 } else { 427 /* Can't happen. The configuration parser should reject this. */ 428 msg_panic("%s:%s: empty 'projection' and 'result_attribute'", 429 dict_mongodb->dict.type, dict_mongodb->dict.name); 430 } 431 432 /* 433 * Expand filter template. This uses a quoting function to prevent 434 * metacharacter injection with parts from a crafted email address. 435 */ 436 INIT_VSTR(queryString, BUFFER_SIZE); 437 if (!db_common_expand(dict_mongodb->ctx, dict_mongodb->query_filter, 438 name, 0, queryString, dict_mongdb_quote)) 439 /* Suppress the actual lookup if the expansion is empty. */ 440 DICT_MONGODB_LOOKUP_RETURN(0); 441 442 /* Create the query from the expanded query template. */ 443 query = bson_new_from_json((uint8_t *) vstring_str(queryString), 444 -1, &error); 445 if (!query) { 446 msg_warn("%s:%s: failed to create a query from '%s': %s", 447 dict_mongodb->dict.type, dict_mongodb->dict.name, 448 vstring_str(queryString), error.message); 449 DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); 450 } 451 /* Run the query. */ 452 cursor = mongoc_collection_find_with_opts(coll, query, options, NULL); 453 if (mongoc_cursor_error(cursor, &error)) { 454 msg_warn("%s:%s: cursor error for '%s': %s", 455 dict_mongodb->dict.type, dict_mongodb->dict.name, 456 vstring_str(queryString), error.message); 457 DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); 458 } 459 /* Convert the lookup result to C string. */ 460 INIT_VSTR(resultString, BUFFER_SIZE); 461 while (mongoc_cursor_next(cursor, &doc)) { 462 if (bson_iter_init(&iter, doc)) { 463 result = get_result_string(dict_mongodb, resultString, &iter, 464 name, &expansion, name); 465 } 466 } 467 DICT_MONGODB_LOOKUP_RETURN(result); 468 } 469 470 /* dict_mongodb_close - close MongoDB database */ 471 472 static void dict_mongodb_close(DICT *dict) 473 { 474 DICT_MONGODB *dict_mongodb = (DICT_MONGODB *) dict; 475 476 cfg_parser_free(dict_mongodb->parser); 477 if (dict_mongodb->ctx) { 478 db_common_free_ctx(dict_mongodb->ctx); 479 } 480 myfree(dict_mongodb->uri); 481 myfree(dict_mongodb->dbname); 482 myfree(dict_mongodb->collection); 483 myfree(dict_mongodb->query_filter); 484 485 if (dict_mongodb->result_attribute) { 486 myfree(dict_mongodb->result_attribute); 487 } 488 if (dict_mongodb->result_format) { 489 myfree(dict_mongodb->result_format); 490 } 491 if (dict_mongodb->projection) { 492 myfree(dict_mongodb->projection); 493 } 494 if (dict_mongodb->client) { 495 mongoc_client_destroy(dict_mongodb->client); 496 } 497 dict_free(dict); 498 } 499 500 /* dict_mongodb_open - open MongoDB database connection */ 501 502 DICT *dict_mongodb_open(const char *name, int open_flags, int dict_flags) 503 { 504 DICT_MONGODB *dict_mongodb; 505 CFG_PARSER *parser; 506 mongoc_uri_t *uri = 0; 507 bson_error_t error; 508 509 /* Sanity checks. */ 510 if (open_flags != O_RDONLY) { 511 return (dict_surrogate(DICT_TYPE_MONGODB, name, open_flags, dict_flags, 512 "%s:%s: map requires O_RDONLY access mode", 513 DICT_TYPE_MONGODB, name)); 514 } 515 /* Open the configuration file. */ 516 if ((parser = cfg_parser_alloc(name)) == 0) { 517 return (dict_surrogate(DICT_TYPE_MONGODB, name, open_flags, dict_flags, 518 "open %s: %m", name)); 519 } 520 /* Create the dictionary object. */ 521 dict_mongodb = (DICT_MONGODB *) dict_alloc(DICT_TYPE_MONGODB, name, 522 sizeof(*dict_mongodb)); 523 dict_mongodb->dict.lookup = dict_mongodb_lookup; 524 dict_mongodb->dict.close = dict_mongodb_close; 525 dict_mongodb->dict.flags = dict_flags; 526 dict_mongodb->parser = parser; 527 dict_mongodb->dict.owner = cfg_get_owner(dict_mongodb->parser); 528 dict_mongodb->client = NULL; 529 530 /* Parse config. */ 531 mongodb_parse_config(dict_mongodb, name); 532 if (!dict_mongodb->projection == !dict_mongodb->result_attribute) { 533 dict_mongodb_close(&dict_mongodb->dict); 534 return (dict_surrogate(DICT_TYPE_MONGODB, name, open_flags, dict_flags, 535 "%s:%s: specify exactly one of 'projection' or 'result_attribute'", 536 DICT_TYPE_MONGODB, name)); 537 } 538 /* One-time initialization of libmongoc 's internals. */ 539 if (!init_done) { 540 mongoc_init(); 541 init_done = true; 542 } 543 #define DICT_MONGODB_OPEN_ERR_RETURN(d) do { \ 544 DICT *_d = (d); \ 545 if (uri) mongoc_uri_destroy(uri); \ 546 dict_mongodb_close(&dict_mongodb->dict); \ 547 return (_d); \ 548 } while (0); 549 550 uri = mongoc_uri_new_with_error(dict_mongodb->uri, &error); 551 if (!uri) 552 DICT_MONGODB_OPEN_ERR_RETURN(dict_surrogate(DICT_TYPE_MONGODB, name, 553 open_flags, dict_flags, 554 "%s:%s: failed to parse URI '%s': %s", 555 DICT_TYPE_MONGODB, name, 556 dict_mongodb->uri, error.message)); 557 558 dict_mongodb->client = mongoc_client_new_from_uri_with_error(uri, &error); 559 if (!dict_mongodb->client) 560 DICT_MONGODB_OPEN_ERR_RETURN(dict_surrogate(DICT_TYPE_MONGODB, name, 561 open_flags, dict_flags, 562 "%s:%s: failed to create client for '%s': %s", 563 DICT_TYPE_MONGODB, name, 564 dict_mongodb->uri, 565 error.message)); 566 567 mongoc_uri_destroy(uri); 568 mongoc_client_set_error_api(dict_mongodb->client, MONGOC_ERROR_API_VERSION_2); 569 return (&dict_mongodb->dict); 570 } 571 572 #endif 573