]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
anvil: Add support for tracking alternative usernames
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Wed, 15 Dec 2021 23:58:35 +0000 (01:58 +0200)
committerTimo Sirainen <timo.sirainen@open-xchange.com>
Tue, 8 Feb 2022 09:48:24 +0000 (10:48 +0100)
src/anvil/anvil-connection.c
src/anvil/connect-limit.c
src/anvil/connect-limit.h
src/anvil/test-connect-limit.c

index ae01034f819bf0ad5c8fb9827976f1a7e12446b5..92b14b8cb21f896ae4ef1c1dd0f63e0238de222e 100644 (file)
@@ -121,8 +121,13 @@ anvil_connection_request(struct anvil_connection *conn,
                        }
                        args++;
                }
+               const char *const *alt_usernames = NULL;
+               if (args[0] != NULL) {
+                       alt_usernames = t_strsplit_tabescaped(args[0]);
+                       args++;
+               }
                connect_limit_connect(connect_limit, pid, &key,
-                                     conn_guid, kick_type);
+                                     conn_guid, kick_type, alt_usernames);
        } else if (strcmp(cmd, "DISCONNECT") == 0) {
                if (args[0] == NULL || args[1] == NULL) {
                        *error_r = "DISCONNECT: Not enough parameters";
index 8464782f337a0822ac3ff2a27bd9ea613e8fcb28..1b05e0feba21c873dcee05889442081ff663caef 100644 (file)
@@ -4,6 +4,7 @@
 #include "array.h"
 #include "hash.h"
 #include "llist.h"
+#include "sort.h"
 #include "str.h"
 #include "str-table.h"
 #include "strescape.h"
@@ -23,6 +24,18 @@ struct userip {
        struct ip_addr ip;
 };
 
+struct session_alt_username {
+       /* alt_username_hash sessions linked list */
+       struct session_alt_username *prev, *next;
+
+       /* points to alt_username_hash keys */
+       const char *alt_username;
+       /* session where this alt_username belongs to */
+       struct session *session;
+};
+HASH_TABLE_DEFINE_TYPE(session_alt_username, char *,
+                      struct session_alt_username *);
+
 struct session {
        /* process->sessions linked list */
        struct session *process_prev, *process_next;
@@ -33,6 +46,17 @@ struct session {
        struct userip *userip;
        struct process *process;
        guid_128_t conn_guid;
+
+       /* Fields in the same order as connect_limit.alt_username_fields.
+          Note that these may be session-specific, which is why they're not in
+          struct user. */
+       unsigned int alt_usernames_count;
+       struct session_alt_username *alt_usernames;
+};
+
+struct alt_username_field {
+       char *name;
+       unsigned int refcount;
 };
 
 struct connect_limit {
@@ -48,6 +72,14 @@ struct connect_limit {
        HASH_TABLE(const uint8_t *, struct session *) session_hash;
        /* pid_t => struct process */
        HASH_TABLE(void *, struct process *) process_hash;
+
+       /* Array of alt username fields. Note that if there are refcount=0
+          fields they may be reused for other usernames later on, but there
+          are never any name=NULL fields. */
+       ARRAY(struct alt_username_field) alt_username_fields;
+       /* alt_username => struct session linked list. This array is resized
+          every time a new alt_username_field index is added. */
+       HASH_TABLE_TYPE(session_alt_username) *alt_username_hashes;
 };
 
 struct connect_limit_iter {
@@ -80,6 +112,7 @@ struct connect_limit *connect_limit_init(void)
 
        limit = i_new(struct connect_limit, 1);
        limit->strings = str_table_init();
+       i_array_init(&limit->alt_username_fields, 8);
        hash_table_create(&limit->user_hash, default_pool, 0,
                          str_hash, strcmp);
        hash_table_create(&limit->userip_hash, default_pool, 0,
@@ -93,12 +126,22 @@ struct connect_limit *connect_limit_init(void)
 void connect_limit_deinit(struct connect_limit **_limit)
 {
        struct connect_limit *limit = *_limit;
+       struct alt_username_field *alt_fields;
+       unsigned int i, count;
 
        *_limit = NULL;
        hash_table_destroy(&limit->user_hash);
        hash_table_destroy(&limit->userip_hash);
        hash_table_destroy(&limit->session_hash);
        hash_table_destroy(&limit->process_hash);
+
+       alt_fields = array_get_modifiable(&limit->alt_username_fields, &count);
+       for (i = 0; i < count; i++) {
+               hash_table_destroy(&limit->alt_username_hashes[i]);
+               i_free(alt_fields[i].name);
+       }
+       i_free(limit->alt_username_hashes);
+       array_free(&limit->alt_username_fields);
        str_table_deinit(&limit->strings);
        i_free(limit);
 }
@@ -159,10 +202,155 @@ session_unlink_process(struct connect_limit *limit, struct session *session)
        }
 }
 
+static bool
+alt_username_field_find(struct connect_limit *limit, const char *name,
+                       unsigned int *idx_r)
+{
+       struct alt_username_field *fields;
+       unsigned int i, count, first_empty_idx = UINT_MAX;
+
+       fields = array_get_modifiable(&limit->alt_username_fields, &count);
+       for (i = 0; i < count; i++) {
+               if (strcmp(fields[i].name, name) == 0) {
+                       *idx_r = i;
+                       return TRUE;
+               }
+               if (fields[i].refcount == 0 && first_empty_idx == UINT_MAX)
+                       first_empty_idx = i;
+       }
+       *idx_r = first_empty_idx;
+       return FALSE;
+}
+
+static unsigned int
+alt_username_field_ref(struct connect_limit *limit, const char *name)
+{
+       struct alt_username_field *field;
+       unsigned int idx;
+
+       if (!alt_username_field_find(limit, name, &idx)) {
+               /* Field wasn't found, but an existing field with refcount=0
+                  may have been reused. */
+               unsigned int old_count =
+                       array_count(&limit->alt_username_fields);
+               if (idx == UINT_MAX)
+                       idx = old_count;
+               field = array_idx_get_space(&limit->alt_username_fields, idx);
+               i_free(field->name);
+               field->name = i_strdup(name);
+
+               limit->alt_username_hashes =
+                       i_realloc(limit->alt_username_hashes,
+                                 sizeof(limit->alt_username_hashes[0]) *
+                                 old_count,
+                                 sizeof(limit->alt_username_hashes[0]) *
+                                 I_MAX((idx+1), old_count));
+               hash_table_create(&limit->alt_username_hashes[idx],
+                                 default_pool, 0, str_hash, strcmp);
+       } else {
+               field = array_idx_modifiable(&limit->alt_username_fields, idx);
+       }
+       field->refcount++;
+       return idx;
+}
+
+static void
+alt_username_field_unref(struct connect_limit *limit, unsigned int alt_idx)
+{
+       struct alt_username_field *field;
+
+       field = array_idx_modifiable(&limit->alt_username_fields, alt_idx);
+       i_assert(field->refcount > 0);
+       field->refcount--;
+}
+
+static void
+alt_username_value_link(struct connect_limit *limit,
+                       struct session_alt_username *alt,
+                       unsigned int alt_idx, const char *alt_username)
+{
+       struct session_alt_username *first_alt;
+       char *orig_key;
+
+       if (!hash_table_lookup_full(limit->alt_username_hashes[alt_idx],
+                                   alt_username, &orig_key, &first_alt)) {
+               orig_key = i_strdup(alt_username);
+               alt->alt_username = orig_key;
+               hash_table_insert(limit->alt_username_hashes[alt_idx],
+                                 orig_key, alt);
+       } else {
+               alt->alt_username = orig_key;
+               DLLIST_PREPEND(&first_alt, alt);
+               hash_table_update(limit->alt_username_hashes[alt_idx],
+                                 orig_key, first_alt);
+       }
+}
+
+static void
+session_set_alt_usernames(struct connect_limit *limit, struct session *session,
+                         const char *const *alt_usernames)
+{
+       unsigned int count = str_array_length(alt_usernames)/2;
+       if (count == 0)
+               return;
+
+       unsigned int max_alt_idx = 0;
+       unsigned int *alt_indexes = t_new(unsigned int, count);
+       for (unsigned int i = 0; i < count; i++) {
+               alt_indexes[i] = alt_username_field_ref(limit, alt_usernames[i*2]);
+               max_alt_idx = I_MAX(max_alt_idx, alt_indexes[i]);
+       }
+
+       session->alt_usernames_count = max_alt_idx + 1;
+       session->alt_usernames =
+               i_new(struct session_alt_username,
+                     session->alt_usernames_count);
+       for (unsigned int i = 0; i < count; i++) {
+               unsigned int alt_idx = alt_indexes[i];
+               i_assert(session->alt_usernames[alt_idx].session == NULL);
+               session->alt_usernames[alt_idx].session = session;
+               alt_username_value_link(limit, &session->alt_usernames[alt_idx],
+                                       alt_idx, alt_usernames[i*2 + 1]);
+       }
+}
+
+static void
+session_unset_alt_usernames(struct connect_limit *limit,
+                           struct session *session)
+{
+       struct session_alt_username *first_alt, *alt;
+       char *orig_key;
+       unsigned int alt_idx;
+
+       for (alt_idx = 0; alt_idx < session->alt_usernames_count; alt_idx++) {
+               alt = &session->alt_usernames[alt_idx];
+               if (alt->session == NULL)
+                       continue;
+
+               if (!hash_table_lookup_full(limit->alt_username_hashes[alt_idx],
+                                           alt->alt_username,
+                                           &orig_key, &first_alt))
+                       i_panic("connect limit hash tables are inconsistent");
+               bool hash_update = (first_alt == alt);
+               DLLIST_REMOVE(&first_alt, alt);
+               if (first_alt == NULL) {
+                       hash_table_remove(limit->alt_username_hashes[alt_idx],
+                                         orig_key);
+                       i_free(orig_key);
+               } else if (hash_update) {
+                       hash_table_update(limit->alt_username_hashes[alt_idx],
+                                         orig_key, first_alt);
+               }
+               alt_username_field_unref(limit, alt_idx);
+       }
+       i_free(session->alt_usernames);
+}
+
 void connect_limit_connect(struct connect_limit *limit, pid_t pid,
                           const struct connect_limit_key *key,
                           const guid_128_t conn_guid,
-                          enum kick_type kick_type)
+                          enum kick_type kick_type,
+                          const char *const *alt_usernames)
 {
        struct session *session, *first_user_session;
        struct userip *userip;
@@ -210,6 +398,9 @@ void connect_limit_connect(struct connect_limit *limit, pid_t pid,
        session = i_new(struct session, 1);
        session->userip = userip;
        guid_128_copy(session->conn_guid, conn_guid);
+       T_BEGIN {
+               session_set_alt_usernames(limit, session, alt_usernames);
+       } T_END;
 
        session_link_process(limit, session, pid, kick_type);
        const uint8_t *conn_guid_p = session->conn_guid;
@@ -256,6 +447,8 @@ session_free(struct connect_limit *limit, struct session *session)
                hash_table_update(limit->user_hash, orig_username,
                                  first_user_session);
        }
+       session_unset_alt_usernames(limit, session);
+       i_free(session->alt_usernames);
        i_free(session);
 }
 
@@ -351,11 +544,10 @@ connect_limit_iter_result_cmp(const struct connect_limit_iter_result *result1,
        return guid_128_cmp(result1->conn_guid, result2->conn_guid);
 }
 
-struct connect_limit_iter *
-connect_limit_iter_begin(struct connect_limit *limit, const char *username)
+static struct connect_limit_iter *
+connect_limit_iter_init_common(struct connect_limit *limit)
 {
        struct connect_limit_iter *iter;
-       struct session *session;
 
        i_assert(limit->iter == NULL);
 
@@ -363,6 +555,17 @@ connect_limit_iter_begin(struct connect_limit *limit, const char *username)
        iter->limit = limit;
        i_array_init(&iter->results, 32);
 
+       limit->iter = iter;
+       return iter;
+}
+
+struct connect_limit_iter *
+connect_limit_iter_begin(struct connect_limit *limit, const char *username)
+{
+       struct connect_limit_iter *iter;
+       struct session *session;
+
+       iter = connect_limit_iter_init_common(limit);
        session = hash_table_lookup(limit->user_hash, username);
        while (session != NULL) {
                struct connect_limit_iter_result *result =
@@ -370,12 +573,44 @@ connect_limit_iter_begin(struct connect_limit *limit, const char *username)
                result->kick_type = session->process->kick_type;
                result->pid = session->process->pid;
                result->service = session->userip->service;
+               result->username = session->userip->username;
                guid_128_copy(result->conn_guid, session->conn_guid);
                session = session->user_next;
        }
        array_sort(&iter->results, connect_limit_iter_result_cmp);
+       return iter;
+}
 
-       limit->iter = iter;
+struct connect_limit_iter *
+connect_limit_iter_begin_alt_username(struct connect_limit *limit,
+                                     const char *alt_username_field,
+                                     const char *alt_username,
+                                     const struct ip_addr *except_ip)
+{
+       struct connect_limit_iter *iter;
+       struct session_alt_username *alt;
+       unsigned int alt_idx;
+
+       iter = connect_limit_iter_init_common(limit);
+       if (!alt_username_field_find(limit, alt_username_field, &alt_idx))
+               return iter;
+
+       alt = hash_table_lookup(limit->alt_username_hashes[alt_idx],
+                               alt_username);
+       while (alt != NULL) {
+               if (except_ip == NULL ||
+                   !net_ip_compare(&alt->session->userip->ip, except_ip)) {
+                       struct connect_limit_iter_result *result =
+                               array_append_space(&iter->results);
+                       result->kick_type = alt->session->process->kick_type;
+                       result->pid = alt->session->process->pid;
+                       result->service = alt->session->userip->service;
+                       guid_128_copy(result->conn_guid, alt->session->conn_guid);
+                       result->username = alt->session->userip->username;
+               }
+               alt = alt->next;
+       }
+       array_sort(&iter->results, connect_limit_iter_result_cmp);
        return iter;
 }
 
index 30b5971a38f7d9c77368671909f4d8565a20d53e..f0eccb514ff25c26d83998468837cf3539c656d4 100644 (file)
@@ -27,6 +27,7 @@ struct connect_limit_iter_result {
        enum kick_type kick_type;
        pid_t pid;
        const char *service;
+       const char *username;
        guid_128_t conn_guid;
 };
 
@@ -39,7 +40,8 @@ connect_limit_lookup(struct connect_limit *limit,
 void connect_limit_connect(struct connect_limit *limit, pid_t pid,
                           const struct connect_limit_key *key,
                           const guid_128_t conn_guid,
-                          enum kick_type kick_type);
+                          enum kick_type kick_type,
+                          const char *const *alt_usernames);
 void connect_limit_disconnect(struct connect_limit *limit, pid_t pid,
                              const struct connect_limit_key *key,
                              const guid_128_t conn_guid);
@@ -50,6 +52,11 @@ void connect_limit_dump(struct connect_limit *limit, struct ostream *output);
    modified while the iterator exists. The results are sorted by pid. */
 struct connect_limit_iter *
 connect_limit_iter_begin(struct connect_limit *limit, const char *username);
+struct connect_limit_iter *
+connect_limit_iter_begin_alt_username(struct connect_limit *limit,
+                                     const char *alt_username_field,
+                                     const char *alt_username,
+                                     const struct ip_addr *except_ip);
 bool connect_limit_iter_next(struct connect_limit_iter *iter,
                             struct connect_limit_iter_result *result_r);
 void connect_limit_iter_deinit(struct connect_limit_iter **iter);
index 01e7d71faf3a5eb4041a25031c10d1c0fc380666..af42eb1465b46290bf7bde31dcd4aa72129a0dc4 100644 (file)
@@ -66,8 +66,14 @@ static void test_connect_limit(void)
                .username = "user1",
                .service = "service1",
        };
+       const char *const alt_usernames1[] = {
+               "altkey1", "altvalueA",
+               "altkey2", "altvalueB",
+               NULL
+       };
        test_assert(net_addr2ip("1.2.3.4", &key.ip) == 0);
-       connect_limit_connect(limit, 501, &key, session1_guid, KICK_TYPE_NONE);
+       connect_limit_connect(limit, 501, &key, session1_guid, KICK_TYPE_NONE,
+                             alt_usernames1);
 #define TEST_SESSION1_STR "501\tuser1\tservice1\t1.2.3.4\t"SESSION1_HEX"\n"
        test_session_dump(limit, TEST_SESSION1_STR);
        test_assert(connect_limit_lookup(limit, &key) == 1);
@@ -77,8 +83,15 @@ static void test_connect_limit(void)
                .username = "user1",
                .service = "service1",
        };
+       const char *const alt_usernames2[] = {
+               "altkey1", "altvalueA",
+               "altkey2", "altvalueC",
+               "altkey3", "altvalueA",
+               NULL
+       };
        test_assert(net_addr2ip("1.2.3.4", &key2.ip) == 0);
-       connect_limit_connect(limit, 501, &key2, session2_guid, KICK_TYPE_NONE);
+       connect_limit_connect(limit, 501, &key2, session2_guid, KICK_TYPE_NONE,
+                             alt_usernames2);
 #define TEST_SESSION2_STR "501\tuser1\tservice1\t1.2.3.4\t"SESSION2_HEX"\n"
        test_session_dump(limit, TEST_SESSION1_STR TEST_SESSION2_STR);
        test_assert(connect_limit_lookup(limit, &key) == 2);
@@ -88,8 +101,15 @@ static void test_connect_limit(void)
                .username = "user2",
                .service = "service2",
        };
