/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" #include "ioloop.h" #include "array.h" #include "hash.h" #include "llist.h" #include "mail-host.h" #include "director.h" /* n% of timeout_secs */ #define USER_NEAR_EXPIRING_PERCENTAGE 10 /* but min/max. of this many secs */ #define USER_NEAR_EXPIRING_MIN 3 #define USER_NEAR_EXPIRING_MAX 30 /* This shouldn't matter what it is exactly, just try it sometimes later. */ #define USER_BEING_KILLED_EXPIRE_RETRY_SECS 60 struct user_directory_iter { struct user_directory *dir; struct user *pos, *stop_after_tail; }; struct user_directory { struct director *director; /* unsigned int username_hash => user */ HASH_TABLE(void *, struct user *) hash; /* sorted by time. may be unsorted while handshakes are going on. */ struct user *head, *tail; ARRAY(struct user_directory_iter *) iters; user_free_hook_t *user_free_hook; unsigned int timeout_secs; /* If user's expire time is less than this many seconds away, don't assume that other directors haven't yet expired it */ unsigned int user_near_expiring_secs; struct timeout *to_expire; time_t to_expire_timestamp; bool sort_pending; }; static void user_move_iters(struct user_directory *dir, struct user *user) { struct user_directory_iter *iter; array_foreach_elem(&dir->iters, iter) { if (iter->pos == user) iter->pos = user->next; if (iter->stop_after_tail == user) { iter->stop_after_tail = user->prev != NULL ? user->prev : user->next; } } } static void user_free(struct user_directory *dir, struct user *user) { i_assert(user->host->user_count > 0); user->host->user_count--; if (dir->user_free_hook != NULL) dir->user_free_hook(user); user_move_iters(dir, user); hash_table_remove(dir->hash, POINTER_CAST(user->username_hash)); DLLIST2_REMOVE(&dir->head, &dir->tail, user); i_free(user); } static bool user_directory_user_has_connections(struct user_directory *dir, struct user *user, time_t *expire_timestamp_r) { time_t expire_timestamp = user->timestamp + dir->timeout_secs; if (expire_timestamp > ioloop_time) { *expire_timestamp_r = expire_timestamp; return TRUE; } if (USER_IS_BEING_KILLED(user)) { /* don't free this user until the kill is finished */ *expire_timestamp_r = ioloop_time + USER_BEING_KILLED_EXPIRE_RETRY_SECS; return TRUE; } if (user->weak) { if (expire_timestamp + USER_NEAR_EXPIRING_MAX > ioloop_time) { *expire_timestamp_r = expire_timestamp + USER_NEAR_EXPIRING_MAX; return TRUE; } e_warning(dir->director->event, "User %u weakness appears to be stuck, removing it", user->username_hash); } return FALSE; } static void user_directory_drop_expired(struct user_directory *dir) { time_t expire_timestamp = 0; while (dir->head != NULL && !user_directory_user_has_connections(dir, dir->head, &expire_timestamp)) { user_free(dir, dir->head); expire_timestamp = 0; } i_assert(expire_timestamp > ioloop_time || expire_timestamp == 0); if (expire_timestamp != dir->to_expire_timestamp) { timeout_remove(&dir->to_expire); if (expire_timestamp != 0) { struct timeval tv = { .tv_sec = expire_timestamp }; dir->to_expire_timestamp = tv.tv_sec; dir->to_expire = timeout_add_absolute(&tv, user_directory_drop_expired, dir); } } } unsigned int user_directory_count(struct user_directory *dir) { return hash_table_count(dir->hash); } struct user *user_directory_lookup(struct user_directory *dir, unsigned int username_hash) { struct user *user; time_t expire_timestamp; user_directory_drop_expired(dir); user = hash_table_lookup(dir->hash, POINTER_CAST(username_hash)); if (user != NULL && !user_directory_user_has_connections(dir, user, &expire_timestamp)) { user_free(dir, user); user = NULL; } return user; } struct user * user_directory_add(struct user_directory *dir, unsigned int username_hash, struct mail_host *host, time_t timestamp) { struct user *user; /* make sure we don't add timestamps higher than ioloop time */ if (timestamp > ioloop_time) timestamp = ioloop_time; user = i_new(struct user, 1); user->username_hash = username_hash; user->host = host; user->host->user_count++; user->timestamp = timestamp; DLLIST2_APPEND(&dir->head, &dir->tail, user); if (dir->to_expire == NULL) { struct timeval tv = { .tv_sec = ioloop_time + dir->timeout_secs }; dir->to_expire_timestamp = tv.tv_sec; dir->to_expire = timeout_add_absolute(&tv, user_directory_drop_expired, dir); } hash_table_insert(dir->hash, POINTER_CAST(user->username_hash), user); return user; } void user_directory_refresh(struct user_directory *dir, struct user *user) { user_move_iters(dir, user); user->timestamp = ioloop_time; DLLIST2_REMOVE(&dir->head, &dir->tail, user); DLLIST2_APPEND(&dir->head, &dir->tail, user); } void user_directory_remove_host(struct user_directory *dir, struct mail_host *host) { struct user *user, *next; for (user = dir->head; user != NULL; user = next) { next = user->next; if (user->host == host) user_free(dir, user); } } static int user_timestamp_cmp(struct user *const *user1, struct user *const *user2) { if ((*user1)->timestamp < (*user2)->timestamp) return -1; if ((*user1)->timestamp > (*user2)->timestamp) return 1; return 0; } void user_directory_sort(struct user_directory *dir) { ARRAY(struct user *) users; struct user *user; unsigned int i, users_count = hash_table_count(dir->hash); dir->sort_pending = FALSE; if (users_count == 0) { i_assert(dir->head == NULL); return; } if (array_count(&dir->iters) > 0) { /* We can't sort the directory while there are iterators or they'll skip users. Do the sort after there are no more iterators. */ dir->sort_pending = TRUE; return; } /* place all users into array and sort it */ i_array_init(&users, users_count); user = dir->head; for (i = 0; i < users_count; i++, user = user->next) array_push_back(&users, &user); i_assert(user == NULL); array_sort(&users, user_timestamp_cmp); /* recreate the linked list */ dir->head = dir->tail = NULL; array_foreach_elem(&users, user) DLLIST2_APPEND(&dir->head, &dir->tail, user); i_assert(dir->head != NULL && dir->head->timestamp <= dir->tail->timestamp); array_free(&users); } bool user_directory_user_is_recently_updated(struct user_directory *dir, struct user *user) { return (time_t)(user->timestamp + dir->timeout_secs/2) >= ioloop_time; } bool user_directory_user_is_near_expiring(struct user_directory *dir, struct user *user) { time_t expire_timestamp; expire_timestamp = user->timestamp + (dir->timeout_secs - dir->user_near_expiring_secs); return expire_timestamp < ioloop_time; } struct user_directory * user_directory_init(struct director *director, unsigned int timeout_secs, user_free_hook_t *user_free_hook) { struct user_directory *dir; i_assert(timeout_secs > USER_NEAR_EXPIRING_MIN); dir = i_new(struct user_directory, 1); dir->director = director; dir->timeout_secs = timeout_secs; dir->user_near_expiring_secs = timeout_secs * USER_NEAR_EXPIRING_PERCENTAGE / 100; dir->user_near_expiring_secs = I_MIN(dir->user_near_expiring_secs, USER_NEAR_EXPIRING_MAX); dir->user_near_expiring_secs = I_MAX(dir->user_near_expiring_secs, USER_NEAR_EXPIRING_MIN); i_assert(dir->timeout_secs/2 > dir->user_near_expiring_secs); dir->user_free_hook = user_free_hook; hash_table_create_direct(&dir->hash, default_pool, 0); i_array_init(&dir->iters, 8); return dir; } void user_directory_deinit(struct user_directory **_dir) { struct user_directory *dir = *_dir; *_dir = NULL; i_assert(array_count(&dir->iters) == 0); while (dir->head != NULL) user_free(dir, dir->head); timeout_remove(&dir->to_expire); hash_table_destroy(&dir->hash); array_free(&dir->iters); i_free(dir); } struct user_directory_iter * user_directory_iter_init(struct user_directory *dir, bool iter_until_current_tail) { struct user_directory_iter *iter; iter = i_new(struct user_directory_iter, 1); iter->dir = dir; iter->pos = dir->head; iter->stop_after_tail = iter_until_current_tail ? dir->tail : NULL; array_push_back(&dir->iters, &iter); user_directory_drop_expired(dir); return iter; } struct user *user_directory_iter_next(struct user_directory_iter *iter) { struct user *user; user = iter->pos; if (user == NULL) return NULL; iter->pos = user->next; if (user == iter->stop_after_tail) { /* this is the last user we want to iterate */ iter->pos = NULL; } return user; } void user_directory_iter_deinit(struct user_directory_iter **_iter) { struct user_directory_iter *iter = *_iter; struct user_directory_iter *const *iters; unsigned int i, count; *_iter = NULL; iters = array_get(&iter->dir->iters, &count); for (i = 0; i < count; i++) { if (iters[i] == iter) { array_delete(&iter->dir->iters, i, 1); break; } } if (array_count(&iter->dir->iters) == 0 && iter->dir->sort_pending) user_directory_sort(iter->dir); i_free(iter); }