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 CONTRIBUTORS ``AS IS'' AND ANY EXPRESS */
20 /* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED */
21 /* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */
22 /* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY */
23 /* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL */
24 /* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS */
25 /* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) */
26 /* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, */
27 /* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN */
28 /* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */
29 /* POSSIBILITY OF SUCH DAMAGE. */
31 /* Copyright (c) 2006-2008 the Authors */
32 /* see AUTHORS and source files for details */
33 /******************************************************************************/
36 * Copyright © 2008 Florent Bruneau
44 #include "policy_tokens.h"
45 #include "resources.h"
47 typedef struct strlist_local_t {
54 ARRAY(strlist_local_t)
56 typedef struct strlist_resource_t {
63 typedef struct strlist_config_t {
64 A(strlist_local_t) locals;
74 unsigned is_hostname :1;
76 unsigned match_sender :1;
77 unsigned match_recipient :1;
79 unsigned match_helo :1;
80 unsigned match_client :1;
81 unsigned match_reverse :1;
84 typedef struct strlist_async_data_t {
85 A(rbl_result_t) results;
89 } strlist_async_data_t;
91 static filter_type_t filter_type = FTK_UNKNOWN;
94 static void strlist_local_wipe(strlist_local_t *entry)
96 if (entry->filename != NULL) {
97 resource_release("strlist", entry->filename);
98 p_delete(&entry->filename);
102 static void strlist_resource_wipe(strlist_resource_t *res)
104 trie_delete(&res->trie1);
105 trie_delete(&res->trie2);
109 static strlist_config_t *strlist_config_new(void)
111 return p_new(strlist_config_t, 1);
114 static void strlist_config_delete(strlist_config_t **config)
117 array_deep_wipe((*config)->locals, strlist_local_wipe);
118 array_wipe((*config)->hosts);
119 array_wipe((*config)->host_offsets);
120 array_wipe((*config)->host_weights);
125 static inline void strlist_copy(char *dest, const char *str, ssize_t str_len,
130 for (const char *src = str + str_len - 1 ; src >= str ; --src) {
131 *dest = ascii_tolower(*src);
135 for (int i = 0 ; i < str_len ; ++i) {
136 *dest = ascii_tolower(str[i]);
145 static bool strlist_create(strlist_local_t *local,
146 const char *file, int weight,
147 bool reverse, bool partial, bool lock)
153 time_t now = time(0);
155 if (!file_map_open(&map, file, false)) {
160 while (end > p && end[-1] != '\n') {
163 if (end != map.end) {
164 warn("%s: final \\n missing, ignoring last line", file);
167 strlist_resource_t *res = resource_get("strlist", file);
169 res = p_new(strlist_resource_t, 1);
170 resource_set("strlist", file, res, (resource_destructor_t)strlist_resource_wipe);
171 } else if (res->trie2 != NULL) {
172 err("%s not loaded: the file is already used as a rbldns zone file", file);
173 resource_release("strlist", file);
174 file_map_close(&map);
179 local->filename = m_strdup(file);
180 local->db = &res->trie1;
181 local->weight = weight;
182 local->reverse = reverse;
183 local->partial = partial;
184 if (res->size == map.st.st_size && res->mtime == map.st.st_mtime) {
185 info("%s loaded: already up-to-date", file);
186 file_map_close(&map);
189 trie_delete(&res->trie1);
190 res->trie1 = trie_new();
191 res->size = map.st.st_size;
192 res->mtime = map.st.st_mtime;
194 while (p < end && p != NULL) {
195 const char *eol = (char *)memchr(p, '\n', end - p);
199 if (eol - p >= BUFSIZ) {
200 err("%s not loaded: unreasonnable long line", file);
201 file_map_close(&map);
202 trie_delete(&res->trie1);
203 strlist_local_wipe(local);
207 const char *eos = eol;
208 while (p < eos && isspace(*p)) {
211 while (p < eos && isspace(eos[-1])) {
215 strlist_copy(line, p, eos - p, reverse);
216 trie_insert(res->trie1, line);
222 file_map_close(&map);
223 trie_compile(res->trie1, lock);
224 info("%s loaded: done in %us, %u entries", file, (uint32_t)(time(0) - now), count);
228 static bool strlist_create_from_rhbl(strlist_local_t *hosts, strlist_local_t *domains,
229 const char *file, int weight, bool lock)
231 uint32_t host_count, domain_count;
235 time_t now = time(0);
237 if (!file_map_open(&map, file, false)) {
242 while (end > p && end[-1] != '\n') {
245 if (end != map.end) {
246 warn("%s: final \\n missing, ignoring last line", file);
250 strlist_resource_t *res = resource_get("strlist", file);
252 res = p_new(strlist_resource_t, 1);
253 resource_set("strlist", file, res, (resource_destructor_t)strlist_resource_wipe);
254 } else if (res->trie2 == NULL) {
255 err("%s not loaded: the file is already used as a strlist-file parameter", file);
256 resource_release("strlist", file);
257 file_map_close(&map);
262 hosts->filename = m_strdup(file);
263 hosts->db = &res->trie1;
264 hosts->weight = weight;
265 hosts->reverse = true;
269 /* don't set filename */
270 domains->db = &res->trie2;
271 domains->weight = weight;
272 domains->reverse = true;
273 domains->partial = true;
276 if (map.st.st_size == res->size && map.st.st_mtime == res->mtime) {
277 info("%s loaded: already up-to-date", file);
278 file_map_close(&map);
282 trie_delete(&res->trie1);
283 trie_delete(&res->trie2);
284 res->trie1 = trie_new();
285 res->trie2 = trie_new();
286 res->size = map.st.st_size;
287 res->mtime = map.st.st_mtime;
289 while (p < end && p != NULL) {
290 const char *eol = (char *)memchr(p, '\n', end - p);
294 if (eol - p >= BUFSIZ) {
295 err("%s not loaded: unreasonnable long line", file);
296 file_map_close(&map);
297 trie_delete(&res->trie1);
298 trie_delete(&res->trie2);
299 strlist_local_wipe(hosts);
303 const char *eos = eol;
304 while (p < eos && isspace(*p)) {
307 while (p < eos && isspace(eos[-1])) {
312 strlist_copy(line, p, eos - p, true);
313 trie_insert(res->trie1, line);
315 } else if (*p == '*') {
317 strlist_copy(line, p, eos - p, true);
318 trie_insert(res->trie2, line);
325 file_map_close(&map);
326 if (host_count > 0) {
327 trie_compile(res->trie1, lock);
329 trie_delete(&res->trie1);
331 if (domain_count > 0) {
332 trie_compile(res->trie2, lock);
334 trie_delete(&res->trie2);
336 if (res->trie1 == NULL && res->trie2 == NULL) {
337 err("%s not loaded: no data found", file);
338 strlist_local_wipe(hosts);
341 info("%s loaded: done in %us, %u hosts, %u domains", file,
342 (uint32_t)(time(0) - now), host_count, domain_count);
347 static bool strlist_filter_constructor(filter_t *filter)
349 strlist_config_t *config = strlist_config_new();
351 #define PARSE_CHECK(Expr, Str, ...) \
353 err(Str, ##__VA_ARGS__); \
354 strlist_config_delete(&config); \
358 config->hard_threshold = 1;
359 config->soft_threshold = 1;
360 foreach (filter_param_t *param, filter->params) {
361 switch (param->type) {
362 /* file parameter is:
363 * [no]lock:(partial-)(prefix|suffix):weight:filename
365 * - lock: memlock the database in memory.
366 * - nolock: don't memlock the database in memory.
367 * - prefix: perform "prefix" compression on storage.
368 * - suffix perform "suffix" compression on storage.
369 * - \d+: a number describing the weight to give to the match
370 * the given list [mandatory]
371 * the file pointed by filename MUST be a valid string list (one string per
372 * line, empty lines and lines beginning with a '#' are ignored).
377 bool reverse = false;
378 bool partial = false;
379 const char *current = param->value;
380 const char *p = m_strchrnul(param->value, ':');
382 for (int i = 0 ; i < 4 ; ++i) {
383 PARSE_CHECK(i == 3 || *p,
384 "file parameter must contains a locking state "
385 "and a weight option");
388 if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
390 } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
393 PARSE_CHECK(false, "illegal locking state %.*s",
394 (int)(p - current), current);
399 if (p - current > (ssize_t)strlen("partial-")
400 && strncmp(current, "partial-", strlen("partial-")) == 0) {
402 current += strlen("partial-");
404 if ((p - current) == 6 && strncmp(current, "suffix", 6) == 0) {
406 } else if ((p - current) == 6 && strncmp(current, "prefix", 6) == 0) {
409 PARSE_CHECK(false, "illegal character order value %.*s",
410 (int)(p - current), current);
415 weight = strtol(current, &next, 10);
416 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
417 "illegal weight value %.*s",
418 (int)(p - current), current);
422 strlist_local_t entry;
423 PARSE_CHECK(strlist_create(&entry, current, weight,
424 reverse, partial, lock),
425 "cannot load string list from %s", current);
426 array_add(config->locals, entry);
431 p = m_strchrnul(current, ':');
436 /* rbldns parameter is:
437 * [no]lock::weight:filename
439 * - lock: memlock the database in memory.
440 * - nolock: don't memlock the database in memory.
441 * - \d+: a number describing the weight to give to the match
442 * the given list [mandatory]
443 * directly import a file issued from a rhbl in rbldns format.
448 const char *current = param->value;
449 const char *p = m_strchrnul(param->value, ':');
451 for (int i = 0 ; i < 3 ; ++i) {
452 PARSE_CHECK(i == 2 || *p,
453 "file parameter must contains a locking state "
454 "and a weight option");
457 if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
459 } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
462 PARSE_CHECK(false, "illegal locking state %.*s",
463 (int)(p - current), current);
468 weight = strtol(current, &next, 10);
469 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
470 "illegal weight value %.*s",
471 (int)(p - current), current);
475 strlist_local_t trie_hosts, trie_domains;
476 PARSE_CHECK(strlist_create_from_rhbl(&trie_hosts, &trie_domains,
477 current, weight, lock),
478 "cannot load string list from rhbl %s", current);
479 if (trie_hosts.db != NULL) {
480 array_add(config->locals, trie_hosts);
482 if (trie_domains.db != NULL) {
483 array_add(config->locals, trie_domains);
485 config->is_hostname = true;
490 p = m_strchrnul(current, ':');
497 * define a RBL to use through DNS resolution.
501 const char *current = param->value;
502 const char *p = m_strchrnul(param->value, ':');
504 for (int i = 0 ; i < 2 ; ++i) {
505 PARSE_CHECK(i == 1 || *p,
506 "host parameter must contains a weight option");
509 weight = strtol(current, &next, 10);
510 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
511 "illegal weight value %.*s",
512 (int)(p - current), current);
516 array_add(config->host_offsets, array_len(config->hosts));
517 array_append(config->hosts, current, strlen(current) + 1);
518 array_add(config->host_weights, weight);
523 p = m_strchrnul(current, ':');
528 /* hard_threshold parameter is an integer.
529 * If the matching score is greater or equal than this threshold,
530 * the hook "hard_match" is called.
531 * hard_threshold = 1 means, that all matches are hard matches.
534 FILTER_PARAM_PARSE_INT(HARD_THRESHOLD, config->hard_threshold);
536 /* soft_threshold parameter is an integer.
537 * if the matching score is greater or equal than this threshold
538 * and smaller or equal than the hard_threshold, the hook "soft_match"
542 FILTER_PARAM_PARSE_INT(SOFT_THRESHOLD, config->soft_threshold);
544 /* fields to match againes:
545 * fields = field_name(,field_name)*
547 * - hostname: helo_name,client_name,reverse_client_name
548 * - email: sender,recipient
551 const char *current = param->value;
552 const char *p = m_strchrnul(param->value, ',');
554 postlicyd_token tok = policy_tokenize(current, p - current);
556 #define CASE(Up, Low, Type) \
558 config->match_ ## Low = true; \
559 config->is_ ## Type = true; \
561 CASE(HELO_NAME, helo, hostname);
562 CASE(CLIENT_NAME, client, hostname);
563 CASE(REVERSE_CLIENT_NAME, reverse, hostname);
564 CASE(SENDER_DOMAIN, sender, hostname);
565 CASE(RECIPIENT_DOMAIN, recipient, hostname);
566 CASE(SENDER, sender, email);
567 CASE(RECIPIENT, recipient, email);
570 PARSE_CHECK(false, "unknown field %.*s", (int)(p - current), current);
577 p = m_strchrnul(current, ',');
585 PARSE_CHECK(config->is_email != config->is_hostname,
586 "matched field MUST be emails XOR hostnames");
587 PARSE_CHECK(config->locals.len || config->host_offsets.len,
588 "no file parameter in the filter %s", filter->name);
589 filter->data = config;
593 static void strlist_filter_destructor(filter_t *filter)
595 strlist_config_t *config = filter->data;
596 strlist_config_delete(&config);
597 filter->data = config;
600 static void strlist_filter_async(rbl_result_t *result, void *arg)
602 filter_context_t *context = arg;
603 const filter_t *filter = context->current_filter;
604 const strlist_config_t *data = filter->data;
605 strlist_async_data_t *async = context->contexts[filter_type];
607 if (*result != RBL_ERROR) {
608 async->error = false;
612 debug("got asynchronous request result for filter %s, rbl %d, still awaiting %d answers",
613 filter->name, (int)(result - array_ptr(async->results, 0)), async->awaited);
615 if (async->awaited == 0) {
616 filter_result_t res = HTK_FAIL;
621 #define DO_SUM(Field) \
622 if (data->match_ ## Field) { \
623 for (uint32_t i = 0 ; i < array_len(data->host_offsets) ; ++i) { \
624 int weight = array_elt(data->host_weights, i); \
626 switch (array_elt(async->results, j)) { \
628 crit("no more awaited answer but result is ASYNC"); \
631 async->sum += weight; \
645 debug("score is %d", async->sum);
646 if (async->sum >= (uint32_t)data->hard_threshold) {
647 res = HTK_HARD_MATCH;
648 } else if (async->sum >= (uint32_t)data->soft_threshold) {
649 res = HTK_SOFT_MATCH;
652 debug("answering to filter %s", filter->name);
653 filter_post_async_result(context, res);
658 static filter_result_t strlist_filter(const filter_t *filter, const query_t *query,
659 filter_context_t *context)
661 char reverse[BUFSIZ];
663 const strlist_config_t *config = filter->data;
664 strlist_async_data_t *async = context->contexts[filter_type];
668 array_ensure_exact_capacity(async->results, (config->match_client
669 + config->match_sender + config->match_helo
670 + config->match_recipient + config->match_reverse)
671 * array_len(config->host_offsets));
675 if (config->is_email &&
676 ((config->match_sender && query->state < SMTP_MAIL)
677 || (config->match_recipient && query->state != SMTP_RCPT))) {
678 warn("trying to match an email against a field that is not "
679 "available in current protocol state");
681 } else if (config->is_hostname && config->match_helo && query->state < SMTP_HELO) {
682 warn("trying to match hostname against helo before helo is received");
685 #define LOOKUP(Flag, Field) \
686 if (config->match_ ## Flag) { \
687 const int len = query->Field.len; \
688 strlist_copy(normal, query->Field.str, len, false); \
689 strlist_copy(reverse, query->Field.str, len, true); \
690 foreach (strlist_local_t *entry, config->locals) { \
691 if ((!entry->partial && trie_lookup(*(entry->db), \
692 entry->reverse ? reverse : normal)) \
693 || (entry->partial && trie_prefix(*(entry->db), \
694 entry->reverse ? reverse : normal))) { \
695 async->sum += entry->weight; \
696 if (async->sum >= (uint32_t)config->hard_threshold) { \
697 return HTK_HARD_MATCH; \
700 async->error = false; \
703 #define DNS(Flag, Field) \
704 if (config->match_ ## Flag) { \
705 const int len = query->Field.len; \
706 strlist_copy(normal, query->Field.str, len, false); \
707 for (uint32_t i = 0 ; len > 0 && i < config->host_offsets.len ; ++i) { \
708 const char *rbl = array_ptr(config->hosts, \
709 array_elt(config->host_offsets, i)); \
710 debug("running check of field %s (%s) against %s", STR(Field), \
712 if (rhbl_check(rbl, normal, array_ptr(async->results, result_pos), \
713 strlist_filter_async, context)) { \
714 async->error = false; \
721 if (config->is_email) {
722 LOOKUP(sender, sender);
723 LOOKUP(recipient, recipient);
725 DNS(recipient, recipient);
726 } else if (config->is_hostname) {
727 LOOKUP(helo, helo_name);
728 LOOKUP(client, client_name);
729 LOOKUP(reverse, reverse_client_name);
730 LOOKUP(recipient, recipient_domain);
731 LOOKUP(sender, sender_domain);
732 DNS(helo, helo_name);
733 DNS(client, client_name);
734 DNS(reverse, reverse_client_name);
735 DNS(recipient, recipient_domain);
736 DNS(sender, sender_domain);
740 if (async->awaited > 0) {
744 err("filter %s: all the rbls returned an error", filter->name);
747 if (async->sum >= (uint32_t)config->hard_threshold) {
748 return HTK_HARD_MATCH;
749 } else if (async->sum >= (uint32_t)config->soft_threshold) {
750 return HTK_SOFT_MATCH;
756 static void *strlist_context_constructor(void)
758 return p_new(strlist_async_data_t, 1);
761 static void strlist_context_destructor(void *data)
763 strlist_async_data_t *ctx = data;
764 array_wipe(ctx->results);
768 static int strlist_init(void)
770 filter_type = filter_register("strlist", strlist_filter_constructor,
771 strlist_filter_destructor, strlist_filter,
772 strlist_context_constructor,
773 strlist_context_destructor);
776 (void)filter_hook_register(filter_type, "abort");
777 (void)filter_hook_register(filter_type, "error");
778 (void)filter_hook_register(filter_type, "fail");
779 (void)filter_hook_register(filter_type, "hard_match");
780 (void)filter_hook_register(filter_type, "soft_match");
784 (void)filter_param_register(filter_type, "file");
785 (void)filter_param_register(filter_type, "rbldns");
786 (void)filter_param_register(filter_type, "dns");
787 (void)filter_param_register(filter_type, "hard_threshold");
788 (void)filter_param_register(filter_type, "soft_threshold");
789 (void)filter_param_register(filter_type, "fields");
792 module_init(strlist_init);