+       const char *const alt_usernames3[] = {
+               "altkey1", "altvalueA",
+               "altkey2", "altvalueC",
+               "altkey4", "altvalueD",
+               NULL
+       };
        test_assert(net_addr2ip("4.3.2.1", &key3.ip) == 0);
-       connect_limit_connect(limit, 600, &key3, session3_guid, KICK_TYPE_SIGNAL);
+       connect_limit_connect(limit, 600, &key3, session3_guid, KICK_TYPE_SIGNAL,
+                             alt_usernames3);
 #define TEST_SESSION3_STR "600\tuser2\tservice2\t4.3.2.1\t"SESSION3_HEX"\n"
        test_session_dump(limit, TEST_SESSION1_STR TEST_SESSION2_STR TEST_SESSION3_STR);
        test_assert(connect_limit_lookup(limit, &key) == 2);
@@ -102,7 +122,8 @@ static void test_connect_limit(void)
        };
        test_assert(net_addr2ip("4.3.2.1", &key4.ip) == 0);
        test_expect_error_string("connect limit: connection for duplicate connection GUID "SESSION2_HEX" (pid=501 -> 600, user=user1 -> user3, service=service1 -> service3, ip=1.2.3.4 -> 4.3.2.1)");
-       connect_limit_connect(limit, 600, &key4, session2_guid, KICK_TYPE_SIGNAL);
+       connect_limit_connect(limit, 600, &key4, session2_guid, KICK_TYPE_SIGNAL,
+                             alt_usernames3);
        test_expect_no_more_errors();
        test_session_dump(limit, TEST_SESSION1_STR TEST_SESSION2_STR TEST_SESSION3_STR);
 
