1 /******************************************************************************/
2 /* pfixtools: a collection of postfix related tools */
4 /* ________________________________________________________________________ */
6 /* Redistribution and use in source and binary forms, with or without */
7 /* modification, are permitted provided that the following conditions */
10 /* 1. Redistributions of source code must retain the above copyright */
11 /* notice, this list of conditions and the following disclaimer. */
12 /* 2. Redistributions in binary form must reproduce the above copyright */
13 /* notice, this list of conditions and the following disclaimer in the */
14 /* documentation and/or other materials provided with the distribution. */
15 /* 3. The names of its contributors may not be used to endorse or promote */
16 /* products derived from this software without specific prior written */
19 /* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND */
20 /* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE */
21 /* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR */
22 /* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS */
23 /* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR */
24 /* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF */
25 /* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS */
26 /* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN */
27 /* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) */
28 /* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF */
29 /* THE POSSIBILITY OF SUCH DAMAGE. */
30 /******************************************************************************/
33 * Copyright © 2008 Florent Bruneau
41 #include "policy_tokens.h"
43 typedef struct strlist_config_t {
57 unsigned is_hostname :1;
59 unsigned match_sender :1;
60 unsigned match_recipient :1;
62 unsigned match_helo :1;
63 unsigned match_client :1;
64 unsigned match_reverse :1;
67 typedef struct strlist_async_data_t {
68 A(rbl_result_t) results;
72 } strlist_async_data_t;
74 static filter_type_t filter_type = FTK_UNKNOWN;
77 static strlist_config_t *strlist_config_new(void)
79 return p_new(strlist_config_t, 1);
82 static void strlist_config_delete(strlist_config_t **config)
85 array_deep_wipe((*config)->tries, trie_delete);
86 array_wipe((*config)->weights);
87 array_wipe((*config)->reverses);
88 array_wipe((*config)->partiales);
89 array_wipe((*config)->hosts);
90 array_wipe((*config)->host_offsets);
91 array_wipe((*config)->host_weights);
96 static inline void strlist_copy(char *dest, const char *str, ssize_t str_len,
101 for (const char *src = str + str_len - 1 ; src >= str ; --src) {
102 *dest = ascii_tolower(*src);
106 for (int i = 0 ; i < str_len ; ++i) {
107 *dest = ascii_tolower(str[i]);
116 static trie_t *strlist_create(const char *file, bool reverse, bool lock)
124 if (!file_map_open(&map, file, false)) {
129 while (end > p && end[-1] != '\n') {
132 if (end != map.end) {
133 warn("file %s miss a final \\n, ignoring last line",
138 while (p < end && p != NULL) {
139 const char *eol = (char *)memchr(p, '\n', end - p);
143 if (eol - p >= BUFSIZ) {
144 err("unreasonnable long line");
145 file_map_close(&map);
150 const char *eos = eol;
151 while (p < eos && isspace(*p)) {
154 while (p < eos && isspace(eos[-1])) {
158 strlist_copy(line, p, eos - p, reverse);
159 trie_insert(db, line);
165 file_map_close(&map);
166 trie_compile(db, lock);
167 info("%s loaded, %u entries", file, count);
171 static bool strlist_create_from_rhbl(const char *file, bool lock,
172 trie_t **phosts, trie_t **pdomains)
174 trie_t *hosts, *domains;
175 uint32_t host_count, domain_count;
180 if (!file_map_open(&map, file, false)) {
185 while (end > p && end[-1] != '\n') {
188 if (end != map.end) {
189 warn("file %s miss a final \\n, ignoring last line",
195 domains = trie_new();
197 while (p < end && p != NULL) {
198 const char *eol = (char *)memchr(p, '\n', end - p);
202 if (eol - p >= BUFSIZ) {
203 err("unreasonnable long line");
204 file_map_close(&map);
206 trie_delete(&domains);
210 const char *eos = eol;
211 while (p < eos && isspace(*p)) {
214 while (p < eos && isspace(eos[-1])) {
219 strlist_copy(line, p, eos - p, true);
220 trie_insert(hosts, line);
222 } else if (*p == '*') {
224 strlist_copy(line, p, eos - p, true);
225 trie_insert(domains, line);
232 file_map_close(&map);
233 if (host_count > 0) {
234 trie_compile(hosts, lock);
240 if (domain_count > 0) {
241 trie_compile(domains, lock);
244 trie_delete(&domains);
247 info("rhbl %s loaded, %u hosts, %u domains", file, host_count, domain_count);
248 return hosts != NULL || domains != NULL;
253 static bool strlist_filter_constructor(filter_t *filter)
255 strlist_config_t *config = strlist_config_new();
257 #define PARSE_CHECK(Expr, Str, ...) \
259 err(Str, ##__VA_ARGS__); \
260 strlist_config_delete(&config); \
264 config->hard_threshold = 1;
265 config->soft_threshold = 1;
266 foreach (filter_param_t *param, filter->params) {
267 switch (param->type) {
268 /* file parameter is:
269 * [no]lock:(partial-)(prefix|suffix):weight:filename
271 * - lock: memlock the database in memory.
272 * - nolock: don't memlock the database in memory.
273 * - prefix: perform "prefix" compression on storage.
274 * - suffix perform "suffix" compression on storage.
275 * - \d+: a number describing the weight to give to the match
276 * the given list [mandatory]
277 * the file pointed by filename MUST be a valid string list (one string per
278 * line, empty lines and lines beginning with a '#' are ignored).
283 bool reverse = false;
284 bool partial = false;
286 const char *current = param->value;
287 const char *p = m_strchrnul(param->value, ':');
289 for (int i = 0 ; i < 4 ; ++i) {
290 PARSE_CHECK(i == 3 || *p,
291 "file parameter must contains a locking state "
292 "and a weight option");
295 if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
297 } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
300 PARSE_CHECK(false, "illegal locking state %.*s",
301 (int)(p - current), current);
306 if (p - current > (ssize_t)strlen("partial-")
307 && strncmp(current, "partial-", strlen("partial-")) == 0) {
309 current += strlen("partial-");
311 if ((p - current) == 6 && strncmp(current, "suffix", 6) == 0) {
313 } else if ((p - current) == 6 && strncmp(current, "prefix", 6) == 0) {
316 PARSE_CHECK(false, "illegal character order value %.*s",
317 (int)(p - current), current);
322 weight = strtol(current, &next, 10);
323 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
324 "illegal weight value %.*s",
325 (int)(p - current), current);
329 trie = strlist_create(current, reverse, lock);
330 PARSE_CHECK(trie != NULL,
331 "cannot load string list from %s", current);
332 array_add(config->tries, trie);
333 array_add(config->weights, weight);
334 array_add(config->reverses, reverse);
335 array_add(config->partiales, partial);
340 p = m_strchrnul(current, ':');
345 /* rbldns parameter is:
346 * [no]lock::weight:filename
348 * - lock: memlock the database in memory.
349 * - nolock: don't memlock the database in memory.
350 * - \d+: a number describing the weight to give to the match
351 * the given list [mandatory]
352 * directly import a file issued from a rhbl in rbldns format.
357 trie_t *trie_hosts = NULL;
358 trie_t *trie_domains = NULL;
359 const char *current = param->value;
360 const char *p = m_strchrnul(param->value, ':');
362 for (int i = 0 ; i < 3 ; ++i) {
363 PARSE_CHECK(i == 2 || *p,
364 "file parameter must contains a locking state "
365 "and a weight option");
368 if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
370 } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
373 PARSE_CHECK(false, "illegal locking state %.*s",
374 (int)(p - current), current);
379 weight = strtol(current, &next, 10);
380 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
381 "illegal weight value %.*s",
382 (int)(p - current), current);
386 PARSE_CHECK(strlist_create_from_rhbl(current, lock,
387 &trie_hosts, &trie_domains),
388 "cannot load string list from rhbl %s", current);
389 if (trie_hosts != NULL) {
390 array_add(config->tries, trie_hosts);
391 array_add(config->weights, weight);
392 array_add(config->reverses, true);
393 array_add(config->partiales, false);
395 if (trie_domains != NULL) {
396 array_add(config->tries, trie_domains);
397 array_add(config->weights, weight);
398 array_add(config->reverses, true);
399 array_add(config->partiales, true);
401 config->is_hostname = true;
406 p = m_strchrnul(current, ':');
413 * define a RBL to use through DNS resolution.
417 const char *current = param->value;
418 const char *p = m_strchrnul(param->value, ':');
420 for (int i = 0 ; i < 2 ; ++i) {
421 PARSE_CHECK(i == 1 || *p,
422 "host parameter must contains a weight option");
425 weight = strtol(current, &next, 10);
426 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
427 "illegal weight value %.*s",
428 (int)(p - current), current);
432 array_add(config->host_offsets, array_len(config->hosts));
433 array_append(config->hosts, current, strlen(current) + 1);
434 array_add(config->host_weights, weight);
439 p = m_strchrnul(current, ':');
444 /* hard_threshold parameter is an integer.
445 * If the matching score is greater or equal than this threshold,
446 * the hook "hard_match" is called.
447 * hard_threshold = 1 means, that all matches are hard matches.
450 FILTER_PARAM_PARSE_INT(HARD_THRESHOLD, config->hard_threshold);
452 /* soft_threshold parameter is an integer.
453 * if the matching score is greater or equal than this threshold
454 * and smaller or equal than the hard_threshold, the hook "soft_match"
458 FILTER_PARAM_PARSE_INT(SOFT_THRESHOLD, config->soft_threshold);
460 /* fields to match againes:
461 * fields = field_name(,field_name)*
463 * - hostname: helo_name,client_name,reverse_client_name
464 * - email: sender,recipient
467 const char *current = param->value;
468 const char *p = m_strchrnul(param->value, ',');
470 postlicyd_token tok = policy_tokenize(current, p - current);
472 #define CASE(Up, Low, Type) \
474 config->match_ ## Low = true; \
475 config->is_ ## Type = true; \
477 CASE(HELO_NAME, helo, hostname);
478 CASE(CLIENT_NAME, client, hostname);
479 CASE(REVERSE_CLIENT_NAME, reverse, hostname);
480 CASE(SENDER_DOMAIN, sender, hostname);
481 CASE(RECIPIENT_DOMAIN, recipient, hostname);
482 CASE(SENDER, sender, email);
483 CASE(RECIPIENT, recipient, email);
486 PARSE_CHECK(false, "unknown field %.*s", (int)(p - current), current);
493 p = m_strchrnul(current, ',');
501 PARSE_CHECK(config->is_email != config->is_hostname,
502 "matched field MUST be emails XOR hostnames");
503 PARSE_CHECK(config->tries.len || config->host_offsets.len,
504 "no file parameter in the filter %s", filter->name);
505 filter->data = config;
509 static void strlist_filter_destructor(filter_t *filter)
511 strlist_config_t *config = filter->data;
512 strlist_config_delete(&config);
513 filter->data = config;
516 static void strlist_filter_async(rbl_result_t *result, void *arg)
518 filter_context_t *context = arg;
519 const filter_t *filter = context->current_filter;
520 const strlist_config_t *data = filter->data;
521 strlist_async_data_t *async = context->contexts[filter_type];
523 if (*result != RBL_ERROR) {
524 async->error = false;
528 debug("got asynchronous request result for filter %s, rbl %d, still awaiting %d answers",
529 filter->name, (int)(result - array_ptr(async->results, 0)), async->awaited);
531 if (async->awaited == 0) {
532 filter_result_t res = HTK_FAIL;
537 #define DO_SUM(Field) \
538 if (data->match_ ## Field) { \
539 for (uint32_t i = 0 ; i < array_len(data->host_offsets) ; ++i) { \
540 int weight = array_elt(data->host_weights, i); \
542 switch (array_elt(async->results, j)) { \
544 crit("no more awaited answer but result is ASYNC"); \
547 async->sum += weight; \
561 debug("score is %d", async->sum);
562 if (async->sum >= (uint32_t)data->hard_threshold) {
563 res = HTK_HARD_MATCH;
564 } else if (async->sum >= (uint32_t)data->soft_threshold) {
565 res = HTK_SOFT_MATCH;
568 debug("answering to filter %s", filter->name);
569 filter_post_async_result(context, res);
574 static filter_result_t strlist_filter(const filter_t *filter, const query_t *query,
575 filter_context_t *context)
577 char reverse[BUFSIZ];
579 const strlist_config_t *config = filter->data;
580 strlist_async_data_t *async = context->contexts[filter_type];
584 array_ensure_exact_capacity(async->results, (config->match_client
585 + config->match_sender + config->match_helo
586 + config->match_recipient + config->match_reverse)
587 * array_len(config->host_offsets));
591 if (config->is_email &&
592 ((config->match_sender && query->state < SMTP_MAIL)
593 || (config->match_recipient && query->state != SMTP_RCPT))) {
594 warn("trying to match an email against a field that is not "
595 "available in current protocol state");
597 } else if (config->is_hostname && config->match_helo && query->state < SMTP_HELO) {
598 warn("trying to match hostname against helo before helo is received");
601 #define LOOKUP(Flag, Field) \
602 if (config->match_ ## Flag) { \
603 const int len = m_strlen(query->Field); \
604 strlist_copy(normal, query->Field, len, false); \
605 strlist_copy(reverse, query->Field, len, true); \
606 for (uint32_t i = 0 ; i < config->tries.len ; ++i) { \
607 const int weight = array_elt(config->weights, i); \
608 const trie_t *trie = array_elt(config->tries, i); \
609 const bool rev = array_elt(config->reverses, i); \
610 const bool part = array_elt(config->partiales, i); \
611 if ((!part && trie_lookup(trie, rev ? reverse : normal)) \
612 || (part && trie_prefix(trie, rev ? reverse : normal))) { \
613 async->sum += weight; \
614 if (async->sum >= (uint32_t)config->hard_threshold) { \
615 return HTK_HARD_MATCH; \
618 async->error = false; \
621 #define DNS(Flag, Field) \
622 if (config->match_ ## Flag) { \
623 const int len = m_strlen(query->Field); \
624 strlist_copy(normal, query->Field, len, false); \
625 for (uint32_t i = 0 ; len > 0 && i < config->host_offsets.len ; ++i) { \
626 const char *rbl = array_ptr(config->hosts, \
627 array_elt(config->host_offsets, i)); \
628 debug("running check of field %s (%s) against %s", STR(Field), \
630 if (rhbl_check(rbl, normal, array_ptr(async->results, result_pos), \
631 strlist_filter_async, context)) { \
632 async->error = false; \
639 if (config->is_email) {
640 LOOKUP(sender, sender);
641 LOOKUP(recipient, recipient);
643 DNS(recipient, recipient);
644 } else if (config->is_hostname) {
645 LOOKUP(helo, helo_name);
646 LOOKUP(client, client_name);
647 LOOKUP(reverse, reverse_client_name);
648 LOOKUP(recipient, recipient_domain);
649 LOOKUP(sender, sender_domain);
650 DNS(helo, helo_name);
651 DNS(client, client_name);
652 DNS(reverse, reverse_client_name);
653 DNS(recipient, recipient_domain);
654 DNS(sender, sender_domain);
658 if (async->awaited > 0) {
662 err("filter %s: all the rbls returned an error", filter->name);
665 if (async->sum >= (uint32_t)config->hard_threshold) {
666 return HTK_HARD_MATCH;
667 } else if (async->sum >= (uint32_t)config->soft_threshold) {
668 return HTK_SOFT_MATCH;
674 static void *strlist_context_constructor(void)
676 return p_new(strlist_async_data_t, 1);
679 static void strlist_context_destructor(void *data)
681 strlist_async_data_t *ctx = data;
682 array_wipe(ctx->results);
686 static int strlist_init(void)
688 filter_type = filter_register("strlist", strlist_filter_constructor,
689 strlist_filter_destructor, strlist_filter,
690 strlist_context_constructor,
691 strlist_context_destructor);
694 (void)filter_hook_register(filter_type, "abort");
695 (void)filter_hook_register(filter_type, "error");
696 (void)filter_hook_register(filter_type, "fail");
697 (void)filter_hook_register(filter_type, "hard_match");
698 (void)filter_hook_register(filter_type, "soft_match");
702 (void)filter_param_register(filter_type, "file");
703 (void)filter_param_register(filter_type, "rbldns");
704 (void)filter_param_register(filter_type, "dns");
705 (void)filter_param_register(filter_type, "hard_threshold");
706 (void)filter_param_register(filter_type, "soft_threshold");
707 (void)filter_param_register(filter_type, "fields");
710 module_init(strlist_init);