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;
68 static strlist_config_t *strlist_config_new(void)
70 return p_new(strlist_config_t, 1);
73 static void strlist_config_delete(strlist_config_t **config)
76 array_deep_wipe((*config)->tries, trie_delete);
77 array_wipe((*config)->weights);
78 array_wipe((*config)->reverses);
79 array_wipe((*config)->partiales);
80 array_wipe((*config)->hosts);
81 array_wipe((*config)->host_offsets);
82 array_wipe((*config)->host_weights);
87 static inline void strlist_copy(char *dest, const char *str, ssize_t str_len,
92 for (const char *src = str + str_len - 1 ; src >= str ; --src) {
93 *dest = ascii_tolower(*src);
97 for (int i = 0 ; i < str_len ; ++i) {
98 *dest = ascii_tolower(str[i]);
107 static trie_t *strlist_create(const char *file, bool reverse, bool lock)
114 if (!file_map_open(&map, file, false)) {
119 while (end > p && end[-1] != '\n') {
122 if (end != map.end) {
123 warn("file %s miss a final \\n, ignoring last line",
128 while (p < end && p != NULL) {
129 const char *eol = (char *)memchr(p, '\n', end - p);
133 if (eol - p >= BUFSIZ) {
134 err("unreasonnable long line");
135 file_map_close(&map);
140 const char *eos = eol;
141 while (p < eos && isspace(*p)) {
144 while (p < eos && isspace(eos[-1])) {
148 strlist_copy(line, p, eos - p, reverse);
149 trie_insert(db, line);
154 file_map_close(&map);
155 trie_compile(db, lock);
159 static bool strlist_create_from_rhbl(const char *file, bool lock,
160 trie_t **phosts, trie_t **pdomains)
162 trie_t *hosts, *domains;
163 uint32_t host_count, domain_count;
168 if (!file_map_open(&map, file, false)) {
173 while (end > p && end[-1] != '\n') {
176 if (end != map.end) {
177 warn("file %s miss a final \\n, ignoring last line",
183 domains = trie_new();
185 while (p < end && p != NULL) {
186 const char *eol = (char *)memchr(p, '\n', end - p);
190 if (eol - p >= BUFSIZ) {
191 err("unreasonnable long line");
192 file_map_close(&map);
194 trie_delete(&domains);
198 const char *eos = eol;
199 while (p < eos && isspace(*p)) {
202 while (p < eos && isspace(eos[-1])) {
207 strlist_copy(line, p, eos - p, true);
208 trie_insert(hosts, line);
210 } else if (*p == '*') {
212 strlist_copy(line, p, eos - p, true);
213 trie_insert(domains, line);
220 file_map_close(&map);
221 if (host_count > 0) {
222 trie_compile(hosts, lock);
228 if (domain_count > 0) {
229 trie_compile(domains, lock);
232 trie_delete(&domains);
235 return hosts != NULL || domains != NULL;
240 static bool strlist_filter_constructor(filter_t *filter)
242 strlist_config_t *config = strlist_config_new();
244 #define PARSE_CHECK(Expr, Str, ...) \
246 err(Str, ##__VA_ARGS__); \
247 strlist_config_delete(&config); \
251 config->hard_threshold = 1;
252 config->soft_threshold = 1;
253 foreach (filter_param_t *param, filter->params) {
254 switch (param->type) {
255 /* file parameter is:
256 * [no]lock:(partial-)(prefix|suffix):weight:filename
258 * - lock: memlock the database in memory.
259 * - nolock: don't memlock the database in memory.
260 * - prefix: perform "prefix" compression on storage.
261 * - suffix perform "suffix" compression on storage.
262 * - \d+: a number describing the weight to give to the match
263 * the given list [mandatory]
264 * the file pointed by filename MUST be a valid string list (one string per
265 * line, empty lines and lines beginning with a '#' are ignored).
270 bool reverse = false;
271 bool partial = false;
273 const char *current = param->value;
274 const char *p = m_strchrnul(param->value, ':');
276 for (int i = 0 ; i < 4 ; ++i) {
277 PARSE_CHECK(i == 3 || *p,
278 "file parameter must contains a locking state "
279 "and a weight option");
282 if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
284 } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
287 PARSE_CHECK(false, "illegal locking state %.*s",
288 (int)(p - current), current);
293 if (p - current > (ssize_t)strlen("partial-")
294 && strncmp(current, "partial-", strlen("partial-")) == 0) {
296 current += strlen("partial-");
298 if ((p - current) == 6 && strncmp(current, "suffix", 6) == 0) {
300 } else if ((p - current) == 6 && strncmp(current, "prefix", 6) == 0) {
303 PARSE_CHECK(false, "illegal character order value %.*s",
304 (int)(p - current), current);
309 weight = strtol(current, &next, 10);
310 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
311 "illegal weight value %.*s",
312 (int)(p - current), current);
316 trie = strlist_create(current, reverse, lock);
317 PARSE_CHECK(trie != NULL,
318 "cannot load string list from %s", current);
319 array_add(config->tries, trie);
320 array_add(config->weights, weight);
321 array_add(config->reverses, reverse);
322 array_add(config->partiales, partial);
327 p = m_strchrnul(current, ':');
332 /* rbldns parameter is:
333 * [no]lock::weight:filename
335 * - lock: memlock the database in memory.
336 * - nolock: don't memlock the database in memory.
337 * - \d+: a number describing the weight to give to the match
338 * the given list [mandatory]
339 * directly import a file issued from a rhbl in rbldns format.
344 trie_t *trie_hosts = NULL;
345 trie_t *trie_domains = NULL;
346 const char *current = param->value;
347 const char *p = m_strchrnul(param->value, ':');
349 for (int i = 0 ; i < 3 ; ++i) {
350 PARSE_CHECK(i == 2 || *p,
351 "file parameter must contains a locking state "
352 "and a weight option");
355 if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
357 } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
360 PARSE_CHECK(false, "illegal locking state %.*s",
361 (int)(p - current), current);
366 weight = strtol(current, &next, 10);
367 PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
368 "illegal weight value %.*s",
369 (int)(p - current), current);
373 PARSE_CHECK(strlist_create_from_rhbl(current, lock,
374 &trie_hosts, &trie_domains),
375 "cannot load string list from rhbl %s", current);
376 if (trie_hosts != NULL) {
377 array_add(config->tries, trie_hosts);
378 array_add(config->weights, weight);
379 array_add(config->reverses, true);
380 array_add(config->partiales, false);
382 if (trie_domains != NULL) {
383 array_add(config->tries, trie_domains);
384 array_add(config->weights, weight);
385 array_add(config->reverses, true);
386 array_add(config->partiales, true);
388 config->is_hostname = true;
393 p = m_strchrnul(current, ':');
400 * define a RBL to use through DNS resolution.
404 const char *current = param->value;
405 const char *p = m_strchrnul(param->value, ':');
407 for (int i = 0 ; i < 2 ; ++i) {
408 PARSE_CHECK(i == 1 || *p,
409 "host parameter must contains a weight option");
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 array_add(config->host_offsets, array_len(config->hosts));
420 array_append(config->hosts, current, strlen(current) + 1);
421 array_add(config->host_weights, weight);
426 p = m_strchrnul(current, ':');
431 /* hard_threshold parameter is an integer.
432 * If the matching score is greater or equal than this threshold,
433 * the hook "hard_match" is called.
434 * hard_threshold = 1 means, that all matches are hard matches.
437 FILTER_PARAM_PARSE_INT(HARD_THRESHOLD, config->hard_threshold);
439 /* soft_threshold parameter is an integer.
440 * if the matching score is greater or equal than this threshold
441 * and smaller or equal than the hard_threshold, the hook "soft_match"
445 FILTER_PARAM_PARSE_INT(SOFT_THRESHOLD, config->soft_threshold);
447 /* fields to match againes:
448 * fields = field_name(,field_name)*
450 * - hostname: helo_name,client_name,reverse_client_name
451 * - email: sender,recipient
454 const char *current = param->value;
455 const char *p = m_strchrnul(param->value, ',');
457 postlicyd_token tok = policy_tokenize(current, p - current);
459 #define CASE(Up, Low, Type) \
461 config->match_ ## Low = true; \
462 config->is_ ## Type = true; \
464 CASE(HELO_NAME, helo, hostname);
465 CASE(CLIENT_NAME, client, hostname);
466 CASE(REVERSE_CLIENT_NAME, reverse, hostname);
467 CASE(SENDER_DOMAIN, sender, hostname);
468 CASE(RECIPIENT_DOMAIN, recipient, hostname);
469 CASE(SENDER, sender, email);
470 CASE(RECIPIENT, recipient, email);
473 PARSE_CHECK(false, "unknown field %.*s", (int)(p - current), current);
480 p = m_strchrnul(current, ',');
488 PARSE_CHECK(config->is_email != config->is_hostname,
489 "matched field MUST be emails XOR hostnames");
490 PARSE_CHECK(config->tries.len || config->host_offsets.len,
491 "no file parameter in the filter %s", filter->name);
492 filter->data = config;
496 static void strlist_filter_destructor(filter_t *filter)
498 strlist_config_t *config = filter->data;
499 strlist_config_delete(&config);
500 filter->data = config;
503 static filter_result_t strlist_filter(const filter_t *filter, const query_t *query)
505 char reverse[BUFSIZ];
507 const strlist_config_t *config = filter->data;
512 if (config->is_email &&
513 ((config->match_sender && query->state < SMTP_MAIL)
514 || (config->match_recipient && query->state != SMTP_RCPT))) {
515 warn("trying to match an email against a field that is not "
516 "available in current protocol state");
518 } else if (config->is_hostname && config->match_helo && query->state < SMTP_HELO) {
519 warn("trying to match hostname against helo before helo is received");
522 #define LOOKUP(Flag, Field) \
523 if (config->match_ ## Flag) { \
524 const int len = m_strlen(query->Field); \
525 strlist_copy(normal, query->Field, len, false); \
526 strlist_copy(reverse, query->Field, len, true); \
527 for (uint32_t i = 0 ; i < config->tries.len ; ++i) { \
528 const int weight = array_elt(config->weights, i); \
529 const trie_t *trie = array_elt(config->tries, i); \
530 const bool rev = array_elt(config->reverses, i); \
531 const bool part = array_elt(config->partiales, i); \
532 if ((!part && trie_lookup(trie, rev ? reverse : normal)) \
533 || (part && trie_prefix(trie, rev ? reverse : normal))) { \
535 if (sum >= config->hard_threshold) { \
536 return HTK_HARD_MATCH; \
542 #define DNS(Flag, Field) \
543 if (config->match_ ## Flag) { \
544 const int len = m_strlen(query->Field); \
545 strlist_copy(normal, query->Field, len, false); \
546 for (uint32_t i = 0 ; len > 0 && i < config->host_offsets.len ; ++i) { \
547 const char *rbl = array_ptr(config->hosts, \
548 array_elt(config->host_offsets, i));\
549 const int weight = array_elt(config->host_weights, i); \
550 switch (rhbl_check(normal, rbl)) { \
554 if (sum >= config->hard_threshold) { \
555 return HTK_HARD_MATCH; \
562 warn("rbl %s unavailable", rbl); \
568 if (config->is_email) {
569 LOOKUP(sender, sender);
570 LOOKUP(recipient, recipient);
572 DNS(recipient, recipient);
573 } else if (config->is_hostname) {
574 LOOKUP(helo, helo_name);
575 LOOKUP(client, client_name);
576 LOOKUP(reverse, reverse_client_name);
577 LOOKUP(recipient, recipient_domain);
578 LOOKUP(sender, sender_domain);
579 DNS(helo, helo_name);
580 DNS(client, client_name);
581 DNS(reverse, reverse_client_name);
582 DNS(recipient, recipient_domain);
583 DNS(sender, sender_domain);
588 err("filter %s: all the rbls returned an error", filter->name);
591 if (sum >= config->hard_threshold) {
592 return HTK_HARD_MATCH;
593 } else if (sum >= config->soft_threshold) {
594 return HTK_SOFT_MATCH;
600 static int strlist_init(void)
602 filter_type_t type = filter_register("strlist", strlist_filter_constructor,
603 strlist_filter_destructor, strlist_filter);
606 (void)filter_hook_register(type, "abort");
607 (void)filter_hook_register(type, "error");
608 (void)filter_hook_register(type, "fail");
609 (void)filter_hook_register(type, "hard_match");
610 (void)filter_hook_register(type, "soft_match");
614 (void)filter_param_register(type, "file");
615 (void)filter_param_register(type, "rbldns");
616 (void)filter_param_register(type, "dns");
617 (void)filter_param_register(type, "hard_threshold");
618 (void)filter_param_register(type, "soft_threshold");
619 (void)filter_param_register(type, "fields");
622 module_init(strlist_init);