Cleanup logging, add stats for strlists.
[apps/pfixtools.git] / postlicyd / strlist.c
1 /******************************************************************************/
2 /*          pfixtools: a collection of postfix related tools                  */
3 /*          ~~~~~~~~~                                                         */
4 /*  ________________________________________________________________________  */
5 /*                                                                            */
6 /*  Redistribution and use in source and binary forms, with or without        */
7 /*  modification, are permitted provided that the following conditions        */
8 /*  are met:                                                                  */
9 /*                                                                            */
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     */
17 /*     permission.                                                            */
18 /*                                                                            */
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 /******************************************************************************/
31
32 /*
33  * Copyright © 2008 Florent Bruneau
34  */
35
36 #include "filter.h"
37 #include "trie.h"
38 #include "file.h"
39 #include "str.h"
40 #include "rbl.h"
41 #include "policy_tokens.h"
42
43 typedef struct strlist_config_t {
44     PA(trie_t) tries;
45     A(int)     weights;
46     A(bool)    reverses;
47     A(bool)    partiales;
48
49     A(char)     hosts;
50     A(int)      host_offsets;
51     A(int)      host_weights;
52
53     int soft_threshold;
54     int hard_threshold;
55
56     unsigned is_email         :1;
57     unsigned is_hostname      :1;
58
59     unsigned match_sender     :1;
60     unsigned match_recipient  :1;
61
62     unsigned match_helo       :1;
63     unsigned match_client     :1;
64     unsigned match_reverse    :1;
65 } strlist_config_t;
66
67 typedef struct strlist_async_data_t {
68     A(rbl_result_t) results;
69     int awaited;
70     uint32_t sum;
71     bool error;
72 } strlist_async_data_t;
73
74 static filter_type_t filter_type = FTK_UNKNOWN;
75
76
77 static strlist_config_t *strlist_config_new(void)
78 {
79     return p_new(strlist_config_t, 1);
80 }
81
82 static void strlist_config_delete(strlist_config_t **config)
83 {
84     if (*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);
92         p_delete(config);
93     }
94 }
95
96 static inline void strlist_copy(char *dest, const char *str, ssize_t str_len,
97                                 bool reverse)
98 {
99     if (str_len > 0) {
100         if (reverse) {
101             for (const char *src = str + str_len - 1 ; src >= str ; --src) {
102                 *dest = ascii_tolower(*src);
103                 ++dest;
104             }
105         } else {
106             for (int i = 0 ; i < str_len ; ++i) {
107                 *dest = ascii_tolower(str[i]);
108                 ++dest;
109             }
110         }
111     }
112     *dest = '\0';
113 }
114
115
116 static trie_t *strlist_create(const char *file, bool reverse, bool lock)
117 {
118     trie_t *db;
119     file_map_t map;
120     const char *p, *end;
121     char line[BUFSIZ];
122     uint32_t count = 0;
123
124     if (!file_map_open(&map, file, false)) {
125         return NULL;
126     }
127     p   = map.map;
128     end = map.end;
129     while (end > p && end[-1] != '\n') {
130         --end;
131     }
132     if (end != map.end) {
133         warn("file %s miss a final \\n, ignoring last line",
134              file);
135     }
136
137     db = trie_new();
138     while (p < end && p != NULL) {
139         const char *eol = (char *)memchr(p, '\n', end - p);
140         if (eol == NULL) {
141             eol = end;
142         }
143         if (eol - p >= BUFSIZ) {
144             err("unreasonnable long line");
145             file_map_close(&map);
146             trie_delete(&db);
147             return NULL;
148         }
149         if (*p != '#') {
150             const char *eos = eol;
151             while (p < eos && isspace(*p)) {
152                 ++p;
153             }
154             while (p < eos && isspace(eos[-1])) {
155                 --eos;
156             }
157             if (p < eos) {
158                 strlist_copy(line, p, eos - p, reverse);
159                 trie_insert(db, line);
160                 ++count;
161             }
162         }
163         p = eol + 1;
164     }
165     file_map_close(&map);
166     trie_compile(db, lock);
167     info("%s loaded, %u entries", file, count);
168     return db;
169 }
170
171 static bool strlist_create_from_rhbl(const char *file, bool lock,
172                                      trie_t **phosts, trie_t **pdomains)
173 {
174     trie_t *hosts, *domains;
175     uint32_t host_count, domain_count;
176     file_map_t map;
177     const char *p, *end;
178     char line[BUFSIZ];
179
180     if (!file_map_open(&map, file, false)) {
181         return false;
182     }
183     p   = map.map;
184     end = map.end;
185     while (end > p && end[-1] != '\n') {
186         --end;
187     }
188     if (end != map.end) {
189         warn("file %s miss a final \\n, ignoring last line",
190              file);
191     }
192
193     hosts = trie_new();
194     host_count = 0;
195     domains = trie_new();
196     domain_count = 0;
197     while (p < end && p != NULL) {
198         const char *eol = (char *)memchr(p, '\n', end - p);
199         if (eol == NULL) {
200             eol = end;
201         }
202         if (eol - p >= BUFSIZ) {
203             err("unreasonnable long line");
204             file_map_close(&map);
205             trie_delete(&hosts);
206             trie_delete(&domains);
207             return false;
208         }
209         if (*p != '#') {
210             const char *eos = eol;
211             while (p < eos && isspace(*p)) {
212                 ++p;
213             }
214             while (p < eos && isspace(eos[-1])) {
215                 --eos;
216             }
217             if (p < eos) {
218                 if (isalnum(*p)) {
219                     strlist_copy(line, p, eos - p, true);
220                     trie_insert(hosts, line);
221                     ++host_count;
222                 } else if (*p == '*') {
223                     ++p;
224                     strlist_copy(line, p, eos - p, true);
225                     trie_insert(domains, line);
226                     ++domain_count;
227                 }
228             }
229         }
230         p = eol + 1;
231     }
232     file_map_close(&map);
233     if (host_count > 0) {
234         trie_compile(hosts, lock);
235         *phosts = hosts;
236     } else {
237         trie_delete(&hosts);
238         *phosts = NULL;
239     }
240     if (domain_count > 0) {
241         trie_compile(domains, lock);
242         *pdomains = domains;
243     } else {
244         trie_delete(&domains);
245         *pdomains = NULL;
246     }
247     info("rhbl %s loaded, %u hosts, %u domains", file, host_count, domain_count);
248     return hosts != NULL || domains != NULL;
249
250 }
251
252
253 static bool strlist_filter_constructor(filter_t *filter)
254 {
255     strlist_config_t *config = strlist_config_new();
256
257 #define PARSE_CHECK(Expr, Str, ...)                                            \
258     if (!(Expr)) {                                                             \
259         err(Str, ##__VA_ARGS__);                                               \
260         strlist_config_delete(&config);                                        \
261         return false;                                                          \
262     }
263
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
270            *  valid options are:
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).
279            */
280           case ATK_FILE: {
281             bool lock = false;
282             int  weight = 0;
283             bool reverse = false;
284             bool partial = false;
285             trie_t *trie = NULL;
286             const char *current = param->value;
287             const char *p = m_strchrnul(param->value, ':');
288             char *next = NULL;
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");
293                 switch (i) {
294                   case 0:
295                     if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
296                         lock = true;
297                     } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
298                         lock = false;
299                     } else {
300                         PARSE_CHECK(false, "illegal locking state %.*s",
301                                     (int)(p - current), current);
302                     }
303                     break;
304
305                   case 1:
306                     if (p - current > (ssize_t)strlen("partial-") 
307                         && strncmp(current, "partial-", strlen("partial-")) == 0) {
308                         partial = true;
309                         current += strlen("partial-");
310                     }
311                     if ((p - current) == 6 && strncmp(current, "suffix", 6) == 0) {
312                         reverse = true;
313                     } else if ((p - current) == 6 && strncmp(current, "prefix", 6) == 0) {
314                         reverse = false;
315                     } else {
316                         PARSE_CHECK(false, "illegal character order value %.*s",
317                                     (int)(p - current), current);
318                     }
319                     break;
320
321                   case 2:
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);
326                     break;
327
328                   case 3:
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);
336                     break;
337                 }
338                 if (i != 3) {
339                     current = p + 1;
340                     p = m_strchrnul(current, ':');
341                 }
342             }
343           } break;
344
345           /* rbldns parameter is:
346            *  [no]lock::weight:filename
347            *  valid options are:
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.
353            */
354           case ATK_RBLDNS: {
355             bool lock = false;
356             int  weight = 0;
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, ':');
361             char *next = NULL;
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");
366                 switch (i) {
367                   case 0:
368                     if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
369                         lock = true;
370                     } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
371                         lock = false;
372                     } else {
373                         PARSE_CHECK(false, "illegal locking state %.*s",
374                                     (int)(p - current), current);
375                     }
376                     break;
377
378                   case 1:
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);
383                     break;
384
385                   case 2:
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);
394                     }
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);
400                     }
401                     config->is_hostname = true;
402                     break;
403                 }
404                 if (i != 2) {
405                     current = p + 1;
406                     p = m_strchrnul(current, ':');
407                 }
408             }
409           } break;
410
411           /* dns parameter.
412            *  weight:hostname.
413            * define a RBL to use through DNS resolution.
414            */
415           case ATK_DNS: {
416             int  weight = 0;
417             const char *current = param->value;
418             const char *p = m_strchrnul(param->value, ':');
419             char *next = NULL;
420             for (int i = 0 ; i < 2 ; ++i) {
421                 PARSE_CHECK(i == 1 || *p,
422                             "host parameter must contains a weight option");
423                 switch (i) {
424                   case 0:
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);
429                     break;
430
431                   case 1:
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);
435                     break;
436                 }
437                 if (i != 1) {
438                     current = p + 1;
439                     p = m_strchrnul(current, ':');
440                 }
441             }
442           } break;
443
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.
448            * default is 1;
449            */
450           FILTER_PARAM_PARSE_INT(HARD_THRESHOLD, config->hard_threshold);
451
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"
455            *  is called.
456            * default is 1;
457            */
458           FILTER_PARAM_PARSE_INT(SOFT_THRESHOLD, config->soft_threshold);
459
460           /* fields to match againes:
461            *  fields = field_name(,field_name)*
462            *  field_names are
463            *    - hostname: helo_name,client_name,reverse_client_name
464            *    - email: sender,recipient
465            */
466           case ATK_FIELDS: {
467             const char *current = param->value;
468             const char *p = m_strchrnul(param->value, ',');
469             do {
470                 postlicyd_token tok = policy_tokenize(current, p - current);
471                 switch (tok) {
472 #define           CASE(Up, Low, Type)                                          \
473                   case PTK_ ## Up:                                             \
474                     config->match_ ## Low = true;                              \
475                     config->is_ ## Type = true;                                \
476                     break
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);
484 #undef CASE
485                   default:
486                     PARSE_CHECK(false, "unknown field %.*s", (int)(p - current), current);
487                     break;
488                 }
489                 if (!*p) {
490                     break;
491                 }
492                 current = p + 1;
493                 p = m_strchrnul(current, ',');
494             } while (true);
495           } break;
496
497           default: break;
498         }
499     }}
500
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;
506     return true;
507 }
508
509 static void strlist_filter_destructor(filter_t *filter)
510 {
511     strlist_config_t *config = filter->data;
512     strlist_config_delete(&config);
513     filter->data = config;
514 }
515
516 static void strlist_filter_async(rbl_result_t *result, void *arg)
517 {
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];
522
523     if (*result != RBL_ERROR) {
524         async->error = false;
525     }
526     --async->awaited;
527
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);
530
531     if (async->awaited == 0) {
532         filter_result_t res = HTK_FAIL;
533         if (async->error) {
534             res = HTK_ERROR;
535         } else {
536             uint32_t j = 0;
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);                 \
541                                                                                \
542                 switch (array_elt(async->results, j)) {                        \
543                   case RBL_ASYNC:                                              \
544                     crit("no more awaited answer but result is ASYNC");        \
545                     abort();                                                   \
546                   case RBL_FOUND:                                              \
547                     async->sum += weight;                                      \
548                     break;                                                     \
549                   default:                                                     \
550                     break;                                                     \
551                 }                                                              \
552                 ++j;                                                           \
553             }                                                                  \
554         }
555             DO_SUM(helo);
556             DO_SUM(client);
557             DO_SUM(reverse);
558             DO_SUM(recipient);
559             DO_SUM(sender);
560 #undef DO_SUM
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;
566             }
567         }
568         debug("answering to filter %s", filter->name);
569         filter_post_async_result(context, res);
570     }
571 }
572
573
574 static filter_result_t strlist_filter(const filter_t *filter, const query_t *query,
575                                       filter_context_t *context)
576 {
577     char reverse[BUFSIZ];
578     char normal[BUFSIZ];
579     const strlist_config_t *config = filter->data;
580     strlist_async_data_t *async = context->contexts[filter_type];
581     int result_pos = 0;
582     async->sum = 0;
583     async->error = true;
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));
588     async->awaited = 0;
589
590
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");
596         return HTK_ABORT;
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");
599         return HTK_ABORT;
600     }
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;                                     \
616                 }                                                              \
617             }                                                                  \
618             async->error = false;                                              \
619         }                                                                      \
620     }
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),     \
629                   normal, rbl);                                                \
630             if (rhbl_check(rbl, normal, array_ptr(async->results, result_pos), \
631                            strlist_filter_async, context)) {                   \
632                 async->error = false;                                          \
633                 ++async->awaited;                                              \
634             }                                                                  \
635             ++result_pos;                                                      \
636         }                                                                      \
637     }
638
639     if (config->is_email) {
640         LOOKUP(sender, sender);
641         LOOKUP(recipient, recipient);
642         DNS(sender, sender);
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);
655     }
656 #undef  DNS
657 #undef  LOOKUP
658     if (async->awaited > 0) {
659         return HTK_ASYNC;
660     }
661     if (async->error) {
662         err("filter %s: all the rbls returned an error", filter->name);
663         return HTK_ERROR;
664     }
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;
669     } else {
670         return HTK_FAIL;
671     }
672 }
673
674 static void *strlist_context_constructor(void)
675 {
676     return p_new(strlist_async_data_t, 1);
677 }
678
679 static void strlist_context_destructor(void *data)
680 {
681     strlist_async_data_t *ctx = data;
682     array_wipe(ctx->results);
683     p_delete(&ctx);
684 }
685
686 static int strlist_init(void)
687 {
688     filter_type =  filter_register("strlist", strlist_filter_constructor,
689                                    strlist_filter_destructor, strlist_filter,
690                                    strlist_context_constructor,
691                                    strlist_context_destructor);
692     /* Hooks.
693      */
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");
699
700     /* Parameters.
701      */
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");
708     return 0;
709 }
710 module_init(strlist_init);