Use resources for the greylister too.
[apps/pfixtools.git] / postlicyd / greylist.c
index e556883..a7fedc6 100644 (file)
 #include <tcbdb.h>
 
 #include "common.h"
-#include "greylist.h"
 #include "str.h"
-
-struct greylist_cfg greylist_cfg = {
-   .lookup_by_host = false,
-   .delay          = 300,
-   .retry_window   = 2 * 24 * 2600,
-   .client_awl     = 5,
-};
-static TCBDB *awl_db, *obj_db;
+#include "resources.h"
+
+
+typedef struct greylist_config_t {
+    unsigned lookup_by_host : 1;
+    unsigned no_sender      : 1;
+    unsigned no_recipient   : 1;
+    int delay;
+    int retry_window;
+    int client_awl;
+    int max_age;
+    int cleanup_period;
+
+    char  *awlfilename;
+    TCBDB **awl_db;
+    char  *objfilename;
+    TCBDB **obj_db;
+} greylist_config_t;
+
+#define GREYLIST_INIT { .lookup_by_host = false,       \
+                        .no_sender = false,            \
+                        .no_recipient = false,         \
+                        .delay = 300,                  \
+                        .retry_window = 2 * 24 * 3600, \
+                        .client_awl = 5,               \
+                        .max_age = 35 * 3600,          \
+                        .cleanup_period = 86400,       \
+                        .awlfilename = NULL,           \
+                        .awl_db = NULL,                \
+                        .objfilename = NULL,           \
+                        .obj_db = NULL }
 
 struct awl_entry {
     int32_t count;
@@ -57,51 +79,222 @@ struct obj_entry {
     time_t last;
 };
 
-int greylist_initialize(const char *directory, const char *prefix)
+typedef struct greylist_resource_t {
+    TCBDB *db;
+} greylist_resource_t;
+
+
+static void greylist_resource_wipe(greylist_resource_t *res)
 {
-    char path[PATH_MAX];
+    if (res->db) {
+        tcbdbsync(res->db);
+        tcbdbdel(res->db);
+    }
+    p_delete(&res);
+}
 
-    if (greylist_cfg.client_awl) {
-        snprintf(path, sizeof(path), "%s/%swhitelist.db", directory, prefix);
+static inline bool greylist_check_awlentry(const greylist_config_t *config,
+                                           struct awl_entry *aent, time_t now)
+{
+    return !(config->max_age > 0 && now - aent->last > config->max_age);
+}
+
+static inline bool greylist_check_object(const greylist_config_t *config,
+                                         const struct obj_entry *oent, time_t now)
+{
+    return !((config->max_age > 0 && now - oent->last > config->max_age)
+             || (oent->last - oent->first < config->delay
+                 && now - oent->last > config->retry_window));
+}
+
+typedef bool (*db_entry_checker_t)(const greylist_config_t *, const void *, time_t);
+
+static inline bool greylist_db_need_cleanup(const greylist_config_t *config, TCBDB *db)
+{
+    int len = 0;
+    time_t now = time(NULL);
+    const time_t *last_cleanup = tcbdbget3(db, "@@cleanup@@", strlen("@@cleanup@@"), &len);
+    if (last_cleanup == NULL) {
+        debug("No last cleanup time");
+    } else {
+        debug("Last cleanup time %u, (ie %us ago)",
+              (uint32_t)*last_cleanup, (uint32_t)(now - *last_cleanup));
+    }
+    return last_cleanup == NULL
+        || len != sizeof(*last_cleanup)
+        || (now - *last_cleanup) >= config->cleanup_period;
+}
+
+static TCBDB **greylist_db_get(const greylist_config_t *config, const char *path,
+                              size_t entry_len, db_entry_checker_t check)
+{
+    TCBDB *awl_db, *tmp_db;
+    time_t now = time(NULL);
+
+    greylist_resource_t *res = resource_get("greylist", path);
+    if (res == NULL) {
+        res = p_new(greylist_resource_t, 1);
+        resource_set("greylist", path, res, (resource_destructor_t)greylist_resource_wipe);
+    }
+
+    /* Open the database and check if cleanup is needed
+     */
+    awl_db = res->db;
+    res->db = NULL;
+    if (awl_db == NULL) {
         awl_db = tcbdbnew();
         if (!tcbdbopen(awl_db, path, BDBOWRITER | BDBOCREAT)) {
+            err("can not open database: %s", tcbdberrmsg(tcbdbecode(awl_db)));
             tcbdbdel(awl_db);
-            awl_db = NULL;
+            resource_release("greylist", path);
+            return NULL;
+        }
+    }
+    if (!greylist_db_need_cleanup(config, awl_db) || config->max_age <= 0) {
+        info("no cleanup needed");
+        res->db = awl_db;
+        return &res->db;
+    } else {
+        tcbdbsync(awl_db);
+        tcbdbdel(awl_db);
+    }
+
+    /* Rebuild a new database after removing too old entries.
+     */
+    if (config->max_age > 0) {
+        uint32_t old_count = 0;
+        uint32_t new_count = 0;
+        bool replace = false;
+        bool trashable = false;
+        char tmppath[PATH_MAX];
+        snprintf(tmppath, PATH_MAX, "%s.tmp", path);
+
+        awl_db = tcbdbnew();
+        if (tcbdbopen(awl_db, path, BDBOREADER)) {
+            tmp_db = tcbdbnew();
+            if (tcbdbopen(tmp_db, tmppath, BDBOWRITER | BDBOCREAT | BDBOTRUNC)) {
+                BDBCUR *cur = tcbdbcurnew(awl_db);
+                TCXSTR *key, *value;
+
+                key = tcxstrnew();
+                value = tcxstrnew();
+                if (tcbdbcurfirst(cur)) {
+                    replace = true;
+                    do {
+                        tcxstrclear(key);
+                        tcxstrclear(value);
+                        (void)tcbdbcurrec(cur, key, value);
+
+                        if ((size_t)tcxstrsize(value) == entry_len
+                            && check(config, tcxstrptr(value), now)) {
+                            tcbdbput(tmp_db, tcxstrptr(key), tcxstrsize(key),
+                                     tcxstrptr(value), entry_len);
+                            ++new_count;
+                        }
+                        ++old_count;
+                    } while (tcbdbcurnext(cur));
+                    now = time(0);
+                    tcbdbput(tmp_db, "@@cleanup@@", strlen("@@cleanup@@"), &now, sizeof(now));
+                }
+                tcxstrdel(key);
+                tcxstrdel(value);
+                tcbdbcurdel(cur);
+                tcbdbsync(tmp_db);
+            } else {
+                warn("cannot run database cleanup: can't open destination database: %s",
+                     tcbdberrmsg(tcbdbecode(awl_db)));
+            }
+            tcbdbdel(tmp_db);
+        } else {
+            int ecode = tcbdbecode(awl_db);
+            warn("can not open database: %s", tcbdberrmsg(ecode));
+            trashable = ecode != TCENOPERM && ecode != TCEOPEN && ecode != TCENOFILE && ecode != TCESUCCESS;
+        }
+        tcbdbdel(awl_db);
+
+        /** Cleanup successful, replace the old database with the new one.
+         */
+        if (trashable) {
+            info("database cleanup finished: database was corrupted, create a new one");
+            unlink(path);
+        } else if (replace) {
+            info("database cleanup finished: before %u entries, after %d entries",
+                   old_count, new_count);
+            unlink(path);
+            if (rename(tmppath, path) != 0) {
+                UNIXERR("rename");
+                resource_release("greylist", path);
+                return NULL;
+            }
+        } else {
+            unlink(tmppath);
+            info("database cleanup finished: nothing to do, %u entries", new_count);
+        }
+    }
+
+    /* Effectively open the database.
+     */
+    res->db = NULL;
+    awl_db = tcbdbnew();
+    if (!tcbdbopen(awl_db, path, BDBOWRITER | BDBOCREAT)) {
+        err("can not open database: %s", tcbdberrmsg(tcbdbecode(awl_db)));
+        tcbdbdel(awl_db);
+        resource_release("greylist", path);
+        return NULL;
+    }
+
+    res->db = awl_db;
+    return &res->db;
+}
+
+
+static bool greylist_initialize(greylist_config_t *config,
+                                const char *directory, const char *prefix)
+{
+    char path[PATH_MAX];
+
+    if (config->client_awl) {
+        snprintf(path, sizeof(path), "%s/%swhitelist.db", directory, prefix);
+        info("loading auto-whitelist database");
+        config->awl_db = greylist_db_get(config, path,
+                                         sizeof(struct awl_entry),
+                                         (db_entry_checker_t)(greylist_check_awlentry));
+        if (config->awl_db == NULL) {
+            return false;
         }
-        return -1;
+        config->awlfilename = m_strdup(path);
     }
 
     snprintf(path, sizeof(path), "%s/%sgreylist.db", directory, prefix);
-    obj_db = tcbdbnew();
-    if (!tcbdbopen(obj_db, path, BDBOWRITER | BDBOCREAT)) {
-        tcbdbdel(obj_db);
-        obj_db = NULL;
-        if (awl_db) {
-            tcbdbdel(awl_db);
-            awl_db = NULL;
+    info("loading greylist database");
+    config->obj_db = greylist_db_get(config, path,
+                                     sizeof(struct obj_entry),
+                                     (db_entry_checker_t)(greylist_check_object));
+    if (config->obj_db == NULL) {
+        if (config->awlfilename) {
+            resource_release("greylist", config->awlfilename);
+            p_delete(&config->awlfilename);
         }
-        return -1;
+        return false;
     }
+    config->objfilename = m_strdup(path);
 
-    return 0;
+    return true;
 }
 
-static void greylist_shutdown(void)
+static void greylist_shutdown(greylist_config_t *config)
 {
-    if (awl_db) {
-        tcbdbsync(awl_db);
-        tcbdbdel(awl_db);
-        awl_db = NULL;
+    if (config->awlfilename) {
+        resource_release("greylist", config->awlfilename);
+        p_delete(&config->awlfilename);
     }
-    if (obj_db) {
-        tcbdbsync(obj_db);
-        tcbdbdel(obj_db);
-        obj_db = NULL;
+    if (config->objfilename) {
+        resource_release("greylist", config->objfilename);
+        p_delete(&config->objfilename);
     }
 }
-module_exit(greylist_shutdown);
 
-const char *sender_normalize(const char *sender, char *buf, int len)
+static const char *sender_normalize(const char *sender, char *buf, int len)
 {
     const char *at = strchr(sender, '@');
     int rpos = 0, wpos = 0, userlen;
@@ -136,13 +329,14 @@ const char *sender_normalize(const char *sender, char *buf, int len)
     return buf;
 }
 
-static const char *
-c_net(const char *c_addr, const char *c_name, char *cnet, int cnetlen)
+static const char *c_net(const greylist_config_t *config,
+                         const char *c_addr, const char *c_name,
+                         char *cnet, int cnetlen)
 {
     char ip2[4], ip3[4];
     const char *dot, *p;
 
-    if (greylist_cfg.lookup_by_host)
+    if (config->lookup_by_host)
         return c_addr;
 
     if (!(dot = strchr(c_addr, '.')))
@@ -169,12 +363,16 @@ c_net(const char *c_addr, const char *c_name, char *cnet, int cnetlen)
     return cnet;
 }
 
-bool try_greylist(const char *sender, const char *c_addr,
-                  const char *c_name, const char *rcpt)
+
+static bool try_greylist(const greylist_config_t *config,
+                         const char *sender, const char *c_addr,
+                         const char *c_name, const char *rcpt)
 {
 #define INCR_AWL                                              \
     aent.count++;                                             \
     aent.last = now;                                          \
+    debug("whitelist entry for %.*s updated, count %d",       \
+          c_addrlen, c_addr, aent.count);                     \
     tcbdbput(awl_db, c_addr, c_addrlen, &aent, sizeof(aent));
 
     char sbuf[BUFSIZ], cnet[64], key[BUFSIZ];
@@ -185,18 +383,30 @@ bool try_greylist(const char *sender, const char *c_addr,
     struct awl_entry aent = { 0, 0 };
 
     int len, klen, c_addrlen = strlen(c_addr);
+    TCBDB * const awl_db = config->awl_db ? *(config->awl_db) : NULL;
+    TCBDB * const obj_db = config->obj_db ? *(config->obj_db) : NULL;
 
     /* Auto whitelist clients.
      */
-    if (greylist_cfg.client_awl) {
+    if (config->client_awl) {
         res = tcbdbget3(awl_db, c_addr, c_addrlen, &len);
         if (res && len == sizeof(aent)) {
             memcpy(&aent, res, len);
+            debug("client %.*s has a whitelist entry, count is %d",
+                  c_addrlen, c_addr, aent.count);
+        }
+
+        if (!greylist_check_awlentry(config, &aent, now)) {
+            aent.count = 0;
+            aent.last  = 0;
+            debug("client %.*s whitelist entry too old",
+                  c_addrlen, c_addr);
         }
 
         /* Whitelist if count is enough.
          */
-        if (aent.count > greylist_cfg.client_awl) {
+        if (aent.count >= config->client_awl) {
+            debug("client %.*s whitelisted", c_addrlen, c_addr);
             if (now < aent.last + 3600) {
                 INCR_AWL
             }
@@ -210,21 +420,27 @@ bool try_greylist(const char *sender, const char *c_addr,
     /* Lookup.
      */
     klen = snprintf(key, sizeof(key), "%s/%s/%s",
-                    c_net(c_addr, c_name, cnet, sizeof(cnet)),
-                    sender_normalize(sender, sbuf, sizeof(sbuf)), rcpt);
+                    c_net(config, c_addr, c_name, cnet, sizeof(cnet)),
+                    config->no_sender ? "" : sender_normalize(sender, sbuf, sizeof(sbuf)),
+                    config->no_recipient ? "" : rcpt);
     klen = MIN(klen, ssizeof(key) - 1);
 
     res = tcbdbget3(obj_db, key, klen, &len);
     if (res && len == sizeof(oent)) {
         memcpy(&oent, res, len);
+        debug("found a greylist entry for %.*s", klen, key);
     }
 
     /* Discard stored first-seen if it is the first retrial and
-     * it is beyong the retry window.
+     * it is beyong the retry window and too old entries.
      */
-    if (oent.last - oent.first < greylist_cfg.delay
-        &&  now - oent.first > greylist_cfg.retry_window) {
+    if (!greylist_check_object(config, &oent, now)) {
         oent.first = now;
+        debug("invalid retry for %.*s: %s", klen, key,
+              (config->max_age > 0 && now - oent.last > config->max_age) ?
+                  "too old entry"
+                : (oent.last - oent.first < config->delay ?
+                  "retry too early" : "retry too late" ));
     }
 
     /* Update.
@@ -239,8 +455,9 @@ bool try_greylist(const char *sender, const char *c_addr,
      *                                       -> withelist if count > limit
      *        - client whitelisted already ? -> update last-seen timestamp.
      */
-    if (oent.first + greylist_cfg.delay < now) {
-        if (greylist_cfg.client_awl) {
+    if (oent.first + config->delay < now) {
+        debug("valid retry for %.*s", klen, key);
+        if (config->client_awl) {
             INCR_AWL
         }
 
@@ -253,3 +470,114 @@ bool try_greylist(const char *sender, const char *c_addr,
      */
     return false;
 }
+
+
+/* postlicyd filter declaration */
+
+#include "filter.h"
+
+static greylist_config_t *greylist_config_new(void)
+{
+    const greylist_config_t g = GREYLIST_INIT;
+    greylist_config_t *config = p_new(greylist_config_t, 1);
+    *config = g;
+    return config;
+}
+
+static void greylist_config_delete(greylist_config_t **config)
+{
+    if (*config) {
+        greylist_shutdown(*config);
+        p_delete(config);
+    }
+}
+
+static bool greylist_filter_constructor(filter_t *filter)
+{
+    const char* path   = NULL;
+    const char* prefix = NULL;
+    greylist_config_t *config = greylist_config_new();
+
+#define PARSE_CHECK(Expr, Str, ...)                                            \
+    if (!(Expr)) {                                                             \
+        err(Str, ##__VA_ARGS__);                                               \
+        greylist_config_delete(&config);                                       \
+        return false;                                                          \
+    }
+
+    foreach (filter_param_t *param, filter->params) {
+        switch (param->type) {
+          FILTER_PARAM_PARSE_STRING(PATH,   path);
+          FILTER_PARAM_PARSE_STRING(PREFIX, prefix);
+          FILTER_PARAM_PARSE_BOOLEAN(LOOKUP_BY_HOST, config->lookup_by_host);
+          FILTER_PARAM_PARSE_BOOLEAN(NO_SENDER, config->no_sender);
+          FILTER_PARAM_PARSE_BOOLEAN(NO_RECIPIENT, config->no_recipient);
+          FILTER_PARAM_PARSE_INT(RETRY_WINDOW, config->retry_window);
+          FILTER_PARAM_PARSE_INT(CLIENT_AWL,   config->client_awl);
+          FILTER_PARAM_PARSE_INT(DELAY,        config->delay);
+          FILTER_PARAM_PARSE_INT(MAX_AGE,      config->max_age);
+
+          default: break;
+        }
+    }}
+
+    PARSE_CHECK(path, "path to greylist db not given");
+    PARSE_CHECK(greylist_initialize(config, path, prefix ? prefix : ""),
+                "can not load greylist database");
+
+    filter->data = config;
+    return true;
+}
+
+static void greylist_filter_destructor(filter_t *filter)
+{
+    greylist_config_t *data = filter->data;
+    greylist_config_delete(&data);
+    filter->data = data;
+}
+
+static filter_result_t greylist_filter(const filter_t *filter,
+                                       const query_t *query,
+                                       filter_context_t *context)
+{
+    const greylist_config_t *config = filter->data;
+    if (!config->no_recipient && query->state != SMTP_RCPT) {
+        warn("greylisting on recipient only works as smtpd_recipient_restrictions");
+        return HTK_ABORT;
+    }
+    if (!config->no_sender && query->state < SMTP_MAIL) {
+        warn("greylisting on sender must be performed after (or at) MAIL TO");
+        return HTK_ABORT;
+    }
+
+    return try_greylist(config, query->sender, query->client_address,
+                        query->client_name, query->recipient) ?
+           HTK_WHITELIST : HTK_GREYLIST;
+}
+
+static int greylist_init(void)
+{
+    filter_type_t type =  filter_register("greylist", greylist_filter_constructor,
+                                          greylist_filter_destructor,
+                                          greylist_filter, NULL, NULL);
+    /* Hooks.
+     */
+    (void)filter_hook_register(type, "abort");
+    (void)filter_hook_register(type, "error");
+    (void)filter_hook_register(type, "greylist");
+    (void)filter_hook_register(type, "whitelist");
+
+    /* Parameters.
+     */
+    (void)filter_param_register(type, "lookup_by_host");
+    (void)filter_param_register(type, "no_sender");
+    (void)filter_param_register(type, "no_recipient");
+    (void)filter_param_register(type, "delay");
+    (void)filter_param_register(type, "retry_window");
+    (void)filter_param_register(type, "client_awl");
+    (void)filter_param_register(type, "max_age");
+    (void)filter_param_register(type, "path");
+    (void)filter_param_register(type, "prefix");
+    return 0;
+}
+module_init(greylist_init)