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"
42 #include "resources.h"
44 typedef struct strlist_local_t {
51 ARRAY(strlist_local_t)
53 typedef struct strlist_resource_t {
60 typedef struct strlist_config_t {
61 A(strlist_local_t) locals;
71 unsigned is_hostname :1;
73 unsigned match_sender :1;
74 unsigned match_recipient :1;
76 unsigned match_helo :1;
77 unsigned match_client :1;
78 unsigned match_reverse :1;
81 typedef struct strlist_async_data_t {
82 A(rbl_result_t) results;
86 } strlist_async_data_t;
88 static filter_type_t filter_type = FTK_UNKNOWN;
91 static void strlist_local_wipe(strlist_local_t *entry)
93 if (entry->filename != NULL) {
94 resource_release("strlist", entry->filename);
95 p_delete(&entry->filename);
99 static void strlist_resource_wipe(strlist_resource_t *res)
101 trie_delete(&res->trie1);
102 trie_delete(&res->trie2);
106 static strlist_config_t *strlist_config_new(void)
108 return p_new(strlist_config_t, 1);
111 static void strlist_config_delete(strlist_config_t **config)
114 array_deep_wipe((*config)->locals, strlist_local_wipe);
115 array_wipe((*config)->hosts);
116 array_wipe((*config)->host_offsets);
117 array_wipe((*config)->host_weights);
122 static inline void strlist_copy(char *dest, const char *str, ssize_t str_len,
127 for (const char *src = str + str_len - 1 ; src >= str ; --src) {
128 *dest = ascii_tolower(*src);
132 for (int i = 0 ; i < str_len ; ++i) {
133 *dest = ascii_tolower(str[i]);
142 static bool strlist_create(strlist_local_t *local,
143 const char *file, int weight,
144 bool reverse, bool partial, bool lock)
151 if (!file_map_open(&map, file, false)) {
156 while (end > p && end[-1] != '\n') {
159 if (end != map.end) {
160 warn("file %s miss a final \\n, ignoring last line",
164 strlist_resource_t *res = resource_get("strlist", file);
166 res = p_new(strlist_resource_t, 1);
167 resource_set("strlist", file, res, (resource_destructor_t)strlist_resource_wipe);
168 } else if (res->trie2 != NULL) {
169 err("A file (%s) cannot be used as a rbldns zone file and a strlist file at the same time",
171 resource_release("strlist", file);
172 file_map_close(&map);
177 local->filename = m_strdup(file);
178 local->db = &res->trie1;
179 local->weight = weight;
180 local->reverse = reverse;
181 local->partial = partial;
182 if (res->size == map.st.st_size && res->mtime == map.st.st_mtime) {
183 info("strlist %s up to date", file);
184 file_map_close(&map);
187 trie_delete(&res->trie1);
188 res->trie1 = trie_new();
189 res->size = map.st.st_size;
190 res->mtime = map.st.st_mtime;
192 while (p < end && p != NULL) {
193 const char *eol = (char *)memchr(p, '\n', end - p);
197 if (eol - p >= BUFSIZ) {
198 err("unreasonnable long line");
199 file_map_close(&map);
200 trie_delete(&res->trie1);
201 strlist_local_wipe(local);
205 const char *eos = eol;
206 while (p < eos && isspace(*p)) {
209 while (p < eos && isspace(eos[-1])) {
213 strlist_copy(line, p, eos - p, reverse);
214 trie_insert(res->trie1, line);
220 file_map_close(&map);
221 trie_compile(res->trie1, lock);
222 info("%s loaded, %u entries", file, count);
226 static bool strlist_create_from_rhbl(strlist_local_t *hosts, strlist_local_t *domains,
227 const char *file, int weight, bool lock)
229 uint32_t host_count, domain_count;
234 if (!file_map_open(&map, file, false)) {
239 while (end > p && end[-1] != '\n') {
242 if (end != map.end) {
243 warn("file %s miss a final \\n, ignoring last line",
248 strlist_resource_t *res = resource_get("strlist", file);
250 res = p_new(strlist_resource_t, 1);
251 resource_set("strlist", file, res, (resource_destructor_t)strlist_resource_wipe);
252 } else if (res->trie2 == NULL) {
253 err("A file (%s) cannot be used as a rbldns zone file and a strlist file at the same time",
255 resource_release("strlist", file);
256 file_map_close(&map);
261 hosts->filename = m_strdup(file);
262 hosts->db = &res->trie1;
263 hosts->weight = weight;
264 hosts->reverse = true;
268 /* don't set filename */
269 domains->db = &res->trie2;
270 domains->weight = weight;
271 domains->reverse = true;
272 domains->partial = true;
275 if (map.st.st_size == res->size && map.st.st_mtime == res->mtime) {
276 info("rbldns %s up to date", file);
277 file_map_close(&map);
281 trie_delete(&res->trie1);
282 trie_delete(&res->trie2);
283 res->trie1 = trie_new();
284 res->trie2 = trie_new();
285 res->size = map.st.st_size;
286 res->mtime = map.st.st_mtime;
288 while (p < end && p != NULL) {
289 const char *eol = (char *)memchr(p, '\n', end - p);
293 if (eol - p >= BUFSIZ) {
294 err("unreasonnable long line");
295 file_map_close(&map);
296 trie_delete(&res->trie1);
297 trie_delete(&res->trie2);
298 strlist_local_wipe(hosts);
302 const char *eos = eol;
303 while (p < eos && isspace(*p)) {
306 while (p < eos && isspace(eos[-1])) {
311 strlist_copy(line, p, eos - p, true);
312 trie_insert(res->trie1, line);
314 } else if (*p == '*') {
316 strlist_copy(line, p, eos - p, true);
317 trie_insert(res->trie2, line);
324 file_map_close(&map);
325 if (host_count > 0) {
326 trie_compile(res->trie1, lock);
328 trie_delete(&res->trie1);
330 if (domain_count > 0) {
331 trie_compile(res->trie2, lock);
333 trie_delete(&res->trie2);
335 info("rhbl %s loaded, %u hosts, %u domains", file, host_count, domain_count);
336 if (res->trie1 == NULL && res->trie2 == NULL) {
337 strlist_local_wipe(hosts);
344 static bool strlist_filter_constructor(filter_t *filter)
346 strlist_config_t *config = strlist_config_new();
348 #define PARSE_CHECK(Expr, Str, ...) \
350 err(Str, ##__VA_ARGS__); \
351 strlist_config_delete(&config); \
355 config->hard_threshold = 1;
356 config->soft_threshold = 1;
357 foreach (filter_param_t *param, filter->params) {
358 switch (param->type) {
359 /* file parameter is:
360 * [no]lock:(partial-)(prefix|suffix):weight:filename
362 * - lock: memlock the database in memory.
363 * - nolock: don't memlock the database in memory.
364 * - prefix: perform "prefix" compression on storage.
365 * - suffix perform "suffix" compression on storage.
366 * - \d+: a number describing the weight to give to the match
367 * the given list [mandatory]
368 * the file pointed by filename MUST be a valid string list (one string per
369 * line, empty lines and lines beginning with a '#' are ignored).
374 bool reverse = false;
375 bool partial = false;
376 const char *current = param->value;
377 const char *p = m_strchrnul(param->value, ':');
379 for (int i = 0 ; i < 4 ; ++i) {
380 PARSE_CHECK(i == 3 || *p,
381 "file parameter must contains a locking state "
382 "and a weight option");
385 if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
387 } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
390 PARSE_CHECK(false, "illegal locking state %.*s",
391 (int)(p - current), current);
396 if (p - current > (ssize_t)strlen("partial-")
397 && strncmp(current, "partial-", strlen("partial-")) == 0) {
399 current += strlen("partial-");
401 if ((p - current) == 6 && strncmp(current, "suffix", 6) == 0) {
403 } else if ((p - current) == 6 && strncmp(current, "prefix", 6) == 0) {
406 PARSE_CHECK(false, "illegal character order value %.*s",
407 (int)(p - current), current);
412 weight = strtol(current, &next, 10);
413 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
414 "illegal weight value %.*s",
415 (int)(p - current), current);
419 strlist_local_t entry;
420 PARSE_CHECK(strlist_create(&entry, current, weight,
421 reverse, partial, lock),
422 "cannot load string list from %s", current);
423 array_add(config->locals, entry);
428 p = m_strchrnul(current, ':');
433 /* rbldns parameter is:
434 * [no]lock::weight:filename
436 * - lock: memlock the database in memory.
437 * - nolock: don't memlock the database in memory.
438 * - \d+: a number describing the weight to give to the match
439 * the given list [mandatory]
440 * directly import a file issued from a rhbl in rbldns format.
445 const char *current = param->value;
446 const char *p = m_strchrnul(param->value, ':');
448 for (int i = 0 ; i < 3 ; ++i) {
449 PARSE_CHECK(i == 2 || *p,
450 "file parameter must contains a locking state "
451 "and a weight option");
454 if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
456 } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
459 PARSE_CHECK(false, "illegal locking state %.*s",
460 (int)(p - current), current);
465 weight = strtol(current, &next, 10);
466 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
467 "illegal weight value %.*s",
468 (int)(p - current), current);
472 strlist_local_t trie_hosts, trie_domains;
473 PARSE_CHECK(strlist_create_from_rhbl(&trie_hosts, &trie_domains,
474 current, weight, lock),
475 "cannot load string list from rhbl %s", current);
476 if (trie_hosts.db != NULL) {
477 array_add(config->locals, trie_hosts);
479 if (trie_domains.db != NULL) {
480 array_add(config->locals, trie_domains);
482 config->is_hostname = true;
487 p = m_strchrnul(current, ':');
494 * define a RBL to use through DNS resolution.
498 const char *current = param->value;
499 const char *p = m_strchrnul(param->value, ':');
501 for (int i = 0 ; i < 2 ; ++i) {
502 PARSE_CHECK(i == 1 || *p,
503 "host parameter must contains a weight option");
506 weight = strtol(current, &next, 10);
507 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
508 "illegal weight value %.*s",
509 (int)(p - current), current);
513 array_add(config->host_offsets, array_len(config->hosts));
514 array_append(config->hosts, current, strlen(current) + 1);
515 array_add(config->host_weights, weight);
520 p = m_strchrnul(current, ':');
525 /* hard_threshold parameter is an integer.
526 * If the matching score is greater or equal than this threshold,
527 * the hook "hard_match" is called.
528 * hard_threshold = 1 means, that all matches are hard matches.
531 FILTER_PARAM_PARSE_INT(HARD_THRESHOLD, config->hard_threshold);
533 /* soft_threshold parameter is an integer.
534 * if the matching score is greater or equal than this threshold
535 * and smaller or equal than the hard_threshold, the hook "soft_match"
539 FILTER_PARAM_PARSE_INT(SOFT_THRESHOLD, config->soft_threshold);
541 /* fields to match againes:
542 * fields = field_name(,field_name)*
544 * - hostname: helo_name,client_name,reverse_client_name
545 * - email: sender,recipient
548 const char *current = param->value;
549 const char *p = m_strchrnul(param->value, ',');
551 postlicyd_token tok = policy_tokenize(current, p - current);
553 #define CASE(Up, Low, Type) \
555 config->match_ ## Low = true; \
556 config->is_ ## Type = true; \
558 CASE(HELO_NAME, helo, hostname);
559 CASE(CLIENT_NAME, client, hostname);
560 CASE(REVERSE_CLIENT_NAME, reverse, hostname);
561 CASE(SENDER_DOMAIN, sender, hostname);
562 CASE(RECIPIENT_DOMAIN, recipient, hostname);
563 CASE(SENDER, sender, email);
564 CASE(RECIPIENT, recipient, email);
567 PARSE_CHECK(false, "unknown field %.*s", (int)(p - current), current);
574 p = m_strchrnul(current, ',');
582 PARSE_CHECK(config->is_email != config->is_hostname,
583 "matched field MUST be emails XOR hostnames");
584 PARSE_CHECK(config->locals.len || config->host_offsets.len,
585 "no file parameter in the filter %s", filter->name);
586 filter->data = config;
590 static void strlist_filter_destructor(filter_t *filter)
592 strlist_config_t *config = filter->data;
593 strlist_config_delete(&config);
594 filter->data = config;
597 static void strlist_filter_async(rbl_result_t *result, void *arg)
599 filter_context_t *context = arg;
600 const filter_t *filter = context->current_filter;
601 const strlist_config_t *data = filter->data;
602 strlist_async_data_t *async = context->contexts[filter_type];
604 if (*result != RBL_ERROR) {
605 async->error = false;
609 debug("got asynchronous request result for filter %s, rbl %d, still awaiting %d answers",
610 filter->name, (int)(result - array_ptr(async->results, 0)), async->awaited);
612 if (async->awaited == 0) {
613 filter_result_t res = HTK_FAIL;
618 #define DO_SUM(Field) \
619 if (data->match_ ## Field) { \
620 for (uint32_t i = 0 ; i < array_len(data->host_offsets) ; ++i) { \
621 int weight = array_elt(data->host_weights, i); \
623 switch (array_elt(async->results, j)) { \
625 crit("no more awaited answer but result is ASYNC"); \
628 async->sum += weight; \
642 debug("score is %d", async->sum);
643 if (async->sum >= (uint32_t)data->hard_threshold) {
644 res = HTK_HARD_MATCH;
645 } else if (async->sum >= (uint32_t)data->soft_threshold) {
646 res = HTK_SOFT_MATCH;
649 debug("answering to filter %s", filter->name);
650 filter_post_async_result(context, res);
655 static filter_result_t strlist_filter(const filter_t *filter, const query_t *query,
656 filter_context_t *context)
658 char reverse[BUFSIZ];
660 const strlist_config_t *config = filter->data;
661 strlist_async_data_t *async = context->contexts[filter_type];
665 array_ensure_exact_capacity(async->results, (config->match_client
666 + config->match_sender + config->match_helo
667 + config->match_recipient + config->match_reverse)
668 * array_len(config->host_offsets));
672 if (config->is_email &&
673 ((config->match_sender && query->state < SMTP_MAIL)
674 || (config->match_recipient && query->state != SMTP_RCPT))) {
675 warn("trying to match an email against a field that is not "
676 "available in current protocol state");
678 } else if (config->is_hostname && config->match_helo && query->state < SMTP_HELO) {
679 warn("trying to match hostname against helo before helo is received");
682 #define LOOKUP(Flag, Field) \
683 if (config->match_ ## Flag) { \
684 const int len = m_strlen(query->Field); \
685 strlist_copy(normal, query->Field, len, false); \
686 strlist_copy(reverse, query->Field, len, true); \
687 foreach (strlist_local_t *entry, config->locals) { \
688 if ((!entry->partial && trie_lookup(*(entry->db), \
689 entry->reverse ? reverse : normal)) \
690 || (entry->partial && trie_prefix(*(entry->db), \
691 entry->reverse ? reverse : normal))) { \
692 async->sum += entry->weight; \
693 if (async->sum >= (uint32_t)config->hard_threshold) { \
694 return HTK_HARD_MATCH; \
697 async->error = false; \
700 #define DNS(Flag, Field) \
701 if (config->match_ ## Flag) { \
702 const int len = m_strlen(query->Field); \
703 strlist_copy(normal, query->Field, len, false); \
704 for (uint32_t i = 0 ; len > 0 && i < config->host_offsets.len ; ++i) { \
705 const char *rbl = array_ptr(config->hosts, \
706 array_elt(config->host_offsets, i)); \
707 debug("running check of field %s (%s) against %s", STR(Field), \
709 if (rhbl_check(rbl, normal, array_ptr(async->results, result_pos), \
710 strlist_filter_async, context)) { \
711 async->error = false; \
718 if (config->is_email) {
719 LOOKUP(sender, sender);
720 LOOKUP(recipient, recipient);
722 DNS(recipient, recipient);
723 } else if (config->is_hostname) {
724 LOOKUP(helo, helo_name);
725 LOOKUP(client, client_name);
726 LOOKUP(reverse, reverse_client_name);
727 LOOKUP(recipient, recipient_domain);
728 LOOKUP(sender, sender_domain);
729 DNS(helo, helo_name);
730 DNS(client, client_name);
731 DNS(reverse, reverse_client_name);
732 DNS(recipient, recipient_domain);
733 DNS(sender, sender_domain);
737 if (async->awaited > 0) {
741 err("filter %s: all the rbls returned an error", filter->name);
744 if (async->sum >= (uint32_t)config->hard_threshold) {
745 return HTK_HARD_MATCH;
746 } else if (async->sum >= (uint32_t)config->soft_threshold) {
747 return HTK_SOFT_MATCH;
753 static void *strlist_context_constructor(void)
755 return p_new(strlist_async_data_t, 1);
758 static void strlist_context_destructor(void *data)
760 strlist_async_data_t *ctx = data;
761 array_wipe(ctx->results);
765 static int strlist_init(void)
767 filter_type = filter_register("strlist", strlist_filter_constructor,
768 strlist_filter_destructor, strlist_filter,
769 strlist_context_constructor,
770 strlist_context_destructor);
773 (void)filter_hook_register(filter_type, "abort");
774 (void)filter_hook_register(filter_type, "error");
775 (void)filter_hook_register(filter_type, "fail");
776 (void)filter_hook_register(filter_type, "hard_match");
777 (void)filter_hook_register(filter_type, "soft_match");
781 (void)filter_param_register(filter_type, "file");
782 (void)filter_param_register(filter_type, "rbldns");
783 (void)filter_param_register(filter_type, "dns");
784 (void)filter_param_register(filter_type, "hard_threshold");
785 (void)filter_param_register(filter_type, "soft_threshold");
786 (void)filter_param_register(filter_type, "fields");
789 module_init(strlist_init);