@@ -138,6 +159,27 @@ static void test_connect_limit(void)
        test_assert(!connect_limit_iter_next(iter, &iter_result));
        connect_limit_iter_deinit(&iter);
 
+       /* test alt username iteration */
+       iter = connect_limit_iter_begin_alt_username(limit,
+                       "altkey1", "altvalueA", NULL);
+       test_assert(connect_limit_iter_next(iter, &iter_result));
+       test_assert(iter_result.pid == 501 &&
+                   strcmp(iter_result.service, "service1") == 0 &&
+                   guid_128_cmp(iter_result.conn_guid, session1_guid) == 0 &&
+                   iter_result.kick_type == KICK_TYPE_NONE);
+       test_assert(connect_limit_iter_next(iter, &iter_result));
+       test_assert(iter_result.pid == 501 &&
+                   strcmp(iter_result.service, "service1") == 0 &&
+                   guid_128_cmp(iter_result.conn_guid, session2_guid) == 0 &&
+                   iter_result.kick_type == KICK_TYPE_NONE);
+       test_assert(connect_limit_iter_next(iter, &iter_result));
+       test_assert(iter_result.pid == 600 &&
+                   strcmp(iter_result.service, "service2") == 0 &&
+                   guid_128_cmp(iter_result.conn_guid, session3_guid) == 0 &&
+                   iter_result.kick_type == KICK_TYPE_SIGNAL);
+       test_assert(!connect_limit_iter_next(iter, &iter_result));
+       connect_limit_iter_deinit(&iter);
+
        /* disconnect a single session */
        connect_limit_disconnect(limit, 600, &key3, session3_guid);
        test_session_dump(limit, TEST_SESSION1_STR TEST_SESSION2_STR);